docs: Day 12-13 completion summary and milestone update

This commit is contained in:
AI Clinical Dev Team
2025-10-10 20:33:18 +08:00
parent 702e42febb
commit 8afff23995
17 changed files with 2331 additions and 45 deletions

View File

@@ -12,9 +12,12 @@
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@prisma/client": "^6.17.0",
"axios": "^1.12.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"prisma": "^6.17.0"
"js-yaml": "^4.1.0",
"prisma": "^6.17.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
@@ -888,6 +891,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmmirror.com/asn1.js/-/asn1.js-5.4.1.tgz",
@@ -900,6 +909,12 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -919,6 +934,17 @@
"fastq": "^1.17.1"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1009,6 +1035,19 @@
"url": "https://dotenvx.com"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
@@ -1040,6 +1079,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@@ -1121,6 +1172,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz",
@@ -1158,6 +1218,20 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1196,6 +1270,51 @@
"once": "^1.4.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.10",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.10.tgz",
@@ -1473,6 +1592,42 @@
"node": ">=20"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1488,6 +1643,52 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-tsconfig": {
"version": "4.12.0",
"resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.12.0.tgz",
@@ -1531,6 +1732,18 @@
"node": ">= 6"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
@@ -1541,6 +1754,45 @@
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz",
@@ -1635,6 +1887,18 @@
"node": ">=10"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-ref-resolver": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
@@ -1704,6 +1968,36 @@
"dev": true,
"license": "ISC"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -2021,6 +2315,12 @@
],
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -2472,6 +2772,15 @@
"engines": {
"node": ">=6"
}
},
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -25,9 +25,12 @@
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@prisma/client": "^6.17.0",
"axios": "^1.12.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"prisma": "^6.17.0"
"js-yaml": "^4.1.0",
"prisma": "^6.17.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",

View File

