fix: 品牌保护+知识库全量覆盖 - 6层防御解决传销问题 + 30+产品关键词补全
This commit is contained in:
@@ -1 +1 @@
|
|||||||
34689:1773367134016_22200_768qlmindc7g:1773378240856
|
34689:1773710589395_12316_18r6tb53xr5k:1773716299978
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"token": "47c96fae6ababba95306064d77f8cb9d0ab937523d5c08825e3061a7617b902d",
|
"token": "3a894ee27cf51fedbd1a75833a8f2fd16e58ff41a3795eba2c4a402543d044fe",
|
||||||
"expires_at": 1773378566,
|
"expires_at": 1773717943,
|
||||||
"card_type": 7,
|
"card_type": 7,
|
||||||
"card_expires_at": 1773883128,
|
"card_expires_at": 1773883128,
|
||||||
"created_at": 1773376766
|
"created_at": 1773716143
|
||||||
}
|
}
|
||||||
18
mcp-server-ssh/check_current.cjs
Normal file
18
mcp-server-ssh/check_current.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LAST 200 LINES server-out.log ==='",
|
||||||
|
"tail -200 /var/log/bigwo/server-out.log",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 50 LINES server-error.log ==='",
|
||||||
|
"tail -50 /var/log/bigwo/server-error.log 2>/dev/null || echo 'no error log'",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
23
mcp-server-ssh/check_double_msg.cjs
Normal file
23
mcp-server-ssh/check_double_msg.cjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LATEST SESSION FULL EVENTS ==='",
|
||||||
|
"LAST_SID=$(grep 'processReply start' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -v 'partial' | head -60",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== 公司产品 SESSION EVENTS ==='",
|
||||||
|
"grep '我们公司的产品\\|公司的产品' /var/log/bigwo/server-out.log | tail -5",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== EVENT 550/559/351 FOR LATEST SESSION ==='",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -E 'upstream assistant|flush|persistAssistant|subtitle.*assistant' | tail -20",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
11
mcp-server-ssh/check_dual_reply.cjs
Normal file
11
mcp-server-ssh/check_dual_reply.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
c.exec('grep -E "processReply|upstream assistant|persistAssistant|SayHello|polishForSpeech|local_tts|delivery|blockUpstream|suppress" /var/log/bigwo/server-out.log | tail -50', (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
17
mcp-server-ssh/check_dual_reply2.cjs
Normal file
17
mcp-server-ssh/check_dual_reply2.cjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LATEST SESSION FULL LOG ==='",
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | head -80",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
16
mcp-server-ssh/check_dual_reply3.cjs
Normal file
16
mcp-server-ssh/check_dual_reply3.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -E 'processReply|upstream assistant|persistAssist|suppress|blockUpstream|delivery|local_tts|unblock|tts_event|event.*550|event.*351|event.*559|barge|chunk' | tail -40",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
16
mcp-server-ssh/check_dual_reply4.cjs
Normal file
16
mcp-server-ssh/check_dual_reply4.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -v 'upstream partial' | tail -40",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
24
mcp-server-ssh/check_env.cjs
Normal file
24
mcp-server-ssh/check_env.cjs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== S2S ENV ==='",
|
||||||
|
"grep -E '^VOLC_S2S|^VOLC_DIALOG' /www/wwwroot/demo.tensorgrove.com.cn/server/.env | sed 's/=.*/=***MASKED***/'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== ALL VOLC ENV KEYS ==='",
|
||||||
|
"grep '^VOLC_' /www/wwwroot/demo.tensorgrove.com.cn/server/.env | sed 's/=.*/=.../'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== SESSIONS AFTER DEPLOY (17:06) ==='",
|
||||||
|
"awk '/17:0[6-9]|17:[1-5]/' /var/log/bigwo/server-out.log | grep -E 'upstream ready|upstream error|upstream closed|quota' | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== HEALTH ==='",
|
||||||
|
"curl -s http://127.0.0.1:3012/api/health 2>&1",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
11
mcp-server-ssh/check_greeting.cjs
Normal file
11
mcp-server-ssh/check_greeting.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
c.exec('tail -200 /var/log/bigwo/server-out.log | grep -iE "sendGreeting|replayGreeting|sendSpeechText|upstream ready|tts_event|upstream closed|greeting|ready"', (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
11
mcp-server-ssh/check_greeting2.cjs
Normal file
11
mcp-server-ssh/check_greeting2.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
c.exec('grep -E "sendGreeting|replayGreeting|upstream ready|greeting|readySent" /var/log/bigwo/server-out.log | tail -30', (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
11
mcp-server-ssh/check_greeting3.cjs
Normal file
11
mcp-server-ssh/check_greeting3.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
c.exec('grep "mmsuckxe" /var/log/bigwo/server-out.log | head -60', (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
21
mcp-server-ssh/check_greeting4.cjs
Normal file
21
mcp-server-ssh/check_greeting4.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
// Get the last session's full greeting timeline + audio forwarding evidence
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== GREETING LOGS ==='",
|
||||||
|
"grep -E 'sendGreeting|replayGreeting|sendSpeechText|tts_event|tts_reset|audio block|upstream ready' /var/log/bigwo/server-out.log | tail -30",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST SESSION FULL LOG (first 40 lines) ==='",
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | head -40",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
21
mcp-server-ssh/check_health.cjs
Normal file
21
mcp-server-ssh/check_health.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== HEALTH CHECK ==='",
|
||||||
|
"curl -s http://127.0.0.1:3012/api/health 2>&1",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== PM2 STATUS ==='",
|
||||||
|
"pm2 list 2>/dev/null",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 5 LINES ==='",
|
||||||
|
"tail -5 /var/log/bigwo/server-out.log",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
24
mcp-server-ssh/check_kb_env.cjs
Normal file
24
mcp-server-ssh/check_kb_env.cjs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== ENV KB CONFIG ==='",
|
||||||
|
"grep -E 'VOLC_ARK_ENDPOINT_ID|VOLC_ARK_KNOWLEDGE|VOLC_ARK_API_KEY|VOLC_ACCESS_KEY' /www/wwwroot/demo.tensorgrove.com.cn/server/.env | sed 's/=.\\{8\\}/=***REDACTED/g'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== RECENT KB LOGS (last 500 lines) ==='",
|
||||||
|
"tail -500 /var/log/bigwo/server-out.log | grep -E 'ToolExecutor|searchKnowledge|Ark KB|knowledge|processReply start|processReply handoff|external_rag|tts_reset|blockUpstream|unblock|barge-in|upstream assistant|upstream final|upstream partial'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== RECENT ERROR LOGS ==='",
|
||||||
|
"tail -100 /var/log/bigwo/server-error.log 2>/dev/null | grep -E 'ToolExecutor|Ark|knowledge|timeout|quota' | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== PM2 STATUS ==='",
|
||||||
|
"pm2 list 2>/dev/null || echo 'pm2 not found'",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
21
mcp-server-ssh/check_kb_issue.cjs
Normal file
21
mcp-server-ssh/check_kb_issue.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== RECENT KB RELATED LOGS ==='",
|
||||||
|
"grep -E 'searchKnowledge|Ark KB|rewritten|classifyKnowledge|tryKnowledge|shouldForce|Chat.*User|Chat.*Assistant|基础三合一|三合一|胡辣汤' /var/log/bigwo/server-out.log | tail -80",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== RECENT CHAT LOGS ==='",
|
||||||
|
"grep -E '\\[Chat\\]' /var/log/bigwo/server-out.log | tail -30",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== RECENT ERRORS ==='",
|
||||||
|
"grep -iE 'error|failed|timeout' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
27
mcp-server-ssh/check_kb_issue2.cjs
Normal file
27
mcp-server-ssh/check_kb_issue2.cjs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LAST 50 CHAT LOGS ==='",
|
||||||
|
"grep '\\[Chat\\]' /var/log/bigwo/server-out.log | tail -50",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 30 TOOLEXECUTOR LOGS ==='",
|
||||||
|
"grep '\\[ToolExecutor\\]' /var/log/bigwo/server-out.log | tail -30",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 20 COZE LOGS ==='",
|
||||||
|
"grep '\\[CozeChat\\]' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 20 ARK LOGS ==='",
|
||||||
|
"grep '\\[ArkChat\\]' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== .env KB CONFIG ==='",
|
||||||
|
"grep -E 'VOLC_ARK_KNOWLEDGE|VOLC_ARK_ENDPOINT|VOLC_ARK_API_KEY|COZE_' /www/wwwroot/demo.tensorgrove.com.cn/server/.env | sed 's/=.\\{12\\}/=***REDACTED/g'",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
24
mcp-server-ssh/check_kb_issue3.cjs
Normal file
24
mcp-server-ssh/check_kb_issue3.cjs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LAST 100 LINES WITH CHAT/KB ==='",
|
||||||
|
"grep -E '\\[Chat\\]|\\[ToolExecutor\\]|\\[CozeChat\\]|\\[ArkChat\\]|\\[POST\\]|\\[GET\\]' /var/log/bigwo/server-out.log | tail -100",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== SEARCH FOR 胡辣汤 ==='",
|
||||||
|
"grep -i '胡辣汤' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== SEARCH FOR 基础三合一 ==='",
|
||||||
|
"grep -i '基础三合一\\|基础套装' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== MOST RECENT 50 LINES ==='",
|
||||||
|
"tail -50 /var/log/bigwo/server-out.log",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
16
mcp-server-ssh/check_latency.cjs
Normal file
16
mcp-server-ssh/check_latency.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -v 'upstream partial' | grep -v 'upstream assistant chunk'",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
16
mcp-server-ssh/check_latency2.cjs
Normal file
16
mcp-server-ssh/check_latency2.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -v 'upstream partial' | grep -v 'upstream assistant chunk'",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
12
mcp-server-ssh/check_logs.cjs
Normal file
12
mcp-server-ssh/check_logs.cjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const { Client } = require("ssh2");
|
||||||
|
const c = new Client();
|
||||||
|
c.on("ready", () => {
|
||||||
|
// 搜索包含"产品"的日志行,以及 processReply/handoff 相关行
|
||||||
|
c.exec('tail -50 /var/log/bigwo/server-out.log | grep -E "greeting|upstream|tts_event|assistant|rag|ready|error"', (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = "";
|
||||||
|
s.on("data", (d) => (o += d));
|
||||||
|
s.stderr.on("data", (d) => (o += d));
|
||||||
|
s.on("close", () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: "119.45.10.34", port: 22, username: "root", password: "#xyzh%CS#2512@28" });
|
||||||
30
mcp-server-ssh/check_quota.cjs
Normal file
30
mcp-server-ssh/check_quota.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== ALL QUOTA ERRORS ==='",
|
||||||
|
"grep -i 'quota exceeded' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== QUOTA ERROR DETAILS (full payload) ==='",
|
||||||
|
"grep -i 'quota exceeded' /var/log/bigwo/server-out.log | tail -5 | grep -oP 'payload=\\K.*'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== S2S ENV CONFIG ==='",
|
||||||
|
"grep -E 'VOLC_S2S|VOLC_DIALOG|VOLC_RTC|VOLC_ACCESS' /www/wwwroot/demo.tensorgrove.com.cn/server/.env",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== RECENT UPSTREAM CONNECTIONS ==='",
|
||||||
|
"grep -E 'upstream ready|upstream closed|upstream error|createUpstream' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST ERROR LOG TIMESTAMPS ==='",
|
||||||
|
"grep -i 'quota exceeded' /var/log/bigwo/server-error.log | tail -5",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== TRY FRESH CONNECTION TEST ==='",
|
||||||
|
"curl -s http://127.0.0.1:3012/api/health 2>&1",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
30
mcp-server-ssh/check_quota2.cjs
Normal file
30
mcp-server-ssh/check_quota2.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== S2S ENV CONFIG (full) ==='",
|
||||||
|
"grep -E 'VOLC_S2S|VOLC_DIALOG|VOLC_RTC|VOLC_ACCESS' /www/wwwroot/demo.tensorgrove.com.cn/server/.env",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== ALL QUOTA ERRORS in server-error.log ==='",
|
||||||
|
"grep -i 'quota' /var/log/bigwo/server-error.log 2>/dev/null | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== ALL QUOTA ERRORS in server-out.log ==='",
|
||||||
|
"grep -i 'quota' /var/log/bigwo/server-out.log 2>/dev/null | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LAST 20 UPSTREAM EVENTS ==='",
|
||||||
|
"grep -E 'upstream ready|upstream closed|upstream error' /var/log/bigwo/server-out.log 2>/dev/null | tail -20",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== MOST RECENT ERROR LOG ==='",
|
||||||
|
"tail -20 /var/log/bigwo/server-error.log 2>/dev/null",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== MOST RECENT OUT LOG ==='",
|
||||||
|
"tail -20 /var/log/bigwo/server-out.log 2>/dev/null",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
21
mcp-server-ssh/check_speaker.cjs
Normal file
21
mcp-server-ssh/check_speaker.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== .env speaker/s2s config ==='",
|
||||||
|
"grep -i 'speaker\\|s2s\\|tts\\|voice\\|model' /www/wwwroot/demo.tensorgrove.com.cn/server/.env 2>/dev/null || echo 'no match'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== Latest session start payload ==='",
|
||||||
|
"grep -i 'buildStartSession\\|speaker\\|system_role\\|speaking_style\\|model.*version\\|session.*start' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== Current nativeVoiceGateway speaker line ==='",
|
||||||
|
"grep -n 'speaker' /www/wwwroot/demo.tensorgrove.com.cn/server/services/nativeVoiceGateway.js | head -5",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', d => o += d);
|
||||||
|
s.stderr.on('data', d => o += d);
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
20
mcp-server-ssh/check_voice.cjs
Normal file
20
mcp-server-ssh/check_voice.cjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== SPEAKER ENV ==='",
|
||||||
|
"grep -oP 'VOLC_S2S_SPEAKER_ID=\\K.*' /www/wwwroot/demo.tensorgrove.com.cn/server/.env",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== LATEST SESSION CONFIG ==='",
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Session: $LAST_SID\"",
|
||||||
|
"grep -E 'speaker|SayHello|tts_event|ttsType|voice|config' /var/log/bigwo/server-out.log | tail -20",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
21
mcp-server-ssh/check_voice2.cjs
Normal file
21
mcp-server-ssh/check_voice2.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== .env SPEAKER lines ==='",
|
||||||
|
"grep -i 'speaker' /www/wwwroot/demo.tensorgrove.com.cn/server/.env || echo 'NO SPEAKER in .env'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== .env S2S lines ==='",
|
||||||
|
"grep -i 's2s' /www/wwwroot/demo.tensorgrove.com.cn/server/.env || echo 'NO S2S in .env'",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== Session startup log (speaker) ==='",
|
||||||
|
"grep -i 'speaker\\|tts.*config\\|session.*start\\|StartSession' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
29
mcp-server-ssh/check_voice_issue.cjs
Normal file
29
mcp-server-ssh/check_voice_issue.cjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const c = new Client();
|
||||||
|
c.on('ready', () => {
|
||||||
|
const cmd = [
|
||||||
|
"echo '=== LAST SESSION VOICE LOGS ==='",
|
||||||
|
"LAST_SID=$(grep 'upstream ready' /var/log/bigwo/server-out.log | tail -1 | grep -oP 'session=\\K[^ ]+')",
|
||||||
|
"echo \"Last session: $LAST_SID\"",
|
||||||
|
"grep \"$LAST_SID\" /var/log/bigwo/server-out.log | grep -v 'partial' | tail -40",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== START SESSION PAYLOAD EVIDENCE ==='",
|
||||||
|
"grep -E 'buildStartSession|StartSession|model.*version|modelVersion|system_role' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== UPSTREAM ASSISTANT TEXT ==='",
|
||||||
|
"grep 'upstream assistant' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== 旅游/江西 RELATED ==='",
|
||||||
|
"grep -i '旅游\\|江西\\|景点' /var/log/bigwo/server-out.log | tail -10",
|
||||||
|
"echo ''",
|
||||||
|
"echo '=== processReply LOGS ==='",
|
||||||
|
"grep 'processReply' /var/log/bigwo/server-out.log | tail -15",
|
||||||
|
].join(' && ');
|
||||||
|
c.exec(cmd, (err, s) => {
|
||||||
|
if (err) { console.error(err); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log(o); c.end(); });
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
93
mcp-server-ssh/deploy_asr_alias_fix.cjs
Normal file
93
mcp-server-ssh/deploy_asr_alias_fix.cjs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SSH_CONFIG = {
|
||||||
|
host: '119.45.10.34',
|
||||||
|
port: 22,
|
||||||
|
username: 'root',
|
||||||
|
password: '#xyzh%CS#2512@28',
|
||||||
|
readyTimeout: 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROJECT = '/www/wwwroot/demo.tensorgrove.com.cn';
|
||||||
|
const LOCAL_BASE = path.join(__dirname, '..', 'test2');
|
||||||
|
const filesToDeploy = [
|
||||||
|
{
|
||||||
|
local: 'server/services/realtimeDialogRouting.js',
|
||||||
|
remote: `${PROJECT}/server/services/realtimeDialogRouting.js`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: 'server/services/nativeVoiceGateway.js',
|
||||||
|
remote: `${PROJECT}/server/services/nativeVoiceGateway.js`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function sshExec(command, timeout = 30000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
conn.end();
|
||||||
|
resolve({ stdout, stderr: stderr + '\n[TIMEOUT]', code: -1 });
|
||||||
|
}, timeout);
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.exec(command, (err, stream) => {
|
||||||
|
if (err) { clearTimeout(timer); conn.end(); return reject(err); }
|
||||||
|
stream.on('close', (code) => { clearTimeout(timer); conn.end(); resolve({ stdout, stderr, code }); });
|
||||||
|
stream.on('data', (d) => { stdout += d.toString(); });
|
||||||
|
stream.stderr.on('data', (d) => { stderr += d.toString(); });
|
||||||
|
});
|
||||||
|
}).on('error', (err) => { clearTimeout(timer); reject(err); }).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sshUpload(localPath, remotePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.sftp((err, sftp) => {
|
||||||
|
if (err) { conn.end(); return reject(err); }
|
||||||
|
const content = fs.readFileSync(localPath);
|
||||||
|
const ws = sftp.createWriteStream(remotePath);
|
||||||
|
ws.on('close', () => { conn.end(); resolve(); });
|
||||||
|
ws.on('error', (e) => { conn.end(); reject(e); });
|
||||||
|
ws.write(content);
|
||||||
|
ws.end();
|
||||||
|
});
|
||||||
|
}).on('error', (err) => reject(err)).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== 部署一成系统 ASR 别名修复 ===');
|
||||||
|
const backupDir = `${PROJECT}/server/_backup_${Date.now()}`;
|
||||||
|
await sshExec(`mkdir -p ${backupDir} && cp ${PROJECT}/server/services/realtimeDialogRouting.js ${PROJECT}/server/services/nativeVoiceGateway.js ${backupDir}/`);
|
||||||
|
console.log('备份目录:', backupDir);
|
||||||
|
|
||||||
|
for (const item of filesToDeploy) {
|
||||||
|
const localPath = path.join(LOCAL_BASE, item.local.replace(/\//g, path.sep));
|
||||||
|
await sshUpload(localPath, item.remote);
|
||||||
|
console.log('上传完成:', item.remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
const restart = await sshExec('pm2 restart bigwo-server 2>&1');
|
||||||
|
console.log(restart.stdout);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
const health = await sshExec('curl -s http://127.0.0.1:3012/api/health 2>&1');
|
||||||
|
console.log('Health:', health.stdout);
|
||||||
|
|
||||||
|
const pm2 = await sshExec('pm2 list 2>&1');
|
||||||
|
console.log(pm2.stdout);
|
||||||
|
|
||||||
|
const verify = await sshExec(`wc -l ${PROJECT}/server/services/realtimeDialogRouting.js ${PROJECT}/server/services/nativeVoiceGateway.js 2>&1`);
|
||||||
|
console.log(verify.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('Fatal:', e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
82
mcp-server-ssh/deploy_chat_switch_fix.cjs
Normal file
82
mcp-server-ssh/deploy_chat_switch_fix.cjs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SSH_CONFIG = {
|
||||||
|
host: '119.45.10.34',
|
||||||
|
port: 22,
|
||||||
|
username: 'root',
|
||||||
|
password: '#xyzh%CS#2512@28',
|
||||||
|
readyTimeout: 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROJECT = '/www/wwwroot/demo.tensorgrove.com.cn';
|
||||||
|
const LOCAL_FILE = path.join(__dirname, '..', 'test2', 'server', 'services', 'realtimeDialogRouting.js');
|
||||||
|
const REMOTE_FILE = `${PROJECT}/server/services/realtimeDialogRouting.js`;
|
||||||
|
|
||||||
|
function sshExec(command, timeout = 30000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
conn.end();
|
||||||
|
resolve({ stdout, stderr: stderr + '\n[TIMEOUT]', code: -1 });
|
||||||
|
}, timeout);
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.exec(command, (err, stream) => {
|
||||||
|
if (err) { clearTimeout(timer); conn.end(); return reject(err); }
|
||||||
|
stream.on('close', (code) => { clearTimeout(timer); conn.end(); resolve({ stdout, stderr, code }); });
|
||||||
|
stream.on('data', (d) => { stdout += d.toString(); });
|
||||||
|
stream.stderr.on('data', (d) => { stderr += d.toString(); });
|
||||||
|
});
|
||||||
|
}).on('error', (err) => { clearTimeout(timer); reject(err); }).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sshUpload(localPath, remotePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.sftp((err, sftp) => {
|
||||||
|
if (err) { conn.end(); return reject(err); }
|
||||||
|
const content = fs.readFileSync(localPath);
|
||||||
|
const ws = sftp.createWriteStream(remotePath);
|
||||||
|
ws.on('close', () => { conn.end(); resolve(); });
|
||||||
|
ws.on('error', (e) => { conn.end(); reject(e); });
|
||||||
|
ws.write(content);
|
||||||
|
ws.end();
|
||||||
|
});
|
||||||
|
}).on('error', (err) => reject(err)).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== 部署闲聊切换修复 ===');
|
||||||
|
const backupDir = `${PROJECT}/server/_backup_${Date.now()}`;
|
||||||
|
const backup = await sshExec(`mkdir -p ${backupDir} && cp ${REMOTE_FILE} ${backupDir}/`);
|
||||||
|
console.log('备份目录:', backupDir);
|
||||||
|
if (backup.stderr) console.log(backup.stderr);
|
||||||
|
|
||||||
|
await sshUpload(LOCAL_FILE, REMOTE_FILE);
|
||||||
|
console.log('上传完成:', REMOTE_FILE);
|
||||||
|
|
||||||
|
const restart = await sshExec('pm2 restart bigwo-server 2>&1');
|
||||||
|
console.log(restart.stdout);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
const health = await sshExec('curl -s http://127.0.0.1:3012/api/health 2>&1');
|
||||||
|
console.log('Health:', health.stdout);
|
||||||
|
|
||||||
|
const pm2 = await sshExec('pm2 list 2>&1');
|
||||||
|
console.log(pm2.stdout);
|
||||||
|
|
||||||
|
const verify = await sshExec(`wc -l ${REMOTE_FILE} 2>&1`);
|
||||||
|
console.log(verify.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('Fatal:', e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
51
mcp-server-ssh/deploy_fix.cjs
Normal file
51
mcp-server-ssh/deploy_fix.cjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const c = new Client();
|
||||||
|
const localFile = path.join(__dirname, '..', 'test2', 'server', 'services', 'toolExecutor.js');
|
||||||
|
const remotePath = '/www/wwwroot/demo.tensorgrove.com.cn/server/services/toolExecutor.js';
|
||||||
|
|
||||||
|
const content = fs.readFileSync(localFile, 'utf8');
|
||||||
|
|
||||||
|
c.on('ready', () => {
|
||||||
|
console.log('SSH connected, deploying toolExecutor.js...');
|
||||||
|
|
||||||
|
// Use SFTP to write the file
|
||||||
|
c.sftp((err, sftp) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('SFTP error:', err.message);
|
||||||
|
c.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeStream = sftp.createWriteStream(remotePath);
|
||||||
|
writeStream.on('close', () => {
|
||||||
|
console.log('File uploaded successfully.');
|
||||||
|
|
||||||
|
// Restart PM2
|
||||||
|
c.exec('pm2 restart bigwo-server && sleep 2 && pm2 logs bigwo-server --nostream --lines 10', (err, s) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Restart error:', err.message);
|
||||||
|
c.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => {
|
||||||
|
console.log('PM2 restart output:\n' + o);
|
||||||
|
c.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
console.error('Write error:', err.message);
|
||||||
|
c.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.write(content);
|
||||||
|
writeStream.end();
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
55
mcp-server-ssh/deploy_fixes.cjs
Normal file
55
mcp-server-ssh/deploy_fixes.cjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const c = new Client();
|
||||||
|
const REMOTE_BASE = '/www/wwwroot/demo.tensorgrove.com.cn/server/services';
|
||||||
|
|
||||||
|
// Read local files
|
||||||
|
const gatewayPath = path.join(__dirname, '..', 'test2', 'server', 'services', 'nativeVoiceGateway.js');
|
||||||
|
const toolExecPath = path.join(__dirname, '..', 'test2', 'server', 'services', 'toolExecutor.js');
|
||||||
|
|
||||||
|
const gatewayContent = fs.readFileSync(gatewayPath, 'utf8');
|
||||||
|
const toolExecContent = fs.readFileSync(toolExecPath, 'utf8');
|
||||||
|
|
||||||
|
console.log(`Gateway file size: ${gatewayContent.length} bytes`);
|
||||||
|
console.log(`ToolExecutor file size: ${toolExecContent.length} bytes`);
|
||||||
|
|
||||||
|
c.on('ready', () => {
|
||||||
|
console.log('SSH connected, starting SFTP...');
|
||||||
|
c.sftp((err, sftp) => {
|
||||||
|
if (err) { console.error('SFTP error:', err); c.end(); return; }
|
||||||
|
|
||||||
|
let done = 0;
|
||||||
|
const total = 2;
|
||||||
|
function checkDone() {
|
||||||
|
done++;
|
||||||
|
if (done >= total) {
|
||||||
|
console.log('All files uploaded. Restarting PM2...');
|
||||||
|
c.exec('cd /www/wwwroot/demo.tensorgrove.com.cn && pm2 restart bigwo-server && sleep 2 && pm2 logs bigwo-server --lines 20 --nostream', (err2, s) => {
|
||||||
|
if (err2) { console.error('Restart error:', err2); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => {
|
||||||
|
console.log('=== PM2 Restart Output ===');
|
||||||
|
console.log(o);
|
||||||
|
c.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload nativeVoiceGateway.js
|
||||||
|
const ws1 = sftp.createWriteStream(`${REMOTE_BASE}/nativeVoiceGateway.js`);
|
||||||
|
ws1.on('close', () => { console.log('✓ nativeVoiceGateway.js uploaded'); checkDone(); });
|
||||||
|
ws1.on('error', (e) => { console.error('Upload gateway error:', e); });
|
||||||
|
ws1.end(gatewayContent);
|
||||||
|
|
||||||
|
// Upload toolExecutor.js
|
||||||
|
const ws2 = sftp.createWriteStream(`${REMOTE_BASE}/toolExecutor.js`);
|
||||||
|
ws2.on('close', () => { console.log('✓ toolExecutor.js uploaded'); checkDone(); });
|
||||||
|
ws2.on('error', (e) => { console.error('Upload toolExec error:', e); });
|
||||||
|
ws2.end(toolExecContent);
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
30
mcp-server-ssh/deploy_voice_fix.cjs
Normal file
30
mcp-server-ssh/deploy_voice_fix.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const c = new Client();
|
||||||
|
const localFile = path.join(__dirname, '..', 'test2', 'server', 'services', 'nativeVoiceGateway.js');
|
||||||
|
const remotePath = '/www/wwwroot/demo.tensorgrove.com.cn/server/services/nativeVoiceGateway.js';
|
||||||
|
|
||||||
|
const content = fs.readFileSync(localFile, 'utf8');
|
||||||
|
|
||||||
|
c.on('ready', () => {
|
||||||
|
console.log('SSH connected, deploying nativeVoiceGateway.js...');
|
||||||
|
c.sftp((err, sftp) => {
|
||||||
|
if (err) { console.error('SFTP error:', err.message); c.end(); return; }
|
||||||
|
const ws = sftp.createWriteStream(remotePath);
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('File uploaded successfully.');
|
||||||
|
c.exec('pm2 restart bigwo-server && sleep 2 && pm2 logs bigwo-server --nostream --lines 5', (err, s) => {
|
||||||
|
if (err) { console.error('Restart error:', err.message); c.end(); return; }
|
||||||
|
let o = '';
|
||||||
|
s.on('data', (d) => (o += d));
|
||||||
|
s.stderr.on('data', (d) => (o += d));
|
||||||
|
s.on('close', () => { console.log('PM2 restart:\n' + o); c.end(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ws.on('error', (err) => { console.error('Write error:', err.message); c.end(); });
|
||||||
|
ws.write(content);
|
||||||
|
ws.end();
|
||||||
|
});
|
||||||
|
}).connect({ host: '119.45.10.34', port: 22, username: 'root', password: '#xyzh%CS#2512@28' });
|
||||||
141
mcp-server-ssh/deploy_voice_kb_fix.cjs
Normal file
141
mcp-server-ssh/deploy_voice_kb_fix.cjs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const { Client } = require('ssh2');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SSH_CONFIG = {
|
||||||
|
host: '119.45.10.34',
|
||||||
|
port: 22,
|
||||||
|
username: 'root',
|
||||||
|
password: '#xyzh%CS#2512@28',
|
||||||
|
readyTimeout: 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROJECT = '/www/wwwroot/demo.tensorgrove.com.cn';
|
||||||
|
const LOCAL_BASE = path.join(__dirname, '..', 'test2');
|
||||||
|
|
||||||
|
function sshExec(command, timeout = 30000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
conn.end();
|
||||||
|
resolve({ stdout, stderr: stderr + '\n[TIMEOUT]', code: -1 });
|
||||||
|
}, timeout);
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.exec(command, (err, stream) => {
|
||||||
|
if (err) { clearTimeout(timer); conn.end(); return reject(err); }
|
||||||
|
stream.on('close', (code) => { clearTimeout(timer); conn.end(); resolve({ stdout, stderr, code }); });
|
||||||
|
stream.on('data', (d) => { stdout += d.toString(); });
|
||||||
|
stream.stderr.on('data', (d) => { stderr += d.toString(); });
|
||||||
|
});
|
||||||
|
}).on('error', (err) => { clearTimeout(timer); reject(err); }).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sshUpload(localPath, remotePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
conn.on('ready', () => {
|
||||||
|
conn.sftp((err, sftp) => {
|
||||||
|
if (err) { conn.end(); return reject(err); }
|
||||||
|
const content = fs.readFileSync(localPath);
|
||||||
|
const ws = sftp.createWriteStream(remotePath);
|
||||||
|
ws.on('close', () => { conn.end(); resolve(); });
|
||||||
|
ws.on('error', (e) => { conn.end(); reject(e); });
|
||||||
|
ws.write(content);
|
||||||
|
ws.end();
|
||||||
|
});
|
||||||
|
}).on('error', (err) => reject(err)).connect(SSH_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToDeploy = [
|
||||||
|
// 服务端文件
|
||||||
|
{ local: 'server/services/toolExecutor.js', remote: `${PROJECT}/server/services/toolExecutor.js`, desc: '知识库回答精准度修复' },
|
||||||
|
{ local: 'server/services/nativeVoiceGateway.js', remote: `${PROJECT}/server/services/nativeVoiceGateway.js`, desc: '语音连接提前ready+超时兜底' },
|
||||||
|
// 客户端构建产物
|
||||||
|
{ local: 'client/dist/index.html', remote: `${PROJECT}/client/dist/index.html`, desc: '客户端入口' },
|
||||||
|
{ local: 'client/dist/assets/index-DFs3zFyd.css', remote: `${PROJECT}/client/dist/assets/index-DFs3zFyd.css`, desc: '客户端样式' },
|
||||||
|
{ local: 'client/dist/assets/index-DiJ8zsnj.js', remote: `${PROJECT}/client/dist/assets/index-DiJ8zsnj.js`, desc: '客户端JS(含getUserMedia并行化+超时)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== 语音通话延迟修复 + 知识库精准度修复 部署 ===\n');
|
||||||
|
|
||||||
|
// 1. 备份
|
||||||
|
console.log('--- 1. 备份服务器文件 ---');
|
||||||
|
const backupDir = `${PROJECT}/server/_backup_${Date.now()}`;
|
||||||
|
const backupCmd = [
|
||||||
|
`mkdir -p ${backupDir}`,
|
||||||
|
`cp ${PROJECT}/server/services/toolExecutor.js ${backupDir}/`,
|
||||||
|
`cp ${PROJECT}/server/services/nativeVoiceGateway.js ${backupDir}/`,
|
||||||
|
`mkdir -p ${backupDir}/client_dist_assets`,
|
||||||
|
`cp -r ${PROJECT}/client/dist/assets/* ${backupDir}/client_dist_assets/ 2>/dev/null || true`,
|
||||||
|
`cp ${PROJECT}/client/dist/index.html ${backupDir}/client_dist_index.html 2>/dev/null || true`,
|
||||||
|
].join(' && ');
|
||||||
|
const backupResult = await sshExec(backupCmd);
|
||||||
|
console.log(`备份目录: ${backupDir}`);
|
||||||
|
if (backupResult.stderr && !backupResult.stderr.includes('true')) console.log(backupResult.stderr);
|
||||||
|
|
||||||
|
// 2. 确保远程目录存在
|
||||||
|
console.log('\n--- 2. 确保目录存在 ---');
|
||||||
|
await sshExec(`mkdir -p ${PROJECT}/client/dist/assets`);
|
||||||
|
console.log('OK');
|
||||||
|
|
||||||
|
// 3. 清理旧的客户端资源文件
|
||||||
|
console.log('\n--- 3. 清理旧客户端资源 ---');
|
||||||
|
const cleanResult = await sshExec(`rm -f ${PROJECT}/client/dist/assets/index-*.js ${PROJECT}/client/dist/assets/index-*.css 2>&1`);
|
||||||
|
console.log(cleanResult.stdout || 'OK');
|
||||||
|
|
||||||
|
// 4. 上传文件
|
||||||
|
console.log('\n--- 4. 上传文件 ---');
|
||||||
|
for (const { local, remote, desc } of filesToDeploy) {
|
||||||
|
const localPath = path.join(LOCAL_BASE, local.replace(/\//g, path.sep));
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
console.log(`⚠️ 跳过 ${local} (本地文件不存在)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await sshUpload(localPath, remote);
|
||||||
|
const size = fs.statSync(localPath).size;
|
||||||
|
console.log(`✅ ${local} → ${remote} (${size}B) [${desc}]`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ ${local}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 重启 PM2
|
||||||
|
console.log('\n--- 5. 重启 PM2 ---');
|
||||||
|
const restart = await sshExec('pm2 restart bigwo-server 2>&1');
|
||||||
|
console.log(restart.stdout);
|
||||||
|
|
||||||
|
// 6. 等待检查
|
||||||
|
console.log('--- 6. 等待3秒后检查 ---');
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
const health = await sshExec('curl -s http://127.0.0.1:3012/api/health 2>&1');
|
||||||
|
console.log('Health:', health.stdout);
|
||||||
|
|
||||||
|
const pm2 = await sshExec('pm2 list 2>&1');
|
||||||
|
console.log(pm2.stdout);
|
||||||
|
|
||||||
|
// 7. 检查启动日志
|
||||||
|
console.log('--- 7. 启动日志 ---');
|
||||||
|
const logs = await sshExec('pm2 logs bigwo-server --nostream --lines 15 2>&1');
|
||||||
|
console.log(logs.stdout);
|
||||||
|
if (logs.stderr) console.log(logs.stderr);
|
||||||
|
|
||||||
|
// 8. 验证文件
|
||||||
|
console.log('--- 8. 验证文件行数 ---');
|
||||||
|
const wc = await sshExec(`wc -l ${PROJECT}/server/services/toolExecutor.js ${PROJECT}/server/services/nativeVoiceGateway.js 2>&1`);
|
||||||
|
console.log(wc.stdout);
|
||||||
|
|
||||||
|
const lsAssets = await sshExec(`ls -la ${PROJECT}/client/dist/assets/ 2>&1`);
|
||||||
|
console.log('客户端资源文件:');
|
||||||
|
console.log(lsAssets.stdout);
|
||||||
|
|
||||||
|
console.log('\n=== 部署完成 ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => { console.error('Fatal:', e.message); process.exit(1); });
|
||||||
87
mcp-server-ssh/e2e_test.cjs
Normal file
87
mcp-server-ssh/e2e_test.cjs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const BASE = 'https://demo.tensorgrove.com.cn';
|
||||||
|
|
||||||
|
function post(path, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(path, BASE);
|
||||||
|
const data = JSON.stringify(body);
|
||||||
|
const req = https.request(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
||||||
|
timeout: 60000,
|
||||||
|
}, (res) => {
|
||||||
|
let chunks = '';
|
||||||
|
res.on('data', (d) => chunks += d);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(chunks)); } catch { resolve(chunks); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const TESTS = [
|
||||||
|
{ q: '我们公司的产品有哪些', expect: 'kb', desc: '泛产品查询' },
|
||||||
|
{ q: '基础三合一是什么', expect: 'kb', desc: '基础三合一' },
|
||||||
|
{ q: '火炉原理是什么', expect: 'kb', desc: '火炉原理' },
|
||||||
|
{ q: '一程系统的基础三合一是什么', expect: 'kb', desc: '一程系统基础三合一' },
|
||||||
|
{ q: '我们公司卖手机和平板吗', expect: 'chat', desc: '手机平板(应走Coze)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
// Create a session first
|
||||||
|
let sessionId;
|
||||||
|
try {
|
||||||
|
const startRes = await post('/api/chat/start', { userId: 'e2e_test_' + Date.now() });
|
||||||
|
sessionId = startRes.sessionId;
|
||||||
|
console.log('Session:', sessionId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create session:', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const test of TESTS) {
|
||||||
|
try {
|
||||||
|
const sid = 'e2e_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||||
|
await post('/api/chat/start', { sessionId: sid });
|
||||||
|
const res = await post('/api/chat/send', { sessionId: sid, message: test.q });
|
||||||
|
const reply = (res.data && res.data.content) || res.reply || '';
|
||||||
|
const replyShort = reply.slice(0, 100);
|
||||||
|
|
||||||
|
// Check for hallucination indicators
|
||||||
|
const hasHallucination = /(手机|平板|笔记本|智能手表|护肤品|彩妆|香水|化妆品|电视|冰箱|洗衣机)/i.test(reply);
|
||||||
|
const hasPMContent = /(PM|FitLine|细胞营养|Activize|Basics|Restorate|NTC|火炉|阿育吠陀|一成系统|基础套装|大白|小红|小白|儿童倍适|营养)/i.test(reply);
|
||||||
|
|
||||||
|
let status = '✅';
|
||||||
|
if (test.expect === 'kb' && hasHallucination) status = '❌幻觉';
|
||||||
|
else if (test.expect === 'kb' && !hasPMContent) status = '⚠️无PM内容';
|
||||||
|
|
||||||
|
results.push({ desc: test.desc, q: test.q, status, reply: replyShort });
|
||||||
|
console.log(`${status} [${test.desc}] ${test.q} → ${replyShort}`);
|
||||||
|
} catch (e) {
|
||||||
|
results.push({ desc: test.desc, q: test.q, status: '❌ERROR', reply: e.message });
|
||||||
|
console.log(`❌ [${test.desc}] ${test.q} → ERROR: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== 测试总结 ===');
|
||||||
|
const pass = results.filter(r => r.status === '✅').length;
|
||||||
|
const warn = results.filter(r => r.status.startsWith('⚠️')).length;
|
||||||
|
const fail = results.filter(r => r.status.startsWith('❌')).length;
|
||||||
|
console.log(`通过: ${pass}, 警告: ${warn}, 失败: ${fail}, 总计: ${results.length}`);
|
||||||
|
|
||||||
|
if (warn + fail > 0) {
|
||||||
|
console.log('\n=== 问题详情 ===');
|
||||||
|
results.filter(r => r.status !== '✅').forEach(r => {
|
||||||
|
console.log(`${r.status} [${r.desc}] ${r.q}`);
|
||||||
|
console.log(` 回答: ${r.reply}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests().catch(e => console.error('Fatal:', e));
|
||||||
156
mcp-server-ssh/verify_scenarios.cjs
Normal file
156
mcp-server-ssh/verify_scenarios.cjs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* 本地模拟多场景验证:toolExecutor.js 修复后的路由和分类逻辑
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 模拟环境变量(模拟服务器配置)
|
||||||
|
process.env.VOLC_ARK_ENDPOINT_ID = 'ep-xxx';
|
||||||
|
process.env.VOLC_ARK_API_KEY = 'fake-key';
|
||||||
|
process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS = 'kb-f94cc30193b3b707,kb-a69b0928e1714de7,kb-a6c238e38ca81478,kb-149300b22195d2bf,kb-c7e7cd2bf0580fa0,kb-177fb1d978d88e0e';
|
||||||
|
process.env.VOLC_ARK_KNOWLEDGE_BASE_ROUTING = JSON.stringify([
|
||||||
|
{"name":"product","dataset_ids":["kb-f94cc30193b3b707"],"keywords":["pm产品","fitline","pm-fitline","产品","细胞营养素","activize","oxyplus","basics","restorate","儿童倍适","ntc","营养保送","小红产品","大白产品","小白产品","火炉原理","阿育吠陀","成分","功效","吃法","用法","服用","好转反应","活动","促销","分数"]},
|
||||||
|
{"name":"company","dataset_ids":["kb-a69b0928e1714de7"],"keywords":["pm公司","德国pm公司","公司","地址","电话","联系方式","实力","背景","成立","总部","分公司"]},
|
||||||
|
{"name":"training","dataset_ids":["kb-a6c238e38ca81478"],"keywords":["培训","新人","起步三关","精品会议","会议组织","成长上总裁","团队培训","新人入门"]},
|
||||||
|
{"name":"investment","dataset_ids":["kb-149300b22195d2bf"],"keywords":["招商","代理","代理商","合作","加盟","招募","事业机会","创业","招商稿"]},
|
||||||
|
{"name":"system","dataset_ids":["kb-c7e7cd2bf0580fa0"],"keywords":["一成系统","ai众享","数字化工作室","盛咖学愿","系统","邀约话术","文化解析","ai赋能","团队发展"]},
|
||||||
|
{"name":"general","dataset_ids":["kb-177fb1d978d88e0e"],"keywords":["一成ai","ai落地","转观念","对比","综合知识库"]}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 加载修复后的 toolExecutor
|
||||||
|
const ToolExecutor = require(path.join(__dirname, '..', 'test2', 'server', 'services', 'toolExecutor'));
|
||||||
|
|
||||||
|
console.log('====== 多场景逻辑验证 ======\n');
|
||||||
|
|
||||||
|
// 场景定义
|
||||||
|
const scenarios = [
|
||||||
|
{
|
||||||
|
name: '场景1: 德国PM基础三合一(截图问题)',
|
||||||
|
query: '给我介绍一下德国PM的基础三合一吧?',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'product',
|
||||||
|
expectDeterministic: '德国PM细胞营养素 基础套装 大白 小红 小白',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景2: 我们公司的产品(上下文有基础套装)',
|
||||||
|
query: '介绍一下我们公司的产品。',
|
||||||
|
context: [
|
||||||
|
{ role: 'user', content: '基础三合一是什么?' },
|
||||||
|
{ role: 'assistant', content: '德国PM细胞营养素基础套装包括大白、小红、小白三款产品。' },
|
||||||
|
],
|
||||||
|
expectRoute: 'product',
|
||||||
|
expectDeterministic: '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景3: 德国PM公司介绍(纯公司查询)',
|
||||||
|
query: '介绍一下德国PM公司',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'company',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景4: 德国PM产品(产品+德国PM混合)',
|
||||||
|
query: '德国PM的产品有哪些',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'product',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景5: 一成系统(系统优先路由)',
|
||||||
|
query: '一成系统是什么',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景6: 河南胡辣汤(闲聊,不走知识库)',
|
||||||
|
query: '河南胡辣汤',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'default_or_none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景7: 追问"这个怎么用"(上下文有小红产品)',
|
||||||
|
query: '这个怎么用',
|
||||||
|
context: [
|
||||||
|
{ role: 'user', content: '小红产品是什么?' },
|
||||||
|
{ role: 'assistant', content: 'Activize Oxyplus 是...' },
|
||||||
|
],
|
||||||
|
expectDeterministic: 'Fitline小红产品提升能量原理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景8: classifyKnowledgeAnswer - 未命中模式1',
|
||||||
|
query: 'test',
|
||||||
|
classifyContent: '关于PM细胞营养素基础套装,目前我这边没有找到这三个产品的具体成分和功效说明。建议你直接查看产品包装上的详细信息,或联系官方客服获取准确的使用指导。',
|
||||||
|
expectHit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景9: classifyKnowledgeAnswer - 未命中模式2',
|
||||||
|
query: 'test',
|
||||||
|
classifyContent: '我没有找到德国PM细胞营养素基础三合一的相关介绍。你是想了解它的成分、功效,还是使用方法呢?',
|
||||||
|
expectHit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景10: classifyKnowledgeAnswer - 正常命中',
|
||||||
|
query: 'test',
|
||||||
|
classifyContent: 'PM细胞营养素基础套装包含大白Basics、小红Activize和小白Restorate三款产品,通过NTC营养保送系统发挥协同作用。',
|
||||||
|
expectHit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景11: 招商合作',
|
||||||
|
query: '怎么加盟代理',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'investment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '场景12: 新人培训',
|
||||||
|
query: '新人起步三关是什么',
|
||||||
|
context: [],
|
||||||
|
expectRoute: 'training',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const s of scenarios) {
|
||||||
|
console.log(`--- ${s.name} ---`);
|
||||||
|
|
||||||
|
// 测试 classifyKnowledgeAnswer
|
||||||
|
if (s.classifyContent !== undefined) {
|
||||||
|
const result = ToolExecutor.classifyKnowledgeAnswer(s.query, s.classifyContent);
|
||||||
|
const ok = result.hit === s.expectHit;
|
||||||
|
console.log(` classify hit=${result.hit} reason=${result.reason} ${ok ? '✅' : '❌ EXPECTED hit=' + s.expectHit}`);
|
||||||
|
if (ok) passed++; else failed++;
|
||||||
|
console.log('');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试路由
|
||||||
|
const context = s.context || [];
|
||||||
|
const normalizedQuery = ToolExecutor.normalizeKnowledgeQueryAlias(s.query);
|
||||||
|
const anchored = ToolExecutor.applyKnowledgeQueryAnchor(normalizedQuery);
|
||||||
|
const deterministicQuery = ToolExecutor.buildDeterministicKnowledgeQuery(anchored, context);
|
||||||
|
const effectiveQuery = deterministicQuery || anchored;
|
||||||
|
const kbTarget = ToolExecutor.selectKnowledgeBaseTargets(effectiveQuery, context);
|
||||||
|
|
||||||
|
console.log(` normalized: "${normalizedQuery}"`);
|
||||||
|
console.log(` deterministic: "${deterministicQuery}"`);
|
||||||
|
console.log(` effective: "${effectiveQuery}"`);
|
||||||
|
console.log(` routes: [${kbTarget.matchedRoutes.join(', ')}]`);
|
||||||
|
console.log(` datasets: [${kbTarget.datasetIds.join(', ')}]`);
|
||||||
|
|
||||||
|
if (s.expectDeterministic) {
|
||||||
|
const ok = deterministicQuery === s.expectDeterministic;
|
||||||
|
console.log(` deterministic check: ${ok ? '✅' : '❌ EXPECTED "' + s.expectDeterministic + '"'}`);
|
||||||
|
if (ok) passed++; else failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.expectRoute) {
|
||||||
|
const ok = s.expectRoute === 'default_or_none'
|
||||||
|
? kbTarget.matchedRoutes.includes('default') || kbTarget.matchedRoutes.length === 0
|
||||||
|
: kbTarget.matchedRoutes.includes(s.expectRoute);
|
||||||
|
console.log(` route check: ${ok ? '✅' : '❌ EXPECTED route=' + s.expectRoute}`);
|
||||||
|
if (ok) passed++; else failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n====== 结果:${passed} 通过, ${failed} 失败 ======`);
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
@@ -11,28 +11,19 @@ export function useNativeVoiceChat() {
|
|||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const sessionRef = useRef(null);
|
const sessionRef = useRef(null);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
const greetingUtteranceRef = useRef(null);
|
const greetingFallbackTimerRef = useRef(null);
|
||||||
|
const greetingAudioDetectedRef = useRef(false);
|
||||||
|
|
||||||
const stopGreeting = useCallback(() => {
|
const clearGreetingFallback = useCallback(() => {
|
||||||
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
if (greetingFallbackTimerRef.current) {
|
||||||
window.speechSynthesis.cancel();
|
clearTimeout(greetingFallbackTimerRef.current);
|
||||||
|
greetingFallbackTimerRef.current = null;
|
||||||
}
|
}
|
||||||
greetingUtteranceRef.current = null;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const playGreeting = useCallback((text) => {
|
const stopGreeting = useCallback(() => {
|
||||||
const greetingText = String(text || '').trim();
|
clearGreetingFallback();
|
||||||
if (!greetingText || typeof window === 'undefined' || !('speechSynthesis' in window) || typeof window.SpeechSynthesisUtterance === 'undefined') {
|
}, [clearGreetingFallback]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopGreeting();
|
|
||||||
const utterance = new window.SpeechSynthesisUtterance(greetingText);
|
|
||||||
utterance.lang = 'zh-CN';
|
|
||||||
utterance.rate = 1;
|
|
||||||
utterance.pitch = 1;
|
|
||||||
greetingUtteranceRef.current = utterance;
|
|
||||||
window.speechSynthesis.speak(utterance);
|
|
||||||
}, [stopGreeting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
nativeVoiceService.on('onSubtitle', (subtitle) => {
|
nativeVoiceService.on('onSubtitle', (subtitle) => {
|
||||||
@@ -45,21 +36,60 @@ export function useNativeVoiceChat() {
|
|||||||
const finals = prev.filter((s) => s.isFinal);
|
const finals = prev.filter((s) => s.isFinal);
|
||||||
return [...finals, subtitle];
|
return [...finals, subtitle];
|
||||||
});
|
});
|
||||||
|
if (subtitle?.role === 'assistant' && subtitle?.isFinal && /^greeting_/.test(String(subtitle.sequence || ''))) {
|
||||||
|
clearGreetingFallback();
|
||||||
|
greetingAudioDetectedRef.current = false;
|
||||||
|
greetingFallbackTimerRef.current = setTimeout(() => {
|
||||||
|
if (!greetingAudioDetectedRef.current) {
|
||||||
|
nativeVoiceService.requestGreetingReplay();
|
||||||
|
}
|
||||||
|
greetingFallbackTimerRef.current = null;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
nativeVoiceService.on('onConnectionStateChange', setConnectionState);
|
nativeVoiceService.on('onConnectionStateChange', setConnectionState);
|
||||||
nativeVoiceService.on('onError', (err) => setError(err?.message || 'Native voice error'));
|
nativeVoiceService.on('onError', (err) => setError(err?.message || 'Native voice error'));
|
||||||
|
nativeVoiceService.on('onDiagnostic', (diag) => {
|
||||||
|
if (!diag) return;
|
||||||
|
if (diag.type === 'audio_chunk') {
|
||||||
|
greetingAudioDetectedRef.current = true;
|
||||||
|
clearGreetingFallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (diag.type === 'ws_message' && diag.payload?.type === 'tts_event' && diag.payload?.payload?.tts_type === 'chat_tts_text') {
|
||||||
|
greetingAudioDetectedRef.current = true;
|
||||||
|
clearGreetingFallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nativeVoiceService.on('onIdleTimeout', (timeout) => {
|
||||||
|
const mins = Math.round((timeout || 300000) / 60000);
|
||||||
|
console.log(`[useNativeVoiceChat] Idle timeout (${mins}min), auto disconnecting`);
|
||||||
|
setError(`通话已空闲${mins}分钟,已自动退出`);
|
||||||
|
nativeVoiceService.disconnect();
|
||||||
|
setIsActive(false);
|
||||||
|
setIsMuted(false);
|
||||||
|
setConnectionState('disconnected');
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
sessionRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stopGreeting();
|
stopGreeting();
|
||||||
nativeVoiceService.disconnect();
|
nativeVoiceService.disconnect();
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
};
|
};
|
||||||
}, [stopGreeting]);
|
}, [clearGreetingFallback, stopGreeting]);
|
||||||
|
|
||||||
const start = useCallback(async (options = {}) => {
|
const start = useCallback(async (options = {}) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
|
greetingAudioDetectedRef.current = false;
|
||||||
|
clearGreetingFallback();
|
||||||
|
stopGreeting();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userId = `user_${Date.now().toString(36)}`;
|
const userId = `user_${Date.now().toString(36)}`;
|
||||||
@@ -74,12 +104,12 @@ export function useNativeVoiceChat() {
|
|||||||
speakingStyle: options.speakingStyle,
|
speakingStyle: options.speakingStyle,
|
||||||
modelVersion: options.modelVersion,
|
modelVersion: options.modelVersion,
|
||||||
speaker: options.speaker,
|
speaker: options.speaker,
|
||||||
|
greetingText: options.greetingText,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
setSubtitles([]);
|
setSubtitles([]);
|
||||||
setDuration(0);
|
setDuration(0);
|
||||||
playGreeting(options.greetingText);
|
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
setDuration((d) => d + 1);
|
setDuration((d) => d + 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -93,7 +123,7 @@ export function useNativeVoiceChat() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [clearGreetingFallback, stopGreeting]);
|
||||||
|
|
||||||
const stop = useCallback(async () => {
|
const stop = useCallback(async () => {
|
||||||
let result = { sessionId: null, subtitles: [] };
|
let result = { sessionId: null, subtitles: [] };
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class NativeVoiceService {
|
|||||||
onError: null,
|
onError: null,
|
||||||
onAssistantPending: null,
|
onAssistantPending: null,
|
||||||
onDiagnostic: null,
|
onDiagnostic: null,
|
||||||
|
onIdleTimeout: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ class NativeVoiceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect({ sessionId, userId, botName, systemRole, speakingStyle, modelVersion, speaker }) {
|
async connect({ sessionId, userId, botName, systemRole, speakingStyle, modelVersion, speaker, greetingText }) {
|
||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
const wsUrl = this.resolveWebSocketUrl(sessionId, userId);
|
const wsUrl = this.resolveWebSocketUrl(sessionId, userId);
|
||||||
this.emitConnectionState('connecting');
|
this.emitConnectionState('connecting');
|
||||||
@@ -86,6 +87,22 @@ class NativeVoiceService {
|
|||||||
}
|
}
|
||||||
this.playbackTime = this.playbackContext.currentTime;
|
this.playbackTime = this.playbackContext.currentTime;
|
||||||
|
|
||||||
|
// 并行: 同时预获取麦克风和建立WS连接,节省500ms+
|
||||||
|
const micPromise = navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
channelCount: 1,
|
||||||
|
noiseSuppression: true,
|
||||||
|
echoCancellation: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
},
|
||||||
|
video: false,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('[NativeVoice] Pre-fetch getUserMedia failed:', err.message);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const CONNECTION_TIMEOUT_MS = 12000;
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
this.readyResolver = resolve;
|
this.readyResolver = resolve;
|
||||||
this.readyRejector = reject;
|
this.readyRejector = reject;
|
||||||
@@ -93,6 +110,18 @@ class NativeVoiceService {
|
|||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
|
||||||
|
// 超时兜底:避免无限等待
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (this.readyResolver) {
|
||||||
|
console.warn(`[NativeVoice] Connection timeout (${CONNECTION_TIMEOUT_MS}ms), forcing ready`);
|
||||||
|
this.readyResolver();
|
||||||
|
this.readyResolver = null;
|
||||||
|
this.readyRejector = null;
|
||||||
|
}
|
||||||
|
}, CONNECTION_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const clearTimeoutOnSettle = () => clearTimeout(timeoutId);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
this.emitConnectionState('connected');
|
this.emitConnectionState('connected');
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@@ -104,10 +133,12 @@ class NativeVoiceService {
|
|||||||
speakingStyle,
|
speakingStyle,
|
||||||
modelVersion,
|
modelVersion,
|
||||||
speaker,
|
speaker,
|
||||||
|
greetingText,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
|
clearTimeoutOnSettle();
|
||||||
const error = new Error('WebSocket connection failed');
|
const error = new Error('WebSocket connection failed');
|
||||||
this.callbacks.onError?.(error);
|
this.callbacks.onError?.(error);
|
||||||
this.readyRejector?.(error);
|
this.readyRejector?.(error);
|
||||||
@@ -117,6 +148,7 @@ class NativeVoiceService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
clearTimeoutOnSettle();
|
||||||
this.emitConnectionState('disconnected');
|
this.emitConnectionState('disconnected');
|
||||||
if (this.readyRejector) {
|
if (this.readyRejector) {
|
||||||
this.readyRejector(new Error('WebSocket closed before ready'));
|
this.readyRejector(new Error('WebSocket closed before ready'));
|
||||||
@@ -127,14 +159,20 @@ class NativeVoiceService {
|
|||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
if (typeof event.data === 'string') {
|
if (typeof event.data === 'string') {
|
||||||
this.handleJsonMessage(event.data);
|
const peek = event.data;
|
||||||
|
if (peek.includes('"ready"')) {
|
||||||
|
clearTimeoutOnSettle();
|
||||||
|
}
|
||||||
|
this.handleJsonMessage(peek);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.handleAudioMessage(event.data);
|
this.handleAudioMessage(event.data);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.startCapture();
|
// 使用预获取的mediaStream(已并行获取),避免重复申请
|
||||||
|
const preFetchedStream = await micPromise;
|
||||||
|
await this.startCapture(preFetchedStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleJsonMessage(raw) {
|
handleJsonMessage(raw) {
|
||||||
@@ -164,6 +202,14 @@ class NativeVoiceService {
|
|||||||
this.callbacks.onAssistantPending?.(!!msg.active);
|
this.callbacks.onAssistantPending?.(!!msg.active);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'idle_timeout') {
|
||||||
|
this.callbacks.onIdleTimeout?.(msg.timeout || 300000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === 'upstream_closed') {
|
||||||
|
this.callbacks.onError?.(new Error('语音服务已断开,请重新开始通话'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === 'error') {
|
if (msg.type === 'error') {
|
||||||
this.callbacks.onError?.(new Error(msg.error || 'native voice error'));
|
this.callbacks.onError?.(new Error(msg.error || 'native voice error'));
|
||||||
return;
|
return;
|
||||||
@@ -206,8 +252,8 @@ class NativeVoiceService {
|
|||||||
this.emitDiagnostic('audio_chunk', { samples: pcm16.length, duration: audioBuffer.duration });
|
this.emitDiagnostic('audio_chunk', { samples: pcm16.length, duration: audioBuffer.duration });
|
||||||
}
|
}
|
||||||
|
|
||||||
async startCapture() {
|
async startCapture(preFetchedStream) {
|
||||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
this.mediaStream = preFetchedStream || await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
channelCount: 1,
|
channelCount: 1,
|
||||||
noiseSuppression: true,
|
noiseSuppression: true,
|
||||||
@@ -274,6 +320,13 @@ class NativeVoiceService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestGreetingReplay() {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify({ type: 'replay_greeting' }));
|
||||||
|
this.emitDiagnostic('replay_greeting', { sent: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
if (this.captureProcessor) {
|
if (this.captureProcessor) {
|
||||||
this.captureProcessor.disconnect();
|
this.captureProcessor.disconnect();
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ app.get('/api/health', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
app.use(express.static('../client/dist'));
|
app.use(express.static(path.join(__dirname, '../client/dist')));
|
||||||
|
|
||||||
// 处理单页应用路由
|
// 处理单页应用路由
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
|
|||||||
@@ -9,10 +9,26 @@ async function ensureColumnExists(tableName, columnName, definitionSql) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function columnMatchesType(tableName, columnName, expectedType) {
|
||||||
|
const dbName = process.env.MYSQL_DATABASE || 'bigwo_chat';
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?`,
|
||||||
|
[dbName, tableName, columnName]
|
||||||
|
);
|
||||||
|
if (!rows.length) return false;
|
||||||
|
return rows[0].COLUMN_TYPE.toLowerCase().includes(expectedType.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
async function migrateSchema() {
|
async function migrateSchema() {
|
||||||
await pool.execute("ALTER TABLE `sessions` MODIFY COLUMN `mode` ENUM('voice', 'chat') DEFAULT 'chat'");
|
if (!(await columnMatchesType('sessions', 'mode', "'chat'"))) {
|
||||||
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `role` ENUM('user', 'assistant', 'tool', 'system') NOT NULL");
|
await pool.execute("ALTER TABLE `sessions` MODIFY COLUMN `mode` ENUM('voice', 'chat') DEFAULT 'chat'");
|
||||||
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `source` ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot') NOT NULL");
|
}
|
||||||
|
if (!(await columnMatchesType('messages', 'role', "'system'"))) {
|
||||||
|
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `role` ENUM('user', 'assistant', 'tool', 'system') NOT NULL");
|
||||||
|
}
|
||||||
|
if (!(await columnMatchesType('messages', 'source', "'chat_bot'"))) {
|
||||||
|
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `source` ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot') NOT NULL");
|
||||||
|
}
|
||||||
await ensureColumnExists('messages', 'tool_name', '`tool_name` VARCHAR(64) NULL AFTER `source`');
|
await ensureColumnExists('messages', 'tool_name', '`tool_name` VARCHAR(64) NULL AFTER `source`');
|
||||||
await ensureColumnExists('messages', 'meta_json', '`meta_json` JSON NULL AFTER `tool_name`');
|
await ensureColumnExists('messages', 'meta_json', '`meta_json` JSON NULL AFTER `tool_name`');
|
||||||
await ensureColumnExists('messages', 'created_at', '`created_at` BIGINT NULL AFTER `tool_name`');
|
await ensureColumnExists('messages', 'created_at', '`created_at` BIGINT NULL AFTER `tool_name`');
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ const db = require('../db');
|
|||||||
// 存储文字对话的会话状态(sessionId -> session)
|
// 存储文字对话的会话状态(sessionId -> session)
|
||||||
const chatSessions = new Map();
|
const chatSessions = new Map();
|
||||||
|
|
||||||
|
const BRAND_HARMFUL_PATTERN = /传销|骗局|骗子公司|非法集资|非法经营|不正规|不合法|庞氏骗局|老鼠会|拉人头的|割韭菜/;
|
||||||
|
const BRAND_SAFE_REPLY = '德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家和地区。如果你想了解更多,可以问我关于PM公司的详细介绍哦。';
|
||||||
|
|
||||||
function normalizeAssistantText(text) {
|
function normalizeAssistantText(text) {
|
||||||
return String(text || '')
|
let result = String(text || '')
|
||||||
.replace(/\r/g, ' ')
|
.replace(/\r/g, ' ')
|
||||||
.replace(/\n{2,}/g, '。')
|
.replace(/\n{2,}/g, '。')
|
||||||
.replace(/\n/g, ' ')
|
.replace(/\n/g, ' ')
|
||||||
@@ -19,6 +22,11 @@ function normalizeAssistantText(text) {
|
|||||||
.replace(/([。!?;,])\s*([。!?;,])/g, '$2')
|
.replace(/([。!?;,])\s*([。!?;,])/g, '$2')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
if (BRAND_HARMFUL_PATTERN.test(result)) {
|
||||||
|
console.warn(`[Chat][SafeGuard] blocked harmful content: ${JSON.stringify(result.slice(0, 200))}`);
|
||||||
|
return BRAND_SAFE_REPLY;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHandoffMessages(sessionId, voiceSubtitles = []) {
|
async function loadHandoffMessages(sessionId, voiceSubtitles = []) {
|
||||||
@@ -77,7 +85,13 @@ function buildInitialContextMessages(session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildKnowledgeContextMessages(sessionId, session) {
|
async function buildKnowledgeContextMessages(sessionId, session) {
|
||||||
const dbHistory = await db.getHistoryForLLM(sessionId, 20).catch(() => []);
|
const recentMessages = await db.getRecentMessages(sessionId, 20).catch(() => []);
|
||||||
|
const scopedMessages = session?.fromVoice && session?.handoffSummaryUsed
|
||||||
|
? recentMessages.filter((item) => !/^voice_/i.test(String(item?.source || '')))
|
||||||
|
: recentMessages;
|
||||||
|
const dbHistory = scopedMessages
|
||||||
|
.filter((item) => item && (item.role === 'user' || item.role === 'assistant'))
|
||||||
|
.map((item) => ({ role: item.role, content: item.content }));
|
||||||
const summary = String(session?.handoffSummary || '').trim();
|
const summary = String(session?.handoffSummary || '').trim();
|
||||||
if (!summary || session?.handoffSummaryUsed) {
|
if (!summary || session?.handoffSummaryUsed) {
|
||||||
return dbHistory;
|
return dbHistory;
|
||||||
@@ -98,6 +112,14 @@ function extractKnowledgeReply(result) {
|
|||||||
return typeof result === 'string' ? result : '';
|
return typeof result === 'string' ? result : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFastGreetingReply(message) {
|
||||||
|
const text = String(message || '').trim();
|
||||||
|
if (!/^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安)[,,!。??~~\s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~~]*$/i.test(text)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '你好😊!我是大沃智能助手。你可以直接问我一成系统、德国PM产品、招商合作、营养科普等问题,我会尽量快速给你准确回复。';
|
||||||
|
}
|
||||||
|
|
||||||
async function tryKnowledgeReply(sessionId, session, message) {
|
async function tryKnowledgeReply(sessionId, session, message) {
|
||||||
const text = String(message || '').trim();
|
const text = String(message || '').trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
@@ -106,6 +128,9 @@ async function tryKnowledgeReply(sessionId, session, message) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const result = await ToolExecutor.execute('search_knowledge', { query: text }, context);
|
const result = await ToolExecutor.execute('search_knowledge', { query: text }, context);
|
||||||
|
if (!result?.hit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const content = normalizeAssistantText(extractKnowledgeReply(result));
|
const content = normalizeAssistantText(extractKnowledgeReply(result));
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
@@ -120,6 +145,8 @@ async function tryKnowledgeReply(sessionId, session, message) {
|
|||||||
source: result?.source || null,
|
source: result?.source || null,
|
||||||
original_query: result?.original_query || text,
|
original_query: result?.original_query || text,
|
||||||
rewritten_query: result?.rewritten_query || null,
|
rewritten_query: result?.rewritten_query || null,
|
||||||
|
selected_dataset_ids: result?.selected_dataset_ids || null,
|
||||||
|
selected_kb_routes: result?.selected_kb_routes || null,
|
||||||
hit: typeof result?.hit === 'boolean' ? result.hit : null,
|
hit: typeof result?.hit === 'boolean' ? result.hit : null,
|
||||||
reason: result?.reason || null,
|
reason: result?.reason || null,
|
||||||
error_type: result?.errorType || null,
|
error_type: result?.errorType || null,
|
||||||
@@ -188,6 +215,17 @@ router.post('/send', async (req, res) => {
|
|||||||
// 写入数据库:用户消息
|
// 写入数据库:用户消息
|
||||||
db.addMessage(sessionId, 'user', message, 'chat_user').catch(e => console.warn('[DB] addMessage failed:', e.message));
|
db.addMessage(sessionId, 'user', message, 'chat_user').catch(e => console.warn('[DB] addMessage failed:', e.message));
|
||||||
|
|
||||||
|
const fastGreetingReply = buildFastGreetingReply(message);
|
||||||
|
if (fastGreetingReply) {
|
||||||
|
db.addMessage(sessionId, 'assistant', fastGreetingReply, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
content: fastGreetingReply,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
|
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
|
||||||
if (knowledgeReply) {
|
if (knowledgeReply) {
|
||||||
session.handoffSummaryUsed = true;
|
session.handoffSummaryUsed = true;
|
||||||
@@ -283,15 +321,21 @@ router.post('/send-stream', async (req, res) => {
|
|||||||
res.setHeader('X-Accel-Buffering', 'no');
|
res.setHeader('X-Accel-Buffering', 'no');
|
||||||
res.flushHeaders();
|
res.flushHeaders();
|
||||||
|
|
||||||
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
|
const fastGreetingReply = buildFastGreetingReply(message);
|
||||||
if (knowledgeReply) {
|
if (fastGreetingReply) {
|
||||||
session.handoffSummaryUsed = true;
|
db.addMessage(sessionId, 'assistant', fastGreetingReply, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
|
||||||
db.addMessage(sessionId, 'assistant', knowledgeReply.content, 'chat_bot', 'search_knowledge', knowledgeReply.meta).catch(e => console.warn('[DB] addMessage failed:', e.message));
|
res.write(`data: ${JSON.stringify({ type: 'done', content: fastGreetingReply })}\n\n`);
|
||||||
res.write(`data: ${JSON.stringify({ type: 'done', content: knowledgeReply.content })}\n\n`);
|
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
|
||||||
|
if (knowledgeReply) {
|
||||||
|
session.handoffSummaryUsed = true;
|
||||||
|
db.addMessage(sessionId, 'assistant', knowledgeReply.content, 'chat_bot', 'search_knowledge', knowledgeReply.meta).catch(e => console.warn('[DB] addMessage failed:', e.message));
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'done', content: knowledgeReply.content })}\n\n`);
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
// 首次对话时注入语音历史作为上下文
|
// 首次对话时注入语音历史作为上下文
|
||||||
const extraMessages = !session.conversationId ? buildInitialContextMessages(session) : [];
|
const extraMessages = !session.conversationId ? buildInitialContextMessages(session) : [];
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ router.post('/direct/query', async (req, res) => {
|
|||||||
source: result?.source || null,
|
source: result?.source || null,
|
||||||
original_query: result?.original_query || cleanQuery,
|
original_query: result?.original_query || cleanQuery,
|
||||||
rewritten_query: result?.rewritten_query || null,
|
rewritten_query: result?.rewritten_query || null,
|
||||||
|
selected_dataset_ids: result?.selected_dataset_ids || null,
|
||||||
|
selected_kb_routes: result?.selected_kb_routes || null,
|
||||||
hit: typeof result?.hit === 'boolean' ? result.hit : null,
|
hit: typeof result?.hit === 'boolean' ? result.hit : null,
|
||||||
reason: result?.reason || null,
|
reason: result?.reason || null,
|
||||||
error_type: result?.errorType || null,
|
error_type: result?.errorType || null,
|
||||||
|
|||||||
@@ -9,17 +9,21 @@ class ArkChatService {
|
|||||||
return process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
|
return process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isMockMode() {
|
isMockMode() {
|
||||||
const ep = process.env.VOLC_ARK_ENDPOINT_ID;
|
const ep = process.env.VOLC_ARK_ENDPOINT_ID;
|
||||||
return !ep || ep === 'your_ark_endpoint_id';
|
return !ep || ep === 'your_ark_endpoint_id';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isMockMode() {
|
||||||
|
return this.isMockMode();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取方舟知识库配置(如果已配置)
|
* 获取方舟知识库配置(如果已配置)
|
||||||
* @returns {object|null} 知识库 metadata 配置
|
* @returns {object|null} 知识库 metadata 配置
|
||||||
*/
|
*/
|
||||||
_getKnowledgeBaseConfig() {
|
_getKnowledgeBaseConfig(kbIdsOverride = null) {
|
||||||
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
const kbIds = kbIdsOverride || process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
||||||
if (!kbIds || kbIds === 'your_knowledge_base_dataset_id') return null;
|
if (!kbIds || kbIds === 'your_knowledge_base_dataset_id') return null;
|
||||||
|
|
||||||
const datasetIds = kbIds.split(',').map(id => id.trim()).filter(Boolean);
|
const datasetIds = kbIds.split(',').map(id => id.trim()).filter(Boolean);
|
||||||
@@ -83,12 +87,14 @@ class ArkChatService {
|
|||||||
/**
|
/**
|
||||||
* 非流式调用方舟 LLM
|
* 非流式调用方舟 LLM
|
||||||
*/
|
*/
|
||||||
async chat(messages, tools = []) {
|
async chat(messages, tools = [], options = {}) {
|
||||||
if (this._isMockMode()) {
|
if (this._isMockMode()) {
|
||||||
console.warn('[ArkChat] EndPointId not configured, returning mock response');
|
console.warn('[ArkChat] EndPointId not configured, returning mock response');
|
||||||
return this._mockChat(messages);
|
return this._mockChat(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { useKnowledgeBase = false, knowledgeBaseIds = null } = options || {};
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
model: process.env.VOLC_ARK_ENDPOINT_ID,
|
model: process.env.VOLC_ARK_ENDPOINT_ID,
|
||||||
messages,
|
messages,
|
||||||
@@ -96,8 +102,7 @@ class ArkChatService {
|
|||||||
};
|
};
|
||||||
if (tools.length > 0) body.tools = tools;
|
if (tools.length > 0) body.tools = tools;
|
||||||
|
|
||||||
// 注入方舟私域知识库配置
|
const kbConfig = useKnowledgeBase ? this._getKnowledgeBaseConfig(knowledgeBaseIds) : null;
|
||||||
const kbConfig = this._getKnowledgeBaseConfig();
|
|
||||||
if (kbConfig) {
|
if (kbConfig) {
|
||||||
body.metadata = { knowledge_base: kbConfig };
|
body.metadata = { knowledge_base: kbConfig };
|
||||||
console.log('[ArkChat] Knowledge base enabled:', kbConfig.dataset_ids);
|
console.log('[ArkChat] Knowledge base enabled:', kbConfig.dataset_ids);
|
||||||
@@ -138,7 +143,7 @@ class ArkChatService {
|
|||||||
* @param {function} onToolCall - (toolCalls: Array) => void
|
* @param {function} onToolCall - (toolCalls: Array) => void
|
||||||
* @param {function} onDone - (fullContent: string) => void
|
* @param {function} onDone - (fullContent: string) => void
|
||||||
*/
|
*/
|
||||||
async chatStream(messages, tools = [], { onChunk, onToolCall, onDone }) {
|
async chatStream(messages, tools = [], { onChunk, onToolCall, onDone, useKnowledgeBase = false, knowledgeBaseIds = null } = {}) {
|
||||||
if (this._isMockMode()) {
|
if (this._isMockMode()) {
|
||||||
return this._mockChatStream(messages, { onChunk, onDone });
|
return this._mockChatStream(messages, { onChunk, onDone });
|
||||||
}
|
}
|
||||||
@@ -150,8 +155,7 @@ class ArkChatService {
|
|||||||
};
|
};
|
||||||
if (tools.length > 0) body.tools = tools;
|
if (tools.length > 0) body.tools = tools;
|
||||||
|
|
||||||
// 注入方舟私域知识库配置
|
const kbConfig = useKnowledgeBase ? this._getKnowledgeBaseConfig(knowledgeBaseIds) : null;
|
||||||
const kbConfig = this._getKnowledgeBaseConfig();
|
|
||||||
if (kbConfig) {
|
if (kbConfig) {
|
||||||
body.metadata = { knowledge_base: kbConfig };
|
body.metadata = { knowledge_base: kbConfig };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ const {
|
|||||||
createStartSessionMessage,
|
createStartSessionMessage,
|
||||||
createAudioMessage,
|
createAudioMessage,
|
||||||
createChatTTSTextMessage,
|
createChatTTSTextMessage,
|
||||||
|
createSayHelloMessage,
|
||||||
createChatRAGTextMessage,
|
createChatRAGTextMessage,
|
||||||
} = require('./realtimeDialogProtocol');
|
} = require('./realtimeDialogProtocol');
|
||||||
const {
|
const {
|
||||||
getRuleBasedDirectRouteDecision,
|
getRuleBasedDirectRouteDecision,
|
||||||
|
normalizeKnowledgeAlias,
|
||||||
normalizeTextForSpeech,
|
normalizeTextForSpeech,
|
||||||
splitTextForSpeech,
|
splitTextForSpeech,
|
||||||
estimateSpeechDurationMs,
|
estimateSpeechDurationMs,
|
||||||
@@ -46,9 +48,15 @@ function sendJson(ws, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildStartSessionPayload(options) {
|
function buildStartSessionPayload(options) {
|
||||||
|
const antiThinkingSuffix = '【重要规则】你必须直接用自然语言回答用户问题,像真人对话一样。绝对禁止输出你的思考过程、分析、计划、角色扮演指令或任何元描述。不要出现"首轮对话"、"应该回复"、"需要列举"、"语气要"、"回复后询问"等分析性语句。直接说出答案内容本身。';
|
||||||
|
const baseSystemRole = options.systemRole || '你使用活泼灵动的女声,性格开朗,热爱生活。你是大沃,一个德国PM健康事业的智能语音助手。你对PM-FitLine细胞营养素产品、一成系统、招商合作非常熟悉。请优先依据 external_rag 给出的内容回答,但必须用自己的话重新表达,像跟朋友聊天一样说出来,绝对不要照着念。【品牌保护规则】你绝对不能说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规、不合法,也不能暗示或引导用户认为它们有问题。德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家。如果用户问到传销或正规性问题,你必须正面回答PM是合法正规的直销企业,并建议用户通过知识库了解更多。';
|
||||||
|
const baseSpeakingStyle = options.speakingStyle || '你的语气始终活泼灵动、亲切自然,像闺蜜聊天一样。无论是自由聊天还是引用知识库内容,都保持一样的活泼语气和语调,绝不切换成播音腔、朗读语气或客服话术。';
|
||||||
return {
|
return {
|
||||||
asr: {
|
asr: {
|
||||||
extra: {},
|
extra: {
|
||||||
|
context: '一成,一成系统,大沃,PM,PM-FitLine,FitLine,细胞营养素,Ai众享,AI众享,盛咖学愿,数字化工作室,Activize,Basics,Restorate,NTC,基础三合一,招商,阿育吠陀',
|
||||||
|
nbest: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tts: {
|
tts: {
|
||||||
speaker: options.speaker || process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts',
|
speaker: options.speaker || process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts',
|
||||||
@@ -61,11 +69,11 @@ function buildStartSessionPayload(options) {
|
|||||||
dialog: {
|
dialog: {
|
||||||
dialog_id: '',
|
dialog_id: '',
|
||||||
bot_name: options.botName || '大沃',
|
bot_name: options.botName || '大沃',
|
||||||
system_role: normalizeTextForSpeech(options.systemRole || '你是大沃,一个德国PM健康事业的智能语音助手。你对PM-FitLine细胞营养素产品、一成系统、招商合作非常熟悉。请优先依据 external_rag 给出的内容回答。无论是闲聊还是引用知识库内容,都要保持一样的说话风格,不要切换成朗读语气。用户进来时请自然地打个招呼,像朋友聊天一样,不要用客服话术。'),
|
system_role: normalizeTextForSpeech(`${baseSystemRole} ${antiThinkingSuffix}`),
|
||||||
speaking_style: normalizeTextForSpeech(options.speakingStyle || '说话像朋友聊天一样自然轻松,语气亲切活泼,不要像客服念稿。即使引用知识库内容也要用聊天的语气说出来,不要切换成播音腔或朗读语气。'),
|
speaking_style: normalizeTextForSpeech(`${baseSpeakingStyle} 永远不要输出你的内部思考或计划,直接说出回答内容。`),
|
||||||
extra: {
|
extra: {
|
||||||
input_mod: 'audio',
|
input_mod: 'audio',
|
||||||
model: options.modelVersion || 'O',
|
model: options.modelVersion || 'SC2.0',
|
||||||
strict_audit: false,
|
strict_audit: false,
|
||||||
audit_response: '抱歉,这个问题我暂时无法回答。',
|
audit_response: '抱歉,这个问题我暂时无法回答。',
|
||||||
},
|
},
|
||||||
@@ -87,7 +95,19 @@ function extractUserText(jsonPayload) {
|
|||||||
|| jsonPayload?.results?.[0]?.text
|
|| jsonPayload?.results?.[0]?.text
|
||||||
|| jsonPayload?.results?.[0]?.alternatives?.[0]?.text
|
|| jsonPayload?.results?.[0]?.alternatives?.[0]?.text
|
||||||
|| '';
|
|| '';
|
||||||
return String(text || '').trim();
|
return normalizeKnowledgeAlias(String(text || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const BRAND_HARMFUL_PATTERN = /传销|骗局|骗子公司|非法集资|非法经营|不正规|不合法|庞氏骗局|老鼠会|拉人头的|割韭菜/;
|
||||||
|
const BRAND_SAFE_REPLY = '德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家和地区。如果你想了解更多,可以问我关于PM公司的详细介绍哦。';
|
||||||
|
|
||||||
|
function sanitizeAssistantText(text) {
|
||||||
|
if (!text) return text;
|
||||||
|
if (BRAND_HARMFUL_PATTERN.test(text)) {
|
||||||
|
console.warn(`[NativeVoice][SafeGuard] blocked harmful content: ${JSON.stringify(text.slice(0, 200))}`);
|
||||||
|
return BRAND_SAFE_REPLY;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFinalUserPayload(jsonPayload) {
|
function isFinalUserPayload(jsonPayload) {
|
||||||
@@ -123,7 +143,7 @@ function persistUserSpeech(session, text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function persistAssistantSpeech(session, text, { source = 'voice_bot', toolName = null, persistToDb = true, meta = null } = {}) {
|
function persistAssistantSpeech(session, text, { source = 'voice_bot', toolName = null, persistToDb = true, meta = null } = {}) {
|
||||||
const cleanText = (text || '').trim();
|
const cleanText = sanitizeAssistantText((text || '').trim());
|
||||||
if (!cleanText) return false;
|
if (!cleanText) return false;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (session.lastPersistedAssistantText === cleanText && now - (session.lastPersistedAssistantAt || 0) < 5000) {
|
if (session.lastPersistedAssistantText === cleanText && now - (session.lastPersistedAssistantAt || 0) < 5000) {
|
||||||
@@ -250,21 +270,34 @@ async function sendSpeechText(session, speechText) {
|
|||||||
persistAssistantSpeech(session, greetingText, { source: 'voice_bot' });
|
persistAssistantSpeech(session, greetingText, { source: 'voice_bot' });
|
||||||
clearTimeout(session.greetingTimer);
|
clearTimeout(session.greetingTimer);
|
||||||
clearTimeout(session.readyTimer);
|
clearTimeout(session.readyTimer);
|
||||||
session.greetingTimer = setTimeout(() => {
|
session.greetingSentAt = Date.now();
|
||||||
session.greetingTimer = null;
|
try {
|
||||||
sendSpeechText(session, greetingText)
|
session.upstream.send(createSayHelloMessage(session.sessionId, greetingText));
|
||||||
.then(() => {
|
console.log(`[NativeVoice] sendSayHello event=300 session=${session.sessionId}`);
|
||||||
session.readyTimer = setTimeout(() => {
|
} catch (error) {
|
||||||
session.readyTimer = null;
|
session.hasSentGreeting = false;
|
||||||
sendReady(session);
|
console.warn('[NativeVoice] SayHello failed:', error.message);
|
||||||
}, Math.max(1200, Math.min(estimateSpeechDurationMs(greetingText) + 300, 8000)));
|
}
|
||||||
})
|
sendReady(session);
|
||||||
.catch((error) => {
|
}
|
||||||
session.hasSentGreeting = false;
|
|
||||||
sendReady(session);
|
async function replayGreeting(session) {
|
||||||
console.warn('[NativeVoice] greeting speech failed:', error.message);
|
const greetingText = String(session.greetingText || '').trim();
|
||||||
});
|
if (!greetingText || !session.upstream || session.upstream.readyState !== WebSocket.OPEN) {
|
||||||
}, 800);
|
return;
|
||||||
|
}
|
||||||
|
if (session.greetingSentAt && Date.now() - session.greetingSentAt < 6000) {
|
||||||
|
console.log(`[NativeVoice] replayGreeting skipped (too soon) session=${session.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[NativeVoice] replayGreeting session=${session.sessionId} text=${JSON.stringify(greetingText.slice(0, 80))}`);
|
||||||
|
session.greetingSentAt = Date.now();
|
||||||
|
session.directSpeakUntil = Date.now() + estimateSpeechDurationMs(greetingText) + 800;
|
||||||
|
try {
|
||||||
|
session.upstream.send(createSayHelloMessage(session.sessionId, greetingText));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[NativeVoice] replayGreeting SayHello failed:', error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendExternalRag(session, items) {
|
async function sendExternalRag(session, items) {
|
||||||
@@ -278,6 +311,31 @@ async function sendExternalRag(session, items) {
|
|||||||
session.upstream.send(createChatRAGTextMessage(session.sessionId, JSON.stringify(ragItems)));
|
session.upstream.send(createChatRAGTextMessage(session.sessionId, JSON.stringify(ragItems)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearUpstreamSuppression(session) {
|
||||||
|
clearTimeout(session.suppressReplyTimer);
|
||||||
|
session.suppressReplyTimer = null;
|
||||||
|
session.suppressUpstreamUntil = 0;
|
||||||
|
session.awaitingUpstreamReply = false;
|
||||||
|
session.pendingAssistantSource = null;
|
||||||
|
session.pendingAssistantToolName = null;
|
||||||
|
session.pendingAssistantMeta = null;
|
||||||
|
session.blockUpstreamAudio = false;
|
||||||
|
sendJson(session.client, { type: 'assistant_pending', active: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function suppressUpstreamReply(session, durationMs) {
|
||||||
|
clearTimeout(session.suppressReplyTimer);
|
||||||
|
session.awaitingUpstreamReply = true;
|
||||||
|
session.suppressUpstreamUntil = Date.now() + Math.max(1000, durationMs);
|
||||||
|
session.suppressReplyTimer = setTimeout(() => {
|
||||||
|
session.suppressReplyTimer = null;
|
||||||
|
if ((session.suppressUpstreamUntil || 0) > Date.now()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearUpstreamSuppression(session);
|
||||||
|
}, Math.max(300, session.suppressUpstreamUntil - Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
async function processReply(session, text) {
|
async function processReply(session, text) {
|
||||||
const cleanText = (text || '').trim();
|
const cleanText = (text || '').trim();
|
||||||
if (!cleanText) return;
|
if (!cleanText) return;
|
||||||
@@ -296,6 +354,8 @@ async function processReply(session, text) {
|
|||||||
sendJson(session.client, { type: 'assistant_pending', active: true });
|
sendJson(session.client, { type: 'assistant_pending', active: true });
|
||||||
const isKnowledgeCandidate = shouldForceKnowledgeRoute(cleanText);
|
const isKnowledgeCandidate = shouldForceKnowledgeRoute(cleanText);
|
||||||
if (isKnowledgeCandidate) {
|
if (isKnowledgeCandidate) {
|
||||||
|
session.blockUpstreamAudio = true;
|
||||||
|
suppressUpstreamReply(session, 30000);
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'processing' });
|
sendJson(session.client, { type: 'tts_reset', reason: 'processing' });
|
||||||
}
|
}
|
||||||
console.log(`[NativeVoice] processReply start session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 120))} blocked=${session.blockUpstreamAudio} kbCandidate=${isKnowledgeCandidate}`);
|
console.log(`[NativeVoice] processReply start session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 120))} blocked=${session.blockUpstreamAudio} kbCandidate=${isKnowledgeCandidate}`);
|
||||||
@@ -304,6 +364,7 @@ async function processReply(session, text) {
|
|||||||
if (delivery === 'upstream_chat') {
|
if (delivery === 'upstream_chat') {
|
||||||
if (isKnowledgeCandidate) {
|
if (isKnowledgeCandidate) {
|
||||||
console.log(`[NativeVoice] processReply kb-nohit retrigger session=${session.sessionId}`);
|
console.log(`[NativeVoice] processReply kb-nohit retrigger session=${session.sessionId}`);
|
||||||
|
session.discardNextAssistantResponse = true;
|
||||||
await sendExternalRag(session, [{ title: '用户问题', content: cleanText }]);
|
await sendExternalRag(session, [{ title: '用户问题', content: cleanText }]);
|
||||||
} else {
|
} else {
|
||||||
session.blockUpstreamAudio = false;
|
session.blockUpstreamAudio = false;
|
||||||
@@ -318,14 +379,20 @@ async function processReply(session, text) {
|
|||||||
if (delivery === 'external_rag') {
|
if (delivery === 'external_rag') {
|
||||||
if (!session.blockUpstreamAudio) {
|
if (!session.blockUpstreamAudio) {
|
||||||
session.blockUpstreamAudio = true;
|
session.blockUpstreamAudio = true;
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'knowledge_hit' });
|
|
||||||
}
|
}
|
||||||
session.awaitingUpstreamReply = true;
|
sendJson(session.client, { type: 'tts_reset', reason: 'knowledge_hit' });
|
||||||
session.pendingAssistantSource = source;
|
const kbText = (ragItems || []).map((item) => item?.content || '').filter(Boolean).join('\n').trim();
|
||||||
session.pendingAssistantToolName = toolName;
|
console.log(`[NativeVoice] processReply handoff session=${session.sessionId} route=${routeDecision?.route || 'unknown'} delivery=external_rag→local_tts items=${Array.isArray(ragItems) ? ragItems.length : 0} textLen=${kbText.length}`);
|
||||||
session.pendingAssistantMeta = responseMeta;
|
if (kbText) {
|
||||||
console.log(`[NativeVoice] processReply handoff session=${session.sessionId} route=${routeDecision?.route || 'unknown'} delivery=external_rag items=${Array.isArray(ragItems) ? ragItems.length : 0}`);
|
session.directSpeakUntil = Date.now() + estimateSpeechDurationMs(kbText) + 800;
|
||||||
await sendExternalRag(session, ragItems);
|
suppressUpstreamReply(session, estimateSpeechDurationMs(kbText) + 1800);
|
||||||
|
persistAssistantSpeech(session, kbText, { source, toolName, meta: responseMeta });
|
||||||
|
await sendSpeechText(session, kbText);
|
||||||
|
} else {
|
||||||
|
console.log(`[NativeVoice] processReply external_rag empty content, fallback to upstream session=${session.sessionId}`);
|
||||||
|
session.blockUpstreamAudio = false;
|
||||||
|
clearUpstreamSuppression(session);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!speechText) {
|
if (!speechText) {
|
||||||
@@ -334,12 +401,11 @@ async function processReply(session, text) {
|
|||||||
session.chatTTSUntil = 0;
|
session.chatTTSUntil = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[NativeVoice] processReply resolved session=${session.sessionId} route=${routeDecision?.route || 'unknown'} delivery=local_rag source=${source} tool=${toolName || 'chat'} speechLen=${speechText.length}`);
|
console.log(`[NativeVoice] processReply resolved session=${session.sessionId} route=${routeDecision?.route || 'unknown'} delivery=local_tts source=${source} tool=${toolName || 'chat'} speechLen=${speechText.length}`);
|
||||||
session.awaitingUpstreamReply = true;
|
session.directSpeakUntil = Date.now() + estimateSpeechDurationMs(speechText) + 800;
|
||||||
session.pendingAssistantSource = source;
|
suppressUpstreamReply(session, estimateSpeechDurationMs(speechText) + 1800);
|
||||||
session.pendingAssistantToolName = toolName;
|
persistAssistantSpeech(session, speechText, { source, toolName, meta: responseMeta });
|
||||||
session.pendingAssistantMeta = responseMeta;
|
await sendSpeechText(session, speechText);
|
||||||
await sendExternalRag(session, [{ title: '回复内容', content: speechText }]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[NativeVoice] processReply failed:', error.message);
|
console.error('[NativeVoice] processReply failed:', error.message);
|
||||||
sendJson(session.client, { type: 'error', error: error.message });
|
sendJson(session.client, { type: 'error', error: error.message });
|
||||||
@@ -386,7 +452,8 @@ function handleUpstreamMessage(session, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === MsgType.AUDIO_ONLY_SERVER) {
|
if (message.type === MsgType.AUDIO_ONLY_SERVER) {
|
||||||
if (session.blockUpstreamAudio) {
|
const isSuppressingUpstreamAudio = (session.suppressUpstreamUntil || 0) > Date.now() && session.currentTtsType === 'default';
|
||||||
|
if (session.blockUpstreamAudio || isSuppressingUpstreamAudio) {
|
||||||
if (!session._audioBlockLogOnce) {
|
if (!session._audioBlockLogOnce) {
|
||||||
session._audioBlockLogOnce = true;
|
session._audioBlockLogOnce = true;
|
||||||
console.log(`[NativeVoice] audio blocked (blockUpstream) session=${session.sessionId} ttsType=${session.currentTtsType}`);
|
console.log(`[NativeVoice] audio blocked (blockUpstream) session=${session.sessionId} ttsType=${session.currentTtsType}`);
|
||||||
@@ -419,6 +486,11 @@ function handleUpstreamMessage(session, data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.event === 300) {
|
||||||
|
console.log(`[NativeVoice] SayHello response session=${session.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.event === 350) {
|
if (message.event === 350) {
|
||||||
session.currentTtsType = payload?.tts_type || '';
|
session.currentTtsType = payload?.tts_type || '';
|
||||||
if (payload?.tts_type === 'chat_tts_text' && session.pendingGreetingAck) {
|
if (payload?.tts_type === 'chat_tts_text' && session.pendingGreetingAck) {
|
||||||
@@ -428,7 +500,10 @@ function handleUpstreamMessage(session, data) {
|
|||||||
}
|
}
|
||||||
if (session.blockUpstreamAudio && payload?.tts_type && payload.tts_type !== 'default') {
|
if (session.blockUpstreamAudio && payload?.tts_type && payload.tts_type !== 'default') {
|
||||||
session.blockUpstreamAudio = false;
|
session.blockUpstreamAudio = false;
|
||||||
console.log(`[NativeVoice] unblock audio on ttsType=${payload.tts_type} session=${session.sessionId}`);
|
session.suppressUpstreamUntil = 0;
|
||||||
|
clearTimeout(session.suppressReplyTimer);
|
||||||
|
session.suppressReplyTimer = null;
|
||||||
|
console.log(`[NativeVoice] unblock audio+suppress on ttsType=${payload.tts_type} session=${session.sessionId}`);
|
||||||
}
|
}
|
||||||
console.log(`[NativeVoice] upstream tts_event session=${session.sessionId} ttsType=${payload?.tts_type || ''}`);
|
console.log(`[NativeVoice] upstream tts_event session=${session.sessionId} ttsType=${payload?.tts_type || ''}`);
|
||||||
sendJson(session.client, { type: 'tts_event', payload });
|
sendJson(session.client, { type: 'tts_event', payload });
|
||||||
@@ -436,13 +511,21 @@ function handleUpstreamMessage(session, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLocalChatTTSTextActive = !!session.isSendingChatTTSText && (session.chatTTSUntil || 0) > Date.now();
|
const isLocalChatTTSTextActive = !!session.isSendingChatTTSText && (session.chatTTSUntil || 0) > Date.now();
|
||||||
|
const isSuppressingUpstreamReply = (session.suppressUpstreamUntil || 0) > Date.now();
|
||||||
|
|
||||||
if (message.event === 351) {
|
if (message.event === 351) {
|
||||||
if (isLocalChatTTSTextActive || session.blockUpstreamAudio) {
|
if (isLocalChatTTSTextActive || session.blockUpstreamAudio || isSuppressingUpstreamReply) {
|
||||||
session.assistantStreamBuffer = '';
|
session.assistantStreamBuffer = '';
|
||||||
session.assistantStreamReplyId = '';
|
session.assistantStreamReplyId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (session.discardNextAssistantResponse) {
|
||||||
|
session.discardNextAssistantResponse = false;
|
||||||
|
session.assistantStreamBuffer = '';
|
||||||
|
session.assistantStreamReplyId = '';
|
||||||
|
console.log(`[NativeVoice] discarded stale assistant response (kb-nohit retrigger) session=${session.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pendingAssistantSource = session.pendingAssistantSource || 'voice_bot';
|
const pendingAssistantSource = session.pendingAssistantSource || 'voice_bot';
|
||||||
const pendingAssistantToolName = session.pendingAssistantToolName || null;
|
const pendingAssistantToolName = session.pendingAssistantToolName || null;
|
||||||
const pendingAssistantMeta = session.pendingAssistantMeta || null;
|
const pendingAssistantMeta = session.pendingAssistantMeta || null;
|
||||||
@@ -472,7 +555,7 @@ function handleUpstreamMessage(session, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.event === 550) {
|
if (message.event === 550) {
|
||||||
if (isLocalChatTTSTextActive || session.blockUpstreamAudio) {
|
if (isLocalChatTTSTextActive || session.blockUpstreamAudio || isSuppressingUpstreamReply || session.discardNextAssistantResponse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (session.awaitingUpstreamReply) {
|
if (session.awaitingUpstreamReply) {
|
||||||
@@ -487,7 +570,7 @@ function handleUpstreamMessage(session, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.event === 559) {
|
if (message.event === 559) {
|
||||||
if (isLocalChatTTSTextActive) {
|
if (isLocalChatTTSTextActive || isSuppressingUpstreamReply) {
|
||||||
session.assistantStreamBuffer = '';
|
session.assistantStreamBuffer = '';
|
||||||
session.assistantStreamReplyId = '';
|
session.assistantStreamReplyId = '';
|
||||||
return;
|
return;
|
||||||
@@ -498,6 +581,13 @@ function handleUpstreamMessage(session, data) {
|
|||||||
console.log(`[NativeVoice] blocked response ended (559), keeping block session=${session.sessionId}`);
|
console.log(`[NativeVoice] blocked response ended (559), keeping block session=${session.sessionId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (session.discardNextAssistantResponse) {
|
||||||
|
session.discardNextAssistantResponse = false;
|
||||||
|
session.assistantStreamBuffer = '';
|
||||||
|
session.assistantStreamReplyId = '';
|
||||||
|
console.log(`[NativeVoice] discarded stale stream end (559, kb-nohit retrigger) session=${session.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
session.awaitingUpstreamReply = false;
|
session.awaitingUpstreamReply = false;
|
||||||
session.blockUpstreamAudio = false;
|
session.blockUpstreamAudio = false;
|
||||||
sendJson(session.client, { type: 'assistant_pending', active: false });
|
sendJson(session.client, { type: 'assistant_pending', active: false });
|
||||||
@@ -517,19 +607,23 @@ function handleUpstreamMessage(session, data) {
|
|||||||
if (text) {
|
if (text) {
|
||||||
console.log(`[NativeVoice] upstream partial session=${session.sessionId} text=${JSON.stringify(text.slice(0, 120))}`);
|
console.log(`[NativeVoice] upstream partial session=${session.sessionId} text=${JSON.stringify(text.slice(0, 120))}`);
|
||||||
session.latestUserText = text;
|
session.latestUserText = text;
|
||||||
// 用户开口说话时立即打断 AI 播放
|
// 用户开口说话时立即打断所有 AI 播放(包括 S2S 默认 TTS)
|
||||||
if (session.directSpeakUntil && Date.now() < session.directSpeakUntil) {
|
const now = Date.now();
|
||||||
console.log(`[NativeVoice] user barge-in (partial) session=${session.sessionId}`);
|
const isDirectSpeaking = session.directSpeakUntil && now < session.directSpeakUntil;
|
||||||
|
const isChatTTSSpeaking = session.isSendingChatTTSText && (session.chatTTSUntil || 0) > now;
|
||||||
|
if (isDirectSpeaking || isChatTTSSpeaking) {
|
||||||
|
console.log(`[NativeVoice] user barge-in (partial) session=${session.sessionId} direct=${isDirectSpeaking} chatTTS=${isChatTTSSpeaking}`);
|
||||||
session.directSpeakUntil = 0;
|
session.directSpeakUntil = 0;
|
||||||
session.isSendingChatTTSText = false;
|
session.isSendingChatTTSText = false;
|
||||||
session.chatTTSUntil = 0;
|
session.chatTTSUntil = 0;
|
||||||
clearTimeout(session.chatTTSTimer);
|
clearTimeout(session.chatTTSTimer);
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
if (session.suppressReplyTimer || session.suppressUpstreamUntil) {
|
||||||
} else if (session.isSendingChatTTSText && (session.chatTTSUntil || 0) > Date.now()) {
|
clearUpstreamSuppression(session);
|
||||||
console.log(`[NativeVoice] user barge-in chatTTS (partial) session=${session.sessionId}`);
|
}
|
||||||
session.isSendingChatTTSText = false;
|
}
|
||||||
session.chatTTSUntil = 0;
|
// 无论当前是否在播放,都发送 tts_reset 确保客户端停止所有音频播放
|
||||||
clearTimeout(session.chatTTSTimer);
|
if (!session._lastBargeInResetAt || now - session._lastBargeInResetAt > 500) {
|
||||||
|
session._lastBargeInResetAt = now;
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
||||||
}
|
}
|
||||||
sendJson(session.client, {
|
sendJson(session.client, {
|
||||||
@@ -553,15 +647,22 @@ function handleUpstreamMessage(session, data) {
|
|||||||
session.chatTTSUntil = 0;
|
session.chatTTSUntil = 0;
|
||||||
clearTimeout(session.chatTTSTimer);
|
clearTimeout(session.chatTTSTimer);
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
||||||
|
if (session.suppressReplyTimer || session.suppressUpstreamUntil) {
|
||||||
|
clearUpstreamSuppression(session);
|
||||||
|
}
|
||||||
} else if (session.isSendingChatTTSText && (session.chatTTSUntil || 0) > Date.now()) {
|
} else if (session.isSendingChatTTSText && (session.chatTTSUntil || 0) > Date.now()) {
|
||||||
console.log(`[NativeVoice] user interrupt chatTTS during speaking session=${session.sessionId}`);
|
console.log(`[NativeVoice] user interrupt chatTTS during speaking session=${session.sessionId}`);
|
||||||
session.isSendingChatTTSText = false;
|
session.isSendingChatTTSText = false;
|
||||||
session.chatTTSUntil = 0;
|
session.chatTTSUntil = 0;
|
||||||
clearTimeout(session.chatTTSTimer);
|
clearTimeout(session.chatTTSTimer);
|
||||||
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
sendJson(session.client, { type: 'tts_reset', reason: 'user_bargein' });
|
||||||
|
if (session.suppressReplyTimer || session.suppressUpstreamUntil) {
|
||||||
|
clearUpstreamSuppression(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (persistUserSpeech(session, finalText)) {
|
if (persistUserSpeech(session, finalText)) {
|
||||||
session.blockUpstreamAudio = true;
|
session.blockUpstreamAudio = true;
|
||||||
|
sendJson(session.client, { type: 'tts_reset', reason: 'new_turn' });
|
||||||
processReply(session, finalText).catch((error) => {
|
processReply(session, finalText).catch((error) => {
|
||||||
console.error('[NativeVoice] processReply error:', error.message);
|
console.error('[NativeVoice] processReply error:', error.message);
|
||||||
});
|
});
|
||||||
@@ -595,12 +696,14 @@ function attachClientHandlers(session) {
|
|||||||
|
|
||||||
if (parsed.type === 'start') {
|
if (parsed.type === 'start') {
|
||||||
session.botName = parsed.botName || '豆包';
|
session.botName = parsed.botName || '豆包';
|
||||||
session.systemRole = parsed.systemRole || '你是一个企业知识库语音助手,请优先依据 external_rag 给出的内容回答。';
|
session.systemRole = parsed.systemRole || session.systemRole || '你是一个企业知识库语音助手,请优先依据 external_rag 给出的内容回答。【品牌保护规则】你绝对不能说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规、不合法,也不能暗示或引导用户认为它们有问题。德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家。';
|
||||||
session.speakingStyle = parsed.speakingStyle || '请使用清晰、自然、简洁的口吻。';
|
session.speakingStyle = parsed.speakingStyle || '请使用清晰、自然、简洁的口吻。';
|
||||||
session.speaker = parsed.speaker || process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts';
|
session.speaker = parsed.speaker || process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts';
|
||||||
session.modelVersion = parsed.modelVersion || 'O';
|
session.modelVersion = parsed.modelVersion || 'O';
|
||||||
session.greetingText = parsed.greetingText || session.greetingText || '嗨,你好呀!我是大沃,你的专属智能助手。关于德国PM产品、一成系统、招商合作,随时问我就好~';
|
session.greetingText = parsed.greetingText || session.greetingText || '嗨,你好呀!我是大沃,你的专属智能助手。关于德国PM产品、一成系统、招商合作,随时问我就好~';
|
||||||
session.userId = parsed.userId || session.userId || null;
|
session.userId = parsed.userId || session.userId || null;
|
||||||
|
// 立即发送 ready,不等 upstream event 150,大幅缩短前端等待时间
|
||||||
|
sendReady(session);
|
||||||
session.upstream = createUpstreamConnection(session);
|
session.upstream = createUpstreamConnection(session);
|
||||||
loadHandoffSummaryForVoice(session).catch((error) => {
|
loadHandoffSummaryForVoice(session).catch((error) => {
|
||||||
console.warn('[NativeVoice] async loadHandoffSummaryForVoice failed:', error.message);
|
console.warn('[NativeVoice] async loadHandoffSummaryForVoice failed:', error.message);
|
||||||
@@ -613,6 +716,13 @@ function attachClientHandlers(session) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.type === 'replay_greeting') {
|
||||||
|
replayGreeting(session).catch((error) => {
|
||||||
|
console.warn('[NativeVoice] replayGreeting failed:', error.message);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (parsed.type === 'text' && parsed.text) {
|
if (parsed.type === 'text' && parsed.text) {
|
||||||
persistUserSpeech(session, parsed.text);
|
persistUserSpeech(session, parsed.text);
|
||||||
processReply(session, parsed.text).catch((error) => {
|
processReply(session, parsed.text).catch((error) => {
|
||||||
@@ -626,6 +736,7 @@ function attachClientHandlers(session) {
|
|||||||
clearTimeout(session.greetingTimer);
|
clearTimeout(session.greetingTimer);
|
||||||
clearTimeout(session.greetingAckTimer);
|
clearTimeout(session.greetingAckTimer);
|
||||||
clearTimeout(session.readyTimer);
|
clearTimeout(session.readyTimer);
|
||||||
|
clearTimeout(session.suppressReplyTimer);
|
||||||
clearTimeout(session.idleTimer);
|
clearTimeout(session.idleTimer);
|
||||||
if (session.upstream && session.upstream.readyState === WebSocket.OPEN) {
|
if (session.upstream && session.upstream.readyState === WebSocket.OPEN) {
|
||||||
session.upstream.close();
|
session.upstream.close();
|
||||||
@@ -696,7 +807,7 @@ function createSession(client, sessionId) {
|
|||||||
assistantStreamReplyId: '',
|
assistantStreamReplyId: '',
|
||||||
currentTtsType: '',
|
currentTtsType: '',
|
||||||
botName: '大沃',
|
botName: '大沃',
|
||||||
systemRole: '你是大沃,一个德国PM健康事业的智能语音助手。你对PM-FitLine细胞营养素产品、一成系统、招商合作非常熟悉。请优先依据 external_rag 给出的内容回答。用户进来时请自然地打个招呼,像朋友聊天一样,不要用客服话术。',
|
systemRole: '你是大沃,一个德国PM健康事业的智能语音助手。你对PM-FitLine细胞营养素产品、一成系统、招商合作非常熟悉。请优先依据 external_rag 给出的内容回答。用户进来时请自然地打个招呼,像朋友聊天一样,不要用客服话术。【品牌保护规则】你绝对不能说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规、不合法,也不能暗示或引导用户认为它们有问题。德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家。',
|
||||||
speakingStyle: '说话像朋友聊天一样自然轻松,语气亲切活泼,不要像客服念稿。',
|
speakingStyle: '说话像朋友聊天一样自然轻松,语气亲切活泼,不要像客服念稿。',
|
||||||
speaker: process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts',
|
speaker: process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts',
|
||||||
modelVersion: 'O',
|
modelVersion: 'O',
|
||||||
@@ -714,8 +825,12 @@ function createSession(client, sessionId) {
|
|||||||
pendingAssistantSource: null,
|
pendingAssistantSource: null,
|
||||||
pendingAssistantToolName: null,
|
pendingAssistantToolName: null,
|
||||||
pendingAssistantMeta: null,
|
pendingAssistantMeta: null,
|
||||||
|
suppressReplyTimer: null,
|
||||||
|
suppressUpstreamUntil: 0,
|
||||||
idleTimer: null,
|
idleTimer: null,
|
||||||
lastActivityAt: Date.now(),
|
lastActivityAt: Date.now(),
|
||||||
|
_lastBargeInResetAt: 0,
|
||||||
|
_audioBlockLogOnce: false,
|
||||||
};
|
};
|
||||||
sessions.set(sessionId, session);
|
sessions.set(sessionId, session);
|
||||||
attachClientHandlers(session);
|
attachClientHandlers(session);
|
||||||
|
|||||||
@@ -180,6 +180,18 @@ function createChatTTSTextMessage(sessionId, payload) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSayHelloMessage(sessionId, content) {
|
||||||
|
return marshal({
|
||||||
|
type: MsgType.FULL_CLIENT,
|
||||||
|
typeFlag: MSG_TYPE_FLAG_WITH_EVENT,
|
||||||
|
event: 300,
|
||||||
|
sessionId,
|
||||||
|
payload: Buffer.from(JSON.stringify({
|
||||||
|
content: content || '',
|
||||||
|
}), 'utf8'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createChatRAGTextMessage(sessionId, externalRag) {
|
function createChatRAGTextMessage(sessionId, externalRag) {
|
||||||
return marshal({
|
return marshal({
|
||||||
type: MsgType.FULL_CLIENT,
|
type: MsgType.FULL_CLIENT,
|
||||||
@@ -201,5 +213,6 @@ module.exports = {
|
|||||||
createStartSessionMessage,
|
createStartSessionMessage,
|
||||||
createAudioMessage,
|
createAudioMessage,
|
||||||
createChatTTSTextMessage,
|
createChatTTSTextMessage,
|
||||||
|
createSayHelloMessage,
|
||||||
createChatRAGTextMessage,
|
createChatRAGTextMessage,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,34 @@ function estimateSpeechDurationMs(text) {
|
|||||||
return Math.max(4000, Math.min(60000, length * 180));
|
return Math.max(4000, Math.min(60000, length * 180));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function polishForSpeech(rawText, userQuestion) {
|
||||||
|
const POLISH_TIMEOUT_MS = 3000;
|
||||||
|
try {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: '你是一个语音播报润色助手。请将下面的知识库回答改写为自然、亲切的口语风格,像朋友聊天一样。要求:1) 保留所有关键信息和数据,不得编造;2) 去掉"根据知识库信息"等机械前缀;3) 适合语音朗读,简洁流畅;4) 控制在120字以内;5) 只输出改写后的文本,不要加引号或解释。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `用户问题:${userQuestion}\n\n原始回答:${rawText}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const result = await Promise.race([
|
||||||
|
arkChatService.chat(messages, [], { useKnowledgeBase: false }),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('polish timeout')), POLISH_TIMEOUT_MS)),
|
||||||
|
]);
|
||||||
|
const polished = (result?.content || '').trim();
|
||||||
|
if (polished && polished.length >= 10) {
|
||||||
|
console.log(`[RealtimeRouting] polishForSpeech ok len=${polished.length} original=${rawText.length}`);
|
||||||
|
return polished;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[RealtimeRouting] polishForSpeech failed:', err.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function buildDirectRouteMessages(session, context, userText) {
|
function buildDirectRouteMessages(session, context, userText) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
const systemPrompt = [
|
const systemPrompt = [
|
||||||
@@ -110,12 +138,29 @@ function buildDirectChatMessages(session, context, userText) {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeKnowledgeAlias(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/X{2}系统/gi, '一成系统')
|
||||||
|
.replace(/一城系统|逸城系统|一程系统|易成系统|一诚系统|亦成系统|艺成系统|溢成系统|义成系统|毅成系统|怡成系统|以成系统|已成系统|亿成系统|忆成系统|益成系统/g, '一成系统')
|
||||||
|
.replace(/(?<![一\u4e00-\u9fff])(一城|逸城|一程|易成|一诚|亦成|艺成|溢成|义成|毅成|怡成|以成|已成|亿成|忆成|益成)(?=系统)/g, '一成')
|
||||||
|
.replace(/大窝|大握|大我|大卧/g, '大沃')
|
||||||
|
.replace(/盛咖学院|圣咖学愿|盛咖学院|圣咖学院|盛卡学愿/g, '盛咖学愿')
|
||||||
|
.replace(/AI众享|Ai众享|爱众享|艾众享|哎众享/gi, 'Ai众享')
|
||||||
|
.replace(/暖炉原理/g, '火炉原理');
|
||||||
|
}
|
||||||
|
|
||||||
function hasKnowledgeKeyword(text) {
|
function hasKnowledgeKeyword(text) {
|
||||||
return /(系统|平台|产品|功能|介绍|说明|规则|流程|步骤|配置|接入|开通|操作|怎么用|如何用|适合谁|区别|价格|费用|政策|售后|文档|资料|方案|一成系统|PM公司|德国PM|公司地址|电话|联系方式|公司实力|背景|培训|新人|起步三关|精品会议|成长上总裁|招商|代理|加盟|合作|邀约话术|小红|大白|小白|Activize|Basics|Restorate|FitLine|细胞营养素|NTC|营养保送|火炉原理|阿育吠陀|Ayurveda|儿童倍适|AI落地|ai落地|转观念|科普|营养|成分|功效|基础三合一|三合一|基础套装|套装|Ai众享|数字化工作室|盛咖学愿)/i.test(text || '');
|
const normalized = normalizeKnowledgeAlias(text);
|
||||||
|
return /(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|四大AI生态|四大Ai生态|三大平台|PM公司|德国PM|PM-FitLine|FitLine|PM细胞营养素|细胞营养素|小红|大白|小白|Activize|Basics|Restorate|儿童倍适|NTC|营养保送|火炉原理|暖炉原理|阿育吠陀|Ayurveda|基础三合一|三合一|基础套装|基础二合一|二合一|招商合作|招商|代理|加盟|事业机会|邀约话术|起步三关|精品会议|成长上总裁|AI落地|ai落地|转观念|好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒|促销活动|促销|优惠|活动分数|5\+1|CC套装|CC胶囊|IB5|口腔免疫喷雾|Q10|辅酵素|Women\+|乐活|乳清蛋白|蛋白粉|乳酪煲|乳酪饮品|乳酪|倍力健|关节套装|关节舒缓|男士乳霜|去角质|面膜|发宝|叶黄素|奶昔|健康饮品|传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费|怎么吃|怎么服用|吃多少|服用方法|搭配|功效|成分|原料)/i.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKnowledgeFollowUp(text) {
|
function isKnowledgeFollowUp(text) {
|
||||||
return /^(这个|那个|它|该系统|这个系统|那个系统|这个功能|那个功能|这个产品|那个产品|这个公司|那家公司|这个政策|那个政策|这个培训|那个培训|详细|详细说说|详细查一下|展开说说|继续说|继续讲|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|电话多少|联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人)/.test((text || '').trim());
|
const normalized = String(text || '').trim().replace(/[,,。!??~~\s]+$/g, '').replace(/^(那你|那再|那|你再|再来|再|麻烦你|帮我)[,,、\s]*/g, '');
|
||||||
|
if (!normalized) return false;
|
||||||
|
if (/^(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人)$/.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /^(这个|那个|它|该系统|这个系统|那个系统|这个功能|那个功能|这个产品|那个产品|这个公司|那家公司|这个政策|那个政策|这个培训|那个培训)(的)?(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人)?$/.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldForceKnowledgeRoute(userText, context = []) {
|
function shouldForceKnowledgeRoute(userText, context = []) {
|
||||||
@@ -174,6 +219,9 @@ function getRuleBasedDirectRouteDecision(userText) {
|
|||||||
if (/^[\d\s+\-*/().=%]+$/.test(text) || /(等于多少|帮我算|计算一下|算一下)/.test(text)) {
|
if (/^[\d\s+\-*/().=%]+$/.test(text) || /(等于多少|帮我算|计算一下|算一下)/.test(text)) {
|
||||||
return { route: 'calculate', args: { expression: text.replace(/(帮我算|计算一下|算一下|等于多少)/g, '').trim() || text } };
|
return { route: 'calculate', args: { expression: text.replace(/(帮我算|计算一下|算一下|等于多少)/g, '').trim() || text } };
|
||||||
}
|
}
|
||||||
|
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/.test(text)) {
|
||||||
|
return { route: 'search_knowledge', args: { query: text } };
|
||||||
|
}
|
||||||
if (/^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安|谢谢|感谢|再见|拜拜|嗯|哦|好的|对|是的|没有了|没事了|可以了|行|OK|ok)[,,!。??~~\s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~~]*$/.test(text)) {
|
if (/^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安|谢谢|感谢|再见|拜拜|嗯|哦|好的|对|是的|没有了|没事了|可以了|行|OK|ok)[,,!。??~~\s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~~]*$/.test(text)) {
|
||||||
return { route: 'chat', args: {} };
|
return { route: 'chat', args: {} };
|
||||||
}
|
}
|
||||||
@@ -278,7 +326,20 @@ async function resolveReply(sessionId, session, text) {
|
|||||||
: []);
|
: []);
|
||||||
|
|
||||||
if (ragItems.length > 0) {
|
if (ragItems.length > 0) {
|
||||||
|
let speechText = normalizeTextForSpeech(replyText);
|
||||||
session.handoffSummaryUsed = true;
|
session.handoffSummaryUsed = true;
|
||||||
|
if (toolName === 'search_knowledge' && speechText) {
|
||||||
|
const cleanedText = speechText.replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '');
|
||||||
|
return {
|
||||||
|
delivery: 'external_rag',
|
||||||
|
speechText: '',
|
||||||
|
ragItems: [{ title: '知识库结果', content: cleanedText || speechText }],
|
||||||
|
source,
|
||||||
|
toolName,
|
||||||
|
routeDecision,
|
||||||
|
responseMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
delivery: 'external_rag',
|
delivery: 'external_rag',
|
||||||
speechText: '',
|
speechText: '',
|
||||||
@@ -292,6 +353,19 @@ async function resolveReply(sessionId, session, text) {
|
|||||||
|
|
||||||
if (toolName === 'search_knowledge' && !toolResult?.hit) {
|
if (toolName === 'search_knowledge' && !toolResult?.hit) {
|
||||||
session.handoffSummaryUsed = true;
|
session.handoffSummaryUsed = true;
|
||||||
|
// 敏感问题(传销/正规性)知识库未命中时,不交给S2S自由发挥,直接返回安全回复
|
||||||
|
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/.test(originalText)) {
|
||||||
|
const safeReply = '德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家和地区。它不是传销,是正规的直销企业哦。如果你想了解更多,可以问我关于PM公司或产品的详细介绍。';
|
||||||
|
return {
|
||||||
|
delivery: 'external_rag',
|
||||||
|
speechText: '',
|
||||||
|
ragItems: [{ title: '品牌保护', content: safeReply }],
|
||||||
|
source: 'voice_tool',
|
||||||
|
toolName: 'search_knowledge',
|
||||||
|
routeDecision,
|
||||||
|
responseMeta: { ...responseMeta, hit: true, reason: 'brand_protection' },
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
delivery: 'upstream_chat',
|
delivery: 'upstream_chat',
|
||||||
speechText: '',
|
speechText: '',
|
||||||
@@ -313,6 +387,7 @@ async function resolveReply(sessionId, session, text) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getRuleBasedDirectRouteDecision,
|
getRuleBasedDirectRouteDecision,
|
||||||
|
normalizeKnowledgeAlias,
|
||||||
normalizeTextForSpeech,
|
normalizeTextForSpeech,
|
||||||
splitTextForSpeech,
|
splitTextForSpeech,
|
||||||
estimateSpeechDurationMs,
|
estimateSpeechDurationMs,
|
||||||
|
|||||||
@@ -6,6 +6,181 @@ class ToolExecutor {
|
|||||||
return /(一成系统|PM-FitLine|PM细胞营养素|NTC营养保送系统|Activize Oxyplus|小红产品|Basics|大白产品|Restorate|小白产品|儿童倍适|火炉原理|阿育吠陀)/i.test(String(query || ''));
|
return /(一成系统|PM-FitLine|PM细胞营养素|NTC营养保送系统|Activize Oxyplus|小红产品|Basics|大白产品|Restorate|小白产品|儿童倍适|火炉原理|阿育吠陀)/i.test(String(query || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getKnowledgeBaseRoutingRules() {
|
||||||
|
const raw = process.env.VOLC_ARK_KNOWLEDGE_BASE_ROUTING || process.env.VOLC_ARK_KNOWLEDGE_BASE_MAP;
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const entries = Array.isArray(parsed)
|
||||||
|
? parsed
|
||||||
|
: Object.entries(parsed).map(([name, config]) => ({ name, ...(config || {}) }));
|
||||||
|
return entries
|
||||||
|
.map((item) => ({
|
||||||
|
name: String(item.name || '').trim(),
|
||||||
|
dataset_ids: Array.isArray(item.dataset_ids)
|
||||||
|
? item.dataset_ids.map((id) => String(id || '').trim()).filter(Boolean)
|
||||||
|
: String(item.dataset_ids || item.datasetIds || '')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
keywords: Array.isArray(item.keywords)
|
||||||
|
? item.keywords.map((keyword) => String(keyword || '').trim()).filter(Boolean)
|
||||||
|
: String(item.keywords || '')
|
||||||
|
.split(',')
|
||||||
|
.map((keyword) => keyword.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.name && item.dataset_ids.length > 0 && item.keywords.length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ToolExecutor] parse knowledge base routing failed:', error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static selectKnowledgeBaseTargets(query, context = []) {
|
||||||
|
const defaultDatasetIds = String(process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS || '')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const rules = this.getKnowledgeBaseRoutingRules();
|
||||||
|
if (!rules.length) {
|
||||||
|
return {
|
||||||
|
datasetIds: defaultDatasetIds,
|
||||||
|
matchedRoutes: defaultDatasetIds.length ? ['default'] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentContextText = (Array.isArray(context) ? context : [])
|
||||||
|
.slice(-6)
|
||||||
|
.map((item) => String(item?.content || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
const haystack = `${String(query || '').trim()}\n${recentContextText}`.toLowerCase();
|
||||||
|
|
||||||
|
const priorityRouteNames = [];
|
||||||
|
const hasSystemIntent = /(一成系统|ai众享|数字化工作室|盛咖学愿|赋能工具|四大ai生态|三大平台)/i.test(haystack);
|
||||||
|
const hasCompanyIntent = /(pm公司|德国pm(?!事业|细胞|营养|产品|fitline|\s*基础|\s*大白|\s*小红|\s*小白)|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司|邓白氏|aaa\+|公司介绍)/i.test(haystack);
|
||||||
|
const hasProductIntent = /(细胞营养素|基础套装|基础三合一|三合一|大白产品|小红产品|小白产品|activize|basics|restorate|fitline|儿童倍适|ntc营养保送|火炉原理|阿育吠陀|产品.*介绍|介绍.*产品|产品有哪些|产品列表)/i.test(haystack);
|
||||||
|
if (hasSystemIntent) {
|
||||||
|
priorityRouteNames.push('system');
|
||||||
|
}
|
||||||
|
if (hasCompanyIntent && !hasSystemIntent && !hasProductIntent) {
|
||||||
|
priorityRouteNames.push('company');
|
||||||
|
}
|
||||||
|
if (priorityRouteNames.length > 0) {
|
||||||
|
const priorityRules = rules.filter((rule) => priorityRouteNames.includes(rule.name));
|
||||||
|
const priorityDatasetIds = [...new Set(priorityRules.flatMap((rule) => rule.dataset_ids).filter(Boolean))];
|
||||||
|
if (priorityDatasetIds.length > 0) {
|
||||||
|
return {
|
||||||
|
datasetIds: priorityDatasetIds,
|
||||||
|
matchedRoutes: [...new Set(priorityRules.map((rule) => rule.name))],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedDatasetIds = [];
|
||||||
|
const matchedRoutes = [];
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.keywords.some((keyword) => haystack.includes(keyword.toLowerCase()))) {
|
||||||
|
matchedRoutes.push(rule.name);
|
||||||
|
matchedDatasetIds.push(...rule.dataset_ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasetIds = [...new Set((matchedDatasetIds.length ? matchedDatasetIds : defaultDatasetIds).filter(Boolean))];
|
||||||
|
return {
|
||||||
|
datasetIds,
|
||||||
|
matchedRoutes: matchedRoutes.length ? [...new Set(matchedRoutes)] : (datasetIds.length ? ['default'] : []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static buildDeterministicKnowledgeQuery(query, context = []) {
|
||||||
|
const text = String(query || '').trim();
|
||||||
|
const recentContextText = (Array.isArray(context) ? context : [])
|
||||||
|
.slice(-6)
|
||||||
|
.map((item) => String(item?.content || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
const haystack = `${text}\n${recentContextText}`;
|
||||||
|
|
||||||
|
// 第一层:当前查询文本中有明确产品/系统/主题关键词 → 直接改写(不依赖上下文)
|
||||||
|
if (/(基础三合一|三合一基础套|基础套装|大白小红小白)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
|
||||||
|
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿)/i.test(text)) {
|
||||||
|
if (/(邀约|话术)/i.test(haystack)) return '一成系统 邀约话术';
|
||||||
|
if (/文化/i.test(haystack)) return '一成系统 文化解析';
|
||||||
|
if (/(赋能团队|团队发展|AI赋能|ai赋能)/i.test(haystack)) return '一成系统用AI赋能团队发展';
|
||||||
|
return '一成系统 德国PM事业发展的强大赋能工具 三大平台 四大Ai生态';
|
||||||
|
}
|
||||||
|
if (/(PM公司|德国PM|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司)/i.test(text)) {
|
||||||
|
if (/(产品|细胞营养素|基础套装|基础三合一|小红|大白|小白|activize|basics|restorate|fitline|儿童倍适)/i.test(text)) {
|
||||||
|
return '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适';
|
||||||
|
}
|
||||||
|
if (/(地址|电话|联系方式)/i.test(text)) return '德国PM 日本 美国 加拿大 香港 地址 电话';
|
||||||
|
if (/(实力|背景)/i.test(text)) return '德国PM 公司实力介绍 邓白氏 99分 AAA+';
|
||||||
|
return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍';
|
||||||
|
}
|
||||||
|
if (/儿童倍适/i.test(text)) return '儿童倍适';
|
||||||
|
if (/(小红产品|小红|Activize Oxyplus|Activize)/i.test(text)) return 'Fitline小红产品提升能量原理';
|
||||||
|
if (/(大白产品|大白|倍适|Basics)/i.test(text)) return '德国PM细胞营养素 大白 Basics';
|
||||||
|
if (/(小白产品|小白|维适多|Restorate)/i.test(text)) return '德国PM细胞营养素 小白';
|
||||||
|
if (/(NTC营养保送系统|Nutrient Transport Concept)/i.test(text)) return 'NTC营养保送系统';
|
||||||
|
if (/火炉原理/i.test(text)) return '火炉原理';
|
||||||
|
if (/(阿育吠陀|Ayurveda)/i.test(text)) return '阿育吠陀医学原理';
|
||||||
|
if (/(PM-FitLine|PM细胞营养素)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
|
||||||
|
if (/(我们公司.*产品|公司.*产品|产品.*推荐|推荐.*产品|产品有哪些|产品介绍|产品列表)/i.test(text)) return '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适';
|
||||||
|
if (/(新人起步三关|起步三关)/i.test(text)) return '培训新人起步三关';
|
||||||
|
if (/(精品会议|会议组织)/i.test(text)) return '培训打造精品会议具体如下';
|
||||||
|
if (/成长上总裁/i.test(text)) return '培训成长上总裁';
|
||||||
|
if (/(招商|代理|加盟|合作|事业机会|招商稿|代理政策)/i.test(text)) return '招商与代理';
|
||||||
|
if (/(一成AI|AI落地|ai落地|转观念|落地对比)/i.test(text)) return '2026一成Ai落地对比与转观念';
|
||||||
|
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/i.test(text)) return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍 邓白氏 99分 AAA+ 合法直销';
|
||||||
|
if (/(好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒)/i.test(text)) return 'PM产品整应反应好转反应解析';
|
||||||
|
if (/(促销活动|促销|优惠|打折|活动分数|5\+1)/i.test(text)) return '促销活动 5+1活动分数';
|
||||||
|
if (/暖炉原理/i.test(text)) return '火炉原理';
|
||||||
|
if (/(CC套装|CC胶囊)/i.test(text)) return 'CC套装 CC胶囊';
|
||||||
|
if (/(IB5|口腔免疫喷雾)/i.test(text)) return 'IB5口腔免疫喷雾';
|
||||||
|
if (/(Q10|辅酵素|氧修护)/i.test(text)) return 'Q10辅酵素氧修护';
|
||||||
|
if (/Women\+/i.test(text)) return 'Women+';
|
||||||
|
if (/乐活/i.test(text)) return '乐活';
|
||||||
|
if (/(乳清蛋白|蛋白粉)/i.test(text)) return '乳清蛋白粉';
|
||||||
|
if (/(乳酪煲|乳酪饮品|乳酪)/i.test(text)) return '乳酪煲 乳酪饮品';
|
||||||
|
if (/(基础二合一|二合一)/i.test(text)) return '基础二合一';
|
||||||
|
if (/倍力健/i.test(text)) return '倍力健';
|
||||||
|
if (/(关节套装|关节舒缓)/i.test(text)) return '关节套装 关节舒缓膏';
|
||||||
|
if (/(男士乳霜|男士护肤)/i.test(text)) return '全效男士乳霜';
|
||||||
|
if (/(去角质|面膜)/i.test(text)) return '去角质面膜';
|
||||||
|
if (/发宝/i.test(text)) return '发宝';
|
||||||
|
if (/叶黄素/i.test(text)) return '叶黄素';
|
||||||
|
if (/(奶昔)/i.test(text)) return '奶昔';
|
||||||
|
if (/(健康饮品)/i.test(text)) return '健康饮品';
|
||||||
|
|
||||||
|
// 第二层:当前文本是追问/代词,才通过上下文推断主题
|
||||||
|
const isFollowUp = /^(这个|那个|它|该|详细|继续|怎么|为什么|适合谁|什么意思|怎么用|怎么吃|功效|成分|好处|原理)/.test(text);
|
||||||
|
if (isFollowUp) {
|
||||||
|
if (/(基础三合一|三合一基础套|基础套装|大白小红小白)/i.test(recentContextText)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
|
||||||
|
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿)/i.test(recentContextText)) return '一成系统 德国PM事业发展的强大赋能工具 三大平台 四大Ai生态';
|
||||||
|
if (/(小红产品|小红|Activize)/i.test(recentContextText)) return 'Fitline小红产品提升能量原理';
|
||||||
|
if (/(大白产品|大白|Basics)/i.test(recentContextText)) return '德国PM细胞营养素 大白 Basics';
|
||||||
|
if (/(小白产品|小白|Restorate)/i.test(recentContextText)) return '德国PM细胞营养素 小白';
|
||||||
|
if (/儿童倍适/i.test(recentContextText)) return '儿童倍适';
|
||||||
|
if (/火炉原理/i.test(recentContextText)) return '火炉原理';
|
||||||
|
if (/(阿育吠陀|Ayurveda)/i.test(recentContextText)) return '阿育吠陀医学原理';
|
||||||
|
if (/(NTC营养保送系统)/i.test(recentContextText)) return 'NTC营养保送系统';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
static applyKnowledgeQueryAnchor(query) {
|
||||||
|
let anchoredQuery = String(query || '').trim();
|
||||||
|
if (/一成系统/.test(anchoredQuery) && !/(德国PM|PM事业|赋能工具|Ai众享|数字化工作室|盛咖学愿)/i.test(anchoredQuery)) {
|
||||||
|
anchoredQuery = anchoredQuery.replace(/一成系统/g, '一成系统 德国PM事业赋能工具');
|
||||||
|
}
|
||||||
|
return anchoredQuery.trim();
|
||||||
|
}
|
||||||
|
|
||||||
static normalizeKnowledgeQueryAlias(query) {
|
static normalizeKnowledgeQueryAlias(query) {
|
||||||
return String(query || '')
|
return String(query || '')
|
||||||
.replace(/^[啊哦嗯呢呀哎诶额,。!?、\s]+/g, '')
|
.replace(/^[啊哦嗯呢呀哎诶额,。!?、\s]+/g, '')
|
||||||
@@ -19,11 +194,15 @@ class ToolExecutor {
|
|||||||
.replace(/Activize Oxyplus|Activize/gi, 'Activize Oxyplus')
|
.replace(/Activize Oxyplus|Activize/gi, 'Activize Oxyplus')
|
||||||
.replace(/Restorate/gi, 'Restorate')
|
.replace(/Restorate/gi, 'Restorate')
|
||||||
.replace(/Basics/gi, 'Basics')
|
.replace(/Basics/gi, 'Basics')
|
||||||
.replace(/基础三合一|基础套装?|三合一基础套|大白小红小白/g, 'Basics')
|
.replace(/基础三合一|三合一基础套|大白小红小白|基础套装?/g, 'PM细胞营养素 基础套装')
|
||||||
.replace(/小红产品|小红/g, '小红产品 Activize Oxyplus')
|
|
||||||
.replace(/大白产品|大白/g, '大白产品 Basics')
|
|
||||||
.replace(/小白产品|小白/g, '小白产品 Restorate')
|
|
||||||
.replace(/儿童倍适|儿童产品/g, '儿童倍适')
|
.replace(/儿童倍适|儿童产品/g, '儿童倍适')
|
||||||
|
.replace(/小红产品/g, '小红产品 Activize Oxyplus')
|
||||||
|
.replace(/大白产品/g, '大白产品 Basics')
|
||||||
|
.replace(/小白产品/g, '小白产品 Restorate')
|
||||||
|
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)小红/g, '小红产品 Activize Oxyplus')
|
||||||
|
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)大白/g, '大白产品 Basics')
|
||||||
|
.replace(/(?<!小红产品\s*)(?<!大白产品\s*)(?<!小白产品\s*)(?<!儿童)小白/g, '小白产品 Restorate')
|
||||||
|
.replace(/维适多/g, '小白产品 Restorate')
|
||||||
.replace(/火炉原理/g, '火炉原理')
|
.replace(/火炉原理/g, '火炉原理')
|
||||||
.replace(/阿育吠陀|Ayurveda/gi, '阿育吠陀')
|
.replace(/阿育吠陀|Ayurveda/gi, '阿育吠陀')
|
||||||
.trim();
|
.trim();
|
||||||
@@ -39,7 +218,7 @@ class ToolExecutor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const noHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关信息|没有找到相关信息|知识库中没有相关内容|暂未找到与.*直接相关的信息|无法基于知识库/;
|
const noHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关内容|未找到相关信息|没有找到相关信息|没有找到.*的具体|没有找到.*的相关|没有找到.*的详细|没有找到.*相关介绍|我这边没有找到|目前没有找到|暂时没有找到|知识库中没有相关内容|暂未找到与.*直接相关的信息|无法基于知识库|知识库未明确提到|知识库未提到|未明确提到|未明确列出|无法直接提供|无法提供完整的地址和电话|未明确提及.*地址|未明确提及.*电话|未明确提及.*联系方式|建议通过官方客服渠道|建议通过官方.*查询|建议.*查看产品包装|建议.*联系.*客服|联系官方客服|建议.*咨询.*客服|没有相关.*资料|还没有相关的|没有相关的信息|没有相关的资料|没有.*的资料|知识库里.*没有|暂未收录|目前.*没有.*相关|不在.*知识库|建议.*查阅.*官方|建议.*咨询.*专/;
|
||||||
if (noHitPattern.test(text)) {
|
if (noHitPattern.test(text)) {
|
||||||
return {
|
return {
|
||||||
hit: false,
|
hit: false,
|
||||||
@@ -48,6 +227,43 @@ class ToolExecutor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedQuery = String(query || '').trim();
|
||||||
|
if (/(小红|Activize Oxyplus)/i.test(normalizedQuery) && /(护肤|肤色|敏感肌|眼周)/i.test(text)) {
|
||||||
|
return {
|
||||||
|
hit: false,
|
||||||
|
reason: 'no_hit',
|
||||||
|
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(大白|Basics|倍适)/i.test(normalizedQuery) && /(洗衣机|干衣机|保费|保险|住院津贴|智能健康管理设备|生命体征|Beko)/i.test(text)) {
|
||||||
|
return {
|
||||||
|
hit: false,
|
||||||
|
reason: 'no_hit',
|
||||||
|
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(小白|Restorate|维适多)/i.test(normalizedQuery) && /(客服系统|网站|微信|邮件|软胶囊)/i.test(text)) {
|
||||||
|
return {
|
||||||
|
hit: false,
|
||||||
|
reason: 'no_hit',
|
||||||
|
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/火炉原理/i.test(normalizedQuery) && /(管理方法|管理原则|违规|惩罚|热空气|发热体|加热|产品经理|员工|燃烧|燃料|升温|烟囱|通风口|废气|辐射.*对流)/i.test(text)) {
|
||||||
|
return {
|
||||||
|
hit: false,
|
||||||
|
reason: 'no_hit',
|
||||||
|
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(手机|平板|笔记本电脑|智能手表|电脑|以旧换新|分期付款|护肤品|彩妆|香水|化妆品)/i.test(text) && !/(PM|FitLine|细胞营养|Activize|Basics|Restorate|NTC|火炉原理|阿育吠陀)/i.test(text)) {
|
||||||
|
return {
|
||||||
|
hit: false,
|
||||||
|
reason: 'no_hit',
|
||||||
|
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hit: true,
|
hit: true,
|
||||||
reason: 'hit',
|
reason: 'hit',
|
||||||
@@ -139,21 +355,59 @@ class ToolExecutor {
|
|||||||
const responseMode = response_mode === 'snippet' ? 'snippet' : 'answer';
|
const responseMode = response_mode === 'snippet' ? 'snippet' : 'answer';
|
||||||
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
|
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
|
||||||
const rewrittenQuery = await this.rewriteKnowledgeQuery(query, context);
|
const rewrittenQuery = await this.rewriteKnowledgeQuery(query, context);
|
||||||
|
const kbTarget = this.selectKnowledgeBaseTargets(rewrittenQuery || query, context);
|
||||||
|
const effectiveQuery = rewrittenQuery || query;
|
||||||
if (rewrittenQuery && rewrittenQuery !== query) {
|
if (rewrittenQuery && rewrittenQuery !== query) {
|
||||||
console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`);
|
console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`);
|
||||||
}
|
}
|
||||||
|
if (kbTarget.datasetIds.length > 0) {
|
||||||
|
console.log(`[ToolExecutor] searchKnowledge selected dataset_ids=${kbTarget.datasetIds.join(',')} routes=${kbTarget.matchedRoutes.join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
||||||
if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') {
|
if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') {
|
||||||
|
if (arkChatService.isMockMode()) {
|
||||||
|
const latencyMs = Date.now() - startTime;
|
||||||
|
console.warn('[ToolExecutor] Ark KB search skipped: VOLC_ARK_ENDPOINT_ID not configured (knowledge base IDs are set but endpoint is missing)');
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
original_query: query,
|
||||||
|
rewritten_query: effectiveQuery,
|
||||||
|
selected_dataset_ids: kbTarget.datasetIds,
|
||||||
|
selected_kb_routes: kbTarget.matchedRoutes,
|
||||||
|
latency_ms: latencyMs,
|
||||||
|
errorType: 'endpoint_not_configured',
|
||||||
|
error: '知识库已配置但方舟 LLM 端点未配置,请检查 VOLC_ARK_ENDPOINT_ID',
|
||||||
|
source: 'ark_knowledge',
|
||||||
|
hit: false,
|
||||||
|
reason: 'endpoint_not_configured',
|
||||||
|
};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
console.log('[ToolExecutor] Trying Ark Knowledge Search...');
|
console.log('[ToolExecutor] Trying Ark Knowledge Search...');
|
||||||
const result = await this.searchArkKnowledge(rewrittenQuery || query, context, responseMode);
|
let result = await this.searchArkKnowledge(effectiveQuery, context, responseMode, kbTarget.datasetIds, query);
|
||||||
|
if (!result?.hit) {
|
||||||
|
console.log('[ToolExecutor] Ark KB no_hit, retrying without context...');
|
||||||
|
const retryResult = await this.searchArkKnowledge(effectiveQuery, [], responseMode, kbTarget.datasetIds, query);
|
||||||
|
if (retryResult?.hit || retryResult?.reason !== result?.reason) {
|
||||||
|
result = retryResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!result?.hit && responseMode === 'answer') {
|
||||||
|
console.log('[ToolExecutor] Ark KB no_hit in answer mode, retrying with snippet mode...');
|
||||||
|
const snippetResult = await this.searchArkKnowledge(effectiveQuery, [], 'snippet', kbTarget.datasetIds, query);
|
||||||
|
if (snippetResult?.hit) {
|
||||||
|
result = snippetResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
const latencyMs = Date.now() - startTime;
|
const latencyMs = Date.now() - startTime;
|
||||||
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms`);
|
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms`);
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
original_query: query,
|
original_query: query,
|
||||||
rewritten_query: rewrittenQuery || query,
|
rewritten_query: effectiveQuery,
|
||||||
|
selected_dataset_ids: kbTarget.datasetIds,
|
||||||
|
selected_kb_routes: kbTarget.matchedRoutes,
|
||||||
latency_ms: latencyMs,
|
latency_ms: latencyMs,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -162,7 +416,9 @@ class ToolExecutor {
|
|||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
original_query: query,
|
original_query: query,
|
||||||
rewritten_query: rewrittenQuery || query,
|
rewritten_query: effectiveQuery,
|
||||||
|
selected_dataset_ids: kbTarget.datasetIds,
|
||||||
|
selected_kb_routes: kbTarget.matchedRoutes,
|
||||||
latency_ms: latencyMs,
|
latency_ms: latencyMs,
|
||||||
errorType: error.code === 'ECONNABORTED' || /timeout/i.test(error.message) ? 'timeout' : 'request_failed',
|
errorType: error.code === 'ECONNABORTED' || /timeout/i.test(error.message) ? 'timeout' : 'request_failed',
|
||||||
error: `知识库查询失败: ${error.message}`,
|
error: `知识库查询失败: ${error.message}`,
|
||||||
@@ -178,7 +434,9 @@ class ToolExecutor {
|
|||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
original_query: query,
|
original_query: query,
|
||||||
rewritten_query: rewrittenQuery || query,
|
rewritten_query: effectiveQuery,
|
||||||
|
selected_dataset_ids: kbTarget.datasetIds,
|
||||||
|
selected_kb_routes: kbTarget.matchedRoutes,
|
||||||
latency_ms: latencyMs,
|
latency_ms: latencyMs,
|
||||||
errorType: 'not_configured',
|
errorType: 'not_configured',
|
||||||
error: '知识库未配置,请检查 VOLC_ARK_KNOWLEDGE_BASE_IDS',
|
error: '知识库未配置,请检查 VOLC_ARK_KNOWLEDGE_BASE_IDS',
|
||||||
@@ -194,20 +452,24 @@ class ToolExecutor {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedQuery = this.normalizeKnowledgeQueryAlias(originalQuery);
|
const normalizedQuery = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(originalQuery));
|
||||||
const conciseQuery = normalizedQuery.replace(/[,。!?、,.!?\s]+/g, '');
|
const conciseQuery = normalizedQuery.replace(/[,。!?、,.!?\s]+/g, '');
|
||||||
const recentContext = (Array.isArray(context) ? context : [])
|
const recentContext = (Array.isArray(context) ? context : [])
|
||||||
.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim())
|
.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim())
|
||||||
.slice(-6)
|
.slice(-6)
|
||||||
.map((item) => `${item.role === 'user' ? '用户' : '助手'}:${String(item.content || '').trim()}`)
|
.map((item) => `${item.role === 'user' ? '用户' : '助手'}:${String(item.content || '').trim()}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
const deterministicQuery = this.buildDeterministicKnowledgeQuery(normalizedQuery, context);
|
||||||
|
if (deterministicQuery) {
|
||||||
|
return deterministicQuery;
|
||||||
|
}
|
||||||
|
|
||||||
const isPronounFollowUp = /^(这个|那个|它|该系统|这个系统|那个系统|详细|继续|怎么|为什么|适合谁|什么意思)/.test(normalizedQuery);
|
const isPronounFollowUp = /^(这个|那个|它|该系统|这个系统|那个系统|详细|继续|怎么|为什么|适合谁|什么意思)/.test(normalizedQuery);
|
||||||
if (this.hasCanonicalKnowledgeTerm(normalizedQuery) && conciseQuery.length <= 36 && !isPronounFollowUp) {
|
if (this.hasCanonicalKnowledgeTerm(normalizedQuery) && conciseQuery.length <= 36 && !isPronounFollowUp) {
|
||||||
return normalizedQuery;
|
return normalizedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.VOLC_ARK_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID === 'your_ark_endpoint_id') {
|
if (arkChatService.isMockMode()) {
|
||||||
return normalizedQuery;
|
return normalizedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +484,7 @@ class ToolExecutor {
|
|||||||
content: `最近上下文:\n${recentContext || '无'}\n\n当前原始问题:${normalizedQuery}\n\n请输出最终检索词:`,
|
content: `最近上下文:\n${recentContext || '无'}\n\n当前原始问题:${normalizedQuery}\n\n请输出最终检索词:`,
|
||||||
},
|
},
|
||||||
], []);
|
], []);
|
||||||
const rewritten = this.normalizeKnowledgeQueryAlias(String(result.content || '').replace(/^["'“”]+|["'“”]+$/g, '').trim());
|
const rewritten = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(String(result.content || '').replace(/^["'“”]+|["'“”]+$/g, '').trim()));
|
||||||
return rewritten || normalizedQuery;
|
return rewritten || normalizedQuery;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ToolExecutor] rewriteKnowledgeQuery failed:', error.message);
|
console.warn('[ToolExecutor] rewriteKnowledgeQuery failed:', error.message);
|
||||||
@@ -234,12 +496,26 @@ class ToolExecutor {
|
|||||||
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
|
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
|
||||||
* 使用独立的 LLM 调用,专门用于知识库检索场景(如语音通话的工具回调)
|
* 使用独立的 LLM 调用,专门用于知识库检索场景(如语音通话的工具回调)
|
||||||
*/
|
*/
|
||||||
static async searchArkKnowledge(query, context = [], responseMode = 'answer') {
|
static async searchArkKnowledge(query, context = [], responseMode = 'answer', datasetIdsOverride = null, originalQuery = null) {
|
||||||
const endpointId = process.env.VOLC_ARK_ENDPOINT_ID;
|
const endpointId = process.env.VOLC_ARK_ENDPOINT_ID;
|
||||||
const authKey = process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
|
const authKey = process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
|
||||||
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
|
||||||
|
|
||||||
const datasetIds = kbIds.split(',').map(id => id.trim()).filter(Boolean);
|
if (!endpointId || endpointId === 'your_ark_endpoint_id') {
|
||||||
|
console.warn('[ToolExecutor] searchArkKnowledge skipped: VOLC_ARK_ENDPOINT_ID not configured');
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
results: [{ title: '配置缺失', content: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。` }],
|
||||||
|
total: 1,
|
||||||
|
source: 'ark_knowledge',
|
||||||
|
hit: false,
|
||||||
|
reason: 'endpoint_not_configured',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasetIds = Array.isArray(datasetIdsOverride) && datasetIdsOverride.length > 0
|
||||||
|
? datasetIdsOverride.map((id) => String(id || '').trim()).filter(Boolean)
|
||||||
|
: kbIds.split(',').map(id => id.trim()).filter(Boolean);
|
||||||
const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3;
|
const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3;
|
||||||
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.5;
|
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.5;
|
||||||
|
|
||||||
@@ -249,17 +525,30 @@ class ToolExecutor {
|
|||||||
console.log('[ToolExecutor] Empty query, using default: "' + effectiveQuery + '"');
|
console.log('[ToolExecutor] Empty query, using default: "' + effectiveQuery + '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断是否需要注入原始问题(检索词≠原始问题时,LLM需要知道用户实际问了什么)
|
||||||
|
const cleanOriginal = (originalQuery || '').trim();
|
||||||
|
const hasRewrite = cleanOriginal && cleanOriginal !== effectiveQuery;
|
||||||
|
if (hasRewrite) {
|
||||||
|
console.log(`[ToolExecutor] searchArkKnowledge injecting original question: "${cleanOriginal}" (search query: "${effectiveQuery}")`);
|
||||||
|
}
|
||||||
|
|
||||||
// 提取最近 3 轮对话作为上下文(最多 6 条 user/assistant 消息)
|
// 提取最近 3 轮对话作为上下文(最多 6 条 user/assistant 消息)
|
||||||
const recentContext = context
|
const recentContext = context
|
||||||
.filter(m => m.role === 'user' || m.role === 'assistant')
|
.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||||
.slice(responseMode === 'snippet' ? -4 : -6);
|
.slice(responseMode === 'snippet' ? -4 : -6);
|
||||||
|
|
||||||
|
const baseSnippetPrompt = '你是知识库片段提取助手。请基于知识库提取与用户问题最相关的2到4条简洁知识片段,供语音系统继续组织回复。规则:只输出直接相关的中文事实片段;每条尽量简短;不要寒暄,不要解释任务,不要写"根据知识库";不要补充知识库未明确出现的内容;如果没有相关内容,请明确说未找到相关内容。';
|
||||||
|
const baseAnswerPrompt = '你是企业知识库问答助手,回答将直接用于语音播报。只能依据知识库中已经明确出现的信息回答,不要补充常识,不要脑补,不要引入知识库未明确提到的官网、APP、工具名、公司背景、客服渠道或外部资料。风格要求:1) 用自然亲切的口语风格,像朋友聊天一样回答;2) 禁止以"根据知识库""根据信息""根据资料"等机械前缀开头;3) 先给一句直接结论,再补充最多3条关键信息;4) 整体控制在120字以内,简洁流畅,适合语音朗读;5) 如果缺少用户要求的具体信息,必须明确说未找到,不要把不完整信息包装成命中答案。【品牌保护】绝对禁止说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规或不合法。德国PM是1993年成立于德国的合法直销企业,获邓白氏AAA+认证。如果用户问到正规性或传销问题,必须正面回答PM是合法正规的直销企业。';
|
||||||
|
|
||||||
|
let systemContent = responseMode === 'snippet' ? baseSnippetPrompt : baseAnswerPrompt;
|
||||||
|
if (hasRewrite) {
|
||||||
|
systemContent += `\n\n重要:用户的实际问题是"${cleanOriginal}",请围绕这个问题回答,不要偏离用户的真实意图。下方的检索词仅用于匹配知识库文档,不代表用户的真正提问。`;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: responseMode === 'snippet'
|
content: systemContent,
|
||||||
? '你是知识库片段提取助手。请基于知识库提取与用户问题最相关的2到4条简洁知识片段,供语音系统继续组织回复。规则:只输出直接相关的中文事实片段;每条尽量简短;不要寒暄,不要解释你的任务,不要写“根据知识库”;如果没有相关内容,请明确说未找到相关内容。'
|
|
||||||
: '你是一个知识库检索助手。请根据知识库中的内容回答用户问题。如果知识库中没有相关内容,请如实说明。回答时请引用知识库来源。',
|
|
||||||
},
|
},
|
||||||
...recentContext,
|
...recentContext,
|
||||||
{
|
{
|
||||||
@@ -299,7 +588,8 @@ class ToolExecutor {
|
|||||||
|
|
||||||
const choice = response.data.choices?.[0];
|
const choice = response.data.choices?.[0];
|
||||||
const content = choice?.message?.content || '未找到相关信息';
|
const content = choice?.message?.content || '未找到相关信息';
|
||||||
const classified = this.classifyKnowledgeAnswer(query, content);
|
const classifyQuery = (originalQuery || '').trim() || query;
|
||||||
|
const classified = this.classifyKnowledgeAnswer(classifyQuery, content);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
|
|||||||
Reference in New Issue
Block a user