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,
|
ChatFileReference,
|
||||||
ChatFileUploadResponse,
|
ChatFileUploadResponse,
|
||||||
ChatFileListResponse,
|
ChatFileListResponse,
|
||||||
|
ChatPlaceholderEntry,
|
||||||
} from "./types/chat.js";
|
} from "./types/chat.js";
|
||||||
|
|
||||||
export { API_PREFIX, API } from "./api.js";
|
export { API_PREFIX, API } from "./api.js";
|
||||||
|
|
|
||||||
|
|
@ -117,3 +117,11 @@ export interface ChatBookmarkListResponse {
|
||||||
export interface ChatBookmarkToggleResponse {
|
export interface ChatBookmarkToggleResponse {
|
||||||
bookmarked: boolean;
|
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()
|
.returning()
|
||||||
.then((rows) => rows[0] ?? null);
|
.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