feat(growth,skills,saas,desktop): C线差异化全量实现 — C1日报+C2飞轮+C3引导
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

C3 零配置引导 (P0):
- use-cold-start.ts: 4阶段→6阶段对话驱动状态机 (idle→greeting→industry→identity→task→completed)
- cold-start-mapper.ts: 关键词行业检测 + 肯定/否定/名字提取
- cold_start_prompt.rs: Rust侧6阶段system prompt生成 + 7个测试
- FirstConversationPrompt.tsx: 动态行业卡片 + 行业任务引导 + 通用快捷操作

C1 管家日报 (P0):
- kernel注册DailyReportHand (第8个Hand)
- DailyReportPanel.tsx已存在,事件监听+持久化完整

C2 行业知识飞轮 (P1):
- heartbeat.rs: 经验缓存(EXPERIENCE_CACHE) + check_unresolved_pains增强经验感知
- heartbeat_update_experiences Tauri命令 + VikingStorage持久化
- semantic_router.rs: 经验权重boost(0.05*ln(count+1), 上限0.15) + update_experience_boosts方法
- service.rs: auto_optimize_config() 基于使用频率自动优化行业skill_priorities

验证: tsc 0 errors, cargo check 0 warnings, 7 cold_start + 5 daily_report + 1 experience_boost tests PASS
This commit is contained in:
iven
2026-04-21 18:28:45 +08:00
parent ae56aba366
commit 13507682f7
9 changed files with 741 additions and 74 deletions

View File