@@ -0,0 +1,24 @@
/*
Warnings:
- You are about to drop the column `description` on the `projects` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "conversations" ADD COLUMN "deleted_at" TIMESTAMP(3),
ADD COLUMN "metadata" JSONB;
-- AlterTable
ALTER TABLE "messages" ADD COLUMN "model" TEXT;
-- AlterTable
ALTER TABLE "projects" DROP COLUMN "description",
ADD COLUMN "background" TEXT NOT NULL DEFAULT '',
ADD COLUMN "deleted_at" TIMESTAMP(3),
ADD COLUMN "research_type" TEXT NOT NULL DEFAULT 'observational';
-- CreateIndex
CREATE INDEX "conversations_deleted_at_idx" ON "conversations"("deleted_at");
-- CreateIndex
CREATE INDEX "projects_deleted_at_idx" ON "projects"("deleted_at");

View File

@@ -78,9 +78,11 @@ model Conversation {
modelName String @default("deepseek-v3") @map("model_name")
messageCount Int @default(0) @map("message_count")
totalTokens Int @default(0) @map("total_tokens")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@ -90,6 +92,7 @@ model Conversation {
@@index([projectId])
@@index([agentId])
@@index([createdAt])
@@index([deletedAt])
@@map("conversations")
}
@@ -98,6 +101,7 @@ model Message {
conversationId String @map("conversation_id")
role String
content String @db.Text
model String?
metadata Json?
tokens Int?
isPinned Boolean @default(false) @map("is_pinned")

View File

@@ -0,0 +1,150 @@
import axios from 'axios';
import { ILLMAdapter, Message, LLMOptions, LLMResponse, StreamChunk } from './types.js';
import { config } from '../config/env.js';
export class DeepSeekAdapter implements ILLMAdapter {
modelName: string;
private apiKey: string;
private baseURL: string;
constructor(modelName: string = 'deepseek-chat') {
this.modelName = modelName;
this.apiKey = config.deepseekApiKey || '';
this.baseURL = 'https://api.deepseek.com/v1';
if (!this.apiKey) {
throw new Error('DeepSeek API key is not configured');
}
}
// 非流式调用
async chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
try {
const response = await axios.post(
`${this.baseURL}/chat/completions`,
{
model: this.modelName,
messages: messages,
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2000,
top_p: options?.topP ?? 0.9,
stream: false,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
timeout: 60000, // 60秒超时
}
);
const choice = response.data.choices[0];
return {
content: choice.message.content,
model: response.data.model,
usage: {
promptTokens: response.data.usage.prompt_tokens,
completionTokens: response.data.usage.completion_tokens,
totalTokens: response.data.usage.total_tokens,
},
finishReason: choice.finish_reason,
};
} catch (error: unknown) {
console.error('DeepSeek API Error:', error);
if (axios.isAxiosError(error)) {
throw new Error(
`DeepSeek API调用失败: ${error.response?.data?.error?.message || error.message}`
);
}
throw error;
}
}
// 流式调用
async *chatStream(
messages: Message[],
options?: LLMOptions,
onChunk?: (chunk: StreamChunk) => void
): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await axios.post(
`${this.baseURL}/chat/completions`,
{
model: this.modelName,
messages: messages,
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2000,
top_p: options?.topP ?? 0.9,
stream: true,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
responseType: 'stream',
timeout: 60000,
}
);
const stream = response.data;
let buffer = '';
for await (const chunk of stream) {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
continue;
}
if (trimmedLine.startsWith('data: ')) {
try {
const jsonStr = trimmedLine.slice(6);
const data = JSON.parse(jsonStr);
const choice = data.choices[0];
const content = choice.delta?.content || '';
const streamChunk: StreamChunk = {
content: content,
done: choice.finish_reason === 'stop',
model: data.model,
};
if (choice.finish_reason === 'stop' && data.usage) {
streamChunk.usage = {
promptTokens: data.usage.prompt_tokens,
completionTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
};
}
if (onChunk) {
onChunk(streamChunk);
}
yield streamChunk;
} catch (parseError) {
console.error('Failed to parse SSE data:', parseError);
}
}
}
}
} catch (error) {
console.error('DeepSeek Stream Error:', error);
if (axios.isAxiosError(error)) {
throw new Error(
`DeepSeek流式调用失败: ${error.response?.data?.error?.message || error.message}`
);
}
throw error;
}
}
}

View File

@@ -0,0 +1,77 @@
import { ILLMAdapter, ModelType } from './types.js';
import { DeepSeekAdapter } from './DeepSeekAdapter.js';
import { QwenAdapter } from './QwenAdapter.js';
/**
* LLM工厂类
* 根据模型类型创建相应的适配器实例
*/
export class LLMFactory {
private static adapters: Map<string, ILLMAdapter> = new Map();
/**
* 获取LLM适配器实例单例模式
* @param modelType 模型类型
* @returns LLM适配器实例
*/
static getAdapter(modelType: ModelType): ILLMAdapter {
// 如果已经创建过该适配器,直接返回
if (this.adapters.has(modelType)) {
return this.adapters.get(modelType)!;
}
// 根据模型类型创建适配器
let adapter: ILLMAdapter;
switch (modelType) {
case 'deepseek-v3':
adapter = new DeepSeekAdapter('deepseek-chat');
break;
case 'qwen3-72b':
adapter = new QwenAdapter('qwen-max'); // Qwen3-72B对应的模型名
break;
case 'gemini-pro':
// TODO: 实现Gemini适配器
throw new Error('Gemini adapter is not implemented yet');
default:
throw new Error(`Unsupported model type: ${modelType}`);
}
// 缓存适配器实例
this.adapters.set(modelType, adapter);
return adapter;
}
/**
* 清除适配器缓存
* @param modelType 可选,指定清除某个模型的适配器,不传则清除所有
*/
static clearCache(modelType?: ModelType): void {
if (modelType) {
this.adapters.delete(modelType);
} else {
this.adapters.clear();
}
}
/**
* 检查模型是否支持
* @param modelType 模型类型
* @returns 是否支持
*/
static isSupported(modelType: string): boolean {
return ['deepseek-v3', 'qwen3-72b', 'gemini-pro'].includes(modelType);
}
/**
* 获取所有支持的模型列表
* @returns 支持的模型列表
*/
static getSupportedModels(): ModelType[] {
return ['deepseek-v3', 'qwen3-72b', 'gemini-pro'];
}
}

View File

