diff --git a/package-lock.json b/package-lock.json index 8e05c719..a5f61d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "tdesign-web-components", - "version": "1.2.10", + "version": "1.3.1-alpha.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tdesign-web-components", - "version": "1.2.10", + "version": "1.3.1-alpha.11", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.7", "@popperjs/core": "^2.11.8", - "cherry-markdown": "0.11.0-alpha-2", + "cherry-markdown": "^0.11.0-alpha-5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "copy-to-clipboard": "^3.3.3", @@ -1891,13 +1891,6 @@ "node": ">=6.9.0" } }, - "node_modules/@braintree/sanitize-url": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", - "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", - "license": "MIT", - "optional": true - }, "node_modules/@commitlint/cli": { "version": "19.3.0", "dev": true, @@ -2455,8 +2448,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", - "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -2472,6 +2463,8 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.18.20", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -3358,8 +3351,6 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "license": "MIT", "engines": { "node": ">= 10" @@ -3403,8 +3394,6 @@ }, "node_modules/@types/codemirror": { "version": "0.0.108", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz", - "integrity": "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==", "license": "MIT", "dependencies": { "@types/tern": "*" @@ -3504,8 +3493,6 @@ }, "node_modules/@types/tern": { "version": "0.23.9", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", - "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", "license": "MIT", "dependencies": { "@types/estree": "*" @@ -3753,9 +3740,6 @@ }, "node_modules/abab": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", "license": "BSD-3-Clause" }, "node_modules/accepts": { @@ -3783,8 +3767,6 @@ }, "node_modules/acorn-globals": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "license": "MIT", "dependencies": { "acorn": "^7.1.1", @@ -3793,8 +3775,6 @@ }, "node_modules/acorn-globals/node_modules/acorn": { "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3805,8 +3785,6 @@ }, "node_modules/acorn-globals/node_modules/acorn-walk": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3830,8 +3808,6 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { "debug": "4" @@ -4048,8 +4024,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/at-least-node": { @@ -4267,8 +4241,6 @@ }, "node_modules/browser-process-hrtime": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "license": "BSD-2-Clause" }, "node_modules/browserslist": { @@ -4423,7 +4395,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", + "version": "1.0.30001625", "dev": true, "funding": [ { @@ -4462,9 +4434,9 @@ "license": "MIT" }, "node_modules/cherry-markdown": { - "version": "0.11.0-alpha-2", - "resolved": "https://mirrors.tencent.com/npm/cherry-markdown/-/cherry-markdown-0.11.0-alpha-2.tgz", - "integrity": "sha512-yx2jFkr8nFZbfB27Y3EWan8E0xdRnLvKes1LUuTJeY/lu9NYKxjFJN/w88QhV1DvyOxBBBcI2rLPb9oRjKOIDQ==", + "version": "0.11.0-alpha-5", + "resolved": "https://mirrors.tencent.com/npm/cherry-markdown/-/cherry-markdown-0.11.0-alpha-5.tgz", + "integrity": "sha512-k+gFcg0MJUseONICrdtBR/pzAwSVjIbFvVriSDxoU2pXJxHF/kuw2s2MgBvzW8VYAajJVGVP/kam9nLuIwsMRQ==", "license": "Apache-2.0", "dependencies": { "@types/codemirror": "^0.0.108", @@ -4478,6 +4450,48 @@ "mermaid": "9.4.3" } }, + "node_modules/cherry-markdown/node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "license": "MIT", + "optional": true + }, + "node_modules/cherry-markdown/node_modules/dagre-d3-es": { + "version": "7.0.9", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/cherry-markdown/node_modules/mermaid": { + "version": "9.4.3", + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/cherry-markdown/node_modules/mermaid/node_modules/dompurify": { + "version": "2.4.3", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, "node_modules/chokidar": { "version": "3.5.3", "dev": true, @@ -4733,8 +4747,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5143,8 +5155,6 @@ }, "node_modules/cose-base": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", "license": "MIT", "optional": true, "dependencies": { @@ -5228,8 +5238,6 @@ }, "node_modules/crypto-js": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, "node_modules/crypto-random-string": { @@ -5449,14 +5457,10 @@ }, "node_modules/cssom": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "license": "MIT" }, "node_modules/cssstyle": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "license": "MIT", "dependencies": { "cssom": "~0.3.6" @@ -5467,8 +5471,6 @@ }, "node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, "node_modules/csstype": { @@ -5478,8 +5480,6 @@ }, "node_modules/cytoscape-cose-bilkent": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "license": "MIT", "optional": true, "dependencies": { @@ -5491,8 +5491,6 @@ }, "node_modules/cytoscape-fcose": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "license": "MIT", "optional": true, "dependencies": { @@ -5504,8 +5502,6 @@ }, "node_modules/cytoscape-fcose/node_modules/cose-base": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "license": "MIT", "optional": true, "dependencies": { @@ -5514,8 +5510,6 @@ }, "node_modules/cytoscape-fcose/node_modules/layout-base": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", "license": "MIT", "optional": true }, @@ -5604,8 +5598,6 @@ }, "node_modules/d3": { "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "license": "ISC", "optional": true, "dependencies": { @@ -5646,8 +5638,6 @@ }, "node_modules/d3-array": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "optional": true, "dependencies": { @@ -5659,8 +5649,6 @@ }, "node_modules/d3-axis": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "license": "ISC", "optional": true, "engines": { @@ -5669,8 +5657,6 @@ }, "node_modules/d3-brush": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "license": "ISC", "optional": true, "dependencies": { @@ -5686,8 +5672,6 @@ }, "node_modules/d3-chord": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "license": "ISC", "optional": true, "dependencies": { @@ -5699,8 +5683,6 @@ }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "optional": true, "engines": { @@ -5709,8 +5691,6 @@ }, "node_modules/d3-contour": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "license": "ISC", "optional": true, "dependencies": { @@ -5722,8 +5702,6 @@ }, "node_modules/d3-delaunay": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "license": "ISC", "optional": true, "dependencies": { @@ -5735,8 +5713,6 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "optional": true, "engines": { @@ -5745,8 +5721,6 @@ }, "node_modules/d3-drag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "optional": true, "dependencies": { @@ -5759,8 +5733,6 @@ }, "node_modules/d3-dsv": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "license": "ISC", "optional": true, "dependencies": { @@ -5785,8 +5757,6 @@ }, "node_modules/d3-dsv/node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "optional": true, "engines": { @@ -5795,8 +5765,6 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "optional": true, "engines": { @@ -5805,8 +5773,6 @@ }, "node_modules/d3-fetch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "license": "ISC", "optional": true, "dependencies": { @@ -5818,8 +5784,6 @@ }, "node_modules/d3-force": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "license": "ISC", "optional": true, "dependencies": { @@ -5832,9 +5796,7 @@ } }, "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "version": "3.1.0", "license": "ISC", "optional": true, "engines": { @@ -5843,8 +5805,6 @@ }, "node_modules/d3-geo": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "license": "ISC", "optional": true, "dependencies": { @@ -5856,8 +5816,6 @@ }, "node_modules/d3-hierarchy": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "license": "ISC", "optional": true, "engines": { @@ -5866,8 +5824,6 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "optional": true, "dependencies": { @@ -5879,8 +5835,6 @@ }, "node_modules/d3-path": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "optional": true, "engines": { @@ -5889,8 +5843,6 @@ }, "node_modules/d3-polygon": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "license": "ISC", "optional": true, "engines": { @@ -5899,8 +5851,6 @@ }, "node_modules/d3-quadtree": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "license": "ISC", "optional": true, "engines": { @@ -5909,8 +5859,6 @@ }, "node_modules/d3-random": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", "optional": true, "engines": { @@ -5919,8 +5867,6 @@ }, "node_modules/d3-scale": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "optional": true, "dependencies": { @@ -5936,8 +5882,6 @@ }, "node_modules/d3-scale-chromatic": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "license": "ISC", "optional": true, "dependencies": { @@ -5950,8 +5894,6 @@ }, "node_modules/d3-shape": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "optional": true, "dependencies": { @@ -5963,8 +5905,6 @@ }, "node_modules/d3-time": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "optional": true, "dependencies": { @@ -5976,8 +5916,6 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "optional": true, "dependencies": { @@ -5989,8 +5927,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "optional": true, "engines": { @@ -5999,8 +5935,6 @@ }, "node_modules/d3-transition": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "optional": true, "dependencies": { @@ -6019,8 +5953,6 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "optional": true, "dependencies": { @@ -6034,17 +5966,6 @@ "node": ">=12" } }, - "node_modules/dagre-d3-es": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", - "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", - "license": "MIT", - "optional": true, - "dependencies": { - "d3": "^7.8.2", - "lodash-es": "^4.17.21" - } - }, "node_modules/dargs": { "version": "8.1.0", "dev": true, @@ -6058,8 +5979,6 @@ }, "node_modules/data-urls": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "license": "MIT", "dependencies": { "abab": "^2.0.6", @@ -6072,8 +5991,6 @@ }, "node_modules/data-urls/node_modules/tr46": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "license": "MIT", "dependencies": { "punycode": "^2.1.1" @@ -6084,8 +6001,6 @@ }, "node_modules/data-urls/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -6093,8 +6008,6 @@ }, "node_modules/data-urls/node_modules/whatwg-url": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "license": "MIT", "dependencies": { "tr46": "^3.0.0", @@ -6181,8 +6094,6 @@ }, "node_modules/decimal.js": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, "node_modules/decode-uri-component": { @@ -6264,8 +6175,6 @@ }, "node_modules/delaunator": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "license": "ISC", "optional": true, "dependencies": { @@ -6274,8 +6183,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6402,9 +6309,6 @@ }, "node_modules/domexception": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", "license": "MIT", "dependencies": { "webidl-conversions": "^7.0.0" @@ -6415,8 +6319,6 @@ }, "node_modules/domexception/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -6437,9 +6339,7 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.2.6", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6525,8 +6425,6 @@ }, "node_modules/elkjs": { "version": "0.8.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", - "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", "license": "EPL-2.0", "optional": true }, @@ -6775,8 +6673,6 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -7494,9 +7390,7 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.4", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -8040,8 +7934,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "license": "MIT", "dependencies": { "whatwg-encoding": "^2.0.0" @@ -8051,9 +7943,7 @@ } }, "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "version": "10.0.0", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -8065,14 +7955,12 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" + "domutils": "^3.2.1", + "entities": "^6.0.0" } }, "node_modules/htmlparser2/node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -8085,8 +7973,6 @@ }, "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -8097,8 +7983,6 @@ }, "node_modules/htmlparser2/node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -8112,8 +7996,6 @@ }, "node_modules/htmlparser2/node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -8125,9 +8007,7 @@ } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "6.0.1", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -8153,8 +8033,6 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "license": "MIT", "dependencies": { "@tootallnate/once": "2", @@ -8167,8 +8045,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { "agent-base": "6", @@ -8535,8 +8411,6 @@ }, "node_modules/internmap": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "optional": true, "engines": { @@ -8788,8 +8662,6 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, "node_modules/is-reference": { @@ -9035,8 +8907,6 @@ }, "node_modules/jsdom": { "version": "19.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", - "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", "license": "MIT", "dependencies": { "abab": "^2.0.5", @@ -9081,8 +8951,6 @@ }, "node_modules/jsdom/node_modules/tr46": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "license": "MIT", "dependencies": { "punycode": "^2.1.1" @@ -9093,8 +8961,6 @@ }, "node_modules/jsdom/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -9102,8 +8968,6 @@ }, "node_modules/jsdom/node_modules/whatwg-url": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", - "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", "license": "MIT", "dependencies": { "tr46": "^3.0.0", @@ -9217,8 +9081,6 @@ }, "node_modules/khroma": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", "optional": true }, "node_modules/kind-of": { @@ -9231,8 +9093,6 @@ }, "node_modules/layout-base": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "license": "MIT", "optional": true }, @@ -9903,38 +9763,6 @@ "node": ">= 8" } }, - "node_modules/mermaid": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", - "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@braintree/sanitize-url": "^6.0.0", - "cytoscape": "^3.23.0", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.1.0", - "d3": "^7.4.0", - "dagre-d3-es": "7.0.9", - "dayjs": "^1.11.7", - "dompurify": "2.4.3", - "elkjs": "^0.8.2", - "khroma": "^2.0.0", - "lodash-es": "^4.17.21", - "non-layered-tidy-tree-layout": "^2.0.2", - "stylis": "^4.1.2", - "ts-dedent": "^2.2.0", - "uuid": "^9.0.0", - "web-worker": "^1.2.0" - } - }, - "node_modules/mermaid/node_modules/dompurify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", - "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true - }, "node_modules/methods": { "version": "1.1.2", "dev": true, @@ -10127,8 +9955,6 @@ }, "node_modules/non-layered-tidy-tree-layout": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", - "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", "license": "MIT", "optional": true }, @@ -10380,9 +10206,7 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.21", "license": "MIT" }, "node_modules/object-assign": { @@ -10495,9 +10319,8 @@ }, "node_modules/omi": { "version": "7.7.13", - "resolved": "https://registry.npmjs.org/omi/-/omi-7.7.13.tgz", + "resolved": "https://mirrors.tencent.com/npm/omi/-/omi-7.7.13.tgz", "integrity": "sha512-LA/rLhGE7PO/OVN9Mp4TG4YL3Dt5dERM9fsVyiPmE75CRCuyW7vVfrYK8Ee7EPbQuTycYRsfbrD1YBQytOY/jw==", - "license": "MIT", "dependencies": { "construct-style-sheets-polyfill": "3.0.1", "reactive-signal": "^2.0.1", @@ -10515,7 +10338,7 @@ }, "node_modules/omi-suspense": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/omi-suspense/-/omi-suspense-0.1.6.tgz", + "resolved": "https://mirrors.tencent.com/npm/omi-suspense/-/omi-suspense-0.1.6.tgz", "integrity": "sha512-WrtbsWQ2+EbgRDmqqWP6D77dRjaHibzxPgPPAEbJ8z/vIun64w3d7gkX5acfFfd07ZxeCsNUCPrurGOYWrheuw==", "dev": true, "dependencies": { @@ -10803,8 +10626,6 @@ }, "node_modules/parse5": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "license": "MIT" }, "node_modules/parseurl": { @@ -11595,8 +11416,6 @@ }, "node_modules/psl": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -11670,8 +11489,6 @@ }, "node_modules/querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, "node_modules/queue-microtask": { @@ -11912,8 +11729,6 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, "node_modules/resolve": { @@ -12095,8 +11910,6 @@ }, "node_modules/robust-predicates": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense", "optional": true }, @@ -12664,8 +12477,6 @@ }, "node_modules/rw": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause", "optional": true }, @@ -12763,8 +12574,6 @@ }, "node_modules/saxes": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -13405,8 +13214,6 @@ }, "node_modules/stylis": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT", "optional": true }, @@ -13526,8 +13333,6 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, "node_modules/tailwind-merge": { @@ -13867,8 +13672,6 @@ }, "node_modules/tough-cookie": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -13882,8 +13685,6 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -13910,8 +13711,6 @@ }, "node_modules/ts-dedent": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", "license": "MIT", "optional": true, "engines": { @@ -14268,8 +14067,6 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "license": "MIT", "dependencies": { "querystringify": "^2.1.1", @@ -14301,8 +14098,6 @@ }, "node_modules/uuid": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -14510,9 +14305,6 @@ }, "node_modules/w3c-hr-time": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", "license": "MIT", "dependencies": { "browser-process-hrtime": "^1.0.0" @@ -14520,8 +14312,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", - "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", "license": "MIT", "dependencies": { "xml-name-validator": "^4.0.0" @@ -14547,8 +14337,6 @@ }, "node_modules/web-worker": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", - "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", "license": "Apache-2.0", "optional": true }, @@ -14559,9 +14347,6 @@ }, "node_modules/whatwg-encoding": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -14572,8 +14357,6 @@ }, "node_modules/whatwg-mimetype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "license": "MIT", "engines": { "node": ">=12" @@ -15036,9 +14819,7 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.18.3", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -15058,8 +14839,6 @@ }, "node_modules/xml-name-validator": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "license": "Apache-2.0", "engines": { "node": ">=12" @@ -15067,8 +14846,6 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, "node_modules/y18n": { diff --git a/package.json b/package.json index 26f0a19b..3026d4c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tdesign-web-components", "purename": "tdesign", - "version": "1.2.10", + "version": "1.3.1-alpha.11", "type": "module", "main": "lib/index.js", "module": "lib/index.js", @@ -13,7 +13,8 @@ "site": "cd site && vite build", "site:preview": "cd site && vite build --mode preview && cd ../_site && cp index.html 404.html", "prebuild": "rimraf es/* lib/* dist/* esm/* cjs/*", - "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && npm run build:tsc", + "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && npm run build:tsc && npm run copy:fonts", + "copy:fonts": "mkdir -p lib/assets && cp node_modules/cherry-markdown/dist/fonts/ch-icon.* lib/assets/", "build:analyze": "cross-env NODE_ENV=production ANALYZE=true rollup -c script/rollup.config.js && npm run build:tsc", "build:analyze:all": "cross-env NODE_ENV=production ANALYZE=true ANALYZE_MODE=all rollup -c script/rollup.config.js && npm run build:tsc", "build:analyze:umd": "cross-env NODE_ENV=production ANALYZE=true ANALYZE_MODE=umd rollup -c script/rollup.config.js && npm run build:tsc", @@ -73,7 +74,7 @@ "dependencies": { "@babel/runtime": "^7.24.7", "@popperjs/core": "^2.11.8", - "cherry-markdown": "0.11.0-alpha-2", + "cherry-markdown": "^0.11.0-alpha-5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "copy-to-clipboard": "^3.3.3", diff --git a/script/rollup.config.js b/script/rollup.config.js index 3c25dddd..b38b0923 100644 --- a/script/rollup.config.js +++ b/script/rollup.config.js @@ -94,16 +94,42 @@ const getAnalyzePlugins = (buildType = 'umd') => { }; const input = 'src/index-lib.ts'; + +// todo: 这里先按白名单:只打包 chat 相关组件 const inputList = [ - 'src/**/*.ts', - 'src/**/*.jsx', - 'src/**/*.tsx', + // chat 开头的组件 + 'src/chat-*/**/*.ts', + 'src/chat-*/**/*.tsx', + // 额外需要的组件 + 'src/chatbot/**/*.ts', + 'src/chatbot/**/*.tsx', + 'src/attachments/**/*.ts', + 'src/attachments/**/*.tsx', + 'src/filecard/**/*.ts', + 'src/filecard/**/*.tsx', + // 公共模块 + 'src/_util/**/*.ts', + 'src/_util/**/*.tsx', + 'src/*.ts', + // 排除项 '!src/**/_example', '!src/**/*.d.ts', '!src/**/__tests__', '!src/**/_usage', ]; +// 全部都打包 +// const inputList = [ +// 'src/**/*.ts', +// 'src/**/*.jsx', +// 'src/**/*.tsx', +// '!src/**/_example', +// '!src/**/*.d.ts', +// '!src/**/__tests__', +// '!src/**/_usage', +// ]; + + const getPlugins = ({ env, isProd = false, ignoreLess = false } = {}) => { const plugins = [ nodeResolve(), diff --git a/src/_util/classname.ts b/src/_util/classname.ts index cfb979f2..255c60b0 100644 --- a/src/_util/classname.ts +++ b/src/_util/classname.ts @@ -1,8 +1,8 @@ import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; +// import { twMerge } from 'tailwind-merge'; export default function classname(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return clsx(inputs); } export function getClassPrefix() { diff --git a/src/_util/copyToClipboard.ts b/src/_util/copyToClipboard.ts new file mode 100644 index 00000000..e0bde7a3 --- /dev/null +++ b/src/_util/copyToClipboard.ts @@ -0,0 +1,18 @@ +export function copy(text: string) { + let success: boolean; + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + success = true; + } catch (e) { + console.error('fallback 复制失败', e); + success = false; + } + document.body.removeChild(textarea); + return success; +} diff --git a/src/attachments/attachments.tsx b/src/attachments/attachments.tsx index b66d2d1d..9899b895 100644 --- a/src/attachments/attachments.tsx +++ b/src/attachments/attachments.tsx @@ -35,25 +35,44 @@ export default class Attachments extends Component { containerRef = createRef(); + /** ResizeObserver 实例 */ + private resizeObserver: ResizeObserver | null = null; + + /** 滚动事件处理器(保存引用以便清理) */ + private handleScroll = () => { + this.updateButtonVisibility(); + }; + installed() { // 添加延迟确保DOM完全渲染 requestAnimationFrame(() => { requestAnimationFrame(() => { this.updateButtonVisibility(); // 添加尺寸变化监听 - const resizeObserver = new ResizeObserver(() => { + this.resizeObserver = new ResizeObserver(() => { this.updateButtonVisibility(); }); if (this.containerRef.current) { - resizeObserver.observe(this.containerRef.current); + this.resizeObserver.observe(this.containerRef.current); } }); }); // 监听手动滚动事件 - this.containerRef.current?.addEventListener('scroll', () => { - this.updateButtonVisibility(); - }); + this.containerRef.current?.addEventListener('scroll', this.handleScroll); + } + + uninstalled() { + // 清理 ResizeObserver + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + + // 清理滚动事件监听器 + if (this.containerRef.current) { + this.containerRef.current.removeEventListener('scroll', this.handleScroll); + } } ready(): void { diff --git a/src/chat-action/action.tsx b/src/chat-action/action.tsx index 219b6230..9648bb45 100644 --- a/src/chat-action/action.tsx +++ b/src/chat-action/action.tsx @@ -10,6 +10,7 @@ import '../../src/tooltip'; import { Component, signal, tag } from 'omi'; import { getClassPrefix } from '../_util/classname'; +import { copy as fallbackCopy } from '../_util/copyToClipboard'; import { setExportparts } from '../_util/dom'; import { type ChatComment } from '../chat-engine'; import { MessagePlugin } from '../message'; @@ -30,14 +31,20 @@ export const renderActions = ( const clickCopyHandler = () => { const text = copyText.toString(); if (!text) return; - navigator.clipboard - .writeText(copyText.toString()) - .then(() => { - MessagePlugin.success('复制成功'); - }) - .catch(() => { - MessagePlugin.success('复制失败,请手动复制'); - }); + if (navigator.clipboard) { + navigator.clipboard + .writeText(copyText.toString()) + .then(() => { + MessagePlugin.success('复制成功'); + }) + .catch(() => { + MessagePlugin.success('复制失败,请手动复制'); + }); + } else { + const success = fallbackCopy(text); + if (success) MessagePlugin.success('复制成功'); + else MessagePlugin.error('复制失败,请手动复制'); + } }; const handleClickAction = (action: TdChatActionsName, data: any) => { diff --git a/src/chat-engine/adapters/agui/ActivityManager.ts b/src/chat-engine/adapters/agui/ActivityManager.ts new file mode 100644 index 00000000..09febd41 --- /dev/null +++ b/src/chat-engine/adapters/agui/ActivityManager.ts @@ -0,0 +1,206 @@ +import { ActivityData } from '../../type'; +import { applyJsonPatch } from '../../utils'; + +/** + * Activity 状态管理器 + * + * 职责: + * - 处理 AG-UI Activity 事件的 Snapshot/Delta 增量合并 + * - 提供当前 Activity 数据的快捷访问(用于 event-mapper 生成消息内容) + * + * 注意: + * - Activity 本身会渲染在消息流中,不需要独立订阅机制 + * - 如需监听 Activity 变化,应使用 eventBus 的 MESSAGE_UPDATE 事件 + */ + +export interface ActivityManager { + /** + * 获取当前活跃的 activityType + */ + getCurrentActivityType: () => string | null; + /** + * 获取当前 Activity 内容(基于当前活跃的 activityType) + */ + getCurrentActivity: () => ActivityData | null; + /** + * 获取指定 activityType 的 Activity 内容 + */ + getActivity: (activityType: string) => ActivityData | null; + /** + * 获取所有 activityType + */ + getAllActivityTypes: () => string[]; + /** + * 处理 AG-UI Activity 事件 + */ + handleActivityEvent: (event: { + type: string; + activityType: string; + content?: any; + patch?: any[]; + messageId?: string; + }) => ActivityData | null; + /** + * 清理活动数据 + */ + clear: () => void; +} + +export class ActivityManagerImpl implements ActivityManager { + private activities: Record = {}; + + private currentActivityType: string | null = null; + + /** + * 获取当前活跃的 activityType + */ + getCurrentActivityType(): string | null { + return this.currentActivityType; + } + + /** + * 获取当前 Activity 内容 + */ + getCurrentActivity(): ActivityData | null { + if (!this.currentActivityType) return null; + return this.activities[this.currentActivityType] || null; + } + + /** + * 获取指定 activityType 的 Activity 内容 + */ + getActivity(activityType: string): ActivityData | null { + return this.activities[activityType] || null; + } + + /** + * 获取所有 activityType + */ + getAllActivityTypes(): string[] { + return Object.keys(this.activities); + } + + /** + * 处理 AG-UI Activity 事件 + * 返回更新后的 ActivityData,供 event-mapper 使用 + */ + handleActivityEvent(event: { + type: string; + activityType: string; + content?: any; + patch?: any[]; + messageId?: string; + }): ActivityData | null { + const { activityType, messageId } = event; + + if (event.type === 'ACTIVITY_SNAPSHOT') { + // SNAPSHOT: 全量更新 + const activityData: ActivityData = { + activityType, + content: event.content || {}, + messageId, + }; + this.setActivity(activityType, activityData); + return activityData; + } + + if (event.type === 'ACTIVITY_DELTA') { + // DELTA: 增量更新 + const isFirstDelta = !this.activities[activityType]; + let currentActivity = this.activities[activityType]; + + // 如果没有 snapshot,自动初始化空的 activity(纯增量模式) + if (isFirstDelta) { + currentActivity = { + activityType, + content: this.inferInitialContent(event.patch), + messageId, + }; + } + + // 记录旧的 operations/messages 数量(用于增量标记) + const oldContent = currentActivity.content; + const oldOpsCount = this.getOperationsCount(oldContent); + + let newContent = oldContent; + + // 使用 patch 进行 JSON Patch 更新 + if (event.patch && Array.isArray(event.patch)) { + newContent = applyJsonPatch(oldContent, event.patch); + } + + // 计算新增的 operations 范围 + const newOpsCount = this.getOperationsCount(newContent); + const deltaInfo = newOpsCount > oldOpsCount ? { fromIndex: oldOpsCount, toIndex: newOpsCount } : undefined; + + const activityData: ActivityData = { + activityType, + content: newContent, + messageId, + deltaInfo, + }; + + this.setActivity(activityType, activityData); + return activityData; + } + + return null; + } + + /** + * 清理所有活动数据 + * 用于新一轮对话开始时重置状态 + */ + clear(): void { + this.activities = {}; + this.currentActivityType = null; + } + + /** + * 根据 JSON Patch 路径推断初始内容结构 + */ + private inferInitialContent(patch: any[] | undefined): Record { + if (!patch || !Array.isArray(patch) || patch.length === 0) { + return {}; + } + + const initialContent: Record = {}; + + for (const op of patch) { + if (op.path) { + const pathParts = op.path.split('/').filter(Boolean); + if (pathParts.length > 0) { + const rootKey = pathParts[0]; + // 如果路径包含 - 或数字索引,说明是数组 + if (pathParts.length > 1 && (pathParts[1] === '-' || /^\d+$/.test(pathParts[1]))) { + if (!initialContent[rootKey]) { + initialContent[rootKey] = []; + } + } + } + } + } + + return initialContent; + } + + /** + * 获取 content 中的 operations/messages 数量 + */ + private getOperationsCount(content: Record | null | undefined): number { + if (!content) return 0; + const ops = content.operations || content.messages; + return Array.isArray(ops) ? ops.length : 0; + } + + /** + * 设置 Activity + */ + private setActivity(activityType: string, activity: ActivityData): void { + this.activities[activityType] = activity; + this.currentActivityType = activityType; + } +} + +// 全局单例 +export const activityManager = new ActivityManagerImpl(); diff --git a/src/chat-engine/adapters/agui/state-manager.ts b/src/chat-engine/adapters/agui/StateManager.ts similarity index 100% rename from src/chat-engine/adapters/agui/state-manager.ts rename to src/chat-engine/adapters/agui/StateManager.ts diff --git a/src/chat-engine/adapters/agui/event-mapper.ts b/src/chat-engine/adapters/agui/event-mapper.ts index 4cece46c..eadb5c3e 100644 --- a/src/chat-engine/adapters/agui/event-mapper.ts +++ b/src/chat-engine/adapters/agui/event-mapper.ts @@ -1,9 +1,18 @@ /* eslint-disable class-methods-use-this */ import type { AIMessageContent, SSEChunkData, ToolCall } from '../../type'; -import { EventType, isStateEvent, isTextMessageEvent, isThinkingEvent, isToolCallEvent } from './events'; -import { stateManager } from './state-manager'; +import { + AGUIEventType, + isActivityEvent, + isStateEvent, + isTextMessageEvent, + isThinkingEvent, + isToolCallEvent, +} from './types/events'; +import { activityManager } from './ActivityManager'; +import { stateManager } from './StateManager'; import { addToReasoningData, + createActivityContent, createMarkdownContent, createReasoningContent, createTextContent, @@ -23,6 +32,10 @@ import { * 将AG-UI协议事件(SSEChunkData)转换为AIContentChunkUpdate * 支持多轮对话、增量文本、工具调用、思考、状态快照、消息快照等基础事件 * 同时提供状态变更和步骤生命周期事件的分发机制 + * + * 支持简化模式: + * - TEXT_MESSAGE_CHUNK:自动补全 Start → Content → End 生命周期 + * - TOOL_CALL_CHUNK:自动补全 Start → Args → End 生命周期 */ export class AGUIEventMapper { private toolCallMap: Record = {}; @@ -40,6 +53,20 @@ export class AGUIEventMapper { currentDataIndex: 0, }; + /** + * 暴露 activityManager,供 AGUIAdapter 访问 + */ + public get activityManager() { + return activityManager; + } + + // 简化模式状态跟踪 + private currentTextMessageId: string | null = null; // 当前正在处理的文本消息 ID + + private currentTextMessageRole: 'assistant' | 'system' | null = null; // 当前正在处理的文本消息角色 + + private toolCallChunkStarted: Set = new Set(); // 已自动触发 TOOL_CALL_START 的 toolCallId + /** * 主入口:将SSE事件转换为AIContentChunkUpdate * @@ -64,6 +91,10 @@ export class AGUIEventMapper { return this.handleToolCallEvent(event); } + if (isActivityEvent(event.type)) { + return this.handleActivityEvent(event); + } + if (isStateEvent(event.type)) { return this.handleStateEvent(event); } @@ -85,6 +116,7 @@ export class AGUIEventMapper { clearToolCall(toolCallId: string): void { delete this.toolCallMap[toolCallId]; this.toolCallEnded.delete(toolCallId); + this.toolCallChunkStarted.delete(toolCallId); } /** @@ -105,42 +137,79 @@ export class AGUIEventMapper { this.toolCallMap = {}; this.toolCallEnded.clear(); this.resetReasoningContext(); + // 重置简化模式状态 + this.currentTextMessageId = null; + this.currentTextMessageRole = null; + this.toolCallChunkStarted.clear(); + // 清理 activityManager 状态 + activityManager.clear(); } /** * 处理文本消息事件 + * + * 支持两种模式: + * 1. 标准模式:TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT → TEXT_MESSAGE_END + * 2. 简化模式:仅发送 TEXT_MESSAGE_CHUNK,自动补全生命周期 */ private handleTextMessageEvent(event: any): AIMessageContent | null { switch (event.type) { - case EventType.TEXT_MESSAGE_START: + case AGUIEventType.TEXT_MESSAGE_START: + this.currentTextMessageId = event.messageId || null; // 标记当前消息 ID return createMarkdownContent('', 'streaming', 'append'); - case EventType.TEXT_MESSAGE_CHUNK: - case EventType.TEXT_MESSAGE_CONTENT: - case EventType.TEXT_MESSAGE_END: - return createMarkdownContent( - event.delta || '', - event.type === EventType.TEXT_MESSAGE_END ? 'complete' : 'streaming', - 'merge', - ); + + case AGUIEventType.TEXT_MESSAGE_CHUNK: + return this.handleTextMessageChunk(event); + + case AGUIEventType.TEXT_MESSAGE_CONTENT: + return createMarkdownContent(event.delta || '', 'streaming', 'merge'); + + case AGUIEventType.TEXT_MESSAGE_END: + this.currentTextMessageId = null; // 重置状态 + return createMarkdownContent(event.delta || '', 'complete', 'merge'); + default: return null; } } + /** + * 处理简化模式的 TEXT_MESSAGE_CHUNK 事件 + * 自动补全 Start → Content → End 生命周期 + * + * 关键:通过 messageId 区分不同的文本块, + * 当 messageId 变化时创建新的内容块 + */ + private handleTextMessageChunk(event: any): AIMessageContent | null { + const messageId = event.messageId || 'default'; + const role = event?.role || 'assistant'; + + // 如果是新的 messageId,需要创建新的内容块 + if (this.currentTextMessageId !== messageId) { + this.currentTextMessageId = messageId; + this.currentTextMessageRole = role; + // 创建新内容块,使用 append 策略,通过 ext.role 传递角色信息 + return createMarkdownContent(event.delta || '', 'streaming', 'append', role); + } + + // 同一个 messageId,使用 merge 策略追加内容 + return createMarkdownContent(event.delta || '', 'streaming', 'merge', this.currentTextMessageRole || role); + } + /** * 处理思考事件 */ private handleThinkingEvent(event: any): AIMessageContent | null { switch (event.type) { - case EventType.THINKING_START: + case AGUIEventType.THINKING_START: return this.handleThinkingStart(); - case EventType.THINKING_TEXT_MESSAGE_START: + case AGUIEventType.THINKING_TEXT_MESSAGE_START: return this.handleThinkingTextStart(event); - case EventType.THINKING_TEXT_MESSAGE_CONTENT: + case AGUIEventType.THINKING_TEXT_MESSAGE_CONTENT: return this.handleThinkingTextContent(event); - case EventType.THINKING_TEXT_MESSAGE_END: + case AGUIEventType.THINKING_TEXT_MESSAGE_END: return this.handleThinkingTextEnd(event); - case EventType.THINKING_END: + case AGUIEventType.THINKING_END: return this.handleThinkingEnd(); default: return null; @@ -149,30 +218,66 @@ export class AGUIEventMapper { /** * 处理工具调用事件 + * + * 支持两种模式: + * 1. 标准模式:TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END + * 2. 简化模式:仅发送 TOOL_CALL_CHUNK,自动补全生命周期 */ private handleToolCallEvent(event: any): AIMessageContent | null { switch (event.type) { - case EventType.TOOL_CALL_START: + case AGUIEventType.TOOL_CALL_START: return this.handleToolCallStart(event); - case EventType.TOOL_CALL_ARGS: + case AGUIEventType.TOOL_CALL_ARGS: return this.handleToolCallArgs(event); - case EventType.TOOL_CALL_CHUNK: + case AGUIEventType.TOOL_CALL_CHUNK: return this.handleToolCallChunk(event); - case EventType.TOOL_CALL_RESULT: + case AGUIEventType.TOOL_CALL_RESULT: return this.handleToolCallResult(event); - case EventType.TOOL_CALL_END: + case AGUIEventType.TOOL_CALL_END: return this.handleToolCallEnd(event); default: return null; } } + /** + * 处理活动事件 + * 委托给 activityManager 进行状态管理和订阅通知 + * + * 支持两种模式: + * 1. 标准模式:先收到 ACTIVITY_SNAPSHOT,后续 ACTIVITY_DELTA 基于 snapshot 增量更新 + * 2. 纯增量模式:没有 ACTIVITY_SNAPSHOT,直接收到 ACTIVITY_DELTA,自动初始化空内容 + * + * 注意:不同 activityType 的活动是独立管理的,互不影响 + */ + private handleActivityEvent(event: any): AIMessageContent | null { + const activityType = event.activityType || 'unknown'; + // 委托给 activityManager 处理 + const activityData = activityManager.handleActivityEvent(event); + if (!activityData) { + return null; + } + + // 根据事件类型决定 strategy + const isSnapshot = event.type === AGUIEventType.ACTIVITY_SNAPSHOT; + const isFirstDelta = event.type === AGUIEventType.ACTIVITY_DELTA && !activityManager.getActivity(activityType); + + return createActivityContent( + activityType, + activityData.content, + 'streaming', + // SNAPSHOT 或首次 DELTA 使用 append 创建新内容块,后续使用 merge + isSnapshot || isFirstDelta ? 'append' : 'merge', + activityData.deltaInfo, + ); + } + /** * 处理状态事件 */ private handleStateEvent(event: any): null { stateManager.handleStateEvent(event); - return null; // 让业务层通过useStateSubscription订阅状态变化 + return null; } /** @@ -180,11 +285,11 @@ export class AGUIEventMapper { */ private handleOtherEvent(event: any): AIMessageContent | AIMessageContent[] | null { switch (event.type) { - case EventType.MESSAGES_SNAPSHOT: + case AGUIEventType.MESSAGES_SNAPSHOT: return handleMessagesSnapshot(event.messages); - case EventType.CUSTOM: + case AGUIEventType.CUSTOM: return handleCustomEvent(event); - case EventType.RUN_ERROR: + case AGUIEventType.RUN_ERROR: return [createTextContent(event.message || event.error || '系统未知错误', 'error')]; default: return null; @@ -293,6 +398,9 @@ export class AGUIEventMapper { * 处理工具调用开始事件 */ private handleToolCallStart(event: any): AIMessageContent | null { + // 标记已显式开始(防止后续 chunk 重复触发 start) + this.toolCallChunkStarted.add(event.toolCallId); + // 初始化工具调用 this.toolCallMap[event.toolCallId] = { eventType: 'TOOL_CALL_START', @@ -336,21 +444,66 @@ export class AGUIEventMapper { } /** - * 处理工具调用块事件 + * 处理简化模式的 TOOL_CALL_CHUNK 事件 + * 自动补全 Start → Args → End 生命周期 + * + * TOOL_CALL_CHUNK 事件结构: + * - toolCallId: 工具调用ID(可选,首次时自动生成) + * - toolCallName: 工具名称(首次必需) + * - delta: 参数增量内容 + * - parentMessageId: 父消息ID(可选) */ private handleToolCallChunk(event: any): AIMessageContent | null { - if (!this.toolCallMap[event.toolCallId]) return null; + // 生成或使用 toolCallId + const toolCallId = event.toolCallId || `auto_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + + // 检查是否是该 toolCallId 的第一个 chunk + const isFirstChunk = !this.toolCallChunkStarted.has(toolCallId) && !this.toolCallMap[toolCallId]; + + if (isFirstChunk) { + // 自动触发 TOOL_CALL_START 逻辑 + this.toolCallChunkStarted.add(toolCallId); + + // 重置当前文本消息 ID,确保后续文本消息创建新内容块 + this.currentTextMessageId = null; - const currentChunk = this.toolCallMap[event.toolCallId].chunk || ''; - const newChunk = mergeStringContent(currentChunk, event.delta || ''); + // 初始化工具调用 + this.toolCallMap[toolCallId] = { + eventType: 'TOOL_CALL_START', + toolCallId, + toolCallName: event.toolCallName || 'unknown', + parentMessageId: event.parentMessageId || '', + args: event.delta || '', // 第一个 chunk 的 delta 作为初始 args + }; + + // 创建新的工具调用内容块 + const toolCallContent = createToolCallContent(this.toolCallMap[toolCallId], 'streaming', 'append'); + + if (this.reasoningContext.active) { + // Reasoning 模式:添加 toolcall 到 reasoning.data + const { data, index } = addToReasoningData(this.reasoningContext.currentData, toolCallContent); + this.reasoningContext.currentData = data; + this.reasoningContext.currentDataIndex = index; + + return createReasoningContent(data, 'streaming', 'merge', false); + } + + return toolCallContent; + } + + // 后续 chunk:更新 args + if (!this.toolCallMap[toolCallId]) return null; + + const currentArgs = this.toolCallMap[toolCallId].args || ''; + const newArgs = mergeStringContent(currentArgs, event.delta || ''); // 更新内部ToolCall对象 - this.toolCallMap[event.toolCallId] = updateToolCall(this.toolCallMap[event.toolCallId], { + this.toolCallMap[toolCallId] = updateToolCall(this.toolCallMap[toolCallId], { eventType: 'TOOL_CALL_CHUNK', - chunk: newChunk, + args: newArgs, }); - return this.updateToolCallInContext(event.toolCallId, 'streaming'); + return this.updateToolCallInContext(toolCallId, 'streaming'); } /** @@ -364,7 +517,7 @@ export class AGUIEventMapper { // 更新内部ToolCall对象 this.toolCallMap[event.toolCallId] = updateToolCall(this.toolCallMap[event.toolCallId], { - eventType: EventType.TOOL_CALL_RESULT, + eventType: AGUIEventType.TOOL_CALL_RESULT, result: newResult, }); @@ -398,6 +551,15 @@ export class AGUIEventMapper { private handleToolCallEnd(event: any) { // 标记工具调用结束 this.toolCallEnded.add(event.toolCallId); + + // 更新 toolCallMap 中的 eventType + if (this.toolCallMap[event.toolCallId]) { + this.toolCallMap[event.toolCallId] = { + ...this.toolCallMap[event.toolCallId], + eventType: AGUIEventType.TOOL_CALL_END, + }; + } + return this.updateToolCallInContext(event.toolCallId, 'complete'); } diff --git a/src/chat-engine/adapters/agui/index.ts b/src/chat-engine/adapters/agui/index.ts index 517470f9..648ec6c2 100644 --- a/src/chat-engine/adapters/agui/index.ts +++ b/src/chat-engine/adapters/agui/index.ts @@ -1,10 +1,22 @@ /* eslint-disable class-methods-use-this */ -import type { AIMessageContent, ChatMessagesData,ChatRequestParams, SSEChunkData, ToolCall } from '../../type'; +import type { + AIMessageContent, + ChatMessagesData, + ChatRequestParams, + SSEChunkData, + ToolCall, + UserMessageContent, +} from '../../type'; +import type { BaseEvent, RunErrorEvent, RunFinishedEvent, RunStartedEvent } from './types/events'; +import { AGUIEventType } from './types/events'; import { AGUIEventMapper } from './event-mapper'; -import type { BaseEvent,RunErrorEvent, RunFinishedEvent, RunStartedEvent } from './events'; -import { EventType } from './events'; -import type { AGUIAssistantHistoryMessage,AGUIHistoryMessage, AGUIUserHistoryMessage } from './types'; -import { buildToolCallMap,processReasoningContent, processToolCalls } from './utils'; +import type { + AGUIActivityMessage, + AGUIAssistantHistoryMessage, + AGUIHistoryMessage, + AGUIUserHistoryMessage, +} from './types'; +import { buildToolCallMap, processReasoningContent, processToolCalls } from './utils'; // 重新导出类型,以便其他文件可以使用 export type { @@ -12,6 +24,7 @@ export type { AGUIUserHistoryMessage, AGUIAssistantHistoryMessage, AGUIToolHistoryMessage, + AGUIActivityMessage, } from './types'; /** @@ -34,6 +47,21 @@ export class AGUIAdapter { /** * 转换AG-UI历史消息为ChatMessagesData格式(静态方法) * + * ## 设计说明 + * + * ### AG-UI 协议中的消息角色 + * - `user`: 用户消息 + * - `assistant`: AI 回复,可包含 content、reasoningContent、toolCalls + * - `tool`: 工具调用结果,通过 toolCallId 关联到 assistant 的 toolCalls + * - `activity`: 独立的活动/事件消息,表示代理运行过程中的中间状态 + * + * ### Activity 消息的处理策略 + * Activity 在 AG-UI 中是独立的 role,不是 assistant 的附属。 + * 但在 UI 展示层,我们选择将其合并到 AI 响应中,原因: + * 1. 保持对话气泡的整洁性(一问一答模式) + * 2. Activity 通常是 AI 处理过程的可视化,属于同一个响应周期 + * 3. 通过 activityType 区分不同类型的 Activity 渲染 + * * 转换策略: * 1. 以用户消息为边界进行分组 * 2. 每个用户消息对应一个AI消息,包含该用户输入后的所有AI回复和工具调用结果 @@ -57,13 +85,18 @@ export class AGUIAdapter { /** * 处理消息组,构建AI消息的content数组 + * + * 处理顺序(保持原始顺序): + * 1. assistant 的 reasoningContent(思考过程) + * 2. assistant 的 content(文本回复) + * 3. assistant 的 toolCalls(工具调用) + * 4. activity 消息(活动/状态展示) */ const processMessageGroup = (messages: AGUIHistoryMessage[]): AIMessageContent[] => { const allContent: AIMessageContent[] = []; - messages - .filter((msg) => msg.role === 'assistant') - .forEach((msg) => { + messages.forEach((msg) => { + if (msg.role === 'assistant') { const assistantMsg = msg as AGUIAssistantHistoryMessage; // 处理 reasoningContent(支持 reasoning 和 thinking 两种类型) @@ -87,7 +120,35 @@ export class AGUIAdapter { const toolCallContents = processToolCalls(assistantMsg.toolCalls, toolCallMap); allContent.push(...(toolCallContents as AIMessageContent[])); } - }); + } else if (msg.role === 'activity') { + const activityMsg = msg as AGUIActivityMessage; + + // 检查是否是存储为 Activity 的 CUSTOM 事件 + if (activityMsg.activityType === AGUIEventType.CUSTOM) { + // 将存储为 Activity 的 CUSTOM 事件转换为标准 custom 格式,业务层自行处理 + const customContent: any = { + type: 'custom', + data: { + name: activityMsg.content?.name || '', + value: activityMsg.content?.value, + }, + status: 'complete', + }; + allContent.push(customContent); + } else { + // 普通 Activity 处理 + // 使用 activity-${activityType} 格式的 type,与流式传输时的处理保持一致 + allContent.push({ + type: `activity-${activityMsg.activityType}`, + data: { + activityType: activityMsg.activityType, + content: activityMsg.content, + }, + status: 'complete', + } as any); + } + } + }); return allContent; }; @@ -105,11 +166,32 @@ export class AGUIAdapter { role: 'assistant', content: allContent, status: 'complete', + datetime: new Date(messages.at(-1)?.timestamp || Date.now()).toISOString(), }); } } }; + /** + * 处理用户消息的 content 字段 + * 兼容两种格式: + * 1. 标准 AG-UI 格式:content 是字符串 + * 2. 已转换格式:content 已经是数组 [{ type, data }] + */ + const processUserContent = (content: any): UserMessageContent[] => { + // 如果 content 已经是数组格式,直接返回 + if (Array.isArray(content)) { + return content as UserMessageContent[]; + } + // 如果是字符串,包装为标准格式 + return [ + { + type: 'text', + data: content, + }, + ]; + }; + /** * 处理当前消息组 */ @@ -119,12 +201,7 @@ export class AGUIAdapter { convertedMessages.push({ id: currentUserMessage.id, role: 'user', - content: [ - { - type: 'text', - data: currentUserMessage.content, - }, - ], + content: processUserContent(currentUserMessage.content), datetime: new Date(currentUserMessage.timestamp || Date.now()).toISOString(), }); } @@ -144,8 +221,8 @@ export class AGUIAdapter { // 开始新的组 currentUserMessage = msg as AGUIUserHistoryMessage; - } else if (msg.role === 'assistant' || msg.role === 'tool') { - // 收集助手消息和工具调用结果到当前组 + } else if (msg.role === 'assistant' || msg.role === 'tool' || msg.role === 'activity') { + // 收集助手消息、工具调用结果和活动消息到当前组 currentGroupMessages.push(msg); } }); @@ -227,19 +304,21 @@ export class AGUIAdapter { * * @param event 解析后的事件对象 * @param callbacks 回调函数 - * @returns 是否处理了生命周期事件 + * @returns 是否处理了生命周期事件(仅RUN_STARTED和RUN_FINISHED返回true,RUN_ERROR需要继续处理生成消息内容) */ private handleAGUISpecificEvents(event: BaseEvent, callbacks: AGUIAdapterCallbacks): boolean { switch (event.type) { - case EventType.RUN_STARTED: + case AGUIEventType.RUN_STARTED: callbacks.onRunStart?.(event as RunStartedEvent); return true; - case EventType.RUN_FINISHED: + case AGUIEventType.RUN_FINISHED: callbacks.onRunComplete?.(false, {} as ChatRequestParams, event as RunFinishedEvent); return true; - case EventType.RUN_ERROR: + case AGUIEventType.RUN_ERROR: + // RUN_ERROR 需要触发回调,但不能返回 true + // 因为还需要通过 event-mapper 生成错误消息内容用于 UI 展示 callbacks.onRunError?.(event as RunErrorEvent); - return true; + return false; // 继续处理,让 event-mapper 生成错误消息 default: return false; // 不是生命周期事件 } @@ -249,4 +328,6 @@ export class AGUIAdapter { /** * 状态订阅机制导出 */ -export * from './state-manager'; +export * from './StateManager'; +export * from './ActivityManager'; +export * from './types'; diff --git a/src/chat-engine/adapters/agui/events.ts b/src/chat-engine/adapters/agui/types/events.ts similarity index 76% rename from src/chat-engine/adapters/agui/events.ts rename to src/chat-engine/adapters/agui/types/events.ts index bdce25c9..f1f484bc 100644 --- a/src/chat-engine/adapters/agui/events.ts +++ b/src/chat-engine/adapters/agui/types/events.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { MessageSchema, StateSchema } from './types'; +import { AGUIMessageSchema, StateSchema } from '.'; export type ToolCallEventType = | 'TOOL_CALL_START' @@ -9,7 +9,7 @@ export type ToolCallEventType = | 'TOOL_CALL_CHUNK' | 'TOOL_CALL_RESULT'; -export enum EventType { +export enum AGUIEventType { TEXT_MESSAGE_START = 'TEXT_MESSAGE_START', TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT', TEXT_MESSAGE_END = 'TEXT_MESSAGE_END', @@ -26,6 +26,9 @@ export enum EventType { TOOL_CALL_CHUNK = 'TOOL_CALL_CHUNK', TOOL_CALL_RESULT = 'TOOL_CALL_RESULT', + ACTIVITY_SNAPSHOT = 'ACTIVITY_SNAPSHOT', + ACTIVITY_DELTA = 'ACTIVITY_DELTA', + STATE_SNAPSHOT = 'STATE_SNAPSHOT', STATE_DELTA = 'STATE_DELTA', MESSAGES_SNAPSHOT = 'MESSAGES_SNAPSHOT', @@ -73,6 +76,15 @@ export function isToolCallEvent(eventType: string): boolean { ); } +/** + * 检查事件类型是否为活动相关 + * @param eventType 事件类型 + * @returns 是否为活动事件 + */ +export function isActivityEvent(eventType: string): boolean { + return ['ACTIVITY_SNAPSHOT', 'ACTIVITY_DELTA'].includes(eventType); +} + /** * 检查事件类型是否为状态相关 * @param eventType 事件类型 @@ -83,71 +95,71 @@ export function isStateEvent(eventType: string): boolean { } const BaseEventSchema = z.object({ - type: z.nativeEnum(EventType), + type: z.nativeEnum(AGUIEventType), timestamp: z.number().optional(), rawEvent: z.any().optional(), }); export const TextMessageStartEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TEXT_MESSAGE_START), + type: z.literal(AGUIEventType.TEXT_MESSAGE_START), messageId: z.string(), role: z.literal('assistant'), }); export const TextMessageContentEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TEXT_MESSAGE_CONTENT), + type: z.literal(AGUIEventType.TEXT_MESSAGE_CONTENT), messageId: z.string(), delta: z.string().refine((s) => s.length > 0, 'Delta must not be an empty string'), }); export const TextMessageEndEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TEXT_MESSAGE_END), + type: z.literal(AGUIEventType.TEXT_MESSAGE_END), messageId: z.string(), }); export const TextMessageChunkEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TEXT_MESSAGE_CHUNK), + type: z.literal(AGUIEventType.TEXT_MESSAGE_CHUNK), messageId: z.string().optional(), role: z.literal('assistant').optional(), delta: z.string().optional(), }); export const ThinkingTextMessageStartEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.THINKING_TEXT_MESSAGE_START), + type: z.literal(AGUIEventType.THINKING_TEXT_MESSAGE_START), }); export const ThinkingTextMessageContentEventSchema = TextMessageContentEventSchema.omit({ messageId: true, type: true, }).extend({ - type: z.literal(EventType.THINKING_TEXT_MESSAGE_CONTENT), + type: z.literal(AGUIEventType.THINKING_TEXT_MESSAGE_CONTENT), }); export const ThinkingTextMessageEndEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.THINKING_TEXT_MESSAGE_END), + type: z.literal(AGUIEventType.THINKING_TEXT_MESSAGE_END), }); export const ToolCallStartEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TOOL_CALL_START), + type: z.literal(AGUIEventType.TOOL_CALL_START), toolCallId: z.string(), toolCallName: z.string(), parentMessageId: z.string().optional(), }); export const ToolCallArgsEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TOOL_CALL_ARGS), + type: z.literal(AGUIEventType.TOOL_CALL_ARGS), toolCallId: z.string(), delta: z.string(), }); export const ToolCallEndEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TOOL_CALL_END), + type: z.literal(AGUIEventType.TOOL_CALL_END), toolCallId: z.string(), }); export const ToolCallResultEventSchema = BaseEventSchema.extend({ messageId: z.string(), - type: z.literal(EventType.TOOL_CALL_RESULT), + type: z.literal(AGUIEventType.TOOL_CALL_RESULT), toolCallId: z.string(), toolCallName: z.string(), content: z.string(), @@ -155,76 +167,91 @@ export const ToolCallResultEventSchema = BaseEventSchema.extend({ }); export const ToolCallChunkEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.TOOL_CALL_CHUNK), + type: z.literal(AGUIEventType.TOOL_CALL_CHUNK), toolCallId: z.string().optional(), toolCallName: z.string().optional(), parentMessageId: z.string().optional(), delta: z.string().optional(), }); +export const ActivitySnapshotEventSchema = BaseEventSchema.extend({ + type: z.literal(AGUIEventType.ACTIVITY_SNAPSHOT), + messageId: z.string().optional(), + activityType: z.string(), + content: z.record(z.any()), + replace: z.boolean().optional(), +}); + +export const ActivityDeltaEventSchema = BaseEventSchema.extend({ + type: z.literal(AGUIEventType.ACTIVITY_DELTA), + messageId: z.string().optional(), + activityType: z.string().optional(), + patch: z.array(z.any()).optional(), // JSON Patch (RFC 6902) +}); + export const ThinkingStartEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.THINKING_START), + type: z.literal(AGUIEventType.THINKING_START), title: z.string().optional(), }); export const ThinkingEndEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.THINKING_END), + type: z.literal(AGUIEventType.THINKING_END), title: z.string().optional(), }); export const StateSnapshotEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STATE_SNAPSHOT), + type: z.literal(AGUIEventType.STATE_SNAPSHOT), snapshot: StateSchema, }); export const StateDeltaEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STATE_DELTA), + type: z.literal(AGUIEventType.STATE_DELTA), delta: z.array(z.any()), // JSON Patch (RFC 6902) }); export const MessagesSnapshotEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.MESSAGES_SNAPSHOT), - messages: z.array(MessageSchema), + type: z.literal(AGUIEventType.MESSAGES_SNAPSHOT), + messages: z.array(AGUIMessageSchema), }); export const RawEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RAW), + type: z.literal(AGUIEventType.RAW), event: z.any(), source: z.string().optional(), }); export const CustomEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.CUSTOM), + type: z.literal(AGUIEventType.CUSTOM), name: z.string(), value: z.any(), }); export const RunStartedEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_STARTED), + type: z.literal(AGUIEventType.RUN_STARTED), threadId: z.string(), runId: z.string(), }); export const RunFinishedEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_FINISHED), + type: z.literal(AGUIEventType.RUN_FINISHED), threadId: z.string(), runId: z.string(), result: z.any().optional(), }); export const RunErrorEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_ERROR), + type: z.literal(AGUIEventType.RUN_ERROR), message: z.string(), code: z.string().optional(), }); export const StepStartedEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STEP_STARTED), + type: z.literal(AGUIEventType.STEP_STARTED), stepName: z.string(), }); export const StepFinishedEventSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STEP_FINISHED), + type: z.literal(AGUIEventType.STEP_FINISHED), stepName: z.string(), }); @@ -241,6 +268,8 @@ export const EventSchemas = z.discriminatedUnion('type', [ ToolCallEndEventSchema, ToolCallChunkEventSchema, ToolCallResultEventSchema, + ActivitySnapshotEventSchema, + ActivityDeltaEventSchema, StateSnapshotEventSchema, StateDeltaEventSchema, MessagesSnapshotEventSchema, @@ -266,6 +295,8 @@ export type ToolCallArgsEvent = z.infer; export type ToolCallEndEvent = z.infer; export type ToolCallChunkEvent = z.infer; export type ToolCallResultEvent = z.infer; +export type ActivitySnapshotEvent = z.infer; +export type ActivityDeltaEvent = z.infer; export type ThinkingStartEvent = z.infer; export type ThinkingEndEvent = z.infer; export type StateSnapshotEvent = z.infer; diff --git a/src/chat-engine/adapters/agui/types.ts b/src/chat-engine/adapters/agui/types/index.ts similarity index 50% rename from src/chat-engine/adapters/agui/types.ts rename to src/chat-engine/adapters/agui/types/index.ts index a256c5c7..1ada7725 100644 --- a/src/chat-engine/adapters/agui/types.ts +++ b/src/chat-engine/adapters/agui/types/index.ts @@ -3,6 +3,8 @@ import { z } from 'zod'; +import type { UserMessageContent } from '../../../type'; + export const FunctionCallSchema = z.object({ name: z.string(), arguments: z.string(), @@ -14,7 +16,7 @@ export const ToolCallSchema = z.object({ function: FunctionCallSchema, }); -export const BaseMessageSchema = z.object({ +const BaseMessageSchema = z.object({ id: z.string(), role: z.string(), content: z.string().optional(), @@ -22,48 +24,71 @@ export const BaseMessageSchema = z.object({ timestamp: z.number().optional(), // 添加时间戳字段,用于历史消息 }); -export const DeveloperMessageSchema = BaseMessageSchema.extend({ +const DeveloperMessageSchema = BaseMessageSchema.extend({ role: z.literal('developer'), content: z.string(), }); -export const SystemMessageSchema = BaseMessageSchema.extend({ +const SystemMessageSchema = BaseMessageSchema.extend({ role: z.literal('system'), content: z.string(), }); -export const AssistantMessageSchema = BaseMessageSchema.extend({ +const AssistantMessageSchema = BaseMessageSchema.extend({ role: z.literal('assistant'), content: z.string().optional(), reasoningContent: z.string().optional(), toolCalls: z.array(ToolCallSchema).optional(), }); -export const UserMessageSchema = BaseMessageSchema.extend({ +/** + * 用户消息 Schema + * content 支持两种格式: + * 1. 字符串(标准 AG-UI 格式) + * 2. 数组(已转换为 ChatEngine 的 UserMessageContent[] 格式) + */ +const UserMessageSchema = BaseMessageSchema.extend({ role: z.literal('user'), - content: z.string(), + content: z.union([z.string(), z.array(z.any())]), }); -export const ToolMessageSchema = z.object({ +// 扩展类型定义,支持两种 content 格式 +export type AGUIUserMessageContent = string | UserMessageContent[]; + +const ToolMessageSchema = z.object({ id: z.string(), content: z.string(), role: z.literal('tool'), toolCallId: z.string(), + timestamp: z.number().optional(), }); -export const MessageSchema = z.discriminatedUnion('role', [ +const ActivityMessageSchema = z.object({ + id: z.string(), + role: z.literal('activity'), + activityType: z.string(), + content: z.record(z.any()), + timestamp: z.number().optional(), +}); + +export const AGUIMessageSchema = z.discriminatedUnion('role', [ DeveloperMessageSchema, SystemMessageSchema, AssistantMessageSchema, UserMessageSchema, ToolMessageSchema, + ActivityMessageSchema, ]); -// 历史消息相关的类型定义 +/** + * 历史消息相关的类型定义 + * + */ export const HistoryMessageSchema = z.discriminatedUnion('role', [ UserMessageSchema, AssistantMessageSchema, ToolMessageSchema, + ActivityMessageSchema, ]); export const RoleSchema = z.union([ @@ -72,6 +97,7 @@ export const RoleSchema = z.union([ z.literal('assistant'), z.literal('user'), z.literal('tool'), + z.literal('activity'), ]); export const ContextSchema = z.object({ @@ -89,7 +115,7 @@ export const RunAgentInputSchema = z.object({ threadId: z.string(), runId: z.string(), state: z.any(), - messages: z.array(MessageSchema), + messages: z.array(AGUIMessageSchema), tools: z.array(ToolSchema), context: z.array(ContextSchema), forwardedProps: z.any(), @@ -98,27 +124,30 @@ export const RunAgentInputSchema = z.object({ export const StateSchema = z.any(); export type AGUIToolCall = z.infer; -export type FunctionCall = z.infer; -export type DeveloperMessage = z.infer; -export type SystemMessage = z.infer; -export type AssistantMessage = z.infer; -export type UserMessage = z.infer; -export type ToolMessage = z.infer; -export type Message = z.infer; -export type Context = z.infer; -export type Tool = z.infer; +export type AGUIFunctionCall = z.infer; +export type AGUIDeveloperMessage = z.infer; +export type AGUISystemMessage = z.infer; +export type AGUIAssistantMessage = z.infer; +export type AGUIUserMessage = z.infer; +export type AGUIToolMessage = z.infer; +export type AGUIActivityMessage = z.infer; +export type AGUIMessage = z.infer; +export type AGUIContext = z.infer; +export type AGUITool = z.infer; export type RunAgentInput = z.infer; -export type State = z.infer; -export type Role = z.infer; +export type AGUIState = z.infer; +export type AGUIRole = z.infer; // 历史消息类型别名,复用已有的类型 -export type AGUIHistoryMessage = Message; -export type AGUIUserHistoryMessage = UserMessage; -export type AGUIAssistantHistoryMessage = AssistantMessage; -export type AGUIToolHistoryMessage = ToolMessage; +export type AGUIHistoryMessage = AGUIMessage; +export type AGUIUserHistoryMessage = AGUIUserMessage; +export type AGUIAssistantHistoryMessage = AGUIAssistantMessage; +export type AGUIToolHistoryMessage = AGUIToolMessage; export class AGUIError extends Error { constructor(message: string) { super(message); } } + +export * from './events'; diff --git a/src/chat-engine/adapters/agui/utils.ts b/src/chat-engine/adapters/agui/utils.ts index c05ffaf0..12102838 100644 --- a/src/chat-engine/adapters/agui/utils.ts +++ b/src/chat-engine/adapters/agui/utils.ts @@ -75,8 +75,9 @@ export function handleMessagesSnapshot(messages: any[]): any[] { /** * 处理自定义事件 + * 直接将 CUSTOM 事件转换为通用格式,由业务层自行处理 * @param event 自定义事件对象 - * @returns 处理结果或undefined + * @returns 处理结果 */ export function handleCustomEvent(event: any): any { if (event.name === 'suggestion') { @@ -86,7 +87,14 @@ export function handleCustomEvent(event: any): any { status: 'complete', }; } - return undefined; + return { + type: 'custom', + data: { + name: event.name, + value: event.value, + }, + status: 'complete', + }; } /** @@ -228,19 +236,64 @@ export function createToolCallContent( return createAIMessageContent(type, toolCall, status, finalStrategy); } +/** + * 创建 activity 类型的 AIMessageContent + * @param activityType 活动类型 + * @param content 活动内容 + * @param status 状态 + * @param strategy 策略 + * @param deltaInfo 增量信息(可选,用于 A2UI 等场景的增量处理) + * @returns activity 类型的 AIMessageContent + * + * type 格式: activity-${activityType} + * - activityType: 用于查找对应的渲染组件和区分不同类型的 Activity + * - 支持并行多个不同类型的 Activity + */ +export function createActivityContent( + activityType: string, + content: Record, + status: 'streaming' | 'complete' = 'complete', + strategy: 'append' | 'merge' = 'append', + deltaInfo?: { fromIndex: number; toIndex: number }, +): any { + // 使用 activity-${activityType} 格式,支持并行不同类型的 Activity + const type = `activity-${activityType}`; + + const ext = deltaInfo ? { deltaInfo } : undefined; + + return createAIMessageContent( + type, + { + activityType, + content, + }, + status, + strategy, + ext, + ); +} + /** * 创建 markdown 类型的 AIMessageContent * @param data markdown 数据 * @param status 状态 * @param strategy 策略 + * @param role 消息角色(可选,默认 assistant),通过 ext.role 传递给业务层 * @returns markdown 类型的 AIMessageContent */ export function createMarkdownContent( data: string, status: 'streaming' | 'complete' = 'complete', strategy: 'append' | 'merge' = 'append', + role?: 'assistant' | 'system', ): any { - return createAIMessageContent('markdown', data, status, strategy); + const notAssistant = role && role !== 'assistant'; + // TODO: 这里对于textchunk中设置了role的情况delta没做合并(暂时type=`${role}-text`业务自行处理) + const content = createAIMessageContent(notAssistant ? `${role}-text` : 'markdown', data, status, strategy); + if (notAssistant) { + content.ext = { ...content.ext, role }; + } + return content; } /** diff --git a/src/chat-engine/adapters/index.ts b/src/chat-engine/adapters/index.ts new file mode 100644 index 00000000..53d34e91 --- /dev/null +++ b/src/chat-engine/adapters/index.ts @@ -0,0 +1,6 @@ +/** + * Core 适配器模块导出 + */ + +// AGUI 适配器 +export * from './agui'; diff --git a/src/chat-engine/event-bus/ChatEventBus.ts b/src/chat-engine/event-bus/ChatEventBus.ts new file mode 100644 index 00000000..81b40df9 --- /dev/null +++ b/src/chat-engine/event-bus/ChatEventBus.ts @@ -0,0 +1,364 @@ +/** + * ChatEngine 事件总线实现 + */ +import type { + ChatEngineEventPayloadMap, + ChatEventBusOptions, + EventCallback, + EventFilter, + EventHistoryItem, + IChatEventBus, + UnsubscribeFn, +} from './types'; +import { ChatEngineEventType } from './types'; + +/** + * ChatEventBus - 聊天引擎事件总线 + * + * 提供统一的事件发布/订阅机制,支持: + * - 类型安全的事件订阅和发布 + * - 一次性订阅 (once) + * - Promise 方式等待事件 (waitFor) + * - 带过滤条件的等待 (waitForMatch) + * - 自定义事件支持 + * - 事件历史记录(调试用) + */ +export class ChatEventBus implements IChatEventBus { + private listeners: Map> = new Map(); + + private onceListeners: Map> = new Map(); + + private customListeners: Map> = new Map(); + + private history: EventHistoryItem[] = []; + + private options: Required; + + private destroyed = false; + + constructor(options: ChatEventBusOptions = {}) { + this.options = { + debug: false, + maxListeners: 100, + historySize: 0, + ...options, + }; + } + + /** + * 订阅事件 + */ + on(event: E, callback: EventCallback): UnsubscribeFn { + this.checkDestroyed(); + + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + + const listeners = this.listeners.get(event)!; + + // 检查监听器数量限制 + if (listeners.size >= this.options.maxListeners) { + console.warn( + `[ChatEventBus] Maximum listeners (${this.options.maxListeners}) exceeded for event: ${event}. ` + + 'Consider increasing maxListeners or removing unused listeners.', + ); + } + + listeners.add(callback as EventCallback); + + if (this.options.debug) { + console.log(`[ChatEventBus] Subscribed to event: ${event}, total listeners: ${listeners.size}`); + } + + return () => this.off(event, callback); + } + + /** + * 一次性订阅 + */ + once(event: E, callback: EventCallback): UnsubscribeFn { + this.checkDestroyed(); + + if (!this.onceListeners.has(event)) { + this.onceListeners.set(event, new Set()); + } + + this.onceListeners.get(event)!.add(callback as EventCallback); + + if (this.options.debug) { + console.log(`[ChatEventBus] Once subscribed to event: ${event}`); + } + + return () => this.onceListeners.get(event)?.delete(callback as EventCallback); + } + + /** + * 取消订阅 + */ + off(event: E, callback?: EventCallback): void { + if (callback) { + this.listeners.get(event)?.delete(callback as EventCallback); + this.onceListeners.get(event)?.delete(callback as EventCallback); + + if (this.options.debug) { + console.log(`[ChatEventBus] Unsubscribed specific callback from event: ${event}`); + } + } else { + this.listeners.delete(event); + this.onceListeners.delete(event); + + if (this.options.debug) { + console.log(`[ChatEventBus] Unsubscribed all callbacks from event: ${event}`); + } + } + } + + /** + * 发布事件 + */ + emit(event: E, payload: ChatEngineEventPayloadMap[E]): void { + if (this.destroyed) return; + + // 记录历史 + if (this.options.historySize > 0) { + this.recordHistory(event, payload); + } + + if (this.options.debug) { + console.log(`[ChatEventBus] Emitting event: ${event}`, payload); + } + + // 通知常规订阅者 + this.listeners.get(event)?.forEach((cb) => { + try { + cb(payload); + } catch (error) { + console.error(`[ChatEventBus] Error in event handler for ${event}:`, error); + } + }); + + // 通知一次性订阅者并清理 + const onceSubs = this.onceListeners.get(event); + if (onceSubs && onceSubs.size > 0) { + onceSubs.forEach((cb) => { + try { + cb(payload); + } catch (error) { + console.error(`[ChatEventBus] Error in once handler for ${event}:`, error); + } + }); + onceSubs.clear(); + } + } + + /** + * 等待事件(Promise 方式) + */ + waitFor(event: E, timeout = 30000): Promise { + this.checkDestroyed(); + + return new Promise((resolve, reject) => { + let timer: ReturnType | null = null; + + const handler = (payload: ChatEngineEventPayloadMap[E]) => { + if (timer) { + clearTimeout(timer); + } + resolve(payload); + }; + + if (timeout > 0) { + timer = setTimeout(() => { + this.onceListeners.get(event)?.delete(handler as EventCallback); + reject(new Error(`[ChatEventBus] Timeout waiting for event: ${event} (${timeout}ms)`)); + }, timeout); + } + + this.once(event, handler); + }); + } + + /** + * 带过滤条件的等待 + */ + waitForMatch( + event: E, + filter: EventFilter, + timeout = 30000, + ): Promise { + this.checkDestroyed(); + + return new Promise((resolve, reject) => { + let timer: ReturnType | null = null; + let unsubscribe: UnsubscribeFn | null = null; + + const handler = (payload: ChatEngineEventPayloadMap[E]) => { + try { + if (filter(payload)) { + if (timer) { + clearTimeout(timer); + } + if (unsubscribe) { + unsubscribe(); + } + resolve(payload); + } + } catch (error) { + console.error(`[ChatEventBus] Error in filter for ${event}:`, error); + } + }; + + if (timeout > 0) { + timer = setTimeout(() => { + if (unsubscribe) { + unsubscribe(); + } + reject(new Error(`[ChatEventBus] Timeout waiting for matching event: ${event} (${timeout}ms)`)); + }, timeout); + } + + unsubscribe = this.on(event, handler); + }); + } + + /** + * 订阅自定义事件 + */ + onCustom(eventName: string, callback: EventCallback): UnsubscribeFn { + this.checkDestroyed(); + + const key = `custom:${eventName}`; + if (!this.customListeners.has(key)) { + this.customListeners.set(key, new Set()); + } + + this.customListeners.get(key)!.add(callback as EventCallback); + + if (this.options.debug) { + console.log(`[ChatEventBus] Subscribed to custom event: ${eventName}`); + } + + return () => this.customListeners.get(key)?.delete(callback as EventCallback); + } + + /** + * 发布自定义事件 + */ + emitCustom(eventName: string, data: T): void { + if (this.destroyed) return; + + const key = `custom:${eventName}`; + + // 记录历史 + if (this.options.historySize > 0) { + this.recordHistory(ChatEngineEventType.CUSTOM, { eventName, data }); + } + + if (this.options.debug) { + console.log(`[ChatEventBus] Emitting custom event: ${eventName}`, data); + } + + this.customListeners.get(key)?.forEach((cb) => { + try { + cb(data); + } catch (error) { + console.error(`[ChatEventBus] Error in custom event handler for ${eventName}:`, error); + } + }); + } + + /** + * 获取事件历史记录 + */ + getHistory(): EventHistoryItem[] { + return [...this.history]; + } + + /** + * 清理所有订阅 + */ + clear(): void { + this.listeners.clear(); + this.onceListeners.clear(); + this.customListeners.clear(); + this.history = []; + + if (this.options.debug) { + console.log('[ChatEventBus] All subscriptions cleared'); + } + } + + /** + * 销毁事件总线 + */ + destroy(): void { + this.clear(); + this.destroyed = true; + + if (this.options.debug) { + console.log('[ChatEventBus] Event bus destroyed'); + } + } + + /** + * 获取指定事件的监听器数量 + */ + listenerCount(event: ChatEngineEventType): number { + const regular = this.listeners.get(event)?.size || 0; + const once = this.onceListeners.get(event)?.size || 0; + return regular + once; + } + + /** + * 获取所有事件的监听器数量 + */ + getTotalListenerCount(): number { + let total = 0; + this.listeners.forEach((set) => { + total += set.size; + }); + this.onceListeners.forEach((set) => { + total += set.size; + }); + this.customListeners.forEach((set) => { + total += set.size; + }); + return total; + } + + /** + * 检查是否有指定事件的监听器 + */ + hasListeners(event: ChatEngineEventType): boolean { + return this.listenerCount(event) > 0; + } + + private recordHistory(event: ChatEngineEventType | string, payload: unknown): void { + this.history.push({ + event, + payload, + timestamp: Date.now(), + }); + + // 保持历史记录在限制范围内 + while (this.history.length > this.options.historySize) { + this.history.shift(); + } + } + + private checkDestroyed(): void { + if (this.destroyed) { + throw new Error('[ChatEventBus] Event bus has been destroyed'); + } + } +} + +/** + * 创建事件总线实例 + */ +export function createEventBus(options?: ChatEventBusOptions): IChatEventBus { + return new ChatEventBus(options); +} + +export { ChatEngineEventType }; diff --git a/src/chat-engine/event-bus/index.ts b/src/chat-engine/event-bus/index.ts new file mode 100644 index 00000000..127d94ea --- /dev/null +++ b/src/chat-engine/event-bus/index.ts @@ -0,0 +1,13 @@ +/** + * 事件总线模块导出 + */ +export { ChatEventBus, createEventBus, ChatEngineEventType } from './ChatEventBus'; +export type { + ChatEngineEventPayloadMap, + ChatEventBusOptions, + EventCallback, + EventFilter, + EventHistoryItem, + IChatEventBus, + UnsubscribeFn, +} from './types'; diff --git a/src/chat-engine/event-bus/types.ts b/src/chat-engine/event-bus/types.ts new file mode 100644 index 00000000..3f2607e5 --- /dev/null +++ b/src/chat-engine/event-bus/types.ts @@ -0,0 +1,270 @@ +/** + * ChatEngine 事件总线类型定义 + */ +import type { AIMessageContent, ChatMessagesData, ChatRequestParams, ChatStatus, ToolCall } from '../type'; + +/** + * 事件类型枚举 + */ +export enum ChatEngineEventType { + // 生命周期事件 + ENGINE_INIT = 'engine:init', + ENGINE_DESTROY = 'engine:destroy', + + // 消息事件 + MESSAGE_CREATE = 'message:create', + MESSAGE_UPDATE = 'message:update', + MESSAGE_DELETE = 'message:delete', + MESSAGE_CLEAR = 'message:clear', + MESSAGE_STATUS_CHANGE = 'message:status', + MESSAGE_CONTENT_APPEND = 'message:content:append', + + // 请求事件 + REQUEST_START = 'request:start', + REQUEST_STREAM = 'request:stream', + REQUEST_COMPLETE = 'request:complete', + REQUEST_ERROR = 'request:error', + REQUEST_ABORT = 'request:abort', + + // AG-UI 协议事件 + AGUI_RUN_START = 'agui:run:start', + AGUI_RUN_COMPLETE = 'agui:run:complete', + AGUI_RUN_ERROR = 'agui:run:error', + AGUI_STATE_UPDATE = 'agui:state:update', + AGUI_TOOLCALL = 'agui:toolcall', + AGUI_ACTIVITY = 'agui:activity', + + // 自定义事件 + CUSTOM = 'custom', +} + +/** + * 事件载荷类型映射 + */ +export interface ChatEngineEventPayloadMap { + // 生命周期事件 + [ChatEngineEventType.ENGINE_INIT]: { + timestamp: number; + }; + [ChatEngineEventType.ENGINE_DESTROY]: { + timestamp: number; + }; + + // 消息事件 + [ChatEngineEventType.MESSAGE_CREATE]: { + message: ChatMessagesData; + messages: ChatMessagesData[]; + }; + [ChatEngineEventType.MESSAGE_UPDATE]: { + messageId: string; + content: AIMessageContent | AIMessageContent[]; + message: ChatMessagesData; + }; + [ChatEngineEventType.MESSAGE_DELETE]: { + messageId: string; + messages: ChatMessagesData[]; + }; + [ChatEngineEventType.MESSAGE_CLEAR]: { + timestamp: number; + }; + [ChatEngineEventType.MESSAGE_STATUS_CHANGE]: { + messageId: string; + status: ChatStatus; + previousStatus?: ChatStatus; + }; + [ChatEngineEventType.MESSAGE_CONTENT_APPEND]: { + messageId: string; + content: AIMessageContent; + contentIndex: number; + }; + + // 请求事件 + [ChatEngineEventType.REQUEST_START]: { + params: ChatRequestParams; + messageId?: string; + }; + [ChatEngineEventType.REQUEST_STREAM]: { + messageId: string; + chunk: unknown; + content?: AIMessageContent | AIMessageContent[] | null; + }; + [ChatEngineEventType.REQUEST_COMPLETE]: { + messageId: string; + params: ChatRequestParams; + message: ChatMessagesData; + }; + [ChatEngineEventType.REQUEST_ERROR]: { + messageId: string; + error: Error | unknown; + params?: ChatRequestParams; + }; + [ChatEngineEventType.REQUEST_ABORT]: { + messageId: string; + params: ChatRequestParams; + }; + + // AG-UI 协议事件 + [ChatEngineEventType.AGUI_RUN_START]: { + runId: string; + threadId?: string; + timestamp: number; + }; + [ChatEngineEventType.AGUI_RUN_COMPLETE]: { + runId: string; + threadId?: string; + timestamp: number; + }; + [ChatEngineEventType.AGUI_RUN_ERROR]: { + error: Error | unknown; + runId?: string; + }; + [ChatEngineEventType.AGUI_STATE_UPDATE]: { + stateKey: string; + state: unknown; + previousState?: unknown; + }; + [ChatEngineEventType.AGUI_TOOLCALL]: { + toolCall: ToolCall; + eventType: string; + }; + [ChatEngineEventType.AGUI_ACTIVITY]: { + activityType: string; + messageId?: string; + content: unknown; + }; + + // 自定义事件 + [ChatEngineEventType.CUSTOM]: { + eventName: string; + data: unknown; + }; +} + +/** + * 事件回调函数类型 + */ +export type EventCallback = (payload: T) => void; + +/** + * 取消订阅函数类型 + */ +export type UnsubscribeFn = () => void; + +/** + * 事件过滤器类型 + */ +export type EventFilter = (payload: T) => boolean; + +/** + * 事件总线配置 + */ +export interface ChatEventBusOptions { + /** + * 是否启用调试模式 + * @default false + */ + debug?: boolean; + /** + * 最大监听器数量(每个事件) + * @default 100 + */ + maxListeners?: number; + /** + * 事件历史记录长度(用于调试) + * @default 0 + */ + historySize?: number; +} + +/** + * 事件历史记录 + */ +export interface EventHistoryItem { + event: ChatEngineEventType | string; + payload: unknown; + timestamp: number; +} + +/** + * 事件总线接口 + */ +export interface IChatEventBus { + /** + * 订阅事件 + * @param event 事件类型 + * @param callback 回调函数 + * @returns 取消订阅函数 + */ + on(event: E, callback: EventCallback): UnsubscribeFn; + + /** + * 一次性订阅 + * @param event 事件类型 + * @param callback 回调函数 + * @returns 取消订阅函数 + */ + once(event: E, callback: EventCallback): UnsubscribeFn; + + /** + * 取消订阅 + * @param event 事件类型 + * @param callback 可选的回调函数,不传则取消该事件所有订阅 + */ + off(event: E, callback?: EventCallback): void; + + /** + * 发布事件 + * @param event 事件类型 + * @param payload 事件载荷 + */ + emit(event: E, payload: ChatEngineEventPayloadMap[E]): void; + + /** + * 等待事件(Promise 方式) + * @param event 事件类型 + * @param timeout 超时时间(毫秒) + * @returns Promise + */ + waitFor(event: E, timeout?: number): Promise; + + /** + * 带过滤条件的等待 + * @param event 事件类型 + * @param filter 过滤函数 + * @param timeout 超时时间 + */ + waitForMatch( + event: E, + filter: EventFilter, + timeout?: number, + ): Promise; + + /** + * 订阅自定义事件 + * @param eventName 自定义事件名 + * @param callback 回调函数 + */ + onCustom(eventName: string, callback: EventCallback): UnsubscribeFn; + + /** + * 发布自定义事件 + * @param eventName 自定义事件名 + * @param data 数据 + */ + emitCustom(eventName: string, data: T): void; + + /** + * 获取事件历史记录 + */ + getHistory(): EventHistoryItem[]; + + /** + * 清理所有订阅 + */ + clear(): void; + + /** + * 销毁事件总线 + */ + destroy(): void; +} diff --git a/src/chat-engine/index.ts b/src/chat-engine/index.ts index cec5b64c..2be50137 100644 --- a/src/chat-engine/index.ts +++ b/src/chat-engine/index.ts @@ -2,6 +2,8 @@ /* eslint-disable class-methods-use-this */ import { AGUIAdapter } from './adapters/agui'; import { MessageStore } from './store/message'; +import type { ChatEventBusOptions, IChatEventBus } from './event-bus'; +import { ChatEngineEventType, ChatEventBus } from './event-bus'; import MessageProcessor from './processor'; import { LLMService } from './server'; import type { @@ -23,11 +25,17 @@ import { isAIMessage } from './utils'; export default class ChatEngine implements IChatEngine { public messageStore: MessageStore; + /** + * 事件总线实例 + * @description 用于发布/订阅引擎事件,支持无 UI 场景下的事件分发 + */ + public readonly eventBus: IChatEventBus; + private messageProcessor: MessageProcessor; private llmService!: LLMService; - private configSetter!: ChatServiceConfigSetter; + private config!: ChatServiceConfig; private lastRequestParams: ChatRequestParams | undefined; @@ -35,9 +43,10 @@ export default class ChatEngine implements IChatEngine { private aguiAdapter: AGUIAdapter | null = null; - constructor() { + constructor(eventBusOptions?: ChatEventBusOptions) { this.messageProcessor = new MessageProcessor(); this.messageStore = new MessageStore(); + this.eventBus = new ChatEventBus(eventBusOptions); } /** @@ -58,27 +67,28 @@ export default class ChatEngine implements IChatEngine { return this.messages.at(-1)?.status || 'idle'; } - /** - * 获取当前配置 - * 如果 configSetter 是函数,每次调用都会执行函数获取最新配置 - */ - private get config(): ChatServiceConfig { - return typeof this.configSetter === 'function' ? this.configSetter() : this.configSetter || {}; - } - /** * 销毁聊天引擎实例 * @description 中止请求,清理消息存储和适配器,释放资源 */ destroy(): void { + // 发布销毁事件 + this.eventBus.emit(ChatEngineEventType.ENGINE_DESTROY, { + timestamp: Date.now(), + }); + // 中止当前请求 this.abortChat(); // 清理消息存储 this.messageStore.clearHistory(); + this.messageStore.destroy(); // 清理适配器 this.aguiAdapter = null; + + // 销毁事件总线 + this.eventBus.destroy(); } /** @@ -89,12 +99,18 @@ export default class ChatEngine implements IChatEngine { */ public init(configSetter: ChatServiceConfigSetter, initialMessages?: ChatMessagesData[]) { this.messageStore.initialize(this.convertMessages(initialMessages)); - this.configSetter = configSetter; + this.config = typeof configSetter === 'function' ? configSetter() : configSetter || {}; this.llmService = new LLMService(); - // 初始化AGUI适配器(使用初始配置判断协议类型) + + // 初始化AGUI适配器 if (this.config.protocol === 'agui') { this.aguiAdapter = new AGUIAdapter(); } + + // 发布初始化事件 + this.eventBus.emit(ChatEngineEventType.ENGINE_INIT, { + timestamp: Date.now(), + }); } /** @@ -119,6 +135,13 @@ export default class ChatEngine implements IChatEngine { const userMessage = this.messageProcessor.createUserMessage(prompt, attachments); const aiMessage = this.messageProcessor.createAssistantMessage(); this.messageStore.createMultiMessages([userMessage, aiMessage]); + + // 发布消息创建事件 + this.eventBus.emit(ChatEngineEventType.MESSAGE_CREATE, { + message: userMessage, + messages: this.messages, + }); + if (sendRequest) { const params = { ...requestParams, @@ -135,7 +158,7 @@ export default class ChatEngine implements IChatEngine { * @description 创建并存储一条系统消息,通常用于设置上下文或控制对话流程 */ public async sendSystemMessage(msg: string) { - this.messageStore.createMessage({ + const systemMessage = { role: 'system', content: [ { @@ -143,7 +166,14 @@ export default class ChatEngine implements IChatEngine { data: msg, }, ], - } as SystemMessage); + } as SystemMessage; + this.messageStore.createMessage(systemMessage); + + // 发布消息创建事件 + this.eventBus.emit(ChatEngineEventType.MESSAGE_CREATE, { + message: systemMessage, + messages: this.messages, + }); } /** @@ -160,6 +190,13 @@ export default class ChatEngine implements IChatEngine { status: sendRequest ? 'pending' : 'complete', }); this.messageStore.createMessage(newAIMessage); + + // 发布消息创建事件 + this.eventBus.emit(ChatEngineEventType.MESSAGE_CREATE, { + message: newAIMessage, + messages: this.messages, + }); + if (sendRequest) { params.messageID = newAIMessage.id; await this.sendRequest(params); @@ -182,7 +219,14 @@ export default class ChatEngine implements IChatEngine { if (!this.config.stream) { // 只有在批量模式下才删除最后一条AI消息 if (this.messageStore.lastAIMessage?.id) { - this.messageStore.removeMessage(this.messageStore.lastAIMessage.id); + const messageId = this.messageStore.lastAIMessage.id; + this.messageStore.removeMessage(messageId); + + // 发布消息删除事件 + this.eventBus.emit(ChatEngineEventType.MESSAGE_DELETE, { + messageId, + messages: this.messages, + }); } } } catch (error) { @@ -219,6 +263,11 @@ export default class ChatEngine implements IChatEngine { */ public clearMessages(): void { this.messageStore.clearHistory(); + + // 发布消息清空事件 + this.eventBus.emit(ChatEngineEventType.MESSAGE_CLEAR, { + timestamp: Date.now(), + }); } /** @@ -261,6 +310,13 @@ export default class ChatEngine implements IChatEngine { */ public async sendRequest(params: ChatRequestParams) { const { messageID: id } = params; + + // 发布请求开始事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_START, { + params, + messageId: id, + }); + try { if (this.config.stream) { // 处理sse流式响应模式 @@ -273,6 +329,14 @@ export default class ChatEngine implements IChatEngine { this.lastRequestParams = params; } catch (error) { this.setMessageStatus(id!, 'error'); + + // 发布请求错误事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_ERROR, { + messageId: id!, + error, + params, + }); + throw error; } } @@ -295,33 +359,74 @@ export default class ChatEngine implements IChatEngine { if (result) { this.processMessageResult(id, result); this.setMessageStatus(id, 'complete'); + + // 发布请求完成事件 + const message = this.messageStore.getMessageByID(id); + if (message) { + this.eventBus.emit(ChatEngineEventType.REQUEST_COMPLETE, { + messageId: id, + params, + message, + }); + } } else { this.setMessageStatus(id, 'error'); + + // 发布请求错误事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_ERROR, { + messageId: id, + error: new Error('Batch request returned empty result'), + params, + }); } } - private handleError(id: string, error: any) { + private handleError(id: string, error: unknown) { this.setMessageStatus(id, 'error'); - this.config.onError?.(error); + this.config.onError?.(error as Error); + + // 发布请求错误事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_ERROR, { + messageId: id, + error, + }); } - private handleComplete(id: string, isAborted: boolean, params: ChatRequestParams, chunk?: any) { + private handleComplete(id: string, isAborted: boolean, params: ChatRequestParams, chunk?: unknown) { // 先调用用户自定义的 onComplete 回调,让业务层决定如何处理 const customResult = this.config.onComplete?.(isAborted, params, chunk); // 如果用户返回了自定义内容,处理这些内容 if (Array.isArray(customResult) || (customResult && 'status' in customResult)) { this.processMessageResult(id, customResult); } else { - // 所有消息内容块都失败才算消息体失败 - const allContentFailed = this.messageStore.messages.every((content) => content.status === 'error'); + // 所有消息内容块检查,只要有一个失败,消息的status就是失败 + const allContentFailed = this.messageStore.messages.find((content) => content.status === 'error'); // eslint-disable-next-line no-nested-ternary this.setMessageStatus(id, isAborted ? 'stop' : allContentFailed ? 'error' : 'complete'); } + + // 发布请求完成/中止事件 + const message = this.messageStore.getMessageByID(id); + if (message) { + if (isAborted) { + this.eventBus.emit(ChatEngineEventType.REQUEST_ABORT, { + messageId: id, + params, + }); + } else { + this.eventBus.emit(ChatEngineEventType.REQUEST_COMPLETE, { + messageId: id, + params, + message, + }); + } + } } /** * 处理流式请求 * 根据协议类型选择不同的处理策略 + * SSE 原始数据 → isValidChunk验证 → onMessage解析 → processMessageResult */ private async handleStreamRequest(params: ChatRequestParams) { const id = params.messageID; @@ -348,15 +453,41 @@ export default class ChatEngine implements IChatEngine { onMessage: (chunk: SSEChunkData) => { if (this.stopReceive || !messageId) return null; - let result; + let result: AIMessageContent | AIMessageContent[] | null = null; // SSE数据 → AGUIEventMapper.mapEvent → 用户自定义onMessage(解析后数据 + 原始chunk) // 首先使用AGUI适配器进行通用协议解析 if (this.aguiAdapter) { result = this.aguiAdapter.handleAGUIEvent(chunk, { - onRunStart: (event) => this.config.onStart?.(JSON.stringify(event)), - onRunComplete: (isAborted, params, event) => this.handleComplete(messageId, isAborted, params, event), - onRunError: (error) => this.handleError(messageId, error), + onRunStart: (event) => { + // 重置适配器状态,确保新一轮对话从干净状态开始 + this.aguiAdapter?.reset(); + this.config.onStart?.(JSON.stringify(event)); + // 发布 AGUI 运行开始事件 + this.eventBus.emit(ChatEngineEventType.AGUI_RUN_START, { + runId: event.runId || '', + threadId: event.threadId, + timestamp: Date.now(), + }); + }, + onRunComplete: (isAborted, requestParams, event) => { + this.handleComplete(messageId, isAborted, requestParams, event); + // 发布 AGUI 运行完成事件 + if (!isAborted) { + this.eventBus.emit(ChatEngineEventType.AGUI_RUN_COMPLETE, { + runId: event?.runId || '', + threadId: event?.threadId, + timestamp: Date.now(), + }); + } + }, + onRunError: (error) => { + this.handleError(messageId, error); + // 发布 AGUI 运行错误事件 + this.eventBus.emit(ChatEngineEventType.AGUI_RUN_ERROR, { + error, + }); + }, }); } @@ -368,6 +499,14 @@ export default class ChatEngine implements IChatEngine { result = userResult; } } + + // 发布流数据事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_STREAM, { + messageId, + chunk, + content: result, + }); + // 处理消息结果 this.processMessageResult(messageId, result); return result; @@ -403,6 +542,13 @@ export default class ChatEngine implements IChatEngine { result = this.config.onMessage(chunk, this.messageStore.getMessageByID(messageId)); } + // 发布流数据事件 + this.eventBus.emit(ChatEngineEventType.REQUEST_STREAM, { + messageId, + chunk, + content: result, + }); + // 处理消息结果 this.processMessageResult(messageId, result); return result; @@ -425,20 +571,51 @@ export default class ChatEngine implements IChatEngine { private processMessageResult(messageId: string, result: AIMessageContent | AIMessageContent[] | null) { if (!result) return; - // 处理工具调用 - 仅在AGUI协议下处理 - // if (this.aguiAdapter) { - // const toolCalls = this.aguiAdapter.processToolCalls(result); - // if (toolCalls.length > 0) { - // this.messageStore.setMessageToolCalls(messageId, toolCalls); - // } - // } - - if (Array.isArray(result)) { - // 处理多个内容块 - this.messageStore.updateMultipleContents(messageId, result); - } else { - // 处理单个内容块 - this.processContentUpdate(messageId, result); + // 根据原始类型选择更新方式 + Array.isArray(result) + ? this.messageStore.updateMultipleContents(messageId, result) + : this.processContentUpdate(messageId, result); + + // 发布消息更新事件 + const message = this.messageStore.getMessageByID(messageId); + if (message) { + this.eventBus.emit(ChatEngineEventType.MESSAGE_UPDATE, { + messageId, + content: result, + message, + }); + + // AG-UI 协议:发布细粒度事件 + if (this.aguiAdapter) { + this.emitAGUIDetailEvents(messageId, result); + } + } + } + + /** + * 发布 AG-UI 细粒度事件 + * 根据内容类型分发到对应的事件通道 + * todo: 这里的类型 any问题 + */ + private emitAGUIDetailEvents(messageId: string, result: AIMessageContent | AIMessageContent[]) { + const contents = Array.isArray(result) ? result : [result]; + for (const content of contents) { + // Activity 事件 + if ((content as any).data.activityType) { + this.eventBus.emit(ChatEngineEventType.AGUI_ACTIVITY, { + activityType: (content as any).data.activityType, + messageId, + content: (content as any)?.data?.content, + }); + } + + // ToolCall 事件 + if ((content as any)?.data?.eventType?.startsWith('TOOL_CALL')) { + this.eventBus.emit(ChatEngineEventType.AGUI_TOOLCALL, { + toolCall: (content as any).data, + eventType: (content as any).data.eventType, + }); + } } } @@ -452,7 +629,17 @@ export default class ChatEngine implements IChatEngine { } private setMessageStatus(messageId: string, status: ChatMessagesData['status']) { + const previousStatus = this.messageStore.getMessageByID(messageId)?.status; this.messageStore.setMessageStatus(messageId, status); + + // 发布消息状态变化事件 + if (status) { + this.eventBus.emit(ChatEngineEventType.MESSAGE_STATUS_CHANGE, { + messageId, + status, + previousStatus, + }); + } } // 处理内容更新逻辑 @@ -460,14 +647,9 @@ export default class ChatEngine implements IChatEngine { const message = this.messageStore.messages.find((m) => m.id === messageId); if (!message || !isAIMessage(message) || !message.content) return; - // // 只需要处理最后一个内容快 - // const lastContent = message.content.at(-1); - // const processed = this.messageProcessor.processContentUpdate(lastContent, rawChunk); - // this.messageStore.appendContent(messageId, processed); - let targetIndex: number; // 作为新的内容块追加 - if (rawChunk?.strategy === 'append') { + if ((rawChunk as any)?.strategy === 'append') { targetIndex = -1; } else { // merge 策略:按 type 查找最后一个匹配的类型 @@ -493,5 +675,7 @@ export default class ChatEngine implements IChatEngine { } export * from './utils'; -export * from './adapters/agui'; +export * from './adapters'; +export * from './event-bus'; +export * from './adapters'; export type * from './type'; diff --git a/src/chat-engine/server/llm-service.ts b/src/chat-engine/server/llm-service.ts index 43e2a50e..f6e16e42 100644 --- a/src/chat-engine/server/llm-service.ts +++ b/src/chat-engine/server/llm-service.ts @@ -87,7 +87,10 @@ export class LLMService implements ILLMService { }); this.sseClient.on('message', (msg) => { - config.onMessage?.(msg as SSEChunkData); + const chunk = msg as SSEChunkData; + // 如果配置了 isValidChunk 且返回 false,则跳过该 chunk + if (config.isValidChunk && !config.isValidChunk(chunk)) return; + config.onMessage?.(chunk); }); this.sseClient.on('error', (error) => { diff --git a/src/chat-engine/store/message.ts b/src/chat-engine/store/message.ts index 32106d3d..9891e557 100644 --- a/src/chat-engine/store/message.ts +++ b/src/chat-engine/store/message.ts @@ -90,7 +90,7 @@ export class MessageStore extends ReactiveState { if (isAIMessage(message) && message.content && message.content.length > 0) { const lastContent = message.content[message.content.length - 1]; // 添加类型检查,确保content有status属性 - if ('status' in lastContent) { + if ('status' in lastContent && lastContent.status !== 'error') { lastContent.status = status; } } diff --git a/src/chat-engine/store/reactiveState.ts b/src/chat-engine/store/reactiveState.ts index c779c228..8eecadf7 100644 --- a/src/chat-engine/store/reactiveState.ts +++ b/src/chat-engine/store/reactiveState.ts @@ -129,6 +129,16 @@ export default class ReactiveState { }); } + /** + * 销毁方法,清理资源 + */ + public destroy() { + this.subscribers.clear(); + this.pathSubscribers.clear(); + this.pendingChanges = []; + this.notificationScheduled = false; + } + /** * 调试方法(开发时使用) * @param label 调试标签(默认'State') diff --git a/src/chat-engine/type.ts b/src/chat-engine/type.ts index 9aac1e91..c2315a8b 100644 --- a/src/chat-engine/type.ts +++ b/src/chat-engine/type.ts @@ -1,4 +1,4 @@ -import type { ToolCallEventType } from './adapters/agui/events'; +import type { ToolCallEventType } from './adapters/agui/types/events'; export type ChatMessageRole = 'user' | 'assistant' | 'system'; export type ChatMessageStatus = 'pending' | 'streaming' | 'complete' | 'stop' | 'error'; @@ -14,7 +14,8 @@ export type ChatContentType = | 'video' | 'suggestion' | 'reasoning' - | 'toolcall'; + | 'toolcall' + | 'activity'; export type AttachmentType = 'image' | 'video' | 'audio' | 'pdf' | 'doc' | 'ppt' | 'txt'; @@ -106,6 +107,20 @@ export type ToolCall = { export type ToolCallContent = ChatBaseContent<'toolcall', ToolCall>; +// Activity 内容 +export type ActivityData> = { + activityType: string; + messageId?: string; + content: TContent; + /** 增量更新信息 */ + deltaInfo?: { + fromIndex: number; + toIndex: number; + }; +}; + +export type ActivityContent> = ChatBaseContent<'activity', ActivityData>; + // 消息主体 // 基础消息结构 @@ -131,6 +146,7 @@ type AIContentTypeMap = { suggestion: SuggestionContent; reasoning: ReasoningContent; toolcall: ToolCallContent; + activity: ActivityContent; } & AIContentTypeOverrides; export type AIContentType = keyof AIContentTypeMap; @@ -199,6 +215,25 @@ export interface DefaultEngineCallbacks { params: ChatRequestParams, ) => (ChatRequestParams & RequestInit) | Promise; onStart?: (chunk: string) => void; + /** + * Chunk 验证器 - 用于验证 SSE 数据块是否有效 + * + * 当后端返回空内容或无效数据时,可以通过此配置判断 chunk 是否有效, + * 只有有效的 chunk 才会被处理,避免过早进入 streaming 状态。 + * + * @param chunk SSE 数据块 + * @returns 返回 true 表示该 chunk 有效(需要处理),返回 false 表示无效(跳过) + * + * @example + * ```ts + * // 只处理 content 不为空的 chunk + * isValidChunk: (chunk) => { + * const content = chunk.data?.choices?.[0]?.delta?.content; + * return content !== '' && content !== undefined; + * } + * ``` + */ + isValidChunk?: (chunk: SSEChunkData) => boolean; /** 接收到消息数据块 - 用于解析和处理聊天内容 */ onMessage?: ( chunk: SSEChunkData, diff --git a/src/chat-engine/utils/immutable-patch.ts b/src/chat-engine/utils/immutable-patch.ts new file mode 100644 index 00000000..7e4cbb12 --- /dev/null +++ b/src/chat-engine/utils/immutable-patch.ts @@ -0,0 +1,191 @@ +/* eslint-disable */ +/** + * Immutable JSON Patch with Structural Sharing + * + * 核心特性: + * - 只重建被修改路径上的节点 + * - 未修改的节点保持原引用(实现结构共享) + * - 支持标准 JSON Patch 操作:add, remove, replace, move, copy + * - 支持扩展操作:append(字符串追加) + * + * 性能优势: + * - 配合 React.memo / useSyncExternalStore 使用时,未变化的组件不会重渲染 + * - 避免深拷贝带来的性能开销 + */ + +export type Operation = + | { op: 'add'; path: string; value: any } + | { op: 'remove'; path: string } + | { op: 'replace'; path: string; value: any } + | { op: 'move'; path: string; from: string } + | { op: 'copy'; path: string; from: string } + | { op: 'append'; path: string; value: string }; + +/** + * 解析 JSON Pointer 路径 + * "/elements/deep-progress/props/percentage" => ["elements", "deep-progress", "props", "percentage"] + */ +function parsePath(path: string): string[] { + if (path === '' || path === '/') return []; + return path + .split('/') + .slice(1) + .map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~')); +} + +/** + * 获取嵌套值 + */ +function getByPath(obj: any, path: string[]): any { + let current = obj; + for (const key of path) { + if (current == null) return undefined; + current = current[key]; + } + return current; +} + +/** + * 不可变地设置嵌套值(结构共享) + * 只重建路径上的节点,其他节点保持原引用 + */ +function setByPath(obj: T, path: string[], value: any): T { + if (path.length === 0) { + return value; + } + + const [head, ...tail] = path; + const current = obj as any; + + // 判断当前层是数组还是对象 + const isArray = Array.isArray(current); + // eslint-disable-next-line no-nested-ternary + const index = isArray ? (head === '-' ? current.length : parseInt(head, 10)) : head; + + if (tail.length === 0) { + // 到达目标位置,设置值 + if (isArray) { + const newArr = [...current]; + newArr[index as number] = value; + return newArr as unknown as T; + } + return { ...current, [head]: value }; + } + + // 递归处理子路径 + const child = current[head]; + const newChild = setByPath(child, tail, value); + + if (isArray) { + const newArr = [...current]; + newArr[index as number] = newChild; + return newArr as unknown as T; + } + return { ...current, [head]: newChild }; +} + +/** + * 不可变地删除嵌套值(结构共享) + */ +function removeByPath(obj: T, path: string[]): T { + if (path.length === 0) { + return undefined as unknown as T; + } + + const [head, ...tail] = path; + const current = obj as any; + const isArray = Array.isArray(current); + + if (tail.length === 0) { + // 到达目标位置,删除值 + if (isArray) { + const index = parseInt(head, 10); + const newArr = [...current]; + newArr.splice(index, 1); + return newArr as unknown as T; + } + const { [head]: _, ...rest } = current; + return rest as T; + } + + // 递归处理子路径 + const child = current[head]; + const newChild = removeByPath(child, tail); + + if (isArray) { + const index = parseInt(head, 10); + const newArr = [...current]; + newArr[index] = newChild; + return newArr as unknown as T; + } + return { ...current, [head]: newChild }; +} + +/** + * 应用单个操作(结构共享) + */ +function applyOperationImmutable(document: T, operation: Operation): T { + const path = parsePath(operation.path); + + switch (operation.op) { + case 'add': + case 'replace': + return setByPath(document, path, operation.value); + + case 'remove': + return removeByPath(document, path); + + case 'append': { + const existing = getByPath(document, path); + const newValue = existing == null ? operation.value : String(existing) + operation.value; + return setByPath(document, path, newValue); + } + + case 'move': { + const fromPath = parsePath(operation.from); + const value = getByPath(document, fromPath); + const afterRemove = removeByPath(document, fromPath); + return setByPath(afterRemove, path, value); + } + + case 'copy': { + const fromPath = parsePath(operation.from); + const value = getByPath(document, fromPath); + // 深拷贝 copy 的值,避免共享引用 + return setByPath(document, path, JSON.parse(JSON.stringify(value))); + } + + default: + return document; + } +} + +/** + * 应用 JSON Patch 数组(结构共享版本) + * + * @param document 原始文档 + * @param patch 操作数组 + * @returns 新文档(未修改的节点保持原引用) + * + * @example + * const tree = { elements: { a: { props: { x: 1 } }, b: { props: { y: 2 } } } }; + * const newTree = applyPatchImmutable(tree, [ + * { op: 'replace', path: '/elements/a/props/x', value: 10 } + * ]); + * + * // 结构共享验证: + * tree.elements.b === newTree.elements.b // true - b 节点未变,保持原引用 + * tree.elements.a === newTree.elements.a // false - a 节点被修改,是新引用 + */ +export function applyPatchImmutable(document: T, patch: Operation[]): T { + return patch.reduce((doc, op) => applyOperationImmutable(doc, op), document); +} + +/** + * 兼容原有 applyPatch 的返回格式 + */ +export function applyPatch(document: T, patch: Operation[]): { newDocument: T } { + return { + newDocument: applyPatchImmutable(document, patch), + }; +} diff --git a/src/chat-engine/utils/index.ts b/src/chat-engine/utils/index.ts index fcd695d1..257e9439 100644 --- a/src/chat-engine/utils/index.ts +++ b/src/chat-engine/utils/index.ts @@ -1,6 +1,5 @@ -import { applyPatch } from 'fast-json-patch'; - import type { + ActivityContent, AIMessageContent, AttachmentContent, ChatMessagesData, @@ -13,24 +12,26 @@ import type { ToolCallContent, UserMessageContent, } from '../type'; +import { applyPatchImmutable, type Operation } from './immutable-patch'; /** * 应用JSON Patch操作到状态对象 - * 使用fast-json-patch库实现RFC6902规范的JSON Patch操作 + * + * 使用 Immutable Patch + Structural Sharing: + * - 只重建被修改路径上的节点 + * - 未修改的节点保持原引用 + * - 配合 React.memo 使用时,未变化的组件不会重渲染 + * * @param state 原始状态对象 * @param delta 包含patch操作的数组 - * @returns 更新后的新状态对象 + * @returns 更新后的新状态对象(结构共享) */ export function applyJsonPatch(state: any, delta: any[]): any { try { - // 使用fast-json-patch库应用补丁 - // applyPatch会自动深拷贝原始对象,避免直接修改 - const result = applyPatch(state, delta, false, false); - return result.newDocument; + return applyPatchImmutable(state, delta as Operation[]); } catch (error) { console.warn('JSON Patch操作失败,返回原始状态:', error); - // 如果应用补丁失败,返回原始状态的深拷贝 - return JSON.parse(JSON.stringify(state)); + return state; } } @@ -115,7 +116,11 @@ export function isAttachmentContent(content: UserMessageContent): content is Att } export function isToolCallContent(content: AIMessageContent): content is ToolCallContent { - return content.type.startsWith('toolcall'); + return content.type === 'toolcall' || content.type.startsWith('toolcall-'); +} + +export function isActivityContent(content: AIMessageContent): content is ActivityContent { + return content.type === 'activity' || content.type.startsWith('activity-'); } export function isReasoningContent(content) { diff --git a/src/chat-message/_example/attachment.tsx b/src/chat-message/_example/attachment.tsx index 8f91d22f..df0b9dab 100644 --- a/src/chat-message/_example/attachment.tsx +++ b/src/chat-message/_example/attachment.tsx @@ -36,6 +36,34 @@ export default class BasicExample extends Component { console.log('fileclick', e.detail); }} > + { + console.log('我是自定义点击逻辑', e.detail); + }, + }, + }} + > ); } diff --git a/src/chat-message/chat-item.tsx b/src/chat-message/chat-item.tsx index 62a8fb87..b9ca36ac 100644 --- a/src/chat-message/chat-item.tsx +++ b/src/chat-message/chat-item.tsx @@ -243,7 +243,11 @@ export default class ChatItem extends Component { // 用户消息渲染 if (role === 'user') { if (isAttachmentContent(content)) { - return renderAttachments({ key: elementKey, content: content.data }, this); + return ( + + {renderAttachments({ content: content.data, ...chatContentProps?.attachments })} + + ); } if (isTextContent(content) || isMarkdownContent(content)) { return ( diff --git a/src/chat-message/content/attachment-content.tsx b/src/chat-message/content/attachment-content.tsx index 47f4a5a4..74fd0689 100644 --- a/src/chat-message/content/attachment-content.tsx +++ b/src/chat-message/content/attachment-content.tsx @@ -11,21 +11,18 @@ import styles from '../style/chat-item.less'; const className = `${getClassPrefix()}-chat__item`; export type TdChatAttachmentContentProps = { - key?: string; content?: AttachmentItem[]; onFileClick?: (event: CustomEvent) => void; }; // 纯函数渲染器 -export const renderAttachments = ({ key, content }: TdChatAttachmentContentProps, self: any) => ( -
+export const renderAttachments = ({ content, onFileClick }: TdChatAttachmentContentProps) => ( +
{ - self.fire('fileClick', e.detail, { composed: true }); - }} + onFileClick={onFileClick} style={{ '--td-attachment-image-width': '160px', '--td-attachment-image-height': '160px' }} />
@@ -41,8 +38,12 @@ export default class AttachmentContentComponent extends Component { + this.fire('fileClick', event.detail, { composed: true }); + }; + render(props) { if (!props?.content) return; - return renderAttachments(props, this); + return renderAttachments({ ...props, onFileClick: this.onFileClick }); } } diff --git a/src/chat-message/content/markdown-content.tsx b/src/chat-message/content/markdown-content.tsx index d3c5f669..54e092aa 100644 --- a/src/chat-message/content/markdown-content.tsx +++ b/src/chat-message/content/markdown-content.tsx @@ -1,8 +1,6 @@ -import '../md/chat-md-code'; - import MermaidPlugin from 'cherry-markdown/dist/addons/cherry-code-block-mermaid-plugin.esm.js'; import CherryStream from 'cherry-markdown/dist/cherry-markdown.stream.esm.js'; -import { escape, merge } from 'lodash-es'; +import { merge } from 'lodash-es'; import { Component, createRef, signal, tag } from 'omi'; import { getClassPrefix } from '../../_util/classname'; @@ -10,8 +8,6 @@ import { setExportparts } from '../../_util/dom'; import { AddPartHook } from '../md/utils'; import styles from '../style/chat-content.less'; -// 单独用该组件时,发现动态加载样式不生效,目前直接引入 -import codeStyles from '../style/md/chat-md-code.less'; // 从 CherryStream 类的构造函数参数推断类型 type CherryOptions = ConstructorParameters[0]; @@ -38,13 +34,27 @@ export interface TdChatContentMDPresetConfig { options?: any; } +/** cherry-md中支持的代码块风格 */ +type CherryCodeBlockTheme = + | 'coy' + | 'dark' + | 'default' + | 'funky' + | 'okaidia' + | 'one-dark' + | 'one-light' + | 'solarized-light' + | 'twilight' + | 'vs-dark' + | 'vs-light'; + export type TdChatContentMDPluginConfig = /** 预设插件配置 */ TdChatContentMDPresetConfig; export type TdChatContentMDOptions = Omit & { themeSettings?: { - codeBlockTheme?: 'light' | 'dark'; + codeBlockTheme?: 'light' | 'dark' | CherryCodeBlockTheme; }; }; @@ -55,7 +65,7 @@ export interface TdChatMarkdownContentProps { @tag('t-chat-md-content') export default class ChatCherryMDContent extends Component { - static css = [styles, codeStyles]; + static css = [styles]; static propTypes = { content: String, @@ -86,13 +96,10 @@ export default class ChatCherryMDContent extends Component - ``, - }, - }, + wrap: false, + lineNumber: false, + copyCode: true, + editCode: false, }, }, customSyntax: { @@ -111,7 +118,8 @@ export default class ChatCherryMDContent extends Component { this.isMarkdownInit.value = false; - const customCodeBlock = this.props.options.engine?.syntax?.codeBlock; - const customCodeBlockRenderer = typeof customCodeBlock === 'object' ? customCodeBlock?.customRenderer : undefined; - - const defaultCodeBlockRenderer = { - // 自定义语法渲染器 - all: { - render: (code, _sign, _cherry, lang) => - ``, - }, - }; + const md = new CherryStream({ ...this.markdownOptions, + themeSettings: { + ...this.markdownOptions.themeSettings, + // 兼容老版本light/dark风格 + codeBlockTheme: (() => { + if (this.markdownOptions.themeSettings?.codeBlockTheme === 'dark') { + return 'one-dark'; + } + if (this.markdownOptions.themeSettings?.codeBlockTheme === 'light') { + return 'one-light'; + } + return this.markdownOptions.themeSettings?.codeBlockTheme || 'one-light'; + })(), + }, engine: { ...this.markdownOptions.engine, syntax: { ...this.markdownOptions.engine?.syntax, - codeBlock: { - customRenderer: { ...defaultCodeBlockRenderer, ...customCodeBlockRenderer }, - }, }, }, el: this.mdRef.current, diff --git a/src/chat-message/md/chat-md-code.tsx b/src/chat-message/md/chat-md-code.tsx deleted file mode 100644 index cfc92aa9..00000000 --- a/src/chat-message/md/chat-md-code.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import 'tdesign-icons-web-components/esm/components/file-copy'; -import '../../message'; - -import hljs from 'highlight.js/lib/core'; -// 只导入常用语言包 -import bash from 'highlight.js/lib/languages/bash'; -import cpp from 'highlight.js/lib/languages/cpp'; -import csharp from 'highlight.js/lib/languages/csharp'; -import css from 'highlight.js/lib/languages/css'; -import go from 'highlight.js/lib/languages/go'; -import java from 'highlight.js/lib/languages/java'; -import javascript from 'highlight.js/lib/languages/javascript'; -import json from 'highlight.js/lib/languages/json'; -import kotlin from 'highlight.js/lib/languages/kotlin'; -import less from 'highlight.js/lib/languages/less'; -import markdown from 'highlight.js/lib/languages/markdown'; -import php from 'highlight.js/lib/languages/php'; -import python from 'highlight.js/lib/languages/python'; -import ruby from 'highlight.js/lib/languages/ruby'; -import rust from 'highlight.js/lib/languages/rust'; -import scss from 'highlight.js/lib/languages/scss'; -import shell from 'highlight.js/lib/languages/shell'; -import sql from 'highlight.js/lib/languages/sql'; -import swift from 'highlight.js/lib/languages/swift'; -import typescript from 'highlight.js/lib/languages/typescript'; -import xml from 'highlight.js/lib/languages/xml'; -import yaml from 'highlight.js/lib/languages/yaml'; -import { escape } from 'lodash-es'; -import { Component, tag } from 'omi'; - -import classname, { getClassPrefix } from '../../_util/classname'; -import type { TdChatCodeProps } from '../../chatbot/type'; -import { MessagePlugin } from '../../message'; - -const className = `${getClassPrefix()}-chat__text__markdown__code`; - -@tag('t-chat-md-code') -export default class ChatMDCode extends Component { - // lightDOM组件无法直接挂styleSheet,样式由chat-content加载 - static isLightDOM = true; - - static propTypes = { - // cherryMarkdown会将dom自定义属性自动添加data- - 'data-lang': String, - 'data-code': String, - 'data-theme': String, - }; - - msgInstance = null; - - codeHTML = null; - - constructor() { - super(); - // 注册语言包 - hljs.registerLanguage('javascript', javascript); - hljs.registerLanguage('typescript', typescript); - hljs.registerLanguage('python', python); - hljs.registerLanguage('java', java); - hljs.registerLanguage('cpp', cpp); - hljs.registerLanguage('c++', cpp); - hljs.registerLanguage('c', cpp); - hljs.registerLanguage('csharp', csharp); - hljs.registerLanguage('cs', csharp); - hljs.registerLanguage('php', php); - hljs.registerLanguage('ruby', ruby); - hljs.registerLanguage('go', go); - hljs.registerLanguage('rust', rust); - hljs.registerLanguage('swift', swift); - hljs.registerLanguage('kotlin', kotlin); - hljs.registerLanguage('sql', sql); - hljs.registerLanguage('shell', shell); - hljs.registerLanguage('sh', shell); - hljs.registerLanguage('bash', bash); - hljs.registerLanguage('json', json); - hljs.registerLanguage('xml', xml); - hljs.registerLanguage('html', xml); - hljs.registerLanguage('yaml', yaml); - hljs.registerLanguage('yml', yaml); - hljs.registerLanguage('markdown', markdown); - hljs.registerLanguage('md', markdown); - hljs.registerLanguage('css', css); - hljs.registerLanguage('scss', scss); - hljs.registerLanguage('less', less); - } - - install(): void { - const lang = this.props['data-lang']; - const code = this.props['data-code']; - // 解析代码HTML - this.codeHTML = escape(code); - if (lang && hljs.getLanguage(lang)) { - this.codeHTML = hljs.highlight(code, { - language: lang, - ignoreIllegals: true, - }).value; - } - } - - render() { - const lang = this.props['data-lang']; - const theme = this.props['data-theme']; - - return ( -
-
- {lang} - {/* !事件直接放icon上会触发两次 */} - - - -
-
${this.codeHTML}`}
-        />
-      
- ); - } - - clickCopyHandler = () => { - const code = this.props['data-code'] || ''; - const lang = this.props['data-lang']; - - // 派发事件到外层 - this.fire( - 'code_copy', - { code, lang }, - { - bubbles: true, - composed: true, - }, - ); - - navigator.clipboard - .writeText(code) - .then(() => { - this.msgInstance = MessagePlugin.success('复制成功'); - }) - .catch(() => { - this.msgInstance = MessagePlugin.success('复制失败,请手动复制'); - }); - }; -} diff --git a/src/chat-message/md/utils.ts b/src/chat-message/md/utils.ts index 1006eb9e..23e02b00 100644 --- a/src/chat-message/md/utils.ts +++ b/src/chat-message/md/utils.ts @@ -6,12 +6,64 @@ export const AddPartHook = CherryStream.createSyntaxHook('addPart', CherryStream return str; }, afterMakeHtml(str) { - return str.replace(/<(?!br\b)([^\s/>]+)([^>]*)>/g, (_whole, tag, attr = '') => { - // 已有 part 属性,不添加 - if (/(\s|^)part\s*=/.test(attr)) { - return `<${tag}${attr}>`; + // 用于追踪当前是否在 data-edit-code="true" 的上下文中 + let result = ''; + let depth = 0; // 追踪嵌套深度 + const editCodeStack: number[] = []; // 记录哪些深度层级有 data-edit-code="true" + + // 使用正则表达式逐个处理标签 + const tagRegex = /<\/?(?!br\b)([^\s/>]+)([^>]*)>/g; + let lastIndex = 0; + let match; + + // eslint-disable-next-line no-cond-assign + while ((match = tagRegex.exec(str)) !== null) { + const [whole, tag, attr = ''] = match; + const isClosingTag = whole.startsWith(' 0 || hasDataEditCode; + const partPrefix = inEditCodeContext ? 'md_code_' : 'md_'; + + if (hasPartAttr) { + // 已有 part 属性,不添加 + result += whole; + } else { + // 添加 part 属性 + result += `<${tag}${attr} part="${partPrefix}${tag}">`; + } + + // 如果不是自闭合标签,增加深度 + if (!isSelfClosing) { + depth += 1; + if (hasDataEditCode) { + editCodeStack.push(depth); + } + } } - return `<${tag}${attr} part="md_${tag}">`; - }); + + lastIndex = tagRegex.lastIndex; + } + + // 添加剩余的文本内容 + result += str.slice(lastIndex); + + return result; }, }); diff --git a/src/chat-message/style/chat-content.less b/src/chat-message/style/chat-content.less index 3bad3860..29b58e46 100644 --- a/src/chat-message/style/chat-content.less +++ b/src/chat-message/style/chat-content.less @@ -27,6 +27,40 @@ border-left: none; background-color: transparent; } + + .cherry-previewer-codeBlock-hover-handler { + & > div { + display: var(--td-chat-md-code-button-display); + align-items: var(--td-chat-md-code-button-align-items); + justify-content: var(--td-chat-md-code-button-justify-content); + top: var(--td-chat-md-code-button-top); + border: var(--td-chat-md-code-button-border); + color: var(--td-chat-md-code-button-color); + background: var(--td-chat-md-code-button-bg); + box-shadow: var(--td-chat-md-code-button-shadow); + + &:hover { + color: var(--td-chat-md-code-button-hover-color); + background: var(--td-chat-md-code-button-hover-bg); + transform: var(--td-chat-md-code-button-hover-transform); + } + } + } + + // 针对带有 data-code-block-theme 属性且值包含 -dark 的暗黑主题样式 + &[data-code-block-theme*="-dark"] { + .cherry-previewer-codeBlock-hover-handler { + & > div { + color: var(--td-chat-md-code-button-dark-color); + background: var(--td-chat-md-code-button-dark-bg); + + &:hover { + color: var(--td-chat-md-code-button-dark-hover-color); + background: var(--td-chat-md-code-button-dark-hover-bg); + } + } + } + } } &__markdown { @@ -58,7 +92,13 @@ } div[data-type=codeBlock] { - margin-bottom: 0; + margin: var(--td-chat-md-code-margin); + overflow: hidden; + + pre[class*=language-] { + margin: 0; + border-radius: var(--td-chat-md-code-radius); + } } // override cherry markdown code style diff --git a/src/chat-message/style/chat-item.less b/src/chat-message/style/chat-item.less index e56c1cf8..637bae3e 100644 --- a/src/chat-message/style/chat-item.less +++ b/src/chat-message/style/chat-item.less @@ -83,6 +83,8 @@ align-items: flex-end; .@{prefix}-chat__item__content { + // 处理 https://github.com/Tencent/tdesign-vue-next/issues/6381,处理max-width+不换行的长链接文本+右对齐时,模块右侧有空白的问题 + margin: var(--td-chat-item-right-content-margin); text-align: var(--td-chat-item-right-content-text-align); } @@ -110,6 +112,14 @@ } } + // common variant样式 + &__role--user { + .@{prefix}-chat__item__content { + // 如用户的content设为flex/inline-flex,会导致内部文本强制为block类型,宽度会与附件齐平,导致短文本时背景比文本短一截 + display: block; + } + } + // variant // 仅user类在text上区分variant &--variant--base { diff --git a/src/chat-message/style/md/chat-md-code.less b/src/chat-message/style/md/chat-md-code.less deleted file mode 100644 index 3d3217bd..00000000 --- a/src/chat-message/style/md/chat-md-code.less +++ /dev/null @@ -1,62 +0,0 @@ -@import "../import.less"; - -.@{prefix}-chat__text__markdown__code { - position: relative; - margin: var(--td-chat-md-code-margin); - border-radius: var(--td-chat-md-code-radius); - overflow: hidden; - - // 'highlight.js/styles/atom-one-light.min.css' - &.light { - pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} - } - - // 'highlight.js/styles/atom-one-dark.min.css'; - &.dark { - pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} - - .@{prefix}-chat__text__markdown__code__header { - color: var(--td-chat-md-code-header-color-dark); - background: var(--td-chat-md-code-header-bg-dark); - } - } - - &__header { - display: flex; - justify-content: space-between; - align-items: center; - height: var(--td-chat-md-code-header-height); - padding: var(--td-chat-md-code-header-padding); - background: var(--td-chat-md-code-header-bg); - - &__lang { - font: var(--td-chat-md-code-header-font); - } - - &__copy__wrapper { - display: flex; - } - - &__copy { - cursor: pointer; - color: var(--td-chat-md-code-header-copy-color); - - &:hover { - color: var(--td-chat-md-code-header-copy-hover-color); - } - } - } - - &__body.hljs { - padding: var(--td-chat-md-code-body-padding); - background-color: var(--td-chat-md-code-body-bg); - overflow: var(--td-chat-md-code-body-overflow); - // 覆盖 cherry markdown style - border-bottom-left-radius: var(--td-chat-md-code-radius); - border-bottom-right-radius: var(--td-chat-md-code-radius); - - code { - font-size: var(--td-chat-md-code-body-code-font-size); - } - } -} diff --git a/src/chat-message/type.ts b/src/chat-message/type.ts index ef87c03c..564e1ffc 100644 --- a/src/chat-message/type.ts +++ b/src/chat-message/type.ts @@ -9,6 +9,7 @@ import type { } from '../chat-engine/type'; import type { ChatLoadingAnimationType, TdChatLoadingProps } from '../chat-loading'; import type { TNode } from '../common'; +import { TdChatAttachmentContentProps } from './content/attachment-content'; import type { TdChatMarkdownContentProps } from './content/markdown-content'; type TdChatContentSearchProps = { @@ -41,6 +42,7 @@ export type TdChatContentProps = { thinking?: TdChatContentThinkProps; reasoning?: TdChatContentThinkProps; suggestion?: TdChatContentSuggestionProps; + attachments?: TdChatAttachmentContentProps; [key: string]: any; // 处理其他ContentType情况 } & Partial, any>>; diff --git a/src/chat-sender/README.md b/src/chat-sender/README.md index cfd13e2e..9d2a92d8 100644 --- a/src/chat-sender/README.md +++ b/src/chat-sender/README.md @@ -20,46 +20,84 @@ spline: base ## API -### Chatbot Props - -## 基础属性 - -| 属性名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `placeholder` | `string` | - | 输入框占位文本 | -| `disabled` | `boolean` | `false` | 是否禁用整个输入组件 | -| `value` | `string` | - | 输入框当前值(受控模式) | -| `defaultValue` | `string` | - | 输入框默认值(非受控模式) | -| `loading` | `boolean` | `false` | 是否加载中 | -| `autosize` | `boolean \| { minRows?: number; maxRows?: number }` | `{ minRows: 2 }` | 高度自动撑开 | - -## 操作按钮配置 - -| 属性名 | 类型 | 说明 | -|--------|------|------| -| `actions` | `TdChatSenderAction[]` \| `Function` \| `boolean` | 操作按钮配置: - 数组:自定义操作按钮列表 - 函数:(preset) => 操作按钮数组 - 布尔值:是否显示默认操作按钮 | - -## 透传属性 - -| 属性名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `attachmentsProps` | `TdAttachmentsProps` | `{ items: [], overflow: 'scrollX' }` | 附件列表配置属性 | -| `textareaProps` | `Partial` | `{}` | 文本输入框配置属性 | -| `uploadProps` | `Omit` | - | 文件上传输入框的HTML属性 | - -## 事件回调 - -| 事件名 | 参数类型 | 说明 | -|--------|----------|------| -| `onSend` | `(e: CustomEvent) => void` | 点击发送按钮时触发 | -| `onStop` | `(e: CustomEvent) => void` | 点击停止按钮时触发 | -| `onChange` | `(e: CustomEvent) => void` | 输入内容变化时触发 | -| `onFileSelect` | `(e: CustomEvent) => Promise` | 选择文件时触发 | -| `onFileRemove` | `(e: CustomEvent) => Promise` | 移除文件时触发 | - -## Ref - -| 属性名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `focus` | `(option?: { preventScroll?: boolean; }) => void` | - | 获取焦点 | -| `blur` | `() => void` | - | 取消焦点 | +### ChatSender Props + +| 属性名 | 类型 | 默认值 | 说明 | 必传 | +| ---------------- | ------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| actions | String[] / Object[] / Function / TNode / Boolean | `['send']` | 操作按钮配置。支持预设名称数组 `['uploadImage', 'uploadAttachment', 'attachment'('uploadAttachment'的兼容别名,**后续将废弃**), 'send']`;对象数组 `[{ name: 'uploadImage', uploadProps: { accept: '.png' } }]` 配置上传属性;函数 `(preset) => [{ name, render }]` 自定义渲染;TNode 完全自定义;`true` 显示发送按钮,`false` 隐藏 | N | +| attachmentsProps | Object | `{ items: [], overflow: 'scrollX' }` | 附件列表配置属性,透传 `Attachments` 组件 | N | +| autosize | Boolean / Object | `{ minRows: 2 }` | 高度自动撑开,支持 `true` 或配置项 `{ minRows, maxRows }` | N | +| defaultValue | String | - | 输入框的默认值,非受控模式 | N | +| disabled | Boolean | `false` | 是否禁用输入框 | N | +| loading | Boolean | `false` | 发送按钮是否处于加载状态 | N | +| placeholder | String | - | 输入框默认文案 | N | +| sendBtnDisabled | Boolean / Function | `false` | 禁用发送按钮,支持布尔值或函数 `(inputValue: string) => boolean`;输入为空时发送按钮始终禁用 | N | +| textareaProps | Object | - | 透传 `Textarea` 组件属性 | N | +| uploadProps | Object | - | **已废弃**,建议使用 `actions` 配置上传属性。例如:`actions: [{ name: 'uploadImage', uploadProps: { accept: '.png' } }]`。详见 [TdChatSenderUploadProps](#tdchatsenderuploadprops) | N | +| value | String | - | 输入框的值,支持双向绑定 | N | +| onBlur | Function | - | 输入框失焦事件,TS 类型:`(e: CustomEvent) => void` | N | +| onFocus | Function | - | 输入框聚焦事件,TS 类型:`(e: CustomEvent) => void` | N | + +### ChatSender Events + +| 名称 | 参数 | 描述 | +| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------------- | +| change | `(e: CustomEvent)` | 输入框值发生变化时触发,参数为输入值 | +| focus | `(e: CustomEvent)` | 输入框聚焦时触发,参数为输入值 | +| blur | `(e: CustomEvent)` | 输入框失焦时触发,参数为输入值 | +| send | `(e: CustomEvent<{ value: string; attachments?: TdAttachmentItem[] }>)` | 点击消息发送时触发,参数包含 `value`(输入内容)、`attachments`(附件列表) | +| stop | `(e: CustomEvent)` | 点击消息终止时触发,参数为输入值 | +| fileSelect | `(e: CustomEvent)` | 选择文件时触发,返回新选择的文件列表 | +| fileRemove | `(e: CustomEvent)` | 移除文件时触发,参数为剩余的文件列表 | + +### ChatSender Slots + +| 名称 | 说明 | +| :------------ | ---------------- | +| header | 输入框外顶部区域 | +| inner-header | 输入框内顶部区域 | +| input-prefix | 输入框前方区域 | +| textarea | 替换默认输入框 | +| footer-prefix | 输入框左下角区域 | +| actions | 输入框右下角区域 | + +### ChatSender Methods + +| 名称 | 参数 | 说明 | +| :---------- | :------------------------- | :----------- | +| focus | `(options?: FocusOptions)` | 获取焦点 | +| blur | - | 取消焦点 | +| selectFile | - | 触发文件选择 | + +### 类型定义 + +#### TdChatSenderUploadProps + +上传配置属性。 + +| 名称 | 类型 | 说明 | +| :------- | :------ | :---------------------------------------------- | +| accept | String | 接受的文件类型,如 `'image/*'` 或 `'.jpg,.png'` | +| multiple | Boolean | 是否允许多选 | + +#### TdChatSenderAction + +操作按钮配置。 + +| 名称 | 类型 | 说明 | +| :--------- | :------------------------ | :------------------------------------------------------ | +| name | String | 按钮名称,对应预设按钮:`uploadImage`、`uploadAttachment`、`attachment`、`send` | +| uploadProps | TdChatSenderUploadProps | 上传配置,仅对 `uploadImage` 和 `uploadAttachment` 有效 | + +#### TdAttachmentItem + +附件项数据结构。 + +| 名称 | 类型 | 说明 | +| :----- | :------------------------------------ | :--------------- | +| name | String | 文件名称 | +| url | String | 文件地址 | +| size | Number | 文件大小(字节) | +| type | String | 文件 MIME 类型 | +| status | `'success' \| 'error' \| 'uploading'` | 上传状态 | +| raw | File | 原始文件对象 | diff --git a/src/chat-sender/_example/attachment.tsx b/src/chat-sender/_example/attachment.tsx index 7a1eafa3..50f32659 100644 --- a/src/chat-sender/_example/attachment.tsx +++ b/src/chat-sender/_example/attachment.tsx @@ -3,7 +3,6 @@ import 'tdesign-web-components/chatbot'; import { Component, signal } from 'omi'; import { TdAttachmentItem } from '../../filecard'; -import { TdChatSenderParams } from '../type'; export default class AttachmentExample extends Component { inputValue = signal('输入内容'); @@ -42,7 +41,7 @@ export default class AttachmentExample extends Component { this.files.value = e.detail.concat(this.files.value); }; - onSend = (e: CustomEvent) => { + onSend = (e: CustomEvent<{ value: string; attachments?: TdAttachmentItem[] }>) => { console.log('提交', e); this.inputValue.value = ''; this.files.value = []; diff --git a/src/chat-sender/_example/basic.tsx b/src/chat-sender/_example/basic.tsx index 09e26ffe..c88b3a3f 100644 --- a/src/chat-sender/_example/basic.tsx +++ b/src/chat-sender/_example/basic.tsx @@ -7,12 +7,6 @@ export default class BasicExample extends Component { loading = signal(false); - handleReadyToSend = (v: string): boolean => { - console.log('handleReadyToSend', v); - // 无论内容是否为空,始终允许发送 - return true; - }; - onChange = (e: CustomEvent) => { console.log('onChange', e); this.inputValue.value = e.detail; @@ -44,7 +38,6 @@ export default class BasicExample extends Component { placeholder="请输入内容" loading={this.loading.value} autosize={{ minRows: 2 }} - readyToSend={this.handleReadyToSend} onChange={this.onChange} onSend={this.onSend} onStop={this.onStop} diff --git a/src/chat-sender/chat-sender.tsx b/src/chat-sender/chat-sender.tsx index 48eb238e..60cf369e 100644 --- a/src/chat-sender/chat-sender.tsx +++ b/src/chat-sender/chat-sender.tsx @@ -6,7 +6,6 @@ import '../attachments'; import '../textarea'; import '../button'; import '../tooltip'; -import '../dropdown'; import { isNil } from 'lodash-es'; import { Component, createRef, OmiProps, signal, tag } from 'omi'; @@ -14,6 +13,7 @@ import { Component, createRef, OmiProps, signal, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; import { setExportparts } from '../_util/dom'; import { convertToLightDomNode } from '../_util/lightDom'; +import { TNode } from '../common'; import { TdAttachmentItem } from '../filecard'; import { TdChatSenderAction, TdChatSenderProps } from './type'; @@ -29,19 +29,22 @@ export default class ChatSender extends Component { placeholder: String, disabled: Boolean, value: String, - actions: [Array, Function, Boolean], + actions: [Array, Function, Boolean, Object], + suffix: [Array, Function, Boolean, Object], defaultValue: String, loading: Boolean, - autosize: Object, + autosize: [Boolean, Object], attachmentsProps: Object, textareaProps: Object, uploadProps: Object, - readyToSend: Function, + sendBtnDisabled: [Boolean, Function], onFileSelect: Function, onFileRemove: Function, onSend: Function, onStop: Function, onChange: Function, + onFocus: Function, + onBlur: Function, }; static defaultProps: Partial = { @@ -52,7 +55,7 @@ export default class ChatSender extends Component { }, autosize: { minRows: 2 }, textareaProps: {}, - actions: (presets) => [presets[presets.length - 1]], + actions: ['send'], }; pValue: Omi.SignalValue = signal(''); @@ -61,6 +64,8 @@ export default class ChatSender extends Component { uploadRef = createRef(); + imageUploadRef = createRef(); + inputRef = createRef(); private shiftDown = false; @@ -88,7 +93,7 @@ export default class ChatSender extends Component { private handleAttachmentsRemove = (e: CustomEvent) => { const removed = e.detail; const rest = this.attachmentsValue.filter((item) => item !== removed); - this.pAttachments.value = rest; + this.fire('fileRemove', rest, { composed: true, }); @@ -106,6 +111,7 @@ export default class ChatSender extends Component { } if (props.attachmentsProps.items !== oldProps.attachmentsProps.items) return true; if (props.loading !== oldProps.loading) return true; + if (props.sendBtnDisabled !== oldProps.sendBtnDisabled) return true; return false; } @@ -135,6 +141,31 @@ export default class ChatSender extends Component { this.uploadRef.current.value = ''; }; + private handleImageSelected = () => { + const files = Array.from(this.imageUploadRef.current?.files || []); + if (!files.length) { + return; + } + this.fire('fileSelect', files, { + composed: true, + }); + this.imageUploadRef.current.value = ''; + }; + + /** 上传图片按钮 */ + private renderUploadImage = () => ( + + { + this.imageUploadRef.current?.click(); + }} + > + {convertToLightDomNode()} + + + ); + /** 上传附件按钮 */ private renderUploadAttachment = () => ( @@ -160,11 +191,11 @@ export default class ChatSender extends Component { className={classname([ `${className}__button`, { - [`${className}__button--focus`]: this.isReadyToSend || this.props.loading, + [`${className}__button--focus`]: !this.isSendBtnDisabled || this.props.loading, }, ])} onClick={this.clickSend} - disabled={disabled} + disabled={disabled || (!this.props.loading && this.isSendBtnDisabled)} > {convertToLightDomNode( this.props.loading ? ( @@ -177,8 +208,16 @@ export default class ChatSender extends Component { ); }; - get presetActions(): TdChatSenderAction[] { + get presetActions(): Array<{ name: string; render: TNode }> { return [ + { + name: 'uploadImage', + render: this.renderUploadImage(), + }, + { + name: 'uploadAttachment', + render: this.renderUploadAttachment(), + }, { name: 'attachment', render: this.renderUploadAttachment(), @@ -190,40 +229,95 @@ export default class ChatSender extends Component { ]; } - get isReadyToSend() { - return this.props.readyToSend && typeof this.props.readyToSend === 'function' - ? this.props.readyToSend(this.inputValue) - : this.inputValue && this.inputValue.trim() !== ''; + get isSendBtnDisabled() { + const { sendBtnDisabled } = this.props; + const inputValue = this.inputValue || ''; + + if (typeof sendBtnDisabled === 'boolean') return sendBtnDisabled; + if (typeof sendBtnDisabled === 'function') return sendBtnDisabled(inputValue); + // 输入为空时始终禁用发送 + if (inputValue.trim() === '') return true; + + return false; } private renderActions = () => { - const { actions } = this.props; - if (!actions) { + // 优先使用 actions,如果没有则使用 suffix + const { actions, suffix } = this.props; + const effectiveActions = actions !== undefined ? actions : suffix; + + // false 或 undefined/null 时不显示 + if (!effectiveActions) { return null; } - let arrayActions: TdChatSenderAction[] = this.presetActions; - if (Array.isArray(actions)) { - arrayActions = (actions as string[]).map((name) => arrayActions.find((item) => item.name === name)); + // true 时显示默认按钮 + if (effectiveActions === true) { + return this.presetActions.slice(-1).map((item) => ( +
+ {item.render} +
+ )); } - if (typeof actions === 'function') { - arrayActions = actions(this.presetActions); + // 数组类型:区分字符串数组和对象数组 + if (Array.isArray(effectiveActions)) { + const arrayActions = (effectiveActions as Array).map((action) => { + // 字符串类型:直接查找预设按钮 + if (typeof action === 'string') { + return this.presetActions.find((item) => item.name === action); + } + // 对象类型:从 presetActions 中找到对应的 render + return this.presetActions.find((item) => item.name === action.name); + }); + return arrayActions.map((item, idx) => ( +
+ {item?.render} +
+ )); } - return arrayActions.map((item, idx) => ( -
this.handleAction(item.name, idx)} - > - {item.render} -
- )); + // 函数类型:(preset) => Array<{ name: string; render: TNode }> + if (typeof effectiveActions === 'function') { + const arrayActions = effectiveActions(this.presetActions) as Array<{ name: string; render: TNode }>; + return arrayActions.map((item: { name: string; render: TNode }) => ( +
+ {item.render} +
+ )); + } + // TNode 类型(Object)直接渲染 + return effectiveActions as TNode; }; render(props: TdChatSenderProps) { + // 从 actions 中提取上传配置 + const getUploadConfigFromActions = (actionName: string) => { + const { actions } = this.props; + if (Array.isArray(actions)) { + const action = (actions as TdChatSenderAction[]).find((item) => + typeof item === 'object' && 'name' in item ? item.name === actionName : false, + ); + return (action as TdChatSenderAction)?.uploadProps || {}; + } + return {}; + }; + + // 文件上传配置优先级:actions 中的 uploadAttachment 配置 > uploadProps + const fileUploadConfig = getUploadConfigFromActions('uploadAttachment') || this.props.uploadProps || {}; + // 图片上传配置优先级:actions 中的 uploadImage 配置 > 默认 accept: 'image/*' + const imageUploadConfig = { + ...getUploadConfigFromActions('uploadImage'), + accept: 'image/*', + }; + return (
- + +
@@ -311,18 +405,8 @@ export default class ChatSender extends Component { }); }; - private handleAction = (action: string, index: number) => { - this.fire( - 'action', - { action, index }, - { - composed: true, - }, - ); - }; - private handleSend = () => { - if (this.props.disabled || !this.isReadyToSend) { + if (this.props.disabled || this.isSendBtnDisabled) { return; } this.fire( @@ -335,6 +419,8 @@ export default class ChatSender extends Component { composed: true, }, ); + + // 当非外部受控时,重置输入框和附件 this.pValue.value = ''; this.pAttachments.value = []; }; diff --git a/src/chat-sender/style/chat-sender.less b/src/chat-sender/style/chat-sender.less index 675acacd..e61d7630 100644 --- a/src/chat-sender/style/chat-sender.less +++ b/src/chat-sender/style/chat-sender.less @@ -19,7 +19,6 @@ width: 100%; display: flex; flex-direction: column; - box-sizing: border-box; padding: var(--td-chat-input-padding); border: 1px solid var(--td-chat-input-content-default-border); border-radius: var(--td-chat-input-radius); @@ -175,13 +174,6 @@ color: var(--td-text-color-anti); } - &:hover::part(@{prefix}-button) { - background: var(--td-brand-color-hover); - } - - &:focus::part(@{prefix}-button) { - background: var(--td-brand-color-active); - } &__icon { display: flex; diff --git a/src/chat-sender/type.ts b/src/chat-sender/type.ts index cc240a9e..299925c6 100644 --- a/src/chat-sender/type.ts +++ b/src/chat-sender/type.ts @@ -1,47 +1,99 @@ import { TdAttachmentsProps } from '../attachments'; -import { ChatRequestParams } from '../chat-engine'; import { TNode } from '../common'; import { TdAttachmentItem } from '../filecard'; import { TdTextareaProps } from '../textarea'; -export type TdChatSenderActionName = 'attachment' | 'send'; +export type TdChatSenderActionName = 'uploadImage' | 'uploadAttachment' | 'attachment' | 'send'; + +export type UploadActionType = 'uploadImage' | 'uploadAttachment'; + +export interface TdChatSenderUploadProps { + /** 接受的文件类型,如 'image/*' 或 '.jpg,.png' */ + accept?: string; + /** 是否允许多选 */ + multiple?: boolean; +} export interface TdChatSenderAction { - name: string; - render: TNode; + /** 按钮名称,对应预设按钮 */ + name: TdChatSenderActionName; + /** 上传配置,仅对 uploadImage 和 uploadAttachment 有效 */ + uploadProps?: TdChatSenderUploadProps; } export interface TdChatSenderParams { + /** 输入内容 */ value: string; + /** 附件列表 */ attachments?: TdAttachmentItem[]; } -export interface TdChatSenderProps extends Pick { +export interface TdChatSenderProps { value?: string; placeholder?: string; disabled?: boolean; defaultValue?: string; - actions?: TdChatSenderActionName[] | ((preset: TdChatSenderAction[]) => TdChatSenderAction[]) | boolean; + /** + * 操作按钮配置 + * - Array: 支持字符串或对象数组,如 ['send']、[{ name: 'uploadImage', uploadProps: { accept: '.png' } }, 'send'] + * - Function: (preset: Array<{ name: string; render: TNode }>) => Array<{ name: string; render: TNode }>,接收全部预设按钮后返回最终渲染项 + * - TNode: 完全自定义操作区内容 + * - boolean: true 等价于默认值 ['send'],false 表示不显示操作区 + * 预设按钮名称包括:uploadImage、uploadAttachment、attachment(uploadAttachment 的兼容别名,后续将废弃)、send + * @default ['send'] + */ + actions?: + | Array + | ((preset: Array<{ name: string; render: TNode }>) => Array<{ name: string; render: TNode }>) + | TNode + | boolean; + /** + * 操作按钮配置(内部属性,不对外暴露) + * 功能与 actions 完全相同,当 actions 存在时优先使用 actions + * @internal + */ + suffix?: + | Array + | ((preset: Array<{ name: string; render: TNode }>) => Array<{ name: string; render: TNode }>) + | TNode + | boolean; /** 是否加载中 */ loading?: boolean; /** 透传attachment参数 */ attachmentsProps?: TdAttachmentsProps; + /** + * 高度自动撑开,支持传入 true 或配置项 { minRows, maxRows } + * - true: 启用自动撑高 + * - object: 配置最小/最大行数 + * @default { minRows: 2 } + */ + autosize?: TdTextareaProps['autosize']; /** 透传textarea参数 */ textareaProps?: Partial>; - /** 透传input-file参数 */ - uploadProps?: Omit; /** - * 覆盖发送条件,返回为true时标识为可发送 - * @default value不为空时可发送 + * 文件上传配置 + * @deprecated 已废弃,建议使用 actions 配置上传属性。例如:actions: [{ name: 'uploadImage', uploadProps: { accept: '.png' } }] + */ + uploadProps?: TdChatSenderUploadProps; + /** + * 禁用发送按钮,支持布尔值或函数形式;输入为空时始终禁用 + * @default false */ - readyToSend?: (inputValue: string) => boolean; - onSend?: (e: CustomEvent) => ChatRequestParams | void; + sendBtnDisabled?: boolean | ((inputValue: string) => boolean); + /** 发送消息事件,参数包含 value(输入内容)、attachments(附件列表) */ + onSend?: (e: CustomEvent) => void; + /** 停止发送事件 */ onStop?: (e: CustomEvent) => void; + /** 输入内容变化事件 */ onChange?: (e: CustomEvent) => void; - onFileSelect?: (e: CustomEvent) => void; - onFileRemove?: (e: CustomEvent) => void; + /** 输入框聚焦事件 */ onFocus?: (e: CustomEvent) => void; + /** 输入框失焦事件 */ onBlur?: (e: CustomEvent) => void; + /** 文件选择事件 */ + onFileSelect?: (e: CustomEvent) => void; + /** 文件移除事件,参数为剩余的文件列表 */ + onFileRemove?: (e: CustomEvent) => void; } export interface TdChatSenderApi { @@ -49,4 +101,7 @@ export interface TdChatSenderApi { focus: (opts?: FocusOptions) => void; /** 取消焦点 */ blur: () => void; + + /** 触发文件选择 */ + selectFile: () => void; } diff --git a/src/chatbot/README.md b/src/chatbot/README.md index dc862d74..8ca94ae4 100644 --- a/src/chatbot/README.md +++ b/src/chatbot/README.md @@ -34,6 +34,10 @@ spline: base {{ nostream }} +### 数据分页 + +{{ dataPaging }} + ## API ## Chatbot Props diff --git a/src/chatbot/_example/basic.tsx b/src/chatbot/_example/basic.tsx index ae0ee32f..93c8c3b8 100644 --- a/src/chatbot/_example/basic.tsx +++ b/src/chatbot/_example/basic.tsx @@ -177,7 +177,7 @@ const mockData: ChatMessagesData[] = [ content: [ { type: 'text', - data: '这张图里的帅哥是谁,这张图里的帅哥是谁,这张图里的帅哥是谁,这张图里的帅哥是谁,这张图里的帅哥是谁', + data: '这张图里的帅哥是谁', }, { type: 'attachment', @@ -207,7 +207,7 @@ const mockData: ChatMessagesData[] = [ content: [ { type: 'markdown', - data: '**tdesign** 团队的 *核心开发者* `uyarnchen` 是也。', + data: '**tdesign** 团队的 *核心开发者* `uyarnchen` 是也。', }, ], }, @@ -226,14 +226,15 @@ const defaultChunkParser = (chunk): AIMessageContent => { }; function handleStructuredData(chunk: SSEChunkData): AIMessageContent { - if (!chunk?.data || typeof chunk === 'string') { - return { - type: 'markdown', - data: String(chunk), - }; - } + // if (!chunk?.data || typeof chunk === 'string') { + // return { + // type: 'markdown', + // data: String(chunk), + // }; + // } const { type, ...rest } = chunk.data; + switch (type) { case 'error': return { @@ -271,6 +272,14 @@ function handleStructuredData(chunk: SSEChunkData): AIMessageContent { }; case 'text': { + // 如果是空内容,返回 pending 状态来过滤 + if (!rest?.msg || rest?.msg === '') { + return { + type: 'text', + data: '', + status: 'pending', + }; + } return { type: 'markdown', data: rest?.msg || '', @@ -313,7 +322,7 @@ function handleStructuredData(chunk: SSEChunkData): AIMessageContent { // 测试用的回调配置 const mockModelsWithCallbacks = { // endpoint: 'http://localhost:3000/sse/normal', - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: 'http://localhost:9001/sse/normal', stream: true, // === 业务层回调测试 === @@ -340,7 +349,9 @@ const mockModelsWithCallbacks = { reason: 'user_initiated', }); }, - + // 过滤掉 msg 为空字符串的 chunk + isValidChunk: (chunk) => chunk?.data?.msg && chunk.data.msg !== '', + // 经过isValidChunk过滤后的chunk进入onmessage onMessage: (chunk) => defaultChunkParser(chunk), onRequest: (params) => { @@ -424,7 +435,7 @@ export default class BasicChat extends Component { const search = content.find((item) => item.type === 'search'); return { // variant: 'text', - variant: 'outline', + // variant: 'outline', avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png', actions: ['replay', 'copy', 'good', 'bad'], handleActions: { diff --git a/src/chatbot/_example/dataPaging.tsx b/src/chatbot/_example/dataPaging.tsx new file mode 100644 index 00000000..42cf9161 --- /dev/null +++ b/src/chatbot/_example/dataPaging.tsx @@ -0,0 +1,251 @@ +import 'tdesign-web-components/chatbot/chat-list'; +import 'tdesign-web-components/chat-message'; + +import { Component, createRef, signal } from 'omi'; + +import type { ChatMessagesData } from '../../chat-engine/type'; + +/** 每次加载的消息数量 */ +const PAGE_SIZE = 10; + +/** + * 模拟全量消息数据源(如后端接口返回的历史消息列表) + */ +function generateAllMessages(count: number): ChatMessagesData[] { + const messages: ChatMessagesData[] = []; + for (let i = 0; i < count; i++) { + const isUser = i % 2 === 0; + if (isUser) { + messages.push({ + id: `msg-${i}`, + role: 'user', + status: 'complete', + content: [ + { + type: 'text', + data: `这是第 ${i + 1} 条用户消息,用于测试外部数据分页(组件内控)。`, + }, + ], + }); + } else { + messages.push({ + id: `msg-${i}`, + role: 'assistant', + status: 'complete', + content: [ + { + type: 'markdown', + data: `这是第 ${ + i + 1 + } 条 **AI 回复**。\n\n此示例由 \`chat-list\` 内部自动处理滚动阈值判断和滚动位置维持,用户只需传入 \`hasMore\` 和 \`onLoadMore\` 即可。`, + }, + ], + }); + } + } + return messages; +} + +export default class DataPagingInnerExample extends Component { + static css = [ + ` + .data-paging-inner-demo { + display: flex; + flex-direction: column; + height: 80vh; + border: 1px solid var(--td-border-level-2-color, #dcdcdc); + border-radius: 8px; + overflow: hidden; + } + .data-paging-inner-demo__header { + padding: 12px 16px; + border-bottom: 1px solid var(--td-border-level-2-color, #dcdcdc); + background: var(--td-bg-color-container, #fff); + font-size: 14px; + color: var(--td-text-color-secondary, rgba(0, 0, 0, 0.6)); + display: flex; + align-items: center; + justify-content: space-between; + } + .data-paging-inner-demo__info { + display: flex; + gap: 16px; + align-items: center; + } + .data-paging-inner-demo__tag { + padding: 2px 8px; + border-radius: 4px; + background: var(--td-brand-color-light, #ecf2fe); + color: var(--td-brand-color, #0052d9); + font-size: 12px; + } + .data-paging-inner-demo__tag--success { + background: var(--td-success-color-1, #e8f8e8); + color: var(--td-success-color, #2ba471); + } + .data-paging-inner-demo__custom-load-more { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 0; + font-size: 13px; + color: var(--td-text-color-placeholder, rgba(0, 0, 0, 0.35)); + } + .data-paging-inner-demo__custom-load-more--loading { + color: var(--td-brand-color, #0052d9); + } + .data-paging-inner-demo__custom-spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--td-brand-color, #0052d9); + border-top-color: transparent; + border-radius: 50%; + animation: custom-spin 0.8s linear infinite; + } + @keyframes custom-spin { + to { transform: rotate(360deg); } + } + `, + ]; + + /** 全量消息数据源 */ + allMessages: ChatMessagesData[] = generateAllMessages(100); + + /** 当前渲染的消息切片(初始为最后一页) */ + visibleMessages = signal(this.allMessages.slice(-PAGE_SIZE)); + + /** 是否还有更多历史消息 */ + hasMore = signal(this.allMessages.length > PAGE_SIZE); + + /** 加载中状态(用于自定义标记展示) */ + isLoading = signal(false); + + /** chat-list 组件引用 */ + chatListRef = createRef(); + + /** + * 加载更多历史消息 + * chat-list 在滚动到顶部阈值时自动触发此回调, + * 内部自动处理滚动位置维持,用户只需更新数据 + */ + handleLoadMore = () => { + this.isLoading.value = true; + // 模拟异步加载延迟(实际场景中这里是接口请求) + setTimeout(() => { + const currentCount = this.visibleMessages.value.length; + const totalCount = this.allMessages.length; + const remainingCount = totalCount - currentCount; + + if (remainingCount <= 0) { + this.hasMore.value = false; + return; + } + + // 从全量数据中取出更早的消息 + const loadCount = Math.min(PAGE_SIZE, remainingCount); + const startIndex = totalCount - currentCount - loadCount; + const olderMessages = this.allMessages.slice(startIndex, startIndex + loadCount); + + // 将更早的消息 prepend 到可见列表前面 + // chat-list 内部会自动维持滚动位置,无需用户处理 + this.visibleMessages.value = [...olderMessages, ...this.visibleMessages.value]; + + // 更新状态 + if (this.visibleMessages.value.length >= totalCount) { + this.hasMore.value = false; + } + + console.log(`[DataPagingInner] 已加载 ${this.visibleMessages.value.length}/${totalCount} 条消息`); + this.isLoading.value = false; + }, 300); + }; + + /** + * 滚动事件回调 + * 当列表滚动到底部时,重置分页消息数量到初始状态 + */ + handleScroll = (e: CustomEvent<{ scrollTop: number }>) => { + const listEl = this.chatListRef.current?.shadowRoot?.querySelector('.t-chat__list'); + if (!listEl) return; + + const { scrollTop } = e.detail; + const { scrollHeight, clientHeight } = listEl; + const isAtBottom = scrollHeight - (scrollTop + clientHeight) <= 10; + + if (isAtBottom && this.visibleMessages.value.length > PAGE_SIZE) { + // 滚动到底部,重置为最后一页 + this.visibleMessages.value = this.allMessages.slice(-PAGE_SIZE); + this.hasMore.value = this.allMessages.length > PAGE_SIZE; + console.log('[DataPagingInner] 已滚动到底部,重置分页消息数量'); + } + }; + + render() { + const visible = this.visibleMessages.value; + const total = this.allMessages.length; + const hasMore = this.hasMore.value; + const loading = this.isLoading.value; + + return ( +
+
+ 数据分页示例 +
+ + DOM 节点:{visible.length} / {total} + + {visible.length > PAGE_SIZE && ( + + 滚动到底部将重置分页 + + )} +
+
+ + + {/* 自定义 load-more 标记:通过 slot="load-more" 替换默认的加载提示 */} +
+ {loading ? ( + <> + + 正在加载历史消息... + + ) : ( + 📜 继续上滑查看更早的消息 + )} +
+ {visible.map((msg) => ( + + ))} +
+
+ ); + } +} diff --git a/src/chatbot/_example/markdown.tsx b/src/chatbot/_example/markdown.tsx index de8bbfc5..54796d15 100644 --- a/src/chatbot/_example/markdown.tsx +++ b/src/chatbot/_example/markdown.tsx @@ -4,11 +4,83 @@ import 'tdesign-web-components/switch'; // 公式能力引入,参考cherryMarkdown示例 import 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js'; +// 自定义代码块示例引入 +import hljs from 'highlight.js/lib/core'; +import bash from 'highlight.js/lib/languages/bash'; +import javascript from 'highlight.js/lib/languages/javascript'; +import json from 'highlight.js/lib/languages/json'; +import python from 'highlight.js/lib/languages/python'; +import typescript from 'highlight.js/lib/languages/typescript'; +import xml from 'highlight.js/lib/languages/xml'; import { Component, signal } from 'omi'; import { TdChatMessageProps } from '../../chat-message/type'; import mdContent from '../mock/testMarkdown.md?raw'; +// 注册常用语言 +hljs.registerLanguage('javascript', javascript); +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('python', python); +hljs.registerLanguage('json', json); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('html', xml); + +/** + * 自定义代码块渲染器 + * 使用 highlight.js 进行语法高亮,并内联 GitHub Light 主题样式 + */ +const customCodeBlockRenderer = (code: string, _sign: string, _cherry: any, lang: string) => { + // 使用 highlight.js 进行代码高亮 + let highlightedCode = code; + try { + if (lang && hljs.getLanguage(lang)) { + highlightedCode = hljs.highlight(code, { + language: lang, + ignoreIllegals: true, + }).value; + } else { + // 如果语言不支持,使用自动检测 + highlightedCode = hljs.highlightAuto(code).value; + } + } catch (e) { + console.error('代码高亮失败:', e); + // 降级处理:转义 HTML + highlightedCode = code.replace(/&/g, '&').replace(//g, '>'); + } + + // 返回带有完整样式的代码块 + // 注意:由于在 Shadow DOM 中,我们需要内联所有样式 + return ` +
+
+ ${ + lang || 'text' + } + 代码 +
+
${highlightedCode}
+ +
+ `; +}; + export default class MarkdownExample extends Component { static css = [ ` @@ -23,6 +95,8 @@ export default class MarkdownExample extends Component { hasKatex = signal(false); + isCustomCodeBlock = signal(false); + rerenderKey = signal(1); get itemProps(): TdChatMessageProps { @@ -47,16 +121,40 @@ export default class MarkdownExample extends Component { codeBlockTheme: 'dark', }, engine: { - syntax: this.hasKatex.value - ? { - mathBlock: { - engine: 'katex', - }, - inlineMath: { - engine: 'katex', - }, - } - : undefined, + syntax: { + ...(this.hasKatex.value + ? { + mathBlock: { + engine: 'katex', + }, + inlineMath: { + engine: 'katex', + }, + } + : {}), + ...(this.isCustomCodeBlock.value + ? { + codeBlock: { + customRenderer: { + all: { + render: customCodeBlockRenderer, + }, + }, + }, + } + : { + codeBlock: { + customBtns: [ + { + html: '
自定义按钮
', + onClick: (event, code, lang) => { + console.log(`【${lang}】: ${code}`); + }, + }, + ], + }, + }), + }, }, }, }, @@ -69,6 +167,11 @@ export default class MarkdownExample extends Component { this.rerenderKey.value += 1; }; + changeCodeBlockHandler = async (e) => { + this.isCustomCodeBlock.value = e; + this.rerenderKey.value += 1; + }; + render() { return ( @@ -80,6 +183,10 @@ export default class MarkdownExample extends Component { 公式 + + 自定义代码块 + + ); diff --git a/src/chatbot/chat-list.tsx b/src/chatbot/chat-list.tsx index 4588869a..3ed981ff 100644 --- a/src/chatbot/chat-list.tsx +++ b/src/chatbot/chat-list.tsx @@ -1,7 +1,7 @@ import 'tdesign-icons-web-components/esm/components/arrow-down'; import '../chat-message'; -import { debounce, throttle } from 'lodash-es'; +import { debounce, isNil, throttle } from 'lodash-es'; import { Component, createRef, signal, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; @@ -19,11 +19,15 @@ export default class Chatlist extends Component { autoScroll: Boolean, defaultScrollTo: String, onScroll: Function, + hasMore: Boolean, + loadMoreThreshold: Number, + onLoadMore: Function, }; static defaultProps = { autoScroll: true, defaultScrollTo: 'bottom', + loadMoreThreshold: 80, }; private listRef = createRef(); @@ -47,6 +51,21 @@ export default class Chatlist extends Component { scrollButtonVisible = signal(false); + /** 外部数据分页:用于滚动位置维持的 MutationObserver */ + private externalPagingObserver: MutationObserver = null; + + /** 外部数据分页:加载前的滚动高度,用于位置补偿 */ + private prevScrollHeightBeforeLoad = 0; + + /** 外部数据分页:加载前的滚动位置 */ + private prevScrollTopBeforeLoad = 0; + + /** 外部数据分页:是否正在等待 DOM 更新以修正滚动位置 */ + private waitingForDOMUpdate = false; + + /** 外部数据分页:内部加载状态标记,用于防止重复触发 loadMore */ + private isExternalLoading = signal(false); + /** 触发自动滚动 */ private handleAutoScroll = throttle(() => { const { autoScroll } = this.props; @@ -62,7 +81,7 @@ export default class Chatlist extends Component { private checkAutoScroll = throttle(() => { const { scrollTop, scrollHeight, clientHeight } = this.listRef.current; // 判断上滚:总高度未变更 && 滚动diff大于阈值 - const upScroll = scrollHeight === this.scrollHeightTmp && this.scrollTopTmp - scrollTop >= 10 ? true : false; + const upScroll = scrollHeight === this.scrollHeightTmp && this.scrollTopTmp - scrollTop >= 10; // 用户主动上滚,取消自动滚动,标记为手动阻止 if (upScroll) { @@ -96,9 +115,37 @@ export default class Chatlist extends Component { } }, 70); + /** 检测是否滚动到顶部,触发外部数据分页加载 */ + private checkExternalLoadMore = throttle(() => { + const { hasMore, loadMoreThreshold = 80 } = this.props; + // 仅在外部数据分页模式下生效 + if (hasMore === undefined) return; + if (!hasMore || this.isExternalLoading.value) return; + + const list = this.listRef.current; + if (!list) return; + + if (list.scrollTop < loadMoreThreshold) { + // 记录加载前的滚动状态,并标记为正在加载 + this.prevScrollHeightBeforeLoad = list.scrollHeight; + this.prevScrollTopBeforeLoad = list.scrollTop; + this.waitingForDOMUpdate = true; + this.isExternalLoading.value = true; + + this.fire( + 'loadMore', + {}, + { + composed: true, + }, + ); + } + }, 200); + private handleScroll = (e) => { this.checkAutoScroll(); this.checkAndShowScrollButton(); + this.checkExternalLoadMore(); this.fire( 'scroll', { @@ -110,6 +157,31 @@ export default class Chatlist extends Component { ); }; + receiveProps(props, oldProps): void { + // 当 hasMore 从 false/undefined 变为 true 时(例如外部数据分页场景中首次 setMessages 后), + // 需要处理: + // 1. 重置内部加载状态,防止残留的 isExternalLoading 阻塞后续 loadMore + // 2. 如果自动滚动开启,先确保滚到底部(因为 "加载更多" 区域出现改变了 scrollHeight, + // 而 handleAutoScroll 可能被 throttle 掉了) + // 3. 如果当前已在顶部(scrollTop < threshold),主动触发一次加载更多检查 + if (props.hasMore && !oldProps.hasMore) { + this.isExternalLoading.value = false; + this.waitingForDOMUpdate = false; + requestAnimationFrame(() => { + if (this.isAutoScrollEnabled) { + this.scrollList({ to: 'bottom' }); + // 更新缓存,防止 checkAutoScroll 误判 + const list = this.listRef.current; + if (list) { + this.scrollHeightTmp = list.scrollHeight; + this.scrollTopTmp = list.scrollTop; + } + } + this.checkExternalLoadMore(); + }); + } + } + ready(): void { const { defaultScrollTo } = this.props; defaultScrollTo === 'bottom' && (this.isAutoScrollEnabled = true); @@ -127,11 +199,54 @@ export default class Chatlist extends Component { if (inner) { this.observer.observe(inner); } + + this.initPagingObserver(); + + // defaultScrollTo="bottom" 的初始滚动: + // 确保外部数据分页场景下,初始位置能正确滚动到底部 + if (defaultScrollTo === 'bottom') { + requestAnimationFrame(() => { + this.scrollList({ to: 'bottom' }); + // 初始化完成后记录滚动状态,防止 checkAutoScroll 误判 + this.scrollHeightTmp = list?.scrollHeight || 0; + this.scrollTopTmp = list?.scrollTop || 0; + }); + } + setExportparts(this); } + /** + * 数据分页:初始化 MutationObserver,监听子元素变化以自动维持滚动位置 + */ + initPagingObserver(): void { + const list = this.listRef.current; + + this.externalPagingObserver = new MutationObserver(() => { + if (isNil(this.props.hasMore)) return; + if (!this.waitingForDOMUpdate) return; + this.waitingForDOMUpdate = false; + this.isExternalLoading.value = false; + + // 在 DOM 更新后修正滚动位置,避免跳动 + const newScrollHeight = list?.scrollHeight || 0; + const scrollDiff = newScrollHeight - this.prevScrollHeightBeforeLoad; + if (list && scrollDiff > 0) { + list.scrollTop = this.prevScrollTopBeforeLoad + scrollDiff; + // 更新缓存,防止 checkAutoScroll 误判 + this.scrollHeightTmp = list.scrollHeight; + this.scrollTopTmp = list.scrollTop; + } + }); + this.externalPagingObserver.observe(this, { + childList: true, + subtree: false, + }); + } + uninstall(): void { - this.observer.disconnect(); + this.observer?.disconnect(); + this.externalPagingObserver?.disconnect(); } render() { @@ -152,6 +267,16 @@ export default class Chatlist extends Component {
+ {this.props.hasMore && ( + +
+ {this.isExternalLoading.value ? : null} + + {this.isExternalLoading.value ? '加载中...' : '上滑加载更多'} + +
+
+ )}
diff --git a/src/chatbot/chat.tsx b/src/chatbot/chat.tsx index da282cce..e0356dc8 100644 --- a/src/chatbot/chat.tsx +++ b/src/chatbot/chat.tsx @@ -261,13 +261,6 @@ export default class Chatbot extends Component implements TdChatbot } } - /** - * 选择文件 - */ - selectFile() { - this.ChatSenderRef.current.selectFile(); - } - /** * 最后一条AI消息 */ @@ -282,6 +275,13 @@ export default class Chatbot extends Component implements TdChatbot this.listRef.current?.scrollList(options); } + /** + * 选择文件 + */ + selectFile() { + this.ChatSenderRef.current?.selectFile(); + } + /** * 订阅聊天状态变化 */ diff --git a/src/chatbot/style/_var.less b/src/chatbot/style/_var.less index 25d67669..7182de26 100644 --- a/src/chatbot/style/_var.less +++ b/src/chatbot/style/_var.less @@ -30,6 +30,7 @@ --td-chat-item-header-padding: 0; --td-chat-item-text-header-padding: 0; --td-chat-item-header-padding-right: 0; + --td-chat-item-right-content-margin: 0 0 0 auto; --td-chat-item-name-padding: 0 var(--td-comp-paddingLR-s) 0 0; --td-chat-item-name-padding-right: 0 0 0 var(--td-comp-paddingLR-l); --td-chat-item-name-font: var(--td-font-body-medium); @@ -176,18 +177,21 @@ --td-chat-md-code-radius: 8px; --td-chat-md-code-margin: 0 0 16px 0; - --td-chat-md-code-header-height: 36px; - --td-chat-md-code-header-padding: 0 16px 0 12px; - --td-chat-md-code-header-font: 500 14px / 1.6 @font-family; - --td-chat-md-code-header-bg: #f3f3f3; - --td-chat-md-code-header-bg-dark: #282c34; - --td-chat-md-code-header-color-dark: #abb2bf; - --td-chat-md-code-header-copy-color: var(--td-gray-color-7); - --td-chat-md-code-header-copy-hover-color: var(--td-gray-color-9); - --td-chat-md-code-body-padding: 10px 16px !important; - --td-chat-md-code-body-bg: #f8f8f8; - --td-chat-md-code-body-overflow: auto; - --td-chat-md-code-body-code-font-size: 14px; + --td-chat-md-code-button-display: flex; + --td-chat-md-code-button-align-items: center; + --td-chat-md-code-button-justify-content: center; + --td-chat-md-code-button-top: 30px; + --td-chat-md-code-button-border: none; + --td-chat-md-code-button-color: var(--td-gray-color-7); + --td-chat-md-code-button-hover-color: var(--td-gray-color-9); + --td-chat-md-code-button-bg: hsl(230, 1%, 98%); + --td-chat-md-code-button-hover-bg: rgba(255, 255, 255, 0.7); + --td-chat-md-code-button-dark-color: rgba(255, 255, 255, 0.7); + --td-chat-md-code-button-dark-hover-color: rgba(255, 255, 255, 0.9); + --td-chat-md-code-button-dark-bg: hsl(220, 13%, 18%); + --td-chat-md-code-button-dark-hover-bg: rgba(255, 255, 255, 0.15); + --td-chat-md-code-button-shadow: none; + --td-chat-md-code-button-hover-transform: none; --td-chat-md-katex-block-margin: 1em 0; --td-chat-md-katex-font: normal 1.21em / 1.2 KaTeX_Main, Times New Roman, serif; diff --git a/src/chatbot/style/chat-list.less b/src/chatbot/style/chat-list.less index 3727cc11..09eaa4fe 100644 --- a/src/chatbot/style/chat-list.less +++ b/src/chatbot/style/chat-list.less @@ -57,4 +57,31 @@ box-shadow: var(--td-chat-list-scroll-button-shadow); cursor: pointer; } + + &__load-more { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 12px 0; + + &__spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--td-brand-color-light, #ecf2fe); + border-top-color: var(--td-brand-color, #0052d9); + border-radius: 50%; + animation: t-chat-list-spin 0.8s linear infinite; + } + + &__text { + font-size: var(--td-font-size-body-small, 12px); + color: var(--td-text-color-placeholder, rgba(0, 0, 0, 0.35)); + } + } + + @keyframes t-chat-list-spin { + to { transform: rotate(360deg); } + } } \ No newline at end of file diff --git a/src/chatbot/type.ts b/src/chatbot/type.ts index 36ff8aa5..38027d90 100644 --- a/src/chatbot/type.ts +++ b/src/chatbot/type.ts @@ -94,6 +94,11 @@ export interface TdChatbotApi { */ scrollList: (opt?: TdChatListScrollToOptions) => void; + /** + * 触发文件选择 + */ + selectFile: () => void; + /** * 获取当前消息列表 */ @@ -126,10 +131,6 @@ export interface TdChatbotApi { type: T['type'], // 使用类型中定义的type字段作为参数类型 handler: (chunk: T, existing?: T) => T, ) => void; - /** - * 触发选择文件 - */ - selectFile: () => void; } export type TdChatMessageConfigItem = Omit; @@ -146,7 +147,12 @@ export interface TdChatListProps { autoScroll?: boolean; /** 初始滚动条停留的位置 */ defaultScrollTo?: ScrollPosition; + /** 外部数据分页:是否还有更多历史数据可加载。设置后启用外部数据分页模式 */ + hasMore?: boolean; + /** 外部数据分页:触发加载更多的滚动阈值(距离顶部的像素值),默认为 80 */ + loadMoreThreshold?: number; onScroll?: (e: CustomEvent<{ scrollTop: number }>) => void; + onLoadMore?: () => void; } export interface TdChatListApi { diff --git a/src/collapse/collapse-panel.tsx b/src/collapse/collapse-panel.tsx index 36273bec..3aba10a5 100644 --- a/src/collapse/collapse-panel.tsx +++ b/src/collapse/collapse-panel.tsx @@ -156,7 +156,7 @@ export default class CollapsePanel extends Component { } renderBody() { - const isActive = this.injection.getCollapseValue().includes(this.innerValue.value); + const isActive = this.injection.getCollapseValue?.().includes(this.innerValue.value); const { destroyOnCollapse } = this.props; if (this.afterLeaved.value === null && !isActive) { return null; diff --git a/src/filecard/filecard.tsx b/src/filecard/filecard.tsx index f513c628..5f631b9f 100644 --- a/src/filecard/filecard.tsx +++ b/src/filecard/filecard.tsx @@ -270,18 +270,29 @@ export default class FileCard extends Component { private renderFileOverview(namePrefix: string, nameSuffix: string, icon: any, iconColor: string, desc: string) { const { cardType, item, style } = this.props; if (cardType === 'image') { + const imageWidth = 'var(--td-attachment-image-width, 52px)'; + const imageHeight = 'var(--td-attachment-image-height, 52px)'; + const iconSize = `calc(min(${imageWidth}, ${imageHeight}) * 0.35)`; const newstyle = { - ...{ - width: 'var(--td-attachment-image-width, 52px)', - height: 'var(--td-attachment-image-height, 52px)', - }, + width: imageWidth, + height: imageHeight, ...style, }; + return item.url ? ( - + } + error={} + /> ) : (
- +
); } diff --git a/src/index.ts b/src/index.ts index d9cfcc50..8f78d124 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,48 @@ -export * from './affix'; -export * from './alert'; -export * from './avatar'; -export * from './back-top'; -export * from './badge'; -export * from './breadcrumb'; -export * from './button'; -export * from './card'; -export * from './checkbox'; -export * from './collapse'; -export * from './comment'; -export * from './common'; -export * from './date-picker'; -export * from './dialog'; -export * from './divider'; -export * from './dropdown'; -export * from './grid'; -export * from './image'; -export * from './input'; -export * from './input-number'; -export * from './link'; -export * from './list'; -export * from './loading'; -export * from './menu'; -export * from './message'; -export * from './notification'; -export * from './popconfirm'; -export * from './popup'; -export * from './progress'; -export * from './radio'; -export * from './range-input'; -export * from './select'; -export * from './select-input'; -export * from './skeleton'; -export * from './slider'; -export * from './space'; -export * from './swiper'; -export * from './switch'; -export * from './tabs'; -export * from './tag'; -export * from './tag-input'; -export * from './textarea'; -export * from './tooltip'; -export * from './upload'; -export * from './watermark'; +// export * from './affix'; +// export * from './alert'; +// export * from './avatar'; +// export * from './back-top'; +// export * from './badge'; +// export * from './breadcrumb'; +// export * from './button'; +// export * from './card'; +// export * from './checkbox'; +// export * from './collapse'; +// export * from './comment'; +// export * from './common'; +// export * from './date-picker'; +// export * from './dialog'; +// export * from './divider'; +// export * from './dropdown'; +// export * from './grid'; +// export * from './image'; +// export * from './input'; +// export * from './input-number'; +// export * from './link'; +// export * from './list'; +// export * from './loading'; +// export * from './menu'; +// export * from './message'; +// export * from './notification'; +// export * from './popconfirm'; +// export * from './popup'; +// export * from './progress'; +// export * from './radio'; +// export * from './range-input'; +// export * from './select'; +// export * from './select-input'; +// export * from './skeleton'; +// export * from './slider'; +// export * from './space'; +// export * from './swiper'; +// export * from './switch'; +// export * from './tabs'; +// export * from './tag'; +// export * from './tag-input'; +// export * from './textarea'; +// export * from './tooltip'; +// export * from './upload'; +// export * from './watermark'; export * from './chat-sender'; export * from './chatbot'; diff --git a/src/style/cherry-icon-font.less b/src/style/cherry-icon-font.less new file mode 100644 index 00000000..fff9eb8e --- /dev/null +++ b/src/style/cherry-icon-font.less @@ -0,0 +1,147 @@ +/** + * Cherry Markdown Icon Font + * 用于在 Light DOM 中加载字体,解决 Shadow DOM 中 @font-face 无法加载的问题 + * 打包后字体文件会输出到 lib/assets/ 目录,所以使用 ../assets/ 路径 + */ + +@font-face { + font-family: "ch-icon"; + src: url("../assets/ch-icon.eot"); + src: url("../assets/ch-icon.eot?#iefix") format("eot"), + url("../assets/ch-icon.woff2") format("woff2"), + url("../assets/ch-icon.woff") format("woff"), + url("../assets/ch-icon.ttf") format("truetype"), + url("../assets/ch-icon.svg#ch-icon") format("svg"); + font-weight: normal; + font-style: normal; +} + +.ch-icon:before { + display: inline-block; + font-family: "ch-icon"; + font-style: normal; + font-weight: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.ch-icon-list:before { content: "\ea03"; } +.ch-icon-check:before { content: "\ea04"; } +.ch-icon-square:before { content: "\ea09"; } +.ch-icon-bold:before { content: "\ea0a"; } +.ch-icon-code:before { content: "\ea0b"; } +.ch-icon-color:before { content: "\ea0c"; } +.ch-icon-header:before { content: "\ea0d"; } +.ch-icon-image:before { content: "\ea0e"; } +.ch-icon-italic:before { content: "\ea0f"; } +.ch-icon-link:before { content: "\ea10"; } +.ch-icon-ol:before { content: "\ea11"; } +.ch-icon-size:before { content: "\ea12"; } +.ch-icon-strike:before { content: "\ea13"; } +.ch-icon-table:before { content: "\ea14"; } +.ch-icon-ul:before { content: "\ea15"; } +.ch-icon-underline:before { content: "\ea16"; } +.ch-icon-word:before { content: "\ea17"; } +.ch-icon-blockquote:before { content: "\ea18"; } +.ch-icon-font:before { content: "\ea19"; } +.ch-icon-insertClass:before { content: "\ea1f"; } +.ch-icon-insertFlow:before { content: "\ea20"; } +.ch-icon-insertFormula:before { content: "\ea21"; } +.ch-icon-insertGantt:before { content: "\ea22"; } +.ch-icon-insertGraph:before { content: "\ea23"; } +.ch-icon-insertPie:before { content: "\ea24"; } +.ch-icon-insertSeq:before { content: "\ea25"; } +.ch-icon-insertState:before { content: "\ea26"; } +.ch-icon-line:before { content: "\ea27"; } +.ch-icon-preview:before { content: "\ea28"; } +.ch-icon-previewClose:before { content: "\ea29"; } +.ch-icon-toc:before { content: "\ea2a"; } +.ch-icon-sub:before { content: "\ea2d"; } +.ch-icon-sup:before { content: "\ea2e"; } +.ch-icon-h1:before { content: "\ea2f"; } +.ch-icon-h2:before { content: "\ea30"; } +.ch-icon-h3:before { content: "\ea31"; } +.ch-icon-h4:before { content: "\ea32"; } +.ch-icon-h5:before { content: "\ea33"; } +.ch-icon-h6:before { content: "\ea34"; } +.ch-icon-video:before { content: "\ea35"; } +.ch-icon-insert:before { content: "\ea36"; } +.ch-icon-little_table:before { content: "\ea37"; } +.ch-icon-pdf:before { content: "\ea38"; } +.ch-icon-checklist:before { content: "\ea39"; } +.ch-icon-close:before { content: "\ea40"; } +.ch-icon-fullscreen:before { content: "\ea41"; } +.ch-icon-minscreen:before { content: "\ea42"; } +.ch-icon-insertChart:before { content: "\ea43"; } +.ch-icon-question:before { content: "\ea44"; } +.ch-icon-settings:before { content: "\ea45"; } +.ch-icon-ok:before { content: "\ea46"; } +.ch-icon-br:before { content: "\ea47"; } +.ch-icon-normal:before { content: "\ea48"; } +.ch-icon-undo:before { content: "\ea49"; } +.ch-icon-redo:before { content: "\ea50"; } +.ch-icon-copy:before { content: "\ea51"; } +.ch-icon-phone:before { content: "\ea52"; } +.ch-icon-cherry-table-delete:before { content: "\ea53"; } +.ch-icon-cherry-table-insert-bottom:before { content: "\ea54"; } +.ch-icon-cherry-table-insert-left:before { content: "\ea55"; } +.ch-icon-cherry-table-insert-right:before { content: "\ea56"; } +.ch-icon-cherry-table-insert-top:before { content: "\ea57"; } +.ch-icon-sort-s:before { content: "\ea58"; } +.ch-icon-pinyin:before { content: "\ea59"; } +.ch-icon-create:before { content: "\ea5a"; } +.ch-icon-download:before { content: "\ea5b"; } +.ch-icon-edit:before { content: "\ea5c"; } +.ch-icon-export:before { content: "\ea5d"; } +.ch-icon-folder-open:before { content: "\ea5e"; } +.ch-icon-folder:before { content: "\ea5f"; } +.ch-icon-help:before { content: "\ea60"; } +.ch-icon-pen-fill:before { content: "\ea61"; } +.ch-icon-pen:before { content: "\ea62"; } +.ch-icon-tips:before { content: "\ea64"; } +.ch-icon-warn:before { content: "\ea65"; } +.ch-icon-mistake:before { content: "\ea66"; } +.ch-icon-success:before { content: "\ea67"; } +.ch-icon-danger:before { content: "\ea68"; } +.ch-icon-info:before { content: "\ea69"; } +.ch-icon-primary:before { content: "\ea6a"; } +.ch-icon-warning:before { content: "\ea6b"; } +.ch-icon-justify:before { content: "\ea6c"; } +.ch-icon-justifyCenter:before { content: "\ea6d"; } +.ch-icon-justifyLeft:before { content: "\ea6e"; } +.ch-icon-justifyRight:before { content: "\ea6f"; } +.ch-icon-chevronsLeft:before { content: "\ea70"; } +.ch-icon-chevronsRight:before { content: "\ea71"; } +.ch-icon-trendingUp:before { content: "\ea72"; } +.ch-icon-codeBlock:before { content: "\ea74"; } +.ch-icon-expand:before { content: "\ea75"; } +.ch-icon-unExpand:before { content: "\ea76"; } +.ch-icon-swap-vert:before { content: "\ea77"; } +.ch-icon-swap:before { content: "\ea78"; } +.ch-icon-keyboard:before { content: "\ea79"; } +.ch-icon-command:before { content: "\ea7a"; } +.ch-icon-search:before { content: "\ea7b"; } +.ch-icon-alignCenter:before { content: "\ea7c"; } +.ch-icon-alignJustify:before { content: "\ea7d"; } +.ch-icon-alignLeft:before { content: "\ea7e"; } +.ch-icon-alignRight:before { content: "\ea7f"; } +.ch-icon-align:before { content: "\ea80"; } +.ch-icon-imgDecoBorder:before { content: "\ea81"; } +.ch-icon-imgDecoShadow:before { content: "\ea82"; } +.ch-icon-imgDecoRadius:before { content: "\ea83"; } +.ch-icon-imgAlignLeft:before { content: "\ea84"; } +.ch-icon-imgAlignCenter:before { content: "\ea85"; } +.ch-icon-imgAlignRight:before { content: "\ea86"; } +.ch-icon-imgAlignFloatLeft:before { content: "\ea87"; } +.ch-icon-imgAlignFloatRight:before { content: "\ea88"; } +.ch-icon-insertLineChart:before { content: "\ea8a"; } +.ch-icon-insertBarChart:before { content: "\ea8b"; } +.ch-icon-insertRadarChart:before { content: "\ea8c"; } +.ch-icon-insertMapChart:before { content: "\ea8d"; } +.ch-icon-insertHeatmapChart:before { content: "\ea8e"; } +.ch-icon-insertPieChart:before { content: "\ea8f"; } +.ch-icon-insertScatterChart:before { content: "\ea90"; } +.ch-icon-insertSankeyChart:before { content: "\ea91"; } +.ch-icon-menu:before { content: "\ea92"; } +.ch-icon-main-theme:before { content: "\ea94"; } +.ch-icon-code-theme:before { content: "\ea95"; } diff --git a/src/style/index.js b/src/style/index.js index 6fffd444..34928c00 100644 --- a/src/style/index.js +++ b/src/style/index.js @@ -5,13 +5,15 @@ import popupStyle from '../_common/style/web/components/popup/_index.less'; import theme from '../_common/style/web/theme/_index.less'; import chatSenderVars from '../chat-sender/style/_var.less'; import chatVars from '../chatbot/style/_var.less'; +// Cherry Markdown 字体 - 需要在 Light DOM 中加载,解决 Shadow DOM 字体加载问题 +import cherryIconFont from './cherry-icon-font.less'; const tdesignGlobalStyleId = 'tdesign-wc-global-style'; if (!document.getElementById(tdesignGlobalStyleId)) { const style = document.createElement('style'); style.id = tdesignGlobalStyleId; - style.innerHTML = `${theme}${popupStyle}`; + style.innerHTML = `${theme}${popupStyle}${cherryIconFont}`; document.head.appendChild(style); }