@@ -0,0 +1,215 @@
/**
* 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;
}
/**
* 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;
}
}

View File

@@ -1,10 +1,11 @@
/**
* useColdStart - Cold start state management hook
* useColdStart - 6-stage conversation-driven cold start state machine
*
* Detects first-time users and manages the cold start greeting flow.
* Reuses the onboarding completion key to determine if user is new.
* Stages:
* idle → agent_greeting → industry_discovery → identity_setup → first_task → completed
*
* Flow: idle -> greeting_sent -> waiting_response -> completed
* The agent guides the user through onboarding via natural conversation,
* not forms. Each stage has a distinct system prompt and trigger logic.
*/
import { useState, useEffect, useCallback } from 'react';
@@ -12,61 +13,121 @@ import { createLogger } from './logger';
const log = createLogger('useColdStart');
// Reuse the same key from use-onboarding.ts
const ONBOARDING_COMPLETED_KEY = 'zclaw-onboarding-completed';
// Cold start state persisted to localStorage
const COLD_START_STATE_KEY = 'zclaw-cold-start-state';
// Re-export UserProfile for consumers that need it
export type { UserProfile } from './use-onboarding';
// === Types ===
export type ColdStartPhase = 'idle' | 'greeting_sent' | 'waiting_response' | 'completed';
export type ColdStartPhase =
| 'idle'
| 'agent_greeting'
| 'industry_discovery'
| 'identity_setup'
| 'first_task'
| 'completed';
export interface ColdStartConfig {
detectedIndustry?: string;
suggestedName?: string;
personality?: { tone: string; formality: string; proactiveness: string };
prioritySkills?: string[];
}
export interface ColdStartState {
isColdStart: boolean;
phase: ColdStartPhase;
config: ColdStartConfig;
greetingSent: boolean;
markGreetingSent: () => void;
markWaitingResponse: () => void;
advanceTo: (phase: ColdStartPhase) => void;
updateConfig: (partial: Partial<ColdStartConfig>) => void;
markCompleted: () => void;
getGreetingMessage: (agentName?: string, agentEmoji?: string) => string;
}
// === Default Greeting ===
// === Stage Prompts (frontend-side, for greeting + quick-action context) ===
const DEFAULT_GREETING_BODY =
'我可以帮您处理写作、研究、数据分析、内容生成等各类任务。\n\n请告诉我您需要什么帮助';
const STAGE_GREETINGS: Record<ColdStartPhase, string> = {
idle: '',
agent_greeting: '你好!我是你的 AI 管家。很高兴认识你!你平时主要做什么工作?',
industry_discovery: '',
identity_setup: '',
first_task: '',
completed: '',
};
const FALLBACK_GREETING =
'您好!我是您的工作助手。我可以帮您处理写作、研究、数据分析、内容生成等各类任务。请告诉我您需要什么帮助?';
// Industry quick-action cards shown during industry_discovery
export const INDUSTRY_CARDS = [
{ key: 'healthcare', label: '🏥 医疗行政', description: '排班、报表、医保管理' },
{ key: 'education', label: '🎓 教育培训', description: '课程、成绩、教学计划' },
{ key: 'garment', label: '🏭 制衣制造', description: '订单、面料、生产排期' },
{ key: 'ecommerce', label: '🛒 电商零售', description: '库存、促销、物流跟踪' },
] as const;
// === Persistence Helpers ===
// First-task suggestions per industry
export const INDUSTRY_FIRST_TASKS: Record<string, { label: string; prompt: string }[]> = {
healthcare: [
{ label: '排班查询', prompt: '帮我查一下本周科室排班情况' },
{ label: '数据报表', prompt: '帮我整理上个月的门诊量数据报表' },
{ label: '政策查询', prompt: '最新医保报销政策有哪些变化?' },
{ label: '会议纪要', prompt: '帮我整理今天科室会议的纪要' },
{ label: '库存管理', prompt: '帮我检查一下本月科室耗材库存情况' },
],
education: [
{ label: '课程安排', prompt: '帮我安排下周的课程表' },
{ label: '成绩分析', prompt: '帮我分析这学期学生的成绩分布' },
{ label: '教学计划', prompt: '帮我制定下学期的教学计划' },
{ label: '培训方案', prompt: '帮我设计一个教师培训方案' },
{ label: '测验生成', prompt: '帮我出一份数学测验题' },
],
garment: [
{ label: '订单跟踪', prompt: '帮我查一下这批订单的生产进度' },
{ label: '面料管理', prompt: '帮我整理当前面料库存数据' },
{ label: '生产排期', prompt: '帮我安排下周的生产计划' },
{ label: '成本核算', prompt: '帮我核算这批订单的成本' },
{ label: '质检报告', prompt: '帮我生成这批产品的质检报告' },
],
ecommerce: [
{ label: '库存预警', prompt: '帮我检查哪些商品需要补货' },
{ label: '销售分析', prompt: '帮我分析本周各品类销售数据' },
{ label: '促销方案', prompt: '帮我设计一个促销活动方案' },
{ label: '物流跟踪', prompt: '帮我查一下今天发出的订单物流状态' },
{ label: '商品描述', prompt: '帮我写一个新商品的详情页文案' },
],
_default: [
{ label: '写一篇文章', prompt: '帮我写一篇关于工作总结的文章' },
{ label: '研究分析', prompt: '帮我深度研究一个行业趋势' },
{ label: '数据整理', prompt: '帮我整理一份Excel数据' },
{ label: '内容生成', prompt: '帮我生成一份工作方案' },
{ label: '学习探索', prompt: '帮我了解一个新领域的基础知识' },
],
};
// === Persistence ===
interface PersistedColdStart {
phase: ColdStartPhase;
config: ColdStartConfig;
}
function loadPersistedPhase(): ColdStartPhase {
function loadPersistedState(): PersistedColdStart {
try {
const raw = localStorage.getItem(COLD_START_STATE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as PersistedColdStart;
if (parsed && typeof parsed.phase === 'string') {
return parsed.phase;
return { phase: parsed.phase, config: parsed.config ?? {} };
}
}
} catch (err) {
log.warn('Failed to read cold start state:', err);
}
return 'idle';
return { phase: 'idle', config: {} };
}
function persistPhase(phase: ColdStartPhase): void {
function persistState(phase: ColdStartPhase, config: ColdStartConfig): void {
try {
const data: PersistedColdStart = { phase };
const data: PersistedColdStart = { phase, config };
localStorage.setItem(COLD_START_STATE_KEY, JSON.stringify(data));
} catch (err) {
log.warn('Failed to persist cold start state:', err);
@@ -75,39 +136,33 @@ function persistPhase(phase: ColdStartPhase): void {
// === Greeting Builder ===
const FALLBACK_GREETING = STAGE_GREETINGS.agent_greeting;
function buildGreeting(agentName?: string, agentEmoji?: string): string {
if (!agentName) {
return FALLBACK_GREETING;
}
const emoji = agentEmoji ? ` ${agentEmoji}` : '';
return `好!我是${agentName}${emoji}\n\n${DEFAULT_GREETING_BODY}`;
return `好!我是${agentName}${emoji},你的 AI 管家。很高兴认识你!你平时主要做什么工作?`;
}
// === Hook ===
/**
* Hook to manage cold start state for first-time users.
* 6-stage conversation-driven cold start hook.
*
* A user is considered "cold start" when they have not completed onboarding
* AND have not yet gone through the greeting flow.
* idle → agent_greeting → industry_discovery → identity_setup → first_task → completed
*
* Usage:
* ```tsx
* const { isColdStart, phase, markGreetingSent, getGreetingMessage } = useColdStart();
*
* if (isColdStart && phase === 'idle') {
* const msg = getGreetingMessage(agent.name, agent.emoji);
* sendMessage(msg);
* markGreetingSent();
* }
* ```
* The agent drives each transition through natural conversation,
* not form-filling. The frontend provides context-aware UI hints
* (industry cards, task suggestions) that complement the dialogue.
*/
export function useColdStart(): ColdStartState {
const [phase, setPhase] = useState<ColdStartPhase>(loadPersistedPhase);
const persisted = loadPersistedState();
const [phase, setPhase] = useState<ColdStartPhase>(persisted.phase);
const [config, setConfig] = useState<ColdStartConfig>(persisted.config);
const [isColdStart, setIsColdStart] = useState(false);
// Determine cold start status on mount
useEffect(() => {
try {
const onboardingCompleted = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
@@ -117,40 +172,45 @@ export function useColdStart(): ColdStartState {
setIsColdStart(true);
} else {
setIsColdStart(false);
// If onboarding is completed but phase is not completed,
// force phase to completed to avoid stuck states
if (phase !== 'completed') {
setPhase('completed');
persistPhase('completed');
persistState('completed', config);
}
}
} catch (err) {
log.warn('Failed to check cold start status:', err);
setIsColdStart(false);
}
}, [phase]);
}, [phase, config]);
const markGreetingSent = useCallback(() => {
const nextPhase: ColdStartPhase = 'greeting_sent';
const nextPhase: ColdStartPhase = 'industry_discovery';
setPhase(nextPhase);
persistPhase(nextPhase);
log.debug('Cold start: greeting sent');
}, []);
persistState(nextPhase, config);
log.debug('Cold start: greeting sent, entering industry_discovery');
}, [config]);
const markWaitingResponse = useCallback(() => {
const nextPhase: ColdStartPhase = 'waiting_response';
const advanceTo = useCallback((nextPhase: ColdStartPhase) => {
setPhase(nextPhase);
persistPhase(nextPhase);
log.debug('Cold start: waiting for user response');
}, []);
persistState(nextPhase, config);
log.debug(`Cold start: advanced to ${nextPhase}`);
}, [config]);
const updateConfig = useCallback((partial: Partial<ColdStartConfig>) => {
setConfig((prev) => {
const next = { ...prev, ...partial };
persistState(phase, next);
return next;
});
}, [phase]);
const markCompleted = useCallback(() => {
const nextPhase: ColdStartPhase = 'completed';
setPhase(nextPhase);
persistPhase(nextPhase);
persistState(nextPhase, config);
setIsColdStart(false);
log.debug('Cold start: completed');
}, []);
}, [config]);
const getGreetingMessage = useCallback(
(agentName?: string, agentEmoji?: string): string => {
@@ -162,38 +222,35 @@ export function useColdStart(): ColdStartState {
return {
isColdStart,
phase,
greetingSent: phase === 'greeting_sent' || phase === 'waiting_response' || phase === 'completed',
config,
greetingSent: phase !== 'idle',
markGreetingSent,
markWaitingResponse,
advanceTo,
updateConfig,
markCompleted,
getGreetingMessage,
};
}
// === Non-hook Accessor ===
// === Non-hook Accessors ===
/**
* Get cold start state without React hook (for use outside components).
*/
export function getColdStartState(): { isColdStart: boolean; phase: ColdStartPhase } {
export function getColdStartState(): { isColdStart: boolean; phase: ColdStartPhase; config: ColdStartConfig } {
try {
const onboardingCompleted = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
const isNewUser = onboardingCompleted !== 'true';
const phase = loadPersistedPhase();
const state = loadPersistedState();
return {
isColdStart: isNewUser && phase !== 'completed',
phase,
isColdStart: isNewUser && state.phase !== 'completed',
phase: state.phase,
config: state.config,
};
} catch (err) {
log.warn('Failed to get cold start state:', err);
return { isColdStart: false, phase: 'completed' };
return { isColdStart: false, phase: 'completed', config: {} };
}
}
/**
* Reset cold start state (for testing or debugging).
*/
export function resetColdStartState(): void {
try {
localStorage.removeItem(COLD_START_STATE_KEY);