erp-plugin-sdk/src/backend/help/help.service.ts

131 lines
3.8 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
export interface HelpArticle {
id: string;
title: string | Record<string, string>;
description?: string | Record<string, string>;
}
export interface HelpArticlesJson {
articles: HelpArticle[];
}
@Injectable()
export class HelpService {
private readonly logger = new Logger(HelpService.name);
private readonly helpDir: string;
private cachedArticles: HelpArticle[] | null = null;
constructor() {
this.helpDir = path.resolve(process.cwd(), 'help');
}
getArticles(): HelpArticle[] {
if (this.cachedArticles) return this.cachedArticles;
if (!fs.existsSync(this.helpDir)) {
this.logger.warn(`Help directory not found: ${this.helpDir}`);
this.cachedArticles = [];
return this.cachedArticles;
}
// Try articles.json first
const jsonPath = path.join(this.helpDir, 'articles.json');
if (fs.existsSync(jsonPath)) {
try {
const raw = fs.readFileSync(jsonPath, 'utf-8');
const parsed: HelpArticlesJson = JSON.parse(raw);
if (parsed.articles && Array.isArray(parsed.articles)) {
this.cachedArticles = parsed.articles;
return this.cachedArticles;
}
} catch (e: any) {
this.logger.error(`Failed to parse articles.json: ${e.message}`);
}
}
// Fallback: auto-discover from .md files
return this.discoverArticlesFromFiles();
}
getArticleContent(id: string, lang: string): string | null {
if (!fs.existsSync(this.helpDir)) return null;
// Try lang-specific file first: help/{id}.{lang}.md
const langPath = path.join(this.helpDir, `${id}.${lang}.md`);
if (fs.existsSync(langPath)) {
return fs.readFileSync(langPath, 'utf-8');
}
// Fallback to .en.md
const enPath = path.join(this.helpDir, `${id}.en.md`);
if (fs.existsSync(enPath)) {
return fs.readFileSync(enPath, 'utf-8');
}
// Fallback to plain {id}.md
const plainPath = path.join(this.helpDir, `${id}.md`);
if (fs.existsSync(plainPath)) {
return fs.readFileSync(plainPath, 'utf-8');
}
// Fallback to any matching file
try {
const files = fs.readdirSync(this.helpDir);
const match = files.find(f => f.startsWith(`${id}.`) && f.endsWith('.md'));
if (match) {
return fs.readFileSync(path.join(this.helpDir, match), 'utf-8');
}
} catch (e) {}
return null;
}
clearCache(): void {
this.cachedArticles = null;
}
private discoverArticlesFromFiles(): HelpArticle[] {
try {
const files = fs.readdirSync(this.helpDir).filter(f => f.endsWith('.md'));
const seen = new Set<string>();
const articles: HelpArticle[] = [];
for (const file of files) {
// Pattern: {id}.{lang}.md or {id}.md
const baseName = file.replace(/\.md$/, '');
const parts = baseName.split('.');
let id: string;
if (parts.length >= 2 && parts[parts.length - 1].length === 2) {
// Has language suffix: getting-started.en.md
id = parts.slice(0, -1).join('.');
} else {
// Plain name: getting-started.md
id = baseName;
}
if (seen.has(id)) continue;
seen.add(id);
// Extract title from first h1
const content = fs.readFileSync(path.join(this.helpDir, file), 'utf-8');
const titleMatch = content.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : id;
const descMatch = content.match(/^>\s+(.+)$/m);
const description = descMatch ? descMatch[1].trim() : undefined;
articles.push({ id, title, ...(description ? { description } : {}) });
}
return articles;
} catch (e: any) {
this.logger.error(`Failed to discover help articles: ${e.message}`);
return [];
}
}
}