feat(25-07): create placeholderService and add markAsPlaceholder method
- Create server/src/services/placeholder-service.ts with addEntry, replaceEntry, listEntries - Generates PLACEHOLDERS.md with Active Placeholders and Replaced markdown tables - Add ChatPlaceholderEntry interface to packages/shared/src/types/chat.ts - Export ChatPlaceholderEntry from packages/shared/src/index.ts - Add markAsPlaceholder method to chatFileService in chat-files.ts
This commit is contained in:
parent
e9f5897eec
commit
2cafff8054
4 changed files with 179 additions and 0 deletions
|
|
@ -592,6 +592,7 @@ export type {
|
|||
ChatFileReference,
|
||||
ChatFileUploadResponse,
|
||||
ChatFileListResponse,
|
||||
ChatPlaceholderEntry,
|
||||
} from "./types/chat.js";
|
||||
|
||||
export { API_PREFIX, API } from "./api.js";
|
||||
|
|
|
|||
|
|
@ -117,3 +117,11 @@ export interface ChatBookmarkListResponse {
|
|||
export interface ChatBookmarkToggleResponse {
|
||||
bookmarked: boolean;
|
||||
}
|
||||
|
||||
export interface ChatPlaceholderEntry {
|
||||
fileId: string;
|
||||
filename: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
replacedByFileId?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,5 +118,14 @@ export function chatFileService(db: Db) {
|
|||
.returning()
|
||||
.then((rows) => rows[0] ?? null);
|
||||
},
|
||||
|
||||
markAsPlaceholder(fileId: string) {
|
||||
return db
|
||||
.update(chatFiles)
|
||||
.set({ category: "placeholder", updatedAt: new Date() })
|
||||
.where(eq(chatFiles.id, fileId))
|
||||
.returning()
|
||||
.then((rows) => rows[0] ?? null);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
161
server/src/services/placeholder-service.ts
Normal file
161
server/src/services/placeholder-service.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export interface PlaceholderEntry {
|
||||
fileId: string;
|
||||
filename: string;
|
||||
description: string;
|
||||
replacedByFileId?: string;
|
||||
}
|
||||
|
||||
const MANIFEST_FILENAME = "PLACEHOLDERS.md";
|
||||
|
||||
function serialize(active: PlaceholderEntry[], replaced: PlaceholderEntry[]): string {
|
||||
const activeRows =
|
||||
active.length === 0
|
||||
? "_None_"
|
||||
: active.map((e) => `| ${e.filename} | ${e.description} | ${e.fileId} |`).join("\n");
|
||||
|
||||
const replacedRows =
|
||||
replaced.length === 0
|
||||
? "_None_"
|
||||
: replaced
|
||||
.map((e) => `| ${e.filename} | ${e.description} | ${e.replacedByFileId ?? ""} |`)
|
||||
.join("\n");
|
||||
|
||||
return `# Placeholder Assets
|
||||
|
||||
Auto-maintained by Nexus. Do not edit manually.
|
||||
|
||||
## Active Placeholders
|
||||
|
||||
| File | Description | File ID |
|
||||
|------|-------------|---------|
|
||||
${activeRows}
|
||||
|
||||
## Replaced
|
||||
|
||||
| Original | Description | Replaced By |
|
||||
|----------|-------------|-------------|
|
||||
${replacedRows}
|
||||
`;
|
||||
}
|
||||
|
||||
function parse(content: string): { active: PlaceholderEntry[]; replaced: PlaceholderEntry[] } {
|
||||
const active: PlaceholderEntry[] = [];
|
||||
const replaced: PlaceholderEntry[] = [];
|
||||
|
||||
let section: "active" | "replaced" | null = null;
|
||||
const rowRe = /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/;
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
if (line.includes("## Active Placeholders")) {
|
||||
section = "active";
|
||||
continue;
|
||||
}
|
||||
if (line.includes("## Replaced")) {
|
||||
section = "replaced";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!section) continue;
|
||||
|
||||
const match = rowRe.exec(line);
|
||||
if (!match) continue;
|
||||
|
||||
const [, col1, col2, col3] = match;
|
||||
// Skip header row and separator row
|
||||
if (
|
||||
col1 === "File" ||
|
||||
col1 === "Original" ||
|
||||
col1 === "---" ||
|
||||
col1?.startsWith("-")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// Skip "_None_" placeholder rows
|
||||
if (col1?.trim() === "_None_") continue;
|
||||
|
||||
if (section === "active") {
|
||||
active.push({
|
||||
filename: col1 ?? "",
|
||||
description: col2 ?? "",
|
||||
fileId: col3 ?? "",
|
||||
});
|
||||
} else {
|
||||
replaced.push({
|
||||
filename: col1 ?? "",
|
||||
description: col2 ?? "",
|
||||
replacedByFileId: col3 ?? undefined,
|
||||
fileId: col3 ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { active, replaced };
|
||||
}
|
||||
|
||||
async function readManifest(projectDir: string): Promise<{ active: PlaceholderEntry[]; replaced: PlaceholderEntry[] }> {
|
||||
const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
|
||||
if (!existsSync(manifestPath)) {
|
||||
return { active: [], replaced: [] };
|
||||
}
|
||||
const content = await readFile(manifestPath, "utf-8");
|
||||
return parse(content);
|
||||
}
|
||||
|
||||
async function writeManifest(
|
||||
projectDir: string,
|
||||
active: PlaceholderEntry[],
|
||||
replaced: PlaceholderEntry[],
|
||||
): Promise<void> {
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
|
||||
await writeFile(manifestPath, serialize(active, replaced), "utf-8");
|
||||
}
|
||||
|
||||
export function placeholderService() {
|
||||
return {
|
||||
async addEntry(
|
||||
projectDir: string,
|
||||
entry: { fileId: string; filename: string; description: string },
|
||||
): Promise<void> {
|
||||
const { active, replaced } = await readManifest(projectDir);
|
||||
// Avoid duplicates
|
||||
if (!active.some((e) => e.fileId === entry.fileId)) {
|
||||
active.push({
|
||||
fileId: entry.fileId,
|
||||
filename: entry.filename,
|
||||
description: entry.description,
|
||||
});
|
||||
}
|
||||
await writeManifest(projectDir, active, replaced);
|
||||
},
|
||||
|
||||
async replaceEntry(
|
||||
projectDir: string,
|
||||
oldFileId: string,
|
||||
newFileId: string,
|
||||
): Promise<void> {
|
||||
const { active, replaced } = await readManifest(projectDir);
|
||||
const idx = active.findIndex((e) => e.fileId === oldFileId);
|
||||
if (idx === -1) return;
|
||||
|
||||
const [entry] = active.splice(idx, 1);
|
||||
replaced.push({
|
||||
fileId: entry!.fileId,
|
||||
filename: entry!.filename,
|
||||
description: entry!.description,
|
||||
replacedByFileId: newFileId,
|
||||
});
|
||||
await writeManifest(projectDir, active, replaced);
|
||||
},
|
||||
|
||||
async listEntries(
|
||||
projectDir: string,
|
||||
): Promise<{ active: PlaceholderEntry[]; replaced: PlaceholderEntry[] }> {
|
||||
return readManifest(projectDir);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue