feat(platform): Fix pg-boss queue conflict and add safety standards
Summary: - Fix pg-boss queue conflict (duplicate key violation on queue_pkey) - Add global error listener to prevent process crash - Reduce connection pool from 10 to 4 - Add graceful shutdown handling (SIGTERM/SIGINT) - Fix researchWorker recursive call bug in catch block - Make screeningWorker idempotent using upsert Security Standards (v1.1): - Prohibit recursive retry in Worker catch blocks - Prohibit payload bloat (only store fileKey/ID in job.data) - Require Worker idempotency (upsert + unique constraint) - Recommend task-specific expireInSeconds settings - Document graceful shutdown pattern New Features: - PKB signed URL endpoint for document preview/download - pg_bigm installation guide for Docker - Dockerfile.postgres-with-extensions for pgvector + pg_bigm Documentation: - Update Postgres-Only async task processing guide (v1.1) - Add troubleshooting SQL queries - Update safety checklist Tested: Local verification passed
This commit is contained in:
@@ -58,5 +58,6 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -288,5 +288,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
54
Dockerfile.postgres-with-extensions
Normal file
54
Dockerfile.postgres-with-extensions
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# PostgreSQL 15 with pgvector + pg_bigm extensions
|
||||||
|
# 用于 AI 临床研究平台的向量检索和中文关键词检索
|
||||||
|
#
|
||||||
|
# 扩展版本:
|
||||||
|
# - pgvector: 0.8.1 (向量相似度搜索)
|
||||||
|
# - pg_bigm: 1.2 (中日韩文本全文搜索)
|
||||||
|
#
|
||||||
|
# 构建命令:
|
||||||
|
# docker build -f Dockerfile.postgres-with-extensions -t ai-clinical-postgres:v1.1 .
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 构建镜像后,修改 docker-compose.yml:
|
||||||
|
# image: ai-clinical-postgres:v1.1
|
||||||
|
# 2. 重启服务:docker compose down && docker compose up -d
|
||||||
|
# 3. 启用扩展:
|
||||||
|
# docker exec -it ai-clinical-postgres psql -U postgres -d ai_clinical_research -c "CREATE EXTENSION IF NOT EXISTS pg_bigm;"
|
||||||
|
|
||||||
|
FROM pgvector/pgvector:pg15
|
||||||
|
|
||||||
|
# 使用阿里云镜像源加速(如果在国内)
|
||||||
|
# RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true
|
||||||
|
|
||||||
|
# 安装编译依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
postgresql-server-dev-15 \
|
||||||
|
wget \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 下载并编译 pg_bigm
|
||||||
|
# pg_bigm 是专门为中日韩(CJK)字符优化的全文搜索扩展
|
||||||
|
RUN cd /tmp \
|
||||||
|
&& wget -q https://github.com/pgbigm/pg_bigm/archive/refs/tags/v1.2-20200228.tar.gz \
|
||||||
|
&& tar -xzf v1.2-20200228.tar.gz \
|
||||||
|
&& cd pg_bigm-1.2-20200228 \
|
||||||
|
&& make USE_PGXS=1 \
|
||||||
|
&& make USE_PGXS=1 install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/pg_bigm* /tmp/v1.2-20200228.tar.gz
|
||||||
|
|
||||||
|
# 清理编译依赖
|
||||||
|
RUN apt-get purge -y --auto-remove \
|
||||||
|
build-essential \
|
||||||
|
postgresql-server-dev-15 \
|
||||||
|
wget \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 添加初始化脚本(自动创建扩展)
|
||||||
|
COPY docker-init-extensions.sql /docker-entrypoint-initdb.d/
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 5432
|
||||||
|
|
||||||
@@ -234,5 +234,6 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -163,5 +163,6 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,5 +64,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -324,5 +324,6 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -186,5 +186,6 @@ npm run dev
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,3 +59,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,3 +54,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,3 +86,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,3 +49,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,3 +90,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,3 +125,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,3 +96,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -82,3 +82,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -124,3 +124,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,3 +35,4 @@ ON CONFLICT (id) DO NOTHING;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,4 @@ ON CONFLICT (id) DO NOTHING;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,4 @@ OSS_INTERNAL=true # 🔴 生产必须用内网
|
|||||||
OSS_SIGNED_URL_EXPIRES=3600
|
OSS_SIGNED_URL_EXPIRES=3600
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,5 +83,6 @@ WHERE table_schema = 'dc_schema'
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,5 +121,6 @@ ORDER BY ordinal_position;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,5 +134,6 @@ runMigration()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,5 +68,6 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,5 +95,6 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,4 @@ USING gin (metadata jsonb_path_ops);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ USING gin (tags);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -135,5 +135,6 @@ Write-Host ""
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -245,5 +245,6 @@ function extractCodeBlocks(obj, blocks = []) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,3 +44,4 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,3 +118,4 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,5 +264,6 @@ checkDCTables();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -216,5 +216,6 @@ createAiHistoryTable()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,5 +203,6 @@ createToolCTable()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -200,5 +200,6 @@ createToolCTable()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -322,3 +322,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -129,3 +129,4 @@ main()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -347,5 +347,6 @@ runTests().catch(error => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,3 +95,4 @@ testAPI().catch(console.error);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,3 +125,4 @@ testDeepSearch().catch(console.error);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -312,5 +312,6 @@ verifySchemas()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -202,3 +202,4 @@ export const jwtService = new JWTService();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class PgBossQueue implements JobQueue {
|
|||||||
this.boss = new PgBoss({
|
this.boss = new PgBoss({
|
||||||
connectionString,
|
connectionString,
|
||||||
schema, // 使用platform_schema
|
schema, // 使用platform_schema
|
||||||
max: 10, // 最大连接数
|
max: 4, // 🛡️ 限制连接数,避免挤占 Prisma 连接配额(RDS 限制 100)
|
||||||
application_name: 'aiclinical-queue',
|
application_name: 'aiclinical-queue',
|
||||||
|
|
||||||
// 调度配置
|
// 调度配置
|
||||||
@@ -61,6 +61,17 @@ export class PgBossQueue implements JobQueue {
|
|||||||
maintenanceIntervalSeconds: 300, // 每5分钟运行维护任务
|
maintenanceIntervalSeconds: 300, // 每5分钟运行维护任务
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 🛡️ 全局错误监听:防止未捕获错误导致进程崩溃
|
||||||
|
this.boss.on('error', (err: any) => {
|
||||||
|
// 静默处理 duplicate key 错误(队列并发初始化时的正常现象)
|
||||||
|
if (err.code === '23505' && err.constraint === 'queue_pkey') {
|
||||||
|
console.log(`[PgBossQueue] ℹ️ Queue concurrency conflict auto-resolved: ${err.detail}`);
|
||||||
|
} else {
|
||||||
|
console.error('[PgBossQueue] ❌ Critical error:', err);
|
||||||
|
// 记录到日志但不崩溃进程
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log('[PgBossQueue] Initialized with schema:', schema)
|
console.log('[PgBossQueue] Initialized with schema:', schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,13 +203,22 @@ export class PgBossQueue implements JobQueue {
|
|||||||
console.log(`[PgBossQueue] 🔧 开始注册 Handler: ${type}`);
|
console.log(`[PgBossQueue] 🔧 开始注册 Handler: ${type}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// pg-boss 9.x 需要显式创建队列
|
// pg-boss 9.x 需要显式创建队列(幂等操作)
|
||||||
await this.boss.createQueue(type, {
|
try {
|
||||||
retryLimit: 3,
|
await this.boss.createQueue(type, {
|
||||||
retryDelay: 60,
|
retryLimit: 3,
|
||||||
expireInSeconds: 6 * 60 * 60 // 6小时
|
retryDelay: 60,
|
||||||
});
|
expireInSeconds: 6 * 60 * 60 // 6小时
|
||||||
console.log(`[PgBossQueue] ✅ Queue created: ${type}`);
|
});
|
||||||
|
console.log(`[PgBossQueue] ✅ Queue created: ${type}`);
|
||||||
|
} catch (createError: any) {
|
||||||
|
// 队列已存在时会报 duplicate key 错误,忽略
|
||||||
|
if (createError.code === '23505' || createError.message?.includes('already exists')) {
|
||||||
|
console.log(`[PgBossQueue] ℹ️ Queue already exists: ${type}`);
|
||||||
|
} else {
|
||||||
|
throw createError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.boss.work<Record<string, any>>(type, {
|
await this.boss.work<Record<string, any>>(type, {
|
||||||
batchSize: 1, // 每次处理1个任务
|
batchSize: 1, // 每次处理1个任务
|
||||||
|
|||||||
@@ -332,5 +332,6 @@ export function getBatchItems<T>(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,3 +85,4 @@ export interface VariableValidation {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -355,3 +355,4 @@ export default ChunkService;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,4 @@ export const DifyClient = DeprecatedDifyClient;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -206,3 +206,4 @@ export function createOpenAIStreamAdapter(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -212,3 +212,4 @@ export async function streamChat(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ export { THINKING_TAGS } from './types';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,3 +105,4 @@ export type SSEEventType =
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -280,3 +280,33 @@ const start = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🛡️ 优雅关闭处理(Graceful Shutdown)
|
||||||
|
// ============================================
|
||||||
|
const gracefulShutdown = async (signal: string) => {
|
||||||
|
console.log(`\n⚠️ 收到 ${signal} 信号,开始优雅关闭...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 停止接收新请求
|
||||||
|
await fastify.close();
|
||||||
|
console.log('✅ HTTP 服务已停止');
|
||||||
|
|
||||||
|
// 2. 停止队列(等待当前任务完成)
|
||||||
|
await jobQueue.stop();
|
||||||
|
console.log('✅ 任务队列已停止');
|
||||||
|
|
||||||
|
// 3. 关闭数据库连接
|
||||||
|
await prisma.$disconnect();
|
||||||
|
console.log('✅ 数据库连接已关闭');
|
||||||
|
|
||||||
|
console.log('👋 优雅关闭完成,再见!');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 优雅关闭失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
|||||||
@@ -91,3 +91,4 @@ export async function moduleRoutes(fastify: FastifyInstance) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,3 +121,4 @@ export interface PaginatedResponse<T> {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -168,3 +168,4 @@ export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -243,3 +243,4 @@ async function matchIntent(query: string): Promise<{
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,3 +97,4 @@ export async function uploadAttachment(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ export { aiaRoutes };
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -368,5 +368,6 @@ runTests().catch((error) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -309,5 +309,6 @@ runTest()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -347,5 +347,6 @@ Content-Type: application/json
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ async function processScreeningBatchWithCheckpoint(
|
|||||||
literature.publicationYear ? Number(literature.publicationYear) : undefined
|
literature.publicationYear ? Number(literature.publicationYear) : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
// 保存结果(使用 screeningService.ts 相同的映射方式)
|
// 保存结果(使用 upsert 保证幂等性,任务重试时覆盖而不是重复创建)
|
||||||
const dbResult = {
|
const dbResult = {
|
||||||
projectId,
|
projectId,
|
||||||
literatureId: literature.id,
|
literatureId: literature.id,
|
||||||
@@ -294,8 +294,16 @@ async function processScreeningBatchWithCheckpoint(
|
|||||||
finalDecision: screeningResult.finalDecision,
|
finalDecision: screeningResult.finalDecision,
|
||||||
};
|
};
|
||||||
|
|
||||||
await prisma.aslScreeningResult.create({
|
// 🛡️ 使用 upsert 实现幂等性(利用 unique_project_literature 约束)
|
||||||
data: dbResult,
|
await prisma.aslScreeningResult.upsert({
|
||||||
|
where: {
|
||||||
|
projectId_literatureId: {
|
||||||
|
projectId,
|
||||||
|
literatureId: literature.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: dbResult,
|
||||||
|
update: dbResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|||||||
@@ -67,12 +67,8 @@ export function registerResearchWorker() {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新任务状态为失败
|
// ❌ 已移除错误的 executeSearch 调用
|
||||||
try {
|
// 任务失败后 pg-boss 会自动重试(最多3次)
|
||||||
await researchService.executeSearch(taskId, query);
|
|
||||||
} catch {
|
|
||||||
// 忽略
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -283,5 +283,6 @@ export const conflictDetectionService = new ConflictDetectionService();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -233,5 +233,6 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -287,5 +287,6 @@ export const streamAIController = new StreamAIController();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,5 +196,6 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -130,5 +130,6 @@ checkTableStructure();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -117,5 +117,6 @@ checkProjectConfig().catch(console.error);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,5 +99,6 @@ main();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -556,5 +556,6 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -191,5 +191,6 @@ console.log('');
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -508,5 +508,6 @@ export const patientWechatService = new PatientWechatService();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -153,5 +153,6 @@ testDifyIntegration().catch(error => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -182,5 +182,6 @@ testIitDatabase()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -168,5 +168,6 @@ if (hasError) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -194,5 +194,6 @@ async function testUrlVerification() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -275,5 +275,6 @@ main().catch((error) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -159,5 +159,6 @@ Write-Host ""
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -252,5 +252,6 @@ export interface CachedProtocolRules {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import * as documentService from '../services/documentService.js';
|
import * as documentService from '../services/documentService.js';
|
||||||
import { storage } from '../../../common/storage/index.js';
|
import { storage } from '../../../common/storage/index.js';
|
||||||
|
import { OSSAdapter } from '../../../common/storage/OSSAdapter.js';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { logger } from '../../../common/logging/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户ID(从JWT Token中获取)
|
* 获取用户ID(从JWT Token中获取)
|
||||||
@@ -374,4 +376,93 @@ export async function getDocumentFullText(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档签名URL(用于前端预览/下载)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* 生成一个带有过期时间的签名URL,前端可以直接使用该URL:
|
||||||
|
* - 在浏览器中预览 PDF
|
||||||
|
* - 下载文件(会恢复原始文件名)
|
||||||
|
*/
|
||||||
|
export async function getDocumentSignedUrl(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
Querystring: {
|
||||||
|
/** 过期时间(秒),默认3600秒 */
|
||||||
|
expires?: string;
|
||||||
|
/** 是否作为附件下载(添加 Content-Disposition),默认 false */
|
||||||
|
download?: string;
|
||||||
|
};
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const expires = parseInt(request.query.expires || '3600', 10);
|
||||||
|
const download = request.query.download === 'true';
|
||||||
|
|
||||||
|
const userId = getUserId(request);
|
||||||
|
|
||||||
|
// 获取文档信息
|
||||||
|
const document = await documentService.getDocumentById(userId, id);
|
||||||
|
|
||||||
|
// 检查是否有存储路径
|
||||||
|
if (!document.storageKey) {
|
||||||
|
logger.warn('[PKB] 文档没有存储路径,可能是旧数据', { documentId: id });
|
||||||
|
return reply.status(404).send({
|
||||||
|
success: false,
|
||||||
|
message: '文档文件不可用,请重新上传',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名URL
|
||||||
|
let signedUrl: string;
|
||||||
|
|
||||||
|
// 检查存储适配器类型
|
||||||
|
if (storage instanceof OSSAdapter) {
|
||||||
|
// OSS: 使用带原始文件名的签名URL
|
||||||
|
signedUrl = download
|
||||||
|
? storage.getSignedUrl(document.storageKey, expires, document.filename)
|
||||||
|
: storage.getSignedUrl(document.storageKey, expires);
|
||||||
|
} else {
|
||||||
|
// 本地存储: 使用 getUrl
|
||||||
|
signedUrl = storage.getUrl(document.storageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[PKB] 生成签名URL', {
|
||||||
|
documentId: id,
|
||||||
|
filename: document.filename,
|
||||||
|
expires,
|
||||||
|
download
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
documentId: document.id,
|
||||||
|
filename: document.filename,
|
||||||
|
fileType: document.fileType,
|
||||||
|
url: signedUrl,
|
||||||
|
expiresIn: expires,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('[PKB] 获取签名URL失败', { error: error.message });
|
||||||
|
|
||||||
|
if (error.message.includes('not found') || error.message.includes('access denied')) {
|
||||||
|
return reply.status(404).send({
|
||||||
|
success: false,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.status(500).send({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Failed to get signed URL',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,5 +65,6 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export default async function knowledgeBaseRoutes(fastify: FastifyInstance) {
|
|||||||
// Phase 2: 获取文档全文
|
// Phase 2: 获取文档全文
|
||||||
fastify.get('/documents/:id/full-text', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocumentFullText);
|
fastify.get('/documents/:id/full-text', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocumentFullText);
|
||||||
|
|
||||||
|
// 获取文档签名URL(用于预览/下载)
|
||||||
|
// Query: ?expires=3600&download=true
|
||||||
|
fastify.get('/documents/:id/signed-url', { preHandler: [authenticate, requireModule('PKB')] }, documentController.getDocumentSignedUrl);
|
||||||
|
|
||||||
// 删除文档
|
// 删除文档
|
||||||
fastify.delete('/documents/:id', { preHandler: [authenticate, requireModule('PKB')] }, documentController.deleteDocument);
|
fastify.delete('/documents/:id', { preHandler: [authenticate, requireModule('PKB')] }, documentController.deleteDocument);
|
||||||
|
|
||||||
|
|||||||
@@ -143,5 +143,6 @@ Content-Type: application/json
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,5 +128,6 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,5 +42,6 @@ export * from './services/utils.js';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,5 +133,6 @@ export function validateAgentSelection(agents: string[]): void {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -433,5 +433,6 @@ SET session_replication_role = 'origin';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,3 +113,4 @@ testCrossLanguageSearch();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -175,3 +175,4 @@ testQueryRewrite();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,3 +121,4 @@ testRerank();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -135,5 +135,6 @@ WHERE key = 'verify_test';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -278,5 +278,6 @@ verifyDatabase()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
backend/src/types/global.d.ts
vendored
1
backend/src/types/global.d.ts
vendored
@@ -68,5 +68,6 @@ export {}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,5 +91,6 @@ Write-Host "✅ 完成!" -ForegroundColor Green
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -179,5 +179,6 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user