返回 oh-my-ppt
source-documents.ts
根目录 / src / main / generation / source-documents.ts
1 import fs from 'fs'
2 import path from 'path'
3 import type { GenerationContext } from './context'
4 import type { GenerateMode } from './types'
5
6 export async function resolveSourceDocuments(
7 ctx: Pick<GenerationContext, 'localFiles'>,
8 args: {
9 sessionId: string
10 projectDir: string
11 rawDocPaths: string[]
12 // Kept as an explicit entry-mode contract for callers. The current resolver
13 // uses the same session reference/raw-doc behavior for every mode.
14 mode: GenerateMode
15 sessionRecord: Record<string, unknown>
16 }
17 ): Promise<string[]> {
18 const { sessionId, projectDir, rawDocPaths, sessionRecord } = args
19 const { assertPathInAllowedRoots } = ctx.localFiles
20 const rawReferenceDocumentPath =
21 sessionRecord.referenceDocumentPath ?? sessionRecord.reference_document_path
22 const referenceDocumentPath =
23 typeof rawReferenceDocumentPath === 'string' ? rawReferenceDocumentPath.trim() : ''
24
25 const sessionDocsDir = path.join(projectDir, 'docs')
26 const sourceDocumentPaths: string[] = []
27 const appendSourceDocumentPath = (docPath: string | null): void => {
28 if (!docPath || sourceDocumentPaths.includes(docPath)) return
29 sourceDocumentPaths.push(docPath)
30 }
31 const resolveExistingSessionDoc = (docPath: string): string | null => {
32 if (!docPath.trim()) return null
33 if (docPath.startsWith('/docs/')) {
34 const filePath = path.resolve(projectDir, docPath.replace(/^\/+/, ''))
35 const relativeToProject = path.relative(projectDir, filePath)
36 if (relativeToProject.startsWith('..') || path.isAbsolute(relativeToProject)) return null
37 try {
38 return fs.statSync(filePath).isFile() ? docPath : null
39 } catch {
40 return null
41 }
42 }
43 if (path.isAbsolute(docPath)) {
44 const absolutePath = path.resolve(docPath)
45 const relativeToProject = path.relative(projectDir, absolutePath)
46 if (relativeToProject.startsWith('..') || path.isAbsolute(relativeToProject)) return null
47 const normalizedRelative = relativeToProject.split(path.sep).join('/')
48 if (!normalizedRelative.startsWith('docs/')) return null
49 try {
50 return fs.statSync(absolutePath).isFile() ? `/${normalizedRelative}` : null
51 } catch {
52 return null
53 }
54 }
55 const normalizedDocPath = `/docs/${docPath}`
56 const filePath = path.resolve(projectDir, normalizedDocPath.replace(/^\/+/, ''))
57 const relativeToProject = path.relative(projectDir, filePath)
58 if (relativeToProject.startsWith('..') || path.isAbsolute(relativeToProject)) return null
59 try {
60 return fs.statSync(filePath).isFile() ? normalizedDocPath : null
61 } catch {
62 return null
63 }
64 }
65
66 if (referenceDocumentPath) {
67 appendSourceDocumentPath(resolveExistingSessionDoc(referenceDocumentPath))
68 }
69
70 if (rawDocPaths.length > 0) {
71 await fs.promises.mkdir(sessionDocsDir, { recursive: true })
72 for (const candidate of rawDocPaths) {
73 const sourcePath = await assertPathInAllowedRoots({
74 filePath: candidate,
75 mode: 'read',
76 sessionId
77 })
78 const safeName = path.basename(sourcePath).replace(/[\\/:"*?<>|]+/g, '-')
79 const targetPath = path.join(sessionDocsDir, safeName)
80 if (path.resolve(sourcePath) !== path.resolve(targetPath)) {
81 await fs.promises.copyFile(sourcePath, targetPath)
82 }
83 appendSourceDocumentPath(`/docs/${safeName}`)
84 }
85 return sourceDocumentPaths
86 }
87
88 if (sourceDocumentPaths.length > 0) await fs.promises.mkdir(sessionDocsDir, { recursive: true })
89 return sourceDocumentPaths
90 }
91
91 lines TYPESCRIPT