001project_wildgrowth/backend/scripts/inspect-paragraphs-full.js

175 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 彻底排查「段落间距」全链路:
* 1. 该课程所有节点outline.suggestedContent 按双换行拆段数 vs node_slides.content.paragraphs 数
* 2. 调用 lesson detail API核对返回的 blocks 数量与内容
* 用法node scripts/inspect-paragraphs-full.js <courseId>
* 例node scripts/inspect-paragraphs-full.js 08253870-7499-4f39-9fa1-9cb76c280770
*/
const { PrismaClient } = require('@prisma/client');
const http = require('http');
const prisma = new PrismaClient();
const API_BASE = process.env.API_BASE || 'http://127.0.0.1:3000';
function get(url) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = http.request(
{ hostname: u.hostname, port: u.port || 80, path: u.pathname + u.search, method: 'GET' },
(res) => {
let body = '';
res.on('data', (ch) => { body += ch; });
res.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error('parse JSON: ' + body.slice(0, 200)));
}
});
}
);
req.on('error', reject);
req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); });
req.end();
});
}
async function main() {
const courseId = process.argv[2] ? process.argv[2].trim() : null;
if (!courseId) {
console.log('用法: node scripts/inspect-paragraphs-full.js <courseId>');
process.exit(1);
}
const task = await prisma.courseGenerationTask.findFirst({
where: { courseId },
orderBy: { createdAt: 'desc' },
include: { course: { select: { id: true, title: true } } },
});
if (!task) {
console.log('未找到 courseId 对应的任务:', courseId);
return;
}
const outline = task.outline;
if (!outline || !outline.chapters || !Array.isArray(outline.chapters)) {
console.log('任务无 outline.chapters');
return;
}
const outlineNodes = [];
for (const ch of outline.chapters) {
if (ch.nodes && Array.isArray(ch.nodes)) {
for (const node of ch.nodes) {
outlineNodes.push({
title: node.title || '',
suggestedContent: node.suggestedContent || '',
});
}
}
}
const dbNodes = await prisma.courseNode.findMany({
where: { courseId },
orderBy: { orderIndex: 'asc' },
include: { slides: { orderBy: { orderIndex: 'asc' } } },
});
console.log('=== 课程:', task.course?.title ?? courseId, '===');
console.log('courseId:', courseId);
console.log('taskId:', task.id);
console.log('outline 小节总数:', outlineNodes.length);
console.log('DB 节点总数:', dbNodes.length);
console.log('');
const rows = [];
for (let i = 0; i < Math.max(outlineNodes.length, dbNodes.length); i++) {
const oNode = outlineNodes[i];
const dNode = dbNodes[i];
let outlineParagraphs = 0;
if (oNode && oNode.suggestedContent) {
const split = oNode.suggestedContent.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
outlineParagraphs = split.length;
}
let slideParagraphs = 0;
let nodeId = '';
let nodeTitle = '';
if (dNode) {
nodeId = dNode.id;
nodeTitle = dNode.title || '';
const slide = dNode.slides && dNode.slides[0];
if (slide && slide.content && typeof slide.content === 'object' && Array.isArray(slide.content.paragraphs)) {
slideParagraphs = slide.content.paragraphs.length;
}
}
const match = outlineParagraphs === slideParagraphs ? '✓' : '✗';
rows.push({
index: i + 1,
nodeId,
nodeTitle: nodeTitle.slice(0, 36) + (nodeTitle.length > 36 ? '…' : ''),
outlineParagraphs,
slideParagraphs,
match,
});
}
console.log('--- 全节点对比outline 按双换行拆段数 vs node_slides.paragraphs 数)---');
console.log('序号\toutline段数\tslide段数\t一致\t节点标题');
rows.forEach((r) => {
console.log(`${r.index}\t${r.outlineParagraphs}\t\t${r.slideParagraphs}\t\t${r.match}\t${r.nodeTitle}`);
});
const mismatchCount = rows.filter((r) => r.match === '✗').length;
console.log('');
if (mismatchCount > 0) {
console.log('⚠️ 不一致节点数:', mismatchCount);
} else {
console.log('✓ 所有节点 outline 段数与 node_slides 段数一致');
}
console.log('');
console.log('--- 调用 lesson detail API 核对(前 3 个节点)---');
for (let i = 0; i < Math.min(3, dbNodes.length); i++) {
const node = dbNodes[i];
const nodeId = node.id;
try {
const res = await get(`${API_BASE}/api/lessons/nodes/${nodeId}/detail`);
if (!res || !res.success || !res.data) {
console.log(`节点 ${i + 1} (${nodeId}): API 返回异常`, res ? (res.error || res) : 'null');
continue;
}
const blocks = res.data.blocks || [];
const blockTypes = blocks.map((b) => b.type).join(', ');
console.log(`节点 ${i + 1}: ${node.title?.slice(0, 32)}`);
console.log(` nodeId: ${nodeId}`);
console.log(` API blocks 数量: ${blocks.length}`);
console.log(` block types: ${blockTypes}`);
blocks.slice(0, 2).forEach((b, bi) => {
console.log(` block[${bi}] type=${b.type} contentLen=${(b.content || '').length} contentPreview=${JSON.stringify((b.content || '').slice(0, 50))}`);
});
const expectedBlocks = rows[i] ? rows[i].slideParagraphs : 0;
if (blocks.length !== expectedBlocks) {
console.log(` ⚠️ 与 node_slides.paragraphs 数(${expectedBlocks}) 不一致`);
}
console.log('');
} catch (e) {
console.log(`节点 ${i + 1} (${nodeId}): 请求失败`, e.message);
console.log('');
}
}
console.log('--- 小结 ---');
console.log('1. 上表outline 段数 = suggestedContent 按 /\\n\\s*\\n/ 拆分后的段落数slide 段数 = node_slides.content.paragraphs 长度');
console.log('2. getLessonDetail 会把每个 slide 的 content.paragraphs 逐条转成 paragraph block故 blocks 数应等于该节点 slide 的 paragraphs 数');
console.log('3. iOS ContentBlockBuilder 在 index>0 的 block 前插入 paragraphSpacing(24pt),多段即有多段间距');
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());