Major Changes: - Add StreamingService with OpenAI Compatible format - Upgrade Chat component V2 with Ant Design X integration - Implement AIA module with 12 intelligent agents - Update API routes to unified /api/v1 prefix - Update system documentation Backend (~1300 lines): - common/streaming: OpenAI Compatible adapter - modules/aia: 12 agents, conversation service, streaming integration - Update route versions (RVW, PKB to v1) Frontend (~3500 lines): - modules/aia: AgentHub + ChatWorkspace (100% prototype restoration) - shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook - Update API endpoints to v1 Documentation: - AIA module status guide - Universal capabilities catalog - System overview updates - All module documentation sync Tested: Stream response verified, authentication working Status: AIA V2.0 core completed (85%)
18 KiB
鈭穃<EFBFBD><EFBFBD>笔<EFBFBD><EFBFBD>𤏸<EFBFBD><EFBFBD>?
*<EFBFBD><EFBFBD>﹝<EFBFBD><EFBFBD>𧋦嚗? V1.1
*<EFBFBD>𥕦遣<EFBFBD>交<EFBFBD>嚗? 2025-11-16
<EFBFBD><EFBFBD><EFBFBD>擧凒<EFBFBD>堆<EFBFBD> 2025-12-13 <20><> Postgres-Only <20>嗆<EFBFBD>閫<EFBFBD><E996AB><EFBFBD>啣<EFBFBD>
*<EFBFBD><EFBFBD>鍂撖寡情嚗? <20><><EFBFBD>匧<EFBFBD><E58CA7>睲犖<E79DB2>? 撘箏<EFBFBD><EFBFBD>改<EFBFBD> <20>?敹<>◆<EFBFBD>萄<EFBFBD>
蝏湔擪<EFBFBD><EFBFBD><EFBFBD> <20>嗆<EFBFBD><E59786>a<EFBFBD>
<EFBFBD><EFBFBD> <20><>﹝霂湔<E99C82>
<EFBFBD>祆<EFBFBD>獢<EFBFBD><EFBFBD>銋劐<EFBFBD><EFBFBD>毺<EFBFBD><EFBFBD>臬<EFBFBD>嚗𠄎erverless SAE + RDS + OSS嚗劐<E59A97><E58A90>?*隞<><E99A9E>閫<EFBFBD><E996AB>**嚗峕<E59A97><E5B395>劐<EFBFBD><E58A90>⊥芋<E28AA5>梹<EFBFBD>ASL<53><4C>IA<49><41>KB蝑㚁<E89D91>敹<EFBFBD>◆<EFBFBD>萄<EFBFBD><E89084>?
<EFBFBD><EFBFBD>粉<EFBFBD>園𡢿嚗?0 <20><><EFBFBD>
**璉<><E79289>仿<EFBFBD><E4BBBF>?*嚗𡁏<E59A97>甈∩誨<E288A9><E8AAA8><EFBFBD>鈭文<E988AD>
<EFBFBD><EFBFBD> <20>詨<EFBFBD><E8A9A8>笔<EFBFBD>嚗𡁜<E59A97><F0A1819C>典像<E585B8>啗<EFBFBD><E59597>?
**潃?<3F>滩<EFBFBD><E6BBA9>鞟內嚗?025-11-16 <20>湔鰵嚗?*嚗𡁜像<F0A1819C>啣歇<E595A3>𣂷<EFBFBD>摰峕㟲<E5B395><E39FB2>抅蝖<E68A85>霈暹鴌<E69AB9>滚𦛚
**霂衣<E99C82><E8A1A3><EFBFBD>﹝**嚗靀撟喳蝱<E596B3>箇<EFBFBD>霈暹鴌閫<E9B48C><E996AB>](../09-<2D>嗆<EFBFBD>摰墧鴌/04-撟喳蝱<E596B3>箇<EFBFBD>霈暹鴌閫<E9B48C><E996AB>.md)
撟喳蝱撌脫<EFBFBD>靘𤤿<EFBFBD><EFBFBD>滚𦛚
銝𡁜𦛚璅∪<EFBFBD>嚗㇁SL/AIA/PKB/DC蝑㚁<E89D91>摨磰砲憭滨鍂隞乩<E99A9E>撟喳蝱<E596B3>賢<EFBFBD>嚗𣬚<E59A97>甇a<E79487>憭滚<E686AD><E6BB9A>堆<EFBFBD>
| <EFBFBD>滚𦛚 | 撖澆<EFBFBD><EFBFBD>孵<EFBFBD> | <EFBFBD>券<EFBFBD>? | <EFBFBD><EFBFBD>﹝ |
|---|---|---|---|
| 摮睃<EFBFBD><EFBFBD>滚𦛚 | import { storage } from '@/common/storage' |
<EFBFBD><EFBFBD>辣銝𠹺<EFBFBD>銝贝蝸 | <EFBFBD>?撟喳蝱蝥? |
| <EFBFBD>亙<EFBFBD>蝟餌<EFBFBD> | import { logger } from '@/common/logging' |
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>𡝗𠯫敹? | <EFBFBD>?撟喳蝱蝥? |
| 撘<EFBFBD>郊隞餃𦛚 | import { jobQueue } from '@/common/jobs' |
<EFBFBD>踵𧒄<EFBFBD>港遙<EFBFBD>? | <EFBFBD>?撟喳蝱蝥? |
| 蝻枏<EFBFBD><EFBFBD>滚𦛚 | import { cache } from '@/common/cache' |
<EFBFBD><EFBFBD><EFBFBD>撘讐<EFBFBD>摮? | <EFBFBD>?撟喳蝱蝥? |
| <EFBFBD><EFBFBD> <20>剔<EFBFBD>蝏凋<E89D8F> | import { CheckpointService } from '@/common/jobs' |
隞餃𦛚<EFBFBD>剔<EFBFBD>蝞∠<EFBFBD> | <EFBFBD>?撟喳蝱蝥改<E89DA5><E694B9>堆<EFBFBD> |
| *<EFBFBD>唳旿摨? | import { prisma } from '@/config/database' |
<EFBFBD>唳旿摨𤘪<EFBFBD>雿? | <EFBFBD>?撟喳蝱蝥? |
| LLM<EFBFBD>賢<EFBFBD> | import { LLMFactory } from '@/common/llm' |
LLM靚<EFBFBD>鍂 | <EFBFBD>?撟喳蝱蝥? |
蝷箔<EFBFBD>嚗𡁏迤蝖桐蝙<EFBFBD>典像<EFBFBD>唳<EFBFBD><EFBFBD>?
// <20>?甇<>&嚗𡁶凒<F0A181B6>亙紡<E4BA99>亙像<E4BA99>唳<EFBFBD><E594B3>?import { storage } from '@/common/storage'
import { logger } from '@/common/logging'
import { jobQueue } from '@/common/jobs'
import { prisma } from '@/config/database'
export class LiteratureService {
async uploadPDF(projectId: string, pdfBuffer: Buffer) {
// 1. 雿輻鍂撟喳蝱摮睃<E691AE><E79D83>滚𦛚
const key = `asl/projects/${projectId}/pdfs/${Date.now()}.pdf`
const url = await storage.upload(key, pdfBuffer)
// 2. 雿輻鍂撟喳蝱<E596B3>亙<EFBFBD>蝟餌<E89D9F>
logger.info('PDF uploaded', { projectId, url })
// 3. 雿輻鍂撟喳蝱<E596B3>唳旿摨? const literature = await prisma.aslLiterature.create({
data: { projectId, pdfUrl: url }
})
return literature
}
}
<EFBFBD>?<3F>躰秤嚗𡁻<E59A97>憭滚<E686AD><E6BB9A>啣像<E595A3>啗<EFBFBD><E59597>?
// <20>?<3F>躰秤嚗𡁜銁銝𡁜𦛚璅∪<E79285>銝剛䌊撌勗<E6928C><E58B97>啣<EFBFBD><E595A3>?// backend/src/modules/asl/storage/LocalStorage.ts <20>?銝滚<E98A9D>霂亙<E99C82><E4BA99>剁<EFBFBD>
export class LocalStorage {
async upload(file: Buffer) {
await fs.writeFile('./uploads/file.pdf', file) // <20>?<3F>滚<EFBFBD>摰䂿緵
}
}
// <20>?<3F>躰秤嚗𡁜銁銝𡁜𦛚璅∪<E79285>銝剛䌊撌勗<E6928C><E58B97>唳𠯫敹?// backend/src/modules/asl/logger/logger.ts <20>?銝滚<E98A9D>霂亙<E99C82><E4BA99>剁<EFBFBD>
export const logger = winston.createLogger({...}) // <20>?<3F>滚<EFBFBD>摰䂿緵
**<2A>笔<EFBFBD>**嚗?- <20>?<3F>滚<EFBFBD>隞<EFBFBD><E99A9E>嚗屸𠗕隞亦輕<E4BAA6>?- <20>?銝滚<E98A9D>璅∪<E79285>摰䂿緵銝滢<E98A9D><E6BBA2>?- <20>?<3F>䭾<EFBFBD>蝏煺<E89D8F><E785BA><EFBFBD>揢<EFBFBD>臬<EFBFBD>嚗<EFBFBD>𧋦<EFBFBD>?鈭𤑳垢嚗?- <20>?瘚芾晶撘<E699B6><E69298>烐𧒄<E78390>?
<EFBFBD>?<3F>刻<EFBFBD><E588BB>𡁏<EFBFBD>嚗㇄O嚗?
1. <20><>辣摮睃<E691AE> <20>?
// <20>?甇<>&嚗帋蝙<E5B88B>典<EFBFBD><E585B8>冽𡂝鞊∪<E99E8A>
import { storage } from '@/common/storage/StorageFactory'
export async function uploadFile(file: Buffer, filename: string) {
const key = `asl/pdfs/${Date.now()}-${filename}`
const url = await storage.upload(key, file)
return url
}
// <20>?甇<>&嚗鍃xcel <20>湔𦻖隞𤾸<E99A9E>摮䁅圾<E48185>?import * as xlsx from 'xlsx'
export async function importExcel(buffer: Buffer) {
const workbook = xlsx.read(buffer, { type: 'buffer' }) // <20><><EFBFBD>閫<EFBFBD><E996AB>
const data = xlsx.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
return data
}
<EFBFBD><EFBFBD>眏嚗?- 摰孵膥<E5ADB5>滚鍳銝滢<E98A9D>銝W仃<EFBCB7><E4BB83>辣
- <EFBFBD>砍𧑐撘<EFBFBD><EFBFBD>穃<EFBFBD><EFBFBD>煺漣<EFBFBD>臬<EFBFBD>隞<EFBFBD><EFBFBD>銝<EFBFBD><EFBFBD>?- <20>芸𢆡<E88AB8>寞旿<E5AF9E>臬<EFBFBD><E887AC>㗛<EFBFBD><E3979B><EFBFBD>揢摮睃<E691AE><E79D83>孵<EFBFBD>
2. <20>唳旿摨栞<E691A8><E6A09E>?<3F>?
// <20>?甇<>&嚗帋蝙<E5B88B>典<EFBFBD>撅<EFBFBD> Prisma Client
import { prisma } from '@/config/database'
export async function createProject(data: any) {
return await prisma.aslScreeningProject.create({ data })
}
// <20>?甇<>&嚗𡁏鸌<F0A1818F>𤩺<EFBFBD>雿靝蝙<E99D9D>其<EFBFBD><E585B6>?export async function importLiteratures(literatures: any[]) {
return await prisma.$transaction(async (tx) => {
return await tx.aslLiterature.createMany({ data: literatures })
})
}
<EFBFBD><EFBFBD>眏嚗?- <20>典<EFBFBD>摰硺<E691B0>憭滨鍂餈墧𦻖
- <EFBFBD>踹<EFBFBD>餈墧𦻖<EFBFBD>啗<EFBFBD>堒偷
- 鈭见𦛚靽肽<EFBFBD><EFBFBD>唳旿銝<EFBFBD><EFBFBD>湔<EFBFBD>?
3. <20>臬<EFBFBD><E887AC>㗛<EFBFBD><E3979B>滨蔭 <20>?
// <20>?甇<>&嚗𡁶<E59A97>銝<EFBFBD><E98A9D>滨蔭蝞∠<E89D9E>
// backend/src/config/env.ts
export const config = {
llm: {
apiKey: process.env.LLM_API_KEY!,
baseUrl: process.env.LLM_BASE_URL!,
},
oss: {
region: process.env.OSS_REGION!,
bucket: process.env.OSS_BUCKET!,
},
database: {
url: process.env.DATABASE_URL!,
}
}
// <20>?甇<>&嚗帋蝙<E5B88B>券<EFBFBD>蝵桀笆鞊?import { config } from '@/config/env'
const apiKey = config.llm.apiKey
<EFBFBD><EFBFBD>眏嚗?- <20>滨蔭<E6BBA8><E894AD>葉蝞∠<E89D9E>
- 蝐餃<EFBFBD>摰匧<EFBFBD>
- 靘蹂<EFBFBD><EFBFBD><EFBFBD>揢<EFBFBD>臬<EFBFBD>
4. <20>踵𧒄<E8B8B5>港遙<E6B8AF>∪<EFBFBD><E288AA>?<3F>?
// <20>?甇<>&嚗𡁜<E59A97>甇乩遙<E4B9A9>?+ 餈𥕦漲頧株砭
export async function startScreening(req, res) {
// 1. <20>𥕦遣隞餃𦛚霈啣<E99C88>
const task = await prisma.aslScreeningTask.create({
data: {
projectId: req.params.projectId,
status: 'pending',
totalItems: 100,
}
})
// 2. 蝡见朖餈𥪜<E9A488>隞餃𦛚ID
res.send({ success: true, taskId: task.id })
// 3. <20>𤾸蝱撘<E89DB1>郊<EFBFBD>扯<EFBFBD>嚗<EFBFBD><E59A97><EFBFBD>餃<EFBFBD>霂瑟<E99C82>嚗? processScreeningAsync(task.id).catch(err => {
console.error('蝑偦<E89D91>劐遙<E58A90>∪仃韐?', err)
})
}
// <20>滨垢頧株砭餈𥕦漲
export async function getTaskProgress(req, res) {
const task = await prisma.aslScreeningTask.findUnique({
where: { id: req.params.taskId }
})
res.send({
status: task.status,
progress: Math.round((task.completedItems / task.totalItems) * 100)
})
}
<EFBFBD><EFBFBD>眏嚗?- <20>踹<EFBFBD>霂瑟<E99C82>頞<EFBFBD>𧒄嚗𠄎AE暺䁅恕30蝘𡜐<E89D98>
- <EFBFBD>冽<EFBFBD>雿㯄<EFBFBD><EFBFBD>游末
- <EFBFBD>舀<EFBFBD><EFBFBD>寥<EFBFBD>隞餃𦛚
**<2A>?摰峕㟲摰噼殿<E599BC><E6AEBF><EFBFBD>?*嚗?霂西<E99C82> Postgres-Only撘<79>郊隞餃𦛚憭<F0A69B9A><E686AD><EFBFBD><EFBFBD><EFBFBD>嚗<EFBFBD>抅鈭𥟠C Tool C摰峕㟲摰噼殿嚗?
5. <20>亙<EFBFBD>颲枏枂 <20>?
// <20>?甇<>&嚗帋蝙<E5B88B>?logger 颲枏枂<E69E8F>?stdout
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info'
})
export async function uploadFile(req, res) {
logger.info({
userId: req.userId,
filename: req.file.filename
}, 'File uploaded')
// ... 銝𠹺<E98A9D><F0A0B9BA>餉<EFBFBD>
}
// <20>?甇<>&嚗𡁶<E59A97><F0A181B6><EFBFBD><EFBFBD><EFBFBD>亙<EFBFBD>
logger.error({
error: err.message,
stack: err.stack,
userId: req.userId
}, 'Upload failed')
<EFBFBD><EFBFBD>眏嚗?- SAE <20>芸𢆡<E88AB8><F0A286A1><EFBFBD> stdout <20>亙<EFBFBD>
- 蝏𤘪<EFBFBD><EFBFBD>碶噶鈭擧䰻霂W<EFBFBD><EFBFBD>?- <20><>葉<EFBFBD>亦<EFBFBD>嚗䔶<E59A97>隡帋腺憭?
6. <20>躰秤憭<E7A7A4><E686AD> <20>?
// <20>?甇<>&嚗𡁶<E59A97>銝<EFBFBD><E98A9D>躰秤憭<E7A7A4><E686AD>
export async function uploadPdf(req, res) {
try {
const file = await req.file()
const buffer = await file.toBuffer()
const url = await storage.upload(key, buffer)
res.send({ success: true, url })
} catch (error) {
logger.error({ error }, 'PDF upload failed')
res.status(500).send({
success: false,
error: {
code: 'UPLOAD_FAILED',
message: '<27><>辣銝𠹺<E98A9D>憭梯揖嚗諹窈<E8ABB9>滩<EFBFBD>'
}
})
}
}
<EFBFBD><EFBFBD>眏嚗?- <20>冽<EFBFBD><E586BD>见<EFBFBD><E8A781>见末<E8A781>躰秤靽⊥<E99DBD>
- <EFBFBD>亙<EFBFBD>霈啣<EFBFBD>霂衣<EFBFBD><EFBFBD>躰秤
- 銝齿𠂔<EFBFBD>脣<EFBFBD><EFBFBD>典<EFBFBD><EFBFBD>?
7. 銝湔𧒄<E6B994><F0A79284>辣憭<E8BEA3><E686AD> <20>?
// <20>?甇<>&嚗?tmp <20>桀<EFBFBD><E6A180>典<EFBFBD>蝡见朖<E8A781>𣳇膄
import fs from 'fs/promises'
import path from 'path'
export async function extractPdfText(ossKey: string): Promise<string> {
const tmpPath = path.join('/tmp', `${Date.now()}.pdf`)
try {
// 1. 隞?OSS 銝贝蝸<E8B49D>?/tmp
await storage.download(ossKey, tmpPath)
// 2. <20>𣂼<EFBFBD><F0A382BC><EFBFBD>𧋦
const text = await extractWithNougat(tmpPath)
return text
} finally {
// 3. 蝡见朖<E8A781>𣳇膄銝湔𧒄<E6B994><F0A79284>辣嚗<E8BEA3><E59A97>霈箸<E99C88><E7AEB8>笔仃韐伐<E99F90>
try {
await fs.unlink(tmpPath)
} catch (err) {
logger.warn({ tmpPath }, 'Failed to delete temp file')
}
}
}
<EFBFBD><EFBFBD>眏嚗?- /tmp 摰寥<E691B0><E5AFA5>厰<EFBFBD>嚗?12MB嚗?- 摰孵膥<E5ADB5>滚鍳隡𡁏<E99AA1>蝛?- <20>踹<EFBFBD>蝤<EFBFBD><E89DA4><EFBFBD>䭾說
<EFBFBD><EFBFBD> Postgres-Only <20>嗆<EFBFBD>閫<EFBFBD><E996AB>嚗?025-12-13 <20>啣<EFBFBD>嚗?
<EFBFBD>詨<EFBFBD><EFBFBD><EFBFBD>艙
**Platform-Only 璅∪<E79285>**嚗𡁏<E59A97><F0A1818F>劐遙<E58A90>∠恣<E288A0><E681A3>縑<EFBFBD>舐<EFBFBD>銝<EFBFBD>摮睃<E691AE><E79D83>?platform_schema.job.data嚗䔶<EFBFBD><EFBFBD>∟”<EFBFBD>芸<EFBFBD><EFBFBD>其<EFBFBD><EFBFBD>∩縑<EFBFBD>胯<EFBFBD>?
隞餃𦛚蝞∠<EFBFBD><EFBFBD><EFBFBD>迤蝖桀<EFBFBD>瘜?
<EFBFBD>?DO: 雿輻鍂 job.data 摮睃<E691AE>隞餃𦛚蝞∠<E89D9E>靽⊥<E99DBD>
// <20>?甇<>&嚗帋遙<E5B88B>⊥<EFBFBD><E28AA5><EFBFBD><EFBFBD><EFBFBD>剔<EFBFBD>靽⊥<E99DBD>摮睃<E691AE><E79D83>?job.data
import { jobQueue } from '@/common/jobs';
import { CheckpointService } from '@/common/jobs/CheckpointService';
// <20>券<EFBFBD><E588B8>遙<EFBFBD>⊥𧒄嚗<F0A79284><E59A97><EFBFBD>怠<EFBFBD><E680A0>港縑<E6B8AF>?await jobQueue.push('asl:screening:batch', {
// 銝𡁜𦛚靽⊥<E99DBD>
taskId: 'xxx',
projectId: 'yyy',
literatureIds: [...],
// <20>?隞餃𦛚<E9A483><F0A69B9A><EFBFBD>靽⊥<E99DBD>嚗<EFBFBD><E59A97><EFBFBD>典銁 job.data嚗? batchIndex: 3,
totalBatches: 20,
startIndex: 150,
endIndex: 200,
// <20>?餈𥕦漲餈質葵
processedCount: 0,
successCount: 0,
failedCount: 0,
});
// Worker 銝凋蝙<E5878B>?CheckpointService
const checkpointService = new CheckpointService(prisma);
// 靽嘥<E99DBD><E598A5>剔<EFBFBD><E58994>?job.data
await checkpointService.saveCheckpoint(job.id, {
currentBatchIndex: 5,
currentIndex: 250,
processedBatches: 5,
totalBatches: 20
});
// <20>㰘蝸<E3B098>剔<EFBFBD>隞?job.data
const checkpoint = await checkpointService.loadCheckpoint(job.id);
if (checkpoint) {
resumeFrom = checkpoint.currentIndex;
}
<EFBFBD>?DON'T: <20>其<EFBFBD><E585B6>∟”銝剖<E98A9D><E58996>其遙<E585B6>∠恣<E288A0><E681A3>縑<EFBFBD>?
// <20>?<3F>躰秤嚗𡁜銁銝𡁜𦛚銵函<E98AB5> Schema 銝剜溶<E5899C>牐遙<E78990>∠恣<E288A0><E681A3><EFBFBD>畾?model AslScreeningTask {
id String @id
projectId String
// <20>?銝滩<E98A9D>瘛餃<E7989B>餈嗘<E9A488>摮埈挾嚗? totalBatches Int // <20>?摨磰砲<E7A3B0>?job.data 銝? processedBatches Int // <20>?摨磰砲<E7A3B0>?job.data 銝? currentIndex Int // <20>?摨磰砲<E7A3B0>?job.data 銝? checkpointData Json? // <20>?摨磰砲<E7A3B0>?job.data 銝?}
// <20>?<3F>躰秤嚗朞䌊撌勗<E6928C><E58B97>唳鱏<E594B3>寞<EFBFBD><E5AF9E>?class MyCheckpointService {
async save(taskId: string) {
await prisma.aslScreeningTask.update({
where: { id: taskId },
data: { checkpointData: {...} } // <20>?銝滩<E98A9D>餈蹱甅<E8B9B1>𡄯<EFBFBD>
});
}
}
銝箔<EFBFBD>銋<EFBFBD><EFBFBD>撖對<EFBFBD>
- <EFBFBD>?瘥譍葵璅∪<E79285><E288AA>質<EFBFBD>瘛餃<E7989B><E9A483>詨<EFBFBD><E8A9A8><EFBFBD><EFBFBD>畾蛛<E795BE>隞<EFBFBD><E99A9E><EFBFBD>滚<EFBFBD>嚗?- <20>?餈嘥<E9A488> DRY <20>笔<EFBFBD>
- <EFBFBD>?餈嘥<E9A488> 3 撅<>沲<EFBFBD><E6B2B2><EFBFBD><EFBFBD>?- <20>?蝏湔擪<E6B994>圈𠗕嚗<F0A09795>耨<EFBFBD>寥<EFBFBD>餉<EFBFBD><E9A489><EFBFBD>閬<EFBFBD>㺿憭𡁜<E686AD>嚗?
<EFBFBD>箄<EFBFBD><EFBFBD><EFBFBD><EFBFBD>澆ế<EFBFBD>剔<EFBFBD>閫<EFBFBD><EFBFBD>
<EFBFBD>?DO: 摰䂿緵<E482BF>箄<EFBFBD><E7AE84>峕芋撘誩<E69298><E8AAA9>?
const QUEUE_THRESHOLD = 50; // <20>刻<EFBFBD><E588BB><EFBFBD><EFBFBD>?
export async function startTask(items: any[]) {
const useQueue = items.length >= QUEUE_THRESHOLD;
if (useQueue) {
// <20>笔<EFBFBD>璅∪<E79285>嚗𡁜之隞餃𦛚嚗<F0A69B9A>竉50<35>∴<EFBFBD>
const chunks = splitIntoChunks(items, 50);
for (const chunk of chunks) {
await jobQueue.push('task:batch', {...});
}
} else {
// <20>湔𦻖璅∪<E79285>嚗𡁜<E59A97>隞餃𦛚嚗?50<35>∴<EFBFBD>
processDirectly(items); // 敹恍<E695B9>笔<EFBFBD>摨? }
}
*銝箔<EFBFBD>銋<EFBFBD><EFBFBD><EFBFBD>瑕<EFBFBD>嚗?
- <EFBFBD>?撠譍遙<E8AD8D>∪翰<E288AA>笔<EFBFBD>摨䈑<E691A8><E48891>𣳇<EFBFBD><F0A3B387>堒辣餈<E8BEA3><E9A488>
- <EFBFBD>?憭找遙<E689BE>⊿<EFBFBD><E28ABF>舫<EFBFBD>嚗<EFBFBD>𣈲<EFBFBD><F0A388B2>鱏<EFBFBD>寧賒隡𩤃<E99AA1>
- <EFBFBD>?<3F>扯<EFBFBD>銝𤾸虾<F0A4BEB8>䭾<EFBFBD>批像銵?
<EFBFBD>?DON'T: <20><><EFBFBD>劐遙<E58A90>⊿<EFBFBD>韏圈<E99F8F><E59C88>?
// <20>?<3F>躰秤嚗𡁜朖雿?<3F>∟扇敶蓥<E695B6>雿輻鍂<E8BCBB>笔<EFBFBD>
export async function startTask(items: any[]) {
// <20>㰘捏憭𡁜<E686AD><F0A1819C>唳旿嚗屸<E59A97><E5B1B8>券<EFBFBD><E588B8><EFBFBD><EFBFBD>笔<EFBFBD>
await jobQueue.push('task:batch', items); // <20>?撠譍遙<E8AD8D>∩<EFBFBD><E288A9>匧辣餈?}
銝箔<EFBFBD>銋<EFBFBD><EFBFBD>撖對<EFBFBD>
- <EFBFBD>?撠譍遙<E8AD8D>∪<EFBFBD>摨娍<E691A8>嚗<EFBFBD><E59A97><EFBFBD>埈<EFBFBD>頧株砭<E6A0AA>湧<EFBFBD>嚗?- <20>?瘚芾晶<E88ABE>笔<EFBFBD>韏<EFBFBD><E99F8F>
- <EFBFBD>?<3F>冽<EFBFBD>雿㯄<E99BBF>撌?
<EFBFBD><EFBFBD><EFBFBD>潭綫<EFBFBD>𣂼<EFBFBD>?
| 隞餃𦛚蝐餃<EFBFBD> | <EFBFBD>刻<EFBFBD><EFBFBD><EFBFBD><EFBFBD>? | <EFBFBD><EFBFBD>眏 |
|---|---|---|
| <EFBFBD><EFBFBD>讃蝑偦<EFBFBD>? | 50蝭? | <EFBFBD>閧<EFBFBD>~7蝘𡜐<E89D98>50蝭㻬5<E3BBAC><35><EFBFBD> |
| <EFBFBD>唳旿<EFBFBD>𣂼<EFBFBD> | 50<EFBFBD>? | <EFBFBD>閙辺~5-10蝘𡜐<E89D98>50<35>﹚5<EFB99A><35><EFBFBD> |
| 蝏蠘恣璅∪<EFBFBD> | 30銝? | <EFBFBD>蓥葵~10蝘𡜐<E89D98>30銝泠5<E6B3A0><35><EFBFBD> |
| 暺䁅恕 | 50<EFBFBD>? | <EFBFBD>𡁶鍂<EFBFBD>刻<EFBFBD><EFBFBD>? |
<EFBFBD>?蝳<>迫<EFBFBD>𡁏<EFBFBD>嚗㇄ON'T嚗?
1. <20>砍𧑐<E7A08D><F0A79190>辣摮睃<E691AE> <20>?
// <20>?蝳<>迫嚗𡁏𧋦<F0A1818F>唳<EFBFBD>隞嗥頂蝏笔<E89D8F><E7AC94>?import fs from 'fs'
export async function uploadFile(req, res) {
const file = await req.file()
const buffer = await file.toBuffer()
// <20>?<3F>躰秤嚗𡁜捆<F0A1819C>券<EFBFBD><E588B8>臭腺憭? fs.writeFileSync('./uploads/file.pdf', buffer)
res.send({ url: '/uploads/file.pdf' })
}
// <20>?蝳<>迫嚗帋<E59A97>韏𡝗𧋦<F0A19D97>啗楝敺?const uploadDir = '/var/app/uploads'
const filePath = path.join(uploadDir, filename)
**<2A>桅<EFBFBD>**嚗?- 摰孵膥<E5ADB5>滚鍳<E6BB9A>𡝗<EFBFBD>摰孵<E691B0><E5ADB5><EFBFBD>辣銝W仃
- 憭𡁜<EFBFBD>靘钅𡢿<EFBFBD>䭾<EFBFBD><EFBFBD>曹澈<EFBFBD><EFBFBD>辣
- 蝤<EFBFBD><EFBFBD>蝛粹𡢿<EFBFBD>厰<EFBFBD>
**甇<>&<EFBFBD>𡁏<EFBFBD>**嚗帋蝙<E5B88B>?storage.upload() 銝𠹺<E98A9D><F0A0B9BA>?OSS
2. <20><><EFBFBD>蝻枏<E89DBB> <20>?
// <20>?蝳<>迫嚗𡁜<E59A97>摮条<E691AE>摮矋<E691AE>憭𡁜<E686AD>靘衤<E99D98><E8A1A4>曹澈嚗?const cache = new Map<string, any>()
export async function getProject(id: string) {
// <20>?<3F>躰秤嚗𡁏<E59A97>摰孵<E691B0><E5ADB5>嗡<EFBFBD>摰硺<E691B0>霂颱<E99C82><E9A2B1>啁<EFBFBD>摮? if (cache.has(id)) {
return cache.get(id)
}
const project = await prisma.aslScreeningProject.findUnique({ where: { id } })
cache.set(id, project)
return project
}
// <20>?蝳<>迫嚗𡁜<E59A97>撅<EFBFBD><E69285>㗛<EFBFBD>摮睃<E691AE><E79D83>嗆<EFBFBD>?let taskStatus = {} // 憭𡁜<E686AD>靘衤<E99D98><E8A1A4>峕郊
**<2A>桅<EFBFBD>**嚗?- 憭𡁜<E686AD>靘钅𡢿<E99285>唳旿銝滚<E98A9D>甇?- <20>拙捆<E68B99>𡒊<EFBFBD>摮睃仃<E79D83>?- <20><><EFBFBD><EFBFBD>删鍂銝滚虾<E6BB9A>? **甇<>&<EFBFBD>𡁏<EFBFBD>**嚗帋蝙<E5B88B>?Redis <20>𡝗㺭<F0A19D97>桀<EFBFBD>
3. 蝖祉<E89D96><E7A589><EFBFBD><EFBFBD>蝵?<3F>?
// <20>?蝳<>迫嚗𡁶′蝻𣇉<E89DBB>
const LLM_API_KEY = 'sk-xxx'
const DB_HOST = '192.168.1.100'
const OSS_BUCKET = 'my-bucket'
// <20>?蝳<>迫嚗𡁜<E59A97>甇餌垢<E9A48C>?app.listen(3001)
// <20>?蝳<>迫嚗𡁜<E59A97>甇餃<E79487><E9A483>?const baseUrl = 'https://api.example.com'
**<2A>桅<EFBFBD>**嚗?- <20>䭾<EFBFBD><E4ADBE><EFBFBD>揢<EFBFBD>臬<EFBFBD>
- 摰匧<EFBFBD>憌𡡞埯嚗<EFBFBD><EFBFBD><EFBFBD>交<EFBFBD><EFBFBD>莎<EFBFBD>
- <EFBFBD>函蔡<EFBFBD>圈𠗕
**甇<>&<EFBFBD>𡁏<EFBFBD>**嚗?```typescript // <20>?雿輻鍂<E8BCBB>臬<EFBFBD><E887AC>㗛<EFBFBD> const apiKey = process.env.LLM_API_KEY const port = process.env.PORT || 3001 app.listen(port)
---
### 4. <20>峕郊<E5B395>蹂遙<E8B982>?<3F>?
```typescript
// <20>?蝳<>迫嚗𡁜<E59A97>甇亙<E79487><E4BA99><EFBFBD>鵭隞餃𦛚
export async function screenLiteratures(req, res) {
const literatures = await prisma.aslLiterature.findMany({...})
// <20>?<3F>躰秤嚗?00蝭<30>虾<EFBFBD>質<EFBFBD>餈?0蝘? for (const lit of literatures) {
await llmScreening(lit) // 瘥讐<E798A5>2-3蝘? }
res.send({ success: true }) // <20>航<EFBFBD>撌脩<E6928C>頞<EFBFBD>𧒄
}
// <20>?蝳<>迫嚗𡁏瓷<F0A1818F>㕑<EFBFBD><E39591>嗡<EFBFBD><E597A1>?const result = await axios.get(url) // <20>航<EFBFBD>瘞訾<E7989E>蝑匧<E89D91>
**<2A>桅<EFBFBD>**嚗?- SAE 霂瑟<E99C82>頞<EFBFBD>𧒄 30 蝘?- <20>滨垢蝑匧<E89D91><E58CA7>園𡢿餈<F0A1A2BF>鵭
- <EFBFBD>䭾<EFBFBD><EFBFBD>曄內餈𥕦漲
**甇<>&<EFBFBD>𡁏<EFBFBD>**嚗𡁜<E59A97>甇乩遙<E4B9A9>?+ 餈𥕦漲頧株砭嚗<E7A0AD><E59A97> DO 蝚?<3F>∴<EFBFBD>
5. <20>砍𧑐<E7A08D>亙<EFBFBD><E4BA99><EFBFBD>辣 <20>?
// <20>?蝳<>迫嚗𡁜<E59A97><F0A1819C>交𧋦<E4BAA4>唳<EFBFBD>隞?import fs from 'fs'
export function logError(error: Error) {
// <20>?<3F>躰秤嚗𡁜捆<F0A1819C>券<EFBFBD><E588B8>臭腺憭梧<E686AD><E6A2A7>䭾<EFBFBD><E4ADBE><EFBFBD>葉<EFBFBD>亦<EFBFBD>
fs.appendFileSync('/var/log/app.log', error.message + '\n')
}
// <20>?蝳<>迫嚗帋蝙<E5B88B>?console.log <20>䔶<EFBFBD><E494B6>?logger
console.log('User logged in') // <20>删<EFBFBD><E588A0><EFBFBD><EFBFBD>嚗屸𠗕隞交䰻霂?```
**<EFBFBD>桅<EFBFBD>**嚗?- 摰孵膥<EFBFBD>滚鍳<EFBFBD>亙<EFBFBD>銝W仃
- 憭𡁜<EFBFBD>靘𧢲𠯫敹堒<EFBFBD><EFBFBD>?- <EFBFBD>䭾<EFBFBD><EFBFBD><EFBFBD>葉<EFBFBD><EFBFBD><EFBFBD>
**甇<EFBFBD>&<EFBFBD>𡁏<EFBFBD>**嚗?```typescript
// <20>?颲枏枂<E69E8F>?stdout嚗䔶蝙<E494B6>?logger
logger.info({ userId, action: 'login' }, 'User logged in')
6. <20>啣遣<E595A3>唳旿摨栞<E691A8><E6A09E>?<3F>?
// <20>?蝳<>迫嚗𡁏<E59A97>甈∟窈瘙<E7AA88>鰵撱箄<E692B1><E7AE84>?import { PrismaClient } from '@prisma/client'
export async function getProjects(req, res) {
// <20>?<3F>躰秤嚗𡁏<E59A97>甈⊥鰵撱綽<E692B1>餈墧𦻖<E5A2A7>唳𠂔憓? const prisma = new PrismaClient()
const projects = await prisma.aslScreeningProject.findMany()
await prisma.$disconnect()
res.send(projects)
}
// <20>?蝳<>迫嚗𡁶凒<F0A181B6>乩蝙<E4B9A9>?pg <20>硋<EFBFBD>隞㚚店<E39A9A>?import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
**<2A>桅<EFBFBD>**嚗?- 餈墧𦻖<E5A2A7>啣翰<E595A3>蠘<EFBFBD>堒偷嚗㇌DS<44>𣂼<EFBFBD> 400 餈墧𦻖嚗?- <20>扯<EFBFBD>雿𦒘<E99BBF>嚗<EFBFBD><E59A97><EFBFBD>亙遣蝡贝<E89DA1>埈𧒄嚗?- 韏<><E99F8F>瘚芾晶
**甇<>&<EFBFBD>𡁏<EFBFBD>**嚗?```typescript // <20>?雿輻鍂<E8BCBB>典<EFBFBD> Prisma Client import { prisma } from '@/config/database' const projects = await prisma.aslScreeningProject.findMany()
---
### 7. 敹賜裦<E8B39C>躰秤 <20>?
```typescript
// <20>?蝳<>迫嚗𡁶征<F0A181B6>?catch
try {
await storage.upload(key, buffer)
} catch (error) {
// <20>?<3F>躰秤鋡怠<E98BA1><E680A0>㚁<EFBFBD><E39A81>䭾<EFBFBD><E4ADBE>埝䰻
}
// <20>?蝳<>迫嚗帋<E59A97>憭<EFBFBD><E686AD> Promise rejection
processAsync(taskId) // 瘝⊥<E7989D> .catch()
// <20>?蝳<>迫嚗朞<E59A97><E69C9E>墧芋蝟𢠃<E89D9F>霂?catch (error) {
res.status(500).send({ error: 'Something went wrong' })
// <20>冽<EFBFBD>銝滨䰻<E6BBA8>㮖<EFBFBD>銋<EFBFBD><E98A8B>鈭<EFBFBD><E988AD>憒<EFBFBD><E68692>閫<EFBFBD><E996AB>
}
**<2A>桅<EFBFBD>**嚗?- <20>躰秤<E8BAB0>䭾<EFBFBD>餈質葵
- <EFBFBD>冽<EFBFBD>雿㯄<EFBFBD>撌?- <20>埝䰻<E59F9D>圈𠗕
**甇<>&<EFBFBD>𡁏<EFBFBD>**嚗?```typescript // <20>?霈啣<E99C88><E595A3>亙<EFBFBD> + <20>见末<E8A781>躰秤靽⊥<E99DBD> try { await storage.upload(key, buffer) } catch (error) { logger.error({ error, key }, 'Upload failed') res.status(500).send({ success: false, error: { code: 'UPLOAD_FAILED', message: '<27><>辣銝𠹺<E98A9D>憭梯揖嚗諹窈璉<E7AA88><E79289>亦<EFBFBD>蝏𨅯<E89D8F><F0A885AF>滩<EFBFBD>' } }) }
---
## <20><> 隞<><E99A9E>摰⊥䰻璉<E4B0BB><E79289>交<EFBFBD><E4BAA4>?
<0A>?*<2A>𣂷漱隞<E6BCB1><E99A9E><EFBFBD>?*嚗諹窈<E8ABB9>鞾★璉<E29885><E79289>伐<EFBFBD>
### <20><>辣摮睃<E691AE>
- [ ] <20>臬炏雿輻鍂 `storage.upload()` <20>屸<EFBFBD> `fs.writeFile()`嚗?- [ ] Excel <20>臬炏隞𤾸<E99A9E>摮䁅圾<E48185>琜<EFBFBD><E7909C>屸<EFBFBD>靽嘥<E99DBD><E598A5>唳𧋦<E594B3>堆<EFBFBD>
- [ ] PDF <20>𣂼<EFBFBD><F0A382BC>擧糓<E693A7>衣<EFBFBD><E8A1A3>喳<EFBFBD><E596B3>支葩<E694AF>嗆<EFBFBD>隞塚<E99A9E>
### <20>唳旿摨?- [ ] <20>臬炏雿輻鍂<E8BCBB>典<EFBFBD> `prisma` 摰硺<E691B0>嚗?- [ ] <20>臬炏<E887AC>踹<EFBFBD><E8B8B9>典儐<E585B8>臭葉<E887AD>扯<EFBFBD><E689AF>閙辺<E99699>亥砭嚗<E7A0AD><E59A97>摨磰砲<E7A3B0>寥<EFBFBD><E5AFA5>滢<EFBFBD>嚗?- [ ] <20>寥<EFBFBD><E5AFA5>滢<EFBFBD><E6BBA2>臬炏雿輻鍂鈭见𦛚嚗?
### <20>滨蔭蝞∠<E89D9E>
- [ ] <20>臬炏<E887AC><E7828F><EFBFBD>厰<EFBFBD>蝵桅<E89DB5>隞?`process.env` 霂餃<E99C82>嚗?- [ ] <20>臬炏瘝⊥<E7989D>蝖祉<E89D96><E7A589><EFBFBD><EFBFBD> IP<49><50><EFBFBD><EFBFBD>溻<EFBFBD><E6BABB><EFBFBD><EFBFBD>伐<EFBFBD>
- [ ] `.env.example` <20>臬炏撌脫凒<E884AB>堆<EFBFBD>
### <20>踵𧒄<E8B8B5>港遙<E6B8AF>?- [ ] 頞<><E9A09E> 10 蝘垍<E89D98>隞餃𦛚<E9A483>臬炏<E887AC>嫣蛹撘<E89BB9>郊嚗?- [ ] <20>臬炏<E887AC>𣂷<EFBFBD>鈭<EFBFBD><E988AD>摨行䰻霂X𦻖<EFBCB8><F0A6BB96><EFBFBD>
- [ ] <20>滨垢<E6BBA8>臬炏<E887AC>㕑蔭霂X<E99C82> WebSocket <20>瑕<EFBFBD>餈𥕦漲嚗?
### <20>亙<EFBFBD>
- [ ] <20>臬炏雿輻鍂 `logger` <20>屸<EFBFBD> `console.log`嚗?- [ ] <20>亙<EFBFBD><E4BA99>臬炏蝏𤘪<E89D8F><F0A498AA>吔<EFBFBD>JSON<4F>澆<EFBFBD>嚗㚁<E59A97>
- [ ] <20>臬炏霈啣<E99C88>鈭<EFBFBD><E988AD><EFBFBD>格<EFBFBD>雿頣<E99BBF>userId<49><64>ction嚗㚁<E59A97>
### <20>躰秤憭<E7A7A4><E686AD>
- [ ] <20><><EFBFBD>?async <20>賣㺭<E8B3A3>臬炏<E887AC>?try-catch嚗?- [ ] <20>臬炏霈啣<E99C88>鈭<EFBFBD>祕蝏<E7A595><E89D8F>霂舀𠯫敹梹<E695B9>
- [ ] <20>臬炏餈𥪜<E9A488>鈭<EFBFBD><E988AD>憟賜<E6869F><E8B39C>躰秤靽⊥<E99DBD>嚗?
### 銝湔𧒄<E6B994><F0A79284>辣
- [ ] `/tmp` <20>桀<EFBFBD>雿輻鍂<E8BCBB>擧糓<E693A7>衣<EFBFBD><E8A1A3>喳<EFBFBD><E596B3>歹<EFBFBD>
- [ ] <20>臬炏<E887AC>?`finally` <20>𦯀葉皜<E89189><E79A9C>嚗?- [ ] <20>臬炏<E887AC>踹<EFBFBD><E8B8B9>踵<EFBFBD>靘肽<E99D98> `/tmp`嚗?
---
## <20>㴓 敹恍<E695B9>蠘䌊璉<E48C8A>嚗?<3F><><EFBFBD>嚗?
**餈鞱<E9A488>隞乩<E99A9E><E4B9A9>賭誘嚗峕<E59A97><E5B395>乩誨<E4B9A9><E8AAA8>葉<EFBFBD>臬炏<E887AC>㕑<EFBFBD>閫?*嚗?
```bash
# 璉<><E79289>交糓<E4BAA4>行<EFBFBD><E8A18C>砍𧑐<E7A08D><F0A79190>辣摮睃<E691AE>
grep -r "fs.writeFile\|fs.appendFile" backend/src/modules/
# 璉<><E79289>交糓<E4BAA4>行<EFBFBD>蝖祉<E89D96><E7A589><EFBFBD><EFBFBD>蝵?grep -r "sk-\|http://\|192.168" backend/src/modules/
# 璉<><E79289>交糓<E4BAA4>行<EFBFBD><E8A18C>啣遣 Prisma 餈墧𦻖
grep -r "new PrismaClient" backend/src/modules/
# 璉<><E79289>交糓<E4BAA4>行<EFBFBD> console.log
grep -r "console.log" backend/src/modules/
**憸<><E686B8>蝏𤘪<E89D8F>**嚗𡁏<E59A97><F0A1818F>㗇<EFBFBD><E39787>亙<EFBFBD>霂亥<E99C82><E4BAA5>?*0 銝芸龪<E88AB8>?
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>獢?
- [鈭穃<E988AD><E7A983>罸<EFBFBD>蝵脫沲<E884AB><E6B2B2><EFBFBD><EFBFBD>㻩(../09-<2D>嗆<EFBFBD>摰墧鴌/03-鈭穃<E988AD><E7A983>罸<EFBFBD>蝵脫沲<E884AB><E6B2B2><EFBFBD><EFBFBD>?md) - <20><>鉄摰峕㟲隞<E39FB2><E99A9E>蝷箔<E89DB7>
- <EFBFBD>滚<EFBFBD>蝡舀芋<EFBFBD>堒<EFBFBD><EFBFBD>嗆<EFBFBD>霈曇恣-V2 - <20>嗆<EFBFBD><E59786>餌熔
- [<5B>唳旿摨栞挽霈∟<E99C88><E2889F><EFBFBD>(./01-<2D>唳旿摨栞挽霈∟<E99C88><E2889F>?md)
- API霈曇恣閫<EFBFBD><EFBFBD>
- 隞<EFBFBD><EFBFBD>閫<EFBFBD><EFBFBD>
- Git<EFBFBD>𣂷漱閫<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20>湔鰵<E6B994>亙<EFBFBD>
| <EFBFBD>交<EFBFBD> | <EFBFBD><EFBFBD>𧋦 | <EFBFBD>䀹凒<EFBFBD><EFBFBD>捆 | 蝏湔擪<EFBFBD>? |
|---|---|---|---|
| 2025-11-16 | V1.0 | <EFBFBD>𥕦遣<EFBFBD><EFBFBD>﹝嚗<EFBFBD><EFBFBD>銋劐<EFBFBD><EFBFBD>毺<EFBFBD>撘<EFBFBD><EFBFBD>𤏸<EFBFBD><EFBFBD>? | <EFBFBD>嗆<EFBFBD><EFBFBD>a<EFBFBD> |
<EFBFBD><EFBFBD>﹝蝏湔擪<EFBFBD><EFBFBD><EFBFBD> <20>嗆<EFBFBD><E59786>a<EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>擧凒<EFBFBD>堆<EFBFBD> 2025-11-16
<EFBFBD><EFBFBD>﹝<EFBFBD>嗆<EFBFBD><EFBFBD><EFBFBD> <20>?撌脣<E6928C><E884A3>?
*撘箏<EFBFBD><EFBFBD>扯<EFBFBD>嚗? <20>?<3F><><EFBFBD>劐誨<E58A90><E8AAA8><EFBFBD>鈭文<E988AD>敹<EFBFBD>◆璉<E29786><E79289>?