documentationService.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /**
  2. * 文档服务模块 - 处理文档管理功能
  3. */
  4. const fs = require('fs').promises;
  5. const path = require('path');
  6. const logger = require('../logger');
  7. const DOCUMENTATION_DIR = path.join(__dirname, '..', 'documentation');
  8. const META_DIR = path.join(DOCUMENTATION_DIR, 'meta');
  9. // 确保文档目录存在
  10. async function ensureDocumentationDir() {
  11. try {
  12. await fs.access(DOCUMENTATION_DIR);
  13. logger.debug('文档目录已存在');
  14. // 确保meta目录存在
  15. try {
  16. await fs.access(META_DIR);
  17. logger.debug('文档meta目录已存在');
  18. } catch (error) {
  19. if (error.code === 'ENOENT') {
  20. await fs.mkdir(META_DIR, { recursive: true });
  21. logger.success('文档meta目录已创建');
  22. } else {
  23. throw error;
  24. }
  25. }
  26. } catch (error) {
  27. if (error.code === 'ENOENT') {
  28. await fs.mkdir(DOCUMENTATION_DIR, { recursive: true });
  29. logger.success('文档目录已创建');
  30. // 创建meta目录
  31. await fs.mkdir(META_DIR, { recursive: true });
  32. logger.success('文档meta目录已创建');
  33. } else {
  34. throw error;
  35. }
  36. }
  37. }
  38. // 获取文档列表
  39. async function getDocumentationList() {
  40. try {
  41. await ensureDocumentationDir();
  42. const files = await fs.readdir(DOCUMENTATION_DIR);
  43. const documents = await Promise.all(files.map(async file => {
  44. // 跳过目录和非文档文件
  45. if (file === 'meta' || file.startsWith('.')) return null;
  46. // 处理JSON文件
  47. if (file.endsWith('.json')) {
  48. try {
  49. const filePath = path.join(DOCUMENTATION_DIR, file);
  50. const content = await fs.readFile(filePath, 'utf8');
  51. const doc = JSON.parse(content);
  52. return {
  53. id: path.parse(file).name,
  54. title: doc.title,
  55. published: doc.published,
  56. createdAt: doc.createdAt || new Date().toISOString(),
  57. updatedAt: doc.updatedAt || new Date().toISOString()
  58. };
  59. } catch (fileError) {
  60. logger.error(`读取JSON文档文件 ${file} 失败:`, fileError);
  61. return null;
  62. }
  63. }
  64. // 处理MD文件
  65. if (file.endsWith('.md')) {
  66. try {
  67. const id = path.parse(file).name;
  68. let metaData = { published: true, title: path.parse(file).name };
  69. // 尝试读取meta数据
  70. try {
  71. const metaPath = path.join(META_DIR, `${id}.json`);
  72. const metaContent = await fs.readFile(metaPath, 'utf8');
  73. metaData = { ...metaData, ...JSON.parse(metaContent) };
  74. } catch (metaError) {
  75. // meta文件不存在或无法解析,使用默认值
  76. logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
  77. }
  78. // 确保有发布状态
  79. if (typeof metaData.published !== 'boolean') {
  80. metaData.published = true;
  81. }
  82. return {
  83. id,
  84. title: metaData.title || id,
  85. path: file, // 不直接加载内容,而是提供路径
  86. published: metaData.published,
  87. createdAt: metaData.createdAt || new Date().toISOString(),
  88. updatedAt: metaData.updatedAt || new Date().toISOString()
  89. };
  90. } catch (mdError) {
  91. logger.error(`处理MD文档 ${file} 失败:`, mdError);
  92. return null;
  93. }
  94. }
  95. return null;
  96. }));
  97. return documents.filter(doc => doc !== null);
  98. } catch (error) {
  99. logger.error('获取文档列表失败:', error);
  100. throw error;
  101. }
  102. }
  103. // 获取已发布文档
  104. async function getPublishedDocuments() {
  105. const documents = await getDocumentationList();
  106. return documents.filter(doc => doc.published);
  107. }
  108. // 获取单个文档
  109. async function getDocument(id) {
  110. try {
  111. await ensureDocumentationDir();
  112. // 首先尝试读取JSON文件
  113. try {
  114. const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
  115. const jsonContent = await fs.readFile(jsonPath, 'utf8');
  116. return JSON.parse(jsonContent);
  117. } catch (jsonError) {
  118. // JSON文件不存在,尝试读取MD文件
  119. if (jsonError.code === 'ENOENT') {
  120. const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
  121. const mdContent = await fs.readFile(mdPath, 'utf8');
  122. // 读取meta数据
  123. let metaData = { published: true, title: id };
  124. try {
  125. const metaPath = path.join(META_DIR, `${id}.json`);
  126. const metaContent = await fs.readFile(metaPath, 'utf8');
  127. metaData = { ...metaData, ...JSON.parse(metaContent) };
  128. } catch (metaError) {
  129. // meta文件不存在或无法解析,使用默认值
  130. logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
  131. }
  132. return {
  133. id,
  134. title: metaData.title || id,
  135. content: mdContent,
  136. published: metaData.published,
  137. createdAt: metaData.createdAt || new Date().toISOString(),
  138. updatedAt: metaData.updatedAt || new Date().toISOString()
  139. };
  140. }
  141. // 其他错误,直接抛出
  142. throw jsonError;
  143. }
  144. } catch (error) {
  145. logger.error(`获取文档 ${id} 失败:`, error);
  146. throw error;
  147. }
  148. }
  149. // 保存文档
  150. async function saveDocument(id, title, content) {
  151. try {
  152. await ensureDocumentationDir();
  153. const docId = id || Date.now().toString();
  154. const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
  155. // 检查是否已存在,保留发布状态
  156. let published = false;
  157. try {
  158. const existingDoc = await fs.readFile(docPath, 'utf8');
  159. published = JSON.parse(existingDoc).published || false;
  160. } catch (error) {
  161. // 文件不存在,使用默认值
  162. }
  163. const now = new Date().toISOString();
  164. const docData = {
  165. title,
  166. content,
  167. published,
  168. createdAt: now,
  169. updatedAt: now
  170. };
  171. await fs.writeFile(
  172. docPath,
  173. JSON.stringify(docData, null, 2),
  174. 'utf8'
  175. );
  176. return { id: docId, ...docData };
  177. } catch (error) {
  178. logger.error('保存文档失败:', error);
  179. throw error;
  180. }
  181. }
  182. // 删除文档
  183. async function deleteDocument(id) {
  184. try {
  185. await ensureDocumentationDir();
  186. // 删除JSON文件(如果存在)
  187. try {
  188. const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
  189. await fs.unlink(jsonPath);
  190. } catch (error) {
  191. if (error.code !== 'ENOENT') {
  192. logger.warn(`删除JSON文档 ${id} 失败:`, error);
  193. }
  194. }
  195. // 删除MD文件(如果存在)
  196. try {
  197. const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
  198. await fs.unlink(mdPath);
  199. } catch (error) {
  200. if (error.code !== 'ENOENT') {
  201. logger.warn(`删除MD文档 ${id} 失败:`, error);
  202. }
  203. }
  204. // 删除meta文件(如果存在)
  205. try {
  206. const metaPath = path.join(META_DIR, `${id}.json`);
  207. await fs.unlink(metaPath);
  208. } catch (error) {
  209. if (error.code !== 'ENOENT') {
  210. logger.warn(`删除文档 ${id} 的meta数据失败:`, error);
  211. }
  212. }
  213. return { success: true };
  214. } catch (error) {
  215. logger.error(`删除文档 ${id} 失败:`, error);
  216. throw error;
  217. }
  218. }
  219. // 切换文档发布状态
  220. async function toggleDocumentPublish(id) {
  221. try {
  222. await ensureDocumentationDir();
  223. // 尝试读取JSON文件
  224. try {
  225. const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
  226. const content = await fs.readFile(jsonPath, 'utf8');
  227. const doc = JSON.parse(content);
  228. doc.published = !doc.published;
  229. doc.updatedAt = new Date().toISOString();
  230. await fs.writeFile(jsonPath, JSON.stringify(doc, null, 2), 'utf8');
  231. return doc;
  232. } catch (jsonError) {
  233. // 如果JSON文件不存在,尝试处理MD文件的meta数据
  234. if (jsonError.code === 'ENOENT') {
  235. const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
  236. // 确认MD文件存在
  237. try {
  238. await fs.access(mdPath);
  239. } catch (mdError) {
  240. throw new Error(`文档 ${id} 不存在`);
  241. }
  242. // 获取或创建meta数据
  243. const metaPath = path.join(META_DIR, `${id}.json`);
  244. let metaData = { published: true, title: id };
  245. try {
  246. const metaContent = await fs.readFile(metaPath, 'utf8');
  247. metaData = { ...metaData, ...JSON.parse(metaContent) };
  248. } catch (metaError) {
  249. // meta文件不存在,使用默认值
  250. }
  251. // 切换发布状态
  252. metaData.published = !metaData.published;
  253. metaData.updatedAt = new Date().toISOString();
  254. // 保存meta数据
  255. await fs.writeFile(metaPath, JSON.stringify(metaData, null, 2), 'utf8');
  256. // 获取MD文件内容
  257. const mdContent = await fs.readFile(mdPath, 'utf8');
  258. return {
  259. id,
  260. title: metaData.title,
  261. content: mdContent,
  262. published: metaData.published,
  263. createdAt: metaData.createdAt,
  264. updatedAt: metaData.updatedAt
  265. };
  266. }
  267. // 其他错误,直接抛出
  268. throw jsonError;
  269. }
  270. } catch (error) {
  271. logger.error(`切换文档 ${id} 发布状态失败:`, error);
  272. throw error;
  273. }
  274. }
  275. module.exports = {
  276. ensureDocumentationDir,
  277. getDocumentationList,
  278. getPublishedDocuments,
  279. getDocument,
  280. saveDocument,
  281. deleteDocument,
  282. toggleDocumentPublish
  283. };