@@ -0,0 +1,162 @@
import axios from 'axios';
import { ILLMAdapter, Message, LLMOptions, LLMResponse, StreamChunk } from './types.js';
import { config } from '../config/env.js';
export class QwenAdapter implements ILLMAdapter {
modelName: string;
private apiKey: string;
private baseURL: string;
constructor(modelName: string = 'qwen-turbo') {
this.modelName = modelName;
this.apiKey = config.qwenApiKey || '';
this.baseURL = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
if (!this.apiKey) {
throw new Error('Qwen API key is not configured');
}
}
// 非流式调用
async chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
try {
const response = await axios.post(
this.baseURL,
{
model: this.modelName,
input: {
messages: messages,
},
parameters: {
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2000,
top_p: options?.topP ?? 0.9,
result_format: 'message',
},
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
timeout: 60000,
}
);
const output = response.data.output;
const usage = response.data.usage;
return {
content: output.choices[0].message.content,
model: this.modelName,
usage: {
promptTokens: usage.input_tokens,
completionTokens: usage.output_tokens,
totalTokens: usage.total_tokens || usage.input_tokens + usage.output_tokens,
},
finishReason: output.choices[0].finish_reason,
};
} catch (error: unknown) {
console.error('Qwen API Error:', error);
if (axios.isAxiosError(error)) {
throw new Error(
`Qwen API调用失败: ${error.response?.data?.message || error.message}`
);
}
throw error;
}
}
// 流式调用
async *chatStream(
messages: Message[],
options?: LLMOptions,
onChunk?: (chunk: StreamChunk) => void
): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await axios.post(
this.baseURL,
{
model: this.modelName,
input: {
messages: messages,
},
parameters: {
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2000,
top_p: options?.topP ?? 0.9,
result_format: 'message',
incremental_output: true,
},
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
'X-DashScope-SSE': 'enable',
},
responseType: 'stream',
timeout: 60000,
}
);
const stream = response.data;
let buffer = '';
for await (const chunk of stream) {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith(':')) {
continue;
}
if (trimmedLine.startsWith('data:')) {
try {
const jsonStr = trimmedLine.slice(5).trim();
const data = JSON.parse(jsonStr);
const output = data.output;
const choice = output.choices[0];
const content = choice.message?.content || '';
const streamChunk: StreamChunk = {
content: content,
done: choice.finish_reason === 'stop',
model: this.modelName,
};
if (choice.finish_reason === 'stop' && data.usage) {
streamChunk.usage = {
promptTokens: data.usage.input_tokens,
completionTokens: data.usage.output_tokens,
totalTokens: data.usage.total_tokens || data.usage.input_tokens + data.usage.output_tokens,
};
}
if (onChunk) {
onChunk(streamChunk);
}
yield streamChunk;
} catch (parseError) {
console.error('Failed to parse Qwen SSE data:', parseError);
}
}
}
}
} catch (error) {
console.error('Qwen Stream Error:', error);
if (axios.isAxiosError(error)) {
throw new Error(
`Qwen流式调用失败: ${error.response?.data?.message || error.message}`
);
}
throw error;
}
}
}

View File

