Compare commits
17 Commits
f83f434863
...
ab7b7fb189
| Author | SHA1 | Date | |
|---|---|---|---|
| ab7b7fb189 | |||
| 0d60120219 | |||
| 459cc76edf | |||
| 844562303c | |||
| df5b60eb53 | |||
| b3f77e8ac6 | |||
| b225b0a0c7 | |||
| de51a817fb | |||
| 4c72754739 | |||
| d765f86b65 | |||
| 60843f7dbf | |||
| 897fad95eb | |||
| f34028368d | |||
| 8463274c4b | |||
| f2e3d84fb1 | |||
| 3e1f3b554d | |||
| 6eeb4ced7b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -415,6 +415,7 @@ data/
|
||||
backend/bin
|
||||
backend/server
|
||||
backend/desktop
|
||||
!src/**/*
|
||||
|
||||
# Embedfs generated
|
||||
embedfs/assets/
|
||||
|
||||
245
bin/init-dev-branch.js
Normal file
245
bin/init-dev-branch.js
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 开发分支工作区初始化脚本
|
||||
*
|
||||
* 用于创建基于远端分支或新建的开发分支工作区。
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { relative, resolve } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
function addWorktree(name, dir, base) {
|
||||
const args = ["worktree", "add", "-b", name, dir];
|
||||
if (base) args.push(base);
|
||||
git(args);
|
||||
}
|
||||
|
||||
function addWorktreeSafe(name, dir, base) {
|
||||
assertCanCreate(name, dir);
|
||||
addWorktree(name, dir, base);
|
||||
console.log(`工作区已创建于 ${dir}`);
|
||||
}
|
||||
|
||||
function ask(rl, prompt) {
|
||||
return new Promise((resolve) => rl.question(prompt, resolve));
|
||||
}
|
||||
|
||||
function assertCanCreate(name, dir) {
|
||||
if (existsSync(dir)) {
|
||||
throw new Error(`工作区已存在于 ${dir}`);
|
||||
}
|
||||
if (worktreeExists(dir)) {
|
||||
throw new Error(`工作区 '${name}' 已存在`);
|
||||
}
|
||||
if (localBranchExists(name)) {
|
||||
throw new Error(`本地分支 '${name}' 已存在`);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchRemote() {
|
||||
try {
|
||||
git(["fetch", "--quiet"]);
|
||||
} catch {
|
||||
console.warn("警告: 无法获取远端信息,继续使用本地数据");
|
||||
}
|
||||
}
|
||||
|
||||
function getRootDir() {
|
||||
try {
|
||||
return resolve(git(["rev-parse", "--show-toplevel"]).trim());
|
||||
} catch {
|
||||
console.error("错误: 不在 git 仓库中");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function git(args, opts) {
|
||||
return execFileSync("git", args, { encoding: "utf-8", stdio: "pipe", ...opts });
|
||||
}
|
||||
|
||||
async function handleWithName(name, worktreesDir) {
|
||||
const dir = resolve(worktreesDir, name);
|
||||
|
||||
try {
|
||||
assertCanCreate(name, dir);
|
||||
} catch (e) {
|
||||
console.error(`错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const remotes = matchingRemoteBranches(name);
|
||||
|
||||
try {
|
||||
if (remotes.length > 0) {
|
||||
const base = await selectFromList(remotes, "找到远端分支:", true);
|
||||
addWorktree(name, dir, base ?? undefined);
|
||||
} else {
|
||||
console.log("未找到远端分支,创建新分支");
|
||||
addWorktree(name, dir);
|
||||
}
|
||||
console.log(`工作区已创建于 ${dir}`);
|
||||
} catch (e) {
|
||||
console.error(`错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithoutName(worktreesDir) {
|
||||
const remotes = listRemoteBranches();
|
||||
|
||||
if (remotes.length === 0) {
|
||||
console.log("未找到远端分支");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const selected = await selectFromList(remotes, "远端分支列表:", true);
|
||||
|
||||
if (selected) {
|
||||
const name = shortBranchName(selected);
|
||||
const dir = resolve(worktreesDir, name);
|
||||
try {
|
||||
addWorktreeSafe(name, dir, selected);
|
||||
} catch (e) {
|
||||
console.error(`错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const name = await inputBranchName();
|
||||
const dir = resolve(worktreesDir, name);
|
||||
try {
|
||||
addWorktreeSafe(name, dir);
|
||||
} catch (e) {
|
||||
console.error(`错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function inputBranchName() {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let cancelled = false;
|
||||
rl.on("close", () => {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const name = (await ask(rl, "请输入新分支名称: ")).trim();
|
||||
if (cancelled) {
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
if (name) {
|
||||
rl.close();
|
||||
return name;
|
||||
}
|
||||
console.log("错误: 分支名称不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
function listRemoteBranches() {
|
||||
try {
|
||||
return git(["branch", "-r"])
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.includes(" -> "));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function localBranchExists(name) {
|
||||
try {
|
||||
git(["show-ref", "--verify", "--quiet", `refs/heads/${name}`]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function matchingRemoteBranches(name) {
|
||||
return listRemoteBranches().filter((l) => l.endsWith(`/${name}`));
|
||||
}
|
||||
|
||||
async function selectFromList(items, prompt, allowCreate) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
console.log(prompt);
|
||||
items.forEach((item, i) => console.log(` ${i + 1}\t${item}`));
|
||||
if (allowCreate) console.log(` ${items.length + 1}\t创建新分支`);
|
||||
console.log();
|
||||
|
||||
const max = allowCreate ? items.length + 1 : items.length;
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let cancelled = false;
|
||||
rl.on("close", () => {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const raw = await ask(rl, `请选择 (1-${max}): `);
|
||||
if (cancelled) {
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
const n = Number.parseInt(raw, 10);
|
||||
if (Number.isNaN(n) || n < 1 || n > max) {
|
||||
console.log(`错误: 请输入 1-${max} 之间的数字`);
|
||||
continue;
|
||||
}
|
||||
if (n <= items.length) {
|
||||
const sel = items[n - 1];
|
||||
console.log(`已选择: ${sel}`);
|
||||
rl.close();
|
||||
return sel;
|
||||
}
|
||||
rl.close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shortBranchName(remoteRef) {
|
||||
const idx = remoteRef.indexOf("/");
|
||||
return idx === -1 ? remoteRef : remoteRef.slice(idx + 1);
|
||||
}
|
||||
|
||||
function worktreeExists(worktreeDir) {
|
||||
try {
|
||||
const out = git(["worktree", "list"]);
|
||||
const target = resolve(worktreeDir);
|
||||
return out.split(/\r?\n/).some((line) => {
|
||||
const fields = line.trim().split(/\s+/);
|
||||
return fields.length > 0 && resolve(fields[0]) === target;
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => process.exit(1));
|
||||
|
||||
async function main() {
|
||||
const branchName = process.argv[2];
|
||||
|
||||
const rootDir = getRootDir();
|
||||
const worktreesDir = resolve(rootDir, ".worktrees");
|
||||
mkdirSync(worktreesDir, { recursive: true });
|
||||
|
||||
console.log("正在从远端获取最新分支信息...");
|
||||
fetchRemote();
|
||||
|
||||
if (branchName) {
|
||||
await handleWithName(branchName, worktreesDir);
|
||||
} else {
|
||||
await handleWithoutName(worktreesDir);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(`错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
419
bun.lock
419
bun.lock
@@ -10,7 +10,6 @@
|
||||
"@ai-sdk/openai-compatible": "^2.0.48",
|
||||
"@ai-sdk/react": "^3.0.195",
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@ant-design/x": "^2.7.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@tanstack/react-query": "^5.100.14",
|
||||
"ai": "^6.0.193",
|
||||
@@ -25,6 +24,8 @@
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router": "^7.15.1",
|
||||
"recharts": "^3.8.1",
|
||||
"streamdown": "^2.5.0",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^21.0.1",
|
||||
@@ -85,9 +86,7 @@
|
||||
|
||||
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
|
||||
|
||||
"@ant-design/x": ["@ant-design/x@2.7.0", "https://registry.npmmirror.com/@ant-design/x/-/x-2.7.0.tgz", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/cssinjs": "^2.0.1", "@ant-design/cssinjs-utils": "^2.0.2", "@ant-design/fast-color": "^3.0.0", "@ant-design/icons": "^6.0.0", "@babel/runtime": "^7.25.6", "@rc-component/motion": "^1.1.6", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "lodash.throttle": "^4.1.1", "mermaid": "^11.12.1", "react-syntax-highlighter": "^16.1.0" }, "peerDependencies": { "antd": "^6.1.1", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-p5OtxQ9elbmeFRllGt1yj5wi6VHe41PIAmwrBU/OlaYydru5qIYsJzCS3DPRhkWkVdErU5oZwU74Z2oce2F5Uw=="],
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
|
||||
|
||||
@@ -131,11 +130,11 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@bramus/specificity": ["@bramus/specificity@2.4.2", "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||
|
||||
"@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", "", {}, "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=="],
|
||||
|
||||
@@ -281,9 +280,9 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@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", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@iconify/utils": ["@iconify/utils@3.1.3", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.3.tgz", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
|
||||
"@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
@@ -295,7 +294,7 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.1.1.tgz", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
@@ -455,75 +454,79 @@
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/d3": ["@types/d3@7.4.3", "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-axis": ["@types/d3-axis@3.0.6", "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||
|
||||
"@types/d3-brush": ["@types/d3-brush@3.0.6", "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||
|
||||
"@types/d3-chord": ["@types/d3-chord@3.0.6", "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-contour": ["@types/d3-contour@3.0.6", "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||
|
||||
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
|
||||
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||
|
||||
"@types/d3-drag": ["@types/d3-drag@3.0.7", "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||
|
||||
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||
|
||||
"@types/d3-force": ["@types/d3-force@3.0.10", "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||
|
||||
"@types/d3-format": ["@types/d3-format@3.0.4", "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.0", "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||
|
||||
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||
|
||||
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||
|
||||
"@types/d3-random": ["@types/d3-random@3.0.3", "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
|
||||
"@types/d3-selection": ["@types/d3-selection@3.0.11", "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.8", "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/d3-transition": ["@types/d3-transition@3.0.9", "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||
|
||||
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/jsdom": ["@types/jsdom@28.0.3", "https://registry.npmmirror.com/@types/jsdom/-/jsdom-28.0.3.tgz", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="],
|
||||
|
||||
@@ -531,9 +534,11 @@
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.6.2", "https://registry.npmmirror.com/@types/node/-/node-25.6.2.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@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/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.6.2", "https://registry.npmmirror.com/@types/node/-/node-25.6.2.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -541,9 +546,9 @@
|
||||
|
||||
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "https://registry.npmmirror.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
@@ -567,6 +572,8 @@
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
|
||||
@@ -605,7 +612,7 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.2.0.tgz", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
@@ -651,6 +658,8 @@
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="],
|
||||
@@ -675,11 +684,15 @@
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001792", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
@@ -691,9 +704,9 @@
|
||||
|
||||
"colorette": ["colorette@2.0.20", "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"compare-func": ["compare-func@2.0.0", "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
||||
|
||||
@@ -711,7 +724,7 @@
|
||||
|
||||
"cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cose-base": ["cose-base@1.0.3", "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@9.0.1", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="],
|
||||
|
||||
@@ -723,63 +736,63 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"cytoscape": ["cytoscape@3.33.4", "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.4.tgz", {}, "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww=="],
|
||||
"cytoscape": ["cytoscape@3.33.4", "", {}, "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww=="],
|
||||
|
||||
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
|
||||
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
|
||||
|
||||
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
|
||||
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
|
||||
|
||||
"d3": ["d3@7.9.0", "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-axis": ["d3-axis@3.0.0", "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||
|
||||
"d3-brush": ["d3-brush@3.0.0", "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||
|
||||
"d3-chord": ["d3-chord@3.0.1", "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-contour": ["d3-contour@4.0.2", "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||
|
||||
"d3-delaunay": ["d3-delaunay@6.0.4", "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
|
||||
"d3-dispatch": ["d3-dispatch@3.0.1", "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||
|
||||
"d3-drag": ["d3-drag@3.0.0", "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||
|
||||
"d3-dsv": ["d3-dsv@3.0.1", "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-fetch": ["d3-fetch@3.0.1", "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||
|
||||
"d3-force": ["d3-force@3.0.0", "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-geo": ["d3-geo@3.1.1", "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
|
||||
"d3-hierarchy": ["d3-hierarchy@3.1.2", "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-polygon": ["d3-polygon@3.0.1", "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
|
||||
"d3-quadtree": ["d3-quadtree@3.0.1", "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||
|
||||
"d3-random": ["d3-random@3.0.1", "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||
|
||||
"d3-sankey": ["d3-sankey@0.12.3", "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
|
||||
"d3-selection": ["d3-selection@3.0.0", "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
@@ -789,11 +802,11 @@
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"d3-transition": ["d3-transition@3.0.1", "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||
|
||||
"d3-zoom": ["d3-zoom@3.0.0", "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||
|
||||
"dagre-d3-es": ["dagre-d3-es@7.0.14", "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
|
||||
"dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
|
||||
|
||||
"data-urls": ["data-urls@7.0.0", "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||
|
||||
@@ -815,7 +828,7 @@
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
@@ -823,17 +836,19 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"delaunator": ["delaunator@5.1.0", "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"dompurify": ["dompurify@3.4.7", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.7.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="],
|
||||
"dompurify": ["dompurify@3.4.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="],
|
||||
|
||||
"dot-prop": ["dot-prop@5.3.0", "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
|
||||
|
||||
@@ -913,12 +928,16 @@
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.1.0", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-copy": ["fast-copy@4.0.3", "https://registry.npmmirror.com/fast-copy/-/fast-copy-4.0.3.tgz", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -933,8 +952,6 @@
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"fault": ["fault@1.0.4", "https://registry.npmmirror.com/fault/-/fault-1.0.4.tgz", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
@@ -947,8 +964,6 @@
|
||||
|
||||
"for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"format": ["format@0.2.2", "https://registry.npmmirror.com/format/-/format-0.2.2.tgz", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@@ -983,7 +998,7 @@
|
||||
|
||||
"gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"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", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
@@ -997,9 +1012,21 @@
|
||||
|
||||
"hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||
|
||||
"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-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||
|
||||
"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
@@ -1007,15 +1034,15 @@
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
|
||||
"highlight.js": ["highlight.js@10.7.3", "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
|
||||
|
||||
"highlightjs-vue": ["highlightjs-vue@1.0.0", "https://registry.npmmirror.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"husky": ["husky@9.1.7", "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
@@ -1023,19 +1050,21 @@
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"ini": ["ini@6.0.0", "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
@@ -1057,7 +1086,7 @@
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
@@ -1069,7 +1098,7 @@
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
@@ -1133,13 +1162,13 @@
|
||||
|
||||
"json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"katex": ["katex@0.16.47", "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
||||
"katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||
|
||||
"layout-base": ["layout-base@1.0.2", "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
@@ -1175,27 +1204,113 @@
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
|
||||
|
||||
"lodash.throttle": ["lodash.throttle@4.1.1", "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
|
||||
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
|
||||
|
||||
"log-update": ["log-update@6.1.0", "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
|
||||
|
||||
"lowlight": ["lowlight@1.20.0", "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.3.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"marked": ["marked@16.4.2", "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
|
||||
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"meow": ["meow@13.2.0", "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
|
||||
|
||||
"mermaid": ["mermaid@11.15.0", "https://registry.npmmirror.com/mermaid/-/mermaid-11.15.0.tgz", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
|
||||
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
@@ -1245,17 +1360,17 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "https://registry.npmmirror.com/parse-entities/-/parse-entities-4.0.2.tgz", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"parse5": ["parse5@8.0.1", "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
|
||||
|
||||
"path-data-parser": ["path-data-parser@0.1.0", "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
@@ -1277,9 +1392,9 @@
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@7.1.0", "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||
|
||||
"points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||
|
||||
"points-on-path": ["points-on-path@0.2.1", "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
@@ -1293,11 +1408,9 @@
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"process-warning": ["process-warning@5.0.0", "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
@@ -1315,8 +1428,6 @@
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"recharts": ["recharts@3.8.1", "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
||||
@@ -1327,10 +1438,24 @@
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"refractor": ["refractor@5.0.0", "https://registry.npmmirror.com/refractor/-/refractor-5.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
|
||||
|
||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||
|
||||
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
@@ -1345,13 +1470,13 @@
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"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", "", {}, "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=="],
|
||||
|
||||
"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", "", { "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=="],
|
||||
|
||||
"rw": ["rw@1.3.3", "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
||||
|
||||
@@ -1361,7 +1486,7 @@
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"saxes": ["saxes@6.0.0", "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
@@ -1405,7 +1530,7 @@
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
@@ -1413,6 +1538,8 @@
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="],
|
||||
|
||||
"string-argv": ["string-argv@0.3.2", "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
|
||||
|
||||
"string-convert": ["string-convert@0.2.1", "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
|
||||
@@ -1425,12 +1552,18 @@
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@5.0.3", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"stylis": ["stylis@4.4.0", "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
@@ -1441,6 +1574,8 @@
|
||||
|
||||
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||
|
||||
"thread-stream": ["thread-stream@4.2.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-4.2.0.tgz", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
|
||||
|
||||
"throttle-debounce": ["throttle-debounce@5.0.2", "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
|
||||
@@ -1461,9 +1596,13 @@
|
||||
|
||||
"tr46": ["tr46@6.0.0", "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||
|
||||
@@ -1491,6 +1630,18 @@
|
||||
|
||||
"undici-types": ["undici-types@7.25.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.25.0.tgz", {}, "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
@@ -1499,7 +1650,13 @@
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"uuid": ["uuid@14.0.0", "https://registry.npmmirror.com/uuid/-/uuid-14.0.0.tgz", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1507,6 +1664,8 @@
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@8.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
||||
@@ -1549,6 +1708,8 @@
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
@@ -1603,13 +1764,13 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||
|
||||
"d3-dsv/commander": ["commander@7.2.0", "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"d3-sankey/d3-array": ["d3-array@2.12.1", "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
|
||||
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||
|
||||
"eslint/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
@@ -1621,6 +1782,8 @@
|
||||
|
||||
"eslint-plugin-import/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
|
||||
@@ -1629,7 +1792,11 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@17.0.2", "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
@@ -1709,16 +1876,18 @@
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
|
||||
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||
|
||||
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
|
||||
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
|
||||
|
||||
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
@@ -32,9 +32,9 @@ docs/
|
||||
## 入口文档
|
||||
|
||||
| 入口 | 定位 |
|
||||
| --------------------------------- | -------------------------------------- |
|
||||
| --------------------------------- | -------------------------------------------------------- |
|
||||
| [项目 README](../README.md) | 项目整体介绍、快速开始、文档引导 |
|
||||
| [开发文档](development/README.md) | 开发入口、全局规则、常用命令、质量门禁 |
|
||||
| [开发文档](development/README.md) | 开发入口、**全部开发规范**、常用命令、质量门禁、专题索引 |
|
||||
| [用户文档](user/README.md) | 使用、配置、部署、排障入口 |
|
||||
|
||||
## 按任务阅读路径
|
||||
@@ -55,13 +55,14 @@ docs/
|
||||
## 文档归属矩阵
|
||||
|
||||
| 变更类型 | 默认更新位置 |
|
||||
| --------------------------------------------------------- | ---------------------------------------- |
|
||||
| -------------------------------------------------------- | ---------------------------------------- |
|
||||
| 项目定位、核心能力、快速开始、顶层文档导航 | `README.md` |
|
||||
| 文档路由、文档更新规则、文档归属矩阵 | `docs/README.md`、`openspec/config.yaml` |
|
||||
| 开发入口、常用命令、质量门禁、全局工程规则、OpenSpec 约定 | `docs/development/README.md` |
|
||||
| 开发规范(全局规则、库优先级、类型规范、样式、测试等) | `docs/development/README.md` |
|
||||
| 开发入口、常用命令、质量门禁、目录边界、OpenSpec 约定 | `docs/development/README.md` |
|
||||
| 架构边界、启动流程、运行时流程、前后端边界 | `docs/development/architecture.md` |
|
||||
| 后端 API、配置加载、logger、helpers、类型规范、后端测试 | `docs/development/backend.md` |
|
||||
| 前端技术栈、组件、样式、数据层、前端测试 | `docs/development/frontend.md` |
|
||||
| 后端模块 API、工具函数索引、数据库 schema、AI 层实现 | `docs/development/backend.md` |
|
||||
| 前端运行时代码结构、组件索引、页面组成、hooks/工具清单 | `docs/development/frontend.md` |
|
||||
| 构建、发布、脚本、前后端静态资源集成 | `docs/development/release.md` |
|
||||
| 快速开始、安装配置 | `docs/user/usage.md` |
|
||||
| YAML 配置、变量语法、server/storage/logging、JSON Schema | `docs/user/config.md` |
|
||||
@@ -73,9 +74,10 @@ docs/
|
||||
开发文档解释"如何实现和维护"。代码变更影响开发者理解、开发流程、测试方式或架构边界时,必须更新 `docs/development/` 对应文档。
|
||||
|
||||
- 全局规则、常用命令、质量门禁、目录边界、OpenSpec 约定更新到 `docs/development/README.md`。
|
||||
- **全部开发规范**(全局规则、后端规范、前端通用规范、样式规范、测试规范)统一维护在 `docs/development/README.md`。
|
||||
- 架构图、启动链路、运行时流程、前后端边界更新到 `docs/development/architecture.md`。
|
||||
- 后端 API、配置加载、logger、helpers、类型规范和后端测试规范更新到 `docs/development/backend.md`。
|
||||
- 前端技术栈、组件边界、数据流、样式规则和前端测试规范更新到 `docs/development/frontend.md`。
|
||||
- 后端模块 API、工具函数索引、数据库 schema、AI 层实现、日志模块更新到 `docs/development/backend.md`。
|
||||
- 前端运行时代码结构、组件索引、页面组成、hooks/工具清单更新到 `docs/development/frontend.md`。
|
||||
- 构建、脚本和发布验证更新到 `docs/development/release.md`。
|
||||
- 不新增"杂项"开发文档;优先把内容放入上述最贴近的专题,确需新增专题时先更新本文档和 `openspec/config.yaml`。
|
||||
|
||||
|
||||
@@ -1,120 +1,261 @@
|
||||
# 开发文档
|
||||
# 开发规范
|
||||
|
||||
本文档是 alfred 的开发入口。AI 工具和开发者应先阅读 [`../README.md`](../README.md) 判断文档归属,再阅读本文和最小必要专题。
|
||||
AI 工具必须严格遵守以下全部约束。
|
||||
|
||||
适用场景:修改源码、测试、构建脚本、开发流程、架构边界或项目工程规则。
|
||||
|
||||
## 专题索引
|
||||
## 专题文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
| ---------------------------------- | ---------------------------------------------------------------- |
|
||||
| [architecture.md](architecture.md) | 项目结构、启动流程、运行时流程、HTTP 请求流程、前后端边界 |
|
||||
| [backend.md](backend.md) | 后端库优先级、API 路由、共享工具、类型规范、配置契约、日志、测试 |
|
||||
| [frontend.md](frontend.md) | React、Ant Design、TanStack Query、组件、样式和前端测试规范 |
|
||||
| [release.md](release.md) | 开发服务、前后端集成、构建、脚本、环境变量 |
|
||||
| [../README.md](../README.md) | 文档路由、文档归属矩阵、development/user 文档更新规则 |
|
||||
| ---------------------------------- | ---------------------------------- |
|
||||
| [architecture.md](architecture.md) | 项目结构、启动流程、前后端边界 |
|
||||
| [backend.md](backend.md) | 模块 API、数据访问函数、AI 层说明 |
|
||||
| [frontend.md](frontend.md) | 组件索引、页面组成、hooks/工具清单 |
|
||||
| [release.md](release.md) | 开发服务、构建、脚本、环境变量 |
|
||||
|
||||
---
|
||||
|
||||
## 一、全局规则
|
||||
|
||||
### 语言与环境
|
||||
|
||||
- 使用中文编写注释、文档和交流内容。
|
||||
- 仅使用 bun 作为包管理器和 bunx 作为工具运行器;禁止 npm、pnpm、yarn、npx、pnpx。
|
||||
- 无需考虑向前兼容。
|
||||
|
||||
### 依赖引入
|
||||
|
||||
**后端**优先级(上层已有方案则不得引入新依赖):
|
||||
|
||||
1. Bun 内置 API(Bun.serve、bun:sqlite 等)
|
||||
2. es-toolkit
|
||||
3. 标准 Web API(fetch、Headers 等)
|
||||
4. 已批准三方库:pino、@sinclair/typebox、ajv、drizzle-orm、ai、@ai-sdk/\*
|
||||
5. 自行实现(仅以上都不满足时)
|
||||
|
||||
**前端**:优先复用已有组件/hooks/依赖库;确需新增依赖时先说明原因。
|
||||
|
||||
**Zod**:AI 工具层(`src/server/ai/tools/`)使用 Zod 定义 `tool()` 的 `inputSchema`,以满足 AI SDK 对 `ZodSchema` 的类型推断要求,属于框架级约束而非项目选型冲突。配置校验层使用 TypeBox + Ajv,两层级各司其职,不混用。
|
||||
|
||||
### 目录边界
|
||||
|
||||
| 目录 | 约束 |
|
||||
| ------------------------ | -------------------------------------------- |
|
||||
| `src/server/` | 后端,禁止 import src/web/ |
|
||||
| `src/server/db/` | 数据库层:schema、connection、migration、DAO |
|
||||
| `src/server/ai/` | AI Provider Registry + Agent + 工具 |
|
||||
| `src/server/helpers/` | 跨路由工具:响应格式化、URL 拼接 |
|
||||
| `src/server/middleware/` | 参数校验 + 错误处理中间件 |
|
||||
| `src/web/` | 前端,禁止 import src/server/ 运行时实现 |
|
||||
| `src/web/consoles/` | 控制台外壳(Admin / Workbench) |
|
||||
| `src/shared/` | 前后端共享类型(api.ts)和常量(app.ts) |
|
||||
| `scripts/` | 独立脚本,可 import 项目源码 |
|
||||
| `drizzle/` | SQL migration 文件(开发期产出) |
|
||||
| `tests/` | 测试目录,镜像 src/ 结构 |
|
||||
|
||||
### 类型与配置
|
||||
|
||||
- 共享类型唯一源头:`src/shared/api.ts`;应用常量唯一源头:`src/shared/app.ts`;版本号唯一源头:`package.json`。
|
||||
- 配置加载流程:unknown → AuthoringConfig → NormalizedConfig → ValidatedConfig → ServerConfig。
|
||||
- Ajv 严格拒绝模式:不类型转换、不注入默认值、不删除未知字段。
|
||||
- 新增/修改配置字段必须同步更新 TypeBox schema、`config.schema.json`、测试和用户文档。
|
||||
|
||||
### 后端日志
|
||||
|
||||
- 运行时代码通过 Logger 接口输出,禁止 `console.*`(仅 `logger.ts` 实现类内部可用)。
|
||||
- 敏感字段(authorization、cookie、password 等)自动 redact。
|
||||
|
||||
### API 路由
|
||||
|
||||
- 每个端点一个文件:`src/server/routes/{资源}/{操作}.ts`。
|
||||
- 路由在 `server.ts` 的 `Bun.serve({ routes })` 中声明式注册。
|
||||
- 新增路由:创建 handler → 在 `server.ts` 注册 → 在 `tests/server/` 添加测试。
|
||||
|
||||
### 前端数据层
|
||||
|
||||
- 统一使用 `fetch`,不引入 axios。
|
||||
- 错误抛异常,TanStack Query error 状态承接。
|
||||
- 返回类型必须匹配后端 JSON 形状;包装对象(如 `{ project }`)须在 hook 内提取业务对象。
|
||||
- 服务端状态用 TanStack Query,组件内状态用 `useState`,不引入额外状态管理库。
|
||||
|
||||
### 禁止事项
|
||||
|
||||
- 禁止前端 import `src/server/`
|
||||
- 禁止后端使用 `console.*`
|
||||
- 禁止组件内联 `style` 属性
|
||||
- 禁止 CSS 覆盖 `.ant-*` 内部类名
|
||||
- 禁止 `!important`
|
||||
- 禁止硬编码色值(使用 `var(--ant-*)` CSS 变量)
|
||||
- 禁止路由 handler 直接操作 `bun:sqlite` / Drizzle ORM(须通过 DAO 层)
|
||||
- 禁止跳过或忽略测试
|
||||
- 禁止在 `Modal onOk` 中直接执行异步提交(用 `Form onFinish`)
|
||||
|
||||
---
|
||||
|
||||
## 二、后端红线
|
||||
|
||||
### 路由 handler
|
||||
|
||||
- 签名:`(req: Request, db: Database, mode: RuntimeMode, logger?: Logger): Promise<Response>`。
|
||||
- 业务错误:`jsonResponse(createApiError(msg, status), { mode, status })`;未知异常直接 throw,`withErrorHandler` 兜底。
|
||||
- body 解析后立即校验必填字段和类型,失败返回 400。
|
||||
- ID 参数走 `validateIdParam`,分页参数走 `validatePagination`。
|
||||
- 路由注册:`withErrorHandler` 包裹 + 动态 `await import()` 加载 handler。
|
||||
|
||||
### 数据访问层
|
||||
|
||||
- handler 通过 `src/server/db/*.ts` 函数操作数据库,禁止直接使用 `bun:sqlite` 或 Drizzle。
|
||||
- DAO 函数第一个参数接收原始 `Database`,内部 `wrap(db)` 转 Drizzle。
|
||||
- 返回联合类型:成功 `{ resource: T }`,失败 `{ error: string; status: number }`。
|
||||
- 输入输出类型来自 `src/shared/api.ts`。
|
||||
- 列表查询使用 `paginateQuery()`,不重复实现分页。
|
||||
- 列名 snake_case,TS 类型 camelCase,Drizzle schema 映射。
|
||||
|
||||
### AI 调用层
|
||||
|
||||
- Provider 实例:`buildProviderRegistry(db)`,每次从 DB 重建,不缓存。
|
||||
- Agent 实例:`createAlfredAgent(model)` 工厂。
|
||||
- SSE 响应:`createAgentUIStreamResponse`,`onFinish` 持久化完整 parts。
|
||||
- 工具定义:`src/server/ai/tools/`。
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 业务异常用 `AppError(statusCode)` 或 `jsonResponse(createApiError(...))`。
|
||||
- handler 外层 `withErrorHandler` 兜底,不手动 try-catch 整个 handler。
|
||||
- 生产模式错误响应不暴露内部细节。
|
||||
|
||||
---
|
||||
|
||||
## 三、前端红线
|
||||
|
||||
### 组件规范
|
||||
|
||||
- 优先 antd 默认能力 + props,不额外改写视觉。
|
||||
- `ConfigProvider(zhCN)` 配置中文 locale 和主题,不在 CSS 硬编码亮/暗分支。
|
||||
- 应用级能力(message、modal、notification)通过 `AntApp` + `App.useApp()` 获取。
|
||||
- 页面保持编排职责,组合 hooks + 展示组件;复杂页面按功能边界拆分。
|
||||
|
||||
### 能力优先级(上层满足则不用下层)
|
||||
|
||||
1. antd 组件/props
|
||||
2. antd 布局组件(Layout、Space、Flex)
|
||||
3. antd theme token + CSS 变量
|
||||
4. TanStack Query + useState
|
||||
5. 已有 hooks(`use-*.ts`)和工具函数(`utils/`)
|
||||
6. CSS Modules(就近放置)
|
||||
7. 引入新依赖(需说明原因)
|
||||
|
||||
### 样式红线
|
||||
|
||||
- 严禁内联 `style`、覆盖 `.ant-*`、`!important`、硬编码色值。
|
||||
- 颜色使用 `var(--ant-*)` CSS 变量。
|
||||
- 不引入 Tailwind/Sass/Less/CSS-in-JS 等额外样式方案。
|
||||
- 全局 CSS 类名用 `app-*` 前缀,禁止泛名。样式增长后用 CSS Modules 就近维护。
|
||||
|
||||
### 表单与交互
|
||||
|
||||
- Modal + Form:`Form onFinish` 处理提交,`Modal onOk` 只调 `form.submit()`。
|
||||
- 必填文本字段同时配 `required` + `whitespace`。
|
||||
- 操作确认用 `Popconfirm`,反馈用 antd message。
|
||||
|
||||
### 错误边界
|
||||
|
||||
- 生产入口必须启用 `ErrorBoundary`。`ReactQueryDevtools` 仅 `DEV` 模式渲染。
|
||||
|
||||
---
|
||||
|
||||
## 四、测试规范
|
||||
|
||||
### 后端
|
||||
|
||||
- 路由测试通过真实 `startServer` 覆盖路由注册、HTTP method、fallback、header 和核心错误路径。
|
||||
- SQLite 测试复用 `tests/helpers.ts`,不分散实现临时目录清理。
|
||||
- DAO/路由测试优先用真实 migration 初始化。
|
||||
- logger/bootstrap fallback 输出须捕获断言,正常测试不污染 stdout/stderr。
|
||||
|
||||
### 前端
|
||||
|
||||
- 目录 `tests/web/`,结构对应 `src/web/`。
|
||||
- 用 jsdom + `@testing-library/react` 测试用户行为,断言基于可见文本/role/按钮。
|
||||
- 系统边界复用 `tests/web/test-utils.tsx`。
|
||||
- 数据页面覆盖:请求参数、成功可见结果、关键错误路径。
|
||||
- ErrorBoundary/hooks/fetch helper 用单元测试覆盖异常,页面测试只保留用户路径。
|
||||
|
||||
---
|
||||
|
||||
## 常用命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
| -------------------------------- | -------------------------------------- |
|
||||
| `bun install` | 安装依赖 |
|
||||
| `bun run dev config.yaml` | 启动双进程开发环境 |
|
||||
| `bun run dev:server config.yaml` | 仅启动后端 API server |
|
||||
| `bun run dev:web` | 仅启动 Vite dev server |
|
||||
| `bun run schema` | 生成 config.schema.json |
|
||||
| `bun run schema:check` | 检查导出 schema 是否同步 |
|
||||
| `bun run typecheck` | TypeScript 类型检查 |
|
||||
| `bun run lint` | ESLint 和 Prettier 格式检查 |
|
||||
| `bun run format` | Prettier 自动格式化 |
|
||||
| `bun run format:check` | Prettier 格式检查 |
|
||||
| `bun test` | 运行全部测试 |
|
||||
| `bun run dev:server config.yaml` | 仅后端 API server |
|
||||
| `bun run dev:web` | 仅 Vite dev server |
|
||||
| `bun run check` | schema:check + typecheck + lint + test |
|
||||
| `bun run build` | 构建生产可执行文件 |
|
||||
| `bun run verify` | check + build 完整验证 |
|
||||
| `bun run clean` | 清理构建缓存与临时文件 |
|
||||
| `bun run version:patch` | 升迁 patch 版本(x.y.Z) |
|
||||
| `bun run version:minor` | 升迁 minor 版本(x.Y.0) |
|
||||
| `bun run version:major` | 升迁 major 版本(X.0.0) |
|
||||
| `bun run build` | 构建生产可执行文件 |
|
||||
| `bun run schema` | 生成 config.schema.json |
|
||||
| `bun run schema:check` | 检查 schema 同步 |
|
||||
| `bun run typecheck` | TypeScript 类型检查 |
|
||||
| `bun run lint` | ESLint + Prettier 检查 |
|
||||
| `bun run format` | Prettier 格式化 |
|
||||
| `bun test` | 运行全部测试 |
|
||||
| `bun run clean` | 清理构建缓存 |
|
||||
| `bun run version:patch` | 升迁 patch 版本 |
|
||||
| `bun run version:minor` | 升迁 minor 版本 |
|
||||
| `bun run version:major` | 升迁 major 版本 |
|
||||
| `bun run version:set` | 显式设置版本号 |
|
||||
|
||||
## 质量门禁
|
||||
|
||||
代码变更必须按影响范围执行验证。
|
||||
|
||||
| 变更类型 | 必跑命令 |
|
||||
| -------------------------- | ------------------------------------------------------------- |
|
||||
| ----------------------- | --------------------------------------------------------- |
|
||||
| 常规代码变更 | `bun run check` |
|
||||
| 构建、部署、前后端集成变更 | `bun run verify` |
|
||||
| 构建、部署、集成变更 | `bun run verify` |
|
||||
| 配置 schema 变化 | `bun run schema`、`bun run schema:check`、`bun run check` |
|
||||
| SQLite 测试基础设施变化 | 相关单文件测试 + SQLite 聚焦 `--rerun-each` + `bun run check` |
|
||||
| SQLite 测试基础设施变化 | 相关测试 + SQLite 聚焦 `--rerun-each` + `bun run check` |
|
||||
| 仅文档变更 | 检查链接、索引和文档归属一致性 |
|
||||
|
||||
正式提交或影响构建产物时优先运行 `bun run verify`。如果因环境限制无法执行完整验证,必须在收尾说明中记录未执行项和原因。
|
||||
正式提交优先运行 `bun run verify`。无法执行时须在收尾说明中记录。
|
||||
|
||||
## 已知设计决策
|
||||
|
||||
本节记录项目中有意保留的设计决策,避免后续审查重复报错。
|
||||
|
||||
| 决策 | 原因 |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Provider.apiKey 返回给前端 | 个人项目,apiKey 非严格密码,前端需要展示和编辑。如需保护,应改为返回脱敏值或仅后端存储。 |
|
||||
|
||||
## 全局工程规则
|
||||
|
||||
- 使用中文编写注释、文档和项目内交流内容。
|
||||
- 仅使用 bun 作为包管理器,禁止使用 npm、pnpm、yarn。
|
||||
- 运行工具使用 bunx,禁止使用 npx、pnpx。
|
||||
- 新增代码优先复用已有组件、工具和依赖库,不引入新依赖;确需新增依赖时先说明原因。
|
||||
- 后端优先使用 Bun 内置 API,其次是 es-toolkit、标准 Web API、主流三方库,最后才自行实现。
|
||||
- 前端优先使用 Ant Design 组件默认能力和组件 props 组合界面,具体组件、样式、数据流和测试细节见 [frontend.md](frontend.md)。
|
||||
- 当前项目无需考虑向前兼容。
|
||||
|
||||
## 包管理、依赖与提交
|
||||
|
||||
- 仅使用 bun 安装依赖和运行项目脚本,锁文件为 bun.lock。
|
||||
- 新增依赖前先确认 Bun 内置 API、es-toolkit、标准 Web API、现有三方库和项目公共工具是否已满足需求。
|
||||
- Git 提交信息使用中文,格式为"类型: 简短描述"。
|
||||
- 提交类型限定为 feat、fix、refactor、docs、style、test、chore。
|
||||
- 多行提交描述时,标题和正文之间空一行。
|
||||
|
||||
## 目录边界
|
||||
|
||||
| 目录 | 约定 |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| `src/server/` | Bun 后端代码,不能 import src/web/,HTML import 集成除外 |
|
||||
| `src/server/db/` | SQLite 数据库模块,包含 schema、connection、migration 和 data access |
|
||||
| `src/server/ai/` | AI Provider Registry 构建与连接测试 |
|
||||
| `src/web/` | React 前端,不能 import src/server/ 运行时实现 |
|
||||
| `src/shared/` | 前后端共享 TypeScript 类型 |
|
||||
| `scripts/` | 独立运行脚本,可 import 项目源码 |
|
||||
| `drizzle/` | Drizzle Kit 生成的 SQL migration 文件(开发期产出) |
|
||||
| `tests/` | 测试目录,结构镜像 src/ |
|
||||
| `docs/user/` | 用户使用、配置、部署和排障文档 |
|
||||
| `docs/development/` | 架构、后端、前端、发布开发文档 |
|
||||
| `openspec/` | OpenSpec 变更管理与规格文档 |
|
||||
|
||||
## 文档影响分析
|
||||
|
||||
每次代码变更都必须执行文档影响分析。
|
||||
|
||||
| 如果变更影响 | 更新 |
|
||||
| ------------------------------------------ | ------------------------------------------ |
|
||||
| 变更影响 | 更新 |
|
||||
| -------------------------------------- | ------------------------------------------ |
|
||||
| 用户可见行为、配置、部署、运行行为 | `docs/user/` 对应文档 |
|
||||
| 开发流程、架构、测试、构建发布流程 | `docs/development/` 对应文档 |
|
||||
| 项目定位、快速开始、核心能力列表、文档导航 | `README.md` |
|
||||
| 文档同步规则或文档归属矩阵 | `docs/README.md` 和 `openspec/config.yaml` |
|
||||
| 项目定位、快速开始、核心能力、文档导航 | `README.md` |
|
||||
| 文档同步规则或归属矩阵 | `docs/README.md` 和 `openspec/config.yaml` |
|
||||
| 开发规范变化 | 本文档对应章节 |
|
||||
|
||||
如果无需更新文档,必须在收尾说明中说明原因。详细规则见 [文档总览](../README.md)。
|
||||
|
||||
## 事实来源
|
||||
|
||||
| 主题 | 事实来源 |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| 代码结构和实现 | `src/`、`scripts/`、`tests/` |
|
||||
| 配置 schema | TypeBox fragments、config.schema.json、schema 测试 |
|
||||
| 项目全局规则 | `openspec/config.yaml`、本文档、本目录专题文档 |
|
||||
无需更新文档时,须在收尾说明中说明原因。
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改常用命令、质量门禁、全局工程规则、目录边界或开发文档索引时,必须更新本文档。
|
||||
修改常用命令、质量门禁、开发规范(任何章节)、目录边界或开发文档索引时,必须更新本文档。
|
||||
|
||||
### 文档编撰规范
|
||||
|
||||
本节开发文档面向 AI 工具阅读,编撰时遵循以下原则:
|
||||
|
||||
**精简原则**
|
||||
|
||||
- 删除引导语、适用场景、过渡句等装饰性文字。AI 无需"应首先阅读"或"本文档说明…"类引导。
|
||||
- 不重复项目结构树,AI 可通过 glob 获取目录结构。
|
||||
- 不重复已在 README.md 声明的规范细节,专题文档只记录实现层面的函数签名、API 端点、模块职责等索引信息。
|
||||
- 表格标题自明时不再加说明段落。
|
||||
|
||||
**信息完整性**
|
||||
|
||||
- 所有函数签名、API 端点(方法+路径+说明)、数据访问函数清单必须完整列举,不可用"等"省略。
|
||||
- 页面行为描述须包含关键交互逻辑(如 Tab 切换、默认值、条件跳转、测试不阻止保存等),不可只写组件名。
|
||||
- 配置文件列表必须完整,不可遗漏已有文件。
|
||||
|
||||
**结构规范**
|
||||
|
||||
- 每个专题文档末尾保留 `## 更新触发条件` 章节,明确列出哪些变更必须更新该文档。
|
||||
- 用表格和编号列表替代散文段落,减少 token 消耗。
|
||||
- 同一信息只在一处维护,避免多处重复导致不一致。
|
||||
|
||||
@@ -1,116 +1,61 @@
|
||||
# 架构与边界
|
||||
|
||||
本文档说明 alfred 的项目结构、启动链路、运行时流程、HTTP 请求流程和前后端边界。
|
||||
|
||||
适用场景:修改目录边界、启动流程、运行时调度、HTTP server、前后端集成方式或主要模块职责。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
src/
|
||||
server/
|
||||
bootstrap.ts 统一启动引导(loadServerConfig -> DB 初始化 -> startServer)
|
||||
config.ts CLI 参数解析与配置文件加载 facade
|
||||
config/ 配置解析模块(types、issues、variables、normalizer、schema)
|
||||
db/ SQLite 数据库模块
|
||||
schema.ts Drizzle ORM schema 定义
|
||||
connection.ts 数据库连接与 PRAGMA 设置
|
||||
load-migrations.ts 从文件系统加载 migration SQL
|
||||
migrate.ts migration 执行器(备份 + 事务应用)
|
||||
projects.ts 项目数据访问函数
|
||||
providers.ts 供应商数据访问函数
|
||||
models.ts 模型数据访问函数
|
||||
conversations.ts 会话数据访问函数
|
||||
index.ts 数据库模块导出
|
||||
ai/ AI 服务层
|
||||
types.ts AI 配置类型定义
|
||||
registry.ts AI Provider Registry 构建与连接测试
|
||||
agent-stream.ts AI Agent 流式调用
|
||||
dev.ts 开发模式启动入口
|
||||
main.ts 生产模式启动入口
|
||||
server.ts HTTP server 启动工厂(Bun.serve routes 声明式路由)
|
||||
static.ts 生产模式静态资源服务
|
||||
helpers.ts 共享响应格式化工具
|
||||
middleware.ts API 参数校验中间件
|
||||
logger.ts 结构化日志(基于 pino + pino-roll)
|
||||
version.ts 运行时版本号读取
|
||||
routes/ API 路由处理器
|
||||
providers/ 供应商 CRUD 路由
|
||||
models/ 模型 CRUD 路由
|
||||
chat/ 聊天会话与消息路由
|
||||
shared/
|
||||
api.ts 前后端共享 TypeScript 类型定义
|
||||
app.ts 应用全局常量(name、title、subtitle、description)
|
||||
web/ React 前端(通过 Vite 构建)
|
||||
index.html HTML 入口
|
||||
main.tsx React 入口
|
||||
app.tsx 根组件
|
||||
routes.tsx 路由配置
|
||||
styles.css 全局样式
|
||||
pages/ 页面组件
|
||||
models/ 模型管理页面
|
||||
components/ UI 组件
|
||||
hooks/ React Hooks
|
||||
utils/ 前端工具函数
|
||||
scripts/ 独立运行脚本
|
||||
tests/ 测试文件(镜像 src 目录结构)
|
||||
docs/ 项目文档
|
||||
openspec/ OpenSpec 规格、变更与 fast-drive workflow schema
|
||||
```
|
||||
|
||||
## 启动流程
|
||||
|
||||
```text
|
||||
dev.ts / main.ts
|
||||
-> parseRuntimeArgs(cli args)
|
||||
-> 必须指定 config.yaml
|
||||
-> parseRuntimeArgs(cli args) — 必须指定 config.yaml
|
||||
-> bootstrap({ configPath, mode })
|
||||
-> loadServerConfig(configPath)
|
||||
-> createRuntimeLogger(config.logging)
|
||||
-> 确保 dataDir 就绪(mkdirSync)
|
||||
-> 加载 migrations(生产:嵌入的 bytes;开发:磁盘 drizzle/ 目录)
|
||||
-> mkdirSync(dataDir)
|
||||
-> 加载 migrations(生产:嵌入 bytes;开发:磁盘 drizzle/)
|
||||
-> createDatabase(dataDir)
|
||||
-> runMigrations(db, migrations)(pending migration 存在时先备份 DB)
|
||||
-> runMigrations(db, migrations) — pending 时先备份 DB
|
||||
-> startServer({ config, logger, db })
|
||||
-> logger 记录启动成功
|
||||
-> SIGINT/SIGTERM -> db.close() -> logger.flush() -> exit
|
||||
```
|
||||
|
||||
## HTTP 请求流程
|
||||
|
||||
```text
|
||||
Request
|
||||
-> Bun.serve routes 声明式匹配
|
||||
-> routes/*.ts handler
|
||||
-> helpers.ts 响应格式化
|
||||
-> Response
|
||||
Request -> Bun.serve routes 声明式匹配 -> routes/*.ts handler -> helpers/ 响应格式化 -> Response
|
||||
```
|
||||
|
||||
生产模式下,非 API 路径由 fetch fallback 处理:有文件扩展名的返回静态资源或 404,无扩展名的返回 SPA index.html。
|
||||
|
||||
开发模式下,Vite proxy 将 /api 请求转发到 Bun API server。
|
||||
- 生产模式:非 API 路径由 fetch fallback 处理,有扩展名返回静态资源或 404,无扩展名返回 SPA index.html。
|
||||
- 开发模式:Vite proxy 将 /api 转发到 Bun。
|
||||
|
||||
## 前后端边界
|
||||
|
||||
- 前端只通过 HTTP 调用后端,API 路径为 /api/\*。
|
||||
- 共享类型放在 src/shared/。
|
||||
- 前端不得 import src/server/ 的运行时实现。
|
||||
- 后端不得依赖 src/web/ 运行时代码,HTML import 集成除外。
|
||||
- 前端只通过 HTTP /api/\* 调用后端。
|
||||
- 共享类型在 `src/shared/`。
|
||||
- 前端禁止 import `src/server/` 运行时实现;后端禁止依赖 `src/web/` 运行时代码(HTML import 集成除外)。
|
||||
|
||||
## 主要模块职责
|
||||
## 主要模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
| ------------------------- | --------------------------------------------- |
|
||||
| `src/server/bootstrap.ts` | 统一启动引导、DB 初始化和 shutdown 编排 |
|
||||
| `src/server/server.ts` | Bun HTTP server 和 routes 注册 |
|
||||
| `src/server/routes/` | API handler,按端点拆分 |
|
||||
| `src/server/db/` | SQLite 连接、schema、migration 和 data access |
|
||||
| `src/server/ai/` | AI Provider Registry 构建与 Agent 流式调用 |
|
||||
| `src/server/config/` | 配置解析模块(types、variables、schema) |
|
||||
| `src/web/` | React 前端 |
|
||||
| ------------------------- | ------------------------------------------------ |
|
||||
| `src/server/bootstrap.ts` | 统一启动引导、DB 初始化、shutdown 编排 |
|
||||
| `src/server/server.ts` | Bun HTTP server + routes 注册 |
|
||||
| `src/server/routes/` | API handler,按资源端点拆分 |
|
||||
| `src/server/db/` | SQLite 连接、schema、migration、data access |
|
||||
| `src/server/ai/` | AI Provider Registry + Agent 流式调用 |
|
||||
| `src/server/config/` | 配置解析(types、variables、normalizer、schema) |
|
||||
| `src/server/helpers/` | 响应格式化、URL 工具 |
|
||||
| `src/server/middleware/` | 参数校验 + 错误处理 |
|
||||
| `src/shared/api.ts` | 前后端共享 API 类型 |
|
||||
| `src/shared/app.ts` | 应用全局常量 |
|
||||
|
||||
## 路由分组
|
||||
|
||||
| 资源 | 路径前缀 | 文件目录 |
|
||||
| --------- | ----------------------------------------------- | ------------------- |
|
||||
| meta | `/api/meta` | `routes/meta.ts` |
|
||||
| providers | `/api/providers` | `routes/providers/` |
|
||||
| models | `/api/models` | `routes/models/` |
|
||||
| projects | `/api/projects` | `routes/projects/` |
|
||||
| chat | `/api/projects/:id/conversations` 和 `:id/chat` | `routes/chat/` |
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改项目结构、启动流程、HTTP 请求流程、前后端边界或主要模块职责时,必须更新本文档。
|
||||
|
||||
@@ -1,104 +1,50 @@
|
||||
# 后端开发
|
||||
|
||||
本文档说明 alfred 后端的 API、配置加载、日志、版本管理和后端测试开发约定。
|
||||
|
||||
适用场景:修改 src/server/、src/shared/api.ts、后端测试、配置契约、API 响应或日志模块。
|
||||
|
||||
## 库使用优先级
|
||||
|
||||
| 优先级 | 来源 | 典型用途 |
|
||||
| ------ | ------------ | ---------------------------------------------------- |
|
||||
| 1 | Bun 内置 API | Bun.serve、Bun.file、Bun.YAML、Bun.spawn、bun:sqlite |
|
||||
| 2 | es-toolkit | 类型判断、深度比较、并发控制 |
|
||||
| 3 | 标准 Web API | Headers、fetch、AbortController |
|
||||
| 4 | 主流三方库 | pino、@sinclair/typebox、ajv、drizzle-orm |
|
||||
| 5 | 自行实现 | 仅在以上都无法满足时 |
|
||||
|
||||
新增依赖前必须先检查上述每一层是否已有可用方案。
|
||||
|
||||
## API 路由开发
|
||||
|
||||
路由文件位于 src/server/routes/,每个端点一个文件。路由通过 server.ts 的 Bun.serve({ routes }) 声明式注册。
|
||||
|
||||
新增路由步骤:
|
||||
|
||||
1. 在 src/server/routes/ 下创建 <name>.ts
|
||||
2. 实现 handler 函数并 export
|
||||
3. 在 server.ts 的 routes 对象中注册路径和 method handler
|
||||
4. 在 tests/server/ 中添加对应测试
|
||||
开发规范见 [开发规范文档](README.md)。
|
||||
|
||||
## 共享工具
|
||||
|
||||
helpers.ts 提供跨路由共用的响应工具:
|
||||
`src/server/helpers/`:
|
||||
|
||||
- createApiError(error, status) — 构造 API 错误体
|
||||
- createHeaders(mode, init) — 创建响应 Headers
|
||||
- jsonResponse(body, options) — JSON 响应构造
|
||||
- `response.ts`:`createApiError(error, status)`、`createHeaders(mode, init)`、`createMetaResponse(version)`、`formatDuration(ms)`、`jsonResponse(body, options)`
|
||||
- `url.ts`:`parseIdFromUrl(url)`
|
||||
|
||||
middleware.ts 提供 API 参数校验函数:
|
||||
`src/server/middleware/`:
|
||||
|
||||
- validateIdParam(idStr, mode) — 校验 ID 参数格式
|
||||
- validatePagination(pageParam, pageSizeParam, mode) — 校验分页参数
|
||||
- validateTimeRange(from, to, mode) — 校验时间范围参数
|
||||
- `validate.ts`:`validateIdParam(idStr, mode)` — 校验 ID 格式(字母数字 + `_-`);`validatePagination(pageParam, pageSizeParam, mode)` — page≥1, pageSize≤200;`validateTimeRange(from, to, mode)`
|
||||
- `error-handler.ts`:`AppError` — 业务异常类(含 statusCode);`withErrorHandler(fn, mode, logger?)` — 包裹 handler 捕获异常
|
||||
|
||||
## 数据库
|
||||
|
||||
项目使用 SQLite 作为存储后端,通过 bun:sqlite + Drizzle ORM 实现类型安全的数据访问。
|
||||
SQLite + bun:sqlite + Drizzle ORM。
|
||||
|
||||
### schema 定义
|
||||
- `src/server/db/schema.ts`:Drizzle 表结构,列名 snake_case,TS 类型 camelCase。
|
||||
- `src/server/db/connection.ts`:`createDatabase(dataDir, logger)` 打开 `alfred.db`,PRAGMA:foreign_keys=ON、journal_mode=WAL、busy_timeout=5000。`wrap(db)` 转为 Drizzle 实例。`paginateQuery()` 分页工具。
|
||||
- Migration:开发期 `drizzle-kit generate` 产出到 `drizzle/`;生产期嵌入可执行文件,启动时自动应用。备份到 `<dataDir>/backups/`,事务中执行,失败回滚。
|
||||
|
||||
`src/server/db/schema.ts` 使用 Drizzle ORM 定义表结构,列名使用 snake_case,TypeScript 类型使用 camelCase,Drizzle schema 负责映射。
|
||||
### 数据访问函数
|
||||
|
||||
### 数据库连接
|
||||
| 文件 | 函数 |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `projects.ts` | createProject、getProject、listProjects、updateProject、deleteProject、archiveProject、restoreProject |
|
||||
| `providers.ts` | createProvider、getProvider、listProviders、listProviderOptions、updateProvider、deleteProvider |
|
||||
| `models.ts` | createModel、getModel、listModels、getModelsByProviderId、updateModel、deleteModel |
|
||||
| `conversations.ts` | createConversation、getConversation、listConversations、updateConversation、updateConversationTimestamp、deleteConversation、createMessage、createMessages、listMessages |
|
||||
|
||||
`src/server/db/connection.ts` 的 `createDatabase(dataDir, logger)` 打开 `<dataDir>/alfred.db`,设置 PRAGMA(foreign_keys=ON、journal_mode=WAL、busy_timeout=5000)。
|
||||
|
||||
### migration 机制
|
||||
|
||||
- 开发期:使用 `drizzle-kit generate` 从 TS schema 生成 SQL migration 文件到 `drizzle/` 目录
|
||||
- 生产期:构建时将 `drizzle/*.sql` 嵌入可执行文件,启动时自动应用 pending migrations
|
||||
- 每次 migration 前自动备份现有 DB 到 `<dataDir>/backups/alfred-<timestamp>.db`
|
||||
- migration 在事务中执行,失败则回滚并停止启动
|
||||
|
||||
### 数据访问
|
||||
|
||||
`src/server/db/projects.ts` 提供项目数据访问函数,`src/server/db/providers.ts` 提供供应商数据访问函数,`src/server/db/models.ts` 提供模型数据访问函数,`src/server/db/conversations.ts` 提供会话和消息数据访问函数。输入输出使用 `src/shared/api.ts` 的类型。函数内部使用 Drizzle query builder 包装 `bun:sqlite` Database。
|
||||
输入输出类型来自 `src/shared/api.ts`。
|
||||
|
||||
## AI 服务层
|
||||
|
||||
`src/server/ai/` 提供 AI Provider Registry 构建与连接测试能力。
|
||||
- `src/server/ai/types.ts`:`AIProviderConfig`(name、type、baseUrl、apiKey)、`AIModelConfig`(providerId、modelId、capabilities)。
|
||||
- `src/server/ai/registry.ts`:
|
||||
- `buildProviderRegistry(db)` — 从 DB 查询供应商构建 AI SDK Provider Registry,每次调用重建,不缓存。通过 `registry.languageModel('providerId:modelId')` 获取模型实例。
|
||||
- `testProviderConnection(config)` — 测试 Base URL 可达性 + `/models` 接口
|
||||
- `testModelConnection(config)` — 测试模型连通性
|
||||
- `countModels(db)` — 统计已配置模型数
|
||||
- `src/server/ai/agents/alfred-agent.ts`:`createAlfredAgent(model)` — ToolLoopAgent + `stepCountIs(20)` + `getCurrentTime` 工具。
|
||||
- `src/server/ai/tools/`:AI 工具定义。
|
||||
|
||||
### 类型定义
|
||||
|
||||
`src/server/ai/types.ts` 定义 AI 配置类型:
|
||||
|
||||
- `AIProviderConfig` — 供应商配置(name、type、baseUrl、apiKey)
|
||||
- `AIModelConfig` — 模型配置(providerId、modelId、capabilities)
|
||||
- `AIRegistryConfig` — Registry 构建配置(providers、models),供后续 AI 调用层组合使用
|
||||
|
||||
### Registry 构建
|
||||
|
||||
`src/server/ai/registry.ts` 提供:
|
||||
|
||||
- `buildProviderRegistry(db)` — 从 DB 查询所有供应商,构建 Vercel AI SDK Provider Registry
|
||||
- `testProviderConnection(config)` — 先测试 Base URL 可达性,再请求 `/models` 验证 API Key 和模型列表接口
|
||||
|
||||
每次 AI 调用时从 DB 查询 providers,构建 registry 后通过 `registry.languageModel('providerId:modelId')` 获取模型实例。不使用缓存层。模型是否存在以及业务能力标签由调用方基于 models 表先行校验,registry 只负责将 providerId/modelId 映射到 AI SDK 模型实例。
|
||||
|
||||
### Agent 流式调用
|
||||
|
||||
`src/server/ai/agent-stream.ts` 提供 `agentStream(options)` 函数,封装 Vercel AI SDK `streamText` 调用。接收数据库实例、消息数组和模型 DB ID,从 DB 查询模型与供应商信息后构建 Provider Registry,使用 `:` 作为 provider 和 modelId 的分隔符。默认使用 `stepCountIs(1)` 限制单步调用。返回 `StreamTextResult`,路由层通过 `result.toUIMessageStreamResponse()` 转为 SSE 响应。
|
||||
|
||||
### 供应商连通性测试
|
||||
|
||||
供应商连通性测试返回 `{ providerTestResponse: { ok, message } }`,前端根据 `ok` 展示成功或失败提示。
|
||||
|
||||
- `POST /api/providers/test` — 使用表单中尚未保存的供应商配置测试连接
|
||||
- `POST /api/models/test` — 使用模型关联供应商配置和 modelId 测试模型连接
|
||||
|
||||
测试连接不会写入数据库,也不会阻止保存。Base URL 不可达或 API Key 无效返回 `ok: false`;Base URL 可达但 `/models` 不支持、非标准或返回非鉴权错误时返回 `ok: true` 并在 `message` 中提示用户可检查 URL 或忽略提醒。
|
||||
|
||||
### 支持的供应商类型
|
||||
### 供应商类型
|
||||
|
||||
| type | AI SDK factory |
|
||||
| ------------------- | --------------------------------------------------- |
|
||||
@@ -106,83 +52,38 @@ middleware.ts 提供 API 参数校验函数:
|
||||
| `anthropic` | `createAnthropic({ apiKey, baseURL })` |
|
||||
| `openai-compatible` | `createOpenAICompatible({ name, apiKey, baseURL })` |
|
||||
|
||||
## 聊天 API
|
||||
### 连通性测试
|
||||
|
||||
聊天 API 按项目维度组织会话和消息:
|
||||
- `POST /api/providers/test` — 用未保存配置测试,不写入 DB,不阻止保存。Base URL 不可达或 API Key 无效返回 `ok: false`;`/models` 不支持返回 `ok: true` + 提示。
|
||||
- `POST /api/models/test` — 用模型关联供应商 + modelId 测试。
|
||||
|
||||
## 聊天 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ----------------------------------------------- | ------------------ |
|
||||
| GET | `/api/projects/:id/conversations` | 列出项目下所有会话 |
|
||||
| POST | `/api/projects/:id/conversations` | 创建新会话 |
|
||||
| GET | `/api/projects/:id/conversations/:cid` | 获取会话详情 |
|
||||
| DELETE | `/api/projects/:id/conversations/:cid` | 删除会话及其消息 |
|
||||
| GET | `/api/projects/:id/conversations/:cid/messages` | 获取会话消息列表 |
|
||||
| POST | `/api/projects/:id/chat` | 发送消息并流式回复 |
|
||||
| PATCH | `/api/projects/:id/conversations/:cid` | 更新会话 |
|
||||
| DELETE | `/api/projects/:id/conversations/:cid` | 删除会话及消息 |
|
||||
| GET | `/api/projects/:id/conversations/:cid/messages` | 获取消息列表 |
|
||||
| POST | `/api/projects/:id/chat` | 发送消息,SSE 回复 |
|
||||
|
||||
聊天路由处理器位于 `src/server/routes/chat/`,遵循统一的 handler 模式。`send.ts` 处理发送消息:验证会话归属后保存用户消息到 DB,调用 `agentStream` 获取流式响应,返回 SSE UI 消息流,流结束后后台保存 AI 回复到 DB。
|
||||
`send.ts`:验证会话归属 → 保存用户消息 → `createAlfredAgent` → `createAgentUIStreamResponse` → `onFinish` 持久化 AI 回复。
|
||||
|
||||
## 类型规范
|
||||
|
||||
- 共享类型以 src/shared/api.ts 为唯一源头
|
||||
- 应用常量以 src/shared/app.ts 为唯一源头
|
||||
- 版本号以 package.json.version 为唯一源头
|
||||
- 前端不得 import src/server/ 下的任何文件
|
||||
- 严格联合类型优先于宽类型
|
||||
|
||||
## 配置契约
|
||||
|
||||
配置加载流程固定为:unknown -> AuthoringConfig -> NormalizedConfig -> ValidatedConfig -> ServerConfig。
|
||||
|
||||
Ajv 保持严格拒绝模式:allErrors: true、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||
|
||||
新增或修改配置字段时必须同步更新 TypeBox schema fragments、config.schema.json、测试和对应用户文档。
|
||||
|
||||
## 日志模块
|
||||
|
||||
后端运行时代码统一通过 Logger 接口输出日志,禁止直接使用 console.\*。
|
||||
## 日志
|
||||
|
||||
| 实现 | 用途 |
|
||||
| --------------------- | ------------------------ |
|
||||
| --------------------- | -------------- |
|
||||
| PinoLoggerWrapper | 生产运行时 |
|
||||
| ConsoleFallbackLogger | 配置加载失败前的降级日志 |
|
||||
| NoopLogger | 静默丢弃日志 |
|
||||
| ConsoleFallbackLogger | 配置加载前降级 |
|
||||
| NoopLogger | 静默丢弃 |
|
||||
| MemoryLogger | 测试替身 |
|
||||
|
||||
敏感信息会自动 redact authorization、cookie、password 等字段。
|
||||
|
||||
## 版本管理
|
||||
|
||||
项目使用 package.json.version 作为版本号唯一来源。
|
||||
|
||||
版本获取方式:
|
||||
|
||||
- 开发模式:src/server/version.ts 运行时从 package.json 读取
|
||||
- 生产模式:scripts/build.ts 在构建时将版本号烘焙为字面量注入
|
||||
|
||||
版本升迁命令:
|
||||
|
||||
```bash
|
||||
bun run version:patch # 升迁 patch 版本
|
||||
bun run version:minor # 升迁 minor 版本
|
||||
bun run version:major # 升迁 major 版本
|
||||
bun run version:set # 显式设置版本号
|
||||
```
|
||||
|
||||
## 后端测试
|
||||
|
||||
| 变更类型 | 测试重点 |
|
||||
| ------------------ | --------------------------------- |
|
||||
| API 路由 | tests/server/app.test.ts 集成行为 |
|
||||
| 配置 schema | schema 导出、合法/非法配置 |
|
||||
| helpers/middleware | 单元测试 |
|
||||
|
||||
后端测试约定:
|
||||
|
||||
- API 路由集成测试必须通过真实 `startServer` 覆盖路由注册、HTTP method、fallback、响应 header 和核心错误路径。
|
||||
- SQLite 相关测试必须复用 `tests/helpers.ts` 中的测试数据库 helper,不要在测试文件内分散实现临时目录清理或直接裸用 `rmSync(dir, { recursive: true })`。
|
||||
- DAO 和路由边界测试应优先使用真实 migration 初始化测试库,只有 migration 执行器单测可以使用最小 fake migration。
|
||||
- logger/bootstrap 中预期的 fallback 输出必须在测试中捕获并断言,正常通过的测试不应污染 stdout/stderr。
|
||||
唯一来源:`package.json`。开发模式从 package.json 运行时读取;生产模式构建时烘焙为字面量。
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改后端 API、共享类型、配置契约、日志模块、版本管理或后端测试规范时,必须更新本文档。
|
||||
修改后端模块 API、共享工具、数据库 schema、AI 服务层或聊天 API 时,必须更新本文档。
|
||||
|
||||
@@ -1,180 +1,131 @@
|
||||
# 前端开发
|
||||
|
||||
本文档说明 alfred 前端的 React、Ant Design、TanStack Query、组件、样式和前端测试约定。
|
||||
开发规范见 [开发规范文档](README.md)。
|
||||
|
||||
适用场景:修改 src/web/、前端共享类型使用方式、组件结构、样式规则或前端测试。
|
||||
## 控制台架构
|
||||
|
||||
## 技术栈
|
||||
两个控制台入口共享 ConsoleShell(`src/web/components/ConsoleShell/`):
|
||||
|
||||
| 层面 | 技术 | 用途 |
|
||||
| ------ | ----------------------------------------------------- | ------------------------ |
|
||||
| 框架 | React 19 | UI 组件开发 |
|
||||
| 构建 | Vite(开发)+ Bun compile(生产) | 开发服务 HMR 与生产构建 |
|
||||
| 语言 | TypeScript | 类型安全 |
|
||||
| UI 库 | Ant Design (antd) + @ant-design/icons + @ant-design/x | UI 组件、图标与聊天 UI |
|
||||
| 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动刷新 |
|
||||
| AI 层 | Vercel AI SDK (`@ai-sdk/react`) | 聊天状态管理与流式通信 |
|
||||
| 路由 | React Router v7 (Declarative mode) | SPA 路由与页面导航 |
|
||||
- **Admin**(`src/web/consoles/admin/`):路由 `/`(总览)、`/projects`、`/models`。
|
||||
- **Workbench**(`src/web/consoles/workbench/`):路由 `/workbench/:projectId`、`/workbench/:projectId/chat`。`WorkbenchProjectGate` 从 URL 读 projectId,通过 `ProjectContext` 提供项目上下文,仅 active 项目渲染。
|
||||
|
||||
不引入额外状态管理库。TanStack Query 承担服务端状态,组件内状态使用 useState。
|
||||
ConsoleShell 包含:`ConfigProvider(zhCN)` + `AntApp` + `Layout`(Header/Sider/Content) + 主题切换(明亮/黑暗/系统)+ 侧边栏折叠。Header 显示品牌名、版本号和控制台标题。
|
||||
|
||||
## 组件开发规范
|
||||
`Sidebar`(`src/web/components/Sidebar/`)纯展示组件,通过 `menuItems` props 接收配置。
|
||||
|
||||
- 每个 React 组件一个 .tsx 文件,文件名使用 PascalCase
|
||||
- 组件 props 定义为 interface XxxProps,紧邻组件函数声明
|
||||
- 类型从 src/shared/api.ts 导入,使用 import type
|
||||
- 展示组件放在 components/,通过 props 接收数据,通过回调返回事件;页面专属展示组件可就近放在 pages/\*/components/
|
||||
- 容器逻辑放在 hooks 中,组件只做数据消费;全局共享查询可提取为独立 hook(如 use-meta)
|
||||
- 工具函数放在 utils/,保持纯函数无副作用
|
||||
## 页面
|
||||
|
||||
页面组件保持编排职责,组合 hooks 和展示组件;当页面同时承担查询、筛选、分页、表格列、弹窗表单和 mutation 时,应按工具栏、表格、表单弹窗等功能边界拆分。拆分以降低职责复杂度为目标,避免为了拆分而拆分。
|
||||
|
||||
## Ant Design 使用规范
|
||||
|
||||
- 优先使用 antd 组件默认状态和官方交互模式;没有明确产品定制需求时,不额外改写组件视觉。
|
||||
- 优先通过组件 props 配置行为和外观,例如 `collapsible`、`theme`、`scroll`、`locale`、`status`、`variant`、`color`。
|
||||
- 全局使用 `ConfigProvider` 配置 antd 中文 locale;从 `antd/locale/zh_CN` 导入 `zhCN`。
|
||||
- 需要 message、modal、notification 等 antd 应用级能力时,在 `ConfigProvider` 内包裹 `App`(代码中可别名为 `AntApp`),组件内通过 `App.useApp()` 获取。
|
||||
- 状态页、异常页、空结果优先使用 `Result`、`Empty`、`Alert`、`Spin` 等 antd 组件。
|
||||
- 信息展示优先使用 `Typography`、`Card`、`Descriptions`、`Table` 等 antd 组件,避免用原生标签加自定义 CSS 复刻。
|
||||
- 搜索输入优先使用 `Input.Search`,保持回车搜索、按钮搜索和清空行为一致。
|
||||
- 表格在窄屏有挤压风险时必须提供明确的 `scroll` 或响应式列策略。
|
||||
|
||||
## 样式开发规范
|
||||
|
||||
前端基于 Ant Design 构建 UI。样式管理目标是让 antd 继续承担主视觉系统,项目 CSS 只补足页面外壳、局部布局和自有组件视觉,不另起一套与 antd 竞争的样式体系。
|
||||
|
||||
样式开发优先级:
|
||||
|
||||
1. antd 组件默认能力,例如 `Button`、`Card`、`Table`、`Form`、`Result`、`Empty`。
|
||||
2. antd 组件 props,例如 `size`、`type`、`variant`、`color`、`status`、`layout`、`scroll`、`gutter`。
|
||||
3. antd 布局组件,例如 `Layout`、`Flex`、`Space`、`Row`、`Col`,避免为普通排列关系新增 CSS。
|
||||
4. `ConfigProvider` theme token 和 antd 组件 token,处理主题级或组件级统一调整。
|
||||
5. antd CSS 变量(`--ant-*`),用于项目自有 CSS 中引用颜色、间距、字体、圆角和阴影等设计值。
|
||||
6. 全局 CSS,仅承载应用外壳、全局基础样式和少量明确复用的工具类。
|
||||
7. CSS Modules,用于页面专属布局或项目自有组件视觉;仅在局部样式增长到需要就近维护时使用。
|
||||
8. 自行开发视觉组件,仅在 antd 组件和组合方式无法表达明确产品需求时使用。
|
||||
|
||||
红线:
|
||||
|
||||
- 严禁在组件中使用 `style` 属性内联调整样式。
|
||||
- 严禁通过 CSS 覆盖 antd 组件内部类名,例如 `.ant-*`。
|
||||
- 严禁使用 `!important`。
|
||||
- 颜色统一使用 antd Design Token / CSS 变量,不使用硬编码色值。
|
||||
- 默认不引入 Tailwind、UnoCSS、Sass、Less、CSS-in-JS 或额外 PostCSS 插件;确需引入时必须先说明现有 antd + CSS Modules 无法满足的具体问题、影响范围和迁移成本。
|
||||
|
||||
默认状态原则:如果 antd 组件默认样式已经满足当前需求,不为其增加额外 CSS 类;不要通过外层 CSS 修改 Sider、Menu、Table、Modal 等组件内部结构样式。
|
||||
|
||||
全局 CSS 归属:
|
||||
|
||||
- 当前入口保留 `src/web/styles.css`;当文件继续增长时,优先拆分为 `src/web/styles/global.css`、`src/web/styles/app-shell.css`、`src/web/styles/utilities.css` 等按职责命名的文件,再由入口样式文件集中导入。
|
||||
- `global.css` 仅放 `html`、`body`、`:root`、字体渲染、全局背景等应用级基础样式。
|
||||
- `app-shell.css` 仅放应用外壳样式,例如 `app-layout`、`app-header`、`app-content`、Header 内容分布和主内容间距。
|
||||
- `utilities.css` 只放至少两处复用、语义稳定、不会与 antd props 重叠的工具类;只有一处使用时优先改为 antd 布局组件或局部 CSS Modules。
|
||||
- 全局类名必须带有明确前缀,应用外壳使用 `app-*`,工具类使用 `u-*`;禁止新增 `.container`、`.title`、`.content` 等容易跨页面冲突的泛名类。
|
||||
|
||||
CSS Modules 归属:
|
||||
|
||||
- 页面专属样式与页面就近放置,例如 `src/web/pages/projects/projects.module.css`。
|
||||
- 自有组件样式与组件就近放置,例如 `src/web/components/FooCard/foo-card.module.css`。
|
||||
- CSS Modules 中类名使用职责语义,例如 `.root`、`.toolbar`、`.summaryCard`、`.emptyState`;通过导入对象绑定到组件,避免字符串拼写散落。
|
||||
- 同一类样式只服务当前页面或组件;一旦被多处复用,应先判断能否用 antd 组件或 props 表达,再考虑提取共享组件,而不是直接提升为全局 CSS。
|
||||
- 首次实际使用 `*.module.css` 时,同步补全 TypeScript 声明和必要测试,确保类型检查与构建链路稳定。
|
||||
|
||||
antd 定制边界:
|
||||
|
||||
- 优先使用官方 props、theme token 和组件 token;不要因为视觉微调直接写 CSS。
|
||||
- antd v6 组件暴露 `classNames` 语义插槽时,可以把项目自有类绑定到官方稳定插槽;仍然不得选择 `.ant-*` 内部 DOM 类名。
|
||||
- 避免使用 antd `styles` 语义插槽写内联样式;如果必须使用,应先评估是否可以通过 token、CSS 变量或 CSS Modules 表达。
|
||||
- 弹窗、下拉、表格、菜单等复杂组件不依赖内部 DOM 结构做布局修补;发现必须修补时,优先调整组件组合或交互设计。
|
||||
|
||||
token 和 CSS 变量规则:
|
||||
|
||||
- 颜色使用 `var(--ant-color-*)`,例如文本、边框、背景和状态色。
|
||||
- 间距、字号、圆角和阴影优先使用 `var(--ant-padding-*)`、`var(--ant-margin-*)`、`var(--ant-font-size-*)`、`var(--ant-border-radius*)`、`var(--ant-box-shadow*)` 等 antd 变量。
|
||||
- 项目自定义 CSS 变量只能定义在 `:root` 或清晰的主题容器上,并且必须基于 antd token 派生;不要创建与 antd 平行的颜色、间距、字号体系。
|
||||
- 主题切换统一通过 `ConfigProvider` theme algorithm 和 token 控制,不在 CSS 中硬编码亮色或暗色分支。
|
||||
|
||||
响应式规则:
|
||||
|
||||
- 页面必须在桌面和移动端正常加载和可读。
|
||||
- 优先使用 antd `Flex`、`Grid`、`Table scroll`、响应式列配置处理布局收缩。
|
||||
- 媒体查询只处理页面或自有组件的布局断点;不要用媒体查询覆盖 antd 内部结构。
|
||||
- 移动端适配优先保证内容可访问、操作可点击和横向溢出可控,不追求与桌面完全一致的排版。
|
||||
|
||||
新增样式前检查:
|
||||
|
||||
1. 这个需求是否可以由 antd 组件或 props 完成?
|
||||
2. 这个样式是否属于主题级统一调整,应该放到 `ConfigProvider` theme token?
|
||||
3. 这个样式是否只服务页面外壳,应该留在全局 CSS?
|
||||
4. 这个样式是否只服务单个页面或自有组件,应该使用 CSS Modules?
|
||||
5. 这个样式是否在覆盖 antd 内部结构?如果是,应重新设计组件组合。
|
||||
6. 这个样式是否引入了硬编码色值、`style`、`.ant-*` 或 `!important`?如果是,不应合入。
|
||||
|
||||
## 表单与交互规范
|
||||
|
||||
- Modal + Form 提交使用 `Form onFinish` 处理业务提交,`Modal onOk` 只触发 `form.submit()`。
|
||||
- 不在 `Modal onOk` 中直接执行异步 `validateFields` 和提交逻辑,也不通过 lint disable 绕过该问题。
|
||||
- 文本必填字段同时配置 `required: true` 和 `whitespace: true`,保持前端校验与后端 trim 后校验一致。
|
||||
- 提交中状态传给 antd 组件的 loading/confirmLoading 等 props,避免自行实现重复状态样式。
|
||||
- 操作确认优先使用 `Popconfirm`,成功/失败反馈优先使用 antd message。
|
||||
|
||||
## 运行时外壳规范
|
||||
|
||||
前端提供两个入口外壳,共享通用 Console Shell 组件:
|
||||
|
||||
- **Admin(管理台)**:`src/web/consoles/admin/AdminConsoleLayout.tsx`,菜单配置在 `menu.tsx`,路由 `/`、`/projects`、`/models`。
|
||||
- **Workbench(工作台)**:`src/web/consoles/workbench/WorkbenchProjectGate.tsx` → `WorkbenchConsoleLayout.tsx`,菜单配置和路由构造在 `routes.ts`,路由 `/workbench/:projectId`。默认菜单为"聊天室",使用 `ChatPage` 作为主页面。
|
||||
|
||||
通用 Console Shell(`src/web/components/ConsoleShell/ConsoleShell.tsx`)包含 Layout、Header、Sider、Content、主题切换、版本展示和侧边栏折叠状态,由 Admin 和 Workbench 复用。Header 显示品牌名、版本号和控制台标题(Admin 显示"管理台",Workbench 显示"工作台 · 项目名")。
|
||||
|
||||
Sidebar(`src/web/components/Sidebar/index.tsx`)是纯展示/导航组件,通过 `menuItems` props 接收菜单配置,由调用方决定菜单内容和路径。Admin 传入静态路径 `/`、`/projects`、`/models`;Workbench 通过 route builder(`buildWorkbenchPath`)将相对菜单路径拼成 `/workbench/:projectId` 的子路径。
|
||||
|
||||
Workbench 项目上下文通过 `ProjectContext` 提供,在 `WorkbenchProjectGate` 中从 URL path param 读取 `projectId`,通过 `useProject(projectId)` 加载项目,仅 active 项目渲染工作台布局,不存在或 archived 项目显示"项目不存在或不可访问"。
|
||||
|
||||
模型管理页面(`src/web/pages/models/index.tsx`)属于 Admin 路由 `/models`,通过 antd `Tabs` 在同页组织供应商和模型两个视图。页面使用 `ModelsToolbar`、`ProviderTable`、`ProviderFormModal`、`ModelTable`、`ModelFormModal` 拆分筛选、表格和表单职责;模型表单和模型表格必须使用 `GET /api/providers/options` 获取最小供应商选项,不能复用供应商标签页当前分页或搜索结果作为全量选项。
|
||||
|
||||
供应商表单必须支持未保存配置的连通性测试,新建供应商时 type 默认 `openai-compatible`,baseURL 不设默认值。连通性测试返回 `ok: false` 时应展示失败反馈,不得使用成功提示样式;`/models` 不支持或响应格式不兼容属于可忽略提醒,不得阻止保存。
|
||||
| 页面 | 路径 | 入口 |
|
||||
| -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 总览 | `/` | `pages/dashboard/index.tsx` |
|
||||
| 项目管理 | `/projects` | `pages/projects/index.tsx` — ProjectToolbar(Tab 切换 active/archived + 搜索 + 新建) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除,仅 active 项目可跳转工作台。 |
|
||||
| 模型管理 | `/models` | `pages/models/index.tsx` — antd Tabs 切换供应商/模型视图。模型表单和表格使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`),新建时 type 默认 `openai-compatible`,测试 `ok: false` 展示失败但不阻止保存。 |
|
||||
| 聊天室 | `/workbench/:id` | `consoles/workbench/pages/ChatPage.tsx` |
|
||||
| 404 | `*` | `pages/404/index.tsx` |
|
||||
|
||||
### 聊天页面
|
||||
|
||||
Workbench 聊天页面位于 `src/web/consoles/workbench/pages/ChatPage.tsx`,组合 `ChatSidebar` 和 `ChatPanel` 两个子组件。
|
||||
`ChatPage` = `ChatSidebar` + `ChatPanel`。
|
||||
|
||||
- **ChatSidebar**:使用 TanStack Query 管理会话列表,提供创建和删除会话操作。
|
||||
- **ChatPanel**:使用 Vercel AI SDK `useChat` hook(来自 `@ai-sdk/react`)管理聊天状态,通过 `DefaultChatTransport`(来自 `ai` 包)与后端 SSE 端点通信。使用 `@ant-design/x` 的 `Bubble.List` 和 `Sender` 组件渲染消息列表和输入框。`useChat` 返回的 `UIMessage` 使用 `parts` 数组存储内容,不包含 `content` 属性;需从 `parts` 中提取 `type: "text"` 的文本内容用于 Bubble 展示。
|
||||
- **MessageBubble**:简单的纯文本消息气泡组件(MVP)。
|
||||
- **use-conversations hook**:位于 `src/web/hooks/use-conversations.ts`,封装会话 CRUD 的 fetch 调用。
|
||||
- **ChatSidebar**:TanStack Query 管理会话列表,创建/删除操作。
|
||||
- **ChatPanel**:`useChat`(@ai-sdk/react)+ `DefaultChatTransport`(ai 包)与后端 SSE 通信。按 `part.type` 分派渲染:TextPart(streamdown Markdown)、ReasoningPart、ToolPart(四态)。支持编辑重发、重新生成、复制。
|
||||
- **ChatInputArea**:`Input.TextArea` + `Button` + `Select`(模型切换)。
|
||||
|
||||
- 生产入口必须启用 `ErrorBoundary`,运行时渲染异常使用 antd `Result status="500"` 或等价组件展示。
|
||||
- `ReactQueryDevtools` 仅在 `import.meta.env.DEV` 条件下渲染,不进入生产渲染路径。
|
||||
- 主题切换统一通过 `ConfigProvider` 的 antd theme algorithm 控制,不使用硬编码主题色。
|
||||
## Hooks
|
||||
|
||||
## TanStack Query 规范
|
||||
| Hook | 说明 |
|
||||
| ----------------------- | ----------------------------------------- |
|
||||
| `use-meta.ts` | `/api/meta`(30s 轮询,5s staleTime) |
|
||||
| `use-projects.ts` | 项目 CRUD + archive/restore |
|
||||
| `use-providers.ts` | 供应商 CRUD + test connection |
|
||||
| `use-models.ts` | 模型 CRUD + test connection |
|
||||
| `use-conversations.ts` | 会话和消息 fetch 函数(不含 Query hooks) |
|
||||
| `use-theme-preference` | 主题偏好 localStorage 持久化 |
|
||||
| `use-sidebar-collapsed` | 侧边栏折叠 localStorage 持久化 |
|
||||
|
||||
- Query key 使用 structured array,使用 as const 保持字面量类型
|
||||
- 全局面板级查询可持续刷新,详情级查询必须按状态条件启用
|
||||
- 多处页面使用同一后端资源时,应提取共享 hook,避免重复定义 fetch 函数。
|
||||
## 工具函数
|
||||
|
||||
## fetch 封装
|
||||
|
||||
统一使用 fetch,不引入 axios。错误抛异常,由 TanStack Query 的 error 状态承接。
|
||||
|
||||
前后端共享的请求和响应类型定义在 src/shared/api.ts。前端 fetch 函数的返回类型必须匹配后端真实 JSON 形状;如果后端返回包装对象,例如 `{ project: Project }`,应声明对应响应类型并在 hook 内提取业务对象。
|
||||
|
||||
## 前端测试
|
||||
|
||||
- 测试目录为 tests/web/,结构对应 src/web/
|
||||
- 单元测试重点覆盖 utils/ 和 hooks 中的纯逻辑
|
||||
- 组件测试使用 jsdom 和 @testing-library/react
|
||||
- 测试用户行为而非实现细节
|
||||
- 只 mock 系统边界,使用真实的 QueryClientProvider 包裹组件
|
||||
- 组件测试环境由 tests/setup.ts 和 bunfig.toml preload 提供
|
||||
- 断言优先基于用户可见文本、role、按钮和交互结果,不依赖 `.ant-*` 内部类名。
|
||||
- 对 antd 组件只断言本项目传入的可观察行为或配置结果,避免把 antd 内部 DOM 结构当作稳定契约。
|
||||
- fetch mock、路由、QueryClientProvider 等系统边界优先复用 tests/web/test-utils.tsx,避免在每个测试文件重复安装 `window.fetch`。
|
||||
- 项目页这类数据驱动页面至少覆盖请求 URL/query、method/body、成功后的用户可见结果,以及关键错误路径或失败后状态。
|
||||
- ErrorBoundary、hooks 纯逻辑和 fetch request helper 应使用单元测试覆盖异常回退,页面测试只保留真实用户路径。
|
||||
| 文件 | 导出 |
|
||||
| --------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `utils/api.ts` | `handleResponse(response, extract)`、`handleVoidResponse(response)` |
|
||||
| `utils/time.ts` | `formatCountdown`、`formatDurationUnit`、`formatRelativeTime`、`isOlderThan`、`subtractHours` |
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改前端技术栈、组件边界、数据流、样式规则、测试环境或前端验证方式时,必须更新本文档。
|
||||
修改前端技术栈、组件边界、数据流、样式规则、测试环境、前端验证方式、运行时代码结构、页面组成、组件索引或 hooks/工具清单时,必须更新本文档。
|
||||
|
||||
## 日志模块
|
||||
|
||||
### Logger 接口
|
||||
|
||||
`src/web/utils/logger.ts` 提供与后端镜像的 Logger 抽象:
|
||||
|
||||
```typescript
|
||||
export interface Logger {
|
||||
child(bindings: Record<string, unknown>): Logger;
|
||||
debug(message: string, data?: unknown): void;
|
||||
error(message: string, data?: unknown): void;
|
||||
info(message: string, data?: unknown): void;
|
||||
setLevel(level: LogLevel): void;
|
||||
warn(message: string, data?: unknown): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 实现
|
||||
|
||||
| 实现 | 工厂函数 | 用途 |
|
||||
| ----------------------- | --------------------------------------- | ------------------------------------------------------- |
|
||||
| `DefaultLogger` + Sinks | `useLogger()` / `createDefaultLogger()` | 组件内使用,ConsoleSink + AntdMessageSink 双流 |
|
||||
| `ConsoleLogger` | `createConsoleLogger()` | 非组件纯函数(ErrorBoundary、工具函数),仅 ConsoleSink |
|
||||
| `NoopLogger` | `createNoopLogger()` | 测试中不需要日志的场景 |
|
||||
| `MemoryLogger` | `createMemoryLogger()` | 测试断言日志条目 |
|
||||
|
||||
### 使用方式
|
||||
|
||||
**组件内(推荐):**
|
||||
|
||||
```typescript
|
||||
import { useLogger } from "../hooks/use-logger";
|
||||
|
||||
function MyComponent() {
|
||||
const logger = useLogger();
|
||||
logger.info("数据加载完成", { count: 42 });
|
||||
logger.warn("即将超时");
|
||||
logger.error("操作失败", { error: new Error("...") });
|
||||
}
|
||||
```
|
||||
|
||||
**非组件纯函数:**
|
||||
|
||||
```typescript
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const logger = createConsoleLogger();
|
||||
logger.debug("调试信息");
|
||||
```
|
||||
|
||||
**作用域绑定:**
|
||||
|
||||
```typescript
|
||||
const pageLogger = logger.child({ page: "projects" });
|
||||
pageLogger.info("页面加载"); // [Alfred:INFO] 页面加载 [page=projects]
|
||||
```
|
||||
|
||||
### notification 红线
|
||||
|
||||
- `AntdMessageSink` 仅对 **warn**(`message.warning`)和 **error**(`message.error`)触发用户可见通知。
|
||||
- `debug` 和 `info` 级别绝不对用户弹出 notification,仅在开发者控制台输出。
|
||||
- 错误详情通过 `data` 参数传入(如 `logger.error("提交失败", { error })`),`data` 不经序列化透传,保留 Error 堆栈展开能力。
|
||||
|
||||
### 生产环境行为
|
||||
|
||||
生产环境(`import.meta.env["PROD"]`)自动将 ConsoleSink 最小级别设为 `warn`,屏蔽 debug/info 输出。`useLogger()` 和 `createConsoleLogger()` 自动处理此逻辑,调用方无需关心环境判断。
|
||||
|
||||
### ErrorBoundary 特殊说明
|
||||
|
||||
`ErrorBoundary` 是 class 组件,无法使用 `useLogger()` hook。它以 `createConsoleLogger()` 直接创建独立的 ConsoleLogger 实例,仅输出到控制台不触发用户通知。
|
||||
|
||||
### 测试
|
||||
|
||||
- 单元测试使用 `createMemoryLogger()` 断言日志记录,使用 `createNoopLogger()` 静默无关日志。
|
||||
- `createDefaultLogger(sinks, isProduction)` 接受 `isProduction` 参数,测试中可显式控制级别过滤行为,不依赖 `import.meta.env`。
|
||||
|
||||
@@ -1,97 +1,60 @@
|
||||
# 构建与发布
|
||||
|
||||
本文档说明开发服务、前后端集成、生产构建、脚本维护和环境变量。
|
||||
## 开发运行
|
||||
|
||||
适用场景:修改 scripts/、构建流程、静态资源集成或环境变量。
|
||||
- `bun run dev config.yaml` — 双进程(Bun API server --watch + Vite dev server HMR)
|
||||
- `bun run dev:server config.yaml` — 仅后端
|
||||
- `bun run dev:web` — 仅前端
|
||||
|
||||
## 开发期运行
|
||||
|
||||
```bash
|
||||
bun run dev config.yaml
|
||||
```
|
||||
|
||||
scripts/dev.ts 同时启动两个进程:
|
||||
|
||||
| 进程 | 用途 |
|
||||
| --------------- | --------------------------------------- |
|
||||
| Bun API server | 后端 API 服务,--watch 监听变更自动重启 |
|
||||
| Vite dev server | 前端 SPA、HMR 热更新 |
|
||||
|
||||
也可以单独启动:
|
||||
|
||||
```bash
|
||||
bun run dev:server config.yaml # 仅启动后端 API server
|
||||
bun run dev:web # 仅启动 Vite dev server
|
||||
```
|
||||
开发模式 Vite proxy 将 /api/\* 转发到 Bun。
|
||||
|
||||
## 前后端集成
|
||||
|
||||
开发模式下,Vite 通过 proxy 将 /api/\* 转发到 Bun。
|
||||
生产模式:Vite 构建为静态资源 → `import with { type: "file" }` 嵌入 Bun 可执行文件 → 非 API 路径 fetch fallback 处理。
|
||||
|
||||
生产模式下,前端通过 Vite 构建为静态资源,通过 import with { type: "file" } 嵌入 Bun 可执行文件。非 API 路径由 fetch fallback 处理。
|
||||
|
||||
路由优先级:Bun routes 具体路径 > 通配符。/api/meta 优先于 /api/\*。
|
||||
路由优先级:Bun routes 具体路径 > 通配符。`/api/meta` 优先于 `/api/*`。
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
`bun run build` 流程:
|
||||
|
||||
构建流程:
|
||||
1. `Vite build` → `dist/web/`
|
||||
2. `Code generation` → `.build/static-assets.ts` + `.build/migrations-data.ts` + `.build/server-entry.ts`
|
||||
3. `Bun compile` → `dist/alfred`
|
||||
4. `Cleanup` → 清理 `.build/`
|
||||
|
||||
```text
|
||||
1. Vite build -> dist/web/
|
||||
2. Code generation -> .build/static-assets.ts + .build/migrations-data.ts + .build/server-entry.ts
|
||||
3. Bun compile -> dist/alfred
|
||||
4. Cleanup -> 清理 .build/ 临时目录
|
||||
```
|
||||
构建参数:`BUN_TARGET` / `BUILD_TARGET` — 交叉编译目标平台。
|
||||
|
||||
构建参数:
|
||||
|
||||
| 环境变量 | 说明 |
|
||||
| ------------------------- | ---------------- |
|
||||
| BUN_TARGET / BUILD_TARGET | 交叉编译目标平台 |
|
||||
|
||||
## 脚本说明
|
||||
## 脚本
|
||||
|
||||
| 脚本 | 文件 | 说明 |
|
||||
| --------------------- | --------------------------------- | ------------------------------ |
|
||||
| bun run dev | scripts/dev.ts | 双进程开发服务 |
|
||||
| bun run dev:server | src/server/dev.ts | 仅启动后端 API server |
|
||||
| bun run dev:web | Vite CLI | 仅启动 Vite dev server |
|
||||
| bun run build | scripts/build.ts | Vite -> codegen -> Bun compile |
|
||||
| bun run schema | scripts/generate-config-schema.ts | 生成配置 JSON Schema |
|
||||
| bun run schema:check | scripts/generate-config-schema.ts | 检查配置 JSON Schema 同步 |
|
||||
| bun run clean | scripts/clean.ts | 清理构建缓存与临时文件 |
|
||||
| bun run version:patch | scripts/bump-version.ts | 升迁 patch 版本 |
|
||||
| bun run version:minor | scripts/bump-version.ts | 升迁 minor 版本 |
|
||||
| bun run version:major | scripts/bump-version.ts | 升迁 major 版本 |
|
||||
| bun run version:set | scripts/bump-version.ts | 显式设置版本号 |
|
||||
| ----------------------------- | --------------------------------- | ------------------------ |
|
||||
| dev | scripts/dev.ts | 双进程开发服务 |
|
||||
| dev:server | src/server/dev.ts | 仅后端 |
|
||||
| dev:web | Vite CLI | 仅前端 |
|
||||
| build | scripts/build.ts | Vite → codegen → compile |
|
||||
| schema | scripts/generate-config-schema.ts | 生成 JSON Schema |
|
||||
| schema:check | (同上) | 检查 Schema 同步 |
|
||||
| clean | scripts/clean.ts | 清理构建缓存 |
|
||||
| version:patch/minor/major/set | scripts/bump-version.ts | 版本升迁 |
|
||||
|
||||
## 项目配置文件
|
||||
内部辅助:`scripts/bump-version-logic.ts`(版本逻辑)、`scripts/generate-migrations-data.ts`(构建时嵌入 SQL)。
|
||||
|
||||
## 配置文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
| -------------------- | --------------------------- |
|
||||
| package.json | 项目信息、脚本、依赖声明 |
|
||||
| -------------------- | -------------------- |
|
||||
| package.json | 项目信息、脚本、依赖 |
|
||||
| tsconfig.json | TypeScript 配置 |
|
||||
| eslint.config.js | ESLint 规则 |
|
||||
| commitlint.config.js | commitlint 提交信息格式校验 |
|
||||
| .prettierrc.json | Prettier 格式化规则 |
|
||||
| commitlint.config.js | 提交信息格式校验 |
|
||||
| .prettierrc.json | Prettier 规则 |
|
||||
| .lintstagedrc.json | lint-staged 配置 |
|
||||
| config.example.yaml | 配置文件示例 |
|
||||
| config.schema.json | 配置文件 JSON Schema |
|
||||
| config.example.yaml | 配置示例 |
|
||||
| config.schema.json | 配置 JSON Schema |
|
||||
| vite.config.ts | Vite 构建配置 |
|
||||
| bunfig.toml | Bun 配置 |
|
||||
|
||||
## 验证期望
|
||||
|
||||
| 变更类型 | 验证方式 |
|
||||
| ---------------- | -------------------- |
|
||||
| 构建脚本 | bun run verify |
|
||||
| 静态资源集成 | bun run build |
|
||||
| 配置 schema 同步 | bun run schema:check |
|
||||
| 发布前完整验证 | bun run verify |
|
||||
| drizzle.config.ts | Drizzle ORM 配置 |
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ bun run build
|
||||
| ------------------------------ | ------------------- |
|
||||
| http://127.0.0.1:3000/ | 返回前端 SPA |
|
||||
| http://127.0.0.1:3000/api/meta | 返回应用元信息 JSON |
|
||||
| http://127.0.0.1:3000/health | 返回健康检查 |
|
||||
|
||||
## 构建流程
|
||||
|
||||
@@ -23,8 +22,9 @@ scripts/build.ts 执行三步流水线:
|
||||
|
||||
```text
|
||||
1. Vite build -> dist/web/(前端静态资源,含 code splitting)
|
||||
2. Code generation -> .build/static-assets.ts + .build/server-entry.ts(含版本号字面量注入)
|
||||
2. Code generation -> .build/static-assets.ts + .build/migrations-data.ts + .build/server-entry.ts(含版本号字面量注入)
|
||||
3. Bun compile -> dist/alfred(单可执行文件)
|
||||
4. Cleanup -> 清理 .build/ 临时目录
|
||||
```
|
||||
|
||||
- Vite 构建前端资源到 dist/web/,自动 code splitting(vendor-react、vendor-antd、vendor-chart)
|
||||
|
||||
@@ -38,8 +38,6 @@ bun run dev config.yaml
|
||||
| 项目管理 | `/projects` | 创建、编辑、归档、恢复和永久删除项目 |
|
||||
| 模型管理 | `/models` | 配置 AI 供应商和模型,供后续 AI 功能使用 |
|
||||
| 聊天室 | `/workbench/:projectId` | Workbench 工作台聊天室,与 AI 对话 |
|
||||
| 用户管理 | `/users` | 页面建设中 |
|
||||
| 系统设置 | `/settings` | 页面建设中 |
|
||||
|
||||
平台提供两个入口:
|
||||
|
||||
@@ -61,4 +59,12 @@ bun run dev config.yaml
|
||||
|
||||
在 Workbench 工作台中,默认进入聊天室页面。左侧为会话列表,可新建和删除会话;右侧为聊天面板,输入消息后 AI 将流式回复。
|
||||
|
||||
未选择会话时,聊天面板显示欢迎页面,用户可直接输入消息发送,系统会自动创建新会话。会话标题在首次对话后自动生成。
|
||||
|
||||
消息支持以下操作(仅限最后一条消息):
|
||||
|
||||
- **复制**:所有消息均支持复制文本内容
|
||||
- **编辑**:最后一条用户消息可编辑,确认后重新发送
|
||||
- **重新生成**:最后一条 AI 消息可重新生成回复
|
||||
|
||||
使用聊天功能前,需先在 Admin 管理台的模型管理页面配置至少一个 AI 供应商和模型。新建会话时系统会自动选择第一个可用模型。
|
||||
|
||||
60
eslint-rules/enforce-catch-type.js
Normal file
60
eslint-rules/enforce-catch-type.js
Normal file
@@ -0,0 +1,60 @@
|
||||
export const enforceCatchType = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"强制 catch 子句使用 e: unknown,并用 instanceof Error 提取错误信息;空的 catch 块应添加注释说明原因",
|
||||
},
|
||||
messages: {
|
||||
missingTypeAnnotation:
|
||||
"catch 子句缺少类型注解。请使用 catch (e: unknown),然后用 e instanceof Error ? e.message : String(e) 提取错误信息。",
|
||||
nonUnknownType:
|
||||
"catch 的类型注解应为 unknown,切勿使用 any。请改为 catch (e: unknown),然后用 e instanceof Error ? e.message : String(e) 提取错误信息。",
|
||||
emptyCatchNoComment:
|
||||
"空的 catch 块应添加注释说明为什么忽略此异常。如果确有理由静默吞掉错误,请在该 catch body 内添加注释。",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
||||
|
||||
function isUnknownType(typeAnnotation) {
|
||||
if (!typeAnnotation) return false;
|
||||
const type = typeAnnotation.typeAnnotation;
|
||||
if (type?.type === "TSTypeReference") {
|
||||
return type.typeName?.name === "unknown";
|
||||
}
|
||||
if (type?.type === "TSUnknownKeyword") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasCommentsInBody(body) {
|
||||
if (!body) return false;
|
||||
return sourceCode.getCommentsInside(body).length > 0;
|
||||
}
|
||||
|
||||
function check(node) {
|
||||
const { param, body } = node;
|
||||
|
||||
if (param) {
|
||||
const typeAnnotation = param.typeAnnotation;
|
||||
if (!typeAnnotation) {
|
||||
context.report({ node: param, messageId: "missingTypeAnnotation" });
|
||||
} else if (!isUnknownType(typeAnnotation)) {
|
||||
context.report({ node: typeAnnotation, messageId: "nonUnknownType" });
|
||||
}
|
||||
}
|
||||
|
||||
if (body && body.type === "BlockStatement" && body.body.length === 0 && !hasCommentsInBody(body)) {
|
||||
context.report({ node: body, messageId: "emptyCatchNoComment" });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CatchClause: check,
|
||||
};
|
||||
},
|
||||
};
|
||||
59
eslint-rules/no-empty-function.js
Normal file
59
eslint-rules/no-empty-function.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export const noEmptyFunction = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "禁止空函数体。修复方式:在函数体内添加注释说明为何为空实现(如接口契约、测试桩、noop)。",
|
||||
},
|
||||
messages: {
|
||||
unexpected: "空函数体禁止使用。请在 {} 内添加注释说明原因,例如:/* 实现 Logger 接口契约,有意静默丢弃 */。",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
||||
|
||||
const allowedFunctionTypes = new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]);
|
||||
|
||||
function isEmptyBody(body) {
|
||||
return (
|
||||
body.type === "BlockStatement" && body.body.length === 0 && sourceCode.getCommentsInside(body).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
function hasDecorator(node) {
|
||||
return Array.isArray(node.decorators) && node.decorators.length > 0;
|
||||
}
|
||||
|
||||
function isPrivateOrProtectedConstructor(node) {
|
||||
if (node.parent?.type !== "MethodDefinition") return false;
|
||||
if (node.parent.kind !== "constructor") return false;
|
||||
const accessibility = node.parent.accessibility;
|
||||
return accessibility === "private" || accessibility === "protected";
|
||||
}
|
||||
|
||||
function isOverrideMethod(node) {
|
||||
if (node.parent?.type !== "MethodDefinition") return false;
|
||||
return node.parent.override === true;
|
||||
}
|
||||
|
||||
function check(node) {
|
||||
if (!allowedFunctionTypes.has(node.type)) return;
|
||||
if (!isEmptyBody(node.body)) return;
|
||||
if (hasDecorator(node)) return;
|
||||
if (isPrivateOrProtectedConstructor(node)) return;
|
||||
if (isOverrideMethod(node)) return;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpected",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: check,
|
||||
FunctionDeclaration: check,
|
||||
FunctionExpression: check,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -6,9 +6,15 @@ import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
import { enforceCatchType } from "./eslint-rules/enforce-catch-type.js";
|
||||
import { noEmptyFunction } from "./eslint-rules/no-empty-function.js";
|
||||
|
||||
const noDirectConsoleMessage =
|
||||
"后端运行时代码禁止直接使用 console.*;请通过注入的 Logger 实例输出日志,配置加载失败前使用 createConsoleFallback()。";
|
||||
|
||||
const noDirectConsoleFrontendMessage =
|
||||
"前端代码禁止直接使用 console.*;请使用 useLogger() hook(组件内)或 createConsoleLogger()(非组件纯函数)。";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
@@ -21,8 +27,11 @@ export default tseslint.config(
|
||||
".claude/**",
|
||||
".codex/**",
|
||||
".agents/**",
|
||||
".worktrees/**",
|
||||
"bin/**",
|
||||
"bun.lock",
|
||||
"data/**",
|
||||
"eslint-rules/**",
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
@@ -47,14 +56,38 @@ export default tseslint.config(
|
||||
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
|
||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/only-throw-error": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
"import/no-unresolved": ["error", { ignore: ["^bun:"] }],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
message:
|
||||
"禁止 throw 字面量。项目约定只允许 throw new Error(...) 或 throw new AppError(msg, statusCode)。Re-throw 已捕获的 Error 实例时使用 throw e。",
|
||||
selector: "ThrowStatement > Literal",
|
||||
},
|
||||
],
|
||||
"no-undef": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
plugins: {
|
||||
local: {
|
||||
rules: {
|
||||
"enforce-catch-type": enforceCatchType,
|
||||
"no-empty-function": noEmptyFunction,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"local/enforce-catch-type": "warn",
|
||||
"local/no-empty-function": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["eslint.config.js"],
|
||||
rules: {
|
||||
@@ -77,6 +110,7 @@ export default tseslint.config(
|
||||
},
|
||||
{
|
||||
files: ["src/web/**/*.{ts,tsx}"],
|
||||
ignores: ["src/web/**/logger.ts"],
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
@@ -103,6 +137,13 @@ export default tseslint.config(
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
message: noDirectConsoleFrontendMessage,
|
||||
selector: "MemberExpression[object.name='console']",
|
||||
},
|
||||
],
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
schema: fast-drive
|
||||
|
||||
context: |
|
||||
## 项目概览
|
||||
- 本项目为 Bun 全栈应用(Alfred·阿福),Bun 是唯一包管理器和运行时,严禁使用 npm、pnpm、yarn、npx、pnpx
|
||||
- docs/user/ 记录用户使用方法,docs/development/ 记录开发技术细节
|
||||
- 使用中文(注释、文档、交流),面向中文开发者
|
||||
- **优先阅读 docs/README.md** 获取文档路由、归属矩阵和影响分析规则
|
||||
- **其次阅读 docs/development/README.md** 获取开发规范、常用命令、质量门禁和全局规则
|
||||
- 文档文件名优先使用单个英文单词(usage.md、config.md、deploy.md、troubleshoot.md),目录上下文足以消歧时不在文件名重复限定词
|
||||
- 每次代码变更必须执行文档影响分析:
|
||||
- 用户可见行为、配置、部署、运行行为变更 → 更新 docs/user/ 对应文档
|
||||
- 开发流程、架构、测试、构建发布流程变更 → 更新 docs/development/ 对应文档
|
||||
- 项目定位、快速开始、核心能力列表、文档导航变更 → 更新 README.md
|
||||
- 文档同步规则或文档归属矩阵变更 → 更新 docs/README.md 和本文件
|
||||
- 无需更新文档时必须在收尾说明中说明原因
|
||||
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
|
||||
- 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试
|
||||
- 这是基于bun实现的前端后一体化项目,使用bun作为唯一包管理器,严禁使用pnpm、npm,使用bunx运行工具,严禁使用npx、pnpx
|
||||
- src/server目录下是基于bun实现的后端代码
|
||||
- 后端库使用优先级:Bun 内置 API > es-toolkit > 主流三方库 > 项目公共工具 > 自行实现
|
||||
- src/web目录下是基于Bun HTML import、React、Ant Design实现的前端代码
|
||||
- 前端最高规约:优先使用 antd 组件默认能力和组件 props 组合界面,具体组件、样式、数据流和测试细节遵循 docs/development/frontend.md
|
||||
- 前端样式管理:antd 组件/props/token 优先,AppShell 使用最小全局 CSS,页面和自有组件样式增长后使用就近 CSS Modules,默认不引入 Tailwind、UnoCSS、Sass、Less、CSS-in-JS 等额外样式体系
|
||||
- 前端样式红线:禁止组件内联 style、覆盖 antd 内部类名、使用 !important、硬编码色值
|
||||
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
|
||||
- 禁止创建git操作task
|
||||
- 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行
|
||||
- 优先使用提问工具对用户进行提问
|
||||
- 本项目为 Bun 全栈应用(Alfred·阿福),docs/user/ 记录用户使用方法,docs/development/ 记录开发技术细节
|
||||
- 本项目无需考虑向前兼容性
|
||||
|
||||
## 文档入口(按顺序阅读)
|
||||
- **优先阅读 docs/README.md** 获取文档路由、归属矩阵和影响分析规则
|
||||
- **其次阅读 docs/development/README.md** 获取完整开发规范、常用命令和质量门禁
|
||||
|
||||
## 全局红线
|
||||
- 前端禁止导入 src/server/ 的后端运行时实现
|
||||
- 后端运行时代码禁止直接使用 console.*,通过 Logger 实例输出
|
||||
- 新增逻辑必须编写完善的测试,不允许跳过任何测试
|
||||
- 每次代码变更必须执行文档影响分析(详见 docs/README.md)
|
||||
- 新增代码优先复用已有组件、工具、依赖库,不轻易引入新依赖
|
||||
|
||||
## Git 规范
|
||||
- 提交信息中文,格式"类型: 简短描述",类型:feat/fix/refactor/docs/style/test/chore
|
||||
- 禁止创建 git 操作 task
|
||||
|
||||
## 工作方式
|
||||
- 积极使用 subagent 并行独立子任务,节省上下文空间;能并行的步骤明确并行
|
||||
- subagent 仅用于只读收集和分析,禁止用于文件修改、代码生成、git 操作或依赖安装
|
||||
- 单个文件或目录只分配给一个 subagent,不重复分配;subagent 输出文件路径、行号和问题摘要,不输出大段源码
|
||||
- 主 agent 负责最终结论:去重、交叉验证、合并同根因问题
|
||||
- 优先使用提问工具对用户确认
|
||||
|
||||
rules:
|
||||
design:
|
||||
- fast-drive的design.md章节标题和正文使用中文;仅OpenSpec术语、文件名、schema字段名、命令和代码符号保留英文
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"@ai-sdk/openai-compatible": "^2.0.48",
|
||||
"@ai-sdk/react": "^3.0.195",
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@ant-design/x": "^2.7.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@tanstack/react-query": "^5.100.14",
|
||||
"ai": "^6.0.193",
|
||||
@@ -72,6 +71,8 @@
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router": "^7.15.1",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.8.1",
|
||||
"streamdown": "^2.5.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { ModelMessage } from "ai";
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { stepCountIs, streamText } from "ai";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import { wrap } from "../db/connection";
|
||||
import { models, providers } from "../db/schema";
|
||||
import { buildProviderRegistry } from "./registry";
|
||||
|
||||
const SYSTEM_PROMPT = "你是 Alfred 的 AI 助手。你可以帮助用户回答问题、分析数据和完成各种任务。请用中文回复。";
|
||||
|
||||
export interface AgentStreamOptions {
|
||||
db: Database;
|
||||
messages: IncomingMessage[];
|
||||
modelDbId: string;
|
||||
}
|
||||
|
||||
export interface IncomingMessage {
|
||||
content?: string;
|
||||
id?: string;
|
||||
parts?: Array<{ text?: string; type: string }>;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export function agentStream(options: AgentStreamOptions) {
|
||||
const db = wrap(options.db);
|
||||
|
||||
const modelRow = db.select().from(models).where(eq(models.id, options.modelDbId)).get();
|
||||
if (!modelRow) throw new Error(`模型不存在: ${options.modelDbId}`);
|
||||
|
||||
const providerRow = db.select().from(providers).where(eq(providers.id, modelRow.providerId)).get();
|
||||
if (!providerRow) throw new Error(`供应商不存在: ${modelRow.providerId}`);
|
||||
|
||||
const registry = buildProviderRegistry(options.db);
|
||||
const model = registry.languageModel(`${providerRow.id}:${modelRow.modelId}`);
|
||||
|
||||
return streamText({
|
||||
messages: toCoreMessages(options.messages),
|
||||
model,
|
||||
stopWhen: stepCountIs(1),
|
||||
system: SYSTEM_PROMPT,
|
||||
});
|
||||
}
|
||||
|
||||
export function extractTextContent(msg: IncomingMessage): string {
|
||||
return (
|
||||
msg.content ??
|
||||
(Array.isArray(msg.parts)
|
||||
? msg.parts
|
||||
.filter((p) => p.type === "text" && typeof p.text === "string")
|
||||
.map((p) => p.text!)
|
||||
.join("")
|
||||
: "")
|
||||
);
|
||||
}
|
||||
|
||||
function toCoreMessages(messages: IncomingMessage[]): ModelMessage[] {
|
||||
return messages.map((msg) => {
|
||||
const content =
|
||||
msg.content ??
|
||||
(Array.isArray(msg.parts)
|
||||
? msg.parts
|
||||
.filter((p) => p.type === "text" && typeof p.text === "string")
|
||||
.map((p) => p.text!)
|
||||
.join("")
|
||||
: "");
|
||||
|
||||
return { content, role: msg.role as ModelMessage["role"] } as ModelMessage;
|
||||
});
|
||||
}
|
||||
22
src/server/ai/agents/alfred-agent.ts
Normal file
22
src/server/ai/agents/alfred-agent.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type LanguageModel, stepCountIs, ToolLoopAgent } from "ai";
|
||||
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { createGetCurrentTime } from "../tools/get-current-time";
|
||||
|
||||
const SYSTEM_PROMPT = `你是 Alfred,一个 AI 助手。
|
||||
|
||||
## 输出规范
|
||||
- 使用中文回复
|
||||
- 代码块用 Markdown 围栏语法,标注语言
|
||||
- 给出结论时简洁直接,不要长篇铺垫
|
||||
- 不确定的事明确说"不确定"`;
|
||||
|
||||
export function createAlfredAgent(model: LanguageModel, logger?: Logger) {
|
||||
return new ToolLoopAgent({
|
||||
instructions: SYSTEM_PROMPT,
|
||||
model,
|
||||
stopWhen: stepCountIs(20),
|
||||
tools: { getCurrentTime: createGetCurrentTime(logger) },
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import { createProviderRegistry, generateText } from "ai";
|
||||
|
||||
import type { Logger } from "../logger";
|
||||
import type { AIProviderConfig } from "./types";
|
||||
|
||||
export function buildProviderRegistry(db: Database) {
|
||||
@@ -25,6 +26,7 @@ export function buildProviderRegistry(db: Database) {
|
||||
|
||||
export async function testModelConnection(
|
||||
config: AIProviderConfig & { modelId: string },
|
||||
logger: Logger,
|
||||
): Promise<{ message: string; ok: boolean }> {
|
||||
try {
|
||||
const provider = createProvider(config);
|
||||
@@ -36,12 +38,16 @@ export async function testModelConnection(
|
||||
return { message: "模型连接成功", ok: true };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logger.warn({ error: msg, modelId: config.modelId, providerType: config.type }, "模型连接测试失败");
|
||||
return { message: `模型连接失败:${msg}`, ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function testProviderConnection(config: AIProviderConfig): Promise<{ message: string; ok: boolean }> {
|
||||
const baseUrlResult = await probeBaseUrl(config.baseUrl);
|
||||
export async function testProviderConnection(
|
||||
config: AIProviderConfig,
|
||||
logger: Logger,
|
||||
): Promise<{ message: string; ok: boolean }> {
|
||||
const baseUrlResult = await probeBaseUrl(config.baseUrl, logger);
|
||||
if (!baseUrlResult.ok) return baseUrlResult;
|
||||
|
||||
const modelsUrl = buildModelsUrl(config.baseUrl);
|
||||
@@ -82,6 +88,7 @@ export async function testProviderConnection(config: AIProviderConfig): Promise<
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logger.warn({ error: msg, providerType: config.type }, "供应商 /models 请求异常");
|
||||
return { message: `Base URL 可连接,但 /models 请求异常:${msg};可检查 URL 或忽略此提示。`, ok: true };
|
||||
}
|
||||
}
|
||||
@@ -154,7 +161,7 @@ function getProviders(db: Database): Array<{
|
||||
}>;
|
||||
}
|
||||
|
||||
async function probeBaseUrl(baseUrl: string): Promise<{ message: string; ok: boolean }> {
|
||||
async function probeBaseUrl(baseUrl: string, logger: Logger): Promise<{ message: string; ok: boolean }> {
|
||||
try {
|
||||
await fetch(baseUrl, {
|
||||
method: "HEAD",
|
||||
@@ -163,6 +170,7 @@ async function probeBaseUrl(baseUrl: string): Promise<{ message: string; ok: boo
|
||||
return { message: "Base URL 可连接", ok: true };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logger.warn({ baseUrl, error: msg }, "Base URL 不可达");
|
||||
return { message: `Base URL 不可达:${msg}`, ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
42
src/server/ai/tools/get-current-time.ts
Normal file
42
src/server/ai/tools/get-current-time.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
export function createGetCurrentTime(logger?: Logger) {
|
||||
return tool({
|
||||
description: "获取当前日期和时间。可选指定时区,默认返回本地时间。",
|
||||
execute: ({ timezone }) => Promise.resolve(formatCurrentTime(timezone, logger)),
|
||||
inputSchema: z.object({
|
||||
timezone: z.string().optional().describe("IANA 时区名称,如 'Asia/Shanghai'、'America/New_York'"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCurrentTime(timezone?: string, logger?: Logger) {
|
||||
const now = new Date();
|
||||
const iso = now.toISOString();
|
||||
const timestamp = now.getTime();
|
||||
|
||||
let local: string;
|
||||
if (timezone) {
|
||||
try {
|
||||
local = new Intl.DateTimeFormat("zh-CN", {
|
||||
dateStyle: "full",
|
||||
timeStyle: "long",
|
||||
timeZone: timezone,
|
||||
}).format(now);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logger?.warn({ error: msg, timezone }, "无效时区,使用默认格式");
|
||||
local = now.toString();
|
||||
}
|
||||
} else {
|
||||
local = new Intl.DateTimeFormat("zh-CN", {
|
||||
dateStyle: "full",
|
||||
timeStyle: "long",
|
||||
}).format(now);
|
||||
}
|
||||
|
||||
return { iso, local, timestamp };
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
|
||||
|
||||
try {
|
||||
logger = await buildLogger(config.logging, options.mode, options.version);
|
||||
} catch (logInitError) {
|
||||
} catch (logInitError: unknown) {
|
||||
createFallback().fatal(
|
||||
`日志初始化失败: ${logInitError instanceof Error ? logInitError.message : String(logInitError)}`,
|
||||
);
|
||||
@@ -83,7 +83,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
|
||||
staticAssets: options.staticAssets,
|
||||
version: options.version,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (logger) {
|
||||
logger.fatal({ error: error instanceof Error ? error.message : String(error) }, "启动失败");
|
||||
logger.flush();
|
||||
|
||||
@@ -192,7 +192,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
if (bytes <= 0) {
|
||||
issues.push(issue("invalid-value", "server.logging.file.rotation.size", "滚动大小必须为正整数字节数"));
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
issues.push(
|
||||
issue(
|
||||
"invalid-value",
|
||||
|
||||
@@ -2,7 +2,8 @@ import type Database from "bun:sqlite";
|
||||
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
import type { Conversation, Message } from "../../shared/api";
|
||||
import type { Conversation, Message, UpdateConversationRequest } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { conversations, messages, models } from "./schema";
|
||||
@@ -10,6 +11,7 @@ import { conversations, messages, models } from "./schema";
|
||||
export function createConversation(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
logger: Logger,
|
||||
defaultModelId?: string,
|
||||
): { conversation: Conversation } | { error: string; status: number } {
|
||||
const db = wrap(raw);
|
||||
@@ -50,6 +52,7 @@ export function createMessage(
|
||||
parts?: string;
|
||||
role: "assistant" | "system" | "user";
|
||||
},
|
||||
_logger: Logger,
|
||||
): Message {
|
||||
const db = wrap(raw);
|
||||
const id = crypto.randomUUID();
|
||||
@@ -78,6 +81,7 @@ export function createMessages(
|
||||
parts?: string;
|
||||
role: "assistant" | "system" | "user";
|
||||
}>,
|
||||
_logger: Logger,
|
||||
): Message[] {
|
||||
const db = wrap(raw);
|
||||
const now = new Date().toISOString();
|
||||
@@ -102,7 +106,11 @@ export function createMessages(
|
||||
return results;
|
||||
}
|
||||
|
||||
export function deleteConversation(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
export function deleteConversation(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(conversations).where(eq(conversations.id, id)).get();
|
||||
if (!existing) return { error: "会话不存在", status: 404 };
|
||||
@@ -150,6 +158,34 @@ export function listMessages(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateConversation(
|
||||
raw: Database,
|
||||
id: string,
|
||||
data: UpdateConversationRequest,
|
||||
_logger: Logger,
|
||||
): { conversation: Conversation } | { error: string; status: number } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(conversations).where(eq(conversations.id, id)).get();
|
||||
if (!existing) return { error: "会话不存在", status: 404 };
|
||||
|
||||
const updates: { modelId?: string; title?: string; updatedAt: string } = { updatedAt: new Date().toISOString() };
|
||||
|
||||
if (data.modelId !== undefined) {
|
||||
const model = db.select().from(models).where(eq(models.id, data.modelId)).get();
|
||||
if (!model) return { error: "模型不存在", status: 400 };
|
||||
updates.modelId = data.modelId;
|
||||
}
|
||||
|
||||
if (data.title !== undefined) {
|
||||
updates.title = data.title;
|
||||
}
|
||||
|
||||
db.update(conversations).set(updates).where(eq(conversations.id, id)).run();
|
||||
|
||||
const row = db.select().from(conversations).where(eq(conversations.id, id)).get();
|
||||
return { conversation: toConversation(row!) };
|
||||
}
|
||||
|
||||
export function updateConversationTimestamp(raw: Database, id: string): void {
|
||||
const db = wrap(raw);
|
||||
db.update(conversations).set({ updatedAt: new Date().toISOString() }).where(eq(conversations.id, id)).run();
|
||||
|
||||
@@ -11,4 +11,13 @@ export {
|
||||
} from "./conversations";
|
||||
export { loadMigrationsFromDir, type MigrationRecord } from "./load-migrations";
|
||||
export { runMigrations } from "./migrate";
|
||||
export {
|
||||
createModel,
|
||||
deleteModel,
|
||||
getModel,
|
||||
getModelsByProviderId,
|
||||
getModelWithProvider,
|
||||
listModels,
|
||||
updateModel,
|
||||
} from "./models";
|
||||
export { conversations, messages, projects, schemaMigrations } from "./schema";
|
||||
|
||||
@@ -35,9 +35,15 @@ export function runMigrations(db: Database, migrations: MigrationRecord[], dataD
|
||||
|
||||
db.transaction(() => {
|
||||
for (const migration of pending) {
|
||||
try {
|
||||
logger.info({ id: migration.id }, "执行 migration");
|
||||
db.exec(migration.sql);
|
||||
insertApplied.run(migration.id, migration.checksum, new Date().toISOString());
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logger.error({ error: msg, id: migration.id }, "migration 执行失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type Database from "bun:sqlite";
|
||||
import { desc, eq, like, or, sql } from "drizzle-orm";
|
||||
|
||||
import type { CreateModelRequest, Model, ModelCapability, UpdateModelRequest } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { models, providers } from "./schema";
|
||||
@@ -10,6 +11,7 @@ import { models, providers } from "./schema";
|
||||
export function createModel(
|
||||
raw: Database,
|
||||
request: CreateModelRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
|
||||
@@ -49,6 +51,7 @@ export function createModel(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "该供应商下模型 ID 已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "create", table: "models" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -56,7 +59,11 @@ export function createModel(
|
||||
return { model: toModel(row!) };
|
||||
}
|
||||
|
||||
export function deleteModel(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
export function deleteModel(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
if (!existing) return { error: "模型不存在", status: 404 };
|
||||
@@ -83,6 +90,38 @@ export function getModelsByProviderId(raw: Database, providerId: string): number
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
|
||||
export function getModelWithProvider(
|
||||
raw: Database,
|
||||
modelId: string,
|
||||
):
|
||||
| { error: string; status: number }
|
||||
| {
|
||||
model: { modelId: string; name: string; providerId: string };
|
||||
provider: { apiKey: string; baseUrl: string; id: string; type: string };
|
||||
} {
|
||||
const db = wrap(raw);
|
||||
const row = db.select().from(models).where(eq(models.id, modelId)).get();
|
||||
|
||||
if (!row) return { error: "模型不存在", status: 404 };
|
||||
|
||||
const providerRow = db.select().from(providers).where(eq(providers.id, row.providerId)).get();
|
||||
if (!providerRow) return { error: "供应商不存在", status: 404 };
|
||||
|
||||
return {
|
||||
model: {
|
||||
modelId: row.modelId,
|
||||
name: row.name,
|
||||
providerId: row.providerId,
|
||||
},
|
||||
provider: {
|
||||
apiKey: providerRow.apiKey,
|
||||
baseUrl: providerRow.baseUrl,
|
||||
id: providerRow.id,
|
||||
type: providerRow.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function listModels(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number; providerId?: string },
|
||||
@@ -111,6 +150,7 @@ export function updateModel(
|
||||
raw: Database,
|
||||
id: string,
|
||||
request: UpdateModelRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
@@ -164,6 +204,7 @@ export function updateModel(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "该供应商下模型 ID 已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "update", table: "models" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@ import type Database from "bun:sqlite";
|
||||
import { desc, eq, like, or } from "drizzle-orm";
|
||||
|
||||
import type { CreateProjectRequest, Project, ProjectStatus, UpdateProjectRequest } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { projects } from "./schema";
|
||||
|
||||
export function archiveProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
|
||||
export function archiveProject(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { project: Project } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
if (!existing) return { error: "项目不存在", status: 404 };
|
||||
@@ -23,6 +28,7 @@ export function archiveProject(raw: Database, id: string): { error: string; stat
|
||||
export function createProject(
|
||||
raw: Database,
|
||||
request: CreateProjectRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { project: Project } {
|
||||
const db = wrap(raw);
|
||||
const name = request.name.trim();
|
||||
@@ -50,6 +56,7 @@ export function createProject(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "项目名称已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "create", table: "projects" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -57,7 +64,11 @@ export function createProject(
|
||||
return { project: toProject(row!) };
|
||||
}
|
||||
|
||||
export function deleteProject(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
export function deleteProject(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
if (!existing) return { error: "项目不存在", status: 404 };
|
||||
@@ -99,7 +110,11 @@ export function listProjects(
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
|
||||
export function restoreProject(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { project: Project } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
if (!existing) return { error: "项目不存在", status: 404 };
|
||||
@@ -116,6 +131,7 @@ export function updateProject(
|
||||
raw: Database,
|
||||
id: string,
|
||||
request: UpdateProjectRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { project: Project } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
@@ -150,6 +166,7 @@ export function updateProject(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "项目名称已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "update", table: "projects" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type Database from "bun:sqlite";
|
||||
import { desc, eq, like } from "drizzle-orm";
|
||||
|
||||
import type { CreateProviderRequest, Provider, ProviderOption, UpdateProviderRequest } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { providers } from "./schema";
|
||||
@@ -10,6 +11,7 @@ import { providers } from "./schema";
|
||||
export function createProvider(
|
||||
raw: Database,
|
||||
request: CreateProviderRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const name = request.name.trim();
|
||||
@@ -41,6 +43,7 @@ export function createProvider(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "供应商名称已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "create", table: "providers" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -48,7 +51,11 @@ export function createProvider(
|
||||
return { provider: toProvider(row!) };
|
||||
}
|
||||
|
||||
export function deleteProvider(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
export function deleteProvider(
|
||||
raw: Database,
|
||||
id: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
if (!existing) return { error: "供应商不存在", status: 404 };
|
||||
@@ -100,6 +107,7 @@ export function updateProvider(
|
||||
raw: Database,
|
||||
id: string,
|
||||
request: UpdateProviderRequest,
|
||||
logger: Logger,
|
||||
): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
@@ -142,6 +150,7 @@ export function updateProvider(
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "供应商名称已存在", status: 409 };
|
||||
}
|
||||
logger.error({ error: msg, operation: "update", table: "providers" }, "数据库操作失败");
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateConversationRequest, RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { createConversation } from "../../db/conversations";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleCreateConversation(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleCreateConversation(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const projectId = url.pathname.split("/")[3];
|
||||
|
||||
@@ -16,14 +22,16 @@ export async function handleCreateConversation(req: Request, db: Database, mode:
|
||||
let body: CreateConversationRequest = {};
|
||||
try {
|
||||
body = (await req.json()) as CreateConversationRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
// empty body is ok, defaults will be used
|
||||
}
|
||||
|
||||
const result = createConversation(db, validated.id, body.modelId);
|
||||
const result = createConversation(db, validated.id, logger, body.modelId);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ conversationId: result.conversation.id, projectId: validated.id }, "会话创建成功");
|
||||
return jsonResponse({ conversation: result.conversation }, { mode, status: 201 });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { deleteConversation, getConversation } from "../../db/conversations";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteConversation(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleDeleteConversation(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const parts = new URL(req.url).pathname.split("/");
|
||||
const projectId = parts[3];
|
||||
const conversationId = parts[5];
|
||||
@@ -26,10 +27,11 @@ export function handleDeleteConversation(req: Request, db: Database, mode: Runti
|
||||
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
|
||||
}
|
||||
|
||||
const result = deleteConversation(db, validatedConv.id);
|
||||
const result = deleteConversation(db, validatedConv.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ conversationId: validatedConv.id }, "会话删除成功");
|
||||
return jsonResponse({ success: true }, { mode });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getConversation } from "../../db/conversations";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetConversation(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleGetConversation(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const parts = new URL(req.url).pathname.split("/");
|
||||
const projectId = parts[3];
|
||||
const conversationId = parts[5];
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { listConversations } from "../../db/conversations";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validateIdParam, validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListConversations(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListConversations(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const projectId = url.pathname.split("/")[3];
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getConversation, listMessages } from "../../db/conversations";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam, validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListMessages(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListMessages(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const parts = new URL(req.url).pathname.split("/");
|
||||
const projectId = parts[3];
|
||||
const conversationId = parts[5];
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { IncomingMessage } from "../../ai/agent-stream";
|
||||
import { createAgentUIStreamResponse, generateText, type UIMessage } from "ai";
|
||||
|
||||
import { agentStream, extractTextContent } from "../../ai/agent-stream";
|
||||
import { createMessage, getConversation, updateConversationTimestamp } from "../../db/conversations";
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { createAlfredAgent } from "../../ai/agents/alfred-agent";
|
||||
import { buildProviderRegistry } from "../../ai/registry";
|
||||
import {
|
||||
createMessage,
|
||||
getConversation,
|
||||
updateConversation,
|
||||
updateConversationTimestamp,
|
||||
} from "../../db/conversations";
|
||||
import { getModelWithProvider } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleSendChat(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleSendChat(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const projectId = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(projectId ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
let body: { conversationId?: string; messages?: IncomingMessage[] };
|
||||
let body: { conversationId?: string; messages?: UIMessage[] };
|
||||
try {
|
||||
body = (await req.json()) as typeof body;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -46,46 +56,109 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
|
||||
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
|
||||
}
|
||||
|
||||
for (const msg of body.messages ?? []) {
|
||||
createMessage(db, {
|
||||
content: extractTextContent(msg),
|
||||
conversationId: conversation.id,
|
||||
role: (msg.role ?? "user") as "assistant" | "system" | "user",
|
||||
});
|
||||
}
|
||||
|
||||
updateConversationTimestamp(db, conversation.id);
|
||||
|
||||
try {
|
||||
const result = agentStream({
|
||||
const lastMsg = body.messages[body.messages.length - 1];
|
||||
if (lastMsg?.role === "user") {
|
||||
const content =
|
||||
lastMsg.parts
|
||||
?.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("") ?? "";
|
||||
createMessage(
|
||||
db,
|
||||
messages: body.messages,
|
||||
modelDbId: conversation.modelId,
|
||||
});
|
||||
|
||||
const stream = result.toUIMessageStreamResponse();
|
||||
|
||||
const saveReply = async () => {
|
||||
try {
|
||||
const fullContent = await result.text;
|
||||
if (fullContent) {
|
||||
createMessage(db, {
|
||||
content: fullContent,
|
||||
{
|
||||
content,
|
||||
conversationId: conversation.id,
|
||||
role: "assistant",
|
||||
});
|
||||
parts: JSON.stringify(lastMsg.parts ?? []),
|
||||
role: "user",
|
||||
},
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
updateConversationTimestamp(db, conversation.id);
|
||||
}
|
||||
} catch {
|
||||
// stream ended without content, nothing to persist
|
||||
}
|
||||
};
|
||||
|
||||
void saveReply();
|
||||
let model;
|
||||
try {
|
||||
const result = getModelWithProvider(db, conversation.modelId);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return stream;
|
||||
const registry = buildProviderRegistry(db);
|
||||
model = registry.languageModel(`${result.provider.id}:${result.model.modelId}`);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return jsonResponse(createApiError(`模型初始化失败:${msg}`, 500), { mode, status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
const firstUserMsg = body.messages.find((m) => m.role === "user");
|
||||
const firstUserText =
|
||||
firstUserMsg?.parts
|
||||
?.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("") ?? "";
|
||||
|
||||
if (conversation.title === "新会话" && firstUserText) {
|
||||
generateConversationTitle(firstUserText, model, db, conversation.id, logger);
|
||||
}
|
||||
|
||||
const agent = createAlfredAgent(model);
|
||||
return await createAgentUIStreamResponse({
|
||||
agent,
|
||||
onFinish: ({ responseMessage }) => {
|
||||
const text = responseMessage.parts
|
||||
.filter((p): p is { text: string; type: "text" } => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("");
|
||||
createMessage(
|
||||
db,
|
||||
{
|
||||
content: text,
|
||||
conversationId: conversation.id,
|
||||
parts: JSON.stringify(responseMessage.parts),
|
||||
role: "assistant",
|
||||
},
|
||||
logger,
|
||||
);
|
||||
updateConversationTimestamp(db, conversation.id);
|
||||
},
|
||||
uiMessages: body.messages,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return jsonResponse(createApiError(`AI 调用失败:${msg}`, 500), { mode, status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
function generateConversationTitle(
|
||||
firstUserText: string,
|
||||
model: ReturnType<ReturnType<typeof buildProviderRegistry>["languageModel"]>,
|
||||
db: Database,
|
||||
conversationId: string,
|
||||
logger: Logger,
|
||||
): void {
|
||||
if (firstUserText.length <= 5) {
|
||||
updateConversation(db, conversationId, { title: firstUserText }, logger);
|
||||
return;
|
||||
}
|
||||
|
||||
void generateText({
|
||||
model,
|
||||
prompt: `请根据以下对话开头生成一个简短标题(不超过10个字):${firstUserText}`,
|
||||
system: "你是一个标题生成助手,只返回标题文本,不要解释。",
|
||||
})
|
||||
.then((result) => {
|
||||
const title = result.text.trim().slice(0, 10);
|
||||
updateConversation(db, conversationId, { title: title || firstUserText.slice(0, 10) }, logger);
|
||||
})
|
||||
.catch((titleError: unknown) => {
|
||||
const titleMsg = titleError instanceof Error ? titleError.message : String(titleError);
|
||||
logger.error({ conversationId, error: titleMsg }, "标题生成失败");
|
||||
try {
|
||||
updateConversation(db, conversationId, { title: firstUserText.slice(0, 10) }, logger);
|
||||
} catch {
|
||||
logger.error({ conversationId }, "标题兜底更新失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
55
src/server/routes/chat/update.ts
Normal file
55
src/server/routes/chat/update.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateConversationRequest } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getConversation, updateConversation } from "../../db/conversations";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateConversation(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const parts = url.pathname.split("/");
|
||||
const projectId = parts[3];
|
||||
const conversationId = parts[5];
|
||||
|
||||
const validatedProject = validateIdParam(projectId ?? "", mode);
|
||||
if (validatedProject instanceof Response) return validatedProject;
|
||||
|
||||
const validatedConv = validateIdParam(conversationId ?? "", mode);
|
||||
if (validatedConv instanceof Response) return validatedConv;
|
||||
|
||||
const existing = getConversation(db, validatedConv.id);
|
||||
if ("error" in existing) {
|
||||
return jsonResponse(createApiError(existing.error, existing.status), { mode, status: existing.status });
|
||||
}
|
||||
|
||||
if (existing.conversation.projectId !== validatedProject.id) {
|
||||
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
|
||||
}
|
||||
|
||||
let body: UpdateConversationRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateConversationRequest;
|
||||
} 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.modelId === undefined && body.title === undefined) {
|
||||
return jsonResponse(createApiError("至少需要传 modelId 或 title", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const result = updateConversation(db, validatedConv.id, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ conversationId: result.conversation.id }, "会话更新成功");
|
||||
return jsonResponse({ conversation: result.conversation }, { mode });
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RuntimeMode } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { createMetaResponse, jsonResponse } from "../helpers";
|
||||
|
||||
export function handleMeta(mode: RuntimeMode, version: string): Response {
|
||||
export function handleMeta(mode: RuntimeMode, version: string, _logger: Logger): Response {
|
||||
return jsonResponse(createMetaResponse(version), { mode });
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateModelRequest, RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { MODEL_CAPABILITIES } from "../../../shared/api";
|
||||
import { createModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleCreateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleCreateModel(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
let body: CreateModelRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateModelRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -44,11 +51,15 @@ export async function handleCreateModel(req: Request, db: Database, mode: Runtim
|
||||
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
|
||||
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
|
||||
|
||||
const result = createModel(db, body);
|
||||
const result = createModel(db, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ modelId: result.model.id, name: result.model.name, providerId: result.model.providerId },
|
||||
"模型创建成功",
|
||||
);
|
||||
return jsonResponse(result, { mode, status: 201 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { deleteModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleDeleteModel(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = deleteModel(db, validated.id);
|
||||
const result = deleteModel(db, validated.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ modelId: validated.id }, "模型删除成功");
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleGetModel(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { listModels } from "../../db/models";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListModels(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListModels(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, TestModelRequest } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { testModelConnection } from "../../ai/registry";
|
||||
import { getProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleTestModelConfig(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleTestModelConfig(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
let body: TestModelRequest;
|
||||
try {
|
||||
body = (await req.json()) as TestModelRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -30,13 +37,23 @@ export async function handleTestModelConfig(req: Request, db: Database, mode: Ru
|
||||
});
|
||||
}
|
||||
|
||||
const testResult = await testModelConnection({
|
||||
const testResult = await testModelConnection(
|
||||
{
|
||||
apiKey: providerResult.provider.apiKey,
|
||||
baseUrl: providerResult.provider.baseUrl,
|
||||
modelId: body.modelId,
|
||||
name: providerResult.provider.name,
|
||||
type: providerResult.provider.type,
|
||||
});
|
||||
},
|
||||
logger,
|
||||
);
|
||||
|
||||
if (!testResult.ok) {
|
||||
logger.warn(
|
||||
{ message: testResult.message, modelId: body.modelId, providerId: body.providerId },
|
||||
"模型连接测试失败",
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse({ modelTestResponse: testResult }, { mode });
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateModelRequest } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { MODEL_CAPABILITIES } from "../../../shared/api";
|
||||
import { updateModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleUpdateModel(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
@@ -17,7 +23,8 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
|
||||
let body: UpdateModelRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateModelRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -40,11 +47,12 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
|
||||
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
|
||||
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
|
||||
|
||||
const result = updateModel(db, validated.id, body);
|
||||
const result = updateModel(db, validated.id, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ modelId: result.model.id }, "模型更新成功");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { archiveProject } from "../../db/projects";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleArchiveProject(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleArchiveProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = archiveProject(db, validated.id);
|
||||
const result = archiveProject(db, validated.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ projectId: validated.id }, "项目归档成功");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateProjectRequest, RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { createProject } from "../../db/projects";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleCreateProject(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleCreateProject(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
let body: CreateProjectRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateProjectRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -17,10 +24,11 @@ export async function handleCreateProject(req: Request, db: Database, mode: Runt
|
||||
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const result = createProject(db, body);
|
||||
const result = createProject(db, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ name: result.project.name, projectId: result.project.id }, "项目创建成功");
|
||||
return jsonResponse(result, { mode, status: 201 });
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { deleteProject } from "../../db/projects";
|
||||
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteProject(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleDeleteProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = parseIdFromUrl(url);
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = deleteProject(db, validated.id);
|
||||
const result = deleteProject(db, validated.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ projectId: validated.id }, "项目删除成功");
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getProject } from "../../db/projects";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetProject(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleGetProject(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { listProjects } from "../../db/projects";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListProjects(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListProjects(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { restoreProject } from "../../db/projects";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleRestoreProject(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleRestoreProject(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = restoreProject(db, validated.id);
|
||||
const result = restoreProject(db, validated.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ projectId: validated.id }, "项目恢复成功");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateProjectRequest } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { updateProject } from "../../db/projects";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateProject(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleUpdateProject(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
@@ -16,18 +22,22 @@ export async function handleUpdateProject(req: Request, db: Database, mode: Runt
|
||||
let body: UpdateProjectRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateProjectRequest;
|
||||
} catch {
|
||||
} 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 && !body.description && body.name !== "" && body.description !== "") {
|
||||
const hasName = body.name !== undefined && body.name.trim() !== "";
|
||||
const hasDescription = body.description !== undefined && body.description.trim() !== "";
|
||||
if (!hasName && !hasDescription) {
|
||||
return jsonResponse(createApiError("At least one of name or description is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const result = updateProject(db, validated.id, body);
|
||||
const result = updateProject(db, validated.id, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ projectId: result.project.id }, "项目更新成功");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { createProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleCreateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleCreateProvider(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
let body: CreateProviderRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateProviderRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -32,10 +39,14 @@ export async function handleCreateProvider(req: Request, db: Database, mode: Run
|
||||
});
|
||||
}
|
||||
|
||||
const result = createProvider(db, body);
|
||||
const result = createProvider(db, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ name: result.provider.name, providerId: result.provider.id, type: result.provider.type },
|
||||
"供应商创建成功",
|
||||
);
|
||||
return jsonResponse(result, { mode, status: 201 });
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getModelsByProviderId } from "../../db/models";
|
||||
import { deleteProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse, parseIdFromUrl } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = parseIdFromUrl(url);
|
||||
|
||||
@@ -19,10 +20,11 @@ export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMo
|
||||
return jsonResponse(createApiError("该供应商下存在模型,无法删除", 409), { mode, status: 409 });
|
||||
}
|
||||
|
||||
const result = deleteProvider(db, validated.id);
|
||||
const result = deleteProvider(db, validated.id, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ providerId: validated.id }, "供应商删除成功");
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { getProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleGetProvider(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { listProviders } from "../../db/providers";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const url = new URL(req.url);
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { listProviderOptions } from "../../db/providers";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
|
||||
export function handleListProviderOptions(db: Database, mode: RuntimeMode): Response {
|
||||
export function handleListProviderOptions(db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
return jsonResponse({ items: listProviderOptions(db) }, { mode });
|
||||
}
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { testProviderConnection } from "../../ai/registry";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleTestProviderConfig(req: Request, _db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
const validated = await readProviderConfig(req, mode);
|
||||
export async function handleTestProviderConfig(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const validated = await readProviderConfig(req, mode, logger);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const testResult = await testProviderConnection({
|
||||
const testResult = await testProviderConnection(
|
||||
{
|
||||
apiKey: validated.apiKey,
|
||||
baseUrl: validated.baseUrl,
|
||||
name: validated.name,
|
||||
type: validated.type,
|
||||
});
|
||||
},
|
||||
logger,
|
||||
);
|
||||
|
||||
if (!testResult.ok) {
|
||||
logger.warn({ message: testResult.message, name: validated.name, type: validated.type }, "供应商连接测试失败");
|
||||
}
|
||||
return jsonResponse({ providerTestResponse: testResult }, { mode });
|
||||
}
|
||||
|
||||
async function readProviderConfig(req: Request, mode: RuntimeMode): Promise<CreateProviderRequest | Response> {
|
||||
async function readProviderConfig(
|
||||
req: Request,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<CreateProviderRequest | Response> {
|
||||
let body: CreateProviderRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateProviderRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateProviderRequest } from "../../../shared/api";
|
||||
import type { Logger } from "../../logger";
|
||||
|
||||
import { updateProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
export async function handleUpdateProvider(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
@@ -16,7 +22,8 @@ export async function handleUpdateProvider(req: Request, db: Database, mode: Run
|
||||
let body: UpdateProviderRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateProviderRequest;
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
logger.warn({ error: e instanceof Error ? e.message : String(e) }, "请求 JSON 解析失败");
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
@@ -27,10 +34,18 @@ export async function handleUpdateProvider(req: Request, db: Database, mode: Run
|
||||
});
|
||||
}
|
||||
|
||||
const result = updateProvider(db, validated.id, body);
|
||||
if (body.name === undefined && body.baseUrl === undefined && body.apiKey === undefined && body.type === undefined) {
|
||||
return jsonResponse(createApiError("至少需要提供 name, baseUrl, apiKey 或 type 中的一个字段", 400), {
|
||||
mode,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const result = updateProvider(db, validated.id, body, logger);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
logger.info({ providerId: result.provider.id }, "供应商更新成功");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async () => {
|
||||
const resolvedVersion = await resolveVersion();
|
||||
return handleMeta(mode, resolvedVersion);
|
||||
return handleMeta(mode, resolvedVersion, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -52,7 +52,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleListModels } = await import("./routes/models/list");
|
||||
return handleListModels(req, db, mode);
|
||||
return handleListModels(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -60,7 +60,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleCreateModel } = await import("./routes/models/create");
|
||||
return handleCreateModel(req, db, mode);
|
||||
return handleCreateModel(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -70,7 +70,7 @@ export function startServer(options: StartServerOptions) {
|
||||
DELETE: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleDeleteModel } = await import("./routes/models/delete");
|
||||
return handleDeleteModel(req, db, mode);
|
||||
return handleDeleteModel(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -78,7 +78,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleGetModel } = await import("./routes/models/get");
|
||||
return handleGetModel(req, db, mode);
|
||||
return handleGetModel(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -86,7 +86,7 @@ export function startServer(options: StartServerOptions) {
|
||||
PATCH: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleUpdateModel } = await import("./routes/models/update");
|
||||
return handleUpdateModel(req, db, mode);
|
||||
return handleUpdateModel(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -96,7 +96,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleTestModelConfig } = await import("./routes/models/test");
|
||||
return handleTestModelConfig(req, db, mode);
|
||||
return handleTestModelConfig(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -106,7 +106,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleListProjects } = await import("./routes/projects/list");
|
||||
return handleListProjects(req, db, mode);
|
||||
return handleListProjects(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -114,7 +114,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleCreateProject } = await import("./routes/projects/create");
|
||||
return handleCreateProject(req, db, mode);
|
||||
return handleCreateProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -124,7 +124,7 @@ export function startServer(options: StartServerOptions) {
|
||||
DELETE: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleDeleteProject } = await import("./routes/projects/delete");
|
||||
return handleDeleteProject(req, db, mode);
|
||||
return handleDeleteProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -132,7 +132,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleGetProject } = await import("./routes/projects/get");
|
||||
return handleGetProject(req, db, mode);
|
||||
return handleGetProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -140,7 +140,7 @@ export function startServer(options: StartServerOptions) {
|
||||
PATCH: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleUpdateProject } = await import("./routes/projects/update");
|
||||
return handleUpdateProject(req, db, mode);
|
||||
return handleUpdateProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -150,7 +150,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleArchiveProject } = await import("./routes/projects/archive");
|
||||
return handleArchiveProject(req, db, mode);
|
||||
return handleArchiveProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -160,7 +160,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleSendChat } = await import("./routes/chat/send");
|
||||
return handleSendChat(req, db, mode);
|
||||
return handleSendChat(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -170,7 +170,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleListConversations } = await import("./routes/chat/list");
|
||||
return handleListConversations(req, db, mode);
|
||||
return handleListConversations(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -178,7 +178,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleCreateConversation } = await import("./routes/chat/create");
|
||||
return handleCreateConversation(req, db, mode);
|
||||
return handleCreateConversation(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -188,7 +188,7 @@ export function startServer(options: StartServerOptions) {
|
||||
DELETE: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleDeleteConversation } = await import("./routes/chat/delete");
|
||||
return handleDeleteConversation(req, db, mode);
|
||||
return handleDeleteConversation(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -196,7 +196,15 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleGetConversation } = await import("./routes/chat/get");
|
||||
return handleGetConversation(req, db, mode);
|
||||
return handleGetConversation(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
),
|
||||
PATCH: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleUpdateConversation } = await import("./routes/chat/update");
|
||||
return handleUpdateConversation(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -206,7 +214,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleListMessages } = await import("./routes/chat/messages");
|
||||
return handleListMessages(req, db, mode);
|
||||
return handleListMessages(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -216,7 +224,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleRestoreProject } = await import("./routes/projects/restore");
|
||||
return handleRestoreProject(req, db, mode);
|
||||
return handleRestoreProject(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -226,7 +234,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleListProviders } = await import("./routes/providers/list");
|
||||
return handleListProviders(req, db, mode);
|
||||
return handleListProviders(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -234,7 +242,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleCreateProvider } = await import("./routes/providers/create");
|
||||
return handleCreateProvider(req, db, mode);
|
||||
return handleCreateProvider(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -244,7 +252,7 @@ export function startServer(options: StartServerOptions) {
|
||||
DELETE: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleDeleteProvider } = await import("./routes/providers/delete");
|
||||
return handleDeleteProvider(req, db, mode);
|
||||
return handleDeleteProvider(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -252,7 +260,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleGetProvider } = await import("./routes/providers/get");
|
||||
return handleGetProvider(req, db, mode);
|
||||
return handleGetProvider(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -260,7 +268,7 @@ export function startServer(options: StartServerOptions) {
|
||||
PATCH: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleUpdateProvider } = await import("./routes/providers/update");
|
||||
return handleUpdateProvider(req, db, mode);
|
||||
return handleUpdateProvider(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -270,7 +278,7 @@ export function startServer(options: StartServerOptions) {
|
||||
GET: withErrorHandler(
|
||||
async () => {
|
||||
const { handleListProviderOptions } = await import("./routes/providers/options");
|
||||
return handleListProviderOptions(db, mode);
|
||||
return handleListProviderOptions(db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
@@ -280,7 +288,7 @@ export function startServer(options: StartServerOptions) {
|
||||
POST: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleTestProviderConfig } = await import("./routes/providers/test");
|
||||
return handleTestProviderConfig(req, db, mode);
|
||||
return handleTestProviderConfig(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
|
||||
@@ -42,11 +42,6 @@ export interface CreateProjectRequest {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 在此定义你的业务类型
|
||||
// 前后端共享的类型都放在这个文件中
|
||||
// ==========================================
|
||||
|
||||
export interface CreateProviderRequest {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
@@ -54,6 +49,11 @@ export interface CreateProviderRequest {
|
||||
type: ProviderType;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 在此定义你的业务类型
|
||||
// 前后端共享的类型都放在这个文件中
|
||||
// ==========================================
|
||||
|
||||
export interface Message {
|
||||
content: string;
|
||||
conversationId: string;
|
||||
@@ -99,9 +99,9 @@ export type ModelCapability =
|
||||
| "video-generation"
|
||||
| "video-recognition";
|
||||
|
||||
export interface SendMessageRequest {
|
||||
conversationId: string;
|
||||
messages: Array<{ content: string; role: "assistant" | "system" | "user" }>;
|
||||
export interface UpdateConversationRequest {
|
||||
modelId?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const MODEL_CAPABILITIES: readonly ModelCapability[] = [
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { ErrorInfo, ReactNode } from "react";
|
||||
import { Button, Result } from "antd";
|
||||
import { Component } from "react";
|
||||
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -19,7 +21,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
console.error("渲染错误:", error, info.componentStack);
|
||||
createConsoleLogger().error("渲染错误", { componentStack: info.componentStack, error });
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
68
src/web/consoles/workbench/components/chat/ChatInputArea.tsx
Normal file
68
src/web/consoles/workbench/components/chat/ChatInputArea.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Button, Flex, Input, Select } from "antd";
|
||||
|
||||
interface ChatInputAreaProps {
|
||||
displayModelId: null | string;
|
||||
input: string;
|
||||
isLoading: boolean;
|
||||
modelOptions: Array<{ label: string; value: string }>;
|
||||
onInputChange: (value: string) => void;
|
||||
onModelChange: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputArea({
|
||||
displayModelId,
|
||||
input,
|
||||
isLoading,
|
||||
modelOptions,
|
||||
onInputChange,
|
||||
onModelChange,
|
||||
onSend,
|
||||
onStop,
|
||||
}: ChatInputAreaProps) {
|
||||
return (
|
||||
<div className="chat-input-area">
|
||||
<Input.TextArea
|
||||
autoSize={{ maxRows: 6, minRows: 1 }}
|
||||
className="chat-textarea"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
void onSend();
|
||||
}
|
||||
}}
|
||||
placeholder="输入消息..."
|
||||
value={input}
|
||||
/>
|
||||
<Flex align="center" gap={8} justify="space-between">
|
||||
<Select
|
||||
className="chat-model-select"
|
||||
disabled={isLoading}
|
||||
onChange={onModelChange}
|
||||
options={modelOptions}
|
||||
placeholder="选择模型"
|
||||
value={displayModelId}
|
||||
/>
|
||||
<Flex align="center" gap={8}>
|
||||
{isLoading ? (
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
onStop?.();
|
||||
}}
|
||||
>
|
||||
停止
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => void onSend()} type="primary">
|
||||
发送
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
import type { BubbleItemType } from "@ant-design/x";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { Bubble, Sender } from "@ant-design/x";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { App, Empty, Spin } from "antd";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { CopyOutlined, EditOutlined, RedoOutlined, RobotOutlined } from "@ant-design/icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { DefaultChatTransport, type UIMessage } from "ai";
|
||||
import { App, Button, Card, Flex, Input, Spin, Typography } from "antd";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { fetchMessages } from "../../../../hooks/use-conversations";
|
||||
import { MessageBubble } from "./MessageBubble";
|
||||
import {
|
||||
createConversation,
|
||||
fetchConversation,
|
||||
fetchMessages,
|
||||
updateConversation,
|
||||
} from "../../../../hooks/use-conversations";
|
||||
import { useLogger } from "../../../../hooks/use-logger";
|
||||
import { useModelList } from "../../../../hooks/use-models";
|
||||
import { ChatInputArea } from "./ChatInputArea";
|
||||
import { ReasoningPart } from "./parts/ReasoningPart";
|
||||
import { TextPart } from "./parts/TextPart";
|
||||
import { ToolPart } from "./parts/ToolPart";
|
||||
|
||||
interface ChatPanelProps {
|
||||
conversationId: null | string;
|
||||
@@ -15,24 +24,33 @@ interface ChatPanelProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function ChatPanel({ conversationId, projectId }: ChatPanelProps) {
|
||||
export function ChatPanel({ conversationId, onConversationCreated, projectId }: ChatPanelProps) {
|
||||
const { message } = App.useApp();
|
||||
const logger = useLogger().child({ component: "ChatPanel", page: "workbench" });
|
||||
const queryClient = useQueryClient();
|
||||
const [input, setInput] = useState("");
|
||||
const [editingMessageId, setEditingMessageId] = useState<null | string>(null);
|
||||
const [editText, setEditText] = useState("");
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [selectedModelId, setSelectedModelId] = useState<null | string>(null);
|
||||
const fetchRef = useRef(fetchMessages);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const skipHistoryLoadRef = useRef<null | string>(null);
|
||||
|
||||
const conversationIdRef = useRef(conversationId);
|
||||
useEffect(() => {
|
||||
conversationIdRef.current = conversationId;
|
||||
});
|
||||
const { data: modelsData } = useModelList({ pageSize: 200 });
|
||||
const textModels = useMemo(
|
||||
() => (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text")),
|
||||
[modelsData],
|
||||
);
|
||||
|
||||
const { messages, sendMessage, setMessages, status } = useChat({
|
||||
const modelOptions = useMemo(() => textModels.map((m) => ({ label: m.name, value: m.id })), [textModels]);
|
||||
|
||||
const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({
|
||||
onError: (err) => {
|
||||
logger.error("聊天发送失败", { error: err.message });
|
||||
void message.error(`发送失败:${err.message}`);
|
||||
},
|
||||
transport: new DefaultChatTransport({
|
||||
api: `/api/projects/${projectId}/chat`,
|
||||
}),
|
||||
transport: new DefaultChatTransport({ api: `/api/projects/${projectId}/chat` }),
|
||||
});
|
||||
|
||||
const isLoading = status === "submitted" || status === "streaming";
|
||||
@@ -43,26 +61,36 @@ export function ChatPanel({ conversationId, projectId }: ChatPanelProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipHistoryLoadRef.current === conversationId) {
|
||||
skipHistoryLoadRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const load = async () => {
|
||||
setLoadingHistory(true);
|
||||
setInput("");
|
||||
setMessages([]);
|
||||
try {
|
||||
const data = await fetchRef.current(projectId, conversationId);
|
||||
const msgPromise = fetchRef.current(projectId, conversationId);
|
||||
|
||||
const data = await msgPromise;
|
||||
if (cancelled) return;
|
||||
|
||||
const history = data.items
|
||||
.filter((m: { role: string }) => m.role === "user" || m.role === "assistant")
|
||||
.reverse()
|
||||
.map((m: { content: string; id: string; role: string }) => ({
|
||||
.map((m: { content: string; id: string; parts: null | string; role: string }) => ({
|
||||
id: m.id,
|
||||
parts: [{ text: m.content, type: "text" as const }],
|
||||
parts: m.parts ? (JSON.parse(m.parts) as UIMessage["parts"]) : [{ text: m.content, type: "text" as const }],
|
||||
role: m.role as "assistant" | "user",
|
||||
}));
|
||||
setMessages(history);
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.error("加载历史失败", { conversationId, error: msg, projectId });
|
||||
void message.error(`加载历史失败:${msg}`);
|
||||
}
|
||||
} finally {
|
||||
@@ -75,30 +103,229 @@ export function ChatPanel({ conversationId, projectId }: ChatPanelProps) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [conversationId, projectId, setMessages, message]);
|
||||
}, [conversationId, projectId, setMessages, message, logger]);
|
||||
|
||||
const bubbleItems: BubbleItemType[] = messages.map((msg) => ({
|
||||
content: msg.parts
|
||||
.filter((p): p is { text: string; type: "text" } => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join(""),
|
||||
key: msg.id,
|
||||
role: msg.role === "user" ? "user" : "ai",
|
||||
}));
|
||||
useEffect(() => {
|
||||
if (!conversationId) return;
|
||||
const firstTextId = textModels[0]?.id;
|
||||
if (!firstTextId) return;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(nextInput: string) => {
|
||||
if (!nextInput.trim()) return;
|
||||
setInput("");
|
||||
void sendMessage({ text: nextInput }, { body: { conversationId: conversationIdRef.current } });
|
||||
void fetchConversation(projectId, conversationId)
|
||||
.then((conv) => {
|
||||
if (textModels.every((m) => m.id !== conv.modelId)) {
|
||||
setSelectedModelId(firstTextId);
|
||||
void updateConversation(projectId, conversationId, { modelId: firstTextId });
|
||||
} else {
|
||||
setSelectedModelId(conv.modelId);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn("获取会话模型信息失败", { conversationId, error: msg, projectId });
|
||||
});
|
||||
}, [conversationId, textModels, projectId, logger]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ behavior: "smooth", top: scrollRef.current.scrollHeight });
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "ready" && conversationId) {
|
||||
void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] });
|
||||
}
|
||||
}, [status, conversationId, projectId, queryClient]);
|
||||
|
||||
const displayModelId = conversationId ? selectedModelId : (textModels[0]?.id ?? null);
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedModelId(value);
|
||||
if (conversationId) {
|
||||
void updateConversation(projectId, conversationId, { modelId: value }).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn("更新会话模型失败", { conversationId, error: msg, projectId });
|
||||
});
|
||||
}
|
||||
},
|
||||
[sendMessage],
|
||||
[projectId, conversationId, logger],
|
||||
);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!input.trim()) return;
|
||||
const text = input;
|
||||
setInput("");
|
||||
|
||||
if (!conversationId) {
|
||||
try {
|
||||
const conv = await createConversation(projectId, displayModelId ?? undefined);
|
||||
skipHistoryLoadRef.current = conv.id;
|
||||
void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] });
|
||||
void sendMessage({ text }, { body: { conversationId: conv.id } });
|
||||
onConversationCreated(conv.id);
|
||||
} catch (err: unknown) {
|
||||
setInput(text);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.error("创建会话失败", { error: msg, projectId });
|
||||
void message.error(`创建会话失败:${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void sendMessage({ text }, { body: { conversationId } }).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.error("发送消息失败", { conversationId, error: msg, projectId });
|
||||
});
|
||||
}, [
|
||||
input,
|
||||
sendMessage,
|
||||
conversationId,
|
||||
projectId,
|
||||
onConversationCreated,
|
||||
message,
|
||||
queryClient,
|
||||
displayModelId,
|
||||
logger,
|
||||
]);
|
||||
|
||||
const extractText = useCallback((msg: UIMessage) => {
|
||||
return msg.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => (p as { text: string; type: "text" }).text)
|
||||
.join("");
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(msg: UIMessage) => {
|
||||
const text = extractText(msg);
|
||||
void navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
void message.success("已复制");
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn("复制失败", { error: msg });
|
||||
});
|
||||
},
|
||||
[extractText, message, logger],
|
||||
);
|
||||
|
||||
const handleEditStart = useCallback(
|
||||
(msg: UIMessage) => {
|
||||
setEditingMessageId(msg.id);
|
||||
setEditText(extractText(msg));
|
||||
},
|
||||
[extractText],
|
||||
);
|
||||
|
||||
const handleEditConfirm = useCallback(() => {
|
||||
if (!editText.trim() || !conversationId) return;
|
||||
setEditingMessageId(null);
|
||||
const idx = messages.findIndex((m) => m.id === editingMessageId);
|
||||
if (idx === -1) return;
|
||||
setMessages(messages.slice(0, idx));
|
||||
void sendMessage({ text: editText }, { body: { conversationId } }).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.error("重新发送消息失败", { conversationId, error: msg, projectId });
|
||||
});
|
||||
}, [editText, conversationId, messages, editingMessageId, setMessages, sendMessage, logger, projectId]);
|
||||
|
||||
const handleEditCancel = useCallback(() => {
|
||||
setEditingMessageId(null);
|
||||
setEditText("");
|
||||
}, []);
|
||||
|
||||
const handleRegenerate = useCallback(() => {
|
||||
if (!conversationId) return;
|
||||
void regenerate({ body: { conversationId } }).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.error("重新生成失败", { conversationId, error: msg, projectId });
|
||||
});
|
||||
}, [regenerate, conversationId, logger, projectId]);
|
||||
|
||||
const getCardExtra = useCallback(
|
||||
(msg: UIMessage, idx: number) => {
|
||||
const isLast = idx === messages.length - 1;
|
||||
const lastUserIdx = messages.findLastIndex((m) => m.role === "user");
|
||||
const isLastUser = lastUserIdx >= 0 && idx === lastUserIdx;
|
||||
const isLastAssistant = isLast && msg.role === "assistant";
|
||||
const isEditing = editingMessageId === msg.id;
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
const buttons: React.ReactNode[] = [];
|
||||
|
||||
buttons.push(
|
||||
<Button
|
||||
className="btn-dimmed"
|
||||
icon={<CopyOutlined />}
|
||||
key="copy"
|
||||
onClick={() => handleCopy(msg)}
|
||||
size="small"
|
||||
type="text"
|
||||
/>,
|
||||
);
|
||||
|
||||
if (isLastUser && !isEditing) {
|
||||
buttons.push(
|
||||
<Button
|
||||
className="btn-dimmed"
|
||||
icon={<EditOutlined />}
|
||||
key="edit"
|
||||
onClick={() => handleEditStart(msg)}
|
||||
size="small"
|
||||
type="text"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (isLastAssistant) {
|
||||
buttons.push(
|
||||
<Button
|
||||
className="btn-dimmed"
|
||||
icon={<RedoOutlined />}
|
||||
key="regenerate"
|
||||
onClick={handleRegenerate}
|
||||
size="small"
|
||||
type="text"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return <Flex gap={4}>{buttons}</Flex>;
|
||||
},
|
||||
[messages, isLoading, editingMessageId, handleCopy, handleEditStart, handleRegenerate],
|
||||
);
|
||||
|
||||
if (!conversationId) {
|
||||
return (
|
||||
<div className="app-chat-panel app-chat-panel-empty">
|
||||
<Empty description="选择或创建一个会话开始聊天" />
|
||||
<div className="app-chat-panel">
|
||||
<div className="chat-welcome-area">
|
||||
<Flex align="center" gap={12} vertical>
|
||||
<RobotOutlined className="welcome-icon" />
|
||||
<Typography.Title className="welcome-title" level={3}>
|
||||
你好,我是阿福
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">有什么我可以帮助你的吗?</Typography.Text>
|
||||
</Flex>
|
||||
</div>
|
||||
<ChatInputArea
|
||||
displayModelId={displayModelId}
|
||||
input={input}
|
||||
isLoading={isLoading}
|
||||
modelOptions={modelOptions}
|
||||
onInputChange={setInput}
|
||||
onModelChange={handleModelChange}
|
||||
onSend={() => {
|
||||
void handleSend();
|
||||
}}
|
||||
onStop={() => {
|
||||
void stop().catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn("停止聊天失败", { error: msg });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -110,23 +337,81 @@ export function ChatPanel({ conversationId, projectId }: ChatPanelProps) {
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<Bubble.List
|
||||
items={bubbleItems}
|
||||
role={{
|
||||
ai: {
|
||||
contentRender: (content: string) => <MessageBubble content={content} />,
|
||||
placement: "start",
|
||||
},
|
||||
user: {
|
||||
placement: "end",
|
||||
},
|
||||
}}
|
||||
style={{ flex: 1, overflow: "auto", padding: "16px" }}
|
||||
<div className="chat-scroll-area" ref={scrollRef}>
|
||||
<Flex gap={8} vertical>
|
||||
{messages.map((msg, idx) => (
|
||||
<Card
|
||||
classNames={{ extra: "card-extra-actions" }}
|
||||
extra={getCardExtra(msg, idx)}
|
||||
key={msg.id}
|
||||
size="small"
|
||||
title={msg.role === "user" ? "用户" : <span className="msg-title-ai">阿福</span>}
|
||||
>
|
||||
<div className="message-body">
|
||||
{editingMessageId === msg.id ? (
|
||||
<Flex gap={8} vertical>
|
||||
<Input.TextArea
|
||||
autoSize={{ maxRows: 6, minRows: 1 }}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
value={editText}
|
||||
/>
|
||||
<Flex gap={8} justify="flex-end">
|
||||
<Button onClick={handleEditCancel} size="small">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleEditConfirm} size="small" type="primary">
|
||||
确认
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
msg.parts.map((part: Record<string, unknown>, i: number) => (
|
||||
<PartRenderer key={i} part={part} role={msg.role} />
|
||||
))
|
||||
)}
|
||||
<div className="app-chat-panel-sender">
|
||||
<Sender loading={isLoading} onChange={setInput} onSubmit={onSubmit} placeholder="输入消息..." value={input} />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{isLoading && (
|
||||
<Flex className="chat-loading-indicator" justify="center">
|
||||
<Spin size="small" />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
<ChatInputArea
|
||||
displayModelId={displayModelId}
|
||||
input={input}
|
||||
isLoading={isLoading}
|
||||
modelOptions={modelOptions}
|
||||
onInputChange={setInput}
|
||||
onModelChange={handleModelChange}
|
||||
onSend={() => {
|
||||
void handleSend();
|
||||
}}
|
||||
onStop={() => {
|
||||
void stop().catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn("停止聊天失败", { error: msg });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PartRenderer({ part, role }: { part: Record<string, unknown>; role: string }) {
|
||||
const partType = typeof part["type"] === "string" ? part["type"] : "";
|
||||
|
||||
if (partType === "text") {
|
||||
return <TextPart part={part} role={role} />;
|
||||
}
|
||||
if (partType.startsWith("tool-")) {
|
||||
return <ToolPart part={part} />;
|
||||
}
|
||||
if (partType === "reasoning") {
|
||||
return <ReasoningPart part={part} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { App, Button, Flex, Popconfirm, Spin, Typography } from "antd";
|
||||
import type { Conversation } from "../../../../../shared/api";
|
||||
|
||||
import { createConversation, deleteConversation, fetchConversations } from "../../../../hooks/use-conversations";
|
||||
import { useModelList } from "../../../../hooks/use-models";
|
||||
|
||||
interface ChatSidebarProps {
|
||||
activeId: null | string;
|
||||
@@ -23,8 +24,12 @@ export function ChatSidebar({ activeId, onSelect, projectId }: ChatSidebarProps)
|
||||
queryKey: CONVERSATIONS_KEY,
|
||||
});
|
||||
|
||||
const { data: modelsData } = useModelList({ pageSize: 200 });
|
||||
const textModels = (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text"));
|
||||
const defaultModelId = textModels[0]?.id;
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createConversation(projectId),
|
||||
mutationFn: () => createConversation(projectId, defaultModelId),
|
||||
onError: (err: Error) => {
|
||||
void message.error(`创建会话失败:${err.message}`);
|
||||
},
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
interface MessageBubbleProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function MessageBubble({ content }: MessageBubbleProps) {
|
||||
return <div className="app-chat-message-bubble">{content}</div>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CheckCircleFilled, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Collapse, Flex, Typography } from "antd";
|
||||
|
||||
import type { PartProps } from "./types";
|
||||
|
||||
export function ReasoningPart({ part }: PartProps) {
|
||||
const text = typeof part["text"] === "string" ? part["text"] : "";
|
||||
const state = typeof part["state"] === "string" ? part["state"] : "";
|
||||
const isStreaming = state === "streaming";
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
children: <Typography.Text type="secondary">{text}</Typography.Text>,
|
||||
key: "reasoning",
|
||||
label: isStreaming ? (
|
||||
<Flex align="center" component="span" gap={4}>
|
||||
<LoadingOutlined className="icon-primary" />
|
||||
<Typography.Text type="secondary">思考中</Typography.Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex align="center" component="span" gap={4}>
|
||||
<CheckCircleFilled className="icon-success" />
|
||||
<Typography.Text type="secondary">思考完成</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Typography } from "antd";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import type { PartProps } from "./types";
|
||||
|
||||
interface TextPartProps extends PartProps {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export function TextPart({ part, role }: TextPartProps) {
|
||||
const text = typeof part["text"] === "string" ? part["text"] : "";
|
||||
|
||||
return (
|
||||
<div className="part-body">
|
||||
{role === "user" ? (
|
||||
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
|
||||
) : (
|
||||
<Streamdown parseIncompleteMarkdown>{text}</Streamdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Collapse, Flex, Typography } from "antd";
|
||||
|
||||
import type { PartProps } from "./types";
|
||||
|
||||
interface ToolPartData {
|
||||
errorText?: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function getToolState(part: ToolPartData) {
|
||||
if ("errorText" in part && part.errorText) return "output-error" as const;
|
||||
if ("output" in part) return "output-available" as const;
|
||||
if ("input" in part) return "input-available" as const;
|
||||
return "input-streaming" as const;
|
||||
}
|
||||
|
||||
const FORMAT_JSON = (v: unknown) => JSON.stringify(v, null, 2);
|
||||
|
||||
export function ToolPart({ part }: PartProps) {
|
||||
const toolPart = part as unknown as ToolPartData;
|
||||
const state = getToolState(toolPart);
|
||||
const toolName = toolPart.toolName ?? (toolPart.type ?? "unknown").replace(/^tool-/, "");
|
||||
|
||||
const isStreaming = state === "input-streaming" || state === "input-available";
|
||||
|
||||
if (state === "output-error") {
|
||||
return (
|
||||
<Collapse
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
children: <Typography.Text type="danger">{toolPart.errorText}</Typography.Text>,
|
||||
key: toolPart.toolCallId ?? toolName,
|
||||
label: (
|
||||
<Flex align="center" component="span" gap={4}>
|
||||
<CloseCircleFilled className="icon-error" />
|
||||
<Typography.Text type="danger">{toolName} 失败</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
defaultActiveKey={isStreaming ? [toolPart.toolCallId ?? toolName] : undefined}
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
children: (
|
||||
<Flex gap={4} vertical>
|
||||
{toolPart.input != null && (
|
||||
<>
|
||||
<Typography.Text type="secondary">参数:</Typography.Text>
|
||||
<pre className="tool-result-pre">{FORMAT_JSON(toolPart.input)}</pre>
|
||||
</>
|
||||
)}
|
||||
{"output" in toolPart && toolPart.output != null && (
|
||||
<>
|
||||
<Typography.Text type="secondary">结果:</Typography.Text>
|
||||
<pre className="tool-result-pre">{FORMAT_JSON(toolPart.output)}</pre>
|
||||
</>
|
||||
)}
|
||||
{!toolPart.input && !("output" in toolPart) && (
|
||||
<Typography.Text type="secondary">生成中...</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
),
|
||||
key: toolPart.toolCallId ?? toolName,
|
||||
label: isStreaming ? (
|
||||
<Flex align="center" component="span" gap={4}>
|
||||
<LoadingOutlined className="icon-primary" />
|
||||
<Typography.Text type="secondary">
|
||||
{state === "input-streaming" ? "生成参数" : `调用 ${toolName}`}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex align="center" component="span" gap={4}>
|
||||
<CheckCircleFilled className="icon-success" />
|
||||
<Typography.Text type="secondary">{toolName}</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface PartProps {
|
||||
part: Record<string, unknown>;
|
||||
}
|
||||
@@ -3,9 +3,13 @@ import type {
|
||||
ConversationListResponse,
|
||||
ConversationResponse,
|
||||
MessageListResponse,
|
||||
UpdateConversationRequest,
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export async function createConversation(projectId: string, modelId?: string): Promise<Conversation> {
|
||||
const response = await fetch(`/api/projects/${projectId}/conversations`, {
|
||||
@@ -22,24 +26,47 @@ export async function deleteConversation(projectId: string, conversationId: stri
|
||||
}
|
||||
|
||||
export async function fetchConversation(projectId: string, conversationId: string): Promise<Conversation> {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`);
|
||||
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
|
||||
} catch (err: unknown) {
|
||||
logger.error("获取会话失败", {
|
||||
conversationId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
projectId,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchConversations(projectId: string): Promise<ConversationListResponse> {
|
||||
const response = await fetch(`/api/projects/${projectId}/conversations?pageSize=100`);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<ConversationListResponse>;
|
||||
return handleResponse(response, (data) => data as ConversationListResponse);
|
||||
}
|
||||
|
||||
export async function fetchMessages(projectId: string, conversationId: string): Promise<MessageListResponse> {
|
||||
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}/messages?pageSize=200`);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<MessageListResponse>;
|
||||
return handleResponse(response, (data) => data as MessageListResponse);
|
||||
}
|
||||
|
||||
export async function updateConversation(
|
||||
projectId: string,
|
||||
conversationId: string,
|
||||
data: UpdateConversationRequest,
|
||||
): Promise<Conversation> {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}`, {
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
return handleResponse(response, (data) => (data as ConversationResponse).conversation);
|
||||
} catch (err: unknown) {
|
||||
logger.error("更新会话失败", {
|
||||
conversationId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
projectId,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
14
src/web/hooks/use-logger.ts
Normal file
14
src/web/hooks/use-logger.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { App } from "antd";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { Logger } from "../utils/logger";
|
||||
|
||||
import { AntdMessageSink, ConsoleSink, createDefaultLogger } from "../utils/logger";
|
||||
|
||||
export function useLogger(): Logger {
|
||||
const { message } = App.useApp();
|
||||
return useMemo(() => {
|
||||
const isProduction = !!import.meta.env["PROD"];
|
||||
return createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
|
||||
}, [message]);
|
||||
}
|
||||
@@ -12,8 +12,10 @@ import type {
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const MODELS_KEY = ["models"] as const;
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export async function createModel(data: CreateModelRequest): Promise<Model> {
|
||||
const response = await fetch("/api/models", {
|
||||
@@ -82,7 +84,8 @@ export function useCreateModel() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createModel,
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("模型创建成功", { modelId: data.modelId, providerId: data.providerId });
|
||||
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -92,7 +95,8 @@ export function useDeleteModel() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteModel,
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, variables) => {
|
||||
logger.info("模型删除成功", { modelId: variables });
|
||||
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -123,7 +127,8 @@ export function useUpdateModel() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (args: { data: UpdateModelRequest; id: string }) => updateModel(args.id, args.data),
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("模型更新成功", { modelId: data.modelId, providerId: data.providerId });
|
||||
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,8 +10,10 @@ import type {
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const PROJECTS_KEY = ["projects"] as const;
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export async function archiveProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}/archive`, { method: "POST" });
|
||||
@@ -76,7 +78,8 @@ export function useArchiveProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: archiveProject,
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("项目归档成功", { name: data.name, projectId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -86,7 +89,8 @@ export function useCreateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createProject,
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("项目创建成功", { name: data.name, projectId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -96,7 +100,8 @@ export function useDeleteProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteProject,
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, variables) => {
|
||||
logger.info("项目删除成功", { projectId: variables });
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -121,7 +126,8 @@ export function useRestoreProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: restoreProject,
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("项目恢复成功", { name: data.name, projectId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -131,7 +137,8 @@ export function useUpdateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (args: { data: UpdateProjectRequest; id: string }) => updateProject(args.id, args.data),
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("项目更新成功", { name: data.name, projectId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,9 +12,11 @@ import type {
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const PROVIDERS_KEY = ["providers"] as const;
|
||||
const MODELS_KEY = ["models"] as const;
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export async function createProvider(data: CreateProviderRequest): Promise<Provider> {
|
||||
const response = await fetch("/api/providers", {
|
||||
@@ -90,7 +92,8 @@ export function useCreateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createProvider,
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("供应商创建成功", { name: data.name, providerId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
||||
},
|
||||
});
|
||||
@@ -100,7 +103,8 @@ export function useDeleteProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteProvider,
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, variables) => {
|
||||
logger.info("供应商删除成功", { providerId: variables });
|
||||
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
||||
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
|
||||
},
|
||||
@@ -139,7 +143,8 @@ export function useUpdateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (args: { data: UpdateProviderRequest; id: string }) => updateProvider(args.id, args.data),
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
logger.info("供应商更新成功", { name: data.name, providerId: data.id });
|
||||
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
@@ -6,8 +6,11 @@ import { BrowserRouter } from "react-router";
|
||||
|
||||
import { App } from "./app";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { createConsoleLogger } from "./utils/logger";
|
||||
import "./styles.css";
|
||||
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
@@ -16,6 +19,19 @@ const queryClient = new QueryClient({
|
||||
staleTime: 5000,
|
||||
},
|
||||
},
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error: Error, _variables, _context, mutation) => {
|
||||
logger.error("mutation failed", {
|
||||
error: error.message,
|
||||
mutationKey: mutation.options.mutationKey,
|
||||
});
|
||||
},
|
||||
}),
|
||||
queryCache: new QueryCache({
|
||||
onError: (error: Error, query) => {
|
||||
logger.error("query failed", { error: error.message, queryKey: query.queryKey });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
@@ -36,3 +52,18 @@ createRoot(rootElement).render(
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
logger.error("未处理的异常", {
|
||||
colno,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
lineno,
|
||||
message,
|
||||
source,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
|
||||
const msg = event.reason instanceof Error ? event.reason.message : String(event.reason);
|
||||
logger.error("unhandled rejection", { reason: msg });
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ export function ModelFormModal({
|
||||
message.success("模型已创建");
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
message.error(err.message);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export function ModelFormModal({
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ModelTable({
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("模型已删除");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ProviderFormModal({
|
||||
message.success("供应商已创建");
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
message.error(err.message);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function ProviderFormModal({
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
message.error(err.message);
|
||||
}
|
||||
@@ -135,10 +135,18 @@ export function ProviderFormModal({
|
||||
<Form.Item label="供应商类型" name="type" rules={[{ message: "请选择供应商类型", required: true }]}>
|
||||
<Select options={TYPE_OPTIONS} placeholder="请选择供应商类型" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Base URL" name="baseUrl" rules={[{ message: "请输入 Base URL", required: true }]}>
|
||||
<Form.Item
|
||||
label="Base URL"
|
||||
name="baseUrl"
|
||||
rules={[{ message: "请输入 Base URL", required: true, whitespace: true }]}
|
||||
>
|
||||
<Input placeholder="https://api.openai.com/v1" />
|
||||
</Form.Item>
|
||||
<Form.Item label="API Key" name="apiKey" rules={[{ message: "请输入 API Key", required: true }]}>
|
||||
<Form.Item
|
||||
label="API Key"
|
||||
name="apiKey"
|
||||
rules={[{ message: "请输入 API Key", required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.Password placeholder="请输入 API Key" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, p
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("供应商已删除");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ModelsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="large" style={{ flex: 1 }}>
|
||||
<Space className="app-page-flex" orientation="vertical" size="large">
|
||||
<ModelsToolbar
|
||||
activeTab={activeTab}
|
||||
key={activeTab}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ProjectFormModal({
|
||||
message.success("项目已创建");
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
message.error(err.message);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ProjectTable({
|
||||
try {
|
||||
await onArchive(id);
|
||||
message.success("项目已归档");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
@@ -78,7 +78,7 @@ export function ProjectTable({
|
||||
try {
|
||||
await onRestore(id);
|
||||
message.success("项目已恢复");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
@@ -87,7 +87,7 @@ export function ProjectTable({
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("项目已永久删除");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ProjectsPage() {
|
||||
const isRowActionPending = archiveMutation.isPending || restoreMutation.isPending || deleteMutation.isPending;
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="large" style={{ flex: 1 }}>
|
||||
<Space className="app-page-flex" orientation="vertical" size="large">
|
||||
<ProjectToolbar
|
||||
activeTab={tabValue}
|
||||
keyword={keyword}
|
||||
|
||||
@@ -4,7 +4,8 @@ body {
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
@@ -50,11 +51,13 @@ body {
|
||||
}
|
||||
|
||||
.app-content {
|
||||
overflow: auto;
|
||||
padding: var(--ant-padding-xl) var(--ant-padding-xl);
|
||||
}
|
||||
|
||||
.app-chat-page {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-console-title {
|
||||
@@ -92,6 +95,7 @@ body {
|
||||
|
||||
.app-chat-sidebar-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -135,12 +139,16 @@ body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel-empty {
|
||||
.chat-welcome-area {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ant-padding-xl);
|
||||
}
|
||||
|
||||
.app-chat-panel-loading {
|
||||
@@ -150,13 +158,99 @@ body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-chat-panel-sender {
|
||||
padding: var(--ant-padding-sm) var(--ant-padding);
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-left: var(--ant-padding-sm);
|
||||
padding-top: var(--ant-padding-sm);
|
||||
border-top: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
.app-chat-message-bubble {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
.message-body {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.message-body pre {
|
||||
background: var(--ant-color-bg-layout);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chat-scroll-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overflow-anchor: auto;
|
||||
padding-left: var(--ant-padding-sm);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat-loading-indicator {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chat-textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-body-text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.step-divider {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.tool-result-pre {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.msg-title-ai {
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.part-body {
|
||||
padding: 0 var(--ant-padding-sm);
|
||||
}
|
||||
|
||||
.icon-primary {
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
color: var(--ant-color-success);
|
||||
}
|
||||
|
||||
.icon-error {
|
||||
color: var(--ant-color-error);
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
color: var(--ant-color-primary);
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-model-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.card-extra-actions .btn-dimmed {
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
|
||||
.card-extra-actions .btn-dimmed:hover {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.app-page-flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
import { createConsoleLogger } from "./logger";
|
||||
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export async function handleResponse<T>(response: Response, extract: (data: unknown) => T): Promise<T> {
|
||||
const start = performance.now();
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
const errorBody = body?.error ?? `HTTP ${response.status}`;
|
||||
logger.warn("API request failed", {
|
||||
duration: Math.round(performance.now() - start),
|
||||
errorBody,
|
||||
status: response.status,
|
||||
url: response.url,
|
||||
});
|
||||
throw new Error(errorBody);
|
||||
}
|
||||
const data: unknown = await response.json();
|
||||
if (import.meta.env["DEV"]) {
|
||||
logger.debug("API request", {
|
||||
duration: Math.round(performance.now() - start),
|
||||
status: response.status,
|
||||
url: response.url,
|
||||
});
|
||||
}
|
||||
return extract(data);
|
||||
}
|
||||
|
||||
export async function handleVoidResponse(response: Response): Promise<void> {
|
||||
const start = performance.now();
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
const errorBody = body?.error ?? `HTTP ${response.status}`;
|
||||
logger.warn("API request failed", {
|
||||
duration: Math.round(performance.now() - start),
|
||||
errorBody,
|
||||
status: response.status,
|
||||
url: response.url,
|
||||
});
|
||||
throw new Error(errorBody);
|
||||
}
|
||||
if (import.meta.env["DEV"]) {
|
||||
logger.debug("API request", {
|
||||
duration: Math.round(performance.now() - start),
|
||||
status: response.status,
|
||||
url: response.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
176
src/web/utils/logger.ts
Normal file
176
src/web/utils/logger.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { MessageInstance } from "antd/es/message/interface";
|
||||
|
||||
export type LogLevel = "debug" | "error" | "info" | "warn";
|
||||
|
||||
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 0, error: 3, info: 1, warn: 2 };
|
||||
|
||||
export interface Logger {
|
||||
child(bindings: Record<string, unknown>): Logger;
|
||||
debug(message: string, data?: unknown): void;
|
||||
error(message: string, data?: unknown): void;
|
||||
info(message: string, data?: unknown): void;
|
||||
setLevel(level: LogLevel): void;
|
||||
warn(message: string, data?: unknown): void;
|
||||
}
|
||||
|
||||
export interface Sink {
|
||||
write(level: LogLevel, message: string, data: unknown, bindings: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
class AntdMessageSink implements Sink {
|
||||
constructor(private messageApi: MessageInstance) {
|
||||
// 仅存储依赖,无需初始化操作
|
||||
}
|
||||
|
||||
write(level: LogLevel, message: string, _data: unknown, _bindings: Record<string, unknown>): void {
|
||||
if (level === "warn") this.messageApi.warning(message);
|
||||
else if (level === "error") this.messageApi.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseLogger implements Logger {
|
||||
private minLevel: LogLevel;
|
||||
|
||||
constructor(
|
||||
minLevel: LogLevel,
|
||||
protected sinks: Sink[],
|
||||
protected bindings: Record<string, unknown>,
|
||||
) {
|
||||
this.minLevel = minLevel;
|
||||
}
|
||||
|
||||
child(bindings: Record<string, unknown>): Logger {
|
||||
return new BaseLogger(this.minLevel, this.sinks, { ...this.bindings, ...bindings });
|
||||
}
|
||||
|
||||
debug(message: string, data?: unknown): void {
|
||||
this.log("debug", message, data);
|
||||
}
|
||||
|
||||
error(message: string, data?: unknown): void {
|
||||
this.log("error", message, data);
|
||||
}
|
||||
|
||||
info(message: string, data?: unknown): void {
|
||||
this.log("info", message, data);
|
||||
}
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
this.minLevel = level;
|
||||
}
|
||||
|
||||
warn(message: string, data?: unknown): void {
|
||||
this.log("warn", message, data);
|
||||
}
|
||||
|
||||
private log(level: LogLevel, message: string, data?: unknown): void {
|
||||
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.minLevel]) return;
|
||||
for (const sink of this.sinks) {
|
||||
sink.write(level, message, data, this.bindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleSink implements Sink {
|
||||
constructor(private isProduction: boolean) {
|
||||
// 仅存储配置,无需初始化操作
|
||||
}
|
||||
|
||||
write(level: LogLevel, message: string, data: unknown, bindings: Record<string, unknown>): void {
|
||||
if (this.isProduction && LEVEL_ORDER[level] < LEVEL_ORDER.warn) return;
|
||||
|
||||
const prefix = `[Alfred:${level.toUpperCase()}]`;
|
||||
const bindingStr = formatBindings(bindings);
|
||||
const fullMessage = `${prefix} ${message}${bindingStr}`;
|
||||
|
||||
if (level === "error") console.error(fullMessage, data);
|
||||
else if (level === "warn") console.warn(fullMessage, data);
|
||||
else console.log(fullMessage, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopLogger implements Logger {
|
||||
child(_bindings: Record<string, unknown>): Logger {
|
||||
return this;
|
||||
}
|
||||
|
||||
debug(_message: string, _data?: unknown): void {
|
||||
/* NoopLogger 实现 Logger 接口契约,有意静默丢弃所有日志 */
|
||||
}
|
||||
|
||||
error(_message: string, _data?: unknown): void {
|
||||
/* NoopLogger 实现 Logger 接口契约,有意静默丢弃所有日志 */
|
||||
}
|
||||
|
||||
info(_message: string, _data?: unknown): void {
|
||||
/* NoopLogger 实现 Logger 接口契约,有意静默丢弃所有日志 */
|
||||
}
|
||||
|
||||
setLevel(_level: LogLevel): void {
|
||||
/* NoopLogger 实现 Logger 接口契约,有意静默丢弃所有日志 */
|
||||
}
|
||||
|
||||
warn(_message: string, _data?: unknown): void {
|
||||
/* NoopLogger 实现 Logger 接口契约,有意静默丢弃所有日志 */
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryLogger implements Logger {
|
||||
entries: Array<{ data?: unknown; level: LogLevel; message: string }> = [];
|
||||
|
||||
child(bindings: Record<string, unknown>): Logger {
|
||||
void bindings;
|
||||
return this;
|
||||
}
|
||||
|
||||
debug(message: string, data?: unknown): void {
|
||||
this.capture("debug", message, data);
|
||||
}
|
||||
|
||||
error(message: string, data?: unknown): void {
|
||||
this.capture("error", message, data);
|
||||
}
|
||||
|
||||
info(message: string, data?: unknown): void {
|
||||
this.capture("info", message, data);
|
||||
}
|
||||
|
||||
setLevel(_level: LogLevel): void {
|
||||
// MemoryLogger.setLevel 为接口兼容,无需实际过滤
|
||||
}
|
||||
|
||||
warn(message: string, data?: unknown): void {
|
||||
this.capture("warn", message, data);
|
||||
}
|
||||
|
||||
private capture(level: LogLevel, message: string, data?: unknown): void {
|
||||
this.entries.push({ data, level, message });
|
||||
}
|
||||
}
|
||||
|
||||
export function createConsoleLogger(): Logger {
|
||||
const isProduction = !!import.meta.env["PROD"];
|
||||
const minLevel: LogLevel = isProduction ? "warn" : "debug";
|
||||
return new BaseLogger(minLevel, [new ConsoleSink(isProduction)], {});
|
||||
}
|
||||
|
||||
export function createDefaultLogger(sinks: Sink[], isProduction: boolean): Logger {
|
||||
const minLevel: LogLevel = isProduction ? "warn" : "debug";
|
||||
return new BaseLogger(minLevel, sinks, {});
|
||||
}
|
||||
|
||||
export function createMemoryLogger(): MemoryLogger {
|
||||
return new MemoryLogger();
|
||||
}
|
||||
|
||||
export { AntdMessageSink, ConsoleSink };
|
||||
|
||||
export function createNoopLogger(): Logger {
|
||||
return new NoopLogger();
|
||||
}
|
||||
|
||||
function formatBindings(bindings: Record<string, unknown>): string {
|
||||
const entries = Object.entries(bindings);
|
||||
if (entries.length === 0) return "";
|
||||
return " " + entries.map(([k, v]) => `[${k}=${String(v)}]`).join("");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedTestDatabase } from "../../helpers";
|
||||
|
||||
void mock.module("ai", () => ({
|
||||
@@ -36,12 +37,15 @@ describe("AI registry", () => {
|
||||
test("testProviderConnection reports unreachable Base URL", async () => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
const result = await testProviderConnection(
|
||||
{
|
||||
apiKey: "bad-key",
|
||||
baseUrl: "http://127.0.0.1:1",
|
||||
name: "Bad",
|
||||
type: "openai-compatible",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("Base URL 不可达");
|
||||
@@ -51,12 +55,15 @@ describe("AI registry", () => {
|
||||
await withProviderServer(new Response(null, { status: 401 }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
const result = await testProviderConnection(
|
||||
{
|
||||
apiKey: "bad-key",
|
||||
baseUrl,
|
||||
name: "Bad",
|
||||
type: "openai-compatible",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("API Key 无效");
|
||||
@@ -68,12 +75,15 @@ describe("AI registry", () => {
|
||||
await withProviderServer(Response.json({ data: [{ id: "gpt-4o" }] }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
const result = await testProviderConnection(
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl,
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("/models 返回 1 个模型");
|
||||
@@ -84,12 +94,15 @@ describe("AI registry", () => {
|
||||
await withProviderServer(new Response(null, { status: 404 }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
const result = await testProviderConnection(
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl,
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("可能不支持 /models");
|
||||
@@ -134,13 +147,16 @@ describe("AI registry", () => {
|
||||
test("testModelConnection 成功返回 ok:true", async () => {
|
||||
const { testModelConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testModelConnection({
|
||||
const result = await testModelConnection(
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
modelId: "gpt-4o",
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("模型连接成功");
|
||||
|
||||
25
tests/server/ai/tools/get-current-time.test.ts
Normal file
25
tests/server/ai/tools/get-current-time.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { formatCurrentTime } from "../../../../src/server/ai/tools/get-current-time";
|
||||
|
||||
describe("getCurrentTime 工具", () => {
|
||||
test("formatCurrentTime 返回 ISO/local/timestamp", () => {
|
||||
const result = formatCurrentTime();
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.iso).toBe("string");
|
||||
expect(typeof result.local).toBe("string");
|
||||
expect(typeof result.timestamp).toBe("number");
|
||||
});
|
||||
|
||||
test("formatCurrentTime 指定 timezone", () => {
|
||||
const result = formatCurrentTime("Asia/Shanghai");
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.local).toBe("string");
|
||||
});
|
||||
|
||||
test("formatCurrentTime 无效 timezone 优雅降级", () => {
|
||||
const result = formatCurrentTime("Invalid/Zone");
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.local).toBe("string");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
updateModel,
|
||||
} from "../../../src/server/db/models";
|
||||
import { createProvider } from "../../../src/server/db/providers";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedTestDatabase } from "../../helpers";
|
||||
|
||||
function seedProvider(db: Database, name = "TestProvider"): string {
|
||||
const result = createProvider(db, { apiKey: "sk-test", baseUrl: "https://api.test.com/v1", name, type: "openai" });
|
||||
const result = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-test", baseUrl: "https://api.test.com/v1", name, type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
return (result as { provider: { id: string } }).provider.id;
|
||||
}
|
||||
|
||||
@@ -32,12 +37,16 @@ describe("模型数据访问层", () => {
|
||||
test("创建模型", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const result = createModel(db, {
|
||||
const result = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text", "reasoning"],
|
||||
modelId: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
providerId,
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(false);
|
||||
const model = (result as { model: { capabilities: string[]; modelId: string; name: string; providerId: string } })
|
||||
.model;
|
||||
@@ -50,12 +59,16 @@ describe("模型数据访问层", () => {
|
||||
|
||||
test("供应商不存在时创建失败", () => {
|
||||
withDb((db) => {
|
||||
const result = createModel(db, {
|
||||
const result = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text"],
|
||||
modelId: "test",
|
||||
name: "Test",
|
||||
providerId: "nonexistent",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(400);
|
||||
});
|
||||
@@ -64,8 +77,12 @@ describe("模型数据访问层", () => {
|
||||
test("同一供应商下模型 ID 唯一", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "Model1", providerId });
|
||||
const result = createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "Model2", providerId });
|
||||
createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "Model1", providerId }, createNoopLogger());
|
||||
const result = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "gpt-4o", name: "Model2", providerId },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("已存在");
|
||||
});
|
||||
@@ -75,8 +92,16 @@ describe("模型数据访问层", () => {
|
||||
withDb((db) => {
|
||||
const p1 = seedProvider(db, "P1");
|
||||
const p2 = seedProvider(db, "P2");
|
||||
const r1 = createModel(db, { capabilities: ["text"], modelId: "same-id", name: "M1", providerId: p1 });
|
||||
const r2 = createModel(db, { capabilities: ["text"], modelId: "same-id", name: "M2", providerId: p2 });
|
||||
const r1 = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "same-id", name: "M1", providerId: p1 },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const r2 = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "same-id", name: "M2", providerId: p2 },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in r1).toBe(false);
|
||||
expect("error" in r2).toBe(false);
|
||||
});
|
||||
@@ -85,7 +110,11 @@ describe("模型数据访问层", () => {
|
||||
test("能力标签为空时创建失败", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const result = createModel(db, { capabilities: [], modelId: "test", name: "Test", providerId });
|
||||
const result = createModel(
|
||||
db,
|
||||
{ capabilities: [], modelId: "test", name: "Test", providerId },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("能力标签");
|
||||
});
|
||||
@@ -95,9 +124,9 @@ describe("模型数据访问层", () => {
|
||||
withDb((db) => {
|
||||
const p1 = seedProvider(db, "P1");
|
||||
const p2 = seedProvider(db, "P2");
|
||||
createModel(db, { capabilities: ["text"], modelId: "m1", name: "Alpha", providerId: p1 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m2", name: "Beta", providerId: p1 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m3", name: "Gamma", providerId: p2 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m1", name: "Alpha", providerId: p1 }, createNoopLogger());
|
||||
createModel(db, { capabilities: ["text"], modelId: "m2", name: "Beta", providerId: p1 }, createNoopLogger());
|
||||
createModel(db, { capabilities: ["text"], modelId: "m3", name: "Gamma", providerId: p2 }, createNoopLogger());
|
||||
|
||||
const all = listModels(db, { page: 1, pageSize: 20 });
|
||||
expect(all.total).toBe(3);
|
||||
@@ -113,7 +142,11 @@ describe("模型数据访问层", () => {
|
||||
test("获取模型详情", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const created = createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "GPT-4o", providerId });
|
||||
const created = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "gpt-4o", name: "GPT-4o", providerId },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { model: { id: string } }).model.id;
|
||||
|
||||
const result = getModel(db, id);
|
||||
@@ -133,10 +166,14 @@ describe("模型数据访问层", () => {
|
||||
test("更新模型", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const created = createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "原名", providerId });
|
||||
const created = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "gpt-4o", name: "原名", providerId },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { model: { id: string } }).model.id;
|
||||
|
||||
const result = updateModel(db, id, { capabilities: ["text", "reasoning"], name: "新名" });
|
||||
const result = updateModel(db, id, { capabilities: ["text", "reasoning"], name: "新名" }, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
const updated = (result as { model: { capabilities: string[]; name: string } }).model;
|
||||
expect(updated.name).toBe("新名");
|
||||
@@ -147,10 +184,14 @@ describe("模型数据访问层", () => {
|
||||
test("删除模型", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const created = createModel(db, { capabilities: ["text"], modelId: "gpt-4o", name: "删除测试", providerId });
|
||||
const created = createModel(
|
||||
db,
|
||||
{ capabilities: ["text"], modelId: "gpt-4o", name: "删除测试", providerId },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { model: { id: string } }).model.id;
|
||||
|
||||
const result = deleteModel(db, id);
|
||||
const result = deleteModel(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const after = getModel(db, id);
|
||||
@@ -162,9 +203,9 @@ describe("模型数据访问层", () => {
|
||||
withDb((db) => {
|
||||
const p1 = seedProvider(db, "P1");
|
||||
const p2 = seedProvider(db, "P2");
|
||||
createModel(db, { capabilities: ["text"], modelId: "m1", name: "M1", providerId: p1 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m2", name: "M2", providerId: p1 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m3", name: "M3", providerId: p2 });
|
||||
createModel(db, { capabilities: ["text"], modelId: "m1", name: "M1", providerId: p1 }, createNoopLogger());
|
||||
createModel(db, { capabilities: ["text"], modelId: "m2", name: "M2", providerId: p1 }, createNoopLogger());
|
||||
createModel(db, { capabilities: ["text"], modelId: "m3", name: "M3", providerId: p2 }, createNoopLogger());
|
||||
|
||||
expect(getModelsByProviderId(db, p1)).toBe(2);
|
||||
expect(getModelsByProviderId(db, p2)).toBe(1);
|
||||
@@ -174,14 +215,18 @@ describe("模型数据访问层", () => {
|
||||
test("可选字段 contextLength 和 maxOutputTokens", () => {
|
||||
withDb((db) => {
|
||||
const providerId = seedProvider(db);
|
||||
const result = createModel(db, {
|
||||
const result = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text"],
|
||||
contextLength: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
modelId: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
providerId,
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(false);
|
||||
const model = (result as { model: { contextLength: null | number; maxOutputTokens: null | number } }).model;
|
||||
expect(model.contextLength).toBe(128000);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
restoreProject,
|
||||
updateProject,
|
||||
} from "../../../src/server/db/projects";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedTestDatabase } from "../../helpers";
|
||||
|
||||
function withProjectsDb(callback: (db: Database) => void): void {
|
||||
@@ -26,7 +27,7 @@ function withProjectsDb(callback: (db: Database) => void): void {
|
||||
describe("项目数据访问层", () => {
|
||||
test("创建项目", () => {
|
||||
withProjectsDb((db) => {
|
||||
const result = createProject(db, { description: "测试描述", name: "测试项目" });
|
||||
const result = createProject(db, { description: "测试描述", name: "测试项目" }, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
expect((result as { project: unknown }).project).toBeDefined();
|
||||
|
||||
@@ -43,8 +44,8 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("项目名称全局唯一(含归档项目)", () => {
|
||||
withProjectsDb((db) => {
|
||||
createProject(db, { name: "唯一名称" });
|
||||
const result2 = createProject(db, { name: "唯一名称" });
|
||||
createProject(db, { name: "唯一名称" }, createNoopLogger());
|
||||
const result2 = createProject(db, { name: "唯一名称" }, createNoopLogger());
|
||||
expect("error" in result2).toBe(true);
|
||||
expect((result2 as unknown as { error: string }).error).toContain("已存在");
|
||||
});
|
||||
@@ -52,7 +53,7 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("trim 后名称为空时创建失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const result = createProject(db, { name: " " });
|
||||
const result = createProject(db, { name: " " }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("不能为空");
|
||||
});
|
||||
@@ -60,9 +61,9 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("列表查询(分页和关键字)", () => {
|
||||
withProjectsDb((db) => {
|
||||
createProject(db, { description: "descA", name: "项目A" });
|
||||
createProject(db, { description: "descB", name: "项目B" });
|
||||
createProject(db, { name: "其他" });
|
||||
createProject(db, { description: "descA", name: "项目A" }, createNoopLogger());
|
||||
createProject(db, { description: "descB", name: "项目B" }, createNoopLogger());
|
||||
createProject(db, { name: "其他" }, createNoopLogger());
|
||||
|
||||
const result1 = listProjects(db, { page: 1, pageSize: 20 });
|
||||
expect(result1.total).toBe(3);
|
||||
@@ -79,7 +80,7 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("获取项目详情", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { description: "详情", name: "详情项目" });
|
||||
const created = createProject(db, { description: "详情", name: "详情项目" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = getProject(db, id);
|
||||
@@ -99,10 +100,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("更新项目名称和描述", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "原名" });
|
||||
const created = createProject(db, { name: "原名" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = updateProject(db, id, { description: "新描述", name: "新名" });
|
||||
const result = updateProject(db, id, { description: "新描述", name: "新名" }, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const updated = result as { project: { description: string; name: string } };
|
||||
@@ -113,11 +114,11 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("更新已归档项目失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "待归档" });
|
||||
const created = createProject(db, { name: "待归档" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
archiveProject(db, id);
|
||||
archiveProject(db, id, createNoopLogger());
|
||||
|
||||
const result = updateProject(db, id, { name: "新名称" });
|
||||
const result = updateProject(db, id, { name: "新名称" }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(409);
|
||||
});
|
||||
@@ -125,10 +126,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("归档项目", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "待归档" });
|
||||
const created = createProject(db, { name: "待归档" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = archiveProject(db, id);
|
||||
const result = archiveProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const archived = (result as { project: { archivedAt: null | string; status: string } }).project;
|
||||
@@ -146,10 +147,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("对已归档项目重复归档失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "测试" });
|
||||
const created = createProject(db, { name: "测试" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
archiveProject(db, id);
|
||||
const result = archiveProject(db, id);
|
||||
archiveProject(db, id, createNoopLogger());
|
||||
const result = archiveProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(409);
|
||||
});
|
||||
@@ -157,11 +158,11 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("恢复已归档项目", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "恢复测试" });
|
||||
const created = createProject(db, { name: "恢复测试" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
archiveProject(db, id);
|
||||
archiveProject(db, id, createNoopLogger());
|
||||
|
||||
const result = restoreProject(db, id);
|
||||
const result = restoreProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const restored = (result as { project: { archivedAt: null | string; status: string } }).project;
|
||||
@@ -172,9 +173,9 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("恢复 active 项目失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "活跃项目" });
|
||||
const created = createProject(db, { name: "活跃项目" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
const result = restoreProject(db, id);
|
||||
const result = restoreProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(409);
|
||||
});
|
||||
@@ -182,11 +183,11 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("永久删除已归档项目", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "删除测试" });
|
||||
const created = createProject(db, { name: "删除测试" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
archiveProject(db, id);
|
||||
archiveProject(db, id, createNoopLogger());
|
||||
|
||||
const result = deleteProject(db, id);
|
||||
const result = deleteProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const after = getProject(db, id);
|
||||
@@ -196,10 +197,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("删除 active 项目失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "活跃项目" });
|
||||
const created = createProject(db, { name: "活跃项目" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = deleteProject(db, id);
|
||||
const result = deleteProject(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(409);
|
||||
});
|
||||
@@ -207,7 +208,7 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("创建项目名称超过 10 个字符失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const result = createProject(db, { name: "这是一个非常非常长的名字" });
|
||||
const result = createProject(db, { name: "这是一个非常非常长的名字" }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("不能超过 10 个字符");
|
||||
});
|
||||
@@ -215,7 +216,7 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("创建项目名称刚好 10 个字符成功", () => {
|
||||
withProjectsDb((db) => {
|
||||
const result = createProject(db, { name: "一二三四五六七八九十" });
|
||||
const result = createProject(db, { name: "一二三四五六七八九十" }, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
const project = (result as { project: { name: string } }).project;
|
||||
expect(project.name).toBe("一二三四五六七八九十");
|
||||
@@ -224,10 +225,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("更新项目名称超过 10 个字符失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "短名" });
|
||||
const created = createProject(db, { name: "短名" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = updateProject(db, id, { name: "这是一个非常非常长的名字" });
|
||||
const result = updateProject(db, id, { name: "这是一个非常非常长的名字" }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("不能超过 10 个字符");
|
||||
});
|
||||
@@ -235,10 +236,10 @@ describe("项目数据访问层", () => {
|
||||
|
||||
test("更新项目名称 trim 后为空失败", () => {
|
||||
withProjectsDb((db) => {
|
||||
const created = createProject(db, { name: "原名" });
|
||||
const created = createProject(db, { name: "原名" }, createNoopLogger());
|
||||
const id = (created as { project: { id: string } }).project.id;
|
||||
|
||||
const result = updateProject(db, id, { name: " " });
|
||||
const result = updateProject(db, id, { name: " " }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("不能为空");
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
listProviders,
|
||||
updateProvider,
|
||||
} from "../../../src/server/db/providers";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedTestDatabase } from "../../helpers";
|
||||
|
||||
function withDb(callback: (db: Database) => void): void {
|
||||
@@ -35,12 +36,16 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("创建供应商", () => {
|
||||
withDb((db) => {
|
||||
const result = createProvider(db, {
|
||||
const result = createProvider(
|
||||
db,
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
name: "OpenAI",
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(false);
|
||||
const provider = (result as { provider: { apiKey: string; baseUrl: string; name: string; type: string } })
|
||||
.provider;
|
||||
@@ -53,8 +58,16 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("供应商名称唯一", () => {
|
||||
withDb((db) => {
|
||||
createProvider(db, { apiKey: "sk-1", baseUrl: "https://a.com", name: "唯一", type: "openai" });
|
||||
const result = createProvider(db, { apiKey: "sk-2", baseUrl: "https://b.com", name: "唯一", type: "openai" });
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-1", baseUrl: "https://a.com", name: "唯一", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const result = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-2", baseUrl: "https://b.com", name: "唯一", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("已存在");
|
||||
});
|
||||
@@ -62,7 +75,11 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("名称为空时创建失败", () => {
|
||||
withDb((db) => {
|
||||
const result = createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: " ", type: "openai" });
|
||||
const result = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk", baseUrl: "https://a.com", name: " ", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("不能为空");
|
||||
});
|
||||
@@ -70,9 +87,21 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("列表查询(分页和关键字)", () => {
|
||||
withDb((db) => {
|
||||
createProvider(db, { apiKey: "sk-1", baseUrl: "https://a.com", name: "Alpha", type: "openai" });
|
||||
createProvider(db, { apiKey: "sk-2", baseUrl: "https://b.com", name: "Beta", type: "anthropic" });
|
||||
createProvider(db, { apiKey: "sk-3", baseUrl: "https://c.com", name: "Gamma", type: "openai-compatible" });
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-1", baseUrl: "https://a.com", name: "Alpha", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-2", baseUrl: "https://b.com", name: "Beta", type: "anthropic" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-3", baseUrl: "https://c.com", name: "Gamma", type: "openai-compatible" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
|
||||
const result1 = listProviders(db, { page: 1, pageSize: 20 });
|
||||
expect(result1.total).toBe(3);
|
||||
@@ -88,7 +117,11 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("获取供应商详情", () => {
|
||||
withDb((db) => {
|
||||
const created = createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "详情", type: "openai" });
|
||||
const created = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk", baseUrl: "https://a.com", name: "详情", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { provider: { id: string } }).provider.id;
|
||||
|
||||
const result = getProvider(db, id);
|
||||
@@ -107,10 +140,14 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("更新供应商", () => {
|
||||
withDb((db) => {
|
||||
const created = createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "原名", type: "openai" });
|
||||
const created = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk", baseUrl: "https://a.com", name: "原名", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { provider: { id: string } }).provider.id;
|
||||
|
||||
const result = updateProvider(db, id, { name: "新名" });
|
||||
const result = updateProvider(db, id, { name: "新名" }, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
expect((result as { provider: { name: string } }).provider.name).toBe("新名");
|
||||
});
|
||||
@@ -118,11 +155,19 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("更新供应商名称重复失败", () => {
|
||||
withDb((db) => {
|
||||
createProvider(db, { apiKey: "sk-1", baseUrl: "https://a.com", name: "已存在", type: "openai" });
|
||||
const created = createProvider(db, { apiKey: "sk-2", baseUrl: "https://b.com", name: "原名", type: "openai" });
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-1", baseUrl: "https://a.com", name: "已存在", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const created = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-2", baseUrl: "https://b.com", name: "原名", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { provider: { id: string } }).provider.id;
|
||||
|
||||
const result = updateProvider(db, id, { name: "已存在" });
|
||||
const result = updateProvider(db, id, { name: "已存在" }, createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { error: string }).error).toContain("已存在");
|
||||
});
|
||||
@@ -130,10 +175,14 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("删除供应商", () => {
|
||||
withDb((db) => {
|
||||
const created = createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "删除测试", type: "openai" });
|
||||
const created = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk", baseUrl: "https://a.com", name: "删除测试", type: "openai" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const id = (created as { provider: { id: string } }).provider.id;
|
||||
|
||||
const result = deleteProvider(db, id);
|
||||
const result = deleteProvider(db, id, createNoopLogger());
|
||||
expect("error" in result).toBe(false);
|
||||
|
||||
const after = getProvider(db, id);
|
||||
@@ -143,7 +192,7 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("删除不存在的供应商返回 404", () => {
|
||||
withDb((db) => {
|
||||
const result = deleteProvider(db, "nonexistent");
|
||||
const result = deleteProvider(db, "nonexistent", createNoopLogger());
|
||||
expect("error" in result).toBe(true);
|
||||
expect((result as unknown as { status: number }).status).toBe(404);
|
||||
});
|
||||
@@ -151,20 +200,28 @@ describe("供应商数据访问层", () => {
|
||||
|
||||
test("默认类型为 openai-compatible", () => {
|
||||
withDb((db) => {
|
||||
createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "默认类型", type: "openai-compatible" });
|
||||
const result = createProvider(db, {
|
||||
createProvider(
|
||||
db,
|
||||
{ apiKey: "sk", baseUrl: "https://a.com", name: "默认类型", type: "openai-compatible" },
|
||||
createNoopLogger(),
|
||||
);
|
||||
const result = createProvider(
|
||||
db,
|
||||
{
|
||||
apiKey: "sk2",
|
||||
baseUrl: "https://b.com",
|
||||
name: "显式默认",
|
||||
type: "openai-compatible",
|
||||
});
|
||||
},
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect((result as { provider: { type: string } }).provider.type).toBe("openai-compatible");
|
||||
});
|
||||
});
|
||||
|
||||
test("供应商 options 返回最小字段", () => {
|
||||
withDb((db) => {
|
||||
createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "选项", type: "openai" });
|
||||
createProvider(db, { apiKey: "sk", baseUrl: "https://a.com", name: "选项", type: "openai" }, createNoopLogger());
|
||||
|
||||
const options = listProviderOptions(db);
|
||||
expect(options.length).toBe(1);
|
||||
|
||||
@@ -7,81 +7,116 @@ import type { Conversation, Message, RuntimeMode } from "../../../src/shared/api
|
||||
import { createModel } from "../../../src/server/db/models";
|
||||
import { createProject } from "../../../src/server/db/projects";
|
||||
import { createProvider } from "../../../src/server/db/providers";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
void mock.module("ai", () => ({
|
||||
createAgentUIStreamResponse: (opts: {
|
||||
agent: unknown;
|
||||
messages: unknown[];
|
||||
onFinish:
|
||||
| ((event: { finishReason?: string; responseMessage: { parts?: Array<{ text: string; type: string }> } }) => void)
|
||||
| undefined;
|
||||
}) => {
|
||||
if (opts.onFinish) {
|
||||
opts.onFinish({
|
||||
responseMessage: {
|
||||
parts: [{ text: "test reply from AI", type: "text" }],
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
'data: {"type":"start-step"}\n\ndata: {"type":"text-start","id":"txt-1"}\n\ndata: {"type":"text-delta","id":"txt-1","delta":"test reply from AI"}\n\ndata: {"type":"text-end","id":"txt-1"}\n\ndata: {"type":"finish-step"}\n\ndata: {"type":"finish"}\n\n',
|
||||
{
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
createProviderRegistry: () => ({
|
||||
languageModel: () => ({}),
|
||||
}),
|
||||
generateText: () => Promise.resolve({ text: "mock", usage: {} }),
|
||||
generateText: () => Promise.resolve({ text: "AI总结标题", usage: {} }),
|
||||
stepCountIs: () => () => true,
|
||||
streamText: () => ({
|
||||
text: Promise.resolve("test reply from AI"),
|
||||
toUIMessageStreamResponse: () =>
|
||||
new Response('data: {"type":"text","text":"test reply from AI"}\n\n', {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
}),
|
||||
tool: () => ({ execute: async () => await Promise.resolve({}) }),
|
||||
ToolLoopAgent: function M() {
|
||||
// no-op: createAgentUIStreamResponse handles streaming
|
||||
},
|
||||
}));
|
||||
|
||||
async function createConversationViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateConversation: h } = await import("../../../src/server/routes/chat/create");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function deleteConversationViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteConversation: h } = await import("../../../src/server/routes/chat/delete");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getConversationViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetConversation: h } = await import("../../../src/server/routes/chat/get");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listConversationsViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListConversations: h } = await import("../../../src/server/routes/chat/list");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listMessagesViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListMessages: h } = await import("../../../src/server/routes/chat/messages");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function seedModel(db: Database, providerId: string, modelName = "GPT-4o"): string {
|
||||
const result = createModel(db, {
|
||||
async function patchConversationViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateConversation: h } = await import("../../../src/server/routes/chat/update");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function seedModel(db: Database, providerId: string, modelName = "GPT-4o", modelId = "gpt-4o"): string {
|
||||
const result = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text"],
|
||||
modelId: "gpt-4o",
|
||||
modelId,
|
||||
name: modelName,
|
||||
providerId,
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.model.id;
|
||||
}
|
||||
|
||||
function seedProject(db: Database, name = "测试项目"): string {
|
||||
const result = createProject(db, { description: "测试", name });
|
||||
const result = createProject(db, { description: "测试", name }, LOG);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.project.id;
|
||||
}
|
||||
|
||||
function seedProvider(db: Database, name = "测试供应商"): string {
|
||||
const result = createProvider(db, {
|
||||
const result = createProvider(
|
||||
db,
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.test.com/v1",
|
||||
name,
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.provider.id;
|
||||
}
|
||||
|
||||
async function sendChatViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleSendChat: h } = await import("../../../src/server/routes/chat/send");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
describe("聊天 API 路由", () => {
|
||||
@@ -346,6 +381,167 @@ describe("聊天 API 路由", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/projects/:id/conversations/:cid", () => {
|
||||
test("更新会话 modelId 成功", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-model");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
const modelId1 = seedModel(db, providerId, "GPT-4o", "gpt-4o");
|
||||
const modelId2 = seedModel(db, providerId, "GPT-4o-mini", "gpt-4o-mini");
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({ modelId: modelId1 }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ modelId: modelId2 }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { conversation: Conversation };
|
||||
expect(body.conversation.modelId).toBe(modelId2);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("更新会话 title 成功", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-title");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ title: "新标题" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { conversation: Conversation };
|
||||
expect(body.conversation.title).toBe("新标题");
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("跨项目更新会话返回 403", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-403");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectA = seedProject(db, "项目A");
|
||||
const projectB = seedProject(db, "项目B");
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectA}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectB}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ title: "探测" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("不存在的会话返回 404", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-404");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/nonexistent`, {
|
||||
body: JSON.stringify({ title: "探测" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("无效 modelId 返回 400", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-bad-model");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ modelId: "invalid-model-id" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/projects/:id/conversations/:cid/messages", () => {
|
||||
test("跨项目获取消息返回 403", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-msg-403");
|
||||
@@ -420,8 +616,56 @@ describe("聊天 API 路由", () => {
|
||||
db,
|
||||
);
|
||||
const msgBody = (await msgRes.json()) as { items: Message[] };
|
||||
expect(msgBody.items.length).toBeGreaterThanOrEqual(1);
|
||||
expect(msgBody.items.length).toBeGreaterThanOrEqual(2);
|
||||
expect(msgBody.items.some((m) => m.role === "user")).toBe(true);
|
||||
expect(msgBody.items.some((m) => m.role === "assistant")).toBe(true);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("首次发送消息时触发标题生成", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-send-title");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
const modelId = seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
expect(created.title).toBe("新会话");
|
||||
|
||||
await sendChatViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/chat`, {
|
||||
body: JSON.stringify({
|
||||
conversationId: created.id,
|
||||
messages: [{ parts: [{ text: "请帮我分析一下这个项目的性能瓶颈", type: "text" }], role: "user" }],
|
||||
modelDbId: modelId,
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const getRes = await getConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`),
|
||||
db,
|
||||
);
|
||||
const body = (await getRes.json()) as { conversation: Conversation };
|
||||
expect(body.conversation.title).not.toBe("新会话");
|
||||
expect(body.conversation.title).toBe("AI总结标题");
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
|
||||
@@ -4,40 +4,46 @@ import { describe, expect, mock, test } from "bun:test";
|
||||
|
||||
import type { Model, RuntimeMode } from "../../../src/shared/api";
|
||||
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
async function createModelViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateModel: h } = await import("../../../src/server/routes/models/create");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function createTestModel(db: Database, pName: string, providerId?: string): Model {
|
||||
const pid = providerId ?? seedProvider(db);
|
||||
const result = createModel(db, {
|
||||
const result = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text"],
|
||||
modelId: pName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
||||
name: pName,
|
||||
providerId: pid,
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.model;
|
||||
}
|
||||
|
||||
async function deleteModelViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteModel: h } = await import("../../../src/server/routes/models/delete");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getModelViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetModel: h } = await import("../../../src/server/routes/models/get");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listModelsViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListModels: h } = await import("../../../src/server/routes/models/list");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
import { createModel } from "../../../src/server/db/models";
|
||||
@@ -51,24 +57,28 @@ void mock.module("ai", () => ({
|
||||
}));
|
||||
|
||||
function seedProvider(db: Database, name?: string): string {
|
||||
const result = createProvider(db, {
|
||||
const result = createProvider(
|
||||
db,
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.test.com/v1",
|
||||
name: name ?? "TestProvider",
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.provider.id;
|
||||
}
|
||||
|
||||
async function testModelViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleTestModelConfig: h } = await import("../../../src/server/routes/models/test");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function updateModelViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateModel: h } = await import("../../../src/server/routes/models/update");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
|
||||
|
||||
@@ -4,50 +4,52 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { Project, RuntimeMode } from "../../../src/shared/api";
|
||||
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
async function archiveProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleArchiveProject: h } = await import("../../../src/server/routes/projects/archive");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
// Inline imports for actual route handler tests (each handler is in separate file)
|
||||
async function createProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateProject: h } = await import("../../../src/server/routes/projects/create");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function createTestProject(db: Database, name = "测试项目"): Project {
|
||||
const result = createProject(db, { name });
|
||||
const result = createProject(db, { name }, LOG);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
async function deleteProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteProject: h } = await import("../../../src/server/routes/projects/delete");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetProject: h } = await import("../../../src/server/routes/projects/get");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listProjectsViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListProjects: h } = await import("../../../src/server/routes/projects/list");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function restoreProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleRestoreProject: h } = await import("../../../src/server/routes/projects/restore");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function updateProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateProject: h } = await import("../../../src/server/routes/projects/update");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
// Need db/projects for setup
|
||||
@@ -135,7 +137,7 @@ describe("项目 API 路由", () => {
|
||||
test("POST /api/projects/:id/restore 恢复项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "恢复路由");
|
||||
archiveProject(db, project.id);
|
||||
archiveProject(db, project.id, LOG);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/restore`, { method: "POST" });
|
||||
const res = await restoreProjectViaHandler(req, db);
|
||||
@@ -148,7 +150,7 @@ describe("项目 API 路由", () => {
|
||||
test("DELETE /api/projects/:id 永久删除已归档项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "删除路由");
|
||||
archiveProject(db, project.id);
|
||||
archiveProject(db, project.id, LOG);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}`, { method: "DELETE" });
|
||||
const res = await deleteProjectViaHandler(req, db);
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { Provider, ProviderOption, RuntimeMode } from "../../../src/shared/
|
||||
|
||||
import { createModel } from "../../../src/server/db/models";
|
||||
import { createProvider } from "../../../src/server/db/providers";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
void mock.module("ai", () => ({
|
||||
createProviderRegistry: () => ({
|
||||
@@ -18,48 +20,52 @@ void mock.module("ai", () => ({
|
||||
|
||||
async function createProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateProvider: h } = await import("../../../src/server/routes/providers/create");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function createTestProvider(db: Database, name = "测试供应商", baseUrl = "https://api.test.com/v1"): Provider {
|
||||
const result = createProvider(db, {
|
||||
const result = createProvider(
|
||||
db,
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
baseUrl,
|
||||
name,
|
||||
type: "openai",
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.provider;
|
||||
}
|
||||
|
||||
async function deleteProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteProvider: h } = await import("../../../src/server/routes/providers/delete");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetProvider: h } = await import("../../../src/server/routes/providers/get");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listProviderOptionsViaHandler(_req: Request, db: Database): Promise<Response> {
|
||||
const { handleListProviderOptions: h } = await import("../../../src/server/routes/providers/options");
|
||||
return h(db, MODE);
|
||||
return h(db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listProvidersViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListProviders: h } = await import("../../../src/server/routes/providers/list");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function testProviderConfigViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleTestProviderConfig: h } = await import("../../../src/server/routes/providers/test");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function updateProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateProvider: h } = await import("../../../src/server/routes/providers/update");
|
||||
return h(req, db, MODE);
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function withProviderServer(
|
||||
@@ -182,12 +188,16 @@ describe("供应商 API 路由", () => {
|
||||
test("DELETE /api/providers/:id 存在关联模型时返回 409", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const provider = createTestProvider(db, "有关联模型");
|
||||
const modelResult = createModel(db, {
|
||||
const modelResult = createModel(
|
||||
db,
|
||||
{
|
||||
capabilities: ["text"],
|
||||
modelId: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
providerId: provider.id,
|
||||
});
|
||||
},
|
||||
LOG,
|
||||
);
|
||||
if ("error" in modelResult) throw new Error(modelResult.error);
|
||||
|
||||
const req = new Request(`http://localhost/api/providers/${provider.id}`, { method: "DELETE" });
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* 后端测试无需 DOM 环境,前端测试依赖 jsdom 及 antd polyfill
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
// 仅当前端测试需要时初始化 jsdom(所有测试共享 preload,后端测试也在此环境中运行)
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
|
||||
167
tests/web/components/ChatPanel.test.tsx
Normal file
167
tests/web/components/ChatPanel.test.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
import type { Conversation, Model } from "../../../src/shared/api";
|
||||
|
||||
import { ChatPanel } from "../../../src/web/consoles/workbench/components/chat/ChatPanel";
|
||||
import { installFetchMock, jsonResponse, renderWithProviders } from "../test-utils";
|
||||
|
||||
const PROJECT_ID = "proj-1";
|
||||
|
||||
const TEXT_MODEL: Model = {
|
||||
capabilities: ["text"],
|
||||
contextLength: null,
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
id: "model-1",
|
||||
maxOutputTokens: null,
|
||||
modelId: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
providerId: "pv1",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const CONVERSATION: Conversation = {
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
id: "conv-1",
|
||||
modelId: "model-1",
|
||||
projectId: PROJECT_ID,
|
||||
title: "新会话",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const noop = () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
void mock.module("@ai-sdk/react", () => ({
|
||||
useChat: () => ({
|
||||
messages: [],
|
||||
regenerate: noop,
|
||||
sendMessage: noop,
|
||||
setMessages: (msgs: unknown) => msgs,
|
||||
status: "ready",
|
||||
stop: noop,
|
||||
}),
|
||||
}));
|
||||
|
||||
void mock.module("ai", () => ({
|
||||
DefaultChatTransport: function () {
|
||||
return undefined;
|
||||
},
|
||||
}));
|
||||
|
||||
function getSendButton() {
|
||||
return screen.getByRole("button", { name: /发.*送/ });
|
||||
}
|
||||
|
||||
function setupFetchMock() {
|
||||
return installFetchMock((call) => {
|
||||
if (call.url.includes("/models")) {
|
||||
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
||||
}
|
||||
if (call.url.endsWith("/conversations") && call.method === "POST") {
|
||||
return jsonResponse({ conversation: { ...CONVERSATION, id: "conv-new" } }, { status: 201 });
|
||||
}
|
||||
if (/\/conversations\/conv-1$/.exec(call.url)) {
|
||||
return jsonResponse({ conversation: CONVERSATION });
|
||||
}
|
||||
if (call.url.includes("/messages")) {
|
||||
return jsonResponse({ items: [], total: 0 });
|
||||
}
|
||||
if (/\/conversations$/.exec(call.url) && call.method === "GET") {
|
||||
return jsonResponse({ items: [], total: 0 });
|
||||
}
|
||||
return jsonResponse({ error: "not found" }, { status: 404 });
|
||||
});
|
||||
}
|
||||
|
||||
describe("ChatPanel", () => {
|
||||
describe("欢迎页", () => {
|
||||
test("无会话时显示欢迎页和输入框", () => {
|
||||
setupFetchMock();
|
||||
renderWithProviders(
|
||||
createElement(ChatPanel, {
|
||||
conversationId: null,
|
||||
onConversationCreated: noop,
|
||||
projectId: PROJECT_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByText("你好,我是阿福")).toBeTruthy();
|
||||
expect(screen.getByText("有什么我可以帮助你的吗?")).toBeTruthy();
|
||||
expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy();
|
||||
expect(getSendButton()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("自动创建会话", () => {
|
||||
test("输入并发送后自动创建会话并通知父组件", async () => {
|
||||
const calls = setupFetchMock();
|
||||
const onCreated = mock<(id: string) => void>(() => undefined);
|
||||
|
||||
renderWithProviders(
|
||||
createElement(ChatPanel, {
|
||||
conversationId: null,
|
||||
onConversationCreated: onCreated,
|
||||
projectId: PROJECT_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("输入消息...");
|
||||
fireEvent.change(input, { target: { value: "你好" } });
|
||||
fireEvent.click(getSendButton());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onCreated).toHaveBeenCalledWith("conv-new");
|
||||
});
|
||||
|
||||
const createCall = calls.find((c) => c.url.endsWith("/conversations") && c.method === "POST");
|
||||
expect(createCall).toBeTruthy();
|
||||
});
|
||||
|
||||
test("创建会话失败时恢复输入文本", async () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/models")) {
|
||||
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
|
||||
}
|
||||
if (call.url.endsWith("/conversations") && call.method === "POST") {
|
||||
return jsonResponse({ error: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
return jsonResponse({ error: "not found" }, { status: 404 });
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
createElement(ChatPanel, {
|
||||
conversationId: null,
|
||||
onConversationCreated: noop,
|
||||
projectId: PROJECT_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("输入消息...");
|
||||
fireEvent.change(input, { target: { value: "测试输入" } });
|
||||
fireEvent.click(getSendButton());
|
||||
|
||||
await waitFor(() => {
|
||||
expect((input as HTMLTextAreaElement).value).toBe("测试输入");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("聊天面板", () => {
|
||||
test("选中会话时显示消息列表区域", () => {
|
||||
setupFetchMock();
|
||||
renderWithProviders(
|
||||
createElement(ChatPanel, {
|
||||
conversationId: "conv-1",
|
||||
onConversationCreated: noop,
|
||||
projectId: PROJECT_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.queryByText("你好,我是阿福")).toBeNull();
|
||||
expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ describe("ErrorBoundary", () => {
|
||||
expect(screen.getByText("渲染错误")).not.toBeNull();
|
||||
expect(screen.getByText("页面渲染出现异常,请刷新重试")).not.toBeNull();
|
||||
expect(screen.getByRole("button", { name: "刷新页面" })).not.toBeNull();
|
||||
expect(errors.some((line) => line.includes("渲染错误:"))).toBe(true);
|
||||
expect(errors.some((line) => line.includes("[Alfred:ERROR] 渲染错误"))).toBe(true);
|
||||
});
|
||||
|
||||
test("点击刷新页面按钮不会破坏错误兜底界面", () => {
|
||||
|
||||
102
tests/web/components/query-client-logging.test.tsx
Normal file
102
tests/web/components/query-client-logging.test.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createElement, useRef } from "react";
|
||||
|
||||
import { useCreateProject } from "../../../src/web/hooks/use-projects";
|
||||
import { installFetchMock, jsonResponse } from "../test-utils";
|
||||
|
||||
describe("QueryClient MutationCache onError", () => {
|
||||
test("mutation 错误触发 MutationCache onError 回调", async () => {
|
||||
installFetchMock(() => jsonResponse({ error: "项目名称已存在" }, { status: 409 }));
|
||||
|
||||
const errors: string[] = [];
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error: Error) => {
|
||||
errors.push(error.message);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
function TestComponent({ onResult }: { onResult: (mutate: () => void) => void }) {
|
||||
const { mutate } = useCreateProject();
|
||||
const called = useRef(false);
|
||||
|
||||
if (!called.current) {
|
||||
called.current = true;
|
||||
onResult(() => {
|
||||
mutate(
|
||||
{ name: "test" },
|
||||
{
|
||||
onError: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(
|
||||
createElement(
|
||||
QueryClientProvider,
|
||||
{ client: queryClient },
|
||||
createElement(TestComponent, { onResult: (fn) => fn() }),
|
||||
),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toBe("项目名称已存在");
|
||||
});
|
||||
});
|
||||
|
||||
describe("QueryClient QueryCache onError", () => {
|
||||
test("query 错误触发 QueryCache onError 回调", async () => {
|
||||
installFetchMock(() => new Response("broken", { status: 500 }));
|
||||
|
||||
const errors: string[] = [];
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
queryCache: new QueryCache({
|
||||
onError: (error: Error) => {
|
||||
errors.push(error.message);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
function TestComponent({ onResult }: { onResult: (trigger: () => void) => void }) {
|
||||
const called = useRef(false);
|
||||
|
||||
useQuery({
|
||||
queryFn: () => Promise.reject(new Error("test query error")),
|
||||
queryKey: ["test-query-error"],
|
||||
});
|
||||
|
||||
if (!called.current) {
|
||||
called.current = true;
|
||||
onResult(() => {
|
||||
// no-op trigger
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(
|
||||
createElement(
|
||||
QueryClientProvider,
|
||||
{ client: queryClient },
|
||||
createElement(TestComponent, { onResult: (fn) => fn() }),
|
||||
),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toBe("test query error");
|
||||
});
|
||||
});
|
||||
398
tests/web/hooks/on-success-logging.test.tsx
Normal file
398
tests/web/hooks/on-success-logging.test.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createElement, useRef } from "react";
|
||||
|
||||
import {
|
||||
useCreateModel,
|
||||
useDeleteModel,
|
||||
useTestModelConnection,
|
||||
useUpdateModel,
|
||||
} from "../../../src/web/hooks/use-models";
|
||||
import {
|
||||
useArchiveProject,
|
||||
useCreateProject,
|
||||
useDeleteProject,
|
||||
useRestoreProject,
|
||||
useUpdateProject,
|
||||
} from "../../../src/web/hooks/use-projects";
|
||||
import {
|
||||
useCreateProvider,
|
||||
useDeleteProvider,
|
||||
useTestProviderConfig,
|
||||
useUpdateProvider,
|
||||
} from "../../../src/web/hooks/use-providers";
|
||||
import { installFetchMock, jsonResponse } from "../test-utils";
|
||||
|
||||
const MODEL = {
|
||||
autoAdapt: true,
|
||||
capabilities: ["text"] as string[],
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
customApiKey: null,
|
||||
customBaseUrl: null,
|
||||
description: "测试模型",
|
||||
id: "m1",
|
||||
modelId: "gpt-4",
|
||||
name: "测试模型",
|
||||
providerId: "prov-1",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const PROJECT = {
|
||||
archivedAt: null,
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
description: "测试",
|
||||
id: "p1",
|
||||
name: "测试项目",
|
||||
status: "active" as const,
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const PROVIDER = {
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
id: "prov-1",
|
||||
name: "测试供应商",
|
||||
type: "openai" as const,
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
function getLogMessages(spy: ReturnType<typeof mock>) {
|
||||
return spy.mock.calls.map((c) => c[0] as string).filter((s) => s.includes("[Alfred:INFO]"));
|
||||
}
|
||||
|
||||
function makeQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
}
|
||||
|
||||
function setupModelFetches(result: unknown) {
|
||||
installFetchMock((call) => {
|
||||
if (call.method === "DELETE") return new Response(null, { status: 204 });
|
||||
if (call.url.includes("test")) return jsonResponse({ modelTestResponse: { message: "ok", ok: true } });
|
||||
return jsonResponse({ model: result }, { status: 201 });
|
||||
});
|
||||
}
|
||||
|
||||
function setupProjectFetches(result: unknown) {
|
||||
installFetchMock((call) => {
|
||||
if (call.method === "DELETE") return new Response(null, { status: 204 });
|
||||
if (call.url.includes("archive")) return jsonResponse({ project: result });
|
||||
if (call.url.includes("restore")) return jsonResponse({ project: result });
|
||||
return jsonResponse({ project: result }, { status: 201 });
|
||||
});
|
||||
}
|
||||
|
||||
function setupProviderFetches(result: unknown) {
|
||||
installFetchMock((call) => {
|
||||
if (call.method === "DELETE") return new Response(null, { status: 204 });
|
||||
if (call.url.includes("test")) return jsonResponse({ providerTestResponse: { message: "ok", ok: true } });
|
||||
return jsonResponse({ provider: result }, { status: 201 });
|
||||
});
|
||||
}
|
||||
|
||||
function spyConsoleLog() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const spy = mock((..._args: any[]) => {});
|
||||
const orig = console.log;
|
||||
console.log = spy;
|
||||
return { orig, restore: () => (console.log = orig), spy };
|
||||
}
|
||||
|
||||
describe("useProjects onSuccess 日志", () => {
|
||||
const qc = makeQueryClient();
|
||||
|
||||
test("create onSuccess", async () => {
|
||||
setupProjectFetches(PROJECT);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useCreateProject();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ name: "x" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/项目创建成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("update onSuccess", async () => {
|
||||
setupProjectFetches(PROJECT);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useUpdateProject();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ data: { name: "y" }, id: "p1" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/项目更新成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("delete onSuccess", async () => {
|
||||
setupProjectFetches(PROJECT);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useDeleteProject();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate("p1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/项目删除成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("archive onSuccess", async () => {
|
||||
setupProjectFetches(PROJECT);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useArchiveProject();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate("p1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/项目归档成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("restore onSuccess", async () => {
|
||||
setupProjectFetches(PROJECT);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useRestoreProject();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate("p1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/项目恢复成功/);
|
||||
restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useModels onSuccess 日志", () => {
|
||||
const qc = makeQueryClient();
|
||||
|
||||
test("create onSuccess", async () => {
|
||||
setupModelFetches(MODEL);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useCreateModel();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ capabilities: ["text"], modelId: "gpt-4", name: "x", providerId: "p1" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/模型创建成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("update onSuccess", async () => {
|
||||
setupModelFetches(MODEL);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useUpdateModel();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ data: { name: "y" }, id: "m1" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/模型更新成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("delete onSuccess", async () => {
|
||||
setupModelFetches(MODEL);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useDeleteModel();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate("m1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/模型删除成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("test onSuccess", async () => {
|
||||
setupModelFetches(MODEL);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useTestModelConnection();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ modelId: "gpt-4", providerId: "p1" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
restore();
|
||||
// useTestModelConnection has no onSuccess logger
|
||||
const infoCalls = spy.mock.calls.filter((c) => typeof c[0] === "string" && c[0].includes("[Alfred:INFO]"));
|
||||
expect(infoCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useProviders onSuccess 日志", () => {
|
||||
const qc = makeQueryClient();
|
||||
|
||||
test("create onSuccess", async () => {
|
||||
setupProviderFetches(PROVIDER);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useCreateProvider();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ apiKey: "k", baseUrl: "http://x", name: "x", type: "openai" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/供应商创建成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("update onSuccess", async () => {
|
||||
setupProviderFetches(PROVIDER);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useUpdateProvider();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ data: { name: "y" }, id: "prov-1" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/供应商更新成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("delete onSuccess", async () => {
|
||||
setupProviderFetches(PROVIDER);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useDeleteProvider();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate("prov-1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const msgs = getLogMessages(spy);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0]).toMatch(/供应商删除成功/);
|
||||
restore();
|
||||
});
|
||||
|
||||
test("test onSuccess", async () => {
|
||||
setupProviderFetches(PROVIDER);
|
||||
const { restore, spy } = spyConsoleLog();
|
||||
|
||||
function T({ onResult }: { onResult: (fn: () => void) => void }) {
|
||||
const { mutate } = useTestProviderConfig();
|
||||
const c = useRef(false);
|
||||
if (!c.current) {
|
||||
c.current = true;
|
||||
onResult(() => mutate({ apiKey: "k", baseUrl: "http://x", name: "x", type: "openai" }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render(createElement(QueryClientProvider, { client: qc }, createElement(T, { onResult: (fn) => fn() })));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
restore();
|
||||
// useTestProviderConfig has no onSuccess logger
|
||||
const infoMsgs = spy.mock.calls.filter((c) => typeof c[0] === "string" && String(c[0]).includes("[Alfred:INFO]"));
|
||||
expect(infoMsgs.length).toBe(0);
|
||||
});
|
||||
});
|
||||
72
tests/web/hooks/use-logger.test.ts
Normal file
72
tests/web/hooks/use-logger.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
import type { Logger } from "../../../src/web/utils/logger";
|
||||
|
||||
import { useLogger } from "../../../src/web/hooks/use-logger";
|
||||
import { renderWithProviders } from "../test-utils";
|
||||
|
||||
function HookTester({ onMount }: { onMount: (logger: Logger) => void }) {
|
||||
const logger = useLogger();
|
||||
onMount(logger);
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("useLogger", () => {
|
||||
test("返回 Logger 实例含所有方法", () => {
|
||||
let logger: Logger | undefined;
|
||||
const onMount = (l: Logger) => {
|
||||
logger = l;
|
||||
};
|
||||
|
||||
renderWithProviders(createElement(HookTester, { onMount }));
|
||||
|
||||
expect(logger).toBeDefined();
|
||||
expect(typeof logger!.debug).toBe("function");
|
||||
expect(typeof logger!.info).toBe("function");
|
||||
expect(typeof logger!.warn).toBe("function");
|
||||
expect(typeof logger!.error).toBe("function");
|
||||
expect(typeof logger!.child).toBe("function");
|
||||
expect(typeof logger!.setLevel).toBe("function");
|
||||
});
|
||||
|
||||
test("调用 logger.warn 时静默不抛异常", () => {
|
||||
const warnSpy = mock(() => {});
|
||||
const origWarn = console.warn;
|
||||
console.warn = warnSpy;
|
||||
|
||||
let logger: Logger | undefined;
|
||||
renderWithProviders(
|
||||
createElement(HookTester, {
|
||||
onMount: (l: Logger) => {
|
||||
logger = l;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => logger!.warn("测试警告")).not.toThrow();
|
||||
|
||||
console.warn = origWarn;
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("调用 logger.error 时静默不抛异常", () => {
|
||||
const errorSpy = mock(() => {});
|
||||
const origError = console.error;
|
||||
console.error = errorSpy;
|
||||
|
||||
let logger: Logger | undefined;
|
||||
renderWithProviders(
|
||||
createElement(HookTester, {
|
||||
onMount: (l: Logger) => {
|
||||
logger = l;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => logger!.error("测试错误")).not.toThrow();
|
||||
|
||||
console.error = origError;
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user