feat(*): 画布助手 - 新增 Live2D 配置导入与模型渲染支持

This commit is contained in:
ddCat
2026-09-21 14:11:29 +08:00
parent 4f9e79f67f
commit 4e0b0a9256
40 changed files with 1668 additions and 59 deletions

View File

@@ -50,6 +50,8 @@
"mammoth": "^1.12.0",
"motion": "^12.42.2",
"nanoid": "^6.0.0",
"pixi-live2d-display": "0.4.0",
"pixi.js": "6.5.10",
"react": "19.2.7",
"react-aria-components": "^1.21.1",
"react-dom": "19.2.7",
@@ -532,6 +534,78 @@
"@pierre/theming": ["@pierre/theming@0.0.2", "https://registry.npmmirror.com/@pierre/theming/-/theming-0.0.2.tgz", { "peerDependencies": { "@pierre/theme": "^1.1.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw=="],
"@pixi/accessibility": ["@pixi/accessibility@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-URrI1H+1kjjHSyhY1QXcUZ8S3omdVTrXg5y0gndtpOhIelErBTC9NWjJfw6s0Rlmv5+x5VAitQTgw9mRiatDgw=="],
"@pixi/app": ["@pixi/app@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-VsNHLajZ5Dbc/Zrj7iWmIl3eu6Fec+afjW/NXXezD8Sp3nTDF0bv5F+GDgN/zSc2gqIvPHyundImT7hQGBDghg=="],
"@pixi/compressed-textures": ["@pixi/compressed-textures@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/loaders": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-41NT5mkfam47DrkB8xMp3HUZDt7139JMB6rVNOmb3u2vm+2mdy9tzi5s9nN7bG9xgXlchxcFzytTURk+jwXVJA=="],
"@pixi/constants": ["@pixi/constants@6.5.10", "", {}, "sha512-PUF2Y9YISRu5eVrVVHhHCWpc/KmxQTg3UH8rIUs8UI9dCK41/wsPd3pEahzf7H47v7x1HCohVZcFO3XQc1bUDw=="],
"@pixi/core": ["@pixi/core@6.5.10", "", { "dependencies": { "@types/offscreencanvas": "^2019.6.4" }, "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/extensions": "6.5.10", "@pixi/math": "6.5.10", "@pixi/runner": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/ticker": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-Gdzp5ENypyglvsh5Gv3teUZnZnmizo4xOsL+QqmWALdFlJXJwLJMVhKVThV/q/095XR6i4Ou54oshn+m4EkuFw=="],
"@pixi/display": ["@pixi/display@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/math": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-NxFdDDxlbH5fQkzGHraLGoTMucW9pVgXqQm13TSmkA3NWIi/SItHL4qT2SI8nmclT9Vid1VDEBCJFAbdeuQw1Q=="],
"@pixi/extensions": ["@pixi/extensions@6.5.10", "", {}, "sha512-EIUGza+E+sCy3dupuIjvRK/WyVyfSzHb5XsxRaxNrPwvG1iIUIqNqZ3owLYCo4h17fJWrj/yXVufNNtUKQccWQ=="],
"@pixi/extract": ["@pixi/extract@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/math": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-hXFIc4EGs14GFfXAjT1+6mzopzCMWeXeai38/Yod3vuBXkkp8+ksen6kE09vTnB9l1IpcIaCM+XZEokuqoGX2A=="],
"@pixi/filter-alpha": ["@pixi/filter-alpha@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10" } }, "sha512-GWHLJvY0QOIDRjVx0hdUff6nl/PePQg84i8XXPmANrvA+gJ/eSRTQRmQcdgInQfawENADB/oRqpcCct6IAcKpQ=="],
"@pixi/filter-blur": ["@pixi/filter-blur@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/settings": "6.5.10" } }, "sha512-LJsRocVOdM9hTzZKjP+jmkfoL1nrJi5XpR0ItgRN8fflOC7A7Ln4iPe7nukbbq3H7QhZSunbygMubbO6xhThZw=="],
"@pixi/filter-color-matrix": ["@pixi/filter-color-matrix@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10" } }, "sha512-C2S44/EoWTrhqedLWOZTq9GZV5loEq1+MhyK9AUzEubWGMHhou1Juhn2mRZ7R6flKPCRQNKrXpStUwCAouud3Q=="],
"@pixi/filter-displacement": ["@pixi/filter-displacement@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/math": "6.5.10" } }, "sha512-fbblMYyPX/hO3Tpoaa4tOBYxqp4TxjNrz6xyt15tKSVxWQElk+Tx98GJ+aaBoiHOKt8ezzHplStWoHG++JIv/w=="],
"@pixi/filter-fxaa": ["@pixi/filter-fxaa@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10" } }, "sha512-wbHL9UtY3g7jTyvO8JaZks6DqV8AO5c96Hfu0zfndWBPs79Ul6/sq3LD2eE+yq5vK5T2R9Sr4s54ls1JT3Sppg=="],
"@pixi/filter-noise": ["@pixi/filter-noise@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10" } }, "sha512-CX+/06NVaw3HsjipZVb7aemkca0TC8I6qfKI4lx2ugxS/6G6zkY5zqd8+nVSXW4DpUXB6eT0emwfRv6N00NvuA=="],
"@pixi/graphics": ["@pixi/graphics@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-KPHGJ910fi8bRQQ+VcTIgrK+bKIm8yAQaZKPqMtm14HzHPGcES6HkgeNY1sd7m8J4aS9btm5wOSyFu0p5IzTpA=="],
"@pixi/interaction": ["@pixi/interaction@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/ticker": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-v809pJmXA2B9dV/vdrDMUqJT+fBB/ARZli2YRmI2dPbEbkaYr8FNmxCAJnwT8o+ymTx044Ie820hn9tVrtMtfA=="],
"@pixi/loaders": ["@pixi/loaders@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-AuK7mXBmyVsDFL9DDFPB8sqP8fwQ2NOktvu98bQuJl0/p/UeK/0OAQnF3wcf3FeBv5YGXfNHL21c2DCisjKfTg=="],
"@pixi/math": ["@pixi/math@6.5.10", "", {}, "sha512-fxeu7ykVbMGxGV2S3qRTupHToeo1hdWBm8ihyURn3BMqJZe2SkZEECPd5RyvIuuNUtjRnmhkZRnF3Jsz2S+L0g=="],
"@pixi/mesh": ["@pixi/mesh@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-tUNPsdp5/t/yRsCmfxIcufIfbQVzgAlMNgQ1igWOkSxzhB7vlEbZ8ZLLW5tQcNyM/r7Nhjz+RoB+RD+/BCtvlA=="],
"@pixi/mesh-extras": ["@pixi/mesh-extras@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/math": "6.5.10", "@pixi/mesh": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-UCG7OOPPFeikrX09haCibCMR0jPQ4UJ+4HiYiAv/3dahq5eEzBx+yAwVtxcVCjonkTf/lu5SzmHdzpsbHLx5aw=="],
"@pixi/mixin-cache-as-bitmap": ["@pixi/mixin-cache-as-bitmap@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-HV4qPZt8R7uuPZf1XE5S0e3jbN4+/EqgAIkueIyK3Em+0IO1rCmIbzzYxFPxkElMUu5VvN1r4hXK846z9ITnhw=="],
"@pixi/mixin-get-child-by-name": ["@pixi/mixin-get-child-by-name@6.5.10", "", { "peerDependencies": { "@pixi/display": "6.5.10" } }, "sha512-YYd9wjnI/4aKY0H5Ij413UppVZn3YE1No2CZrNevV6WbhylsJucowY3hJihtl9mxkpwtaUIyWMjmphkbOinbzA=="],
"@pixi/mixin-get-global-position": ["@pixi/mixin-get-global-position@6.5.10", "", { "peerDependencies": { "@pixi/display": "6.5.10", "@pixi/math": "6.5.10" } }, "sha512-A83gTZP9CdQAyrAvOZl1P707Q0QvIC0V8UnBAMd4GxuhMOXJtXVPCdmfPVXUrfoywgnH+/Bgimq5xhsXTf8Hzg=="],
"@pixi/particle-container": ["@pixi/particle-container@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-CCNAdYGzKoOc3FtK2kyWCNjygdHppeOEqqK189yhg3yRSsvby+HMms/cM6bLK/4Vf6mFoAy1na3w/oXpqTR2Ag=="],
"@pixi/polyfill": ["@pixi/polyfill@6.5.10", "", { "dependencies": { "object-assign": "^4.1.1", "promise-polyfill": "^8.2.0" } }, "sha512-KDTWyr285VvPM8GGTVIZAhmxGrOlTznUGK/9kWS3GtrogwLWn41S/86Yej1gYvotVyUomCcOok33Jzahb+vX1w=="],
"@pixi/prepare": ["@pixi/prepare@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/graphics": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/text": "6.5.10", "@pixi/ticker": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-PHMApz/GPg7IX/7+2S98criN2+Mp+fgiKpojV9cnl0SlW2zMxfAHBBi8zik9rHBgjx8X6d6bR0MG1rPtb6vSxQ=="],
"@pixi/runner": ["@pixi/runner@6.5.10", "", {}, "sha512-4HiHp6diCmigJT/DSbnqQP62OfWKmZB7zPWMdV1AEdr4YT1QxzXAW1wHg7dkoEfyTHqZKl0tm/zcqKq/iH7tMA=="],
"@pixi/settings": ["@pixi/settings@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10" } }, "sha512-ypAS5L7pQ2Qb88yQK72bXtc7sD8OrtLWNXdZ/gnw5kwSWCFaOSoqhKqJCXrR5DQtN98+RQefwbEAmMvqobhFyw=="],
"@pixi/sprite": ["@pixi/sprite@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-UiK+8LgM9XQ/SBDKjRgZ8WggdOSlFRXqiWjEZVmNkiyU8HvXeFzWPRhpc8RR1zDwAUhZWKtMhF8X/ba9m+z2lg=="],
"@pixi/sprite-animated": ["@pixi/sprite-animated@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/ticker": "6.5.10" } }, "sha512-x1kayucAqpVbNk+j+diC/7sQGQsAl6NCH1J2/EEaiQjlV3GOx1MXS9Tft1N1Y1y7otbg1XsnBd60/Yzcp05pxA=="],
"@pixi/sprite-tiling": ["@pixi/sprite-tiling@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/math": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-lDFcPuwExrdJhli+WmjPivChjeCG6NiRl36iQ8n2zVi/MYVv9qfKCA6IdU7HBWk1AZdsg6KUTpwfmVLUI+qz3w=="],
"@pixi/spritesheet": ["@pixi/spritesheet@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/loaders": "6.5.10", "@pixi/math": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-7uOZ1cYyYtPb0ZEgXV1SZ8ujtluZNY0TL5z3+Qc8cgGGZK/MaWG7N6Wf+uR4BR2x8FLNwcyN5IjbQDKCpblrmg=="],
"@pixi/text": ["@pixi/text@6.5.10", "", { "peerDependencies": { "@pixi/core": "6.5.10", "@pixi/math": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-ikwkonLJ+6QmEVW8Ji9fS5CjrKNbU4mHzYuwRQas/VJQuSWgd0myCcaw6ZbF1oSfQe70HgbNOR0sH8Q3Com0qg=="],
"@pixi/text-bitmap": ["@pixi/text-bitmap@6.5.10", "", { "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/loaders": "6.5.10", "@pixi/math": "6.5.10", "@pixi/mesh": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/text": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-g/iFIMGp6Pfi0BvX6Ykp48Z6JXVgKOrc7UCIR9CM21wYcCiQGqtdFwstV236xk6/D8NToUtSOcifhtQ28dVTdQ=="],
"@pixi/ticker": ["@pixi/ticker@6.5.10", "", { "peerDependencies": { "@pixi/extensions": "6.5.10", "@pixi/settings": "6.5.10" } }, "sha512-UqX1XYtzqFSirmTOy8QAK4Ccg4KkIZztrBdRPKwFSOEiKAJoGDCSBmyQBo/9aYQKGObbNnrJ7Hxv3/ucg3/1GA=="],
"@pixi/utils": ["@pixi/utils@6.5.10", "", { "dependencies": { "@types/earcut": "^2.1.0", "earcut": "^2.2.4", "eventemitter3": "^3.1.0", "url": "^0.11.0" }, "peerDependencies": { "@pixi/constants": "6.5.10", "@pixi/settings": "6.5.10" } }, "sha512-4f4qDMmAz9IoSAe08G2LAxUcEtG9jSdudfsMQT2MG+OpfToirboE6cNoO0KnLCvLzDVE/mfisiQ9uJbVA9Ssdw=="],
"@primer/octicons": ["@primer/octicons@19.33.0", "https://registry.npmmirror.com/@primer/octicons/-/octicons-19.33.0.tgz", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-kYrlutFnNVWDgckjQwOv9kXiFIPjTD/N+DIx6qzMBAk82nGTE85UKSvFvbt/RUhviNEoCjFoEr4fn5EAbkkUzQ=="],
"@radix-ui/number": ["@radix-ui/number@1.1.3", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.3.tgz", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="],
@@ -1008,6 +1082,8 @@
"@types/draco3d": ["@types/draco3d@1.4.10", "https://registry.npmmirror.com/@types/draco3d/-/draco3d-1.4.10.tgz", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
"@types/earcut": ["@types/earcut@2.1.4", "", {}, "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ=="],
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -1144,12 +1220,18 @@
"aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="],
"array-uniq": ["array-uniq@1.0.3", "", {}, "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q=="],
"assign-symbols": ["assign-symbols@1.0.0", "https://registry.npmmirror.com/assign-symbols/-/assign-symbols-1.0.0.tgz", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
"ast-types": ["ast-types@0.16.1", "https://registry.npmmirror.com/ast-types/-/ast-types-0.16.1.tgz", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"astring": ["astring@1.9.0", "https://registry.npmmirror.com/astring/-/astring-1.9.0.tgz", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="],
"asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"attr-accept": ["attr-accept@2.2.5", "https://registry.npmmirror.com/attr-accept/-/attr-accept-2.2.5.tgz", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
@@ -1252,6 +1334,8 @@
"commander": ["commander@14.0.3", "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -1414,6 +1498,8 @@
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="],
"eciesjs": ["eciesjs@0.4.18", "https://registry.npmmirror.com/eciesjs/-/eciesjs-0.4.18.tgz", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ=="],
"ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
@@ -1422,6 +1508,8 @@
"elkjs": ["elkjs@0.11.1", "https://registry.npmmirror.com/elkjs/-/elkjs-0.11.1.tgz", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="],
"email-addresses": ["email-addresses@3.1.0", "", {}, "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg=="],
"emoji-mart": ["emoji-mart@5.6.0", "https://registry.npmmirror.com/emoji-mart/-/emoji-mart-5.6.0.tgz", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="],
"emoji-regex": ["emoji-regex@10.6.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -1536,12 +1624,18 @@
"file-selector": ["file-selector@0.5.0", "https://registry.npmmirror.com/file-selector/-/file-selector-0.5.0.tgz", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="],
"filename-reserved-regex": ["filename-reserved-regex@2.0.0", "", {}, "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ=="],
"filenamify": ["filenamify@4.3.0", "", { "dependencies": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.1", "trim-repeated": "^1.0.0" } }, "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg=="],
"fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"filter-obj": ["filter-obj@5.1.0", "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="],
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="],
"find-root": ["find-root@1.1.0", "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -1568,6 +1662,8 @@
"fs-extra": ["fs-extra@11.3.5", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -1592,12 +1688,18 @@
"get-value": ["get-value@2.0.6", "https://registry.npmmirror.com/get-value/-/get-value-2.0.6.tgz", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
"gh-pages": ["gh-pages@4.0.0", "", { "dependencies": { "async": "^2.6.1", "commander": "^2.18.0", "email-addresses": "^3.0.1", "filenamify": "^4.3.0", "find-cache-dir": "^3.3.1", "fs-extra": "^8.1.0", "globby": "^6.1.0" }, "bin": { "gh-pages": "bin/gh-pages.js", "gh-pages-clean": "bin/gh-pages-clean.js" } }, "sha512-p8S0T3aGJc68MtwOcZusul5qPSNZCalap3NWbhRUZYu1YOdp+EjZ+4kPmRM8h3NNRdqw00yuevRjlkuSzCn7iQ=="],
"giscus": ["giscus@1.6.0", "https://registry.npmmirror.com/giscus/-/giscus-1.6.0.tgz", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"globby": ["globby@6.1.0", "", { "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", "object-assign": "^4.0.1", "pify": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw=="],
"glsl-noise": ["glsl-noise@0.0.0", "https://registry.npmmirror.com/glsl-noise/-/glsl-noise-0.0.0.tgz", {}, "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w=="],
"glur": ["glur@1.1.2", "https://registry.npmmirror.com/glur/-/glur-1.1.2.tgz", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="],
@@ -1688,6 +1790,8 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
@@ -1888,6 +1992,8 @@
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
"mammoth": ["mammoth@1.12.0", "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
@@ -2104,6 +2210,8 @@
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"package-manager-detector": ["package-manager-detector@1.8.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.8.0.tgz", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
"pako": ["pako@2.0.3", "https://registry.npmmirror.com/pako/-/pako-2.0.3.tgz", {}, "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="],
@@ -2144,8 +2252,20 @@
"picomatch": ["picomatch@4.0.5", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pinkie": ["pinkie@2.0.4", "", {}, "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg=="],
"pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="],
"pixi-live2d-display": ["pixi-live2d-display@0.4.0", "", { "dependencies": { "gh-pages": "^4.0.0" }, "peerDependencies": { "@pixi/core": "^6", "@pixi/display": "^6", "@pixi/loaders": "^6", "@pixi/math": "^6", "@pixi/sprite": "^6", "@pixi/utils": "^6" } }, "sha512-xeYC6y4Y0Bxe9ksWNlGFZC1rII/MPrzPQK7t1c3ubA8RhkOISIqHJl38fNumXqhGEs+yItmgDOkFT+9dsyGDjA=="],
"pixi.js": ["pixi.js@6.5.10", "", { "dependencies": { "@pixi/accessibility": "6.5.10", "@pixi/app": "6.5.10", "@pixi/compressed-textures": "6.5.10", "@pixi/constants": "6.5.10", "@pixi/core": "6.5.10", "@pixi/display": "6.5.10", "@pixi/extensions": "6.5.10", "@pixi/extract": "6.5.10", "@pixi/filter-alpha": "6.5.10", "@pixi/filter-blur": "6.5.10", "@pixi/filter-color-matrix": "6.5.10", "@pixi/filter-displacement": "6.5.10", "@pixi/filter-fxaa": "6.5.10", "@pixi/filter-noise": "6.5.10", "@pixi/graphics": "6.5.10", "@pixi/interaction": "6.5.10", "@pixi/loaders": "6.5.10", "@pixi/math": "6.5.10", "@pixi/mesh": "6.5.10", "@pixi/mesh-extras": "6.5.10", "@pixi/mixin-cache-as-bitmap": "6.5.10", "@pixi/mixin-get-child-by-name": "6.5.10", "@pixi/mixin-get-global-position": "6.5.10", "@pixi/particle-container": "6.5.10", "@pixi/polyfill": "6.5.10", "@pixi/prepare": "6.5.10", "@pixi/runner": "6.5.10", "@pixi/settings": "6.5.10", "@pixi/sprite": "6.5.10", "@pixi/sprite-animated": "6.5.10", "@pixi/sprite-tiling": "6.5.10", "@pixi/spritesheet": "6.5.10", "@pixi/text": "6.5.10", "@pixi/text-bitmap": "6.5.10", "@pixi/ticker": "6.5.10", "@pixi/utils": "6.5.10" } }, "sha512-Z2mjeoISml2iuVwT1e/BQwERYM2yKoiR08ZdGrg8y5JjeuVptfTrve4DbPMRN/kEDodesgQZGV/pFv0fE9Q2SA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
"png-chunk-text": ["png-chunk-text@1.0.0", "https://registry.npmmirror.com/png-chunk-text/-/png-chunk-text-1.0.0.tgz", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="],
"png-chunks-encode": ["png-chunks-encode@1.0.0", "https://registry.npmmirror.com/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="],
@@ -2176,6 +2296,8 @@
"process-nextick-args": ["process-nextick-args@2.0.1", "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"promise-polyfill": ["promise-polyfill@8.3.0", "", {}, "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg=="],
"promise-worker-transferable": ["promise-worker-transferable@1.0.4", "https://registry.npmmirror.com/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="],
"prompts": ["prompts@2.4.2", "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -2484,6 +2606,8 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strip-outer": ["strip-outer@1.0.1", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg=="],
"style-mod": ["style-mod@4.1.3", "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"style-to-js": ["style-to-js@1.1.21", "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
@@ -2532,6 +2656,8 @@
"trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trim-repeated": ["trim-repeated@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg=="],
"troika-three-text": ["troika-three-text@0.52.4", "https://registry.npmmirror.com/troika-three-text/-/troika-three-text-0.52.4.tgz", { "dependencies": { "bidi-js": "^1.0.2", "troika-three-utils": "^0.52.4", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg=="],
"troika-three-utils": ["troika-three-utils@0.52.4", "https://registry.npmmirror.com/troika-three-utils/-/troika-three-utils-0.52.4.tgz", { "peerDependencies": { "three": ">=0.125.0" } }, "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A=="],
@@ -2594,6 +2720,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="],
"url-join": ["url-join@5.0.0", "https://registry.npmmirror.com/url-join/-/url-join-5.0.0.tgz", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@@ -2772,6 +2900,8 @@
"@pierre/diffs/shiki": ["shiki@4.4.3", "https://registry.npmmirror.com/shiki/-/shiki-4.4.3.tgz", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
"@pixi/utils/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
"@radix-ui/react-accordion/@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.7.tgz", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
"@radix-ui/react-accordion/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.4", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA=="],
@@ -3372,6 +3502,10 @@
"get-intrinsic/hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"gh-pages/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"gh-pages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"hast-util-from-parse5/@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"hast-util-raw/@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@@ -3430,6 +3564,8 @@
"parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"points-on-path/points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"postcss/nanoid": ["nanoid@3.3.16", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
@@ -3500,12 +3636,16 @@
"stats-gl/three": ["three@0.170.0", "https://registry.npmmirror.com/three/-/three-0.170.0.tgz", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
"strip-outer/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"three-stdlib/fflate": ["fflate@0.6.10", "https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="],
"tinyglobby/picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"tldraw/idb": ["idb@7.1.1", "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
"trim-repeated/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"tunnel-rat/zustand": ["zustand@4.5.7", "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"type-is/content-type": ["content-type@2.0.0", "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
@@ -3530,6 +3670,8 @@
"unist-util-visit-parents/@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"url/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="],
"vfile/@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"vfile-location/@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@@ -3816,12 +3958,18 @@
"express/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"gh-pages/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"gh-pages/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"leva/@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.9", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.9.tgz", { "dependencies": { "@radix-ui/react-slot": "1.3.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aX5c84AYUD0EIodbQfpujB5XI0CbEaQ5U9du/wntNQK451i5iCJlE2JN6IN1LgQ3SnoBbnW/Jv1Z4rUowZV+hg=="],
"mermaid/roughjs/points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"radix-ui/@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.3.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-f/Wxm0ctyMymUJK0fqTSQlm85rbzdAkoNbPXJQ5+6caowVO8Yx+NWGjGz/oGhs/D+WIbbQpOrU0hU2Li2/42xQ=="],
"radix-ui/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.3.tgz", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-W0GSYZFKEfi6raMiMEfJSngvVbFDIyxtW5JuVg5NoQBY59l0dVQVGLbhog/tOdwq0qtZ+TXuX1ikSNan4/IZXA=="],
@@ -3910,6 +4058,8 @@
"leva/@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.2", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.3.2.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-nLBh0hpzgQA16ioKAWTSQvGjRj7QcI3pTpngtYZ12b4RP9N8eGSj7ziceHyHe04zrd0bAcLyMijO0ws0DLmDWw=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
@@ -3917,5 +4067,7 @@
"leva/@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.7.tgz", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
"leva/@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.4", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
}
}

View File

@@ -62,6 +62,8 @@
"mammoth": "^1.12.0",
"motion": "^12.42.2",
"nanoid": "^6.0.0",
"pixi-live2d-display": "0.4.0",
"pixi.js": "6.5.10",
"react": "19.2.7",
"react-aria-components": "^1.21.1",
"react-dom": "19.2.7",

View File

@@ -1,19 +1,20 @@
import { ArrowUpRight, Clapperboard, Layers3, Sparkles } from "lucide-react";
import { agentCopy, type CanvasAppearance } from "@/lib/canvas/agent-appearance";
type AgentWelcomeProps = {
brandName: string;
appearance: CanvasAppearance;
nodeCount: number;
onChooseSkill: () => void;
onDraftPrompt: (prompt: string) => void;
};
export function AgentWelcome({ brandName, nodeCount, onChooseSkill, onDraftPrompt }: AgentWelcomeProps) {
export function AgentWelcome({ appearance, nodeCount, onChooseSkill, onDraftPrompt }: AgentWelcomeProps) {
return (
<section className="agent-welcome" aria-label="开始 Agent 创作">
<div className="agent-welcome-intro">
<span className="agent-welcome-orb" aria-hidden="true" />
<h2>在这里,和{brandName}让灵感,慢慢成形</h2>
<p>从一个想法开始,和 Agent 一起创作。</p>
<h2>{agentCopy(appearance.welcomeTitle, appearance.agentName)}</h2>
<p>{agentCopy(appearance.welcomeDescription, appearance.agentName)}</p>
</div>
<div className="agent-welcome-actions">
<button type="button" onClick={onChooseSkill}>

View File

@@ -35,6 +35,9 @@ import { CanvasCloudAgentSettings, agentPermissionLabel, agentPermissionMenuItem
import { useAgentPanelLayout } from "./use-agent-panel-layout";
import { useAgentLauncherPosition } from "./use-agent-launcher-position";
import { AgentWelcome } from "./canvas-agent-welcome";
import { DEFAULT_CANVAS_APPEARANCE, agentCopy } from "@/lib/canvas/agent-appearance";
import { live2DModelURL } from "@/services/api/appearance";
import { Live2DAvatar } from "./live2d-avatar";
import "./canvas-cloud-agent.css";
type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
@@ -42,6 +45,7 @@ type ApprovalState = { approvalId: string; detail: Record<string, unknown>; reas
type AgentPanelView = "chat" | "history" | "settings";
export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const theme = canvasThemes[useActiveTheme()];
const config = useEffectiveConfig();
const updateConfig = useConfigStore((state) => state.updateConfig);
@@ -741,7 +745,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
disabled={Boolean(run && connectionStatus !== "connected") || !historyHydrated || !pendingHydrated}
sending={busy}
running={running}
placeholder={running ? "运行中可直接插话,会在它下一步生效" : "输入操作指导;用 @ 引用画布节点,用 / 或 、 引用 Skills"}
placeholder={running ? "运行中可直接插话,会在它下一步生效" : agentCopy(appearance.inputPlaceholder, appearance.agentName)}
theme={theme}
onPromptChange={setPrompt}
onSubmit={() => void submit()}
@@ -799,22 +803,32 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
}
function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, onOpen }: { theme: CanvasTheme; statusColor: string; approvalPending: boolean; reducedMotion: boolean; onOpen: () => void }) {
const { position, dragging, handlers } = useAgentLauncherPosition(onOpen);
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const live = appearance.avatarType === "live2d" && Boolean(appearance.live2dResourceId && appearance.live2dEntry);
const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight }));
useEffect(() => {
const resize = () => setViewport({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize);
}, []);
const height = live ? Math.max(40, Math.min(appearance.avatarHeight, viewport.height - 60, (viewport.width - 40) / 0.75)) : 76;
const width = live ? Math.round(height * 0.75) : 76;
const { position, dragging, handlers } = useAgentLauncherPosition(onOpen, width, height);
return (
<motion.button
type="button"
aria-label="打开云端 Agent"
aria-label={`打开${appearance.agentName}`}
title={`${approvalPending ? "Agent 等待你的审批" : "打开 Agent 助手"} · 拖动可调整位置,聚焦后可用方向键移动`}
className={cn("canvas-agent-launcher fixed z-[var(--z-modal-overlay)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/35", dragging && "is-dragging")}
style={{ ...position, color: theme.node.text, "--canvas-agent-launcher-shadow": theme.spatial.shadow } as CSSProperties}
className={cn("canvas-agent-launcher fixed z-[var(--z-modal-overlay)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/35", dragging && "is-dragging", live && "canvas-agent-launcher-live2d")}
style={{ ...position, width, height, color: theme.node.text, "--canvas-agent-launcher-shadow": theme.spatial.shadow } as CSSProperties}
data-canvas-no-zoom
{...handlers}
whileHover={reducedMotion || dragging ? undefined : { scale: 1.035 }}
whileTap={reducedMotion || dragging ? undefined : { scale: 0.96 }}
transition={{ duration: reducedMotion ? 0 : 0.18 }}
>
<FluidOrb size={62} color="#7164f6" />
<span className="canvas-agent-launcher-label">Agent</span>
{live ? <Live2DAvatar url={live2DModelURL(appearance.live2dResourceId, appearance.live2dEntry)} width={width} height={height} reducedMotion={reducedMotion} fallback={<FluidOrb size={62} color="#7164f6" />} /> : <FluidOrb size={62} color="#7164f6" />}
{appearance.launcherLabel ? <span className="canvas-agent-launcher-label">{appearance.launcherLabel}</span> : null}
<span className={cn("canvas-agent-launcher-status", approvalPending && "is-pending")} style={{ "--canvas-agent-status-color": statusColor } as CSSProperties} />
{approvalPending ? <span className="canvas-agent-launcher-badge">待审批</span> : null}
</motion.button>
@@ -822,17 +836,18 @@ function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, onO
}
function AgentHeader({ theme, hasMessages, statusLabel, statusColor, nodeCount, onNew, onHistory, onSettings, onCollapse, onExport, exporting }: { theme: CanvasTheme; hasMessages: boolean; statusLabel: string; statusColor: string; nodeCount: number; onNew: () => void; onHistory: () => void; onSettings: () => void; onCollapse: () => void; onExport: () => void; exporting: boolean }) {
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
return (
<header data-agent-drag-handle className="agent-panel-header flex shrink-0 items-center gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="agent-panel-title">{hasMessages ? "画布助手" : "新对话"}</span>
<span className="agent-panel-title">{agentCopy(appearance.panelTitle, appearance.agentName)}{hasMessages ? "" : " · 新对话"}</span>
<span className="agent-panel-status flex items-center gap-1" style={{ color: statusColor }}>
<CircleDot className="size-3" />
{statusLabel}
</span>
</div>
<div className="agent-panel-context">Agent · 当前画布 {nodeCount} 个节点</div>
<div className="agent-panel-context">{appearance.agentName} · 当前画布 {nodeCount} 个节点</div>
</div>
<div className="agent-header-actions flex items-center gap-0.5" style={{ color: theme.node.muted }}>
<Button type="text" shape="circle" icon={<Download className="size-4" />} loading={exporting} onClick={onExport} aria-label="导出 Agent 调试记录" title="导出对话、工具参数、审批和错误(分享前请检查隐私)" />
@@ -918,7 +933,7 @@ function AgentConversation({
onApprove: (settings?: AgentMediaSettings) => void;
onReject: () => void;
}) {
const brandName = useAppearanceStore((state) => state.appearance.brandName);
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const scrollRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const followRef = useRef(true);
@@ -949,7 +964,7 @@ function AgentConversation({
const element = event.currentTarget;
followRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 48;
}}>
{!messages.length ? <AgentWelcome brandName={brandName} nodeCount={nodeCount} onChooseSkill={onChooseSkill} onDraftPrompt={onDraftPrompt} /> : null}
{!messages.length ? <AgentWelcome appearance={appearance} nodeCount={nodeCount} onChooseSkill={onChooseSkill} onDraftPrompt={onDraftPrompt} /> : null}
<div ref={contentRef} className="agent-conversation-messages">
{messages.map((item) => (
<AgentChatMessage key={item.id} item={item} theme={theme} references={references} onFocusNode={onFocusNode} isStreaming={busy && !approval && item.streaming === true && item === messages.at(-1)} />

View File

@@ -561,6 +561,16 @@
backdrop-filter: blur(10px);
}
.canvas-agent-launcher.canvas-agent-launcher-live2d {
padding: 0;
border-radius: var(--radius-lg);
box-shadow: none;
}
.canvas-agent-launcher-live2d::before {
display: none;
}
.canvas-agent-launcher-status {
position: absolute;
top: 6px;

View File

@@ -0,0 +1,196 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { installLive2DLoader } from "@/services/live2d-loader";
import { adaptLive2DRenderOrders } from "@/lib/canvas/live2d-core-adapter";
let corePromise: Promise<void> | undefined;
function loadCore() {
if ("Live2DCubismCore" in window) return Promise.resolve();
if (corePromise) return corePromise;
corePromise = new Promise<void>((resolve, reject) => {
const script = document.createElement("script");
// Deployment-owned runtime only. Uploaded models may never provide executable code.
script.src = `${import.meta.env.BASE_URL}live2d/live2dcubismcore.min.js`;
const timer = window.setTimeout(() => fail(), 15000);
const fail = () => {
clearTimeout(timer);
script.remove();
reject(new Error("Live2D 运行库未安装或加载失败,请部署 Cubism Core 后重试"));
};
script.onload = () => {
if (!("Live2DCubismCore" in window)) {
fail();
return;
}
clearTimeout(timer);
resolve();
};
script.onerror = fail;
document.head.append(script);
}).catch((error) => {
corePromise = undefined;
throw error;
});
return corePromise;
}
export function Live2DAvatar({
url,
width,
height,
reducedMotion = false,
fallback,
onReady,
onError,
}: {
url: string;
width: number;
height: number;
reducedMotion?: boolean;
fallback?: ReactNode;
onReady?: () => void;
onError?: (message: string) => void;
}) {
const host = useRef<HTMLSpanElement>(null);
const callbacks = useRef({ onReady, onError });
callbacks.current = { onReady, onError };
const [failed, setFailed] = useState(false);
const [ready, setReady] = useState(false);
useEffect(() => {
let disposed = false;
let cleanup: (() => void) | undefined;
let timer: ReturnType<typeof setTimeout> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
let cancelLoading: (() => void) | undefined;
setFailed(false);
setReady(false);
const init = async () => {
await loadCore();
const [PIXI, { Live2DModel, Live2DFactory, Live2DLoader, MotionPreloadStrategy, Cubism4InternalModel }] = await Promise.all([import("pixi.js"), import("pixi-live2d-display/cubism4")]);
if (disposed) return;
installLive2DLoader(Live2DLoader);
const app = new PIXI.Application({ width, height, backgroundAlpha: 0, resolution: Math.min(window.devicePixelRatio || 1, 1.5), autoDensity: true, autoStart: false, antialias: true });
const canvas = app.view as HTMLCanvasElement;
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.setAttribute("aria-hidden", "true");
host.current?.append(canvas);
let model: InstanceType<typeof Live2DModel> | undefined;
let visible = true;
let lost = false;
let destroyed = false;
let elapsed = performance.now();
const failRendering = (message: string) => {
lost = true;
clearTimeout(deadline);
cancelLoading?.();
cleanup?.();
setFailed(true);
callbacks.current.onError?.(message);
};
const draw = () => {
if (document.hidden || disposed || lost || !visible || !model) return;
try {
const now = performance.now();
model.update(Math.min(now - elapsed, 100));
elapsed = now;
app.render();
} catch {
failRendering("Live2D 渲染失败,已回退默认形象");
}
};
const sync = () => {
clearInterval(timer);
if (!document.hidden && visible && !lost && !reducedMotion && model) {
elapsed = performance.now();
timer = setInterval(draw, 1000 / 30);
}
};
const observer = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
sync();
});
observer.observe(canvas);
const contextLost = (event: Event) => {
event.preventDefault();
failRendering("Live2D 渲染上下文丢失,已回退默认形象");
};
canvas.addEventListener("webglcontextlost", contextLost);
document.addEventListener("visibilitychange", sync);
cleanup = () => {
if (destroyed) return;
destroyed = true;
clearInterval(timer);
observer.disconnect();
document.removeEventListener("visibilitychange", sync);
canvas.removeEventListener("webglcontextlost", contextLost);
app.destroy(true, { children: true, texture: true, baseTexture: true });
};
const loaded = new Live2DModel({ autoUpdate: false, autoInteract: false });
const releaseLoaded = () => {
// Cubism's destroy() assumes internalModel exists, even after failed setup.
if (loaded.internalModel) loaded.destroy({ children: true, texture: true, baseTexture: true });
else {
loaded.emit("destroy");
loaded.textures.forEach((texture) => texture.destroy(true));
}
};
cancelLoading = () => loaded.emit("destroy");
deadline = setTimeout(() => {
disposed = true;
cancelLoading?.();
cleanup?.();
setFailed(true);
callbacks.current.onError?.("Live2D 加载超时,请检查模型和网络后重试");
}, 30000);
try {
await Live2DFactory.setupLive2DModel(loaded, url, { autoUpdate: false, autoInteract: false, crossOrigin: "use-credentials", motionPreload: MotionPreloadStrategy.IDLE });
} catch (error) {
releaseLoaded();
throw error;
} finally {
clearTimeout(deadline);
cancelLoading = undefined;
}
if (disposed || lost) {
releaseLoaded();
cleanup?.();
return;
}
model = loaded;
app.stage.addChild(loaded);
if (!(loaded.internalModel instanceof Cubism4InternalModel)) throw new Error("不支持的 Live2D 模型运行时");
adaptLive2DRenderOrders(loaded.internalModel.coreModel);
if (!(loaded.width > 0 && loaded.height > 0)) throw new Error("Live2D 模型尺寸无效");
const scale = Math.min(width / loaded.width, height / loaded.height);
loaded.scale.set(scale);
loaded.anchor.set(0.5, 1);
loaded.position.set(width / 2, height);
loaded.update(0);
app.render();
sync();
setReady(true);
callbacks.current.onReady?.();
};
void init().catch((error) => {
cleanup?.();
cleanup = undefined;
if (!disposed) {
setFailed(true);
callbacks.current.onError?.(error instanceof Error ? error.message : "Live2D 加载失败");
}
});
return () => {
disposed = true;
clearTimeout(deadline);
cancelLoading?.();
cleanup?.();
};
}, [url, width, height, reducedMotion]);
return (
<span style={{ display: "grid", placeItems: "center", width, height, pointerEvents: "none" }}>
<span ref={host} style={{ gridArea: "1 / 1", visibility: failed || !ready ? "hidden" : "visible", width, height }} />
{failed || !ready ? <span style={{ gridArea: "1 / 1" }}>{fallback}</span> : null}
</span>
);
}

View File

@@ -3,13 +3,14 @@ import { AGENT_LAUNCHER_POSITION_KEY, clampAgentLauncherPosition, moveAgentLaunc
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
export function useAgentLauncherPosition(onOpen: () => void) {
export function useAgentLauncherPosition(onOpen: () => void, width = 76, height = 76) {
const size = { width, height };
const [position, setPosition] = useState(() => {
try {
return restoreAgentLauncherPosition(localStorage.getItem(AGENT_LAUNCHER_POSITION_KEY), viewport());
return restoreAgentLauncherPosition(localStorage.getItem(AGENT_LAUNCHER_POSITION_KEY), viewport(), size);
} catch (error) {
console.warn("Agent 入口位置无法读取", error);
return restoreAgentLauncherPosition(null, viewport());
return restoreAgentLauncherPosition(null, viewport(), size);
}
});
const [dragging, setDragging] = useState(false);
@@ -25,10 +26,11 @@ export function useAgentLauncherPosition(onOpen: () => void) {
};
useEffect(() => {
const resize = () => setPosition((current) => clampAgentLauncherPosition(current, viewport()));
const resize = () => setPosition((current) => clampAgentLauncherPosition(current, viewport(), size));
resize();
window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize);
}, []);
}, [width, height]);
const onPointerDown = (event: PointerEvent<HTMLButtonElement>) => {
event.stopPropagation();
@@ -41,7 +43,7 @@ export function useAgentLauncherPosition(onOpen: () => void) {
event.stopPropagation();
const gesture = gestureRef.current;
if (!gesture || gesture.pointerId !== event.pointerId) return;
const next = moveAgentLauncher(gesture, event.clientX, event.clientY, viewport());
const next = moveAgentLauncher(gesture, event.clientX, event.clientY, viewport(), size);
gesture.dragged = next.dragged;
if (!next.dragged) return;
event.preventDefault();
@@ -54,7 +56,7 @@ export function useAgentLauncherPosition(onOpen: () => void) {
const gesture = gestureRef.current;
if (!gesture || gesture.pointerId !== event.pointerId) return;
gestureRef.current = null;
const next = cancelled ? { dragged: gesture.dragged, position: clampAgentLauncherPosition(position, viewport()) } : moveAgentLauncher(gesture, event.clientX, event.clientY, viewport());
const next = cancelled ? { dragged: gesture.dragged, position: clampAgentLauncherPosition(position, viewport(), size) } : moveAgentLauncher(gesture, event.clientX, event.clientY, viewport(), size);
suppressClick.current = cancelled || next.dragged;
setDragging(false);
setPosition(next.position);
@@ -77,7 +79,7 @@ export function useAgentLauncherPosition(onOpen: () => void) {
const offset = offsets[event.key];
if (!offset) return;
event.preventDefault();
const next = clampAgentLauncherPosition({ right: position.right + offset[0], bottom: position.bottom + offset[1] }, viewport());
const next = clampAgentLauncherPosition({ right: position.right + offset[0], bottom: position.bottom + offset[1] }, viewport(), size);
setPosition(next);
save(next);
};

View File

@@ -0,0 +1,29 @@
export type CanvasAppearance = {
agentName: string;
launcherLabel: string;
panelTitle: string;
welcomeTitle: string;
welcomeDescription: string;
inputPlaceholder: string;
avatarType: "orb" | "live2d";
live2dResourceId: string;
live2dEntry: string;
avatarHeight: number;
};
export const DEFAULT_CANVAS_APPEARANCE: CanvasAppearance = {
agentName: "影策",
launcherLabel: "Agent",
panelTitle: "画布助手",
welcomeTitle: "在这里,和{agentName}让灵感,慢慢成形",
welcomeDescription: "从一个想法开始,和{agentName}一起创作。",
inputPlaceholder: "输入操作指导;用 @ 引用画布节点,用 / 或 、 引用 Skills",
avatarType: "orb",
live2dResourceId: "",
live2dEntry: "",
avatarHeight: 220,
};
export function agentCopy(template: string, name: string) {
return template.replaceAll("{agentName}", () => name);
}

View File

@@ -6,33 +6,33 @@ export const AGENT_LAUNCHER_POSITION_KEY = "canvas:agent-launcher-position:v1";
export const AGENT_LAUNCHER_SIZE = 76;
const MARGIN = 20; // Includes the label below the orb.
export function clampAgentLauncherPosition(position: AgentLauncherPosition, viewport: AgentLauncherViewport): AgentLauncherPosition {
const clamp = (value: number, extent: number) => {
const available = Math.max(0, extent - AGENT_LAUNCHER_SIZE);
export function clampAgentLauncherPosition(position: AgentLauncherPosition, viewport: AgentLauncherViewport, size = { width: AGENT_LAUNCHER_SIZE, height: AGENT_LAUNCHER_SIZE }): AgentLauncherPosition {
const clamp = (value: number, extent: number, dimension: number) => {
const available = Math.max(0, extent - dimension);
const margin = Math.min(MARGIN, available / 2);
return Math.min(Math.max(value, margin), available - margin);
};
return { right: clamp(position.right, viewport.width), bottom: clamp(position.bottom, viewport.height) };
return { right: clamp(position.right, viewport.width, size.width), bottom: clamp(position.bottom, viewport.height, size.height) };
}
export function restoreAgentLauncherPosition(raw: string | null, viewport: AgentLauncherViewport): AgentLauncherPosition {
export function restoreAgentLauncherPosition(raw: string | null, viewport: AgentLauncherViewport, size?: AgentLauncherViewport): AgentLauncherPosition {
if (raw) {
try {
const value = JSON.parse(raw);
if (value && Number.isFinite(value.right) && Number.isFinite(value.bottom)) return clampAgentLauncherPosition(value, viewport);
if (value && Number.isFinite(value.right) && Number.isFinite(value.bottom)) return clampAgentLauncherPosition(value, viewport, size);
} catch {
// Damaged UI preferences must not hide the launcher.
}
}
return clampAgentLauncherPosition({ right: MARGIN, bottom: MARGIN }, viewport);
return clampAgentLauncherPosition({ right: MARGIN, bottom: MARGIN }, viewport, size);
}
export function moveAgentLauncher(gesture: AgentLauncherGesture, x: number, y: number, viewport: AgentLauncherViewport) {
export function moveAgentLauncher(gesture: AgentLauncherGesture, x: number, y: number, viewport: AgentLauncherViewport, size?: AgentLauncherViewport) {
const dx = x - gesture.x;
const dy = y - gesture.y;
const dragged = gesture.dragged || Math.hypot(dx, dy) >= 6;
return {
dragged,
position: clampAgentLauncherPosition(dragged ? { right: gesture.start.right - dx, bottom: gesture.start.bottom - dy } : gesture.start, viewport),
position: clampAgentLauncherPosition(dragged ? { right: gesture.start.right - dx, bottom: gesture.start.bottom - dy } : gesture.start, viewport, size),
};
}

View File

@@ -0,0 +1,21 @@
type CoreModel = {
drawables: { count: number; renderOrders?: Int32Array };
offscreens?: { count: number };
getRenderOrders?: () => Int32Array;
};
// Cubism 5.3 moved render orders to Model and added offscreen render objects.
// Adapt only drawable-only models: the Cubism 4 renderer cannot draw offscreens.
export function adaptLive2DRenderOrders(model: { getModel: () => CoreModel; getDrawableRenderOrders: () => Int32Array }) {
const core = model.getModel();
if (core.offscreens?.count) throw new Error("此模型使用新版离屏渲染,当前 Live2D 渲染器暂不支持");
if (core.drawables.renderOrders) return;
if (!core.getRenderOrders) throw new Error("Live2D Core 缺少绘制顺序接口,请检查运行库版本");
const getOrders = () => {
const orders = core.getRenderOrders!();
if (orders.length !== core.drawables.count) throw new Error("Live2D 模型绘制顺序数量不匹配");
return orders;
};
getOrders();
model.getDrawableRenderOrders = getOrders;
}

View File

@@ -16,7 +16,7 @@ export default function StorageResourcesPanel() {
const { message, modal } = App.useApp();
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("filter") || "";
const kind = normalizeOption(searchParams.get("kind"), ["image", "video", "audio", "file"]);
const kind = normalizeOption(searchParams.get("kind"), ["image", "video", "audio", "file", "live2d"]);
const status = normalizeOption(searchParams.get("status"), ["pending", "ready", "failed", "deleted"]);
const provider = normalizeOption(searchParams.get("provider"), ["local", "aliyun", "tencent", "qiniu", "s3"]);
const userId = searchParams.get("userId") || "";
@@ -317,6 +317,7 @@ const kindOptions = [
{ label: "视频", value: "video" },
{ label: "音频", value: "audio" },
{ label: "文件", value: "file" },
{ label: "Live2D 模型", value: "live2d" },
];
const statusOptions = [
{ label: "全部状态", value: "all" },
@@ -349,7 +350,7 @@ function fileName(objectKey: string) {
return objectKey.split("/").filter(Boolean).at(-1) || "";
}
function kindLabel(kind: string) {
return ({ image: "图片", video: "视频", audio: "音频", file: "文件" } as Record<string, string>)[kind] || kind || "未知";
return ({ image: "图片", video: "视频", audio: "音频", file: "文件", live2d: "Live2D" } as Record<string, string>)[kind] || kind || "未知";
}
function statusLabel(status: string) {
return ({ pending: "待处理", ready: "已就绪", failed: "失败", deleted: "已删除" } as Record<string, string>)[status] || status || "未知";

View File

@@ -10,6 +10,8 @@ import { cn } from "@/lib/utils";
import { cloneSkinDefinition, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, isSkinButtonFill, normalizeSkinDefinition, type SkinDefinition } from "@/lib/skin-themes";
import { SkinThemeEditor } from "@/pages/admin/settings/components/skin-theme-editor";
import { WelcomeSetting } from "@/pages/admin/settings/components/welcome-setting";
import { CanvasAppearanceEditor } from "./components/canvas-appearance-editor";
import { DEFAULT_CANVAS_APPEARANCE, type CanvasAppearance } from "@/lib/canvas/agent-appearance";
import { deleteAdminResources } from "@/services/api/admin-storage";
import { getAdminAppearance, resetAdminAppearance, updateAdminAppearance, uploadAppearanceAsset, type AdminAppearance, type AppearanceAssetSlot } from "@/services/api/appearance";
import { commitPublicAppearance, DEFAULT_PUBLIC_APPEARANCE } from "@/stores/use-appearance-store";
@@ -31,6 +33,8 @@ export default function AppearanceSettingsPage() {
const [activeTab, setActiveTab] = useState("brand");
const [setting, setSetting] = useState<AdminAppearance | null>(null);
const [brandName, setBrandName] = useState("");
const [canvas, setCanvas] = useState<CanvasAppearance>(DEFAULT_CANVAS_APPEARANCE);
const [canvasUploading, setCanvasUploading] = useState(false);
const [brandSlug, setBrandSlug] = useState("");
const [authHeroTitle, setAuthHeroTitle] = useState("");
const [authHeroDescription, setAuthHeroDescription] = useState("");
@@ -61,7 +65,7 @@ export default function AppearanceSettingsPage() {
const dirty =
Boolean(setting) &&
(brandName.trim() !== setting?.brandName ||
(JSON.stringify(canvas) !== JSON.stringify(setting?.canvas || DEFAULT_CANVAS_APPEARANCE) || canvasUploading || brandName.trim() !== setting?.brandName ||
brandSlug.trim().toLocaleLowerCase() !== setting?.brandSlug ||
normalizeDraftCopy(authHeroTitle) !== setting?.authHeroTitle ||
normalizeDraftCopy(authHeroDescription) !== setting?.authHeroDescription ||
@@ -84,6 +88,7 @@ export default function AppearanceSettingsPage() {
const selectedID = themes.some((theme) => theme.id === value.skinId) ? value.skinId : "classic";
setSetting({ ...value, skinThemes: themes, skinId: selectedID });
setBrandName(value.brandName);
setCanvas(value.canvas || DEFAULT_CANVAS_APPEARANCE);
setBrandSlug(value.brandSlug);
setAuthHeroTitle(value.authHeroTitle);
setAuthHeroDescription(value.authHeroDescription);
@@ -136,7 +141,7 @@ export default function AppearanceSettingsPage() {
if (blocker.state !== "blocked") return;
modal.confirm({
title: "放弃站点及外观调整?",
content: "当前品牌、SEO、备案、皮肤或媒体配置尚未保存,离开后草稿会丢失。线上站点不会改变。",
content: "当前品牌、画布 Agent、SEO、备案、皮肤或媒体配置尚未保存,离开后草稿会丢失。线上站点不会改变。",
okText: "放弃并离开",
cancelText: "继续编辑",
okButtonProps: { danger: true },
@@ -191,7 +196,8 @@ export default function AppearanceSettingsPage() {
};
const discardDraft = () => {
if (!setting || saving || restoring) return;
if (!setting || saving || restoring || canvasUploading) return;
setCanvas(setting.canvas || DEFAULT_CANVAS_APPEARANCE);
setBrandName(setting.brandName);
setBrandSlug(setting.brandSlug);
setAuthHeroTitle(setting.authHeroTitle);
@@ -215,13 +221,14 @@ export default function AppearanceSettingsPage() {
};
const requestRefresh = () => {
if (canvasUploading) return;
if (!dirty) {
void load(false);
return;
}
modal.confirm({
title: "放弃调整并重新读取?",
content: "重新读取会丢弃当前品牌、SEO、备案、皮肤和待上传文件。",
content: "重新读取会丢弃当前品牌、画布 Agent、SEO、备案、皮肤和待上传文件。",
okText: "放弃并刷新",
cancelText: "继续编辑",
okButtonProps: { danger: true },
@@ -230,10 +237,10 @@ export default function AppearanceSettingsPage() {
};
const restoreBuiltInAppearance = () => {
if (!setting?.configured || saving || refreshing || restoring) return;
if (!setting?.configured || saving || refreshing || restoring || canvasUploading) return;
modal.confirm({
title: "恢复影策默认品牌标识?",
content: "品牌名称、英文标识、Logo、登录页文案、视频、封面、SEO、备案和皮肤主题会立即恢复为项目内置值。已上传文件仍保留在存储资源中,不会被删除。",
content: "品牌名称、英文标识、Logo、画布 Agent 名称/文案/形象、登录页文案、视频、封面、SEO、备案和皮肤主题会立即恢复为项目内置值。已上传文件仍保留在存储资源中,不会被删除。",
okText: "恢复默认",
cancelText: "取消",
okButtonProps: { danger: true },
@@ -259,7 +266,12 @@ export default function AppearanceSettingsPage() {
};
const save = async () => {
if (!setting || saving || restoring) return;
if (!setting || saving || restoring || canvasUploading) return;
if (![canvas.agentName, canvas.panelTitle, canvas.welcomeTitle, canvas.inputPlaceholder].every((text) => text.trim())) {
setActiveTab("canvas");
message.error("请填写助手名称、面板标题、欢迎标题和输入框提示");
return;
}
const nextBrandName = brandName.trim();
const nextBrandSlug = brandSlug.trim().toLocaleLowerCase();
const nextAuthHeroTitle = normalizeDraftCopy(authHeroTitle);
@@ -330,6 +342,7 @@ export default function AppearanceSettingsPage() {
ids[slot] = resource.id;
}
const updated = await updateAdminAppearance({
canvas,
brandName: nextBrandName,
brandSlug: nextBrandSlug,
authHeroTitle: nextAuthHeroTitle,
@@ -443,17 +456,17 @@ export default function AppearanceSettingsPage() {
</div>
<div className="admin-appearance-command-actions">
{dirty ? (
<Button icon={<Undo2 className="size-4" />} disabled={saving || restoring} onClick={discardDraft}>
<Button icon={<Undo2 className="size-4" />} disabled={saving || restoring || canvasUploading} onClick={discardDraft}>
撤销调整
</Button>
) : null}
<Button icon={<RotateCcw className="size-4" />} loading={restoring} disabled={!setting.configured || saving || refreshing} onClick={restoreBuiltInAppearance}>
<Button icon={<RotateCcw className="size-4" />} loading={restoring} disabled={!setting.configured || saving || refreshing || canvasUploading} onClick={restoreBuiltInAppearance}>
恢复影策默认
</Button>
<Button icon={<RefreshCw className="size-4" />} loading={refreshing} disabled={saving || restoring} onClick={requestRefresh}>
<Button icon={<RefreshCw className="size-4" />} loading={refreshing} disabled={saving || restoring || canvasUploading} onClick={requestRefresh}>
刷新状态
</Button>
<Button type="primary" icon={<Save className="size-4" />} loading={saving} disabled={!dirty || refreshing || restoring} onClick={() => void save()}>
<Button type="primary" icon={<Save className="size-4" />} loading={saving} disabled={!dirty || refreshing || restoring || canvasUploading} onClick={() => void save()}>
保存修改
</Button>
</div>
@@ -754,6 +767,7 @@ export default function AppearanceSettingsPage() {
</SettingsSectionCard>
),
},
{ key: "canvas", label: "画布配置", children: <CanvasAppearanceEditor value={canvas} onChange={setCanvas} disabled={saving || refreshing || restoring} onUploading={setCanvasUploading} /> },
]}
/>
</div>

View File

@@ -0,0 +1,170 @@
import { useRef, useState } from "react";
import { Alert, App, Button, Form, Input, InputNumber, Radio } from "antd";
import { ExternalLink, Upload } from "lucide-react";
import { useReducedMotion } from "motion/react";
import { Live2DAvatar } from "@/components/canvas/live2d-avatar";
import { FluidOrb } from "@/components/ui/fluid-orb";
import { agentCopy, type CanvasAppearance } from "@/lib/canvas/agent-appearance";
import { live2DModelURL, uploadLive2D } from "@/services/api/appearance";
export function CanvasAppearanceEditor({ value, onChange, disabled, onUploading }: { value: CanvasAppearance; onChange: (value: CanvasAppearance) => void; disabled: boolean; onUploading: (value: boolean) => void }) {
const { message } = App.useApp();
const reducedMotion = useReducedMotion() ?? false;
const input = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [readyURL, setReadyURL] = useState("");
const [error, setError] = useState("");
const [attempt, setAttempt] = useState(0);
const url = value.live2dResourceId ? live2DModelURL(value.live2dResourceId, value.live2dEntry, true) : "";
const change = (patch: Partial<CanvasAppearance>) => onChange({ ...value, ...patch });
async function upload(file?: File) {
if (!file) return;
if (!file.name.toLowerCase().endsWith(".zip") || file.size > 128 * 1024 * 1024) {
message.error("请选择不超过 128 MiB 的 Live2D ZIP");
return;
}
setUploading(true);
onUploading(true);
setError("");
try {
const model = await uploadLive2D(file);
setReadyURL("");
change({ live2dResourceId: model.resourceId, live2dEntry: model.entry, avatarType: "orb" });
message.success("导入成功,请预览后选择 Live2D,并点击页面顶部保存");
} catch (cause) {
message.error(cause instanceof Error ? cause.message : "模型导入失败");
} finally {
setUploading(false);
onUploading(false);
}
}
return (
<div className="grid gap-6 lg:grid-cols-2">
<div>
<h3 className="mb-3 font-semibold">Agent 身份与文案</h3>
<p className="mb-4 text-sm text-foreground/60">独立于站点品牌。文案可使用 {"{agentName}"} 引用助手名称;这里只配置展示文字,不修改工具权限。</p>
<Form layout="vertical" disabled={disabled || uploading}>
{(
[
["agentName", "助手名称", 24],
["launcherLabel", "悬浮入口文字(可留空)", 24],
["panelTitle", "对话面板标题", 40],
["welcomeTitle", "欢迎标题", 100],
["welcomeDescription", "欢迎说明", 200],
["inputPlaceholder", "输入框提示", 160],
] as const
).map(([key, label, max]) => (
<Form.Item key={key} label={label}>
<Input value={value[key]} maxLength={max} showCount onChange={(event) => change({ [key]: event.target.value })} />
</Form.Item>
))}
</Form>
</div>
<div className="grid content-start gap-4">
<h3 className="font-semibold">Agent 形象</h3>
<Radio.Group
value={value.avatarType}
disabled={disabled || uploading}
onChange={(event) => change({ avatarType: event.target.value })}
options={[
{ label: "默认动态球", value: "orb" },
{ label: "Live2D", value: "live2d", disabled: !url || readyURL !== url },
]}
/>
<p className="text-sm text-foreground/60">上传有授权的 Cubism 3/4 运行时 ZIP:一个 model3.json、moc3、PNG 纹理及可选动作/表情 JSON。首期不支持音频、旧版模型和编辑工程。模型会发送到浏览器,无法保证防下载。</p>
<div className="grid gap-2 text-sm">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<span className="text-foreground/60">参考资料</span>
<a href="https://www.live2d.com/en/learn/sample/" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 underline underline-offset-4">
官方示例模型库
<ExternalLink className="size-3.5" aria-hidden="true" />
<span className="sr-only">(在新标签页打开)</span>
</a>
<a href="https://docs.live2d.com/en/cubism-sdk-manual/cubism-sdk-for-web/" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 underline underline-offset-4">
Cubism Web SDK 文档
<ExternalLink className="size-3.5" aria-hidden="true" />
<span className="sr-only">(在新标签页打开)</span>
</a>
</div>
<p className="text-xs text-foreground/60">下载前请确认素材授权;官方原始包需按上述要求整理为运行时 ZIP,不能直接整包导入。</p>
</div>
<input
ref={input}
type="file"
accept=".zip,application/zip"
className="hidden"
onChange={(event) => {
void upload(event.target.files?.[0]);
event.currentTarget.value = "";
}}
/>
<div className="flex flex-wrap gap-2">
<Button icon={<Upload className="size-4" />} loading={uploading} disabled={disabled} onClick={() => input.current?.click()}>
导入 Live2D 模型(最大 128 MiB)
</Button>
{url ? (
<Button
disabled={disabled || uploading}
onClick={() => {
change({ avatarType: "orb", live2dResourceId: "", live2dEntry: "" });
setError("");
}}
>
移除模型引用
</Button>
) : null}
</div>
<label className="flex items-center gap-3 text-sm">
形象高度
<InputNumber aria-label="形象高度" min={120} max={360} value={value.avatarHeight} disabled={disabled || uploading} onChange={(height) => height !== null && change({ avatarHeight: height })} />
px
</label>
<div className="grid justify-items-center gap-3 rounded-xl border border-border bg-background p-5" aria-label="Agent 形象预览">
{url ? (
<Live2DAvatar
key={`${url}-${attempt}`}
url={url}
width={Math.round(value.avatarHeight * 0.75)}
height={value.avatarHeight}
reducedMotion={reducedMotion}
fallback={<FluidOrb size={62} color="#7164f6" />}
onReady={() => {
setReadyURL(url);
setError("");
}}
onError={(reason) => {
setReadyURL("");
setError(reason);
}}
/>
) : (
<FluidOrb size={62} color="#7164f6" />
)}
<strong>{agentCopy(value.welcomeTitle, value.agentName)}</strong>
<p className="text-sm text-foreground/60">{agentCopy(value.welcomeDescription, value.agentName)}</p>
{url ? <small>{readyURL === url ? "预览已就绪,可选择 Live2D 并保存启用" : "等待模型预览就绪"}</small> : null}
</div>
{error ? (
<Alert
type="warning"
showIcon
title="Live2D 暂不可用"
description={error}
action={
<Button
size="small"
onClick={() => {
setError("");
setAttempt((n) => n + 1);
}}
>
重试
</Button>
}
/>
) : null}
<p className="text-xs text-foreground/60">部署者需自行确认 Live2D 授权并安装 Cubism Core 至 /live2d/live2dcubismcore.min.js。未安装时名称、文案和默认动态球仍可使用。未启用的导入包可在资源管理中清理。</p>
</div>
</div>
);
}

View File

@@ -1,7 +1,10 @@
import { http } from "@/services/api/request";
import type { SkinDefinition } from "@/lib/skin-themes";
import type { CanvasAppearance } from "@/lib/canvas/agent-appearance";
import { apiBaseURL } from "@/services/api/request";
export type PublicAppearance = {
canvas?: CanvasAppearance;
schemaVersion: number;
brandName: string;
brandSlug: string;
@@ -31,6 +34,7 @@ export type PublicAppearance = {
};
export type AdminAppearance = {
canvas?: CanvasAppearance;
schemaVersion: number;
brandName: string;
brandSlug: string;
@@ -81,6 +85,7 @@ export async function updateAdminAppearance(
input: Pick<
AdminAppearance,
| "brandName"
| "canvas"
| "brandSlug"
| "authHeroTitle"
| "authHeroDescription"
@@ -115,3 +120,13 @@ export async function uploadAppearanceAsset(slot: AppearanceAssetSlot, file: Fil
const result = await http.post<{ resource: AppearanceResource }>(`/admin/settings/appearance/assets/${slot}`, body);
return result.resource;
}
export async function uploadLive2D(file: File) {
const body = new FormData(); body.append("file", file);
const result = await http.post<{ model: { resourceId: string; entry: string } }>("/admin/settings/appearance/live2d", body);
return result.model;
}
export function live2DModelURL(resourceId: string, entry: string, preview = false) {
return `${apiBaseURL.replace(/\/$/, "")}/${preview ? "admin/settings" : "public"}/appearance/live2d/${encodeURIComponent(resourceId)}/${entry.split("/").map(encodeURIComponent).join("/")}`;
}

View File

@@ -0,0 +1,29 @@
import type { Live2DLoader as Loader } from "pixi-live2d-display/cubism4";
import { apiBaseURL } from "@/services/api/request";
const installed = new WeakSet<typeof Loader>();
// The SDK's default XHR loader omits cross-origin cookies and has no deadline.
// Restrict credentialed requests to our own model API, never model-provided hosts.
export function installLive2DLoader(loader: typeof Loader) {
if (installed.has(loader)) return;
installed.add(loader);
loader.middlewares.unshift(async (context) => {
const base = new URL(`${apiBaseURL.replace(/\/$/, "")}/`, window.location.href);
const url = new URL(context.settings ? context.settings.resolveURL(context.url) : context.url, window.location.href);
const allowed = ["public", "admin/settings"].some((prefix) => url.pathname.startsWith(`${base.pathname}${prefix}/appearance/live2d/`));
if (url.origin !== base.origin || !allowed) throw new Error("Live2D 资源必须来自本站模型接口");
const controller = new AbortController();
const abort = () => controller.abort();
const deadline = setTimeout(abort, 20000);
context.target?.once("destroy", abort);
try {
const response = await fetch(url, { credentials: "include", signal: controller.signal });
if (!response.ok) throw new Error(`Live2D 资源加载失败(${response.status})`);
context.result = context.type === "json" ? await response.json() : await response.arrayBuffer();
} finally {
clearTimeout(deadline);
context.target?.off("destroy", abort);
}
});
}

View File

@@ -1,10 +1,12 @@
import { create } from "zustand";
import { DEFAULT_CANVAS_APPEARANCE } from "@/lib/canvas/agent-appearance";
import type { PublicAppearance } from "@/services/api/appearance";
import { applySkinTheme, DEFAULT_CLASSIC_SKIN, normalizeSkinDefinition } from "@/lib/skin-themes";
export const DEFAULT_PUBLIC_APPEARANCE: PublicAppearance = {
schemaVersion: 8,
canvas: DEFAULT_CANVAS_APPEARANCE,
schemaVersion: 9,
brandName: "影策",
brandSlug: "open-ai-canvas",
authHeroTitle: "让一个故事,\n从文字走向银幕。",
@@ -60,7 +62,8 @@ export function normalizePublicAppearance(value?: Partial<PublicAppearance> | nu
return {
...DEFAULT_PUBLIC_APPEARANCE,
...value,
schemaVersion: 8,
schemaVersion: 9,
canvas: { ...DEFAULT_CANVAS_APPEARANCE, ...value?.canvas },
brandName: resolvedBrandName,
brandSlug,
authHeroTitle,

View File

@@ -5,6 +5,13 @@ import { clampAgentLauncherPosition, moveAgentLauncher, restoreAgentLauncherPosi
const viewport = { width: 1280, height: 800 };
const start = { right: 20, bottom: 20 };
test("Live2D bounds use actual rectangular dimensions when restoring and dragging", () => {
const size = { width: 180, height: 240 };
const small = { width: 320, height: 400 };
expect(restoreAgentLauncherPosition('{"right":999,"bottom":999}', small, size)).toEqual({ right: 120, bottom: 140 });
expect(moveAgentLauncher({ start, x: 0, y: 0, dragged: true }, -999, -999, small, size).position).toEqual({ right: 120, bottom: 140 });
});
test("launcher restores a valid position and defaults safely for corrupt preferences", () => {
expect(restoreAgentLauncherPosition('{"right":240,"bottom":180}', viewport)).toEqual({ right: 240, bottom: 180 });
for (const raw of [null, "invalid", "null", "{}", '{"right":"20","bottom":20}', '{"right":1e999,"bottom":20}']) {

View File

@@ -4,11 +4,12 @@ import { AgentWelcome } from "@/components/canvas/canvas-agent-welcome";
import { AgentChatComposer } from "@/components/canvas/canvas-cloud-agent-chat-ui";
import { agentPermissionVisual } from "@/components/canvas/canvas-cloud-agent-settings";
import { canvasThemes } from "@/lib/canvas-theme";
import { DEFAULT_CANVAS_APPEARANCE, agentCopy } from "@/lib/canvas/agent-appearance";
const noop = () => {};
test("welcome uses the configured brand and escapes it as text", () => {
const html = renderToStaticMarkup(<AgentWelcome brandName="我的<工作室>" nodeCount={2} onChooseSkill={noop} onDraftPrompt={noop} />);
test("welcome uses the independent assistant name and escapes it as text", () => {
const html = renderToStaticMarkup(<AgentWelcome appearance={{ ...DEFAULT_CANVAS_APPEARANCE, agentName: "我的<工作室>" }} nodeCount={2} onChooseSkill={noop} onDraftPrompt={noop} />);
expect(html).toContain("在这里,和我的&lt;工作室&gt;让灵感,慢慢成形");
expect(html).toContain("2 个节点");
expect(html.match(/type="button"/g)).toHaveLength(3);
@@ -16,12 +17,16 @@ test("welcome uses the configured brand and escapes it as text", () => {
});
test("empty canvas disables only the canvas-analysis shortcut", () => {
const html = renderToStaticMarkup(<AgentWelcome brandName="影策" nodeCount={0} onChooseSkill={noop} onDraftPrompt={noop} />);
const html = renderToStaticMarkup(<AgentWelcome appearance={DEFAULT_CANVAS_APPEARANCE} nodeCount={0} onChooseSkill={noop} onDraftPrompt={noop} />);
expect(html.match(/disabled=""/g)).toHaveLength(1);
expect(html).toContain("添加节点后,一起梳理创作思路");
expect(html).not.toContain("生成前由你确认");
});
test("assistant template replacement treats names literally", () => {
expect(agentCopy("{agentName} / {agentName}", "$&小鱼")).toBe("$&小鱼 / $&小鱼");
});
test("permission modes have distinct icons, not just distinct colors", () => {
const icons = (["read_only", "request_approval", "auto"] as const).map((mode) => {
const Icon = agentPermissionVisual(mode).icon;

View File

@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test";
import { adaptLive2DRenderOrders } from "../src/lib/canvas/live2d-core-adapter";
describe("Live2D Core render orders", () => {
test("preserves the legacy Core interface", () => {
const orders = new Int32Array([0]);
const original = () => orders;
const model = { getModel: () => ({ drawables: { count: 1, renderOrders: orders } }), getDrawableRenderOrders: original };
adaptLive2DRenderOrders(model);
expect(model.getDrawableRenderOrders).toBe(original);
});
test("reads current orders from the new Core on every frame", () => {
let orders = new Int32Array([1, 0]);
const model = { getModel: () => ({ drawables: { count: 2 }, offscreens: { count: 0 }, getRenderOrders: () => orders }), getDrawableRenderOrders: () => new Int32Array() };
adaptLive2DRenderOrders(model);
expect(model.getDrawableRenderOrders()).toBe(orders);
orders = new Int32Array([0, 1]);
expect(model.getDrawableRenderOrders()).toBe(orders);
});
test("rejects unsupported offscreens", () => {
const model = { getModel: () => ({ drawables: { count: 1 }, offscreens: { count: 1 } }), getDrawableRenderOrders: () => new Int32Array() };
expect(() => adaptLive2DRenderOrders(model)).toThrow("离屏渲染");
});
test("rejects missing or inconsistent orders", () => {
expect(() => adaptLive2DRenderOrders({ getModel: () => ({ drawables: { count: 1 } }), getDrawableRenderOrders: () => new Int32Array() })).toThrow("缺少");
expect(() => adaptLive2DRenderOrders({ getModel: () => ({ drawables: { count: 1 }, getRenderOrders: () => new Int32Array() }), getDrawableRenderOrders: () => new Int32Array() })).toThrow("不匹配");
});
});

View File

@@ -0,0 +1,47 @@
import { afterEach, expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import { installLive2DLoader } from "../src/services/live2d-loader";
import type { Live2DLoader, Live2DLoaderContext } from "pixi-live2d-display/cubism4";
const originalFetch = globalThis.fetch;
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalWindow) Object.defineProperty(globalThis, "window", originalWindow);
else Reflect.deleteProperty(globalThis, "window");
});
test("Live2D loader restricts credentialed URLs, propagates failures and aborts on destroy", async () => {
Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { href: "https://canvas.example/admin" } } });
const loader = { middlewares: [] } as unknown as typeof Live2DLoader;
installLive2DLoader(loader);
installLive2DLoader(loader);
expect(loader.middlewares).toHaveLength(1);
const run = (context: Live2DLoaderContext) => loader.middlewares[0](context, async () => {});
const url = "/api/admin/settings/appearance/live2d/id/model.model3.json";
let requests = 0;
globalThis.fetch = (async (_url: unknown, options: RequestInit) => {
requests++;
expect(options.credentials).toBe("include");
return new Response('{"Version":3}', { status: 200 });
}) as typeof fetch;
const context = { url, type: "json" } as Live2DLoaderContext;
await run(context);
expect(context.result).toEqual({ Version: 3 });
for (const unsafe of ["https://evil.example" + url, "/api/admin/users", "//evil.example/model.json"]) {
await expect(run({ url: unsafe, type: "json" })).rejects.toThrow("本站模型接口");
}
expect(requests).toBe(1);
globalThis.fetch = (async () => new Response("denied", { status: 403 })) as typeof fetch;
await expect(run({ url, type: "json" })).rejects.toThrow("403");
const target = new EventEmitter();
globalThis.fetch = ((_url: unknown, options: RequestInit) =>
new Promise((_resolve, reject) => {
options.signal?.addEventListener("abort", () => reject(new Error("aborted")));
})) as typeof fetch;
const pending = run({ url, type: "json", target: target as unknown as Live2DLoaderContext["target"] });
target.emit("destroy");
await expect(pending).rejects.toThrow("aborted");
expect(target.listenerCount("destroy")).toBe(0);
});