Compare commits
7 Commits
90fdb44b20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 12edf0b545 | |||
| 034496e946 | |||
| d02abce58d | |||
| 74266dc5cc | |||
| b4e05a4a16 | |||
| 2713897bdb | |||
| a1e2897364 |
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"*.{ts,tsx,js,jsx}": ["oxlint --fix", "oxfmt"],
|
"*.{ts,tsx,js,jsx}": ["oxlint --fix", "oxfmt"],
|
||||||
"*.{md,json,yaml,yml}": ["oxfmt"]
|
"*.{md,json,yaml,yml}": ["oxfmt"],
|
||||||
|
"!openspec/**": []
|
||||||
}
|
}
|
||||||
|
|||||||
128
bun.lock
128
bun.lock
@@ -6,14 +6,14 @@
|
|||||||
"name": "gateway-checker",
|
"name": "gateway-checker",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.81",
|
"@ai-sdk/anthropic": "^3.0.81",
|
||||||
"@ai-sdk/openai": "^3.0.66",
|
"@ai-sdk/openai": "^3.0.68",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.48",
|
"@ai-sdk/openai-compatible": "^2.0.48",
|
||||||
"@ai-sdk/react": "^3.0.195",
|
"@ai-sdk/react": "^3.0.199",
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.5",
|
||||||
"@ant-design/x": "^2.7.0",
|
"@ant-design/x": "^2.7.0",
|
||||||
"@sinclair/typebox": "^0.34.49",
|
"@sinclair/typebox": "^0.34.49",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"ai": "^6.0.193",
|
"ai": "^6.0.197",
|
||||||
"ajv": "^8.20.0",
|
"ajv": "^8.20.0",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -24,40 +24,40 @@
|
|||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"pino-roll": "^4.0.0",
|
"pino-roll": "^4.0.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.7",
|
||||||
"react-router": "^7.15.1",
|
"react-router": "^7.17.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"shiki": "^4.2.0",
|
"shiki": "^4.2.0",
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^21.0.1",
|
"@commitlint/cli": "^21.0.2",
|
||||||
"@commitlint/config-conventional": "^21.0.1",
|
"@commitlint/config-conventional": "^21.0.2",
|
||||||
"@happy-dom/global-registrator": "^20.10.1",
|
"@happy-dom/global-registrator": "^20.10.2",
|
||||||
"@tanstack/react-query-devtools": "^5.100.14",
|
"@tanstack/react-query-devtools": "^5.101.0",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^17.0.5",
|
"lint-staged": "^17.0.7",
|
||||||
"oxfmt": "^0.53.0",
|
"oxfmt": "^0.53.0",
|
||||||
"oxlint": "^1.68.0",
|
"oxlint": "^1.68.0",
|
||||||
"oxlint-tsgolint": "^0.23.0",
|
"oxlint-tsgolint": "^0.23.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.0.16",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.81.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.81.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.121", "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.121.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uY248djJRxa5W68MHiyqO8WLdOeKQoRClGg7PVX/VPhVW8SJNM7/l5DcrA5WAM3YfQrLyNkgZa2VOu8T0t8LUw=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.125", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tocl7cUDoTpmhZqeW8XVKMMznZQwwQAEunF0VyNKmf64qt8NbMIAEiet/vRMzh7Jr9WcFeb6EZjmhLTP4Qx2Og=="],
|
||||||
|
|
||||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.66", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.66.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-n9mZ7PbU7O2zN8UMcx495Gfx7sE/rL4KS+o5JzBOUbYJCuwEIxKO6yJaUkxa4r6IyiLxyGib0jegZw91Hh0diA=="],
|
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.68", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FCs/DPr4M95UyZ/ABHJmTmCEYRCka/4J0Bna0nsd78QCdGIS0X/zhn+fVzB7mZJo7464uOWYUjROx9PGNGOb0w=="],
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.48.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="],
|
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.48.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="],
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.195", "https://registry.npmmirror.com/@ai-sdk/react/-/react-3.0.195.tgz", { "dependencies": { "@ai-sdk/provider-utils": "4.0.27", "ai": "6.0.193", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-+yIH84d4bBNzLKfaDDf4EocEH0XQKKNwNShxbrz5xAiJMNIPnWVWT9cyrSerYaGH3iNVS/g2io42PE4HNbc4RA=="],
|
"@ai-sdk/react": ["@ai-sdk/react@3.0.199", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.27", "ai": "6.0.197", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-0QmG6nd1iDTTWpWbQbE5qgSpEm0XkBvrOn1L1rSzBhG5+7BasckcjTF3CQMwUxdvozMMYRNOGXLQODs/1+a3NQ=="],
|
||||||
|
|
||||||
"@ant-design/colors": ["@ant-design/colors@8.0.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="],
|
"@ant-design/colors": ["@ant-design/colors@8.0.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="],
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
|
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
|
||||||
|
|
||||||
"@ant-design/icons": ["@ant-design/icons@6.2.3", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
|
"@ant-design/icons": ["@ant-design/icons@6.2.5", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw=="],
|
||||||
|
|
||||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||||
|
|
||||||
@@ -95,9 +95,9 @@
|
|||||||
|
|
||||||
"@chevrotain/types": ["@chevrotain/types@11.1.2", "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
"@chevrotain/types": ["@chevrotain/types@11.1.2", "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
||||||
|
|
||||||
"@commitlint/cli": ["@commitlint/cli@21.0.1", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.1.tgz", { "dependencies": { "@commitlint/format": "^21.0.1", "@commitlint/lint": "^21.0.1", "@commitlint/load": "^21.0.1", "@commitlint/read": "^21.0.1", "@commitlint/types": "^21.0.1", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-8vq10krmbJwBkvzXKhbs4o4JQEVscd3pqOlWuDUaDBwbeL694/P33UC29tZQFTAgPU9fVJ2+f2m3zw16yKWxHg=="],
|
"@commitlint/cli": ["@commitlint/cli@21.0.2", "", { "dependencies": { "@commitlint/format": "^21.0.1", "@commitlint/lint": "^21.0.2", "@commitlint/load": "^21.0.2", "@commitlint/read": "^21.0.2", "@commitlint/types": "^21.0.1", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg=="],
|
||||||
|
|
||||||
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.1", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-gRorrkfWOh/+V5X8GYWWbQvrzPczopGMS4CCNrQdHkK4xWElv82BDvIsDhJZWTlI7TazOlYea6VATufCsFs+sw=="],
|
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.2", "", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg=="],
|
||||||
|
|
||||||
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.1", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "ajv": "^8.11.0" } }, "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA=="],
|
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.1", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "ajv": "^8.11.0" } }, "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA=="],
|
||||||
|
|
||||||
@@ -107,25 +107,25 @@
|
|||||||
|
|
||||||
"@commitlint/format": ["@commitlint/format@21.0.1", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "picocolors": "^1.1.1" } }, "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw=="],
|
"@commitlint/format": ["@commitlint/format@21.0.1", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "picocolors": "^1.1.1" } }, "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw=="],
|
||||||
|
|
||||||
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.1", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "semver": "^7.6.0" } }, "sha512-iNDP8SFdw8JEkM0CHZ2XFnhTN4Zg5jKUY2d8kBOSFrI2aA+3YJI7fcqVpfgbpJ9xtxFVYpi+DBATU5AvhoTq8g=="],
|
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.2", "", { "dependencies": { "@commitlint/types": "^21.0.1", "semver": "^7.6.0" } }, "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ=="],
|
||||||
|
|
||||||
"@commitlint/lint": ["@commitlint/lint@21.0.1", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.1.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.1", "@commitlint/parse": "^21.0.1", "@commitlint/rules": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-gF+iYtUw1gBG3HUH9z3VxwUjGg2R2G5j+nmvPs8aIeYkiB7TtneBu3wO85I0bUl93bYNsvsCNI9Nte2fmDUMww=="],
|
"@commitlint/lint": ["@commitlint/lint@21.0.2", "", { "dependencies": { "@commitlint/is-ignored": "^21.0.2", "@commitlint/parse": "^21.0.2", "@commitlint/rules": "^21.0.2", "@commitlint/types": "^21.0.1" } }, "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg=="],
|
||||||
|
|
||||||
"@commitlint/load": ["@commitlint/load@21.0.1", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/execute-rule": "^21.0.1", "@commitlint/resolve-extends": "^21.0.1", "@commitlint/types": "^21.0.1", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-Btg1q1mKmiihN4W3x0EsPDrJMOQfMa9NIqlzlJyXAfxvsOGdGXOW5p3R3RcSxDCaY7JabY9flIl+Om1af3PSrw=="],
|
"@commitlint/load": ["@commitlint/load@21.0.2", "", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/execute-rule": "^21.0.1", "@commitlint/resolve-extends": "^21.0.1", "@commitlint/types": "^21.0.1", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA=="],
|
||||||
|
|
||||||
"@commitlint/message": ["@commitlint/message@21.0.1", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.1.tgz", {}, "sha512-R3dVQeJQ0B6yqrZEjkUHD4r7UJYLV9Lvk2xs3PTOmtWk2G3mI6Xgc+YdRxL1PwcDfBiUjv2SkIkW4AUc976w1w=="],
|
"@commitlint/message": ["@commitlint/message@21.0.2", "", {}, "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g=="],
|
||||||
|
|
||||||
"@commitlint/parse": ["@commitlint/parse@21.0.1", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-oh/nCSOqdoeQNA1tO8aAmxkq5EBo8/NzcFQRvv66AWc9HpED28sL2iSicCKU6hPintWuscL6BJEWi77Wq1LPMQ=="],
|
"@commitlint/parse": ["@commitlint/parse@21.0.2", "", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw=="],
|
||||||
|
|
||||||
"@commitlint/read": ["@commitlint/read@21.0.1", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.1.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.1", "@commitlint/types": "^21.0.1", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-pMEu4lbpC8W0ZgKJj2U6WaobXIZWdFlULpIEewYhkPXx+WZcnoO53YrVPc7QErQuNolq2Me8dP58Wu7YAVXVOA=="],
|
"@commitlint/read": ["@commitlint/read@21.0.2", "", { "dependencies": { "@commitlint/top-level": "^21.0.2", "@commitlint/types": "^21.0.1", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw=="],
|
||||||
|
|
||||||
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.1", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg=="],
|
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.1", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg=="],
|
||||||
|
|
||||||
"@commitlint/rules": ["@commitlint/rules@21.0.1", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.1.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.1", "@commitlint/message": "^21.0.1", "@commitlint/to-lines": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-VMooYpz4nJg7xlaUso6CCOWEz8D/ChkvsvZUMARcoJ1ZpfKPyFCGrHNha2tbsETNAb6ErgiRuCr2DvghrvPDYQ=="],
|
"@commitlint/rules": ["@commitlint/rules@21.0.2", "", { "dependencies": { "@commitlint/ensure": "^21.0.1", "@commitlint/message": "^21.0.2", "@commitlint/to-lines": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ=="],
|
||||||
|
|
||||||
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.1", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz", {}, "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw=="],
|
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.1", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz", {}, "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw=="],
|
||||||
|
|
||||||
"@commitlint/top-level": ["@commitlint/top-level@21.0.1", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.1.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-4esUYqzY7K0FCgcJ/1xWEZekV7Ch4yZT1+xjEb7KzqbJ05XEkxHVsTfC8ADKNNtlCE2pj98KEbPGZWw9WwEnVw=="],
|
"@commitlint/top-level": ["@commitlint/top-level@21.0.2", "", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug=="],
|
||||||
|
|
||||||
"@commitlint/types": ["@commitlint/types@21.0.1", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg=="],
|
"@commitlint/types": ["@commitlint/types@21.0.1", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg=="],
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
|
||||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.10.1", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.10.1" } }, "sha512-nIT1Jdsar1KnLuqX+q3oMKwUIDcSJ4GnGMBnCtXLLL+lIpqaBBACPIo9By3NeF15XDPLDLIou8aQd3/xqZoSkQ=="],
|
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.10.2", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.10.2" } }, "sha512-/DC0hluanNJDVPUu69cidD46sGwzt8MJATiGx7WgCScn+ZH48fJQ0fvTfMPXY82/ASXWxnNo8P4BdHyU/dI/EA=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.132.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.132.0.tgz", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
||||||
|
|
||||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.53.0", "https://registry.npmmirror.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg=="],
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.53.0", "https://registry.npmmirror.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg=="],
|
||||||
|
|
||||||
@@ -389,35 +389,35 @@
|
|||||||
|
|
||||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="],
|
||||||
|
|
||||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="],
|
||||||
|
|
||||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="],
|
||||||
|
|
||||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
@@ -447,13 +447,13 @@
|
|||||||
|
|
||||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.100.14", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.100.14.tgz", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="],
|
||||||
|
|
||||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.14", "https://registry.npmmirror.com/@tanstack/query-devtools/-/query-devtools-5.100.14.tgz", {}, "sha512-g96SmSSQecYTYcyuAMRXr895GplJv01UGt7qttQWPOUyZ5EGz5tbRc589bMc2m5BsPFD6O0PCEAHdbDYNP6UBw=="],
|
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.101.0", "", {}, "sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.100.14", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.100.14.tgz", { "dependencies": { "@tanstack/query-core": "5.100.14" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="],
|
||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.14", "https://registry.npmmirror.com/@tanstack/react-query-devtools/-/react-query-devtools-5.100.14.tgz", { "dependencies": { "@tanstack/query-devtools": "5.100.14" }, "peerDependencies": { "@tanstack/react-query": "^5.100.14", "react": "^18 || ^19" } }, "sha512-JkP5VDgKOw3t/QSA1OABRHEqx8BuNs5MfvZRooNqdvN57SzTuGq3fKR1a2IH5rqa5HDLUm+FOXUEnB9ueHiLzg=="],
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.101.0", "", { "dependencies": { "@tanstack/query-devtools": "5.101.0" }, "peerDependencies": { "@tanstack/react-query": "^5.101.0", "react": "^18 || ^19" } }, "sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w=="],
|
||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
"@testing-library/dom": ["@testing-library/dom@10.4.1", "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||||
|
|
||||||
@@ -537,7 +537,7 @@
|
|||||||
|
|
||||||
"@types/prismjs": ["@types/prismjs@1.26.6", "https://registry.npmmirror.com/@types/prismjs/-/prismjs-1.26.6.tgz", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
|
"@types/prismjs": ["@types/prismjs@1.26.6", "https://registry.npmmirror.com/@types/prismjs/-/prismjs-1.26.6.tgz", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.15", "https://registry.npmmirror.com/@types/react/-/react-19.2.15.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
|
||||||
|
|
||||||
"ai": ["ai@6.0.193", "https://registry.npmmirror.com/ai/-/ai-6.0.193.tgz", { "dependencies": { "@ai-sdk/gateway": "3.0.121", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VQOTOse8+X8kMtg61DNSXlYJzwOW4NjMLDJNk/qxClWsFe4oiyFJDHGGG1oezfGcFzuYuQe/8Z7r4kwiZWh2YQ=="],
|
"ai": ["ai@6.0.197", "", { "dependencies": { "@ai-sdk/gateway": "3.0.125", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-U3KsjkqwQXGHC0u0VeUDqUaNaBS/uQc7v4Vj92Cjv5lPx5DIyRBQYk4Hipy5vwD9AQKIG8uRvdaN9R+pAvrtcQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
"ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
@@ -781,7 +781,7 @@
|
|||||||
|
|
||||||
"hachure-fill": ["hachure-fill@0.5.2", "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
"hachure-fill": ["hachure-fill@0.5.2", "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.10.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-awPoqPjx8CgjapJllyDlgzgVHjBExcitKK5ZJkxwhQJyQpHFkyS2bEcqCm7IeW20cQvuCI0cz2Ifq79CJKqtiw=="],
|
"happy-dom": ["happy-dom@20.10.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ=="],
|
||||||
|
|
||||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||||
|
|
||||||
@@ -879,7 +879,7 @@
|
|||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
"lint-staged": ["lint-staged@17.0.5", "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.5.tgz", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-d12yC+/e8RhBjZtaxZn71FyrgU/P5e+uAPifhCLwdosQZP/zamSdKRWDC30ocVIbzDKiFG1McHc/LUgB92GIPw=="],
|
"lint-staged": ["lint-staged@17.0.7", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA=="],
|
||||||
|
|
||||||
"listr2": ["listr2@10.2.1", "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="],
|
"listr2": ["listr2@10.2.1", "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="],
|
||||||
|
|
||||||
@@ -981,15 +981,15 @@
|
|||||||
|
|
||||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
|
|
||||||
"react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.6", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
"react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
"react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.15.1", "https://registry.npmmirror.com/react-router/-/react-router-7.15.1.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A=="],
|
"react-router": ["react-router@7.17.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ=="],
|
||||||
|
|
||||||
"react-syntax-highlighter": ["react-syntax-highlighter@16.1.1", "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA=="],
|
"react-syntax-highlighter": ["react-syntax-highlighter@16.1.1", "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA=="],
|
||||||
|
|
||||||
@@ -1023,7 +1023,7 @@
|
|||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
"robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.2", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
|
"rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
|
||||||
|
|
||||||
"roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
"roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||||
|
|
||||||
@@ -1087,7 +1087,7 @@
|
|||||||
|
|
||||||
"tinyexec": ["tinyexec@1.1.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
|
"tinyexec": ["tinyexec@1.1.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@2.1.0", "https://registry.npmmirror.com/tinypool/-/tinypool-2.1.0.tgz", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
"tinypool": ["tinypool@2.1.0", "https://registry.npmmirror.com/tinypool/-/tinypool-2.1.0.tgz", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||||
|
|
||||||
@@ -1123,7 +1123,7 @@
|
|||||||
|
|
||||||
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||||
|
|
||||||
"vite": ["vite@8.0.14", "https://registry.npmmirror.com/vite/-/vite-8.0.14.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
|
"vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="],
|
||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||||
|
|
||||||
@@ -1145,9 +1145,11 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@ant-design/x/@ant-design/icons": ["@ant-design/icons@6.2.3", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
|
||||||
|
|
||||||
"@commitlint/ensure/es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
"@commitlint/ensure/es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
||||||
|
|
||||||
"@commitlint/load/es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
"@commitlint/read/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||||
|
|
||||||
"@commitlint/resolve-extends/es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
"@commitlint/resolve-extends/es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
||||||
|
|
||||||
@@ -1155,6 +1157,8 @@
|
|||||||
|
|
||||||
"@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
"@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
||||||
|
|
||||||
|
"antd/@ant-design/icons": ["@ant-design/icons@6.2.3", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
|
||||||
|
|
||||||
"cli-truncate/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
"cli-truncate/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
"cliui/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
@@ -1169,6 +1173,8 @@
|
|||||||
|
|
||||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"import-fresh/resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"lint-staged/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||||
|
|
||||||
"log-update/slice-ansi": ["slice-ansi@7.1.2", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
"log-update/slice-ansi": ["slice-ansi@7.1.2", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
||||||
|
|
||||||
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si
|
|||||||
|
|
||||||
## 页面
|
## 页面
|
||||||
|
|
||||||
| 页面 | 路径 | 入口 |
|
| 页面 | 路径 | 入口 |
|
||||||
| -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| 总览 | `/` | `features/dashboard/index.tsx` |
|
| 总览 | `/` | `features/dashboard/index.tsx` |
|
||||||
| 项目管理 | `/projects` | `features/projects/index.tsx` — FilterToolbar(状态 Select + 搜索 + 新建/归档恢复删除) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除、列表排序、URL 同步筛选参数。 |
|
| 项目管理 | `/projects` | `features/projects/index.tsx` — FilterToolbar(状态 Select + 搜索 + 新建/归档恢复删除) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除、列表排序、URL 同步筛选参数。 |
|
||||||
| 模型管理 | `/models` 和 `/models/providers` | 独立路由页面:`ModelListPage.tsx`(FilterToolbar + ModelTable) + `ProviderListPage.tsx`(FilterToolbar + ProviderTable)。模型支持供应商/能力筛选和列表排序,供应商支持类型筛选和列表排序。模型表单使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`)。 |
|
| 模型管理 | `/models` 和 `/models/providers` | 独立路由页面:`ModelListPage.tsx`(FilterToolbar + ModelTable) + `ProviderListPage.tsx`(FilterToolbar + ProviderTable)。模型支持供应商/能力筛选和列表排序,供应商支持类型筛选和列表排序。模型表单使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`)。 |
|
||||||
| 设置 | `/settings` | `features/settings/index.tsx` — 卡片式布局分区管理平台业务设置。"主题"卡片使用 antd Form 水平布局,包含主题模式(Radio.Group 按钮风:系统/明亮/黑暗)和紧凑模式(Switch 开关),使用 `useSettings` hook 通过 `GET/PUT /api/settings` 实时保存,`message` toast 反馈。 |
|
| 设置 | `/settings` | `features/settings/index.tsx` — 卡片式布局分区管理平台业务设置。"主题"卡片使用 antd Form 水平布局,包含主题模式(Radio.Group 按钮风:系统/明亮/黑暗)和紧凑模式(Switch 开关),使用 `useSettings` hook 通过 `GET/PUT /api/settings` 实时保存,`message` toast 反馈。 |
|
||||||
| 聊天室 | `/workbench/:id` | `features/chat/index.tsx` |
|
| 聊天室 | `/workbench/:id` | `features/chat/index.tsx` |
|
||||||
| 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层(selectedId + modalOpen)+ MaterialSidebar(列表容器)+ MaterialDetailPanel(详情容器)+ AddMaterialModal。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 |
|
| 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层(selectedId + modalOpen)+ MaterialSidebar(列表容器)+ MaterialDetailPanel(透明内容区+OS滚动+操作区卡片)+ MaterialContent(纵向卡片流,基本信息Card)+ AddMaterialModal。操作区始终渲染,按钮 fill 样式 + disabled 控制。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 |
|
||||||
| 404 | `*` | `features/not-found/index.tsx` |
|
| 404 | `*` | `features/not-found/index.tsx` |
|
||||||
|
|
||||||
### 聊天页面
|
### 聊天页面
|
||||||
|
|
||||||
|
|||||||
15
drizzle/0007_create_entities.sql
Normal file
15
drizzle/0007_create_entities.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `entities` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`created_at` text NOT NULL,
|
||||||
|
`updated_at` text NOT NULL,
|
||||||
|
`deleted_at` text,
|
||||||
|
`project_id` text NOT NULL REFERENCES `projects`(`id`),
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`type` text NOT NULL DEFAULT 'other',
|
||||||
|
`description` text NOT NULL DEFAULT '',
|
||||||
|
`aliases` text NOT NULL DEFAULT '[]'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS `entities_project_id_idx` ON `entities` (`project_id`);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS `entities_name_idx` ON `entities` (`name`);
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
当 OpenSpec `code-drive` 的 apply 阶段生成 `blocker.md` 并暂停时,按照本提示词修订规划 artifacts。目标是修正不成立的部分,而不是强行继续实现。
|
当 OpenSpec `code-drive` 的 apply 阶段生成 `blocker.md` 并暂停时,按照本提示词修订规划 artifacts。目标是修正不成立的部分,而不是强行继续实现。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
读取 `blocker.md` 及上下游 artifacts,识别阻塞本质,定位最上游的修订入口,通过用户决策流程选定修订方向,按 code-drive 各 artifact 的 instruction 逐层修订受影响的部分,最终让 apply 可以从修订后的待办任务继续。
|
||||||
|
|
||||||
|
本提示词是 code-drive 中除 requirements 之外**唯一允许向用户发起决策型提问**的入口,决策能力受"用户决策流程(强制协议)"约束。
|
||||||
|
|
||||||
## 输入
|
## 输入
|
||||||
|
|
||||||
- 当前 OpenSpec change 目录
|
- 当前 OpenSpec change 目录
|
||||||
@@ -11,89 +17,179 @@
|
|||||||
- `plan.md`
|
- `plan.md`
|
||||||
- `tasks.md`
|
- `tasks.md`
|
||||||
- `openspec/schemas/code-drive/schema.yaml`(用于读取各 artifact 的 instruction)
|
- `openspec/schemas/code-drive/schema.yaml`(用于读取各 artifact 的 instruction)
|
||||||
|
- `openspec/schemas/code-drive/templates/*.md`(用于读取各 artifact 的模板结构)
|
||||||
|
|
||||||
|
## 工具使用(先决说明)
|
||||||
|
|
||||||
|
- **todo 工具**:用于跟踪步骤 5/6/7 内的子步骤进度;子步骤颗粒度(如 5.1 / 5.2 / 5.3)直接对应 todo 条目;todo 只是过程跟踪,最终事实必须写回对应 artifact 文件
|
||||||
|
- **question / choice 工具**:触发用户决策流程时必须优先使用(如工具支持),按"用户决策格式(强制)"组织候选与说明
|
||||||
|
- **读写工具**:本提示词需要写入多个 artifact,但写入前必须先读取对应 artifact 的当前内容;禁止凭印象重写
|
||||||
|
- **只读探索工具**:用于步骤 2 的代码库调查(仅在阻塞点涉及未被探索过的代码时使用)
|
||||||
|
|
||||||
|
## 用户决策流程(强制协议)
|
||||||
|
|
||||||
|
本协议是 blocker-revise 阶段的**唯一决策出口**。任何超出"修订执行细节"层级的方向选择必须走本协议,不得 AI 自决。
|
||||||
|
|
||||||
|
### 触发条件(命中即必须启动)
|
||||||
|
|
||||||
|
1. **核心修订方向存在多种可行路径**:blocker.md 列出的可选方案 ≥ 2 个,或 AI 补充的方案与原方案构成真正的取舍(不是同一思路的两种写法)
|
||||||
|
2. **blocker.md 建议的修订方向会扩展本次业务范围或引入核心新依赖**:与原 requirements / design 不一致,必须显式征得用户同意
|
||||||
|
3. **修订入口在不同 artifact 之间犹豫**:例如 blocker 表面指向 plan,但根因可能在 design 或 requirements,需要用户判断修订起点
|
||||||
|
4. **blocker.md 未列出可选方案,仅描述了阻塞现象**:AI 必须主动补充 2-3 个候选方向并请用户选择
|
||||||
|
|
||||||
|
未命中以上任何条件时,**不得**主动发起决策型提问;AI 自决范围参见下文。
|
||||||
|
|
||||||
|
### AI 自决范围(无需启动用户决策流程)
|
||||||
|
|
||||||
|
以下类型的修订属于执行细节,AI 自决后直接执行,无需启动用户决策流程:
|
||||||
|
|
||||||
|
1. **执行步骤的局部调整**:plan.md 阶段内步骤的拆分、合并、重排
|
||||||
|
2. **任务粒度细化或合并**:tasks.md checkbox 的拆分/合并,但不删除整个分组
|
||||||
|
3. **描述措辞修正**:澄清歧义、补充缺失细节、修正笔误
|
||||||
|
4. **已完成任务的保留决策**:阻塞未证明无效时,已完成 checkbox 必须保留
|
||||||
|
5. **修订记录的措辞与格式**:blocker.md 末尾的修订记录按本提示词模板填写
|
||||||
|
6. **工具使用顺序**:todo / 读写工具的使用顺序与时机
|
||||||
|
|
||||||
|
### 用户决策格式(强制)
|
||||||
|
|
||||||
|
每次启动用户决策流程时,输出必须包含:
|
||||||
|
|
||||||
|
1. **2-3 个候选选项**:每个选项含义明确,避免"方案 A / 方案 B"这类无信息标签
|
||||||
|
2. **推荐方案**:AI 必须明确推荐其中一项,不得回避或"中立呈现"
|
||||||
|
3. **取舍说明**:
|
||||||
|
- 每个非推荐方案:说明未选它的核心理由(一句话即可)
|
||||||
|
- 推荐方案:说明选择它的核心理由 + 主要代价
|
||||||
|
4. **影响范围预测**(blocker-revise 特有):每个选项预测将影响哪些 artifact 需要修订,并粗估修订量(小改 / 中改 / 重写章节)
|
||||||
|
5. **使用工具**:优先使用 question/choice 工具;工具不可用时以 markdown 形式直接呈现
|
||||||
|
|
||||||
|
### 强制语义(不得跳过)
|
||||||
|
|
||||||
|
- 触发条件命中时**必须**启动用户决策流程,即使你倾向"自己决定"或"按 blocker.md 建议执行"
|
||||||
|
- 即使用户回复"你看着办",也必须回复"推荐方案 + 主要代价",请用户**显式确认**推荐方案,不得默认接受"你看着办"作为决策
|
||||||
|
- 决策方向涉及扩展本次业务范围或引入核心新依赖时(触发条件 2),必须**额外显式提示**"本选项将扩展本次范围 / 引入新依赖",并征得用户的明确同意(不接受默认)
|
||||||
|
- 用户未决策前**不得**进入步骤 5 的实际修订
|
||||||
|
|
||||||
|
### 决策归档规则
|
||||||
|
|
||||||
|
用户给出决策后:
|
||||||
|
|
||||||
|
1. **决策结论融入对应 artifact 的相关章节**——不设独立的"决策记录"章节
|
||||||
|
2. **在 `blocker.md` 末尾追加"修订记录"段**:记录选择方案、选择理由、修改的 artifacts 列表、被取消勾选的 tasks——这是审计线索,不是二次决策入口
|
||||||
|
3. **决策引发的修订如果触及 requirements / design 的关键决策**,按各 artifact instruction 中"决策归档规则"融入对应章节
|
||||||
|
4. **决策结论应可在 apply 恢复后直接使用**——明确到 apply 阶段无需重新发起决策
|
||||||
|
|
||||||
## 工作流
|
## 工作流
|
||||||
|
|
||||||
### 1. 阅读并理解阻塞
|
### 步骤 1: 阅读并理解阻塞
|
||||||
|
|
||||||
阅读 `blocker.md`,识别:
|
阅读 `blocker.md`,识别:
|
||||||
|
|
||||||
- 阻塞点的本质(不是症状)
|
- 阻塞点的**本质**(不是症状)
|
||||||
- 当前位置:任务编号、`plan.md` 阶段、相关文件
|
- 当前位置:任务编号、`plan.md` 阶段、相关文件
|
||||||
- 已尝试的方案及失败原因(避免重复)
|
- 已尝试的方案及失败原因(避免重复)
|
||||||
- `blocker.md` 建议的修订目标
|
- `blocker.md` 建议的修订目标(如有)
|
||||||
|
|
||||||
### 2. 影响分析
|
完成本步骤后进入步骤 2。
|
||||||
|
|
||||||
|
### 步骤 2: 影响分析
|
||||||
|
|
||||||
根据 `blocker.md` 的影响范围,系统分析上下游影响链:
|
根据 `blocker.md` 的影响范围,系统分析上下游影响链:
|
||||||
|
|
||||||
- 如果 `requirements.md` 需要修订:检查 `design.md` 的哪些决策依赖它,再检查 `plan.md` 的哪些阶段受影响,最后检查 `tasks.md` 的哪些 checkbox 需要取消
|
- 如果 `requirements.md` 需要修订 → 检查 `design.md` 的哪些决策依赖它,再检查 `plan.md` 的哪些阶段受影响,最后检查 `tasks.md` 的哪些 checkbox 需要取消
|
||||||
- 如果 `design.md` 需要修订:检查 `plan.md` 的哪些阶段依赖它,再检查 `tasks.md` 的哪些 checkbox 需要取消
|
- 如果 `design.md` 需要修订 → 检查 `plan.md` 的哪些阶段依赖它,再检查 `tasks.md` 的哪些 checkbox 需要取消
|
||||||
- 如果 `plan.md` 需要修订:检查 `tasks.md` 的哪些 checkbox 依赖它,以及是否有下游阶段依赖被阻塞阶段的输出
|
- 如果 `plan.md` 需要修订 → 检查 `tasks.md` 的哪些 checkbox 依赖它,以及是否有下游阶段依赖被阻塞阶段的输出
|
||||||
- 如果 `tasks.md` 需要修订:只影响当前任务及其直接依赖
|
- 如果 `tasks.md` 需要修订 → 只影响当前任务及其直接依赖
|
||||||
|
|
||||||
记录每个 artifact 的影响程度:必须修订 / 可能受影响 / 无影响。
|
记录每个 artifact 的影响程度:**必须修订 / 可能受影响 / 无影响**。
|
||||||
|
|
||||||
### 3. 确定修订入口
|
如阻塞点涉及未被探索过的代码模块,使用只读探索工具补充上下文;否则不发起额外探索。
|
||||||
|
|
||||||
根据影响分析,确定需要修订的最上游 artifact:
|
完成本步骤后进入步骤 3。
|
||||||
|
|
||||||
- 如果需要修订 `requirements.md`:从 requirements 开始,依次修订 design、plan、tasks
|
### 步骤 3: 确定修订入口
|
||||||
- 如果需要修订 `design.md`:从 design 开始,依次修订 plan、tasks
|
|
||||||
- 如果需要修订 `plan.md`:从 plan 开始,修订 tasks
|
|
||||||
- 如果只需要修订 `tasks.md`:只修订 tasks
|
|
||||||
|
|
||||||
### 4. 用户决策
|
根据影响分析,确定需要修订的**最上游** artifact:
|
||||||
|
|
||||||
让用户从 `blocker.md` 中列出的可选方案中选择。如果选项不足,提出 2-3 个方案并说明取舍。工具支持时优先使用 question/choice 工具。
|
- 需要修订 `requirements.md` → 从 requirements 开始,依次修订 design、plan、tasks
|
||||||
|
- 需要修订 `design.md` → 从 design 开始,依次修订 plan、tasks
|
||||||
|
- 需要修订 `plan.md` → 从 plan 开始,修订 tasks
|
||||||
|
- 只需要修订 `tasks.md` → 只修订 tasks
|
||||||
|
|
||||||
### 5. 执行修订
|
如果"修订入口在不同 artifact 之间犹豫"(触发条件 3),先暂停并启动用户决策流程;用户决策后回到本步骤。
|
||||||
|
|
||||||
从修订入口开始,按正常 code-drive 流程逐层修订下游 artifacts。
|
完成本步骤后进入步骤 4。
|
||||||
|
|
||||||
每个 artifact 的修订必须遵循该 artifact 的 instruction 和 template:
|
### 步骤 4: 用户决策
|
||||||
|
|
||||||
|
检查是否命中"用户决策流程(强制协议)"中任一触发条件:
|
||||||
|
|
||||||
|
- **命中** → 按强制格式输出候选 + 推荐 + 取舍 + 影响范围预测,等待用户决策
|
||||||
|
- **未命中** → 直接进入步骤 5(属于 AI 自决范围)
|
||||||
|
|
||||||
|
用户决策后或确认无需决策后,进入步骤 5。
|
||||||
|
|
||||||
|
### 步骤 5: 执行修订
|
||||||
|
|
||||||
|
从步骤 3 确定的修订入口开始,按 code-drive 正常流程逐层修订下游 artifacts。
|
||||||
|
|
||||||
|
**子步骤 5.1: 读取 instruction 与 template**
|
||||||
|
|
||||||
|
对每个待修订的 artifact:
|
||||||
|
|
||||||
1. 读取 `schema.yaml` 中该 artifact 的 `instruction`
|
1. 读取 `schema.yaml` 中该 artifact 的 `instruction`
|
||||||
2. 读取该 artifact 的 `template`
|
2. 读取该 artifact 的 `template`
|
||||||
3. 按 instruction 的工作规则和必需章节进行修订
|
3. 后续修订必须遵循 instruction 工作流和 template 结构
|
||||||
4. 确保修订后的 artifact 符合 template 结构
|
|
||||||
|
|
||||||
修订范围原则:
|
**子步骤 5.2: 局部修订**
|
||||||
|
|
||||||
|
按修订范围原则执行:
|
||||||
|
|
||||||
- 只改错误的部分,不重写整个章节(除非整个章节的基础假设不成立)
|
- 只改错误的部分,不重写整个章节(除非整个章节的基础假设不成立)
|
||||||
- 改了 `plan.md` 阶段的实现步骤时,同步更新 `tasks.md` 对应 checkbox 的描述
|
- 改了 `plan.md` 阶段的实现步骤时,同步更新 `tasks.md` 对应 checkbox 的描述
|
||||||
- 改了 `design.md` 的关键决策时,检查 `plan.md` 的代码模式是否需要同步,但不自动重写
|
- 改了 `design.md` 的关键决策时,检查 `plan.md` 的代码模式是否需要同步,但不自动重写
|
||||||
- 改了 `requirements.md` 时,逐层向下检查影响,每层只改受影响的部分
|
- 改了 `requirements.md` 时,逐层向下检查影响,每层只改受影响的部分
|
||||||
- 如果修订导致 `tasks.md` 分组结构变化,重新对齐 plan -> tasks 映射
|
- 如果修订导致 `tasks.md` 分组结构变化,重新对齐 plan → tasks 映射
|
||||||
|
|
||||||
保留已完成任务:
|
**子步骤 5.3: 保留已完成任务**
|
||||||
|
|
||||||
- 已完成且不受阻塞影响的 tasks:保留 checkbox
|
按以下规则处理 tasks.md 中已完成任务:
|
||||||
- 已完成但被阻塞证明无效的 tasks:取消 checkbox,并在修订记录中说明原因
|
|
||||||
- 未完成的 tasks:根据修订结果更新描述或删除
|
|
||||||
- 如果阶段需要拆分:在 `plan.md` 中新增阶段,将已完成部分和待完成部分分开
|
|
||||||
|
|
||||||
### 6. 修订后验证
|
- 已完成且**不受**阻塞影响的 tasks → 保留 checkbox
|
||||||
|
- 已完成但被阻塞证明**无效**的 tasks → 取消 checkbox,并在修订记录中说明原因
|
||||||
|
- 未完成的 tasks → 根据修订结果更新描述或删除
|
||||||
|
- 如果阶段需要拆分 → 在 `plan.md` 中新增阶段,将已完成部分和待完成部分分开
|
||||||
|
|
||||||
每个被修订的 artifact 完成后,执行以下一致性检查:
|
完成本步骤后进入步骤 6。
|
||||||
|
|
||||||
Instruction 合规性:
|
### 步骤 6: 修订后验证
|
||||||
|
|
||||||
- 每个被修订的 artifact 是否符合其 instruction 中的工作规则
|
每个被修订的 artifact 完成后,按两层一致性检查。
|
||||||
- 每个被修订的 artifact 是否包含其 instruction 中要求的必需章节
|
|
||||||
|
**子步骤 6.1: Instruction 合规性检查**
|
||||||
|
|
||||||
|
- 每个被修订的 artifact 是否符合其 instruction 中的工作流和完成标准
|
||||||
|
- 每个被修订的 artifact 是否包含其 instruction / template 要求的章节
|
||||||
- 每个被修订的 artifact 是否符合其 template 结构
|
- 每个被修订的 artifact 是否符合其 template 结构
|
||||||
|
|
||||||
上下游一致性:
|
**子步骤 6.2: 上下游一致性检查**
|
||||||
|
|
||||||
- 需求覆盖:`requirements.md` 的每条需求是否仍有 `design.md` 决策覆盖
|
- **需求覆盖**:`requirements.md` 的每条需求是否仍有 `design.md` 决策覆盖
|
||||||
- 决策落地:`design.md` 的每个关键决策是否在 `plan.md` 中有实现路径
|
- **决策落地**:`design.md` 的每个关键决策是否在 `plan.md` 中有实现路径
|
||||||
- 阶段覆盖:`plan.md` 的每个阶段是否在 `tasks.md` 中有对应分组
|
- **阶段覆盖**:`plan.md` 的每个阶段是否在 `tasks.md` 中有对应分组
|
||||||
- 任务可追溯:`tasks.md` 的每个 checkbox 是否能回溯到 `plan.md` 的某个阶段
|
- **任务可追溯**:`tasks.md` 的每个 checkbox 是否能回溯到 `plan.md` 的某个阶段
|
||||||
- 验证闭环:`design.md` 的“验证方向”是否在 `plan.md` 的“验证策略”中有体现
|
- **验证闭环**:`design.md` 的"验证方向"是否在 `plan.md` 的"验证策略"中有体现
|
||||||
|
|
||||||
### 7. 处理 blocker.md
|
任一项失败 → 回到步骤 5 局部修复,修复后从该项重跑。
|
||||||
|
|
||||||
在 `blocker.md` 末尾追加“修订记录”:
|
全部通过后进入步骤 7。
|
||||||
|
|
||||||
|
### 步骤 7: 处理 blocker.md
|
||||||
|
|
||||||
|
按以下子步骤追加修订记录并归档。
|
||||||
|
|
||||||
|
**子步骤 7.1: 追加修订记录**
|
||||||
|
|
||||||
|
在 `blocker.md` 末尾追加:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## 修订记录
|
## 修订记录
|
||||||
@@ -107,26 +203,32 @@ Instruction 合规性:
|
|||||||
- X.Y:取消原因
|
- X.Y:取消原因
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**子步骤 7.2: 保留或归档**
|
||||||
|
|
||||||
按项目约定保留或归档 `blocker.md`(建议保留作为审计线索)。
|
按项目约定保留或归档 `blocker.md`(建议保留作为审计线索)。
|
||||||
|
|
||||||
### 8. 完成
|
完成本步骤后进入步骤 8。
|
||||||
|
|
||||||
|
### 步骤 8: 完成
|
||||||
|
|
||||||
告诉用户重新运行 `/opsx:apply <change-name>`;apply 应跳过已完成 checkbox 并从修订后的待办任务继续。
|
告诉用户重新运行 `/opsx:apply <change-name>`;apply 应跳过已完成 checkbox 并从修订后的待办任务继续。
|
||||||
|
|
||||||
## 规则
|
## 完成标准
|
||||||
|
|
||||||
- 除非用户明确要求,不要在使用本提示词时实现代码;本提示词只负责修订规划 artifacts。
|
- 工作流步骤 1-8 全部走过
|
||||||
- 不要默认重写全部 artifacts,只修订解决阻塞所需的最小上游点及其下游影响。
|
- 步骤 6 的 instruction 合规性 + 上下游一致性全部通过
|
||||||
- 不要未经用户确认新增需求、依赖、架构方向或范围。
|
- `blocker.md` 已追加修订记录,并被保留或归档
|
||||||
- 不要取消已完成任务的勾选,除非阻塞证明该已完成工作不正确。
|
- 选定的修订方向已记录在受影响 artifact 中
|
||||||
- 每个被修订的 artifact 必须遵循其 instruction 和 template,即使只是局部修订。
|
- 每个被修订的 artifact 符合其 instruction 的工作流和 template 结构
|
||||||
- 如果工具支持,使用 todo/plan 工具跟踪修订流程,但最终事实必须写回 artifacts。
|
- 已完成任务的 checkbox 被保留,除非明确失效
|
||||||
|
- 所有触发条件命中时都执行了用户决策流程(事后可被审计)
|
||||||
|
- 用户知道需要重新运行 apply 继续实现
|
||||||
|
|
||||||
## 完成检查
|
## 规则速查
|
||||||
|
|
||||||
- `blocker.md` 已追加修订记录,并被保留或归档。
|
- 本提示词只负责**修订规划 artifacts**,除非用户明确要求,不要在本提示词中实现代码
|
||||||
- 选定的修订方向已记录在受影响 artifact 中。
|
- **最小修订**:只修订解决阻塞所需的最小上游点及其下游影响,不要默认重写全部 artifacts
|
||||||
- 每个被修订的 artifact 符合其 instruction 的工作规则和必需章节。
|
- **不得擅自扩展**:未经用户确认不得新增需求、依赖、架构方向或范围
|
||||||
- 下游 artifacts 与修订后的上游内容一致。
|
- **已完成任务保护**:不要取消已完成任务的勾选,除非阻塞证明该已完成工作不正确
|
||||||
- 已完成任务的 checkbox 被保留,除非明确失效。
|
- **遵循各 artifact 自身的 instruction**:每个被修订的 artifact 必须遵循其 instruction 和 template,即使只是局部修订
|
||||||
- 用户知道需要重新运行 apply 继续实现。
|
- **todo 跟踪修订流程**:如果工具支持,使用 todo/plan 工具跟踪修订流程,但最终事实必须写回 artifacts
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,21 +26,21 @@
|
|||||||
|
|
||||||
## 可选方案
|
## 可选方案
|
||||||
|
|
||||||
### 方案 A:<!-- 方案名称 -->
|
### 方案 1:<!-- 语义化方案名称,例如“回退 design 调整接入方式” -->
|
||||||
|
|
||||||
- 描述:
|
- 描述:
|
||||||
- 需修订:
|
- 需修订:
|
||||||
- 优势:
|
- 优势:
|
||||||
- 风险 / 代价:
|
- 风险 / 代价:
|
||||||
|
|
||||||
### 方案 B:<!-- 方案名称 -->
|
### 方案 2:<!-- 语义化方案名称 -->
|
||||||
|
|
||||||
- 描述:
|
- 描述:
|
||||||
- 需修订:
|
- 需修订:
|
||||||
- 优势:
|
- 优势:
|
||||||
- 风险 / 代价:
|
- 风险 / 代价:
|
||||||
|
|
||||||
### 方案 C:<!-- 方案名称 -->(可选)
|
### 方案 3:<!-- 语义化方案名称,可选 -->
|
||||||
|
|
||||||
- 描述:
|
- 描述:
|
||||||
- 需修订:
|
- 需修订:
|
||||||
|
|||||||
@@ -68,9 +68,3 @@
|
|||||||
## 验证方向
|
## 验证方向
|
||||||
|
|
||||||
<!-- 概要说明本次变更应从哪些角度验证,作为 plan.md “验证策略”的输入 -->
|
<!-- 概要说明本次变更应从哪些角度验证,作为 plan.md “验证策略”的输入 -->
|
||||||
|
|
||||||
## 待确认事项
|
|
||||||
|
|
||||||
| 状态 | 问题 | 所需决策 |
|
|
||||||
| ---- | ---- | -------- |
|
|
||||||
| 无 | 无待确认事项。 | 无需决策 |
|
|
||||||
|
|||||||
@@ -10,49 +10,7 @@
|
|||||||
| -------- | -------- | -------- |
|
| -------- | -------- | -------- |
|
||||||
| <!-- 文件路径 --> | <!-- 新增 / 修改 / 删除 --> | <!-- 阶段编号 --> |
|
| <!-- 文件路径 --> | <!-- 新增 / 修改 / 删除 --> | <!-- 阶段编号 --> |
|
||||||
|
|
||||||
## 阶段 1: <!-- 阶段名称 -->
|
## 阶段 N: <!-- 阶段名称;按实际阶段重复本块,N 从 1 递增 -->
|
||||||
|
|
||||||
### 目标
|
|
||||||
|
|
||||||
<!-- 本阶段要完成什么 -->
|
|
||||||
|
|
||||||
### 前置条件
|
|
||||||
|
|
||||||
<!-- 本阶段开始前必须满足什么;没有则写“无” -->
|
|
||||||
|
|
||||||
### 详细实现步骤
|
|
||||||
|
|
||||||
<!-- 写清楚关键文件、函数、数据结构、流程或配置变化。不要使用 checkbox。 -->
|
|
||||||
|
|
||||||
### 关键代码模式
|
|
||||||
|
|
||||||
<!-- 记录本阶段的关键实现细节,apply 据此编写代码。至少覆盖以下内容中适用的部分: -->
|
|
||||||
|
|
||||||
**新增 / 修改的函数或方法:**
|
|
||||||
<!-- 函数签名、参数、返回值、核心逻辑;无则写“无” -->
|
|
||||||
|
|
||||||
**新增 / 修改的数据结构:**
|
|
||||||
<!-- 类型定义、字段、约束;无则写“无” -->
|
|
||||||
|
|
||||||
**调用顺序 / 流程:**
|
|
||||||
<!-- 关键调用链、异步流程、状态转换;无则写“无” -->
|
|
||||||
|
|
||||||
**约定 / 模式:**
|
|
||||||
<!-- 命名规范、错误处理模式、日志规范等;无则写“无” -->
|
|
||||||
|
|
||||||
### 验证方式
|
|
||||||
|
|
||||||
<!-- 本阶段如何独立验证 -->
|
|
||||||
|
|
||||||
### 验收标准
|
|
||||||
|
|
||||||
<!-- 本阶段完成的可验证标准;与 requirements.md 验收标准对齐 -->
|
|
||||||
|
|
||||||
### 关联需求
|
|
||||||
|
|
||||||
<!-- 例如:F1、F2 -->
|
|
||||||
|
|
||||||
## 阶段 2: <!-- 阶段名称 -->
|
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
@@ -106,9 +64,3 @@
|
|||||||
- 错误处理:
|
- 错误处理:
|
||||||
- 兼容性:
|
- 兼容性:
|
||||||
- 迁移注意事项:
|
- 迁移注意事项:
|
||||||
|
|
||||||
## 待确认事项
|
|
||||||
|
|
||||||
| 状态 | 问题 | 所需决策 |
|
|
||||||
| ---- | ---- | -------- |
|
|
||||||
| 无 | 无待确认事项。 | 无需决策 |
|
|
||||||
|
|||||||
@@ -51,16 +51,7 @@
|
|||||||
|
|
||||||
<!-- 记录相关模块、流程、配置、文档、外部接口或用户路径 -->
|
<!-- 记录相关模块、流程、配置、文档、外部接口或用户路径 -->
|
||||||
|
|
||||||
### 潜在冲突
|
|
||||||
|
|
||||||
<!-- 记录可能与既有行为、约束、依赖、兼容性或用户预期冲突的点 -->
|
|
||||||
|
|
||||||
### 前置条件
|
### 前置条件
|
||||||
|
|
||||||
<!-- 记录执行前必须满足的条件;没有则写“无” -->
|
<!-- 记录执行前必须满足的条件;没有则写"无" -->
|
||||||
|
|
||||||
## 开放问题
|
|
||||||
|
|
||||||
| 状态 | 问题 | 所需决策 |
|
|
||||||
| ---- | ---- | -------- |
|
|
||||||
| 无 | 无待解决问题。 | 无需决策 |
|
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
## 1. <!-- 对应 plan.md 阶段 1 的名称 -->
|
## X. <!-- 对应 plan.md 阶段 X 的名称;按实际阶段重复本块,X 与 plan 阶段编号一致 -->
|
||||||
|
|
||||||
- [ ] 1.1 阅读 plan.md 阶段 1,确认涉及文件、关键代码模式和验收标准
|
- [ ] X.1 阅读 plan.md 阶段 X,确认涉及文件、关键代码模式和验收标准
|
||||||
- [ ] 1.2 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 1 -->
|
- [ ] X.2 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 X -->
|
||||||
- [ ] 1.3 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 1 -->
|
- [ ] X.3 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 X -->
|
||||||
- [ ] 1.4 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 1 -->
|
- [ ] X.4 <!-- 运行测试或验证命令,确认阶段 X 的关键行为 -->
|
||||||
- [ ] 1.5 <!-- 运行测试或验证命令,确认阶段 1 的关键行为 -->
|
- [ ] X.5 按 plan.md 阶段 X 的验收标准确认阶段完成
|
||||||
- [ ] 1.6 按 plan.md 阶段 1 的验收标准确认阶段完成
|
|
||||||
|
|
||||||
## 2. <!-- 对应 plan.md 阶段 2 的名称 -->
|
## N. 验证与收尾
|
||||||
|
|
||||||
- [ ] 2.1 阅读 plan.md 阶段 2,确认涉及文件、关键代码模式和验收标准
|
- [ ] N.1 阅读 plan.md 验证策略,确认所有验证项已执行
|
||||||
- [ ] 2.2 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 2 -->
|
- [ ] N.2 执行完整测试套件,确认无回归
|
||||||
- [ ] 2.3 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 2 -->
|
- [ ] N.3 逐项对照 requirements.md 验收标准,确认全部满足
|
||||||
- [ ] 2.4 <!-- 动词 + 文件路径:具体动作描述,详见 plan.md 阶段 2 -->
|
- [ ] N.4 检查 design.md 关键决策是否被正确实现
|
||||||
- [ ] 2.5 <!-- 运行测试或验证命令,确认阶段 2 的关键行为 -->
|
- [ ] N.5 如果行为、流程、接口、配置或使用方式发生变化,更新相关文档或交接说明
|
||||||
- [ ] 2.6 按 plan.md 阶段 2 的验收标准确认阶段完成
|
- [ ] N.6 确认所有任务已标记为 `[x]`,未完成或阻塞事项已记录
|
||||||
|
|
||||||
## 3. 验证与收尾
|
|
||||||
|
|
||||||
- [ ] 3.1 阅读 plan.md 验证策略,确认所有验证项已执行
|
|
||||||
- [ ] 3.2 执行完整测试套件,确认无回归
|
|
||||||
- [ ] 3.3 逐项对照 requirements.md 验收标准,确认全部满足
|
|
||||||
- [ ] 3.4 检查 design.md 关键决策是否被正确实现
|
|
||||||
- [ ] 3.5 如果行为、流程、接口、配置或使用方式发生变化,更新相关文档或交接说明
|
|
||||||
- [ ] 3.6 确认所有任务已标记为 `[x]`,未完成或阻塞事项已记录
|
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -25,34 +25,34 @@
|
|||||||
"version:set": "bun run scripts/bump-version.ts set"
|
"version:set": "bun run scripts/bump-version.ts set"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^21.0.1",
|
"@commitlint/cli": "^21.0.2",
|
||||||
"@commitlint/config-conventional": "^21.0.1",
|
"@commitlint/config-conventional": "^21.0.2",
|
||||||
"@happy-dom/global-registrator": "^20.10.1",
|
"@happy-dom/global-registrator": "^20.10.2",
|
||||||
"@tanstack/react-query-devtools": "^5.100.14",
|
"@tanstack/react-query-devtools": "^5.101.0",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^17.0.5",
|
"lint-staged": "^17.0.7",
|
||||||
"oxfmt": "^0.53.0",
|
"oxfmt": "^0.53.0",
|
||||||
"oxlint": "^1.68.0",
|
"oxlint": "^1.68.0",
|
||||||
"oxlint-tsgolint": "^0.23.0",
|
"oxlint-tsgolint": "^0.23.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14"
|
"vite": "^8.0.16"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.81",
|
"@ai-sdk/anthropic": "^3.0.81",
|
||||||
"@ai-sdk/openai": "^3.0.66",
|
"@ai-sdk/openai": "^3.0.68",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.48",
|
"@ai-sdk/openai-compatible": "^2.0.48",
|
||||||
"@ai-sdk/react": "^3.0.195",
|
"@ai-sdk/react": "^3.0.199",
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.5",
|
||||||
"@ant-design/x": "^2.7.0",
|
"@ant-design/x": "^2.7.0",
|
||||||
"@sinclair/typebox": "^0.34.49",
|
"@sinclair/typebox": "^0.34.49",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"ai": "^6.0.193",
|
"ai": "^6.0.197",
|
||||||
"ajv": "^8.20.0",
|
"ajv": "^8.20.0",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"pino-roll": "^4.0.0",
|
"pino-roll": "^4.0.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.7",
|
||||||
"react-router": "^7.15.1",
|
"react-router": "^7.17.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"shiki": "^4.2.0",
|
"shiki": "^4.2.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
@@ -5,55 +5,55 @@
|
|||||||
"source": "vercel/ai",
|
"source": "vercel/ai",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/use-ai-sdk/SKILL.md",
|
"skillPath": "skills/use-ai-sdk/SKILL.md",
|
||||||
"computedHash": "f9381aea9aa207157c88348c6b0ae3551137955f2bd48c855c27fa86ac03cd56"
|
"computedHash": "2249889eb47ef0f61c4dba4cf2afe01c1c8dd793bb4c24230347c1ab909bb7dd"
|
||||||
},
|
},
|
||||||
"ant-design": {
|
"ant-design": {
|
||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/ant-design/SKILL.md",
|
"skillPath": "skills/ant-design/SKILL.md",
|
||||||
"computedHash": "4d0447d48fced080b2825ecc0fb4d7ca836c8015882899c643acca0b864d5179"
|
"computedHash": "096d4ac9513e43030f960aab49b50168a3d5eb35be86926ac6e96e5998ea9466"
|
||||||
},
|
},
|
||||||
"antd": {
|
"antd": {
|
||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/antd/SKILL.md",
|
"skillPath": "skills/antd/SKILL.md",
|
||||||
"computedHash": "4295010f09f85855cab9e9de9ec7f96c14541474b4f3f9d6ef89006430931b94"
|
"computedHash": "5e26c8042060bb811118927b5daf637af7929a00fa973dd8f5f804f3ba6e2bf2"
|
||||||
},
|
},
|
||||||
"migrate-oxfmt": {
|
"migrate-oxfmt": {
|
||||||
"source": "oxc-project/oxc",
|
"source": "oxc-project/oxc",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": ".agents/skills/migrate-oxfmt/SKILL.md",
|
"skillPath": ".agents/skills/migrate-oxfmt/SKILL.md",
|
||||||
"computedHash": "b3ae0d11b61d07471cf466ba0386490a9f7ed5c1186d4956a58ac55a4b6be6ad"
|
"computedHash": "b313a891bc4173aac0bd948d5e8c40f5dd921fe67eb5f51184c364e2b869f0e7"
|
||||||
},
|
},
|
||||||
"migrate-oxlint": {
|
"migrate-oxlint": {
|
||||||
"source": "oxc-project/oxc",
|
"source": "oxc-project/oxc",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": ".agents/skills/migrate-oxlint/SKILL.md",
|
"skillPath": ".agents/skills/migrate-oxlint/SKILL.md",
|
||||||
"computedHash": "efb0bcf1c44791ba292d0536eaef87fac535c1fcc5df3280bd7a79d049b8644c"
|
"computedHash": "7bc6dccdcc557cdb7d887fc16c6632e685b13860d202b9c9ce5da7504f0dbe18"
|
||||||
},
|
},
|
||||||
"performance-lint-rules": {
|
"performance-lint-rules": {
|
||||||
"source": "oxc-project/oxc",
|
"source": "oxc-project/oxc",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": ".agents/skills/performance-lint-rules/SKILL.md",
|
"skillPath": ".agents/skills/performance-lint-rules/SKILL.md",
|
||||||
"computedHash": "0572ca4dfdaa5545a5473b1451d255796bd088418ddbc2a8dbddff68ef34a640"
|
"computedHash": "82a2d3a72bc381c3552834008435635d7157f22ee4efc4b441a33ad6e838c828"
|
||||||
},
|
},
|
||||||
"react-router-data-mode": {
|
"react-router-data-mode": {
|
||||||
"source": "remix-run/agent-skills",
|
"source": "remix-run/agent-skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/react-router-data-mode/SKILL.md",
|
"skillPath": "skills/react-router-data-mode/SKILL.md",
|
||||||
"computedHash": "cbbe1b1cfa8f6ceae1ab26d26b38c612279c9c272cf956471838796d85659860"
|
"computedHash": "76e3e0f70ff47b743bd90999e676515221e25fd7ee89cd9e5b340417b1a601e2"
|
||||||
},
|
},
|
||||||
"react-router-declarative-mode": {
|
"react-router-declarative-mode": {
|
||||||
"source": "remix-run/agent-skills",
|
"source": "remix-run/agent-skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/react-router-declarative-mode/SKILL.md",
|
"skillPath": "skills/react-router-declarative-mode/SKILL.md",
|
||||||
"computedHash": "b399ee32fa82efdbdad1121421702b7725fcffac36424529a0ea452796f3bc92"
|
"computedHash": "d7ebbf1ede90809618f02cb3b3d37b9871cdd6c88a81cf338e63de50a0df6a42"
|
||||||
},
|
},
|
||||||
"react-router-framework-mode": {
|
"react-router-framework-mode": {
|
||||||
"source": "remix-run/agent-skills",
|
"source": "remix-run/agent-skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/react-router-framework-mode/SKILL.md",
|
"skillPath": "skills/react-router-framework-mode/SKILL.md",
|
||||||
"computedHash": "a3294459f3a5065c837929d9700fe7d35730d5051f2979090e0f715e8fea693f"
|
"computedHash": "26c5bdac2f686c47eb4c4b48b6cb52401cde1dc833e6d26408ddfb22ea83c5ca"
|
||||||
},
|
},
|
||||||
"vercel-react-best-practices": {
|
"vercel-react-best-practices": {
|
||||||
"source": "vercel-labs/agent-skills",
|
"source": "vercel-labs/agent-skills",
|
||||||
@@ -66,14 +66,14 @@
|
|||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "packages/x-skill/skills/x-components/SKILL.md",
|
"skillPath": "packages/x-skill/skills/x-components/SKILL.md",
|
||||||
"computedHash": "ebc195a3a5020b6d4f4533adf2e0af33253919f0c704947e727f877aba23a4c2"
|
"computedHash": "efb7661cadf8a35fae32ce9a6b261b82ee8c8a2bb76303b333ff166163c0a729"
|
||||||
},
|
},
|
||||||
"x-markdown": {
|
"x-markdown": {
|
||||||
"source": "ant-design/x",
|
"source": "ant-design/x",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "packages/x-skill/skills/x-markdown/SKILL.md",
|
"skillPath": "packages/x-skill/skills/x-markdown/SKILL.md",
|
||||||
"computedHash": "2d26b8eda1692929e99a8b6163ef8b206f1f096a4a84507b50dbe836a7ec041e"
|
"computedHash": "441c281e8537e4aebbc6db5dce0b12c170df916f81782f33f3c8f66dd3f17b17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
196
src/server/db/entities.ts
Normal file
196
src/server/db/entities.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
import type { CreateEntityRequest, Entity, EntityType, UpdateEntityRequest } from "../../shared/api";
|
||||||
|
import type { Logger } from "../logger";
|
||||||
|
|
||||||
|
import { notDeleted, paginateQuery, softDeleteRecord, timestamp, wrap } from "./connection";
|
||||||
|
import { entities, projects } from "./schema";
|
||||||
|
|
||||||
|
export function createEntity(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
request: CreateEntityRequest,
|
||||||
|
_logger: Logger,
|
||||||
|
): { entity: Entity } | { error: string; status: number } {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const project = db
|
||||||
|
.select()
|
||||||
|
.from(projects)
|
||||||
|
.where(and(eq(projects.id, projectId), notDeleted(projects)))
|
||||||
|
.get();
|
||||||
|
if (!project) return { error: "项目不存在", status: 404 };
|
||||||
|
if (project.status === "archived") return { error: "已归档项目不可操作", status: 409 };
|
||||||
|
|
||||||
|
const name = request.name.trim();
|
||||||
|
if (!name) return { error: "实体名称不能为空", status: 400 };
|
||||||
|
|
||||||
|
const duplicate = db
|
||||||
|
.select({ id: entities.id })
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.projectId, projectId), eq(entities.name, name), notDeleted(entities)))
|
||||||
|
.get();
|
||||||
|
if (duplicate) return { error: "实体名称已存在", status: 409 };
|
||||||
|
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const now = timestamp();
|
||||||
|
|
||||||
|
db.insert(entities)
|
||||||
|
.values({
|
||||||
|
aliases: JSON.stringify(request.aliases ?? []),
|
||||||
|
createdAt: now,
|
||||||
|
description: request.description?.trim() ?? "",
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
projectId,
|
||||||
|
type: request.type,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
const row = db.select().from(entities).where(eq(entities.id, id)).get();
|
||||||
|
return { entity: toEntity(row!) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteEntity(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
entityId: string,
|
||||||
|
_logger: Logger,
|
||||||
|
): { error: string; status: number } | { success: true } {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const row = db
|
||||||
|
.select()
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||||
|
.get();
|
||||||
|
if (!row) return { error: "实体不存在", status: 404 };
|
||||||
|
if (row.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||||
|
|
||||||
|
softDeleteRecord(db, entities, entityId);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEntity(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
entityId: string,
|
||||||
|
): { entity: Entity } | { error: string; status: number } {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const row = db
|
||||||
|
.select()
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||||
|
.get();
|
||||||
|
if (!row) return { error: "实体不存在", status: 404 };
|
||||||
|
if (row.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||||
|
|
||||||
|
return { entity: toEntity(row) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listEntities(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
options: { page: number; pageSize: number; type?: EntityType },
|
||||||
|
): { items: Entity[]; page: number; pageSize: number; total: number } {
|
||||||
|
const conditions = [eq(entities.projectId, projectId)];
|
||||||
|
|
||||||
|
if (options.type) {
|
||||||
|
conditions.push(eq(entities.type, options.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return paginateQuery(raw, entities, {
|
||||||
|
conditions,
|
||||||
|
mapRow: toEntity,
|
||||||
|
orderBy: () => desc(entities.createdAt),
|
||||||
|
page: options.page,
|
||||||
|
pageSize: options.pageSize,
|
||||||
|
softDelete: entities.deletedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listEntityNames(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
): Array<{ aliases: string[]; id: string; name: string }> {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const rows = db
|
||||||
|
.select({ aliases: entities.aliases, id: entities.id, name: entities.name })
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.projectId, projectId), notDeleted(entities)))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
aliases: JSON.parse(row.aliases) as string[],
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateEntity(
|
||||||
|
raw: Database,
|
||||||
|
projectId: string,
|
||||||
|
entityId: string,
|
||||||
|
request: UpdateEntityRequest,
|
||||||
|
_logger: Logger,
|
||||||
|
): { entity: Entity } | { error: string; status: number } {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const existing = db
|
||||||
|
.select()
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||||
|
.get();
|
||||||
|
if (!existing) return { error: "实体不存在", status: 404 };
|
||||||
|
if (existing.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||||
|
|
||||||
|
const updates: Partial<typeof entities.$inferInsert> = {
|
||||||
|
updatedAt: timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = request.name?.trim();
|
||||||
|
if (name === "") return { error: "实体名称不能为空", status: 400 };
|
||||||
|
if (name !== undefined && name !== existing.name) {
|
||||||
|
const duplicate = db
|
||||||
|
.select({ id: entities.id })
|
||||||
|
.from(entities)
|
||||||
|
.where(and(eq(entities.projectId, projectId), eq(entities.name, name), notDeleted(entities)))
|
||||||
|
.get();
|
||||||
|
if (duplicate) return { error: "实体名称已存在", status: 409 };
|
||||||
|
updates.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type !== undefined) {
|
||||||
|
updates.type = request.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.description !== undefined) {
|
||||||
|
updates.description = request.description.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.aliases !== undefined) {
|
||||||
|
updates.aliases = JSON.stringify(request.aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates).length === 1 && updates.updatedAt) {
|
||||||
|
return { entity: toEntity(existing) };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(entities).set(updates).where(eq(entities.id, entityId)).run();
|
||||||
|
|
||||||
|
const updated = db.select().from(entities).where(eq(entities.id, entityId)).get();
|
||||||
|
return { entity: toEntity(updated!) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntity(row: typeof entities.$inferSelect): Entity {
|
||||||
|
return {
|
||||||
|
aliases: JSON.parse(row.aliases) as string[],
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
description: row.description,
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
projectId: row.projectId,
|
||||||
|
type: row.type as EntityType,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,11 +2,19 @@ import type Database from "bun:sqlite";
|
|||||||
|
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
import type { CreateMaterialRequest, Material, MaterialStatus, MaterialType } from "../../shared/api";
|
import type {
|
||||||
|
CreateMaterialRequest,
|
||||||
|
EntityConfirmation,
|
||||||
|
Material,
|
||||||
|
MaterialStatus,
|
||||||
|
MaterialType,
|
||||||
|
ProcessingResult,
|
||||||
|
} from "../../shared/api";
|
||||||
import type { Logger } from "../logger";
|
import type { Logger } from "../logger";
|
||||||
|
|
||||||
import { notDeleted, paginateQuery, softDeleteRecord, timestamp, wrap } from "./connection";
|
import { notDeleted, paginateQuery, softDeleteRecord, timestamp, wrap } from "./connection";
|
||||||
import { materials, projects } from "./schema";
|
import { createEntity, updateEntity } from "./entities";
|
||||||
|
import { entities as schema_entities, materials, projects } from "./schema";
|
||||||
|
|
||||||
const ALLOWED_MATERIAL_TYPES: MaterialType[] = ["general", "meeting"];
|
const ALLOWED_MATERIAL_TYPES: MaterialType[] = ["general", "meeting"];
|
||||||
|
|
||||||
@@ -14,6 +22,7 @@ export function approveMaterial(
|
|||||||
raw: Database,
|
raw: Database,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
materialId: string,
|
materialId: string,
|
||||||
|
entityConfirmations: EntityConfirmation[],
|
||||||
_logger: Logger,
|
_logger: Logger,
|
||||||
): { error: string; status: number } | { material: Material } {
|
): { error: string; status: number } | { material: Material } {
|
||||||
const db = wrap(raw);
|
const db = wrap(raw);
|
||||||
@@ -26,6 +35,36 @@ export function approveMaterial(
|
|||||||
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
|
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
|
||||||
if (row.status !== "review") return { error: "仅待审核素材可通过", status: 409 };
|
if (row.status !== "review") return { error: "仅待审核素材可通过", status: 409 };
|
||||||
|
|
||||||
|
if (row.processedContent && entityConfirmations.length > 0) {
|
||||||
|
try {
|
||||||
|
const processingResult = JSON.parse(row.processedContent) as ProcessingResult;
|
||||||
|
for (const confirmation of entityConfirmations) {
|
||||||
|
const candidate = processingResult.candidateEntities[confirmation.candidateIndex];
|
||||||
|
if (!candidate) continue;
|
||||||
|
|
||||||
|
if (confirmation.action === "create") {
|
||||||
|
createEntity(
|
||||||
|
raw,
|
||||||
|
projectId,
|
||||||
|
{ description: candidate.context, name: candidate.name, type: candidate.type },
|
||||||
|
_logger,
|
||||||
|
);
|
||||||
|
} else if (confirmation.action === "merge" && confirmation.targetEntityId) {
|
||||||
|
const entityResult = getEntityForMerge(raw, confirmation.targetEntityId);
|
||||||
|
if (entityResult) {
|
||||||
|
const newAliases = [...entityResult.aliases];
|
||||||
|
if (!newAliases.includes(candidate.name)) {
|
||||||
|
newAliases.push(candidate.name);
|
||||||
|
}
|
||||||
|
updateEntity(raw, projectId, confirmation.targetEntityId, { aliases: newAliases }, _logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// processedContent 解析失败时不阻塞审核通过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = timestamp();
|
const now = timestamp();
|
||||||
db.update(materials).set({ status: "approved", updatedAt: now }).where(eq(materials.id, materialId)).run();
|
db.update(materials).set({ status: "approved", updatedAt: now }).where(eq(materials.id, materialId)).run();
|
||||||
|
|
||||||
@@ -33,6 +72,17 @@ export function approveMaterial(
|
|||||||
return { material: toMaterial(updated!) };
|
return { material: toMaterial(updated!) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEntityForMerge(raw: Database, entityId: string): { aliases: string[] } | null {
|
||||||
|
const db = wrap(raw);
|
||||||
|
const row = db
|
||||||
|
.select({ aliases: schema_entities.aliases })
|
||||||
|
.from(schema_entities)
|
||||||
|
.where(and(eq(schema_entities.id, entityId), notDeleted(schema_entities)))
|
||||||
|
.get();
|
||||||
|
if (!row) return null;
|
||||||
|
return { aliases: JSON.parse(row.aliases) as string[] };
|
||||||
|
}
|
||||||
|
|
||||||
export function createMaterial(
|
export function createMaterial(
|
||||||
raw: Database,
|
raw: Database,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ export const messages = sqliteTable(
|
|||||||
(table) => [index("messages_conversation_id_idx").on(table.conversationId)],
|
(table) => [index("messages_conversation_id_idx").on(table.conversationId)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const entities = sqliteTable(
|
||||||
|
"entities",
|
||||||
|
{
|
||||||
|
...baseColumns,
|
||||||
|
aliases: text("aliases").notNull().default("[]"),
|
||||||
|
description: text("description").notNull().default(""),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id),
|
||||||
|
type: text("type").notNull().default("other"),
|
||||||
|
},
|
||||||
|
(table) => [index("entities_project_id_idx").on(table.projectId), index("entities_name_idx").on(table.name)],
|
||||||
|
);
|
||||||
|
|
||||||
export const schemaMigrations = sqliteTable("schema_migrations", {
|
export const schemaMigrations = sqliteTable("schema_migrations", {
|
||||||
appliedAt: text("applied_at").notNull(),
|
appliedAt: text("applied_at").notNull(),
|
||||||
checksum: text("checksum").notNull(),
|
checksum: text("checksum").notNull(),
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type Database from "bun:sqlite";
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import { generateText } from "ai";
|
||||||
|
|
||||||
import type { MaterialType } from "../../shared/api";
|
import type { MaterialType } from "../../shared/api";
|
||||||
import type { Logger } from "../logger";
|
import type { Logger } from "../logger";
|
||||||
|
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
import { buildProviderRegistry } from "../ai/registry";
|
||||||
import { notDeleted, timestamp, wrap } from "../db/connection";
|
import { notDeleted, timestamp, wrap } from "../db/connection";
|
||||||
|
import { listEntityNames } from "../db/entities";
|
||||||
|
import { getModelWithProvider, listModels } from "../db/models";
|
||||||
import { materials } from "../db/schema";
|
import { materials } from "../db/schema";
|
||||||
|
import { getSettings } from "../db/settings";
|
||||||
|
|
||||||
import { getTemplate } from "./templates";
|
import { getTemplate } from "./templates";
|
||||||
|
|
||||||
@@ -17,6 +23,7 @@ export interface ProcessableMaterial {
|
|||||||
description: string;
|
description: string;
|
||||||
id: string;
|
id: string;
|
||||||
materialType: MaterialType;
|
materialType: MaterialType;
|
||||||
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MaterialProcessor {
|
export class MaterialProcessor {
|
||||||
@@ -101,6 +108,7 @@ export class MaterialProcessor {
|
|||||||
description: row.description,
|
description: row.description,
|
||||||
id: row.id,
|
id: row.id,
|
||||||
materialType: row.materialType as MaterialType,
|
materialType: row.materialType as MaterialType,
|
||||||
|
projectId: row.projectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastError: unknown;
|
let lastError: unknown;
|
||||||
@@ -143,9 +151,48 @@ export class MaterialProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processOne(material: ProcessableMaterial): Promise<string> {
|
protected async processOne(material: ProcessableMaterial): Promise<string> {
|
||||||
|
const modelInfo = getDefaultTextModel(this.db);
|
||||||
|
if (!modelInfo) {
|
||||||
|
throw new Error("没有可用的文本模型,请在设置中配置默认模型或添加至少一个模型");
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = buildProviderRegistry(this.db);
|
||||||
|
const model = registry.languageModel(`${modelInfo.providerId}:${modelInfo.externalId}`);
|
||||||
|
const existingEntities = listEntityNames(this.db, material.projectId);
|
||||||
const template = getTemplate(material.materialType);
|
const template = getTemplate(material.materialType);
|
||||||
// TODO: 替换为真实 AI Agent 调用
|
const userPrompt = template.buildUserPrompt(material.description, existingEntities);
|
||||||
return Promise.resolve(template.outputTemplate.replace("{description}", material.description));
|
|
||||||
|
const result = await generateText({
|
||||||
|
model,
|
||||||
|
prompt: userPrompt,
|
||||||
|
system: template.systemPrompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const processingResult = template.parseOutput(result.text);
|
||||||
|
return JSON.stringify(processingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultTextModel(db: Database): { externalId: string; providerId: string } | null {
|
||||||
|
try {
|
||||||
|
const settings = getSettings(db);
|
||||||
|
if (settings.defaultModels?.text) {
|
||||||
|
const result = getModelWithProvider(db, settings.defaultModels.text);
|
||||||
|
if (!("error" in result)) {
|
||||||
|
return { externalId: result.model.externalId, providerId: result.provider.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// settings 不存在或解析失败,使用 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = listModels(db, { page: 1, pageSize: 1 });
|
||||||
|
const firstModel = fallback.items[0];
|
||||||
|
if (!firstModel) return null;
|
||||||
|
|
||||||
|
const result = getModelWithProvider(db, firstModel.id);
|
||||||
|
if ("error" in result) return null;
|
||||||
|
|
||||||
|
return { externalId: result.model.externalId, providerId: result.provider.id };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,79 @@
|
|||||||
export const GENERAL_TEMPLATE = {
|
import type { ProcessingResult } from "../../../shared/api";
|
||||||
outputTemplate: "{description}",
|
|
||||||
systemPrompt: "通用素材处理",
|
import type { ProcessingTemplate } from "./index";
|
||||||
|
|
||||||
|
const ENTITY_TYPES_DESC = `
|
||||||
|
实体类型说明:
|
||||||
|
- person: 人
|
||||||
|
- organization: 组织(公司/部门/客户/供应商等)
|
||||||
|
- system: 系统/软件
|
||||||
|
- feature: 功能/模块
|
||||||
|
- requirement: 需求
|
||||||
|
- issue: 问题/风险
|
||||||
|
- term: 术语/概念
|
||||||
|
- other: 其他`;
|
||||||
|
|
||||||
|
export const GENERAL_TEMPLATE: ProcessingTemplate = {
|
||||||
|
buildUserPrompt: (description: string, existingEntities: Array<{ aliases: string[]; id: string; name: string }>) => {
|
||||||
|
let entityList = "";
|
||||||
|
if (existingEntities.length > 0) {
|
||||||
|
entityList =
|
||||||
|
"当前项目下已有以下实体,请对照匹配(注意别名也是同一实体):\n" +
|
||||||
|
existingEntities
|
||||||
|
.map((e) => `- [${e.id}] ${e.name}${e.aliases.length > 0 ? `(别名:${e.aliases.join("、")})` : ""}`)
|
||||||
|
.join("\n") +
|
||||||
|
"\n如果识别到的实体与已有实体匹配,请将 matchedEntityId 设置为对应的 ID。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `请处理以下文本素材:\n\n${description}${entityList ? `\n\n${entityList}` : ""}`;
|
||||||
|
},
|
||||||
|
parseOutput: (text: string): ProcessingResult => {
|
||||||
|
const cleaned = text
|
||||||
|
.replace(/```json\s*/g, "")
|
||||||
|
.replace(/```\s*/g, "")
|
||||||
|
.trim();
|
||||||
|
const parsed = JSON.parse(cleaned) as {
|
||||||
|
candidateEntities?: Array<{ context?: string; matchedEntityId?: null | string; name?: string; type?: string }>;
|
||||||
|
normalizedContent?: string;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
candidateEntities: (parsed.candidateEntities ?? []).map((e) => ({
|
||||||
|
context: e.context ?? "",
|
||||||
|
matchedEntityId: e.matchedEntityId ?? null,
|
||||||
|
name: e.name ?? "",
|
||||||
|
type: (e.type ?? "other") as ProcessingResult["candidateEntities"][number]["type"],
|
||||||
|
})),
|
||||||
|
normalizedContent: parsed.normalizedContent ?? "",
|
||||||
|
summary: parsed.summary ?? "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
systemPrompt: `你是 Alfred 预处理助手,负责整理用户输入的文本素材。
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
分析输入的文本,生成以下结构化输出(纯 JSON 格式,不要包含其他文字):
|
||||||
|
|
||||||
|
{
|
||||||
|
"summary": "内容概要,1-2 句概括文本核心信息",
|
||||||
|
"normalizedContent": "规范化后的完整内容。保持原意,但修正口语化表达、去除冗余、统一格式。",
|
||||||
|
"candidateEntities": [
|
||||||
|
{
|
||||||
|
"name": "识别到的实体名称",
|
||||||
|
"type": "实体类型",
|
||||||
|
"context": "原文中相关的引用片段",
|
||||||
|
"matchedEntityId": "匹配到的已有实体 ID,无匹配则为 null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
${ENTITY_TYPES_DESC}
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
- 只输出 JSON 对象,不要有任何其他文字、注释或 markdown 标记
|
||||||
|
- 识别文本中提到的人名、组织、系统、术语等重要实体
|
||||||
|
- 仔细对照已有实体列表进行匹配(包括别名),如果名称或别名相似则设置 matchedEntityId
|
||||||
|
- 如果实体的别名中包含了某个说法,也应该匹配到该实体
|
||||||
|
- 不要编造文本中未提到的实体
|
||||||
|
- normalizedContent 应保持客观,不要添加原文中没有的信息`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { MaterialType } from "../../../shared/api";
|
import type { MaterialType, ProcessingResult } from "../../../shared/api";
|
||||||
|
|
||||||
import { GENERAL_TEMPLATE } from "./general";
|
import { GENERAL_TEMPLATE } from "./general";
|
||||||
import { MEETING_TEMPLATE } from "./meeting";
|
import { MEETING_TEMPLATE } from "./meeting";
|
||||||
|
|
||||||
export interface ProcessingTemplate {
|
export interface ProcessingTemplate {
|
||||||
outputTemplate: string;
|
buildUserPrompt: (
|
||||||
|
description: string,
|
||||||
|
existingEntities: Array<{ aliases: string[]; id: string; name: string }>,
|
||||||
|
) => string;
|
||||||
|
parseOutput: (text: string) => ProcessingResult;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,79 @@
|
|||||||
export const MEETING_TEMPLATE = {
|
import type { ProcessingResult } from "../../../shared/api";
|
||||||
outputTemplate: "{description}",
|
|
||||||
systemPrompt: "会议素材处理",
|
import type { ProcessingTemplate } from "./index";
|
||||||
|
|
||||||
|
const ENTITY_TYPES_DESC = `
|
||||||
|
实体类型说明:
|
||||||
|
- person: 人
|
||||||
|
- organization: 组织(公司/部门/客户/供应商等)
|
||||||
|
- system: 系统/软件
|
||||||
|
- feature: 功能/模块
|
||||||
|
- requirement: 需求
|
||||||
|
- issue: 问题/风险
|
||||||
|
- term: 术语/概念
|
||||||
|
- other: 其他`;
|
||||||
|
|
||||||
|
export const MEETING_TEMPLATE: ProcessingTemplate = {
|
||||||
|
buildUserPrompt: (description: string, existingEntities: Array<{ aliases: string[]; id: string; name: string }>) => {
|
||||||
|
let entityList = "";
|
||||||
|
if (existingEntities.length > 0) {
|
||||||
|
entityList =
|
||||||
|
"当前项目下已有以下实体,请对照匹配(注意别名也是同一实体):\n" +
|
||||||
|
existingEntities
|
||||||
|
.map((e) => `- [${e.id}] ${e.name}${e.aliases.length > 0 ? `(别名:${e.aliases.join("、")})` : ""}`)
|
||||||
|
.join("\n") +
|
||||||
|
"\n如果识别到的实体与已有实体匹配,请将 matchedEntityId 设置为对应的 ID。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `请处理以下会议相关文本素材:\n\n${description}${entityList ? `\n\n${entityList}` : ""}`;
|
||||||
|
},
|
||||||
|
parseOutput: (text: string): ProcessingResult => {
|
||||||
|
const cleaned = text
|
||||||
|
.replace(/```json\s*/g, "")
|
||||||
|
.replace(/```\s*/g, "")
|
||||||
|
.trim();
|
||||||
|
const parsed = JSON.parse(cleaned) as {
|
||||||
|
candidateEntities?: Array<{ context?: string; matchedEntityId?: null | string; name?: string; type?: string }>;
|
||||||
|
normalizedContent?: string;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
candidateEntities: (parsed.candidateEntities ?? []).map((e) => ({
|
||||||
|
context: e.context ?? "",
|
||||||
|
matchedEntityId: e.matchedEntityId ?? null,
|
||||||
|
name: e.name ?? "",
|
||||||
|
type: (e.type ?? "other") as ProcessingResult["candidateEntities"][number]["type"],
|
||||||
|
})),
|
||||||
|
normalizedContent: parsed.normalizedContent ?? "",
|
||||||
|
summary: parsed.summary ?? "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
systemPrompt: `你是 Alfred 预处理助手,负责整理用户输入的会议相关文本素材。
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
分析输入的文本,生成以下结构化输出(纯 JSON 格式,不要包含其他文字):
|
||||||
|
|
||||||
|
{
|
||||||
|
"summary": "会议内容概要,1-2 句概括核心内容",
|
||||||
|
"normalizedContent": "规范化后的会议完整内容。保持原意,但修正口语化表达、去除冗余、结构化呈现。如包含参会者、讨论要点、决议等内容,保持这些结构。",
|
||||||
|
"candidateEntities": [
|
||||||
|
{
|
||||||
|
"name": "识别到的实体名称(包括会议参与者、讨论中提到的组织/系统/术语等)",
|
||||||
|
"type": "实体类型",
|
||||||
|
"context": "原文中相关的引用片段",
|
||||||
|
"matchedEntityId": "匹配到的已有实体 ID,无匹配则为 null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
${ENTITY_TYPES_DESC}
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
- 只输出 JSON 对象,不要有任何其他文字、注释或 markdown 标记
|
||||||
|
- 重点识别:参会人员、讨论中提到的组织/系统/术语/需求/问题
|
||||||
|
- 仔细对照已有实体列表进行匹配(包括别名)
|
||||||
|
- 如果实体的别名中包含了某个说法,也应该匹配到该实体
|
||||||
|
- 不要编造文本中未提到的信息
|
||||||
|
- normalizedContent 应保持客观,不要添加原文中没有的信息`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
41
src/server/routes/entities/create.ts
Normal file
41
src/server/routes/entities/create.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import type { CreateEntityRequest, RuntimeMode } from "../../../shared/api";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
|
import { createEntity } from "../../db/entities";
|
||||||
|
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
|
||||||
|
import { validateIdParam } from "../../middleware";
|
||||||
|
|
||||||
|
export async function handleCreateEntity(
|
||||||
|
req: Request,
|
||||||
|
db: Database,
|
||||||
|
mode: RuntimeMode,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<Response> {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const projectIdStr = parseIdFromUrl(url);
|
||||||
|
|
||||||
|
const validated = validateIdParam(projectIdStr ?? "", mode);
|
||||||
|
if (validated instanceof Response) return validated;
|
||||||
|
|
||||||
|
let body: CreateEntityRequest;
|
||||||
|
try {
|
||||||
|
body = (await req.json()) as CreateEntityRequest;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||||
|
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.name || typeof body.name !== "string") {
|
||||||
|
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = createEntity(db, validated.id, body, logger);
|
||||||
|
if ("error" in result) {
|
||||||
|
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ entityId: result.entity.id, projectId: validated.id }, "实体创建成功");
|
||||||
|
return jsonResponse(result, { mode, status: 201 });
|
||||||
|
}
|
||||||
29
src/server/routes/entities/delete.ts
Normal file
29
src/server/routes/entities/delete.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import type { RuntimeMode } from "../../../shared/api";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
|
import { deleteEntity } from "../../db/entities";
|
||||||
|
import { createApiError, jsonResponse } from "../../helpers";
|
||||||
|
import { validateIdParam } from "../../middleware";
|
||||||
|
|
||||||
|
export function handleDeleteEntity(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const parts = url.pathname.split("/");
|
||||||
|
const projectIdStr = parts[3];
|
||||||
|
const entityIdStr = parts[5];
|
||||||
|
|
||||||
|
const validatedProject = validateIdParam(projectIdStr ?? "", mode);
|
||||||
|
if (validatedProject instanceof Response) return validatedProject;
|
||||||
|
|
||||||
|
const validatedEntity = validateIdParam(entityIdStr ?? "", mode);
|
||||||
|
if (validatedEntity instanceof Response) return validatedEntity;
|
||||||
|
|
||||||
|
const result = deleteEntity(db, validatedProject.id, validatedEntity.id, logger);
|
||||||
|
if ("error" in result) {
|
||||||
|
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ entityId: validatedEntity.id, projectId: validatedProject.id }, "实体删除成功");
|
||||||
|
return jsonResponse(result, { mode });
|
||||||
|
}
|
||||||
29
src/server/routes/entities/get.ts
Normal file
29
src/server/routes/entities/get.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import type { RuntimeMode } from "../../../shared/api";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
|
import { getEntity } from "../../db/entities";
|
||||||
|
import { createApiError, jsonResponse } from "../../helpers";
|
||||||
|
import { validateIdParam } from "../../middleware";
|
||||||
|
|
||||||
|
export function handleGetEntity(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const parts = url.pathname.split("/");
|
||||||
|
const projectIdStr = parts[3];
|
||||||
|
const entityIdStr = parts[5];
|
||||||
|
|
||||||
|
const validatedProject = validateIdParam(projectIdStr ?? "", mode);
|
||||||
|
if (validatedProject instanceof Response) return validatedProject;
|
||||||
|
|
||||||
|
const validatedEntity = validateIdParam(entityIdStr ?? "", mode);
|
||||||
|
if (validatedEntity instanceof Response) return validatedEntity;
|
||||||
|
|
||||||
|
const result = getEntity(db, validatedProject.id, validatedEntity.id);
|
||||||
|
if ("error" in result) {
|
||||||
|
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ entityId: validatedEntity.id, projectId: validatedProject.id }, "获取实体详情");
|
||||||
|
return jsonResponse(result, { mode });
|
||||||
|
}
|
||||||
45
src/server/routes/entities/list.ts
Normal file
45
src/server/routes/entities/list.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import type { EntityType, RuntimeMode } from "../../../shared/api";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
|
import { listEntities } from "../../db/entities";
|
||||||
|
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
|
||||||
|
import { validateIdParam, validatePagination } from "../../middleware";
|
||||||
|
|
||||||
|
export function handleListEntities(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const projectIdStr = parseIdFromUrl(url);
|
||||||
|
|
||||||
|
const validated = validateIdParam(projectIdStr ?? "", mode);
|
||||||
|
if (validated instanceof Response) return validated;
|
||||||
|
|
||||||
|
const pageParam = url.searchParams.get("page");
|
||||||
|
const pageSizeParam = url.searchParams.get("pageSize");
|
||||||
|
const typeParam = url.searchParams.get("type");
|
||||||
|
|
||||||
|
const pagination = validatePagination(pageParam, pageSizeParam, mode);
|
||||||
|
if (pagination instanceof Response) return pagination;
|
||||||
|
|
||||||
|
const ALLOWED_TYPES = [
|
||||||
|
"person",
|
||||||
|
"organization",
|
||||||
|
"system",
|
||||||
|
"feature",
|
||||||
|
"requirement",
|
||||||
|
"issue",
|
||||||
|
"term",
|
||||||
|
"other",
|
||||||
|
] as const;
|
||||||
|
if (typeParam && !(ALLOWED_TYPES as readonly string[]).includes(typeParam)) {
|
||||||
|
return jsonResponse(createApiError("Invalid type parameter", 400), { mode, status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = listEntities(db, validated.id, {
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
type: (typeParam as EntityType) ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsonResponse(result, { mode });
|
||||||
|
}
|
||||||
42
src/server/routes/entities/update.ts
Normal file
42
src/server/routes/entities/update.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import type { RuntimeMode, UpdateEntityRequest } from "../../../shared/api";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
|
import { updateEntity } from "../../db/entities";
|
||||||
|
import { createApiError, jsonResponse } from "../../helpers";
|
||||||
|
import { validateIdParam } from "../../middleware";
|
||||||
|
|
||||||
|
export async function handleUpdateEntity(
|
||||||
|
req: Request,
|
||||||
|
db: Database,
|
||||||
|
mode: RuntimeMode,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<Response> {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const parts = url.pathname.split("/");
|
||||||
|
const projectIdStr = parts[3];
|
||||||
|
const entityIdStr = parts[5];
|
||||||
|
|
||||||
|
const validatedProject = validateIdParam(projectIdStr ?? "", mode);
|
||||||
|
if (validatedProject instanceof Response) return validatedProject;
|
||||||
|
|
||||||
|
const validatedEntity = validateIdParam(entityIdStr ?? "", mode);
|
||||||
|
if (validatedEntity instanceof Response) return validatedEntity;
|
||||||
|
|
||||||
|
let body: UpdateEntityRequest;
|
||||||
|
try {
|
||||||
|
body = (await req.json()) as UpdateEntityRequest;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||||
|
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = updateEntity(db, validatedProject.id, validatedEntity.id, body, logger);
|
||||||
|
if ("error" in result) {
|
||||||
|
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ entityId: validatedEntity.id, projectId: validatedProject.id }, "实体更新成功");
|
||||||
|
return jsonResponse(result, { mode });
|
||||||
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import type Database from "bun:sqlite";
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
import type { RuntimeMode } from "../../../shared/api";
|
import type { EntityConfirmation, RuntimeMode } from "../../../shared/api";
|
||||||
import type { Logger } from "../../logger";
|
import type { Logger } from "../../logger";
|
||||||
|
|
||||||
import { approveMaterial } from "../../db/materials";
|
import { approveMaterial } from "../../db/materials";
|
||||||
import { createApiError, jsonResponse } from "../../helpers";
|
import { createApiError, jsonResponse } from "../../helpers";
|
||||||
import { validateIdParam } from "../../middleware";
|
import { validateIdParam } from "../../middleware";
|
||||||
|
|
||||||
export function handleApproveMaterial(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
export async function handleApproveMaterial(
|
||||||
|
req: Request,
|
||||||
|
db: Database,
|
||||||
|
mode: RuntimeMode,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<Response> {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const parts = url.pathname.split("/");
|
const parts = url.pathname.split("/");
|
||||||
const projectIdStr = parts[3];
|
const projectIdStr = parts[3];
|
||||||
@@ -19,7 +24,15 @@ export function handleApproveMaterial(req: Request, db: Database, mode: RuntimeM
|
|||||||
const validatedMaterial = validateIdParam(materialIdStr ?? "", mode);
|
const validatedMaterial = validateIdParam(materialIdStr ?? "", mode);
|
||||||
if (validatedMaterial instanceof Response) return validatedMaterial;
|
if (validatedMaterial instanceof Response) return validatedMaterial;
|
||||||
|
|
||||||
const result = approveMaterial(db, validatedProject.id, validatedMaterial.id, logger);
|
let entityConfirmations: EntityConfirmation[] = [];
|
||||||
|
try {
|
||||||
|
const body = (await req.json()) as { entityConfirmations?: EntityConfirmation[] };
|
||||||
|
entityConfirmations = body.entityConfirmations ?? [];
|
||||||
|
} catch {
|
||||||
|
// body 为空时使用默认空数组
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = approveMaterial(db, validatedProject.id, validatedMaterial.id, entityConfirmations, logger);
|
||||||
if ("error" in result) {
|
if ("error" in result) {
|
||||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,50 @@ export function startServer(options: StartServerOptions) {
|
|||||||
logger,
|
logger,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"/api/projects/:id/entities": {
|
||||||
|
GET: withErrorHandler(
|
||||||
|
async (req) => {
|
||||||
|
const { handleListEntities } = await import("./routes/entities/list");
|
||||||
|
return handleListEntities(req, db, mode, logger);
|
||||||
|
},
|
||||||
|
mode,
|
||||||
|
logger,
|
||||||
|
),
|
||||||
|
POST: withErrorHandler(
|
||||||
|
async (req) => {
|
||||||
|
const { handleCreateEntity } = await import("./routes/entities/create");
|
||||||
|
return handleCreateEntity(req, db, mode, logger);
|
||||||
|
},
|
||||||
|
mode,
|
||||||
|
logger,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"/api/projects/:id/entities/:eid": {
|
||||||
|
DELETE: withErrorHandler(
|
||||||
|
async (req) => {
|
||||||
|
const { handleDeleteEntity } = await import("./routes/entities/delete");
|
||||||
|
return handleDeleteEntity(req, db, mode, logger);
|
||||||
|
},
|
||||||
|
mode,
|
||||||
|
logger,
|
||||||
|
),
|
||||||
|
GET: withErrorHandler(
|
||||||
|
async (req) => {
|
||||||
|
const { handleGetEntity } = await import("./routes/entities/get");
|
||||||
|
return handleGetEntity(req, db, mode, logger);
|
||||||
|
},
|
||||||
|
mode,
|
||||||
|
logger,
|
||||||
|
),
|
||||||
|
PATCH: withErrorHandler(
|
||||||
|
async (req) => {
|
||||||
|
const { handleUpdateEntity } = await import("./routes/entities/update");
|
||||||
|
return handleUpdateEntity(req, db, mode, logger);
|
||||||
|
},
|
||||||
|
mode,
|
||||||
|
logger,
|
||||||
|
),
|
||||||
|
},
|
||||||
"/api/projects/:id/restore": {
|
"/api/projects/:id/restore": {
|
||||||
POST: withErrorHandler(
|
POST: withErrorHandler(
|
||||||
async (req) => {
|
async (req) => {
|
||||||
|
|||||||
@@ -55,10 +55,77 @@ export interface CreateProviderRequest {
|
|||||||
type: ProviderType;
|
type: ProviderType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
export interface ApproveMaterialRequest {
|
||||||
// 在此定义你的业务类型
|
entityConfirmations?: EntityConfirmation[];
|
||||||
// 前后端共享的类型都放在这个文件中
|
}
|
||||||
// ==========================================
|
|
||||||
|
export interface CandidateEntity {
|
||||||
|
context: string;
|
||||||
|
matchedEntityId: null | string;
|
||||||
|
name: string;
|
||||||
|
type: EntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEntityRequest {
|
||||||
|
aliases?: string[];
|
||||||
|
description?: string;
|
||||||
|
name: string;
|
||||||
|
type: EntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Entity {
|
||||||
|
aliases: string[];
|
||||||
|
createdAt: string;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
projectId: string;
|
||||||
|
type: EntityType;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityConfirmation {
|
||||||
|
action: "create" | "discard" | "merge" | "select";
|
||||||
|
candidateIndex: number;
|
||||||
|
targetEntityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityListResponse {
|
||||||
|
items: Entity[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityResponse {
|
||||||
|
entity: Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENTITY_TYPES = [
|
||||||
|
"person",
|
||||||
|
"organization",
|
||||||
|
"system",
|
||||||
|
"feature",
|
||||||
|
"requirement",
|
||||||
|
"issue",
|
||||||
|
"term",
|
||||||
|
"other",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type EntityType = (typeof ENTITY_TYPES)[number];
|
||||||
|
|
||||||
|
export interface ProcessingResult {
|
||||||
|
candidateEntities: CandidateEntity[];
|
||||||
|
normalizedContent: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEntityRequest {
|
||||||
|
aliases?: string[];
|
||||||
|
description?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: EntityType;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ListSortParams {
|
export interface ListSortParams {
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
|
|||||||
24
src/web/features/entities/components/EntityCard.tsx
Normal file
24
src/web/features/entities/components/EntityCard.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Tag, Typography } from "antd";
|
||||||
|
|
||||||
|
import type { Entity } from "../types";
|
||||||
|
|
||||||
|
import { ENTITY_TYPE_COLORS, ENTITY_TYPE_LABELS } from "./constants";
|
||||||
|
|
||||||
|
interface EntityCardProps {
|
||||||
|
entity: Entity;
|
||||||
|
onSelect: () => void;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityCard({ entity, onSelect, selected }: EntityCardProps) {
|
||||||
|
const className = selected ? "app-sidebar-list-item app-sidebar-list-item--selected" : "app-sidebar-list-item";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} onClick={onSelect} style={{ padding: "8px 12px" }}>
|
||||||
|
<Typography.Text ellipsis strong style={{ display: "block", marginBottom: 4 }}>
|
||||||
|
{entity.name}
|
||||||
|
</Typography.Text>
|
||||||
|
<Tag color={ENTITY_TYPE_COLORS[entity.type] ?? "default"}>{ENTITY_TYPE_LABELS[entity.type] ?? entity.type}</Tag>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
src/web/features/entities/components/EntityDetailPanel.tsx
Normal file
114
src/web/features/entities/components/EntityDetailPanel.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||||
|
import { Card, Descriptions, Empty, Popconfirm, Result, Space, Spin, Tag } from "antd";
|
||||||
|
import "overlayscrollbars/styles/overlayscrollbars.css";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
|
import type { Entity } from "../types";
|
||||||
|
|
||||||
|
import { useDeleteEntity, useEntity } from "../../../shared/hooks/use-entities";
|
||||||
|
import { formatRelativeTime } from "../../../shared/utils/time";
|
||||||
|
import { ENTITY_TYPE_COLORS, ENTITY_TYPE_LABELS } from "./constants";
|
||||||
|
|
||||||
|
interface EntityDetailPanelProps {
|
||||||
|
entityId: null | string;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onEdit: (entity: Entity) => void;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityDetailPanel({ entityId, onDelete, onEdit, projectId }: EntityDetailPanelProps) {
|
||||||
|
const { data, error, isLoading } = useEntity({ entityId, projectId });
|
||||||
|
const deleteMutation = useDeleteEntity(projectId);
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
return (
|
||||||
|
<div className="app-inbox-panel">
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="app-inbox-content"
|
||||||
|
options={{
|
||||||
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
|
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Empty description="请在左侧选择实体" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="app-inbox-panel">
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="app-inbox-content"
|
||||||
|
options={{
|
||||||
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
|
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
return (
|
||||||
|
<div className="app-inbox-panel">
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="app-inbox-content"
|
||||||
|
options={{
|
||||||
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
|
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Result subTitle="加载实体失败" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
void deleteMutation.mutate({ entityId: data.id, projectId }, { onSuccess: () => onDelete(data.id) });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-inbox-panel">
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="app-inbox-content"
|
||||||
|
options={{ overflow: { x: "hidden", y: "scroll" }, scrollbars: { autoHide: "move", theme: "os-theme-custom" } }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<EditOutlined onClick={() => onEdit(data)} style={{ cursor: "pointer" }} />
|
||||||
|
<Popconfirm
|
||||||
|
description="删除后相关内容退化为普通文本"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
okText="删除"
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
title="确认删除该实体?"
|
||||||
|
>
|
||||||
|
<DeleteOutlined style={{ color: "var(--ant-color-error)", cursor: "pointer" }} />
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
title={data.name}
|
||||||
|
>
|
||||||
|
<Descriptions column={1} size="small">
|
||||||
|
<Descriptions.Item label="类型">
|
||||||
|
<Tag color={ENTITY_TYPE_COLORS[data.type] ?? "default"}>{ENTITY_TYPE_LABELS[data.type] ?? data.type}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="描述">{data.description || "暂无描述"}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="别名">
|
||||||
|
{data.aliases.length > 0 ? data.aliases.join("、") : "无"}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间">{formatRelativeTime(data.createdAt)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="更新时间">{formatRelativeTime(data.updatedAt)}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/web/features/entities/components/EntityFormModal.tsx
Normal file
95
src/web/features/entities/components/EntityFormModal.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { App as AntApp, Form, Input, Modal, Select } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { Entity, EntityType } from "../types";
|
||||||
|
import { ENTITY_TYPES } from "../types";
|
||||||
|
|
||||||
|
import { ENTITY_TYPE_LABELS } from "./constants";
|
||||||
|
|
||||||
|
interface EntityFormModalProps {
|
||||||
|
editingEntity: Entity | null;
|
||||||
|
onCancel: () => void;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSubmit: (entity: Entity | null, values: FormValues) => Promise<void>;
|
||||||
|
open: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
aliases: string[];
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
type: EntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_OPTIONS = ENTITY_TYPES.map((t) => ({
|
||||||
|
label: ENTITY_TYPE_LABELS[t],
|
||||||
|
value: t,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function EntityFormModal({ editingEntity, onCancel, onOpenChange, onSubmit, open }: EntityFormModalProps) {
|
||||||
|
const { message } = AntApp.useApp();
|
||||||
|
const [form] = Form.useForm<FormValues>();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
if (editingEntity) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
aliases: editingEntity.aliases,
|
||||||
|
description: editingEntity.description,
|
||||||
|
name: editingEntity.name,
|
||||||
|
type: editingEntity.type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [form, open, editingEntity]);
|
||||||
|
|
||||||
|
const handleFinish = async (values: FormValues) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSubmit(editingEntity, values);
|
||||||
|
message.success(editingEntity ? "实体已更新" : "实体已创建");
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
confirmLoading={submitting}
|
||||||
|
destroyOnHidden
|
||||||
|
okText="确定"
|
||||||
|
onCancel={() => {
|
||||||
|
onCancel();
|
||||||
|
onOpenChange(false);
|
||||||
|
}}
|
||||||
|
onOk={() => void form.submit()}
|
||||||
|
open={open}
|
||||||
|
title={editingEntity ? "编辑实体" : "新增实体"}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
initialValues={{ aliases: [], description: "", type: "other" as EntityType }}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={(values) => void handleFinish(values)}
|
||||||
|
>
|
||||||
|
<Form.Item label="名称" name="name" rules={[{ message: "请输入实体名称", required: true, whitespace: true }]}>
|
||||||
|
<Input maxLength={100} placeholder="实体名称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="类型" name="type" rules={[{ message: "请选择类型", required: true }]}>
|
||||||
|
<Select options={TYPE_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="描述" name="description">
|
||||||
|
<Input.TextArea maxLength={500} placeholder="实体描述" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="别名" name="aliases">
|
||||||
|
<Select mode="tags" placeholder="输入别名后按回车" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/web/features/entities/components/EntityList.tsx
Normal file
85
src/web/features/entities/components/EntityList.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Empty, Segmented, Skeleton } from "antd";
|
||||||
|
import "overlayscrollbars/styles/overlayscrollbars.css";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import type { Entity } from "../types";
|
||||||
|
import { ENTITY_TYPES } from "../types";
|
||||||
|
|
||||||
|
import { SidebarGroup } from "../../../shared/components/SidebarGroup";
|
||||||
|
import { GROUP_LABELS, groupByDate } from "../../../shared/utils/date-group";
|
||||||
|
import { ENTITY_TYPE_LABELS } from "./constants";
|
||||||
|
import { EntityCard } from "./EntityCard";
|
||||||
|
|
||||||
|
interface EntityListProps {
|
||||||
|
entities: readonly Entity[];
|
||||||
|
loading: boolean;
|
||||||
|
onAddClick: () => void;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
selectedId: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityList({ entities, loading, onAddClick, onSelect, selectedId }: EntityListProps) {
|
||||||
|
const [filterType, setFilterType] = useState<string>("all");
|
||||||
|
|
||||||
|
const filteredEntities = useMemo(() => {
|
||||||
|
if (filterType === "all") return entities;
|
||||||
|
return entities.filter((e) => e.type === filterType);
|
||||||
|
}, [entities, filterType]);
|
||||||
|
|
||||||
|
const groupedEntities = useMemo(() => groupByDate(filteredEntities, "createdAt"), [filteredEntities]);
|
||||||
|
|
||||||
|
const segmentedOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: "全部", value: "all" },
|
||||||
|
...ENTITY_TYPES.map((t) => ({
|
||||||
|
label: ENTITY_TYPE_LABELS[t],
|
||||||
|
value: t,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-sidebar-list" style={{ width: 260 }}>
|
||||||
|
<div className="app-sidebar-list-header">
|
||||||
|
<Button block icon={<PlusOutlined />} onClick={onAddClick} type="primary">
|
||||||
|
新增实体
|
||||||
|
</Button>
|
||||||
|
<Segmented block onChange={(value) => setFilterType(value)} options={segmentedOptions} value={filterType} />
|
||||||
|
</div>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="app-sidebar-list-body"
|
||||||
|
options={{
|
||||||
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
|
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton active paragraph={{ rows: 6 }} title={false} />
|
||||||
|
) : entities.length === 0 ? (
|
||||||
|
<Empty description="暂无实体" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
) : filteredEntities.length === 0 ? (
|
||||||
|
<Empty description="当前筛选条件下无实体" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
) : (
|
||||||
|
groupedEntities.map((group) => {
|
||||||
|
if (group.items.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<SidebarGroup count={group.items.length} key={group.key} label={GROUP_LABELS[group.key]}>
|
||||||
|
{group.items.map((entity) => (
|
||||||
|
<EntityCard
|
||||||
|
entity={entity}
|
||||||
|
key={entity.id}
|
||||||
|
onSelect={() => onSelect(entity.id)}
|
||||||
|
selected={entity.id === selectedId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/web/features/entities/components/constants.ts
Normal file
23
src/web/features/entities/components/constants.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { EntityType } from "../types";
|
||||||
|
|
||||||
|
export const ENTITY_TYPE_LABELS: Record<EntityType, string> = {
|
||||||
|
feature: "功能/模块",
|
||||||
|
issue: "问题/风险",
|
||||||
|
organization: "组织",
|
||||||
|
other: "其他",
|
||||||
|
person: "人",
|
||||||
|
requirement: "需求",
|
||||||
|
system: "系统/软件",
|
||||||
|
term: "术语/概念",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ENTITY_TYPE_COLORS: Record<EntityType, string> = {
|
||||||
|
feature: "blue",
|
||||||
|
issue: "red",
|
||||||
|
organization: "purple",
|
||||||
|
other: "default",
|
||||||
|
person: "green",
|
||||||
|
requirement: "orange",
|
||||||
|
system: "cyan",
|
||||||
|
term: "geekblue",
|
||||||
|
};
|
||||||
82
src/web/features/entities/index.tsx
Normal file
82
src/web/features/entities/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { Entity } from "./types";
|
||||||
|
|
||||||
|
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||||
|
import { useCreateEntity, useDeleteEntity, useEntityList, useUpdateEntity } from "../../shared/hooks/use-entities";
|
||||||
|
import { EntityDetailPanel } from "./components/EntityDetailPanel";
|
||||||
|
import { type FormValues, EntityFormModal } from "./components/EntityFormModal";
|
||||||
|
import { EntityList } from "./components/EntityList";
|
||||||
|
|
||||||
|
export function EntitiesPage() {
|
||||||
|
const project = useCurrentProject();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<null | string>(null);
|
||||||
|
const [editingEntity, setEditingEntity] = useState<Entity | null>(null);
|
||||||
|
|
||||||
|
const { data, isLoading } = useEntityList(project.id, { pageSize: 200 });
|
||||||
|
const createMutation = useCreateEntity(project.id);
|
||||||
|
const updateMutation = useUpdateEntity(project.id);
|
||||||
|
const deleteMutation = useDeleteEntity(project.id);
|
||||||
|
|
||||||
|
const handleEdit = (entity: Entity) => {
|
||||||
|
setEditingEntity(entity);
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (existing: Entity | null, values: FormValues) => {
|
||||||
|
if (existing) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
body: {
|
||||||
|
aliases: values.aliases,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
type: values.type,
|
||||||
|
},
|
||||||
|
entityId: existing.id,
|
||||||
|
projectId: project.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const entity = await createMutation.mutateAsync({
|
||||||
|
body: {
|
||||||
|
aliases: values.aliases,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
type: values.type,
|
||||||
|
},
|
||||||
|
projectId: project.id,
|
||||||
|
});
|
||||||
|
setSelectedId(entity.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
void deleteMutation.mutate({ entityId: id, projectId: project.id });
|
||||||
|
if (selectedId === id) {
|
||||||
|
setSelectedId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-inbox-page">
|
||||||
|
<EntityList
|
||||||
|
entities={data?.items ?? []}
|
||||||
|
loading={isLoading}
|
||||||
|
onAddClick={() => {
|
||||||
|
setEditingEntity(null);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
onSelect={setSelectedId}
|
||||||
|
selectedId={selectedId}
|
||||||
|
/>
|
||||||
|
<EntityDetailPanel entityId={selectedId} onDelete={handleDelete} onEdit={handleEdit} projectId={project.id} />
|
||||||
|
<EntityFormModal
|
||||||
|
editingEntity={editingEntity}
|
||||||
|
onCancel={() => setEditingEntity(null)}
|
||||||
|
onOpenChange={setModalOpen}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
open={modalOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
src/web/features/entities/types.ts
Normal file
2
src/web/features/entities/types.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export type { Entity, EntityType } from "../../../shared/api";
|
||||||
|
export { ENTITY_TYPES } from "../../../shared/api";
|
||||||
138
src/web/features/inbox/components/EntityCandidatePanel.tsx
Normal file
138
src/web/features/inbox/components/EntityCandidatePanel.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { CandidateEntity, EntityConfirmation } from "../../../../shared/api";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Badge, Button, Card, Flex, Modal, Select, Space, Tag, Typography } from "antd";
|
||||||
|
|
||||||
|
import { useEntityList } from "../../../shared/hooks/use-entities";
|
||||||
|
|
||||||
|
interface EntityCandidatePanelProps {
|
||||||
|
candidates: CandidateEntity[];
|
||||||
|
projectId: string;
|
||||||
|
onConfirmationsChange: (confirmations: EntityConfirmation[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityCandidatePanel({ candidates, projectId, onConfirmationsChange }: EntityCandidatePanelProps) {
|
||||||
|
const [confirmations, setConfirmations] = useState<Map<number, EntityConfirmation>>(new Map());
|
||||||
|
const [selectingIndex, setSelectingIndex] = useState<number | null>(null);
|
||||||
|
const [selectValue, setSelectValue] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: entityList } = useEntityList(projectId, { pageSize: 200 });
|
||||||
|
|
||||||
|
const handleAction = (index: number, action: EntityConfirmation["action"], targetEntityId?: string) => {
|
||||||
|
const next = new Map(confirmations);
|
||||||
|
if (action === "discard") {
|
||||||
|
next.set(index, { action: "discard", candidateIndex: index });
|
||||||
|
} else if (action === "merge" && targetEntityId) {
|
||||||
|
next.set(index, { action: "merge", candidateIndex: index, targetEntityId });
|
||||||
|
} else if (action === "create") {
|
||||||
|
next.set(index, { action: "create", candidateIndex: index });
|
||||||
|
} else if (action === "select" && targetEntityId) {
|
||||||
|
next.set(index, { action: "select", candidateIndex: index, targetEntityId });
|
||||||
|
}
|
||||||
|
setConfirmations(next);
|
||||||
|
onConfirmationsChange(Array.from(next.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectConfirm = () => {
|
||||||
|
if (selectingIndex !== null && selectValue) {
|
||||||
|
handleAction(selectingIndex, "select", selectValue);
|
||||||
|
}
|
||||||
|
setSelectingIndex(null);
|
||||||
|
setSelectValue(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!candidates || candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const entityOptions = (entityList?.items ?? []).map((e) => ({
|
||||||
|
label: `${e.name}${e.aliases.length > 0 ? ` (${e.aliases.join("、")})` : ""}`,
|
||||||
|
value: e.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card size="small" title="候选实体">
|
||||||
|
<Flex gap={8} vertical>
|
||||||
|
{candidates.map((candidate, index) => {
|
||||||
|
const conf = confirmations.get(index);
|
||||||
|
return (
|
||||||
|
<Card key={index} size="small" type="inner">
|
||||||
|
<Flex align="center" gap={8} justify="space-between" wrap="wrap">
|
||||||
|
<Flex align="center" gap={8}>
|
||||||
|
<Typography.Text strong>{candidate.name}</Typography.Text>
|
||||||
|
<Tag>{candidate.type}</Tag>
|
||||||
|
{candidate.matchedEntityId && <Badge color="blue" text="有匹配" />}
|
||||||
|
</Flex>
|
||||||
|
<Space size="small">
|
||||||
|
{candidate.matchedEntityId && (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleAction(index, "merge", candidate.matchedEntityId ?? undefined)}
|
||||||
|
size="small"
|
||||||
|
type={conf?.action === "merge" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
合并
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => handleAction(index, "create")}
|
||||||
|
size="small"
|
||||||
|
type={conf?.action === "create" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectingIndex(index);
|
||||||
|
setSelectValue(candidate.matchedEntityId);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
type={conf?.action === "select" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
选择
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger={conf?.action === "discard"}
|
||||||
|
onClick={() => handleAction(index, "discard")}
|
||||||
|
size="small"
|
||||||
|
type={conf?.action === "discard" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
放弃
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
{candidate.context && (
|
||||||
|
<Typography.Paragraph
|
||||||
|
ellipsis={{ rows: 2 }}
|
||||||
|
style={{ color: "var(--ant-color-text-secondary)", fontSize: 12, margin: "4px 0 0 0" }}
|
||||||
|
>
|
||||||
|
原文:{candidate.context}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
<Modal
|
||||||
|
onCancel={() => {
|
||||||
|
setSelectingIndex(null);
|
||||||
|
setSelectValue(null);
|
||||||
|
}}
|
||||||
|
onOk={handleSelectConfirm}
|
||||||
|
open={selectingIndex !== null}
|
||||||
|
title="选择已有实体"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
filterOption={(input, option) => String(option?.label ?? "").includes(input)}
|
||||||
|
onChange={setSelectValue}
|
||||||
|
options={entityOptions}
|
||||||
|
placeholder="搜索实体名称"
|
||||||
|
showSearch
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
value={selectValue}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,48 +1,106 @@
|
|||||||
import { Card, Descriptions, Tag, Typography } from "antd";
|
import { Card, Descriptions, Flex, Tag, Typography } from "antd";
|
||||||
|
|
||||||
import type { Material, MaterialStatus, MaterialType } from "../types";
|
import type { EntityConfirmation, ProcessingResult } from "../../../../shared/api";
|
||||||
|
import type { Material, MaterialType } from "../types";
|
||||||
|
|
||||||
import { formatRelativeTime } from "../../../shared/utils/time";
|
import { formatRelativeTime } from "../../../shared/utils/time";
|
||||||
|
import { STATUS_MAP } from "./constants";
|
||||||
|
import { EntityCandidatePanel } from "./EntityCandidatePanel";
|
||||||
|
|
||||||
interface MaterialContentProps {
|
interface MaterialContentProps {
|
||||||
material: Material;
|
material: Material;
|
||||||
|
projectId?: string;
|
||||||
|
onConfirmationsChange?: (confirmations: EntityConfirmation[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
|
|
||||||
approved: { color: "green", label: "已通过" },
|
|
||||||
discarded: { color: "red", label: "已放弃" },
|
|
||||||
failed: { color: "magenta", label: "失败" },
|
|
||||||
pending: { color: "gold", label: "待处理" },
|
|
||||||
processing: { color: "blue", label: "处理中" },
|
|
||||||
review: { color: "orange", label: "待审核" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
|
const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
|
||||||
general: "通用",
|
general: "通用",
|
||||||
meeting: "会议",
|
meeting: "会议",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MaterialContent({ material }: MaterialContentProps) {
|
function parseProcessingResult(raw: null | string): ProcessingResult | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as Partial<ProcessingResult>;
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
return {
|
||||||
|
candidateEntities: Array.isArray(parsed.candidateEntities) ? parsed.candidateEntities : [],
|
||||||
|
normalizedContent: typeof parsed.normalizedContent === "string" ? parsed.normalizedContent : "",
|
||||||
|
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaterialContent({ material, projectId, onConfirmationsChange }: MaterialContentProps) {
|
||||||
const statusInfo = STATUS_MAP[material.status] ?? { color: "default", label: material.status };
|
const statusInfo = STATUS_MAP[material.status] ?? { color: "default", label: material.status };
|
||||||
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
|
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
|
||||||
|
const processingResult = parseProcessingResult(material.processedContent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<Flex gap={12} vertical>
|
||||||
<Typography.Title level={4}>素材详情</Typography.Title>
|
<Card size="small" title="原始文本">
|
||||||
<Card>
|
|
||||||
<Typography.Paragraph>{material.description}</Typography.Paragraph>
|
<Typography.Paragraph>{material.description}</Typography.Paragraph>
|
||||||
{material.processedContent && (
|
</Card>
|
||||||
|
|
||||||
|
{processingResult && (
|
||||||
|
<>
|
||||||
|
<Card size="small" title="AI 摘要">
|
||||||
|
<Typography.Paragraph>{processingResult.summary}</Typography.Paragraph>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="规范化内容">
|
||||||
|
<Typography.Paragraph
|
||||||
|
style={{
|
||||||
|
background: "var(--ant-color-fill-quaternary)",
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 12,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{processingResult.normalizedContent}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{material.status === "review" && projectId && onConfirmationsChange && (
|
||||||
|
<EntityCandidatePanel
|
||||||
|
candidates={processingResult.candidateEntities}
|
||||||
|
projectId={projectId}
|
||||||
|
onConfirmationsChange={onConfirmationsChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{material.status !== "review" && processingResult.candidateEntities.length > 0 && (
|
||||||
|
<Card size="small" title="候选实体(已确认)">
|
||||||
|
<Flex gap={4} wrap="wrap">
|
||||||
|
{processingResult.candidateEntities.map((ce: { name: string }, i: number) => (
|
||||||
|
<Tag key={i}>{ce.name}</Tag>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!processingResult && material.processedContent && (
|
||||||
|
<Card size="small" title="处理结果">
|
||||||
<Typography.Paragraph
|
<Typography.Paragraph
|
||||||
style={{
|
style={{
|
||||||
background: "var(--ant-color-fill-quaternary)",
|
background: "var(--ant-color-fill-quaternary)",
|
||||||
padding: 12,
|
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
padding: 12,
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{material.processedContent}
|
{material.processedContent}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
)}
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card size="small" title="基本信息">
|
||||||
<Descriptions column={1} size="small">
|
<Descriptions column={1} size="small">
|
||||||
<Descriptions.Item label="状态">
|
<Descriptions.Item label="状态">
|
||||||
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
|
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
|
||||||
@@ -52,6 +110,6 @@ export function MaterialContent({ material }: MaterialContentProps) {
|
|||||||
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
|
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { CheckOutlined, CloseOutlined, RedoOutlined } from "@ant-design/icons";
|
import { CheckOutlined, CloseOutlined, RedoOutlined } from "@ant-design/icons";
|
||||||
import { App as AntApp, Button, Empty, Result, Space, Spin } from "antd";
|
import "overlayscrollbars/styles/overlayscrollbars.css";
|
||||||
|
import { App as AntApp, Button, Empty, Result, Space, Spin, Typography } from "antd";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { Material } from "../types";
|
import type { EntityConfirmation } from "../../../../shared/api";
|
||||||
|
|
||||||
import { useMaterial } from "../../../shared/hooks/use-materials";
|
import { useMaterial } from "../../../shared/hooks/use-materials";
|
||||||
import { MaterialContent } from "./MaterialContent";
|
import { MaterialContent } from "./MaterialContent";
|
||||||
|
|
||||||
|
const OS_OPTIONS = {
|
||||||
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
|
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface MaterialDetailPanelProps {
|
interface MaterialDetailPanelProps {
|
||||||
materialId: null | string;
|
materialId: null | string;
|
||||||
onApprove: (materialId: string) => Promise<void>;
|
onApprove: (materialId: string, entityConfirmations: EntityConfirmation[]) => Promise<void>;
|
||||||
onDiscard: (materialId: string) => Promise<void>;
|
onDiscard: (materialId: string) => Promise<void>;
|
||||||
onRetry: (materialId: string) => Promise<void>;
|
onRetry: (materialId: string) => Promise<void>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -23,8 +31,15 @@ export function MaterialDetailPanel({
|
|||||||
}: MaterialDetailPanelProps) {
|
}: MaterialDetailPanelProps) {
|
||||||
if (!materialId) {
|
if (!materialId) {
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<div className="app-inbox-panel">
|
||||||
<Empty description="请在左侧选择素材" />
|
<OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
|
||||||
|
<Empty description="请在左侧选择素材" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
<div className="app-inbox-action-bar">
|
||||||
|
<Typography.Text style={{ flex: 1, textAlign: "center", color: "var(--ant-color-text-tertiary)" }}>
|
||||||
|
请先选择素材
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,27 +58,34 @@ export function MaterialDetailPanel({
|
|||||||
function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, projectId }: MaterialDetailPanelProps) {
|
function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, projectId }: MaterialDetailPanelProps) {
|
||||||
const { data, error, isLoading } = useMaterial({ materialId, projectId });
|
const { data, error, isLoading } = useMaterial({ materialId, projectId });
|
||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
|
const [entityConfirmations, setEntityConfirmations] = useState<EntityConfirmation[]>([]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<div className="app-inbox-panel">
|
||||||
<Spin />
|
<OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
|
||||||
|
<Spin />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<div className="app-inbox-panel">
|
||||||
<Result subTitle="加载素材详情失败" />
|
<OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
|
||||||
|
<Result subTitle="加载素材详情失败" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !materialId) {
|
if (!data || !materialId) {
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<div className="app-inbox-panel">
|
||||||
<Empty description="请在左侧选择素材" />
|
<OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
|
||||||
|
<Empty description="请在左侧选择素材" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,8 +94,9 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
|
|||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = async () => {
|
||||||
try {
|
try {
|
||||||
await onApprove(id);
|
await onApprove(id, entityConfirmations);
|
||||||
message.success("已通过");
|
message.success("已通过");
|
||||||
|
setEntityConfirmations([]);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||||
}
|
}
|
||||||
@@ -83,6 +106,7 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
|
|||||||
try {
|
try {
|
||||||
await onDiscard(id);
|
await onDiscard(id);
|
||||||
message.success("已放弃");
|
message.success("已放弃");
|
||||||
|
setEntityConfirmations([]);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||||
}
|
}
|
||||||
@@ -98,43 +122,39 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-inbox-content">
|
<div className="app-inbox-panel">
|
||||||
<MaterialContent material={data} />
|
<OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
|
||||||
<ActionButtons material={data} onApprove={handleApprove} onDiscard={handleDiscard} onRetry={handleRetry} />
|
<MaterialContent material={data} projectId={projectId} onConfirmationsChange={setEntityConfirmations} />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
<div className="app-inbox-action-bar">
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={data.status !== "review"}
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
onClick={() => void handleApprove()}
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
disabled={data.status !== "review"}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => void handleDiscard()}
|
||||||
|
>
|
||||||
|
放弃
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={data.status !== "failed"}
|
||||||
|
icon={<RedoOutlined />}
|
||||||
|
onClick={() => void handleRetry()}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionButtonsProps {
|
|
||||||
material: Material;
|
|
||||||
onApprove: () => Promise<void>;
|
|
||||||
onDiscard: () => Promise<void>;
|
|
||||||
onRetry: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActionButtons({ material, onApprove, onDiscard, onRetry }: ActionButtonsProps) {
|
|
||||||
if (material.status === "review") {
|
|
||||||
return (
|
|
||||||
<Space style={{ marginTop: 16 }}>
|
|
||||||
<Button icon={<CheckOutlined />} onClick={() => void onApprove()} type="primary">
|
|
||||||
通过
|
|
||||||
</Button>
|
|
||||||
<Button danger icon={<CloseOutlined />} onClick={() => void onDiscard()}>
|
|
||||||
放弃
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (material.status === "failed") {
|
|
||||||
return (
|
|
||||||
<Space style={{ marginTop: 16 }}>
|
|
||||||
<Button icon={<RedoOutlined />} onClick={() => void onRetry()}>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|||||||
10
src/web/features/inbox/components/constants.ts
Normal file
10
src/web/features/inbox/components/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { MaterialStatus } from "../types";
|
||||||
|
|
||||||
|
export const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
|
||||||
|
approved: { color: "green", label: "已通过" },
|
||||||
|
discarded: { color: "red", label: "已放弃" },
|
||||||
|
failed: { color: "magenta", label: "失败" },
|
||||||
|
pending: { color: "gold", label: "待处理" },
|
||||||
|
processing: { color: "blue", label: "处理中" },
|
||||||
|
review: { color: "orange", label: "待审核" },
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { CreateMaterialRequest, Material } from "./types";
|
import type { CreateMaterialRequest, EntityConfirmation, Material } from "./types";
|
||||||
|
|
||||||
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||||
import {
|
import {
|
||||||
@@ -35,8 +35,8 @@ export function InboxPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = async (materialId: string) => {
|
const handleApprove = async (materialId: string, entityConfirmations: EntityConfirmation[]) => {
|
||||||
await approveMutation.mutateAsync({ materialId, projectId: project.id });
|
await approveMutation.mutateAsync({ entityConfirmations, materialId, projectId: project.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDiscard = async (materialId: string) => {
|
const handleDiscard = async (materialId: string) => {
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
export type { CreateMaterialRequest, Material, MaterialStatus, MaterialType } from "../../../shared/api";
|
export type {
|
||||||
|
CreateMaterialRequest,
|
||||||
|
EntityConfirmation,
|
||||||
|
Material,
|
||||||
|
MaterialStatus,
|
||||||
|
MaterialType,
|
||||||
|
} from "../../../shared/api";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InboxOutlined, MessageOutlined } from "@ant-design/icons";
|
import { InboxOutlined, MessageOutlined, TeamOutlined } from "@ant-design/icons";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
|
|
||||||
import type { MenuItemConfig } from "../../menu";
|
import type { MenuItemConfig } from "../../menu";
|
||||||
@@ -6,6 +6,7 @@ import type { MenuItemConfig } from "../../menu";
|
|||||||
export const WORKBENCH_MENU_ITEMS: readonly MenuItemConfig[] = [
|
export const WORKBENCH_MENU_ITEMS: readonly MenuItemConfig[] = [
|
||||||
{ icon: createElement(MessageOutlined), label: "聊天室", path: "", value: "chat" },
|
{ icon: createElement(MessageOutlined), label: "聊天室", path: "", value: "chat" },
|
||||||
{ icon: createElement(InboxOutlined), label: "收集箱", path: "inbox", value: "inbox" },
|
{ icon: createElement(InboxOutlined), label: "收集箱", path: "inbox", value: "inbox" },
|
||||||
|
{ icon: createElement(TeamOutlined), label: "实体", path: "entities", value: "entities" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function buildWorkbenchPath(projectId: string, relativePath = ""): string {
|
export function buildWorkbenchPath(projectId: string, relativePath = ""): string {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Route, Routes } from "react-router";
|
|||||||
|
|
||||||
import { ChatPage } from "./features/chat/ChatPage";
|
import { ChatPage } from "./features/chat/ChatPage";
|
||||||
import { DashboardPage } from "./features/dashboard";
|
import { DashboardPage } from "./features/dashboard";
|
||||||
|
import { EntitiesPage } from "./features/entities";
|
||||||
import { InboxPage } from "./features/inbox";
|
import { InboxPage } from "./features/inbox";
|
||||||
import { ModelListPage } from "./features/models/ModelListPage";
|
import { ModelListPage } from "./features/models/ModelListPage";
|
||||||
import { ProviderListPage } from "./features/models/ProviderListPage";
|
import { ProviderListPage } from "./features/models/ProviderListPage";
|
||||||
@@ -26,6 +27,7 @@ export function AppRoutes() {
|
|||||||
<Route element={<ChatPage />} path="" />
|
<Route element={<ChatPage />} path="" />
|
||||||
<Route element={<ChatPage />} path="chat" />
|
<Route element={<ChatPage />} path="chat" />
|
||||||
<Route element={<InboxPage />} path="inbox" />
|
<Route element={<InboxPage />} path="inbox" />
|
||||||
|
<Route element={<EntitiesPage />} path="entities" />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<NotFoundPage />} path="*" />
|
<Route element={<NotFoundPage />} path="*" />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
118
src/web/shared/hooks/use-entities.ts
Normal file
118
src/web/shared/hooks/use-entities.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateEntityRequest,
|
||||||
|
Entity,
|
||||||
|
EntityListResponse,
|
||||||
|
EntityResponse,
|
||||||
|
EntityType,
|
||||||
|
UpdateEntityRequest,
|
||||||
|
} from "../../../shared/api";
|
||||||
|
|
||||||
|
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||||
|
import { createConsoleLogger } from "../utils/logger";
|
||||||
|
|
||||||
|
const ENTITIES_KEY = ["entities"] as const;
|
||||||
|
const logger = createConsoleLogger();
|
||||||
|
|
||||||
|
export function createEntity(args: { body: CreateEntityRequest; projectId: string }): Promise<Entity> {
|
||||||
|
const response = fetch(`/api/projects/${args.projectId}/entities`, {
|
||||||
|
body: JSON.stringify(args.body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
return response.then((r) => handleResponse(r, (data) => (data as EntityResponse).entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteEntity(args: { entityId: string; projectId: string }): Promise<void> {
|
||||||
|
const response = fetch(`/api/projects/${args.projectId}/entities/${args.entityId}`, { method: "DELETE" });
|
||||||
|
return response.then(handleVoidResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEntity(args: { entityId: string; projectId: string }): Promise<Entity> {
|
||||||
|
const response = await fetch(`/api/projects/${args.projectId}/entities/${args.entityId}`);
|
||||||
|
return handleResponse(response, (data) => (data as EntityResponse).entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchEntities(
|
||||||
|
projectId: string,
|
||||||
|
params?: { page?: number; pageSize?: number; type?: EntityType },
|
||||||
|
): Promise<EntityListResponse> {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params?.page) searchParams.set("page", String(params.page));
|
||||||
|
if (params?.pageSize) searchParams.set("pageSize", String(params.pageSize));
|
||||||
|
if (params?.type) searchParams.set("type", params.type);
|
||||||
|
const qs = searchParams.toString();
|
||||||
|
const url = `/api/projects/${projectId}/entities${qs ? `?${qs}` : ""}`;
|
||||||
|
const response = fetch(url);
|
||||||
|
return response.then((r) => {
|
||||||
|
if (!r.ok) {
|
||||||
|
return r.json().then((body: null | { error?: string }) => {
|
||||||
|
throw new Error(body?.error ?? `HTTP ${r.status}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return r.json() as Promise<EntityListResponse>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateEntity(args: {
|
||||||
|
body: UpdateEntityRequest;
|
||||||
|
entityId: string;
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<Entity> {
|
||||||
|
const response = fetch(`/api/projects/${args.projectId}/entities/${args.entityId}`, {
|
||||||
|
body: JSON.stringify(args.body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PATCH",
|
||||||
|
});
|
||||||
|
return response.then((r) => handleResponse(r, (data) => (data as EntityResponse).entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateEntity(projectId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: createEntity,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
logger.info("实体创建成功", { entityId: data.id, projectId });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: [...ENTITIES_KEY, "list", projectId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteEntity(projectId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: deleteEntity,
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
logger.info("实体删除成功", { entityId: variables.entityId, projectId });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: [...ENTITIES_KEY, "list", projectId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntity(args: { entityId: null | string; projectId: string }) {
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!args.entityId,
|
||||||
|
queryFn: () => fetchEntity({ entityId: args.entityId!, projectId: args.projectId }),
|
||||||
|
queryKey: [...ENTITIES_KEY, "detail", args.projectId, args.entityId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityList(projectId: string, params?: { page?: number; pageSize?: number; type?: EntityType }) {
|
||||||
|
return useQuery({
|
||||||
|
queryFn: () => fetchEntities(projectId, params),
|
||||||
|
queryKey: [...ENTITIES_KEY, "list", projectId, params],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateEntity(projectId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: updateEntity,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
logger.info("实体更新成功", { entityId: data.id, projectId });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: [...ENTITIES_KEY, "list", projectId] });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: [...ENTITIES_KEY, "detail", projectId, data.id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
CreateMaterialRequest,
|
CreateMaterialRequest,
|
||||||
|
EntityConfirmation,
|
||||||
Material,
|
Material,
|
||||||
MaterialListResponse,
|
MaterialListResponse,
|
||||||
MaterialResponse,
|
MaterialResponse,
|
||||||
@@ -23,8 +24,16 @@ export function createMaterial(args: { body: CreateMaterialRequest; projectId: s
|
|||||||
return response.then((r) => handleResponse(r, (data) => (data as MaterialResponse).material));
|
return response.then((r) => handleResponse(r, (data) => (data as MaterialResponse).material));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function approveMaterial(args: { materialId: string; projectId: string }): Promise<Material> {
|
export function approveMaterial(args: {
|
||||||
const response = fetch(`/api/projects/${args.projectId}/materials/${args.materialId}/approve`, { method: "POST" });
|
entityConfirmations?: EntityConfirmation[];
|
||||||
|
materialId: string;
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<Material> {
|
||||||
|
const response = fetch(`/api/projects/${args.projectId}/materials/${args.materialId}/approve`, {
|
||||||
|
body: JSON.stringify({ entityConfirmations: args.entityConfirmations ?? [] }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
return response.then((r) => handleResponse(r, (data) => (data as MaterialResponse).material));
|
return response.then((r) => handleResponse(r, (data) => (data as MaterialResponse).material));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -334,18 +334,35 @@ body {
|
|||||||
|
|
||||||
.app-inbox-page {
|
.app-inbox-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--ant-margin-sm);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-inbox-content {
|
.app-inbox-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: var(--ant-margin-sm);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: var(--ant-padding-xl);
|
}
|
||||||
overflow-y: auto;
|
|
||||||
|
.app-inbox-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-inbox-action-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--ant-padding-sm);
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-inbox-datepicker {
|
.app-inbox-datepicker {
|
||||||
@@ -462,8 +479,3 @@ body {
|
|||||||
.markdown-table tbody tr:hover td {
|
.markdown-table tbody tr:hover td {
|
||||||
background: var(--ant-color-fill-quaternary);
|
background: var(--ant-color-fill-quaternary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 设置页表单:最后一项无底边距 */
|
|
||||||
.settings-form .ant-form-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
264
tests/server/db/entities.test.ts
Normal file
264
tests/server/db/entities.test.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createEntity,
|
||||||
|
deleteEntity,
|
||||||
|
getEntity,
|
||||||
|
listEntities,
|
||||||
|
listEntityNames,
|
||||||
|
updateEntity,
|
||||||
|
} from "../../../src/server/db/entities";
|
||||||
|
import { createProject } from "../../../src/server/db/projects";
|
||||||
|
import { createNoopLogger } from "../../../src/server/logger";
|
||||||
|
import { createMigratedTestDatabase } from "../../helpers";
|
||||||
|
|
||||||
|
const LOG = createNoopLogger();
|
||||||
|
|
||||||
|
function withEntitiesDb(callback: (db: Database) => void): void {
|
||||||
|
const handle = createMigratedTestDatabase("entities-dao-test");
|
||||||
|
try {
|
||||||
|
callback(handle.db);
|
||||||
|
handle.close();
|
||||||
|
} finally {
|
||||||
|
handle.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupProject(db: Database, name = "测试项目"): string {
|
||||||
|
const result = createProject(db, { name }, LOG);
|
||||||
|
if ("error" in result) throw new Error(result.error);
|
||||||
|
return result.project.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupEntity(
|
||||||
|
db: Database,
|
||||||
|
projectId: string,
|
||||||
|
overrides: Partial<{ aliases: string[]; description: string; name: string; type: string }> = {},
|
||||||
|
): string {
|
||||||
|
const result = createEntity(
|
||||||
|
db,
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
aliases: overrides.aliases,
|
||||||
|
description: overrides.description ?? "测试实体描述",
|
||||||
|
name: overrides.name ?? "测试实体",
|
||||||
|
type: (overrides.type ?? "person") as "person",
|
||||||
|
},
|
||||||
|
LOG,
|
||||||
|
);
|
||||||
|
if ("error" in result) throw new Error(result.error);
|
||||||
|
return result.entity.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("实体数据访问层", () => {
|
||||||
|
describe("createEntity", () => {
|
||||||
|
test("创建实体成功,aliases 为数组", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const result = createEntity(
|
||||||
|
db,
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
description: "描述",
|
||||||
|
name: "张三",
|
||||||
|
type: "person",
|
||||||
|
},
|
||||||
|
LOG,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
const entity = (result as { entity: { aliases: string[]; description: string; name: string; type: string } })
|
||||||
|
.entity;
|
||||||
|
expect(entity.name).toBe("张三");
|
||||||
|
expect(entity.description).toBe("描述");
|
||||||
|
expect(entity.type).toBe("person");
|
||||||
|
expect(entity.aliases).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("创建实体时指定别名", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const result = createEntity(
|
||||||
|
db,
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
aliases: ["小张", "张工"],
|
||||||
|
name: "张三",
|
||||||
|
type: "person",
|
||||||
|
},
|
||||||
|
LOG,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
const entity = (result as { entity: { aliases: string[] } }).entity;
|
||||||
|
expect(entity.aliases).toEqual(["小张", "张工"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("空名称返回 400", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const result = createEntity(db, projectId, { name: " ", type: "other" }, LOG);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
expect((result as { status: number }).status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("同项目下重名返回 409", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
createEntity(db, projectId, { name: "张三", type: "person" }, LOG);
|
||||||
|
const result = createEntity(db, projectId, { name: "张三", type: "person" }, LOG);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
expect((result as { status: number }).status).toBe(409);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("不同项目可重名", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const p1 = setupProject(db, "项目一");
|
||||||
|
const p2 = setupProject(db, "项目二");
|
||||||
|
expect("error" in createEntity(db, p1, { name: "张三", type: "person" }, LOG)).toBe(false);
|
||||||
|
expect("error" in createEntity(db, p2, { name: "张三", type: "person" }, LOG)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEntity", () => {
|
||||||
|
test("获取实体成功", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const entityId = setupEntity(db, projectId);
|
||||||
|
const result = getEntity(db, projectId, entityId);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
expect((result as { entity: { id: string } }).entity.id).toBe(entityId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("不存在返回 404", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const result = getEntity(db, projectId, "nonexistent");
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
expect((result as { status: number }).status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("listEntities", () => {
|
||||||
|
test("分页列出实体", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
setupEntity(db, projectId, { name: "实体1", type: "person" });
|
||||||
|
setupEntity(db, projectId, { name: "实体2", type: "organization" });
|
||||||
|
|
||||||
|
const result = listEntities(db, projectId, { page: 1, pageSize: 10 });
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
const types = result.items.map((i: { type: string }) => i.type).sort();
|
||||||
|
expect(types).toContain("person");
|
||||||
|
expect(types).toContain("organization");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("按类型筛选", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
setupEntity(db, projectId, { name: "人", type: "person" });
|
||||||
|
setupEntity(db, projectId, { name: "公司", type: "organization" });
|
||||||
|
|
||||||
|
const result = listEntities(db, projectId, { page: 1, pageSize: 10, type: "person" });
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
const firstItem = result.items[0]!;
|
||||||
|
expect(firstItem.name).toBe("人");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("软删除实体不出现在列表中", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const entityId = setupEntity(db, projectId);
|
||||||
|
deleteEntity(db, projectId, entityId, LOG);
|
||||||
|
const result = listEntities(db, projectId, { page: 1, pageSize: 10 });
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("listEntityNames", () => {
|
||||||
|
test("返回所有实体名称和别名", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
setupEntity(db, projectId, { aliases: ["小张"], name: "张三", type: "person" });
|
||||||
|
setupEntity(db, projectId, { name: "李四", type: "person" });
|
||||||
|
|
||||||
|
const result = listEntityNames(db, projectId);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
const names = result.map((r) => r.name).sort();
|
||||||
|
expect(names).toEqual(["张三", "李四"]);
|
||||||
|
const p3 = result.find((r) => r.name === "张三")!;
|
||||||
|
expect(p3.aliases).toEqual(["小张"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateEntity", () => {
|
||||||
|
test("更新实体名称和别名", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const entityId = setupEntity(db, projectId, { name: "张三" });
|
||||||
|
const result = updateEntity(db, projectId, entityId, { aliases: ["张总", "老张"], name: "张三丰" }, LOG);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
const entity = (result as { entity: { aliases: string[]; name: string } }).entity;
|
||||||
|
expect(entity.name).toBe("张三丰");
|
||||||
|
expect(entity.aliases).toEqual(["张总", "老张"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("更新时名称去重", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
setupEntity(db, projectId, { name: "已有实体" });
|
||||||
|
const entityId = setupEntity(db, projectId, { name: "张三" });
|
||||||
|
const result = updateEntity(db, projectId, entityId, { name: "已有实体" }, LOG);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
expect((result as { status: number }).status).toBe(409);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("空参数返回原实体", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const entityId = setupEntity(db, projectId);
|
||||||
|
const result = updateEntity(db, projectId, entityId, {}, LOG);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
const entity = (result as { entity: { id: string } }).entity;
|
||||||
|
expect(entity.id).toBe(entityId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteEntity", () => {
|
||||||
|
test("软删除实体成功", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const entityId = setupEntity(db, projectId);
|
||||||
|
const result = deleteEntity(db, projectId, entityId, LOG);
|
||||||
|
expect("error" in result).toBe(false);
|
||||||
|
|
||||||
|
const getResult = getEntity(db, projectId, entityId);
|
||||||
|
expect("error" in getResult).toBe(true);
|
||||||
|
expect((getResult as { status: number }).status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("不存在返回 404", () => {
|
||||||
|
withEntitiesDb((db) => {
|
||||||
|
const projectId = setupProject(db);
|
||||||
|
const result = deleteEntity(db, projectId, "nonexistent", LOG);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
expect((result as { status: number }).status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -136,7 +136,7 @@ describe("素材数据访问层", () => {
|
|||||||
const materialId = setupMaterial(db, projectId);
|
const materialId = setupMaterial(db, projectId);
|
||||||
setMaterialStatus(db, materialId, "review");
|
setMaterialStatus(db, materialId, "review");
|
||||||
|
|
||||||
const result = approveMaterial(db, projectId, materialId, LOG);
|
const result = approveMaterial(db, projectId, materialId, [], LOG);
|
||||||
expect("error" in result).toBe(false);
|
expect("error" in result).toBe(false);
|
||||||
const material = (result as { material: { status: string } }).material;
|
const material = (result as { material: { status: string } }).material;
|
||||||
expect(material.status).toBe("approved");
|
expect(material.status).toBe("approved");
|
||||||
@@ -148,7 +148,7 @@ describe("素材数据访问层", () => {
|
|||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
const materialId = setupMaterial(db, projectId);
|
const materialId = setupMaterial(db, projectId);
|
||||||
|
|
||||||
const result = approveMaterial(db, projectId, materialId, LOG);
|
const result = approveMaterial(db, projectId, materialId, [], LOG);
|
||||||
expect("error" in result).toBe(true);
|
expect("error" in result).toBe(true);
|
||||||
expect((result as { status: number }).status).toBe(409);
|
expect((result as { status: number }).status).toBe(409);
|
||||||
});
|
});
|
||||||
@@ -157,7 +157,7 @@ describe("素材数据访问层", () => {
|
|||||||
test("素材不存在返回 404", () => {
|
test("素材不存在返回 404", () => {
|
||||||
withMaterialsDb((db) => {
|
withMaterialsDb((db) => {
|
||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
const result = approveMaterial(db, projectId, "nonexistent", LOG);
|
const result = approveMaterial(db, projectId, "nonexistent", [], LOG);
|
||||||
expect("error" in result).toBe(true);
|
expect("error" in result).toBe(true);
|
||||||
expect((result as { status: number }).status).toBe(404);
|
expect((result as { status: number }).status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,15 @@ function setMaterialStatus(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FailingProcessor extends MaterialProcessor {
|
class FakeProcessor extends MaterialProcessor {
|
||||||
|
public processOneResult = '{"summary":"test","normalizedContent":"test","candidateEntities":[]}';
|
||||||
|
|
||||||
|
protected override async processOne(_material: ProcessableMaterial): Promise<string> {
|
||||||
|
return Promise.resolve(this.processOneResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FailingProcessor extends FakeProcessor {
|
||||||
public attempts = 0;
|
public attempts = 0;
|
||||||
public failUntilAttempt = Number.POSITIVE_INFINITY;
|
public failUntilAttempt = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
@@ -98,7 +106,7 @@ describe("素材处理器", () => {
|
|||||||
setMaterialStatus(db, id1, "processing");
|
setMaterialStatus(db, id1, "processing");
|
||||||
setMaterialStatus(db, id2, "processing");
|
setMaterialStatus(db, id2, "processing");
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
const recovered = processor.recoverStuckMaterials();
|
const recovered = processor.recoverStuckMaterials();
|
||||||
|
|
||||||
expect(recovered).toBe(2);
|
expect(recovered).toBe(2);
|
||||||
@@ -112,7 +120,7 @@ describe("素材处理器", () => {
|
|||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
setupMaterial(db, projectId);
|
setupMaterial(db, projectId);
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
const recovered = processor.recoverStuckMaterials();
|
const recovered = processor.recoverStuckMaterials();
|
||||||
|
|
||||||
expect(recovered).toBe(0);
|
expect(recovered).toBe(0);
|
||||||
@@ -124,16 +132,17 @@ describe("素材处理器", () => {
|
|||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
const id = setupMaterial(db, projectId, { description: "测试内容" });
|
const id = setupMaterial(db, projectId, { description: "测试内容" });
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
|
processor.processOneResult = '{"summary":"概要","normalizedContent":"规范内容","candidateEntities":[]}';
|
||||||
await processor.processNext();
|
await processor.processNext();
|
||||||
|
|
||||||
const row = getMaterialRow(db, id);
|
const row = getMaterialRow(db, id);
|
||||||
expect(row?.status).toBe("review");
|
expect(row?.status).toBe("review");
|
||||||
expect(row?.processedContent).toBe("测试内容");
|
expect(row?.processedContent).toBe('{"summary":"概要","normalizedContent":"规范内容","candidateEntities":[]}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("processNext 根据 materialType 选择模板", async () => {
|
test("processNext 根据 materialType 调用对应模板", async () => {
|
||||||
await withProcessorDbAsync(async (db) => {
|
await withProcessorDbAsync(async (db) => {
|
||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
const id = setupMaterial(db, projectId, {
|
const id = setupMaterial(db, projectId, {
|
||||||
@@ -141,12 +150,12 @@ describe("素材处理器", () => {
|
|||||||
materialType: "meeting",
|
materialType: "meeting",
|
||||||
});
|
});
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
await processor.processNext();
|
await processor.processNext();
|
||||||
|
|
||||||
const row = getMaterialRow(db, id);
|
const row = getMaterialRow(db, id);
|
||||||
expect(row?.status).toBe("review");
|
expect(row?.status).toBe("review");
|
||||||
expect(row?.processedContent).toBe("会议内容");
|
expect(row?.processedContent).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,7 +197,7 @@ describe("素材处理器", () => {
|
|||||||
await withProcessorDbAsync(async (db) => {
|
await withProcessorDbAsync(async (db) => {
|
||||||
setupProject(db);
|
setupProject(db);
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
await processor.processNext();
|
await processor.processNext();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -201,7 +210,7 @@ describe("素材处理器", () => {
|
|||||||
await new Promise((r) => setTimeout(r, 20));
|
await new Promise((r) => setTimeout(r, 20));
|
||||||
const id2 = setupMaterial(db, projectId, { description: "第二个" });
|
const id2 = setupMaterial(db, projectId, { description: "第二个" });
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
await processor.processNext();
|
await processor.processNext();
|
||||||
|
|
||||||
expect(getMaterialRow(db, id1)?.status).toBe("review");
|
expect(getMaterialRow(db, id1)?.status).toBe("review");
|
||||||
@@ -211,7 +220,7 @@ describe("素材处理器", () => {
|
|||||||
|
|
||||||
test("start 启动后能正常 stop", () => {
|
test("start 启动后能正常 stop", () => {
|
||||||
withProcessorDb((db) => {
|
withProcessorDb((db) => {
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
processor.start(100);
|
processor.start(100);
|
||||||
processor.stop();
|
processor.stop();
|
||||||
});
|
});
|
||||||
@@ -222,7 +231,7 @@ describe("素材处理器", () => {
|
|||||||
const projectId = setupProject(db);
|
const projectId = setupProject(db);
|
||||||
const id = setupMaterial(db, projectId, { description: "定时扫描" });
|
const id = setupMaterial(db, projectId, { description: "定时扫描" });
|
||||||
|
|
||||||
const processor = new MaterialProcessor(db, LOG);
|
const processor = new FakeProcessor(db, LOG);
|
||||||
processor.start(50);
|
processor.start(50);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
@@ -231,7 +240,6 @@ describe("素材处理器", () => {
|
|||||||
|
|
||||||
const row = getMaterialRow(db, id);
|
const row = getMaterialRow(db, id);
|
||||||
expect(row?.status).toBe("review");
|
expect(row?.status).toBe("review");
|
||||||
expect(row?.processedContent).toBe("定时扫描");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
218
tests/server/routes/entities.test.ts
Normal file
218
tests/server/routes/entities.test.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import type Database from "bun:sqlite";
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import type { Entity, RuntimeMode } from "../../../src/shared/api";
|
||||||
|
|
||||||
|
import { createProject } from "../../../src/server/db/projects";
|
||||||
|
import { createNoopLogger } from "../../../src/server/logger";
|
||||||
|
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||||
|
|
||||||
|
const MODE: RuntimeMode = "test";
|
||||||
|
const LOG = createNoopLogger();
|
||||||
|
|
||||||
|
async function createEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||||
|
const { handleCreateEntity: h } = await import("../../../src/server/routes/entities/create");
|
||||||
|
return h(req, db, MODE, LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listEntitiesViaHandler(req: Request, db: Database): Promise<Response> {
|
||||||
|
const { handleListEntities: h } = await import("../../../src/server/routes/entities/list");
|
||||||
|
return h(req, db, MODE, LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||||
|
const { handleGetEntity: h } = await import("../../../src/server/routes/entities/get");
|
||||||
|
return h(req, db, MODE, LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||||
|
const { handleUpdateEntity: h } = await import("../../../src/server/routes/entities/update");
|
||||||
|
return h(req, db, MODE, LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||||
|
const { handleDeleteEntity: h } = await import("../../../src/server/routes/entities/delete");
|
||||||
|
return h(req, db, MODE, LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
|
||||||
|
const handle = createMigratedMemoryTestDatabase("entity-route-test");
|
||||||
|
try {
|
||||||
|
await callback(handle.db);
|
||||||
|
handle.close();
|
||||||
|
} finally {
|
||||||
|
handle.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestProject(db: Database) {
|
||||||
|
const result = createProject(db, { name: "测试项目" }, LOG);
|
||||||
|
if ("error" in result) throw new Error(result.error);
|
||||||
|
return result.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("实体 API 路由", () => {
|
||||||
|
describe("POST /api/projects/:id/entities", () => {
|
||||||
|
test("正常创建实体", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "张三", type: "person", description: "描述", aliases: ["小张"] }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const res = await createEntityViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const body = (await res.json()) as { entity: Entity };
|
||||||
|
expect(body.entity.name).toBe("张三");
|
||||||
|
expect(body.entity.type).toBe("person");
|
||||||
|
expect(body.entity.aliases).toEqual(["小张"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("无 name 返回 400", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ type: "other" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const res = await createEntityViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/projects/:id/entities", () => {
|
||||||
|
test("分页列出实体", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const req1 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "实体1", type: "person" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
await createEntityViaHandler(req1, db);
|
||||||
|
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities?page=1&pageSize=10`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const res = await listEntitiesViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as { items: Entity[]; total: number };
|
||||||
|
expect(body.total).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("按类型筛选", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const req1 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "人", type: "person" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
await createEntityViaHandler(req1, db);
|
||||||
|
const req2 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "公司", type: "organization" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
await createEntityViaHandler(req2, db);
|
||||||
|
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities?type=person`, { method: "GET" });
|
||||||
|
const res = await listEntitiesViaHandler(req, db);
|
||||||
|
const body = (await res.json()) as { items: Entity[]; total: number };
|
||||||
|
expect(body.total).toBe(1);
|
||||||
|
const firstItem = body.items[0]!;
|
||||||
|
expect(firstItem.name).toBe("人");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/projects/:id/entities/:eid", () => {
|
||||||
|
test("获取实体详情", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const createRes = await createEntityViaHandler(createReq, db);
|
||||||
|
const created = (await createRes.json()) as { entity: Entity };
|
||||||
|
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const res = await getEntityViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as { entity: Entity };
|
||||||
|
expect(body.entity.name).toBe("张三");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("不存在的实体返回 404", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities/nonexistent`, { method: "GET" });
|
||||||
|
const res = await getEntityViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PATCH /api/projects/:id/entities/:eid", () => {
|
||||||
|
test("更新实体名称", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const createRes = await createEntityViaHandler(createReq, db);
|
||||||
|
const created = (await createRes.json()) as { entity: Entity };
|
||||||
|
|
||||||
|
const req = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||||
|
body: JSON.stringify({ name: "张三丰" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PATCH",
|
||||||
|
});
|
||||||
|
const res = await updateEntityViaHandler(req, db);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as { entity: Entity };
|
||||||
|
expect(body.entity.name).toBe("张三丰");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DELETE /api/projects/:id/entities/:eid", () => {
|
||||||
|
test("软删除实体", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const project = createTestProject(db);
|
||||||
|
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||||
|
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const createRes = await createEntityViaHandler(createReq, db);
|
||||||
|
const created = (await createRes.json()) as { entity: Entity };
|
||||||
|
|
||||||
|
const deleteReq = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
const res = await deleteEntityViaHandler(deleteReq, db);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const getReq = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const getRes = await getEntityViaHandler(getReq, db);
|
||||||
|
expect(getRes.status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,13 @@
|
|||||||
* 噪声过滤对所有测试生效
|
* 噪声过滤对所有测试生效
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||||
process.stderr.write = (chunk: string | Uint8Array, encodingOrCb?: unknown, cb?: unknown) => {
|
process.stderr.write = (chunk: string | Uint8Array, encodingOrCb?: unknown, cb?: unknown) => {
|
||||||
const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
||||||
@@ -19,6 +26,7 @@ const originalConsoleError = console.error;
|
|||||||
console.error = (...args: unknown[]) => {
|
console.error = (...args: unknown[]) => {
|
||||||
const message = args.map(String).join(" ");
|
const message = args.map(String).join(" ");
|
||||||
if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return;
|
if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return;
|
||||||
|
if (message.includes("not wrapped in act")) return;
|
||||||
originalConsoleError(...args);
|
originalConsoleError(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +87,9 @@ globalThis.Selection = class Selection {
|
|||||||
} as unknown as typeof Selection;
|
} as unknown as typeof Selection;
|
||||||
|
|
||||||
const { afterEach } = await import("bun:test");
|
const { afterEach } = await import("bun:test");
|
||||||
|
const { cleanup } = await import("@testing-library/react");
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ function setupFetchMock() {
|
|||||||
if (call.url.includes("/models")) {
|
if (call.url.includes("/models")) {
|
||||||
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
||||||
}
|
}
|
||||||
|
if (call.url.includes("/messages")) {
|
||||||
|
return jsonResponse({ items: [], total: 0 });
|
||||||
|
}
|
||||||
|
if (call.method === "GET" && /\/conversations\/conv-/.exec(call.url)) {
|
||||||
|
return jsonResponse({ conversation: CONVERSATION });
|
||||||
|
}
|
||||||
if (call.url.includes("/conversations") && call.method === "GET") {
|
if (call.url.includes("/conversations") && call.method === "GET") {
|
||||||
return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 });
|
return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 });
|
||||||
}
|
}
|
||||||
@@ -63,12 +69,6 @@ function setupFetchMock() {
|
|||||||
if (call.method === "DELETE" && call.url.includes("/conversations/")) {
|
if (call.method === "DELETE" && call.url.includes("/conversations/")) {
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
}
|
}
|
||||||
if (call.url.includes("/messages")) {
|
|
||||||
return jsonResponse({ items: [], total: 0 });
|
|
||||||
}
|
|
||||||
if (/\/conversations\/conv-1$/.exec(call.url)) {
|
|
||||||
return jsonResponse({ conversation: CONVERSATION });
|
|
||||||
}
|
|
||||||
return jsonResponse({ error: "not found" }, { status: 404 });
|
return jsonResponse({ error: "not found" }, { status: 404 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -143,15 +143,18 @@ describe("ChatPage", () => {
|
|||||||
if (call.url.includes("/models")) {
|
if (call.url.includes("/models")) {
|
||||||
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
||||||
}
|
}
|
||||||
|
if (call.url.includes("/messages")) {
|
||||||
|
return jsonResponse({ items: [], total: 0 });
|
||||||
|
}
|
||||||
|
if (call.method === "GET" && /\/conversations\/conv-/.exec(call.url)) {
|
||||||
|
return jsonResponse({ conversation: CONVERSATION });
|
||||||
|
}
|
||||||
if (call.url.includes("/conversations") && call.method === "GET") {
|
if (call.url.includes("/conversations") && call.method === "GET") {
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return jsonResponse({ items: [], page: 1, pageSize: 200, total: 0 });
|
return jsonResponse({ items: [], page: 1, pageSize: 200, total: 0 });
|
||||||
}
|
}
|
||||||
return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 });
|
return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 });
|
||||||
}
|
}
|
||||||
if (call.url.includes("/messages")) {
|
|
||||||
return jsonResponse({ items: [], total: 0 });
|
|
||||||
}
|
|
||||||
return jsonResponse({ error: "not found" }, { status: 404 });
|
return jsonResponse({ error: "not found" }, { status: 404 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const DEEPSEEK_PROVIDER: Provider = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function clickLatestConfirmButton() {
|
function clickLatestConfirmButton() {
|
||||||
const buttons = screen.getAllByRole("button", { name: /OK|确定/ });
|
const buttons = screen.getAllByRole("button", { name: /OK|确\s*定/ });
|
||||||
fireEvent.click(buttons[buttons.length - 1]!);
|
fireEvent.click(buttons[buttons.length - 1]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,28 +149,37 @@ const TABLE_ACTION_TEST_CASES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe("ResourceTable", () => {
|
describe("ResourceTable", () => {
|
||||||
|
// Ant Design Table 在 happy-dom 中渲染较慢,并行测试时需要更多时间
|
||||||
for (const tc of TABLE_TEST_CASES) {
|
for (const tc of TABLE_TEST_CASES) {
|
||||||
test(`${tc.componentName} 渲染表格数据`, () => {
|
test(
|
||||||
tc.render();
|
`${tc.componentName} 渲染表格数据`,
|
||||||
tc.assertData();
|
() => {
|
||||||
tc.assertNoExtra();
|
tc.render();
|
||||||
});
|
tc.assertData();
|
||||||
|
tc.assertNoExtra();
|
||||||
|
},
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tc of TABLE_ACTION_TEST_CASES) {
|
for (const tc of TABLE_ACTION_TEST_CASES) {
|
||||||
test(`${tc.componentName} 表格操作触发 edit/delete`, async () => {
|
test(
|
||||||
const onDelete = mock(() => Promise.resolve());
|
`${tc.componentName} 表格操作触发 edit/delete`,
|
||||||
const onEdit = mock(() => undefined);
|
async () => {
|
||||||
|
const onDelete = mock(() => Promise.resolve());
|
||||||
|
const onEdit = mock(() => undefined);
|
||||||
|
|
||||||
tc.render({ onDelete, onEdit });
|
tc.render({ onDelete, onEdit });
|
||||||
|
|
||||||
fireEvent.click(screen.getAllByRole("button", { name: /编辑/ })[0]!);
|
fireEvent.click(screen.getAllByRole("button", { name: /编辑/ })[0]!);
|
||||||
expect(onEdit).toHaveBeenCalledWith(tc.editArg);
|
expect(onEdit).toHaveBeenCalledWith(tc.editArg);
|
||||||
|
|
||||||
fireEvent.click(screen.getAllByRole("button", { name: /删除/ })[0]!);
|
fireEvent.click(screen.getAllByRole("button", { name: /删除/ })[0]!);
|
||||||
await screen.findByText(tc.deleteConfirmText);
|
await screen.findByText(tc.deleteConfirmText);
|
||||||
clickLatestConfirmButton();
|
clickLatestConfirmButton();
|
||||||
await waitFor(() => expect(onDelete).toHaveBeenCalledWith(tc.deleteId));
|
await waitFor(() => expect(onDelete).toHaveBeenCalledWith(tc.deleteId));
|
||||||
});
|
},
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,10 +55,13 @@ describe("AddMaterialModal", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("确 定"));
|
fireEvent.click(screen.getByText("确 定"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(screen.getByText("请输入描述")).not.toBeNull();
|
() => {
|
||||||
});
|
expect(screen.getByText("请输入描述")).not.toBeNull();
|
||||||
});
|
},
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
test("点击确定触发表单提交", async () => {
|
test("点击确定触发表单提交", async () => {
|
||||||
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
|
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
|
||||||
@@ -76,16 +79,19 @@ describe("AddMaterialModal", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("确 定"));
|
fireEvent.click(screen.getByText("确 定"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
() => {
|
||||||
});
|
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
|
||||||
const callArgs = onAdd.mock.calls[0];
|
const callArgs = onAdd.mock.calls[0];
|
||||||
expect(callArgs).toBeDefined();
|
expect(callArgs).toBeDefined();
|
||||||
const calledBody = callArgs![0];
|
const calledBody = callArgs![0];
|
||||||
expect(calledBody.description).toBe("测试描述");
|
expect(calledBody.description).toBe("测试描述");
|
||||||
expect(calledBody.associatedDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
expect(calledBody.associatedDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("提交失败显示错误提示", async () => {
|
test("提交失败显示错误提示", async () => {
|
||||||
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
|
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
|
||||||
@@ -103,8 +109,11 @@ describe("AddMaterialModal", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("确 定"));
|
fireEvent.click(screen.getByText("确 定"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
() => {
|
||||||
});
|
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||||
});
|
},
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe("InboxPage", () => {
|
|||||||
const cards = screen.getAllByText("新增的素材");
|
const cards = screen.getAllByText("新增的素材");
|
||||||
expect(cards.length).toBeGreaterThanOrEqual(1);
|
expect(cards.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("删除素材后列表更新", async () => {
|
test("删除素材后列表更新", async () => {
|
||||||
let deleted = false;
|
let deleted = false;
|
||||||
@@ -131,5 +131,5 @@ describe("InboxPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("暂无素材")).not.toBeNull();
|
expect(screen.getByText("暂无素材")).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("ModelSettingsCard", () => {
|
|||||||
expect(screen.getByText("音频生成")).not.toBeNull();
|
expect(screen.getByText("音频生成")).not.toBeNull();
|
||||||
expect(screen.getByText("视频生成")).not.toBeNull();
|
expect(screen.getByText("视频生成")).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("回显已保存的默认模型值", async () => {
|
test("回显已保存的默认模型值", async () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
@@ -63,5 +63,5 @@ describe("ModelSettingsCard", () => {
|
|||||||
expect(screen.getByText("GPT-4")).not.toBeNull();
|
expect(screen.getByText("GPT-4")).not.toBeNull();
|
||||||
expect(screen.getByText("Claude Vision")).not.toBeNull();
|
expect(screen.getByText("Claude Vision")).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,34 +46,38 @@ function clickLatestConfirmButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ModelFormModal", () => {
|
describe("ModelFormModal", () => {
|
||||||
test("编辑模型表单只提交变更字段", async () => {
|
test(
|
||||||
const updateCalls: unknown[] = [];
|
"编辑模型表单只提交变更字段",
|
||||||
|
async () => {
|
||||||
|
const updateCalls: unknown[] = [];
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ModelFormModal, {
|
createElement(ModelFormModal, {
|
||||||
editingModel: ENABLED_MODEL,
|
editingModel: ENABLED_MODEL,
|
||||||
onCancel: () => undefined,
|
onCancel: () => undefined,
|
||||||
onCreate: () => Promise.resolve(),
|
onCreate: () => Promise.resolve(),
|
||||||
onOpenChange: () => undefined,
|
onOpenChange: () => undefined,
|
||||||
onUpdate: (args: unknown) => {
|
onUpdate: (args: unknown) => {
|
||||||
updateCalls.push(args);
|
updateCalls.push(args);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
open: true,
|
open: true,
|
||||||
providers: [ENABLED_PROVIDER, DISABLED_PROVIDER],
|
providers: [ENABLED_PROVIDER, DISABLED_PROVIDER],
|
||||||
providersError: null,
|
providersError: null,
|
||||||
providersLoading: false,
|
providersLoading: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screen.findByPlaceholderText("请输入模型名称");
|
await screen.findByPlaceholderText("请输入模型名称");
|
||||||
fireEvent.change(screen.getByPlaceholderText("请输入模型名称"), { target: { value: "GPT-4o Mini" } });
|
fireEvent.change(screen.getByPlaceholderText("请输入模型名称"), { target: { value: "GPT-4o Mini" } });
|
||||||
clickLatestConfirmButton();
|
clickLatestConfirmButton();
|
||||||
|
|
||||||
await waitFor(() => expect(updateCalls.length).toBe(1));
|
await waitFor(() => expect(updateCalls.length).toBe(1));
|
||||||
expect(updateCalls[0]).toEqual({ data: { name: "GPT-4o Mini" }, id: "m1" });
|
expect(updateCalls[0]).toEqual({ data: { name: "GPT-4o Mini" }, id: "m1" });
|
||||||
});
|
},
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
|
||||||
test("模型表单校验失败不会提交", async () => {
|
test("模型表单校验失败不会提交", async () => {
|
||||||
const onCreate = mock(() => Promise.resolve());
|
const onCreate = mock(() => Promise.resolve());
|
||||||
@@ -121,80 +125,92 @@ describe("ModelFormModal", () => {
|
|||||||
expect((reasoningCheckbox as { checked?: boolean }).checked).toBe(true);
|
expect((reasoningCheckbox as { checked?: boolean }).checked).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("新建模型展示供应商 options 列表", async () => {
|
test(
|
||||||
renderWithProviders(
|
"新建模型展示供应商 options 列表",
|
||||||
createElement(ModelFormModal, {
|
async () => {
|
||||||
editingModel: null,
|
renderWithProviders(
|
||||||
onCancel: () => undefined,
|
createElement(ModelFormModal, {
|
||||||
onCreate: () => Promise.resolve(),
|
editingModel: null,
|
||||||
onOpenChange: () => undefined,
|
onCancel: () => undefined,
|
||||||
onUpdate: () => Promise.resolve(),
|
onCreate: () => Promise.resolve(),
|
||||||
open: true,
|
onOpenChange: () => undefined,
|
||||||
providers: [ENABLED_PROVIDER, DISABLED_PROVIDER],
|
onUpdate: () => Promise.resolve(),
|
||||||
providersError: null,
|
open: true,
|
||||||
providersLoading: false,
|
providers: [ENABLED_PROVIDER, DISABLED_PROVIDER],
|
||||||
submitting: false,
|
providersError: null,
|
||||||
}),
|
providersLoading: false,
|
||||||
);
|
submitting: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await screen.findByPlaceholderText("请输入模型名称");
|
await screen.findByPlaceholderText("请输入模型名称");
|
||||||
fireEvent.mouseDown(screen.getByRole("combobox"));
|
fireEvent.mouseDown(screen.getByRole("combobox"));
|
||||||
|
|
||||||
expect(await screen.findByText("OpenAI")).not.toBeNull();
|
expect(await screen.findByText("OpenAI")).not.toBeNull();
|
||||||
expect(await screen.findByText("DeepSeek")).not.toBeNull();
|
expect(await screen.findByText("DeepSeek")).not.toBeNull();
|
||||||
});
|
},
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
|
||||||
test("供应商下拉展示加载错误提示", async () => {
|
test(
|
||||||
renderWithProviders(
|
"供应商下拉展示加载错误提示",
|
||||||
createElement(ModelFormModal, {
|
async () => {
|
||||||
editingModel: null,
|
renderWithProviders(
|
||||||
onCancel: () => undefined,
|
createElement(ModelFormModal, {
|
||||||
onCreate: () => Promise.resolve(),
|
editingModel: null,
|
||||||
onOpenChange: () => undefined,
|
onCancel: () => undefined,
|
||||||
onUpdate: () => Promise.resolve(),
|
onCreate: () => Promise.resolve(),
|
||||||
open: true,
|
onOpenChange: () => undefined,
|
||||||
providers: [],
|
onUpdate: () => Promise.resolve(),
|
||||||
providersError: new Error("options failed"),
|
open: true,
|
||||||
providersLoading: false,
|
providers: [],
|
||||||
submitting: false,
|
providersError: new Error("options failed"),
|
||||||
}),
|
providersLoading: false,
|
||||||
);
|
submitting: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await screen.findByPlaceholderText("请输入模型名称");
|
await screen.findByPlaceholderText("请输入模型名称");
|
||||||
fireEvent.mouseDown(screen.getByRole("combobox"));
|
fireEvent.mouseDown(screen.getByRole("combobox"));
|
||||||
|
|
||||||
expect(await screen.findByText("供应商加载失败:options failed")).not.toBeNull();
|
expect(await screen.findByText("供应商加载失败:options failed")).not.toBeNull();
|
||||||
});
|
},
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
|
||||||
test("编辑模型时可测试模型连接", async () => {
|
test(
|
||||||
const testModelConnection = mock(() => Promise.resolve({ message: "模型连接成功", ok: true }));
|
"编辑模型时可测试模型连接",
|
||||||
|
async () => {
|
||||||
|
const testModelConnection = mock(() => Promise.resolve({ message: "模型连接成功", ok: true }));
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
createElement(ModelFormModal, {
|
createElement(ModelFormModal, {
|
||||||
editingModel: ENABLED_MODEL,
|
editingModel: ENABLED_MODEL,
|
||||||
onCancel: () => undefined,
|
onCancel: () => undefined,
|
||||||
onCreate: () => Promise.resolve(),
|
onCreate: () => Promise.resolve(),
|
||||||
onOpenChange: () => undefined,
|
onOpenChange: () => undefined,
|
||||||
onUpdate: () => Promise.resolve(),
|
onUpdate: () => Promise.resolve(),
|
||||||
open: true,
|
open: true,
|
||||||
providers: [ENABLED_PROVIDER],
|
providers: [ENABLED_PROVIDER],
|
||||||
providersError: null,
|
providersError: null,
|
||||||
providersLoading: false,
|
providersLoading: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
testModelConnection,
|
testModelConnection,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screen.findByRole("button", { name: "测试连接" });
|
await screen.findByRole("button", { name: "测试连接" });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "测试连接" }));
|
fireEvent.click(screen.getByRole("button", { name: "测试连接" }));
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(testModelConnection).toHaveBeenCalledWith({
|
expect(testModelConnection).toHaveBeenCalledWith({
|
||||||
externalId: "gpt-4o",
|
externalId: "gpt-4o",
|
||||||
providerId: "pv1",
|
providerId: "pv1",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
|
||||||
test("新建模型也显示测试连接按钮", async () => {
|
test("新建模型也显示测试连接按钮", async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
@@ -300,7 +316,7 @@ describe("ModelListPage", () => {
|
|||||||
expect(screen.getByPlaceholderText("搜索模型名称或 ID")).not.toBeNull();
|
expect(screen.getByPlaceholderText("搜索模型名称或 ID")).not.toBeNull();
|
||||||
expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull();
|
expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull();
|
||||||
expect(calls.some((call) => call.url.includes("/api/models"))).toBe(true);
|
expect(calls.some((call) => call.url.includes("/api/models"))).toBe(true);
|
||||||
}, 15000);
|
}, 30000);
|
||||||
|
|
||||||
test("搜索模型更新请求参数", async () => {
|
test("搜索模型更新请求参数", async () => {
|
||||||
const calls = createModelFetchMock();
|
const calls = createModelFetchMock();
|
||||||
@@ -312,7 +328,7 @@ describe("ModelListPage", () => {
|
|||||||
fireEvent.change(input, { target: { value: "gpt" } });
|
fireEvent.change(input, { target: { value: "gpt" } });
|
||||||
fireEvent.keyDown(input, { key: "Enter" });
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=gpt"))).toBe(true));
|
await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=gpt"))).toBe(true));
|
||||||
}, 15000);
|
}, 30000);
|
||||||
|
|
||||||
test("新建模型弹窗可以打开", async () => {
|
test("新建模型弹窗可以打开", async () => {
|
||||||
createModelFetchMock();
|
createModelFetchMock();
|
||||||
@@ -322,5 +338,5 @@ describe("ModelListPage", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /新建模型/ }));
|
fireEvent.click(screen.getByRole("button", { name: /新建模型/ }));
|
||||||
await screen.findByPlaceholderText("请输入模型名称");
|
await screen.findByPlaceholderText("请输入模型名称");
|
||||||
}, 15000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe("ProjectsPage", () => {
|
|||||||
await waitFor(() => expect(calls.some((call) => call.url.includes("status=archived"))).toBe(true));
|
await waitFor(() => expect(calls.some((call) => call.url.includes("status=archived"))).toBe(true));
|
||||||
|
|
||||||
await screen.findByText("归档项目");
|
await screen.findByText("归档项目");
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("清空搜索条件复位请求参数并重新展示全部项目", async () => {
|
test("清空搜索条件复位请求参数并重新展示全部项目", async () => {
|
||||||
const calls = createProjectFetchMock();
|
const calls = createProjectFetchMock();
|
||||||
@@ -190,7 +190,7 @@ describe("ProjectsPage", () => {
|
|||||||
const createCall = calls.find((call) => call.url.endsWith("/api/projects") && call.method === "POST");
|
const createCall = calls.find((call) => call.url.endsWith("/api/projects") && call.method === "POST");
|
||||||
expect(createCall).toBeDefined();
|
expect(createCall).toBeDefined();
|
||||||
expect(jsonBody(createCall?.body)).toEqual({ description: "新增描述", name: "新增项目" });
|
expect(jsonBody(createCall?.body)).toEqual({ description: "新增描述", name: "新增项目" });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("编辑项目表单只提交变更字段", async () => {
|
test("编辑项目表单只提交变更字段", async () => {
|
||||||
const updateCalls: unknown[] = [];
|
const updateCalls: unknown[] = [];
|
||||||
@@ -217,7 +217,7 @@ describe("ProjectsPage", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(onUpdate).toHaveBeenCalled());
|
await waitFor(() => expect(onUpdate).toHaveBeenCalled());
|
||||||
expect(updateCalls[0]).toEqual({ data: { name: "编辑项目" }, id: "p1" });
|
expect(updateCalls[0]).toEqual({ data: { name: "编辑项目" }, id: "p1" });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("项目表单校验失败不会提交,接口失败时保留弹窗", async () => {
|
test("项目表单校验失败不会提交,接口失败时保留弹窗", async () => {
|
||||||
const onCreate = mock(() => Promise.reject(new Error("创建失败")));
|
const onCreate = mock(() => Promise.reject(new Error("创建失败")));
|
||||||
@@ -244,7 +244,7 @@ describe("ProjectsPage", () => {
|
|||||||
await waitFor(() => expect(onCreate).toHaveBeenCalled());
|
await waitFor(() => expect(onCreate).toHaveBeenCalled());
|
||||||
expect(onOpenChange).not.toHaveBeenCalledWith(false);
|
expect(onOpenChange).not.toHaveBeenCalledWith(false);
|
||||||
expect(screen.getByText("新建项目")).not.toBeNull();
|
expect(screen.getByText("新建项目")).not.toBeNull();
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("项目表格操作触发导航和行级动作", async () => {
|
test("项目表格操作触发导航和行级动作", async () => {
|
||||||
const onArchive = mock(() => Promise.resolve());
|
const onArchive = mock(() => Promise.resolve());
|
||||||
@@ -287,5 +287,5 @@ describe("ProjectsPage", () => {
|
|||||||
await screen.findByText("确认永久删除此项目?");
|
await screen.findByText("确认永久删除此项目?");
|
||||||
await clickLatestConfirmButton();
|
await clickLatestConfirmButton();
|
||||||
await waitFor(() => expect(onDelete).toHaveBeenCalledWith("p2"));
|
await waitFor(() => expect(onDelete).toHaveBeenCalledWith("p2"));
|
||||||
}, 15000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("ProviderFormModal", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(updateCalls.length).toBe(1));
|
await waitFor(() => expect(updateCalls.length).toBe(1));
|
||||||
expect(updateCalls[0]).toEqual({ data: { name: "New OpenAI" }, id: "pv1" });
|
expect(updateCalls[0]).toEqual({ data: { name: "New OpenAI" }, id: "pv1" });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("新建供应商默认使用 openai-compatible 类型", async () => {
|
test("新建供应商默认使用 openai-compatible 类型", async () => {
|
||||||
const createCalls: unknown[] = [];
|
const createCalls: unknown[] = [];
|
||||||
@@ -85,7 +85,7 @@ describe("ProviderFormModal", () => {
|
|||||||
name: "兼容供应商",
|
name: "兼容供应商",
|
||||||
type: "openai-compatible",
|
type: "openai-compatible",
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("供应商表单可使用当前表单配置测试连接", async () => {
|
test("供应商表单可使用当前表单配置测试连接", async () => {
|
||||||
const testCalls: unknown[] = [];
|
const testCalls: unknown[] = [];
|
||||||
@@ -121,7 +121,7 @@ describe("ProviderFormModal", () => {
|
|||||||
name: "兼容供应商",
|
name: "兼容供应商",
|
||||||
type: "openai-compatible",
|
type: "openai-compatible",
|
||||||
});
|
});
|
||||||
});
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
const TEST_PROVIDER: Provider = {
|
const TEST_PROVIDER: Provider = {
|
||||||
@@ -197,7 +197,7 @@ describe("ProviderListPage", () => {
|
|||||||
expect(screen.getByPlaceholderText("搜索供应商名称")).not.toBeNull();
|
expect(screen.getByPlaceholderText("搜索供应商名称")).not.toBeNull();
|
||||||
expect(screen.getByRole("button", { name: /新建供应商/ })).not.toBeNull();
|
expect(screen.getByRole("button", { name: /新建供应商/ })).not.toBeNull();
|
||||||
expect(calls.some((call) => call.url.includes("/api/providers"))).toBe(true);
|
expect(calls.some((call) => call.url.includes("/api/providers"))).toBe(true);
|
||||||
}, 15000);
|
}, 30000);
|
||||||
|
|
||||||
test("搜索供应商更新请求参数", async () => {
|
test("搜索供应商更新请求参数", async () => {
|
||||||
const calls = createProviderFetchMock();
|
const calls = createProviderFetchMock();
|
||||||
@@ -209,7 +209,7 @@ describe("ProviderListPage", () => {
|
|||||||
fireEvent.change(input, { target: { value: "Open" } });
|
fireEvent.change(input, { target: { value: "Open" } });
|
||||||
fireEvent.keyDown(input, { key: "Enter" });
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=Open"))).toBe(true));
|
await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=Open"))).toBe(true));
|
||||||
}, 15000);
|
}, 30000);
|
||||||
|
|
||||||
test("新建供应商弹窗可以打开", async () => {
|
test("新建供应商弹窗可以打开", async () => {
|
||||||
createProviderFetchMock();
|
createProviderFetchMock();
|
||||||
@@ -219,5 +219,5 @@ describe("ProviderListPage", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /新建供应商/ }));
|
fireEvent.click(screen.getByRole("button", { name: /新建供应商/ }));
|
||||||
await screen.findByPlaceholderText("请输入供应商名称");
|
await screen.findByPlaceholderText("请输入供应商名称");
|
||||||
}, 15000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("Workbench 路由", () => {
|
|||||||
},
|
},
|
||||||
{ timeout: 10000 },
|
{ timeout: 10000 },
|
||||||
);
|
);
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("Workbench 显示返回管理台按钮", async () => {
|
test("Workbench 显示返回管理台按钮", async () => {
|
||||||
createMockHandler();
|
createMockHandler();
|
||||||
@@ -75,7 +75,7 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("返回管理台", {}, { timeout: 10000 });
|
await screen.findByText("返回管理台", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("不存在项目显示不可访问", async () => {
|
test("不存在项目显示不可访问", async () => {
|
||||||
createMockHandler();
|
createMockHandler();
|
||||||
@@ -85,7 +85,7 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
|
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("archived 项目显示不可访问", async () => {
|
test("archived 项目显示不可访问", async () => {
|
||||||
createMockHandler({ status: "archived" });
|
createMockHandler({ status: "archived" });
|
||||||
@@ -95,7 +95,7 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
|
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("Workbench 显示聊天室菜单", async () => {
|
test("Workbench 显示聊天室菜单", async () => {
|
||||||
createMockHandler();
|
createMockHandler();
|
||||||
@@ -105,7 +105,7 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("聊天室", {}, { timeout: 10000 });
|
await screen.findByText("聊天室", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("Workbench 收集箱路由可达", async () => {
|
test("Workbench 收集箱路由可达", async () => {
|
||||||
createMockHandler();
|
createMockHandler();
|
||||||
@@ -115,7 +115,7 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("新增素材", {}, { timeout: 10000 });
|
await screen.findByText("新增素材", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
test("Workbench 显示收集箱菜单", async () => {
|
test("Workbench 显示收集箱菜单", async () => {
|
||||||
createMockHandler();
|
createMockHandler();
|
||||||
@@ -125,5 +125,5 @@ describe("Workbench 路由", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText("收集箱", {}, { timeout: 10000 });
|
await screen.findByText("收集箱", {}, { timeout: 10000 });
|
||||||
});
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,32 +24,10 @@ describe("getDateGroup", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("本周内的日期返回 thisWeek", () => {
|
test("本周内的日期返回 thisWeek", () => {
|
||||||
const now = new Date();
|
const now = new Date(2026, 5, 10);
|
||||||
const dayOfWeek = now.getDay();
|
const monday = new Date(2026, 5, 8);
|
||||||
const mondayOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
const result = getDateGroup(monday.toISOString(), now);
|
||||||
const wednesday = new Date(now);
|
expect(result).toBe("thisWeek");
|
||||||
wednesday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) + 2);
|
|
||||||
if (wednesday > now) {
|
|
||||||
const tuesday = new Date(now);
|
|
||||||
tuesday.setDate(now.getDate() - mondayOffset + 1);
|
|
||||||
if (tuesday < now && tuesday.getDate() !== now.getDate() - 1) {
|
|
||||||
const result = getDateGroup(tuesday.toISOString(), now);
|
|
||||||
expect(result).toBe("thisWeek");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tuesday = new Date(now);
|
|
||||||
tuesday.setDate(now.getDate() - mondayOffset + 1);
|
|
||||||
if (tuesday.toDateString() !== now.toDateString()) {
|
|
||||||
const yesterday = new Date(now);
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
if (tuesday.toDateString() !== yesterday.toDateString()) {
|
|
||||||
const result = getDateGroup(tuesday.toISOString(), now);
|
|
||||||
expect(result).toBe("thisWeek");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("本月内的日期返回 thisMonth", () => {
|
test("本月内的日期返回 thisMonth", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user