393 lines
15 KiB
TypeScript
393 lines
15 KiB
TypeScript
// @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. 若开启思考:思考过程**只写对数据的解读、因果推理、关联验证**,严禁复述约束/排版/格式/口径/检查要求,严禁自检清单`;
|
||
}
|
||
|