@@ -0,0 +1,55 @@
// LLM适配器类型定义
export interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface LLMOptions {
temperature?: number;
maxTokens?: number;
topP?: number;
stream?: boolean;
}
export interface LLMResponse {
content: string;
model: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason?: string;
}
export interface StreamChunk {
content: string;
done: boolean;
model?: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
// LLM适配器接口
export interface ILLMAdapter {
// 模型名称
modelName: string;
// 非流式调用
chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse>;
// 流式调用
chatStream(
messages: Message[],
options?: LLMOptions,
onChunk?: (chunk: StreamChunk) => void
): AsyncGenerator<StreamChunk, void, unknown>;
}
// 支持的模型类型
export type ModelType = 'deepseek-v3' | 'qwen3-72b' | 'gemini-pro';

View File

@@ -1,36 +1,58 @@
import { config as dotenvConfig } from 'dotenv';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenvConfig();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 加载.env文件
dotenv.config({ path: path.join(__dirname, '../../.env') });
export const config = {
// 服务器配置
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3001', 10),
host: process.env.HOST || '0.0.0.0',
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
// 数据库配置
databaseUrl: process.env.DATABASE_URL || '',
databaseUrl: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/ai_clinical',
// Redis配置
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
// JWT配置
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
// 大模型API Keys
// LLM API配置
deepseekApiKey: process.env.DEEPSEEK_API_KEY || '',
qwenApiKey: process.env.QWEN_API_KEY || '',
geminiApiKey: process.env.GEMINI_API_KEY || '',
// Dify配置
difyApiUrl: process.env.DIFY_API_URL || 'http://localhost:5001',
difyApiKey: process.env.DIFY_API_KEY || '',
difyApiUrl: process.env.DIFY_API_URL || 'http://localhost/v1',
// 文件上传配置
uploadMaxSize: parseInt(process.env.UPLOAD_MAX_SIZE || '10485760', 10),
uploadMaxSize: parseInt(process.env.UPLOAD_MAX_SIZE || '10485760', 10), // 10MB
uploadDir: process.env.UPLOAD_DIR || './uploads',
// 日志配置
logLevel: process.env.LOG_LEVEL || 'info',
// CORS配置
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:5173',
};
// 验证必需的环境变量
export function validateEnv(): void {
const requiredVars = ['DATABASE_URL'];
const missing = requiredVars.filter(v => !process.env[v]);
if (missing.length > 0) {
console.warn(`Warning: Missing environment variables: ${missing.join(', ')}`);
}
// 检查LLM API Keys
if (!config.deepseekApiKey && !config.qwenApiKey) {
console.warn('Warning: No LLM API keys configured. At least one of DEEPSEEK_API_KEY or QWEN_API_KEY should be set.');
}
}

View File

@@ -0,0 +1,263 @@
import { FastifyRequest, FastifyReply } from 'fastify';
import { conversationService } from '../services/conversationService.js';
import { ModelType } from '../adapters/types.js';
export class ConversationController {
/**
* 创建新对话
*/
async createConversation(
request: FastifyRequest<{
Body: {
projectId: string;
agentId: string;
title?: string;
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1'; // 临时硬编码
const { projectId, agentId, title } = request.body;
const conversation = await conversationService.createConversation({
userId,
projectId,
agentId,
title,
});
reply.code(201).send({
success: true,
data: conversation,
});
} catch (error: any) {
reply.code(400).send({
success: false,
message: error.message || '创建对话失败',
});
}
}
/**
* 获取对话列表
*/
async getConversations(
request: FastifyRequest<{
Querystring: {
projectId?: string;
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1';
const projectId = request.query.projectId;
const conversations = await conversationService.getConversations(
userId,
projectId
);
reply.send({
success: true,
data: conversations,
});
} catch (error: any) {
reply.code(500).send({
success: false,
message: error.message || '获取对话列表失败',
});
}
}
/**
* 获取对话详情
*/
async getConversationById(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1';
const conversationId = request.params.id;
const conversation = await conversationService.getConversationById(
conversationId,
userId
);
reply.send({
success: true,
data: conversation,
});
} catch (error: any) {
reply.code(404).send({
success: false,
message: error.message || '对话不存在',
});
}
}
/**
* 发送消息(非流式)
*/
async sendMessage(
request: FastifyRequest<{
Body: {
conversationId: string;
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1';
const { conversationId, content, modelType, knowledgeBaseIds } =
request.body;
// 验证modelType
if (modelType !== 'deepseek-v3' && modelType !== 'qwen3-72b' && modelType !== 'gemini-pro') {
reply.code(400).send({
success: false,
message: `不支持的模型类型: ${modelType}`,
});
return;
}
const result = await conversationService.sendMessage(
{
conversationId,
content,
modelType,
knowledgeBaseIds,
},
userId
);
reply.send({
success: true,
data: result,
});
} catch (error: any) {
reply.code(400).send({
success: false,
message: error.message || '发送消息失败',
});
}
}
/**
* 发送消息流式输出SSE
*/
async sendMessageStream(
request: FastifyRequest<{
Body: {
conversationId: string;
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1';
const { conversationId, content, modelType, knowledgeBaseIds } =
request.body;
// 验证modelType
if (modelType !== 'deepseek-v3' && modelType !== 'qwen3-72b' && modelType !== 'gemini-pro') {
reply.code(400).send({
success: false,
message: `不支持的模型类型: ${modelType}`,
});
return;
}
// 设置SSE响应头
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
// 流式输出
for await (const chunk of conversationService.sendMessageStream(
{
conversationId,
content,
modelType,
knowledgeBaseIds,
},
userId
)) {
// 发送SSE数据
reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
// 发送结束标记
reply.raw.write('data: [DONE]\n\n');
reply.raw.end();
} catch (error: any) {
console.error('Stream error:', error);
reply.raw.write(
`data: ${JSON.stringify({
error: error.message || '发送消息失败',
})}\n\n`
);
reply.raw.end();
}
}
/**
* 删除对话
*/
async deleteConversation(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = '1';
const conversationId = request.params.id;
await conversationService.deleteConversation(conversationId, userId);
reply.send({
success: true,
message: '对话已删除',
});
} catch (error: any) {
reply.code(400).send({
success: false,
message: error.message || '删除对话失败',
});
}
}
}
export const conversationController = new ConversationController();

View File

@@ -1,9 +1,10 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { config } from './config/env.js';
import { config, validateEnv } from './config/env.js';
import { testDatabaseConnection, prisma } from './config/database.js';
import { projectRoutes } from './routes/projects.js';
import { agentRoutes } from './routes/agents.js';
import { conversationRoutes } from './routes/conversations.js';
const fastify = Fastify({
logger: {
@@ -59,9 +60,15 @@ await fastify.register(projectRoutes, { prefix: '/api/v1' });
// 注册智能体管理路由
await fastify.register(agentRoutes, { prefix: '/api/v1' });
// 注册对话管理路由
await fastify.register(conversationRoutes, { prefix: '/api/v1' });
// 启动服务器
const start = async () => {
try {
// 验证环境变量
validateEnv();
// 测试数据库连接
console.log('🔍 正在测试数据库连接...');
const dbConnected = await testDatabaseConnection();

View File

@@ -0,0 +1,35 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { conversationController } from '../controllers/conversationController.js';
export async function conversationRoutes(fastify: FastifyInstance) {
// 创建对话
fastify.post('/conversations', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.createConversation(request as any, reply);
});
// 获取对话列表
fastify.get('/conversations', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.getConversations(request as any, reply);
});
// 获取对话详情
fastify.get('/conversations/:id', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.getConversationById(request as any, reply);
});
// 发送消息(非流式)
fastify.post('/conversations/message', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.sendMessage(request as any, reply);
});
// 发送消息(流式输出)
fastify.post('/conversations/message/stream', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.sendMessageStream(request as any, reply);
});
// 删除对话
fastify.delete('/conversations/:id', async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.deleteConversation(request as any, reply);
});
}

View File

@@ -0,0 +1,384 @@
import { prisma } from '../config/database.js';
import { LLMFactory } from '../adapters/LLMFactory.js';
import { Message, ModelType, StreamChunk } from '../adapters/types.js';
import { agentService } from './agentService.js';
interface CreateConversationData {
userId: string;
projectId: string;
agentId: string;
title?: string;
}
interface SendMessageData {
conversationId: string;
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
}
export class ConversationService {
/**
* 创建新对话
*/
async createConversation(data: CreateConversationData) {
const { userId, projectId, agentId, title } = data;
// 验证智能体是否存在
const agent = agentService.getAgentById(agentId);
if (!agent) {
throw new Error('智能体不存在');
}
// 验证项目是否存在
const project = await prisma.project.findFirst({
where: {
id: projectId,
userId: userId,
deletedAt: null,
},
});
if (!project) {
throw new Error('项目不存在或无权访问');
}
// 创建对话
const conversation = await prisma.conversation.create({
data: {
userId,
projectId,
agentId,
title: title || `${agent.name}的对话`,
metadata: {
agentName: agent.name,
agentCategory: agent.category,
},
},
});
return conversation;
}
/**
* 获取对话列表
*/
async getConversations(userId: string, projectId?: string) {
const where: any = {
userId,
deletedAt: null,
};
if (projectId) {
where.projectId = projectId;
}
const conversations = await prisma.conversation.findMany({
where,
include: {
project: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
messages: true,
},
},
},
orderBy: {
updatedAt: 'desc',
},
});
return conversations;
}
/**
* 获取对话详情(包含消息)
*/
async getConversationById(conversationId: string, userId: string) {
const conversation = await prisma.conversation.findFirst({
where: {
id: conversationId,
userId,
deletedAt: null,
},
include: {
project: {
select: {
id: true,
name: true,
background: true,
researchType: true,
},
},
messages: {
orderBy: {
createdAt: 'asc',
},
},
},
});
if (!conversation) {
throw new Error('对话不存在或无权访问');
}
return conversation;
}
/**
* 组装上下文消息
*/
private async assembleContext(
conversationId: string,
agentId: string,
projectBackground: string,
userInput: string,
knowledgeBaseContext?: string
): Promise<Message[]> {
// 获取系统Prompt
const systemPrompt = agentService.getSystemPrompt(agentId);
// 获取历史消息最近10条
const historyMessages = await prisma.message.findMany({
where: {
conversationId,
},
orderBy: {
createdAt: 'desc',
},
take: 10,
});
// 反转顺序(最早的在前)
historyMessages.reverse();
// 渲染用户Prompt模板
const renderedUserPrompt = agentService.renderUserPrompt(agentId, {
projectBackground,
userInput,
knowledgeBaseContext,
});
// 组装消息数组
const messages: Message[] = [
{
role: 'system',
content: systemPrompt,
},
];
// 添加历史消息
for (const msg of historyMessages) {
messages.push({
role: msg.role as 'user' | 'assistant',
content: msg.content,
});
}
// 添加当前用户输入
messages.push({
role: 'user',
content: renderedUserPrompt,
});
return messages;
}
/**
* 发送消息(非流式)
*/
async sendMessage(data: SendMessageData, userId: string) {
const { conversationId, content, modelType, knowledgeBaseIds } = data;
// 获取对话信息
const conversation = await this.getConversationById(conversationId, userId);
// 获取知识库上下文(如果有@知识库)
let knowledgeBaseContext = '';
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
// TODO: 调用Dify RAG获取知识库上下文
knowledgeBaseContext = '相关文献内容...';
}
// 组装上下文
const messages = await this.assembleContext(
conversationId,
conversation.agentId,
conversation.project?.background || '',
content,
knowledgeBaseContext
);
// 获取LLM适配器
const adapter = LLMFactory.getAdapter(modelType);
// 获取智能体配置的模型参数
const agent = agentService.getAgentById(conversation.agentId);
const modelConfig = agent?.models?.[modelType];
// 调用LLM
const response = await adapter.chat(messages, {
temperature: modelConfig?.temperature,
maxTokens: modelConfig?.maxTokens,
topP: modelConfig?.topP,
});
// 保存用户消息
const userMessage = await prisma.message.create({
data: {
conversationId,
role: 'user',
content,
metadata: {
knowledgeBaseIds,
},
},
});
// 保存助手回复
const assistantMessage = await prisma.message.create({
data: {
conversationId,
role: 'assistant',
content: response.content,
model: response.model,
tokens: response.usage?.totalTokens,
metadata: {
usage: response.usage,
finishReason: response.finishReason,
},
},
});
// 更新对话的最后更新时间
await prisma.conversation.update({
where: { id: conversationId },
data: { updatedAt: new Date() },
});
return {
userMessage,
assistantMessage,
usage: response.usage,
};
}
/**
* 发送消息(流式)
*/
async *sendMessageStream(
data: SendMessageData,
userId: string
): AsyncGenerator<StreamChunk, void, unknown> {
const { conversationId, content, modelType, knowledgeBaseIds } = data;
// 获取对话信息
const conversation = await this.getConversationById(conversationId, userId);
// 获取知识库上下文(如果有@知识库)
let knowledgeBaseContext = '';
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
// TODO: 调用Dify RAG获取知识库上下文
knowledgeBaseContext = '相关文献内容...';
}
// 组装上下文
const messages = await this.assembleContext(
conversationId,
conversation.agentId,
conversation.project?.background || '',
content,
knowledgeBaseContext
);
// 获取LLM适配器
const adapter = LLMFactory.getAdapter(modelType);
// 获取智能体配置的模型参数
const agent = agentService.getAgentById(conversation.agentId);
const modelConfig = agent?.models?.[modelType];
// 保存用户消息
await prisma.message.create({
data: {
conversationId,
role: 'user',
content,
metadata: {
knowledgeBaseIds,
},
},
});
// 用于累积完整的回复内容
let fullContent = '';
let usage: any = null;
// 流式调用LLM
for await (const chunk of adapter.chatStream(messages, {
temperature: modelConfig?.temperature,
maxTokens: modelConfig?.maxTokens,
topP: modelConfig?.topP,
})) {
fullContent += chunk.content;
if (chunk.usage) {
usage = chunk.usage;
}
yield chunk;
}
// 流式输出完成后,保存助手回复
await prisma.message.create({
data: {
conversationId,
role: 'assistant',
content: fullContent,
model: modelType,
tokens: usage?.totalTokens,
metadata: {
usage,
},
},
});
// 更新对话的最后更新时间
await prisma.conversation.update({
where: { id: conversationId },
data: { updatedAt: new Date() },
});
}
/**
* 删除对话(软删除)
*/
async deleteConversation(conversationId: string, userId: string) {
const conversation = await prisma.conversation.findFirst({
where: {
id: conversationId,
userId,
deletedAt: null,
},
});
if (!conversation) {
throw new Error('对话不存在或无权访问');
}
await prisma.conversation.update({
where: { id: conversationId },
data: { deletedAt: new Date() },
});
return { success: true };
}
}
export const conversationService = new ConversationService();