Files
zclaw_openfang/desktop/src/lib/cold-start-mapper.ts
iven 394cb66311
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
fix(identity): 重构 agent 命名检测正则 — 覆盖"名称改为小芳"等表达
detectAgentNameSuggestion 从固定正则改为 trigger+extract 两步法,
10 个 trigger 模式覆盖中文/英文常见命名表达,stopWords 过滤误匹配。
同时修复 streamStore content 类型处理和 RightPanel 重复事件监听。
2026-04-23 13:13:40 +08:00

258 lines
8.8 KiB
TypeScript

/**
* cold-start-mapper - Extract configuration from conversation content
*
* Maps user messages to cold start config (industry, name, personality, skills).
* Uses keyword matching for deterministic extraction; LLM can refine later.
*/
// cold-start-mapper: keyword-based extraction for cold start configuration
// Future: LLM-based extraction fallback will use structured logger
// === Industry Detection ===
interface IndustryPattern {
id: string;
keywords: string[];
}
const INDUSTRY_PATTERNS: IndustryPattern[] = [
{
id: 'healthcare',
keywords: ['医院', '医疗', '护士', '医生', '科室', '排班', '病历', '门诊', '住院', '行政', '护理', '医保', '挂号'],
},
{
id: 'education',
keywords: ['学校', '教育', '教师', '老师', '学生', '课程', '培训', '教学', '考试', '成绩', '教务', '班级'],
},
{
id: 'garment',
keywords: ['制衣', '服装', '面料', '打版', '缝纫', '裁床', '纺织', '生产', '工厂', '订单', '出货'],
},
{
id: 'ecommerce',
keywords: ['电商', '店铺', '商品', '库存', '物流', '客服', '促销', '直播', '选品', 'SKU', '运营', '零售'],
},
];
export interface ColdStartMapping {
detectedIndustry?: string;
confidence: number;
suggestedName?: string;
personality?: { tone: string; formality: string; proactiveness: string };
prioritySkills?: string[];
}
const INDUSTRY_SKILL_MAP: Record<string, string[]> = {
healthcare: ['data_report', 'schedule_query', 'policy_search', 'meeting_notes'],
education: ['data_report', 'schedule_query', 'content_writing', 'meeting_notes'],
garment: ['data_report', 'schedule_query', 'inventory_mgmt', 'order_tracking'],
ecommerce: ['data_report', 'inventory_mgmt', 'order_tracking', 'content_writing'],
};
const INDUSTRY_NAME_SUGGESTIONS: Record<string, string[]> = {
healthcare: ['小医', '医管家', '康康'],
education: ['小教', '学伴', '知了'],
garment: ['小织', '裁缝', '布管家'],
ecommerce: ['小商', '掌柜', '店小二'],
};
const INDUSTRY_PERSONALITY: Record<string, { tone: string; formality: string; proactiveness: string }> = {
healthcare: { tone: 'professional', formality: 'formal', proactiveness: 'moderate' },
education: { tone: 'friendly', formality: 'semi-formal', proactiveness: 'moderate' },
garment: { tone: 'practical', formality: 'semi-formal', proactiveness: 'low' },
ecommerce: { tone: 'energetic', formality: 'casual', proactiveness: 'high' },
};
/**
* Detect industry from user message using keyword matching.
*/
export function detectIndustry(message: string): ColdStartMapping {
if (!message || message.trim().length === 0) {
return { confidence: 0 };
}
const lower = message.toLowerCase();
let bestMatch = '';
let bestScore = 0;
for (const pattern of INDUSTRY_PATTERNS) {
let score = 0;
for (const keyword of pattern.keywords) {
if (lower.includes(keyword)) {
score += 1;
}
}
if (score > bestScore) {
bestScore = score;
bestMatch = pattern.id;
}
}
// Require at least 1 keyword match
if (bestScore === 0) {
return { confidence: 0 };
}
const confidence = Math.min(bestScore / 3, 1);
const names = INDUSTRY_NAME_SUGGESTIONS[bestMatch] ?? [];
const suggestedName = names.length > 0 ? names[0] : undefined;
return {
detectedIndustry: bestMatch,
confidence,
suggestedName,
personality: INDUSTRY_PERSONALITY[bestMatch],
prioritySkills: INDUSTRY_SKILL_MAP[bestMatch],
};
}
/**
* Detect if user is agreeing/confirming something.
*/
export function detectAffirmative(message: string): boolean {
if (!message) return false;
const affirmativePatterns = ['好', '可以', '行', '没问题', '是的', '对', '嗯', 'OK', 'ok', '确认', '同意'];
const lower = message.toLowerCase().trim();
return affirmativePatterns.some((p) => lower === p || lower.startsWith(p));
}
/**
* Detect if user is rejecting something.
*/
export function detectNegative(message: string): boolean {
if (!message) return false;
const negativePatterns = ['不', '不要', '算了', '换一个', '换', '不好', '不行', '其他', '别的'];
const lower = message.toLowerCase().trim();
return negativePatterns.some((p) => lower === p || lower.startsWith(p));
}
/**
* Detect if user provides a name suggestion.
*/
export function detectNameSuggestion(message: string): string | undefined {
if (!message) return undefined;
// Match patterns like "叫我小王" "叫XX" "用XX" "叫 XX 吧"
const patterns = [/叫[我它他她]?[""''「」]?(\S{1,8})[""''「」]?[吧。!]?$/, /用[""''「」]?(\S{1,8})[""''「」]?[吧。!]?$/];
for (const pattern of patterns) {
const match = message.match(pattern);
if (match && match[1]) {
const name = match[1].replace(/[吧。!,、]/g, '').trim();
if (name.length >= 1 && name.length <= 8) {
return name;
}
}
}
return undefined;
}
/**
* Detect if user gives the agent a name.
* Covers: "叫你小马", "你就叫小芳", "名称改为小芳", "名字叫小马",
* "改名为X", "起名X", "称呼你为X", English patterns, etc.
*/
export function detectAgentNameSuggestion(message: string): string | undefined {
if (!message || typeof message !== 'string') return undefined;
// Trigger phrases: the name appears RIGHT AFTER the matched trigger
const triggers = [
/叫你\s*[""''「」]?/, // "叫你小马"
/你就叫\s*[""''「」]?/, // "你就叫小芳"
/你(?:以後|以后)?叫\s*[""''「」]?/, // "你叫小马" / "你以后叫小马"
/[名].{0,2}[为是叫成]\s*[""''「」]?/, // "名称改为" / "名字是" / "名称改成"
/改[名为称叫]\s*[""''「」]?/, // "改名为X" / "改名X" / "改称X"
/起[个]?名[字]?(?:叫)?\s*[""''「」]?/, // "起名X" / "起名叫X"
/称呼[你你].{0,2}[为是]\s*[""''「」]?/, // "称呼你为X"
/\bname you\s+/i,
/\bcall you\s+/i,
/\byour name\s+(?:is|should be)\s+/i,
];
const stopWords = new Set([
'你', '我', '他', '她', '它', '的', '了', '是', '在', '有', '不',
'也', '都', '还', '又', '这', '那', '什么', '怎么', '为什么', '可以',
'能', '会', '要', '想', '去', '来', '做', '说', '看', '好', '吧',
'呢', '啊', '哦', '嗯', '哈', '呀', '嘛',
]);
for (const trigger of triggers) {
const m = message.match(trigger);
if (!m) continue;
// Extract 1-6 Chinese characters or word chars after the trigger
const rest = message.slice(m.index! + m[0].length);
const nameMatch = rest.match(/^[""''「」]?([一-鿿]{1,6}|\w{1,10})/);
if (nameMatch && nameMatch[1]) {
const raw = nameMatch[1].replace(/[吧。!,、呢啊了]+$/g, '').trim();
if (raw.length >= 1 && raw.length <= 8 && !stopWords.has(raw)) {
return raw;
}
}
}
return undefined;
}
/**
* Determine the next cold start phase based on current phase and user message.
*/
export function determinePhaseTransition(
currentPhase: string,
userMessage: string,
): { nextPhase: string; mapping?: ColdStartMapping } | null {
switch (currentPhase) {
case 'agent_greeting': {
const mapping = detectIndustry(userMessage);
if (mapping.detectedIndustry && mapping.confidence > 0.3) {
return { nextPhase: 'industry_discovery', mapping };
}
// User responded but no industry detected — keep probing
return null;
}
case 'industry_discovery': {
if (detectAffirmative(userMessage)) {
return { nextPhase: 'identity_setup' };
}
if (detectNegative(userMessage)) {
// Try to re-detect from the rejection
const mapping = detectIndustry(userMessage);
if (mapping.detectedIndustry) {
return { nextPhase: 'industry_discovery', mapping };
}
return null;
}
// Direct industry mention
const mapping = detectIndustry(userMessage);
if (mapping.detectedIndustry) {
return { nextPhase: 'identity_setup', mapping };
}
return null;
}
case 'identity_setup': {
const customName = detectNameSuggestion(userMessage);
if (customName) {
return {
nextPhase: 'first_task',
mapping: { confidence: 1, suggestedName: customName },
};
}
if (detectAffirmative(userMessage)) {
return { nextPhase: 'first_task' };
}
if (detectNegative(userMessage)) {
return null; // Stay in identity_setup for another suggestion
}
// User said something else, treat as name preference
return {
nextPhase: 'first_task',
mapping: { confidence: 0.5, suggestedName: userMessage.trim().slice(0, 8) },
};
}
case 'first_task': {
// Any message in first_task is a real task — mark completed
return { nextPhase: 'completed' };
}
default:
return null;
}
}