@@ -102,6 +102,29 @@ export interface LongTermMemoryRow {
102102 updated_at : string ;
103103}
104104
105+ export interface DeleteProjectResult {
106+ projectId : string ;
107+ featureMemories : number ;
108+ memoryRelations : number ;
109+ sharedIndexEntries : number ;
110+ decisionRecords : number ;
111+ longTermMemories : number ;
112+ metaEntries : number ;
113+ }
114+
115+ export interface CleanupGhostResult {
116+ projectsRemoved : number ;
117+ featureMemoriesRemoved : number ;
118+ longTermMemoriesRemoved : number ;
119+ ghosts : Array < {
120+ projectId : string ;
121+ name : string ;
122+ path : string ;
123+ featureMemories : number ;
124+ longTermMemories : number ;
125+ } > ;
126+ }
127+
105128export interface ProjectRepairReport {
106129 scannedProjects : number ;
107130 repairedProjects : number ;
@@ -644,6 +667,142 @@ export class MemoryHubDatabase {
644667 return report ;
645668 }
646669
670+ // ===========================================
671+ // Project 删除与幽灵清理
672+ // ===========================================
673+
674+ /**
675+ * 级联删除项目及其所有关联数据
676+ *
677+ * 删除顺序(按 FK 依赖):
678+ * 1. memory_relations(引用 feature_memories)
679+ * 2. shared_index(引用 feature_memories)
680+ * 3. feature_memories(引用 projects)— FTS 触发器自动同步
681+ * 4. decision_records(引用 projects)
682+ * 5. long_term_memories(引用 projects)— FTS 触发器自动同步
683+ * 6. project_memory_meta(引用 projects)
684+ * 7. projects
685+ */
686+ deleteProject ( projectId : string ) : DeleteProjectResult {
687+ const result : DeleteProjectResult = {
688+ projectId,
689+ featureMemories : 0 ,
690+ memoryRelations : 0 ,
691+ sharedIndexEntries : 0 ,
692+ decisionRecords : 0 ,
693+ longTermMemories : 0 ,
694+ metaEntries : 0 ,
695+ } ;
696+
697+ const tx = this . db . transaction ( ( ) => {
698+ // 获取该项目的所有 feature memory IDs(用于清理 relations)
699+ const memoryIds = this . db
700+ . prepare ( 'SELECT id FROM feature_memories WHERE project_id = ?' )
701+ . all ( projectId )
702+ . map ( ( r : any ) => r . id as number ) ;
703+
704+ // 1. 删除引用该项目 feature memories 的 relations
705+ if ( memoryIds . length > 0 ) {
706+ const placeholders = memoryIds . map ( ( ) => '?' ) . join ( ',' ) ;
707+ const delRelations = this . db . prepare (
708+ `DELETE FROM memory_relations WHERE from_memory_id IN (${ placeholders } ) OR to_memory_id IN (${ placeholders } )` ,
709+ ) ;
710+ const relResult = delRelations . run ( ...memoryIds , ...memoryIds ) ;
711+ result . memoryRelations = relResult . changes ;
712+ }
713+
714+ // 2. 删除引用该项目 feature memories 的 shared_index
715+ if ( memoryIds . length > 0 ) {
716+ const placeholders = memoryIds . map ( ( ) => '?' ) . join ( ',' ) ;
717+ const delShared = this . db . prepare (
718+ `DELETE FROM shared_index WHERE memory_id IN (${ placeholders } )` ,
719+ ) ;
720+ const sharedResult = delShared . run ( ...memoryIds ) ;
721+ result . sharedIndexEntries = sharedResult . changes ;
722+ }
723+
724+ // 3. 删除 feature memories(FTS 触发器自动同步)
725+ const delFeatures = this . db . prepare ( 'DELETE FROM feature_memories WHERE project_id = ?' ) ;
726+ result . featureMemories = delFeatures . run ( projectId ) . changes ;
727+
728+ // 4. 删除 decision records
729+ const delDecisions = this . db . prepare ( 'DELETE FROM decision_records WHERE project_id = ?' ) ;
730+ result . decisionRecords = delDecisions . run ( projectId ) . changes ;
731+
732+ // 5. 删除 long term memories(FTS 触发器自动同步)
733+ const delLongTerm = this . db . prepare ( 'DELETE FROM long_term_memories WHERE project_id = ?' ) ;
734+ result . longTermMemories = delLongTerm . run ( projectId ) . changes ;
735+
736+ // 6. 删除 project_memory_meta
737+ const delMeta = this . db . prepare ( 'DELETE FROM project_memory_meta WHERE project_id = ?' ) ;
738+ result . metaEntries = delMeta . run ( projectId ) . changes ;
739+
740+ // 7. 删除 project 本身
741+ const delProject = this . db . prepare ( 'DELETE FROM projects WHERE id = ?' ) ;
742+ delProject . run ( projectId ) ;
743+ } ) ;
744+
745+ tx ( ) ;
746+ logger . info ( { projectId, result } , '项目及关联数据已级联删除' ) ;
747+ return result ;
748+ }
749+
750+ /**
751+ * 清理幽灵项目(路径不存在的项目)
752+ *
753+ * @param options.mode - 'all' 清理所有不存在路径的项目;'tmp' 仅清理 /tmp 路径项目
754+ */
755+ cleanupGhostProjects ( options : { mode : 'all' | 'tmp' } = { mode : 'tmp' } ) : CleanupGhostResult {
756+ const projects = this . listProjects ( ) ;
757+ const ghostProjects : Array < { id : string ; name : string ; path : string } > = [ ] ;
758+
759+ for ( const project of projects ) {
760+ const isTmp = project . path . startsWith ( '/tmp/' ) || project . path . startsWith ( '/var/folders/' ) ;
761+ const pathExists = fs . existsSync ( project . path ) ;
762+
763+ if ( options . mode === 'tmp' ) {
764+ if ( isTmp ) {
765+ ghostProjects . push ( { id : project . id , name : project . name , path : project . path } ) ;
766+ }
767+ } else {
768+ if ( ! pathExists ) {
769+ ghostProjects . push ( { id : project . id , name : project . name , path : project . path } ) ;
770+ }
771+ }
772+ }
773+
774+ const totalDeleted : CleanupGhostResult = {
775+ projectsRemoved : 0 ,
776+ featureMemoriesRemoved : 0 ,
777+ longTermMemoriesRemoved : 0 ,
778+ ghosts : [ ] ,
779+ } ;
780+
781+ for ( const ghost of ghostProjects ) {
782+ const result = this . deleteProject ( ghost . id ) ;
783+ totalDeleted . projectsRemoved ++ ;
784+ totalDeleted . featureMemoriesRemoved += result . featureMemories ;
785+ totalDeleted . longTermMemoriesRemoved += result . longTermMemories ;
786+ totalDeleted . ghosts . push ( {
787+ projectId : ghost . id ,
788+ name : ghost . name ,
789+ path : ghost . path ,
790+ featureMemories : result . featureMemories ,
791+ longTermMemories : result . longTermMemories ,
792+ } ) ;
793+ }
794+
795+ logger . info (
796+ {
797+ mode : options . mode ,
798+ removed : totalDeleted . projectsRemoved ,
799+ featuresRemoved : totalDeleted . featureMemoriesRemoved ,
800+ } ,
801+ '幽灵项目清理完成' ,
802+ ) ;
803+ return totalDeleted ;
804+ }
805+
647806 // ===========================================
648807 // Project Meta 管理
649808 // ===========================================
0 commit comments