| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
- import { dirname } from 'node:path';
- import type { DomesticDataset } from '../../types/domestic-dataset.js';
- import type {
- ProductKnowledgePage,
- ProductKnowledgeRecord,
- ProductKnowledgeRole,
- ProductKnowledgeStatus,
- ProductKnowledgeStore,
- } from './product-knowledge.store.js';
- interface LocalKnowledgeFile {
- schemaVersion: 1;
- items: ProductKnowledgeRecord[];
- }
- export class LocalProductKnowledgeStore implements ProductKnowledgeStore {
- private readonly records = new Map<string, ProductKnowledgeRecord>();
- constructor(
- dataset: DomesticDataset,
- private readonly workspaceId: string,
- initialItems: ProductKnowledgeRecord[] = [],
- private readonly persistencePath = '',
- private readonly now: () => Date = () => new Date(),
- ) {
- for (const item of initialItems) {
- if (item.workspaceId === workspaceId) this.records.set(item.productKey, { ...item, tags: [...item.tags] });
- }
- if (!this.records.size) this.seedTopProducts(dataset);
- }
- static async open(input: {
- dataset: DomesticDataset;
- workspaceId: string;
- persistencePath: string;
- }): Promise<LocalProductKnowledgeStore> {
- let items: ProductKnowledgeRecord[] = [];
- try {
- const parsed = JSON.parse(await readFile(input.persistencePath, 'utf8')) as Partial<LocalKnowledgeFile>;
- if (parsed.schemaVersion === 1 && Array.isArray(parsed.items)) items = parsed.items;
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
- }
- const store = new LocalProductKnowledgeStore(
- input.dataset,
- input.workspaceId,
- items,
- input.persistencePath,
- );
- await store.persist();
- return store;
- }
- async list(input: {
- workspaceId: string;
- limit: number;
- cursor: string | null;
- status?: ProductKnowledgeStatus;
- productRole?: ProductKnowledgeRole;
- featured?: boolean;
- }): Promise<ProductKnowledgePage> {
- if (input.workspaceId !== this.workspaceId) return { items: [], nextCursor: null };
- const items = [...this.records.values()]
- .filter((item) => !input.status || item.status === input.status)
- .filter((item) => !input.productRole || item.productRole === input.productRole)
- .filter((item) => input.featured === undefined || item.featured === input.featured)
- .sort((left, right) => left.productKey.localeCompare(right.productKey));
- const cursorIndex = input.cursor ? items.findIndex((item) => item.productKey === input.cursor) : -1;
- const start = cursorIndex + 1;
- const page = items.slice(start, start + input.limit + 1);
- const hasMore = page.length > input.limit;
- const visible = page.slice(0, input.limit).map((item) => ({ ...item, tags: [...item.tags] }));
- return {
- items: visible,
- nextCursor: hasMore ? visible.at(-1)?.productKey ?? null : null,
- };
- }
- async upsert(input: {
- workspaceId: string;
- productKey: string;
- productId: string;
- productRole: ProductKnowledgeRole;
- featured: boolean;
- status: ProductKnowledgeStatus;
- tags: string[];
- note: string;
- ownerUserId: string;
- actorUserId: string;
- }): Promise<ProductKnowledgeRecord> {
- const existing = this.records.get(input.productKey);
- const timestamp = this.now().toISOString();
- const item: ProductKnowledgeRecord = {
- id: existing?.id ?? `local-${input.productKey.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
- workspaceId: input.workspaceId,
- productKey: input.productKey,
- productId: input.productId,
- productRole: input.productRole,
- featured: input.featured,
- status: input.status,
- tags: [...new Set(input.tags.map((tag) => tag.trim()).filter(Boolean))],
- note: input.note.trim(),
- ownerUserId: input.ownerUserId.trim(),
- createdBy: existing?.createdBy ?? input.actorUserId,
- updatedBy: input.actorUserId,
- createdAt: existing?.createdAt ?? timestamp,
- updatedAt: timestamp,
- };
- this.records.set(input.productKey, item);
- await this.persist();
- return { ...item, tags: [...item.tags] };
- }
- async archive(workspaceId: string, productKey: string, actorUserId: string): Promise<ProductKnowledgeRecord | null> {
- if (workspaceId !== this.workspaceId) return null;
- const existing = this.records.get(productKey);
- if (!existing) return null;
- const item: ProductKnowledgeRecord = {
- ...existing,
- featured: false,
- status: 'archived',
- updatedBy: actorUserId,
- updatedAt: this.now().toISOString(),
- };
- this.records.set(productKey, item);
- await this.persist();
- return { ...item, tags: [...item.tags] };
- }
- private seedTopProducts(dataset: DomesticDataset): void {
- const timestamp = this.now().toISOString();
- const candidates = [...dataset.products]
- .filter((product) => product.role === 'own')
- .sort((left, right) => right.summary.gmv - left.summary.gmv || right.summary.soldUnits - left.summary.soldUnits)
- .slice(0, 8);
- for (const product of candidates) {
- const category = product.category3 || product.category2 || product.category1;
- const item: ProductKnowledgeRecord = {
- id: `local-${product.productKey.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
- workspaceId: this.workspaceId,
- productKey: product.productKey,
- productId: product.productId,
- productRole: product.role,
- featured: true,
- status: 'active',
- tags: ['经营TOP', ...(category ? [category] : [])],
- note: '按成交金额初始化的重点商品,可在知识库内补充关注事项。',
- ownerUserId: '',
- createdBy: 'local-bootstrap',
- updatedBy: 'local-bootstrap',
- createdAt: timestamp,
- updatedAt: timestamp,
- };
- this.records.set(product.productKey, item);
- }
- }
- private async persist(): Promise<void> {
- if (!this.persistencePath) return;
- await mkdir(dirname(this.persistencePath), { recursive: true });
- const payload: LocalKnowledgeFile = {
- schemaVersion: 1,
- items: [...this.records.values()].sort((left, right) => left.productKey.localeCompare(right.productKey)),
- };
- await writeFile(this.persistencePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
- }
- }
|