feat(server): KB prompt优化、字幕修复、S2S重连、助手配置API
- assistantProfileConfig: KB answer prompt改为分层策略(严格产品信息+灵活常识补充) - nativeVoiceGateway: S2S upstream自动重连(最多50次)、event 351字幕debounce(800ms取最长文本) - toolExecutor: 确定性query改写增强、KB查询传递session上下文 - contextKeywordTracker: 支持KB话题记忆优先enrichment - contentSafeGuard: 新增品牌安全内容过滤服务 - assistantProfileService: 新增助手配置CRUD服务 - routes/assistantProfile: 新增助手配置API路由 - knowledgeKeywords: 扩展KB关键词词典 - fastAsrCorrector: ASR纠错规则更新 - tests/: KB prompt测试、保护窗口测试、Viking性能测试 - docs/: 助手配置API文档、系统提示词目录
This commit is contained in:
178
test2/server/services/assistantProfileService.js
Normal file
178
test2/server/services/assistantProfileService.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const axios = require('axios');
|
||||
const {
|
||||
DEFAULT_VOICE_ASSISTANT_PROFILE,
|
||||
resolveAssistantProfile,
|
||||
} = require('./assistantProfileConfig');
|
||||
|
||||
const assistantProfileCache = new Map();
|
||||
|
||||
function getAssistantProfileApiUrl() {
|
||||
const value = String(process.env.ASSISTANT_PROFILE_API_URL || '').trim();
|
||||
return value && !value.startsWith('your_') ? value : '';
|
||||
}
|
||||
|
||||
function getAssistantProfileApiMethod() {
|
||||
const method = String(process.env.ASSISTANT_PROFILE_API_METHOD || 'GET').trim().toUpperCase();
|
||||
return method === 'POST' ? 'POST' : 'GET';
|
||||
}
|
||||
|
||||
function getAssistantProfileTimeoutMs() {
|
||||
const value = Number(process.env.ASSISTANT_PROFILE_API_TIMEOUT_MS || 5000);
|
||||
return Number.isFinite(value) && value > 0 ? value : 5000;
|
||||
}
|
||||
|
||||
function getAssistantProfileCacheTtlMs() {
|
||||
const value = Number(process.env.ASSISTANT_PROFILE_CACHE_TTL_MS || 60000);
|
||||
return Number.isFinite(value) && value >= 0 ? value : 60000;
|
||||
}
|
||||
|
||||
function getAssistantProfileCacheKey(userId = null) {
|
||||
return String(userId || 'global').trim() || 'global';
|
||||
}
|
||||
|
||||
function parseAssistantProfileHeaders() {
|
||||
const raw = String(process.env.ASSISTANT_PROFILE_API_HEADERS || '').trim();
|
||||
if (!raw) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return {};
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(parsed)
|
||||
.map(([key, value]) => [String(key || '').trim(), String(value || '').trim()])
|
||||
.filter(([key, value]) => key && value)
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[AssistantProfile] parse headers failed:', error.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function pickAssistantProfilePayload(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return {};
|
||||
}
|
||||
const candidates = [
|
||||
payload.assistantProfile,
|
||||
payload.profile,
|
||||
payload.data?.assistantProfile,
|
||||
payload.data?.profile,
|
||||
payload.data,
|
||||
payload,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function sanitizeAssistantProfilePayload(payload) {
|
||||
const source = pickAssistantProfilePayload(payload);
|
||||
return {
|
||||
documents: source.documents,
|
||||
email: source.email,
|
||||
nickname: source.nickname,
|
||||
wxl: source.wxl,
|
||||
mobile: source.mobile,
|
||||
wx_code: source.wx_code,
|
||||
intro: source.intro,
|
||||
sign: source.sign,
|
||||
story: source.story,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchRemoteAssistantProfile(userId = null) {
|
||||
const url = getAssistantProfileApiUrl();
|
||||
if (!url) {
|
||||
return {
|
||||
profile: resolveAssistantProfile(),
|
||||
source: 'default',
|
||||
cached: false,
|
||||
fetchedAt: Date.now(),
|
||||
configured: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
...parseAssistantProfileHeaders(),
|
||||
};
|
||||
const token = String(process.env.ASSISTANT_PROFILE_API_TOKEN || '').trim();
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const method = getAssistantProfileApiMethod();
|
||||
const timeout = getAssistantProfileTimeoutMs();
|
||||
const params = userId ? { userId } : undefined;
|
||||
const response = method === 'POST'
|
||||
? await axios.post(url, userId ? { userId } : {}, { headers, timeout })
|
||||
: await axios.get(url, { headers, timeout, params });
|
||||
const profile = resolveAssistantProfile(sanitizeAssistantProfilePayload(response.data));
|
||||
return {
|
||||
profile,
|
||||
source: 'remote_api',
|
||||
cached: false,
|
||||
fetchedAt: Date.now(),
|
||||
configured: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAssistantProfile(options = {}) {
|
||||
const userId = String(options.userId || '').trim() || null;
|
||||
const forceRefresh = !!options.forceRefresh;
|
||||
const overrides = options.overrides && typeof options.overrides === 'object' ? options.overrides : null;
|
||||
const cacheKey = getAssistantProfileCacheKey(userId);
|
||||
const ttlMs = getAssistantProfileCacheTtlMs();
|
||||
const cached = assistantProfileCache.get(cacheKey);
|
||||
|
||||
if (!forceRefresh && cached && (Date.now() - cached.fetchedAt) <= ttlMs) {
|
||||
return {
|
||||
profile: resolveAssistantProfile({ ...cached.profile, ...(overrides || {}) }),
|
||||
source: cached.source,
|
||||
cached: true,
|
||||
fetchedAt: cached.fetchedAt,
|
||||
configured: cached.configured,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const fetched = await fetchRemoteAssistantProfile(userId);
|
||||
assistantProfileCache.set(cacheKey, fetched);
|
||||
return {
|
||||
profile: resolveAssistantProfile({ ...fetched.profile, ...(overrides || {}) }),
|
||||
source: fetched.source,
|
||||
cached: false,
|
||||
fetchedAt: fetched.fetchedAt,
|
||||
configured: fetched.configured,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
const fallback = cached?.profile || DEFAULT_VOICE_ASSISTANT_PROFILE;
|
||||
return {
|
||||
profile: resolveAssistantProfile({ ...fallback, ...(overrides || {}) }),
|
||||
source: cached ? 'cache_fallback' : 'default_fallback',
|
||||
cached: !!cached,
|
||||
fetchedAt: cached?.fetchedAt || null,
|
||||
configured: !!getAssistantProfileApiUrl(),
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function clearAssistantProfileCache(userId = null) {
|
||||
if (userId == null || String(userId).trim() === '') {
|
||||
assistantProfileCache.clear();
|
||||
return;
|
||||
}
|
||||
assistantProfileCache.delete(getAssistantProfileCacheKey(userId));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAssistantProfile,
|
||||
clearAssistantProfileCache,
|
||||
};
|
||||
Reference in New Issue
Block a user