Agent/reports/md-splitter.ts
2026-08-06 22:17:39 +08:00

393 lines
15 KiB
TypeScript
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.

// @ts-nocheck
export function stripHints(md) {
return String(md || '')
.replace(/<!--\s*HINT\s*:[\s\S]*?-->/gi, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
export function splitModule(mdBlock) {
const lines = String(mdBlock || '').split(/\r?\n/);
const tables = [];
const hints = [];
let currentTable = null;
let hintBuf = null;
const flushTable = () => {
if (!currentTable) return;
currentTable.md = String(currentTable.md || '').trimEnd();
if (currentTable.md) tables.push(currentTable);
currentTable = null;
};
const flushHint = (raw) => {
const m = String(raw || '').match(/<!--\s*HINT\s*:\s*([\s\S]*?)-->/i);
if (m) {
const text = m[1].replace(/\s+/g, ' ').trim();
if (text) hints.push(text);
}
};
for (const line of lines) {
const trimmed = line.trim();
if (hintBuf !== null) {
hintBuf += `\n${line}`;
if (trimmed.includes('-->')) {
flushHint(hintBuf);
hintBuf = null;
}
continue;
}
if (/^<!--\s*HINT\s*:/i.test(trimmed)) {
flushTable();
if (trimmed.includes('-->')) flushHint(trimmed);
else hintBuf = line;
continue;
}
if (/^###\s+/.test(trimmed)) {
flushTable();
currentTable = { title: trimmed.replace(/^###\s+/, '').trim(), md: '' };
continue;
}
if (trimmed.startsWith('|')) {
if (!currentTable) currentTable = { title: '', md: '' };
currentTable.md += `${line}\n`;
continue;
}
if (currentTable && currentTable.md) flushTable();
}
if (hintBuf !== null) flushHint(`${hintBuf}-->`);
flushTable();
return { tables, hints };
}
export function parsePidMap(meta) {
if (!meta || typeof meta !== 'object') return {};
const m = meta.pidMap;
if (!m || typeof m !== 'object') return {};
return m;
}
export function parseMainPid(meta) {
if (!meta || typeof meta !== 'object') return null;
return meta.mainPid || null;
}
export function parseCandidates(meta) {
if (!meta || typeof meta !== 'object') return null;
return Array.isArray(meta.candidates) ? meta.candidates : null;
}
export function replacePlaceholders(md, pidMap, opts = {}) {
if (!md) return { text: md, warnings: [] };
if (!pidMap || typeof pidMap !== 'object') return { text: md, warnings: [] };
const warnings = [];
let text = String(md);
text = text.replace(/\[(?:NAME|STATION|CAR|TRAIN|TEAM):(emp_[a-z0-9]+)\]/gi, (m, pid) => {
const entry = pidMap[pid];
if (entry?.name) return entry.name;
warnings.push({ token: m, pid, kind: 'NAME' });
return m;
});
text = text.replace(/\[ID:(emp_[a-z0-9]+)\]/gi, (m, pid) => {
const entry = pidMap[pid];
if (entry?.id != null) return String(entry.id);
warnings.push({ token: m, pid, kind: 'ID' });
return m;
});
if (opts.replaceBare) {
text = text.replace(/\bemp_[a-z0-9]+\b/gi, (m) => {
const entry = pidMap[m];
if (entry?.id != null) return String(entry.id);
warnings.push({ token: m, pid: m, kind: 'BARE' });
return m;
});
}
return { text, warnings };
}
export function replacePlaceholdersText(md, pidMap, opts) {
return replacePlaceholders(md, pidMap, opts).text;
}
export function parseModules(md) {
const text = String(md || '').replace(/^\uFEFF/, '').trim();
if (!text) return { title: '', modules: [] };
const lines = text.split(/\r?\n/);
let title = '';
const raw = [];
let current = null;
for (const line of lines) {
if (/^#\s+[^#]/.test(line) && !current && raw.length === 0) {
title = line.replace(/^#\s+/, '').trim();
continue;
}
if (/^##\s+/.test(line)) {
if (current) raw.push(current);
current = { name: line.replace(/^##\s+/, '').trim(), lines: [] };
continue;
}
if (current) {
current.lines.push(line);
} else if (line.trim() && !title) {
current = { name: '概况', lines: [line] };
} else if (current === null && line.trim() && title) {
current = { name: title, lines: [line] };
title = '';
}
}
if (current) raw.push(current);
const modules = raw.map((block) => {
const { tables, hints } = splitModule(block.lines.join('\n'));
return { name: block.name, tables, hints };
}).filter((m) => m.tables.length > 0 || m.hints.length > 0 || m.name);
return { title, modules };
}
export function assembleModuleMarkdown(mod, analysis) {
const chunks = [];
if (mod?.name) chunks.push(`## ${mod.name}`);
for (const t of mod?.tables || []) {
if (t.title) chunks.push(`### ${t.title}`);
if (t.md) chunks.push(String(t.md).trim());
}
const a = String(analysis || '').trim();
if (a) chunks.push(a);
return chunks.join('\n\n');
}
export function assembleReportMarkdown(title, modulesWithAnalysis) {
const chunks = [];
if (title) chunks.push(`# ${title}`);
for (const item of modulesWithAnalysis || []) {
chunks.push(assembleModuleMarkdown(item.mod, item.analysis));
}
return chunks.filter(Boolean).join('\n\n').trim();
}
export function compactTablesForPrompt(tables, maxRows = 36) {
const blocks = [];
for (const t of tables || []) {
const md = String(t?.md || '').trim();
if (!md) continue;
const lines = md.split(/\r?\n/).filter((l) => l.trim());
if (!lines.length) continue;
const title = String(t.title || '').trim();
let body = lines;
if (lines.length > maxRows + 2) {
const head = lines.slice(0, 2);
const data = lines.slice(2);
const keep = Math.max(8, maxRows);
const headPart = data.slice(0, Math.ceil(keep * 0.7));
const tailPart = data.slice(-Math.floor(keep * 0.3));
const omitted = data.length - headPart.length - tailPart.length;
body = [
...head,
...headPart,
`| … | (其余 ${omitted} 行已省略,分析时勿臆造被省略行的具体内容) |`,
...tailPart,
];
}
blocks.push([
title ? `### ${title}` : '### 数据表',
body.join('\n'),
].join('\n'));
}
return blocks.join('\n\n').trim();
}
export function buildAnalysisSystemPrompt(opts = {}) {
const kind = opts.reportKind === 'team' ? 'team' : opts.reportKind === 'person' ? 'person' : '';
const isSingleEmployee = !!opts.mainPid;
const tokenConstraint = isSingleEmployee
? `【token 使用规范 - 单员工】
- 表格/HINT 里的 [NAME:emp_xxx] 是占位符,对应本次分析的员工(具体假名见用户消息)
- 引用员工时:可以用「该员工」代词,或直接复制 [NAME:emp_xxx] token,都接受
- 严禁编造姓名(如"张三""李四")
- 严禁拆解 token(如"emp_a3b8c1 可能是...")
- 严禁在输出里出现 emp_xxx 假名字面量(应输出 [NAME:emp_xxx] 或「该员工」)`
: `【token 使用规范 - 多员工对比】
- 当前上下文涉及多名员工,每人对应唯一的 [NAME:emp_xxx] 令牌
- 对比分析时,必须严格使用 [NAME:emp_xxx] 令牌指代(例:「[NAME:emp_a3b8c1] 绩效优于 [NAME:emp_f2c9d0]」)
- 严禁使用代词:「该员工」「前者」「后者」「第一名」等模糊指代,在多员工场景下会让用户混淆
- 严禁编造姓名:你看不到真实姓名,不要猜(如「张三」「李四」)
- 严禁拆解 token:不要解释 emp_xxx 可能对应谁
- token 是占位符,直接复制使用即可`;
const kindRule = kind === 'person'
? '\n【口径】全程保持个人口径,不要写「部分人员」「班组内」「排班策略」「人员调配」等班组管理表述。\n'
: '';
return `【模块分析任务 - 通用规则】
你是业务分析助理。界面已直渲报告表格;用户消息会给你同一份数据与提纲,请**基于表内数字与事实**写分析。
${tokenConstraint}
${kindRule}
排版(必须严格遵守):
用下面三个小标题,每个标题单独成行,标题用 **加粗**:
**总体表现**
(结合表内关键数字概括水平/对比/含义)
**主要问题**
(结合薄弱项、异常行、波动指出问题;无明显问题则写「本模块未见突出短板」并引用依据)
**改进建议**
- 建议一(具体、可执行,对应上面问题)
- 建议二
- 建议三(可选)
硬性要求:
1. 分析必须引用或呼应表里的具体数据(得分/人次/排名/分类名等),禁止空泛套话
2. 正文禁止输出任何表格符号(|)或 Markdown 表,禁止输出 # / ## / ###
3. 不要编造表与提纲之外的数字;全文简体中文
4. 若开启思考:只写对数据的解读与判断(因果/对比/风险),严禁复述输入清单、严禁约束自检清单`;
}
export function buildAnalysisPrompt(moduleName, hints, opts = {}) {
const list = (hints || []).map((h) => `- ${h}`).join('\n') || '- (本模块无额外提纲;请主要依据下方数据表分析)';
const kind = opts.reportKind === 'team' ? 'team' : opts.reportKind === 'person' ? 'person' : '';
const dataBlock = compactTablesForPrompt(opts.tables || []);
const extras = [];
if (opts.mainPid) {
extras.push(`本次分析的员工假名为 ${opts.mainPid}(对应表格/HINT 里的 [NAME:emp_xxx] 占位符)。`);
}
if (moduleName === '综合评价') {
extras.push('若提及前五/后五,只能写进散文句子(如「领先梯队有张三、李四…」),严禁输出任何排名表、名单表或 GFM 表格。');
if (kind === 'person') {
extras.push('本报告为**个人**综合评价:只写该员工定位/优势/业务短板与针对本人的建议;严禁班组人事管理话术(兼职借调清单、排班调配、谈心谈话机制、人员结构均衡、班组共性问题等)。');
}
}
if (kind === 'person' && (moduleName === '基本信息' || moduleName === '六维评分')) {
extras.push('备注仅可原样点出本人状态;无备注则不要提兼职/借调;禁止升维成班组共性问题或管理建议。');
}
const summaryExtra = extras.length
? `\n本模块特别约束:\n${extras.map((e) => `- ${e}`).join('\n')}\n`
: '';
const dataSection = dataBlock
? `【本模块数据(只读,界面已展示;禁止在回复中重打表格或复制 | 行)】\n${dataBlock}\n`
: '【本模块数据】暂无表格,仅依据提纲分析。\n';
return `请为模块「${moduleName}」写结构化分析(写透即可,不设字数上限)。
${summaryExtra}
${dataSection}
【分析要点(HINT,可与表对照,勿只复述 HINT)】
${list}
按 system 中【模块分析任务 - 通用规则】的三个加粗小标题与硬性要求输出,不要输出标题行或表格。`;
}
export function inferReportKind(title) {
const t = String(title || '');
if (/个人.*年度报告|个人报告/.test(t)) return 'person';
if (/班组.*年度报告|班组报告|班组概况/.test(t)) return 'team';
return '';
}
export function buildCrossModulePrompt(prevModules, reportKind) {
const kind = reportKind === 'team' ? 'team' : reportKind === 'person' ? 'person' : '';
const dataBlock = (prevModules || [])
.filter(m => m && m.name && m.name !== '综合评价')
.map(m => {
const hintsTxt = (m.hints || []).map(h => ` - ${h}`).join('\n');
const tablesTxt = compactTablesForPrompt(m.tables || [], 8);
return `### ${m.name}\n${hintsTxt ? `\n关键事实:\n${hintsTxt}` : ''}${tablesTxt ? `\n${tablesTxt}` : ''}`;
})
.join('\n\n---\n\n');
const scopeWord = kind === 'team' ? '该班组' : '该员工';
const scopeWordLine = kind === 'team' ? '全线路' : '';
return `【跨模块深度诊断】
你是 ${kind === 'team' ? '班组' : '员工'}能力评估专家。基于下方全部 ${prevModules?.length || 0} 个模块的事实摘要与表格,写一份跨模块深度诊断。
【分析对象】${scopeWord}${scopeWordLine ? `(${scopeWordLine})` : ''}本年度全部模块数据
【全部模块数据】
${dataBlock || '(暂无模块数据,请基于常识与口径要求输出谨慎分析)'}
【任务要求】
1. **跨模块关联**(必含 ≥2 个):
找出不同模块数据之间的关联。例:
- 公里数异常月份是否与考核扣分月份吻合
- 实操弱项类别与故障处置短板是否一致
- 六维评分弱项与对应模块明细是否对应
- 嘉奖记录与六维优势维度是否匹配
2. **根因分析**(必含):
对每个薄弱维度给出根因假设(基于数据,禁止臆造)
3. **优先级矩阵**(必须输出 2×2 真表格):
用「紧急重要」矩阵排序短板,**严格按下表模板填**(允许且只允许此表用 GFM 表格语法):
| 紧急度 重要性 | 高重要性 | 中低重要性 |
|----------------|---------|-----------|
| **高紧急度**(立即做) | {填短板A,引用数据} | {填短板B} |
| **低紧急度**(计划做) | {填短板C} | {填短板D} |
表格下方用 100-150 字说明:为什么这么排、先补哪个、预期收益
4. **风险预警**:
基于综合数据,识别 1-2 个下一年度风险点
【口径要求】(强制)
- 第三人称(${scopeWord}/姓名),严禁「您」「本人」「自己」
- 管理层视角,聚焦安全/合规/效能/队伍建设
- 诊断部分 1200-1500 字,自然语言段落(优先级矩阵表格除外)
- 每个论断必须引用至少 1 个数据点
- 不重复各模块已写过的描述,只写综合洞察
- 不输出排名表/名单表,前五后五只能写进散文句子里
- 除「优先级矩阵」表格之外,**禁止在正文其他位置输出任何表格符号 |**
排版(必须严格遵守):
用下面四个小标题,每个标题单独成行,标题用 **加粗**:
**跨模块关联**
(至少 2 条关联,每条引用数据,纯文字段落)
**根因分析**
(对每个弱项给根因假设,纯文字段落)
**优先级矩阵**
(必须输出上面的 2×2 GFM 表格 + 100-150 字说明)
**风险预警**
(下一年 1-2 个风险点,纯文字段落)
硬性要求:
1. 除「优先级矩阵」表格外,正文禁止输出 | 或 # / ## / ###
2. 全文简体中文
3. 不要复述模块已写过的描述
4. 若开启思考:思考过程**只写对数据的解读、因果推理、关联验证**,严禁复述约束/排版/格式/口径/检查要求,严禁自检清单`;
}