| 103 | .replace(/[^a-zA-Z0-9_\-.\u4e00-\u9fff]+/g, "_") |
| 104 | .replace(/_+/g, "_") |
| 105 | .replace(/^_+|_+$/g, "") |
| 106 | .slice(0, 80); |
| 107 | |
| 108 | return sanitized || fallback; |
| 109 | } |
| 110 | |
| 111 | private getLocalSaveRoot(): string { |
| 112 | return path.join(process.cwd(), "save"); |
| 113 | } |
| 114 | |
| 115 | private buildUniquePath(dirPath: string, fileName: string): string { |
| 116 | const parsed = path.parse(fileName); |
| 117 | const baseName = parsed.name || "file"; |
| 118 | const extension = parsed.ext || ""; |
| 119 | let candidate = path.join(dirPath, `${baseName}${extension}`); |
| 120 | let counter = 1; |
| 121 | |
| 122 | while (existsSync(candidate)) { |
| 123 | candidate = path.join(dirPath, `${baseName}_${counter}${extension}`); |
| 124 | counter++; |
| 125 | } |
| 126 | |
| 127 | return candidate; |
| 128 | } |
| 129 | |
| 130 | private async moveFile(sourcePath: string, destinationPath: string): Promise<void> { |
| 131 | try { |
| 132 | await fs.rename(sourcePath, destinationPath); |
| 133 | } catch { |
| 134 | await fs.copyFile(sourcePath, destinationPath); |
| 135 | await fs.unlink(sourcePath); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | private async saveLocalMetadata(filePath: string, metadata: Record<string, unknown>): Promise<string> { |
| 140 | const parsed = path.parse(filePath); |
| 141 | const metadataPath = this.buildUniquePath(parsed.dir, `${parsed.name}.json`); |
| 142 | await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf8"); |
| 143 | return metadataPath; |
| 144 | } |
| 145 | |
| 146 | private async ensureLocalChatDirectory(chatId: string): Promise<{ rootDir: string; chatDir: string; displayName: string }> { |
| 147 | const rootDir = this.getLocalSaveRoot(); |
| 148 | await fs.mkdir(rootDir, { recursive: true }); |
| 149 | |
| 150 | const displayName = await this.getChatDisplayName(chatId); |
| 151 | const dirName = this.sanitizePathSegment(chatId, "chat"); |
| 152 | const chatDir = path.join(rootDir, dirName); |
| 153 | await fs.mkdir(chatDir, { recursive: true }); |
| 154 | |
| 155 | return { rootDir, chatDir, displayName }; |
| 156 | } |
| 157 | |
| 158 | private getLocalChatRelativeDirectory(chatId: string): string { |
| 159 | const chatDir = path.join(this.getLocalSaveRoot(), this.sanitizePathSegment(chatId, "chat")); |
| 160 | return path.relative(process.cwd(), chatDir) || chatDir; |
| 161 | } |
| 162 |
nothing calls this directly
no test coverage detected