Files
digital-me-history/digital-me-backend/src/talents.js
T
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

167 lines
14 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.
// 天赋树 = Prompt + 模型行为的「控制平面」(不是游戏成长系统)。
// 主天赋(唯一) = 身份 + 知识边界 + 合规等级 + 默认模型;副天赋(≤N) = 风格/软技能,后置改写,永不改事实。
// 运行时: 主天赋 System Prompt → (可选)RAG → LLM 主回答 → 副天赋后置改写(合并 1 次) → Guardrail → 输出。
// 注册表按「数据表形状」写在代码里,留 DB 化的缝;主天赋 = profession(不另建一套身份)。
import { getProfession } from './professions.js';
// ── 职业类别 → 原型(决定主天赋的行为基线 + 默认模型)──
const ARCH_BY_CAT = { content: 'creator', pro: 'expert', health: 'expert', edu: 'teacher', art: 'artist', biz: 'expert', life: 'expert', sport: 'sport', craft: 'craft', beauty: 'craft', heritage: 'expert', aec: 'expert', other: 'expert' };
// 原型 → 主天赋行为基线(把旧的主树各节点效果,收敛成主天赋的固定行为)
const ARCH_BEHAVIOR = {
expert: '在你的专业领域内保持专业、准确、有深度;尽量基于知识库作答,资料不足就明说、绝不编造;复杂问题先简要拆解再给结论;主动提示适用边界与风险;结尾给一句最关键、可执行的结论。',
creator: '敏锐抓住受众关心的选题与新鲜角度;开头一两句就抛钩子抓注意力;信息密度高、少水话,给能直接用的干货;表达有节奏、有起伏;自然引导到关注/行动,不硬广。',
teacher: '由浅入深、拆成小步讲解;多用例子把抽象讲具体;判断对方水平并调整深度;发现错误温和纠正并讲清原因;讲完适时给一道小题检验;多鼓励、建立信心。',
artist: '聊到相关话题时展示并讲解你的作品;有鲜明的审美主张;把技法/工艺讲清楚、可上手;讲清作品背后的理念与"为什么";强化你独特的风格辨识度;自然引导到合作/约稿/收藏。',
sport: '把比赛/操作拆开复盘、找到关键;精准点出关键技术要点;给出可执行的训练/上分计划;帮对方调节心态、抗压;有感染力、点燃斗志。',
craft: '一步步手把手教、清晰可跟做;给出精确的参数/配方/规格;主动提醒常见坑与注意事项;按对方具体情况给定制方案;给出从入门到进阶的成长路线。',
};
// 原型 → 模型偏好(当前仅 qwen;留路由缝,将来按天赋/成本/返佣切换)
const ARCH_MODEL = { expert: ['qwen'], creator: ['qwen'], teacher: ['qwen'], artist: ['qwen'], sport: ['qwen'], craft: ['qwen'] };
// ── 合规等级 → 安全配置(safety_level 越高越严;接 Guardrail)──
const COMPLIANCE = {
none: { safety_level: 2, audit_required: false, block_keywords: [], disclaimer: '' },
soft: { safety_level: 5, audit_required: false, block_keywords: ['包治', '绝对有效', '100%有效', '保证治好'], disclaimer: '(以上为一般性建议,具体情况请结合你的实际或线下专业意见判断。)' },
strict: { safety_level: 9, audit_required: true, block_keywords: ['包赢', '一定胜诉', '保证治愈', '必胜', '稳赚', '包好', '绝对没问题'], disclaimer: '⚠️ 以上内容仅供参考,不构成正式的法律/医疗/心理诊疗意见;重要决策请咨询线下持牌专业人士。' },
};
// ── 主天赋(由 profession 派生,主天赋 = 身份单元)──
export function mainTalent(profId) {
const p = getProfession(profId);
if (!p) return { id: 'general', name: '通用分身', emoji: '🙂', type: 'main', level: 1, arch: 'expert', entity: '数字分身',
system_prompt: '像本人一样自然、真诚地聊天;不确定就如实说,不编造具体事实。',
rag: { required: false, top_k: 5, scope: [] }, model_preference: ['qwen'],
safety_level: 2, block_keywords: [], disclaimer: '', audit_required: false, compliance: 'none' };
const arch = ARCH_BY_CAT[p.cat] || 'expert';
const c = COMPLIANCE[p.compliance] || COMPLIANCE.none;
return {
id: p.id, name: p.name, emoji: p.emoji, type: 'main', level: 1, arch, entity: p.entity,
system_prompt: `你的职业身份:${p.name}。${ARCH_BEHAVIOR[arch]}`,
rag: { required: arch === 'expert' || p.compliance !== 'none', top_k: 5, scope: [] },
model_preference: ARCH_MODEL[arch] || ['qwen'],
safety_level: c.safety_level, block_keywords: c.block_keywords, disclaimer: c.disclaimer,
audit_required: c.audit_required, compliance: p.compliance || 'none',
};
}
// ── 副天赋注册表(通用、全部后置改写型 = 只碰成稿、绝不改事实)──
// subtype: style(语气) | constraint(结构约束) | hobby(前置注入,机制预留,MVP 未铺货)
// 副天赋 = 说话风格软技能。可多选(叠加);仅「对立性格」互斥(conflicts 双向声明)。
export const SUB_REGISTRY = [
// —— 语气/性格 ——
{ id: 'warm', name: '亲和温度', subtype: 'style', desc: '语气温和、有共情与人情味', rewrite_prompt: '语气更温和、有共情与人情味', conflicts: ['blunt'] },
{ id: 'blunt', name: '直率坦诚', subtype: 'style', desc: '有话直说、不绕弯子', rewrite_prompt: '更直率坦诚、有话直说、不绕弯子(但不失礼貌)', conflicts: ['warm'] },
{ id: 'humor', name: '幽默风趣', subtype: 'style', desc: '适度玩梗、轻松风趣', rewrite_prompt: '加入适度的幽默感与轻松的梗' },
{ id: 'playful', name: '俏皮活泼', subtype: 'style', desc: '语气俏皮、活泼跳脱', rewrite_prompt: '语气更俏皮活泼、跳脱一点', conflicts: ['calm', 'rigorous'] },
{ id: 'calm', name: '沉稳克制', subtype: 'style', desc: '沉稳、不煽情、稳重可靠', rewrite_prompt: '更沉稳克制、稳重可靠,不夸张不煽情', conflicts: ['playful', 'motivate'] },
{ id: 'motivate', name: '激励感染', subtype: 'style', desc: '有鼓励与感染力、让人想行动', rewrite_prompt: '语气更有鼓励和感染力,让人更想行动', conflicts: ['calm'] },
{ id: 'poetic', name: '文艺细腻', subtype: 'style', desc: '文艺、有画面感、细腻', rewrite_prompt: '表达更文艺细腻、有画面感' },
// —— 表达方式 ——
{ id: 'story', name: '会讲故事', subtype: 'style', desc: '用故事或比喻讲得更生动', rewrite_prompt: '善用故事与比喻,让表达更生动(不虚构关键事实)', conflicts: ['concise'] },
{ id: 'vivid', name: '口语生动', subtype: 'style', desc: '像真人说话、去书面腔', rewrite_prompt: '更口语、更像真人说话,去掉书面腔和 AI 味' },
{ id: 'socratic', name: '启发提问', subtype: 'style', desc: '多用反问、引导对方思考', rewrite_prompt: '适当用反问和启发式提问,引导对方自己想明白' },
{ id: 'signature', name: '人设强化', subtype: 'style', desc: '强化口头禅与说话腔调', rewrite_prompt: '强化你独特的口头禅与说话腔调,突出个人风格' },
// —— 结构/严谨(约束型)——
{ id: 'concise', name: '简洁利落', subtype: 'constraint', desc: '先给要点、去掉铺垫废话', rewrite_prompt: '更精简,先给要点、去掉铺垫与废话', conflicts: ['story', 'patient'] },
{ id: 'shortfirst', name: '结论先行', subtype: 'constraint', desc: '结论在前、理由在后', rewrite_prompt: '重排为结论先行:先给结论,再给理由' },
{ id: 'patient', name: '耐心细致', subtype: 'constraint', desc: '循循善诱、拆解得更细', rewrite_prompt: '更耐心细致、循循善诱,把步骤和道理拆解清楚', conflicts: ['concise'] },
{ id: 'rigorous', name: '严谨专业', subtype: 'constraint', desc: '措辞严谨、给依据不夸大', rewrite_prompt: '措辞更严谨专业、给出依据、不夸大不臆断', conflicts: ['playful'] },
{ id: 'datadriven', name: '有理有据', subtype: 'constraint', desc: '爱用数据、例证、引用', rewrite_prompt: '多用数据、具体例证和引用来支撑观点' },
// —— ★擅长深化(老板 2026-07-31「擅长再多点·现在就深化」):给三条【才轴】各补一项副天赋,让顶尖之才够得更高。
// ★向后兼容:老号没这三项 sub → 轴值不变 → 照命完全不变;只有【新点这三项】的人能把对应才轴顶得更高(hit+1)。
// ★gate 不受影响(全表 gate 都是「至少点1项」·加 sub 不改此语义)。挂进 dynasties.js AXES 的 wen/fa/wu。
{ id: 'eloquent', name: '辞采雄辩', subtype: 'style', desc: '出口成章、辞采华美、善用排比铺陈', rewrite_prompt: '辞采更华美讲究、善用排比铺陈与凝练警句,出口成章而不堆砌' },
{ id: 'analytical', name: '洞察缜密', subtype: 'constraint', desc: '洞察本质、条分缕析、逻辑缜密', rewrite_prompt: '更善于洞察本质、条分缕析地拆解问题,逻辑缜密、层层推进' },
{ id: 'bold', name: '果敢担当', subtype: 'style', desc: '敢为敢当、有决断魄力', rewrite_prompt: '更果敢有担当、有决断魄力,遇事敢拍板、扛得起', conflicts: ['patient'] },
// —— 治学之才(hobby 型:前置注入,不进说话风格改写)——
// ★ 第 17 项。★★ 它是【治学之才,不是说话风格】:量的是「你读通了什么」,不是「你怎么说」。
// · subtype:'hobby' ⇒ 走 injectBlock(前置注入 inject_prompt),【不进】 restyleInstruction
// (后者显式 `subtype !== 'hobby'` 排它)⇒ 它永远不改分身的说话风格、不改事实。
// · 没有 rewrite_prompt、没有 conflicts —— 它不与任何「说话风格」软技能互斥(正交)。
// ★★★ 它是 dynasties.js 第 7 轴「经学」的唯一 sub:jingxue 只此一项 ⇒ computeAxes 只吐两个值
// {40, 63}(binary:通经 63 / 不通经 40)。够跑西汉博士弟子的「通一藝」gate(63≥55 开、40<55 关),
// 撑不起卷88 四档(那要多 sub/分级阈值,见 dynasties.js 西汉 xuan.notDone)。
// ★ 画像依据(xihan-zheng.js JINGXUE_PORTRAITS,双证):董仲舒「下帷…三年不窺園」、
// 兒寬「帶經而鉏…以射策爲掌故」——「读通经义、持一家之说」正是这一类人。
{ id: 'jingxue_tong', name: '通经', subtype: 'hobby',
desc: '读通经义、能持一家之说(治学之才,非说话风格)',
inject_prompt: '你读通经义、持一家之说,可据经立论' },
];
const SUB_BY_ID = Object.fromEntries(SUB_REGISTRY.map((s) => [s.id, s]));
// 兼容保留(部分调用点仍引用):不再用作槽位上限,副天赋可多选。
export function maxSubs() { return SUB_REGISTRY.length; }
// 清洗用户提交的副天赋:只留合法 id、去重;对立性格互斥(保留先选,丢后来的冲突项)。不限数量。
export function validateSubs(ids) {
const seen = new Set(); const out = [];
for (const id of Array.isArray(ids) ? ids : []) {
const s = SUB_BY_ID[id];
if (!s || seen.has(id)) continue;
if ((s.conflicts || []).some((c) => seen.has(c))) continue; // 与已选对立 → 跳过
seen.add(id); out.push(id);
}
return out;
}
// User Talent Instance → 解析出 { main, subs[] }
export function resolveTalents(twin, level = 0) {
const main = mainTalent(twin.profession);
const ids = validateSubs(twin.talents?.sub_talents, level);
return { main, subs: ids.map((id) => SUB_BY_ID[id]) };
}
// ── 管线助手 ──
// 主天赋 → System Prompt 片段(身份+行为+强合规时的免责指令)
export function mainPromptBlock(main) {
let s = main.system_prompt;
if (main.disclaimer) s += `\n【合规】你处于${main.compliance === 'strict' ? '强' : '软'}合规身份:不做绝对化承诺,涉及专业判断时说明这只是参考;必要时在结尾附免责说明。`;
return s;
}
// 前置注入(hobby 型副天赋;MVP 未铺货 → 通常为空)
export function injectBlock(subs) {
const inj = subs.filter((s) => s.subtype === 'hobby' && s.inject_prompt).map((s) => '· ' + s.inject_prompt);
return inj.length ? '【兴趣/偏好】' + inj.join(';') : '';
}
// 后置改写指令(把所有 style/constraint 副天赋合并成 1 次改写;不改事实)
export function restyleInstruction(subs) {
const rw = subs.filter((s) => s.rewrite_prompt && s.subtype !== 'hobby').map((s) => '· ' + s.rewrite_prompt);
if (!rw.length) return '';
return `请在【绝对不改变任何事实、数字、结论、引用与专业判断】的前提下,只调整表达风格,重写下面这段回答:\n${rw.join('\n')}\n\n直接输出重写后的回答本身,不要加任何解释或前后缀。`;
}
// Guardrail:屏蔽词软化(总是)+ 按 safety 附免责(非破坏式)。
// includeDisclaimer=false 时跳过免责——用于「会话级一次性」:同一段对话只在第一句附,
// 别每句尾挂,否则把「另一个我」的身份/闲聊体验压成「法律咨询工具」(P0-1)。屏蔽词软化不受影响、始终生效。
export function guardrail(text, main, includeDisclaimer = true) {
let out = String(text || '');
for (const kw of main.block_keywords || []) {
if (out.includes(kw)) out = out.split(kw).join('(依法/依专业不宜绝对化)');
}
if (includeDisclaimer && main.disclaimer && main.safety_level >= 5) {
const head = main.disclaimer.replace(/^[⚠️\s]+/, '').slice(0, 10);
if (!out.includes(head)) out = out.trimEnd() + '\n\n' + main.disclaimer;
}
return out;
}
// GET /talents 的响应(主身份 + 副选择 + 成长解锁)
export function describeTalents(twin, level = 0, verified = false) {
const main = mainTalent(twin.profession);
const selected = validateSubs(twin.talents?.sub_talents, level);
return {
main: {
id: main.id, name: main.name, emoji: main.emoji, entity: main.entity, arch: main.arch,
compliance: main.compliance, safety_level: main.safety_level, rag_required: main.rag.required,
audit_required: main.audit_required, audit_ok: !main.audit_required || verified,
behavior: main.system_prompt,
},
subs: {
selected,
// 副天赋可多选;仅 conflicts 里的对立项互斥(前端据此置灰)。
options: SUB_REGISTRY.map((s) => ({ id: s.id, name: s.name, subtype: s.subtype, desc: s.desc, conflicts: s.conflicts || [] })),
},
growth: { level },
};
}