| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- const fs = require('fs');
- const path = require('path');
- const { okResult, errorResult } = require('../core/result-envelope');
- const { createRunDir, outputsRoot } = require('../core/output-paths');
- const { safeResult } = require('../core/shared-gateway');
- const PLAYBOOK_DIMENSIONS = [
- '客户分层', '开场白', '需求挖掘', '异议处理', '跟进节奏',
- '成交信号', '风险提示', '话术风格', '产品匹配', '转介绍',
- '复购维护', '竞争应对', '服务边界'
- ];
- const PLAYBOOK_KEYWORDS = {
- '客户分层': ['预算', '首付', '刚需', '改善', '投资'],
- '开场白': ['你好', '您好', '感谢', '介绍'],
- '需求挖掘': ['考虑', '想要', '需要', '关注', '看重'],
- '异议处理': ['但是', '不过', '担心', '顾虑', '再考虑'],
- '跟进节奏': ['明天', '下周', '到时候', '再联系', '约'],
- '成交信号': ['定', '签', '可以', '没问题', '确定'],
- '风险提示': ['注意', '提醒', '谨慎', '风险'],
- '话术风格': ['专业', '耐心', '直接', '细致'],
- '产品匹配': ['户型', '面积', '楼层', '朝向', '配套'],
- '转介绍': ['推荐', '介绍', '朋友', '亲戚'],
- '复购维护': ['后续', '服务', '维护', '长期'],
- '竞争应对': ['对比', '别家', '其他', '优势'],
- '服务边界': ['职责', '范围', '不包', '额外']
- };
- function brokerPlaybooksDir() {
- return path.join(outputsRoot(), 'broker-playbooks');
- }
- function ensureBrokerPlaybooksDir() {
- fs.mkdirSync(brokerPlaybooksDir(), { recursive: true });
- }
- function playbookFilePath(brokerUserId) {
- ensureBrokerPlaybooksDir();
- return path.join(brokerPlaybooksDir(), `${brokerUserId}.json`);
- }
- function readPlaybook(brokerUserId) {
- const filePath = playbookFilePath(brokerUserId);
- if (!fs.existsSync(filePath)) return null;
- try {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- } catch {
- return null;
- }
- }
- function writePlaybook(brokerUserId, playbook) {
- const filePath = playbookFilePath(brokerUserId);
- fs.writeFileSync(filePath, JSON.stringify(playbook, null, 2), 'utf8');
- return filePath;
- }
- function listMessageFiles() {
- const dir = path.join(outputsRoot(), 'messages');
- if (!fs.existsSync(dir)) return [];
- return fs.readdirSync(dir)
- .filter(f => f.startsWith('room-') && f.endsWith('.json'))
- .map(f => ({ name: f, path: path.join(dir, f), mtime: fs.statSync(path.join(dir, f)).mtime }))
- .sort((a, b) => b.mtime - a.mtime);
- }
- function extractRoomIdFromFileName(fileName) {
- const match = fileName.match(/^room-([^-]+)-/);
- return match ? match[1] : null;
- }
- function collectMessagesForBroker(brokerUserId, roomIds = null) {
- const files = listMessageFiles();
- const messages = [];
- for (const file of files) {
- const roomId = extractRoomIdFromFileName(file.name);
- if (roomIds && !roomIds.includes(roomId)) continue;
- try {
- const data = JSON.parse(fs.readFileSync(file.path, 'utf8'));
- if (!Array.isArray(data)) continue;
- for (const msg of data) {
- if (msg.senderId === brokerUserId) {
- messages.push({ ...msg, roomId });
- }
- }
- } catch {
- // ignore
- }
- }
- return messages;
- }
- function simpleKeywordPlaybook(messages) {
- const text = messages.map(m => m.content || '').join(' ');
- const playbook = {};
- for (const [dimension, keywords] of Object.entries(PLAYBOOK_KEYWORDS)) {
- const matched = keywords.filter(kw => text.includes(kw));
- if (matched.length) {
- playbook[dimension] = {
- inferred: true,
- keywords: [...new Set(matched)],
- note: `从 ${messages.length} 条消息中推断`
- };
- }
- }
- if (!Object.keys(playbook).length) {
- playbook.summary = { note: `收集了 ${messages.length} 条消息,未识别到明确 playbook 关键词` };
- }
- return playbook;
- }
- function buildPlaybookContext(brokerUserId, messages) {
- const text = messages.map(m => m.content || '').join('\n');
- return {
- brokerUserId,
- messageCount: messages.length,
- sampleMessages: messages.slice(0, 50),
- fullTextPreview: text.slice(0, 8000),
- suggestedDimensions: PLAYBOOK_DIMENSIONS,
- hint: '请基于 sampleMessages 和 fullTextPreview 分析顾问 playbook,返回 JSON 后调用 qiwei_save_broker_playbook 保存。'
- };
- }
- const qiweiPrepareBrokerPlaybook = safeResult(async function qiweiPrepareBrokerPlaybook(input = {}) {
- const brokerUserId = String(input.brokerUserId || input.brokerId || '').trim();
- if (!brokerUserId) return errorResult('缺少 brokerUserId');
- const roomIds = Array.isArray(input.roomIds) ? input.roomIds.map(String) : null;
- const messages = collectMessagesForBroker(brokerUserId, roomIds);
- const context = buildPlaybookContext(brokerUserId, messages);
- ensureBrokerPlaybooksDir();
- const runDir = createRunDir('broker-playbooks', `context-${brokerUserId}`);
- const filePath = path.join(runDir, `context-${brokerUserId}.json`);
- fs.writeFileSync(filePath, JSON.stringify(context, null, 2), 'utf8');
- return okResult({
- assistantMessage: `已为顾问 ${brokerUserId} 准备 playbook 分析上下文:共 ${messages.length} 条消息。`,
- summary: { brokerUserId, messageCount: messages.length },
- data: { context, contextFile: path.relative(outputsRoot(), filePath) },
- files: [filePath],
- nextActions: ['基于 context 分析后调用 qiwei_save_broker_playbook 保存']
- });
- });
- const qiweiDistillBroker = safeResult(async function qiweiDistillBroker(input = {}) {
- const brokerUserId = String(input.brokerUserId || input.brokerId || '').trim();
- if (!brokerUserId) return errorResult('缺少 brokerUserId');
- const aiMode = String(input.aiMode || '').trim();
- const roomIds = Array.isArray(input.roomIds) ? input.roomIds.map(String) : null;
- const messages = collectMessagesForBroker(brokerUserId, roomIds);
- if (aiMode === 'keyword') {
- const playbook = simpleKeywordPlaybook(messages);
- const saved = { brokerUserId, playbook, source: 'keyword', messageCount: messages.length, updatedAt: new Date().toISOString() };
- const filePath = writePlaybook(brokerUserId, saved);
- return okResult({
- assistantMessage: `关键词模式 playbook 蒸馏完成:${brokerUserId}。`,
- summary: { brokerUserId, messageCount: messages.length, dimensions: Object.keys(playbook) },
- data: saved,
- files: [filePath]
- });
- }
- const context = buildPlaybookContext(brokerUserId, messages);
- ensureBrokerPlaybooksDir();
- const runDir = createRunDir('broker-playbooks', `context-${brokerUserId}`);
- const filePath = path.join(runDir, `context-${brokerUserId}.json`);
- fs.writeFileSync(filePath, JSON.stringify(context, null, 2), 'utf8');
- return okResult({
- assistantMessage: `已为顾问 ${brokerUserId} 准备 playbook 分析上下文,请 Agent 分析后调用 qiwei_save_broker_playbook 保存。`,
- summary: { brokerUserId, messageCount: messages.length, requiresAgentAnalysis: true },
- data: { context, contextFile: path.relative(outputsRoot(), filePath), saveEndpoint: 'qiwei_save_broker_playbook' },
- files: [filePath],
- nextActions: ['分析 context 后调用 qiwei_save_broker_playbook']
- });
- });
- const qiweiSaveBrokerPlaybook = safeResult(async function qiweiSaveBrokerPlaybook(input = {}) {
- const brokerUserId = String(input.brokerUserId || input.brokerId || '').trim();
- const playbook = input.playbook || input.content;
- if (!brokerUserId) return errorResult('缺少 brokerUserId');
- if (!playbook || typeof playbook !== 'object') return errorResult('缺少 playbook(JSON)');
- const saved = {
- brokerUserId,
- playbook,
- source: input.source || 'agent',
- messageCount: input.messageCount || 0,
- updatedAt: new Date().toISOString()
- };
- const filePath = writePlaybook(brokerUserId, saved);
- return okResult({
- assistantMessage: `顾问 playbook 已保存:${brokerUserId}。`,
- summary: { brokerUserId, dimensions: Object.keys(playbook) },
- data: saved,
- files: [filePath]
- });
- });
- const qiweiBatchDistillBroker = safeResult(async function qiweiBatchDistillBroker(input = {}) {
- const brokerUserIds = Array.isArray(input.brokerUserIds) ? input.brokerUserIds.map(String).filter(Boolean) : [];
- if (!brokerUserIds.length) return errorResult('缺少 brokerUserIds');
- const results = [];
- for (const brokerUserId of brokerUserIds) {
- try {
- const result = await qiweiDistillBroker({ ...input, brokerUserId });
- results.push({ brokerUserId, status: result.status, summary: result.summary });
- } catch (error) {
- results.push({ brokerUserId, status: 'error', message: String(error && error.message ? error.message : error) });
- }
- }
- return okResult({
- assistantMessage: `批量 playbook 蒸馏完成:${results.filter(r => r.status === 'ok').length}/${results.length}。`,
- summary: { total: results.length, succeeded: results.filter(r => r.status === 'ok').length, failed: results.filter(r => r.status !== 'ok').length },
- data: { results }
- });
- });
- const qiweiBatchSaveBrokerPlaybook = safeResult(async function qiweiBatchSaveBrokerPlaybook(input = {}) {
- const items = Array.isArray(input.items) ? input.items : [];
- if (!items.length) return errorResult('缺少 items({ brokerUserId, playbook } 数组)');
- const results = [];
- for (const item of items) {
- try {
- const result = await qiweiSaveBrokerPlaybook({ brokerUserId: item.brokerUserId, playbook: item.playbook, source: item.source });
- results.push({ brokerUserId: item.brokerUserId, status: result.status, summary: result.summary });
- } catch (error) {
- results.push({ brokerUserId: item.brokerUserId, status: 'error', message: String(error && error.message ? error.message : error) });
- }
- }
- return okResult({
- assistantMessage: `批量 playbook 保存完成:${results.filter(r => r.status === 'ok').length}/${results.length}。`,
- summary: { total: results.length, succeeded: results.filter(r => r.status === 'ok').length, failed: results.filter(r => r.status !== 'ok').length },
- data: { results }
- });
- });
- const qiweiExportBrokerPlaybooks = safeResult(async function qiweiExportBrokerPlaybooks(input = {}) {
- let brokerUserIds = Array.isArray(input.brokerUserIds)
- ? input.brokerUserIds.map(String).filter(Boolean)
- : null;
- if (!brokerUserIds) {
- ensureBrokerPlaybooksDir();
- brokerUserIds = fs.readdirSync(brokerPlaybooksDir())
- .filter(f => f.endsWith('.json') && !f.startsWith('context-'))
- .map(f => f.replace(/\.json$/, ''));
- }
- if (!brokerUserIds.length) return errorResult('没有可导出的 playbook');
- let XLSX;
- try {
- XLSX = require('xlsx');
- } catch {
- return errorResult('当前包尚未安装 xlsx,无法导出 Excel;请运行 npm install xlsx');
- }
- const rows = [];
- for (const brokerUserId of brokerUserIds) {
- const data = readPlaybook(brokerUserId);
- if (!data || !data.playbook) continue;
- rows.push({ brokerUserId, ...flattenObject(data.playbook), updatedAt: data.updatedAt });
- }
- ensureBrokerPlaybooksDir();
- const fileName = `export-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.xlsx`;
- const filePath = path.join(brokerPlaybooksDir(), fileName);
- const worksheet = XLSX.utils.json_to_sheet(rows);
- const workbook = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(workbook, worksheet, 'playbooks');
- XLSX.writeFile(workbook, filePath);
- return okResult({
- assistantMessage: `已导出 ${rows.length} 条顾问 playbook:${filePath}。`,
- summary: { rowCount: rows.length },
- data: { filePath: path.relative(outputsRoot(), filePath) },
- files: [filePath]
- });
- });
- const qiweiGetBrokerPlaybook = safeResult(async function qiweiGetBrokerPlaybook(input = {}) {
- const brokerUserId = String(input.brokerUserId || input.brokerId || '').trim();
- if (!brokerUserId) return errorResult('缺少 brokerUserId');
- const playbook = readPlaybook(brokerUserId);
- if (!playbook) return errorResult(`顾问 ${brokerUserId} 暂无 playbook`);
- return okResult({
- assistantMessage: `已读取顾问 ${brokerUserId} 的 playbook。`,
- summary: { brokerUserId, dimensions: Object.keys(playbook.playbook || {}) },
- data: playbook
- });
- });
- function flattenObject(obj, prefix = '') {
- const result = {};
- for (const [key, value] of Object.entries(obj)) {
- const newKey = prefix ? `${prefix}.${key}` : key;
- if (value && typeof value === 'object' && !Array.isArray(value)) {
- Object.assign(result, flattenObject(value, newKey));
- } else {
- result[newKey] = Array.isArray(value) ? value.join(',') : value;
- }
- }
- return result;
- }
- module.exports = {
- qiweiDistillBroker,
- qiweiPrepareBrokerPlaybook,
- qiweiSaveBrokerPlaybook,
- qiweiBatchDistillBroker,
- qiweiBatchSaveBrokerPlaybook,
- qiweiExportBrokerPlaybooks,
- qiweiGetBrokerPlaybook
- };
|