fix: deep audit round 2 — non-streaming mode config + ClarificationCard + settings restructure
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
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
HIGH fixes: - H-NS-1: Non-streaming agent_chat now builds ChatModeConfig and calls send_message_with_chat_mode(), matching the streaming path - H-FE-1: ClarificationCard component renders structured clarification questions with type badge, question text, and numbered options - H-SEM-1: SemanticSkillRouter annotated as @reserved (Phase 3 wiring) MEDIUM fixes: - M-SETTINGS-1: Settings menu restructured with "高级" section separator; skills/audit/tasks/heartbeat/semantic-memory grouped under advanced - M-MAN-1: LoopEvent→StreamChatEvent mapping completeness checklist added as documentation comment in agent_chat_stream loop - M-ORPH-1: Deleted orphaned Automation/ and SkillMarket/ files, plus transitively orphaned types, hooks, and adapters
This commit is contained in:
@@ -4,6 +4,10 @@
|
|||||||
//! 1. TF-IDF based text similarity (always available, no external deps)
|
//! 1. TF-IDF based text similarity (always available, no external deps)
|
||||||
//! 2. Optional embedding similarity (when an Embedder is configured)
|
//! 2. Optional embedding similarity (when an Embedder is configured)
|
||||||
//! 3. Optional LLM fallback for ambiguous cases
|
//! 3. Optional LLM fallback for ambiguous cases
|
||||||
|
//!
|
||||||
|
//! **@reserved** — This module is fully implemented (719 lines, 200+ lines of tests)
|
||||||
|
//! but NOT yet wired into the message pipeline. It will be activated in Phase 3
|
||||||
|
//! when the "butler mode" semantic auto-routing feature is enabled. Do NOT delete.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ use crate::intelligence::validation::validate_string_length;
|
|||||||
pub struct ChatRequest {
|
pub struct ChatRequest {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
/// Enable extended thinking/reasoning
|
||||||
|
#[serde(default)]
|
||||||
|
pub thinking_enabled: Option<bool>,
|
||||||
|
/// Reasoning effort level (low/medium/high)
|
||||||
|
#[serde(default)]
|
||||||
|
pub reasoning_effort: Option<String>,
|
||||||
|
/// Enable plan mode
|
||||||
|
#[serde(default)]
|
||||||
|
pub plan_mode: Option<bool>,
|
||||||
|
/// Enable sub-agent delegation (Ultra mode only)
|
||||||
|
#[serde(default)]
|
||||||
|
pub subagent_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chat response
|
/// Chat response
|
||||||
@@ -88,7 +100,23 @@ pub async fn agent_chat(
|
|||||||
let id: AgentId = request.agent_id.parse()
|
let id: AgentId = request.agent_id.parse()
|
||||||
.map_err(|_| "Invalid agent ID format".to_string())?;
|
.map_err(|_| "Invalid agent ID format".to_string())?;
|
||||||
|
|
||||||
let response = kernel.send_message(&id, request.message)
|
// Build chat mode config from request fields
|
||||||
|
let chat_mode = if request.thinking_enabled.is_some()
|
||||||
|
|| request.reasoning_effort.is_some()
|
||||||
|
|| request.plan_mode.is_some()
|
||||||
|
|| request.subagent_enabled.is_some()
|
||||||
|
{
|
||||||
|
Some(zclaw_kernel::ChatModeConfig {
|
||||||
|
thinking_enabled: request.thinking_enabled,
|
||||||
|
reasoning_effort: request.reasoning_effort.clone(),
|
||||||
|
plan_mode: request.plan_mode,
|
||||||
|
subagent_enabled: request.subagent_enabled,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = kernel.send_message_with_chat_mode(&id, request.message, chat_mode)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Chat failed: {}", e))?;
|
.map_err(|e| format!("Chat failed: {}", e))?;
|
||||||
|
|
||||||
@@ -258,6 +286,24 @@ pub async fn agent_chat_stream(
|
|||||||
let stream_timeout = tokio::time::Duration::from_secs(300);
|
let stream_timeout = tokio::time::Duration::from_secs(300);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// === LoopEvent → StreamChatEvent mapping ===
|
||||||
|
//
|
||||||
|
// COMPLETENESS CHECKLIST: When adding a new LoopEvent variant, you MUST:
|
||||||
|
// 1. Add a match arm below
|
||||||
|
// 2. Add the corresponding StreamChatEvent variant (see struct defs above)
|
||||||
|
// 3. Add the TypeScript type in desktop/src/lib/kernel-types.ts
|
||||||
|
// 4. Add a handler in desktop/src/lib/kernel-chat.ts
|
||||||
|
//
|
||||||
|
// Current mapping (LoopEvent → StreamChatEvent):
|
||||||
|
// Delta → Delta
|
||||||
|
// ThinkingDelta → ThinkingDelta
|
||||||
|
// ToolStart → ToolStart / HandStart (if name starts with "hand_")
|
||||||
|
// ToolEnd → ToolEnd / HandEnd (if name starts with "hand_")
|
||||||
|
// SubtaskStatus → SubtaskStatus
|
||||||
|
// IterationStart → IterationStart
|
||||||
|
// Complete → Complete
|
||||||
|
// Error → Error
|
||||||
|
// ============================================
|
||||||
// Check cancellation flag before each recv
|
// Check cancellation flag before each recv
|
||||||
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
tracing::info!("[agent_chat_stream] Stream cancelled for session: {}", session_id);
|
tracing::info!("[agent_chat_stream] Stream cancelled for session: {}", session_id);
|
||||||
|
|||||||
@@ -68,24 +68,28 @@ type SettingsPage =
|
|||||||
| 'feedback'
|
| 'feedback'
|
||||||
| 'about';
|
| 'about';
|
||||||
|
|
||||||
const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode }[] = [
|
const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode; group?: 'advanced' }[] = [
|
||||||
|
// --- Core settings ---
|
||||||
{ id: 'general', label: '通用', icon: <SettingsIcon className="w-4 h-4" /> },
|
{ id: 'general', label: '通用', icon: <SettingsIcon className="w-4 h-4" /> },
|
||||||
{ id: 'usage', label: '用量统计', icon: <BarChart3 className="w-4 h-4" /> },
|
{ id: 'usage', label: '用量统计', icon: <BarChart3 className="w-4 h-4" /> },
|
||||||
{ id: 'credits', label: '积分详情', icon: <Coins className="w-4 h-4" /> },
|
{ id: 'credits', label: '积分详情', icon: <Coins className="w-4 h-4" /> },
|
||||||
{ id: 'models', label: '模型与 API', icon: <Cpu className="w-4 h-4" /> },
|
{ id: 'models', label: '模型与 API', icon: <Cpu className="w-4 h-4" /> },
|
||||||
{ id: 'mcp', label: 'MCP 服务', icon: <Puzzle className="w-4 h-4" /> },
|
{ id: 'mcp', label: 'MCP 服务', icon: <Puzzle className="w-4 h-4" /> },
|
||||||
{ id: 'skills', label: '技能', icon: <Zap className="w-4 h-4" /> },
|
|
||||||
{ id: 'im', label: 'IM 频道', icon: <MessageSquare className="w-4 h-4" /> },
|
{ id: 'im', label: 'IM 频道', icon: <MessageSquare className="w-4 h-4" /> },
|
||||||
{ id: 'workspace', label: '工作区', icon: <FolderOpen className="w-4 h-4" /> },
|
{ id: 'workspace', label: '工作区', icon: <FolderOpen className="w-4 h-4" /> },
|
||||||
{ id: 'privacy', label: '数据与隐私', icon: <Shield className="w-4 h-4" /> },
|
{ id: 'privacy', label: '数据与隐私', icon: <Shield className="w-4 h-4" /> },
|
||||||
{ id: 'storage', label: '安全存储', icon: <Key className="w-4 h-4" /> },
|
{ id: 'storage', label: '安全存储', icon: <Key className="w-4 h-4" /> },
|
||||||
|
// --- SaaS / Billing ---
|
||||||
{ id: 'saas', label: 'SaaS 平台', icon: <Cloud className="w-4 h-4" /> },
|
{ id: 'saas', label: 'SaaS 平台', icon: <Cloud className="w-4 h-4" /> },
|
||||||
{ id: 'billing', label: '订阅与计费', icon: <CreditCard className="w-4 h-4" /> },
|
{ id: 'billing', label: '订阅与计费', icon: <CreditCard className="w-4 h-4" /> },
|
||||||
{ id: 'viking', label: '语义记忆', icon: <Database className="w-4 h-4" /> },
|
// --- Advanced ---
|
||||||
{ id: 'security', label: '安全状态', icon: <Shield className="w-4 h-4" /> },
|
{ id: 'skills', label: '技能管理', icon: <Zap className="w-4 h-4" />, group: 'advanced' },
|
||||||
{ id: 'audit', label: '审计日志', icon: <ClipboardList className="w-4 h-4" /> },
|
{ id: 'viking', label: '语义记忆', icon: <Database className="w-4 h-4" />, group: 'advanced' },
|
||||||
{ id: 'tasks', label: '定时任务', icon: <Clock className="w-4 h-4" /> },
|
{ id: 'security', label: '安全状态', icon: <Shield className="w-4 h-4" />, group: 'advanced' },
|
||||||
{ id: 'heartbeat', label: '心跳配置', icon: <Heart className="w-4 h-4" /> },
|
{ id: 'audit', label: '审计日志', icon: <ClipboardList className="w-4 h-4" />, group: 'advanced' },
|
||||||
|
{ id: 'tasks', label: '定时任务', icon: <Clock className="w-4 h-4" />, group: 'advanced' },
|
||||||
|
{ id: 'heartbeat', label: '心跳配置', icon: <Heart className="w-4 h-4" />, group: 'advanced' },
|
||||||
|
// --- Footer ---
|
||||||
{ id: 'feedback', label: '提交反馈', icon: <HelpCircle className="w-4 h-4" /> },
|
{ id: 'feedback', label: '提交反馈', icon: <HelpCircle className="w-4 h-4" /> },
|
||||||
{ id: 'about', label: '关于', icon: <Info className="w-4 h-4" /> },
|
{ id: 'about', label: '关于', icon: <Info className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
@@ -177,20 +181,33 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) {
|
|||||||
|
|
||||||
{/* 导航菜单 */}
|
{/* 导航菜单 */}
|
||||||
<nav className="flex-1 overflow-y-auto custom-scrollbar py-2 px-3 space-y-1">
|
<nav className="flex-1 overflow-y-auto custom-scrollbar py-2 px-3 space-y-1">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item, idx) => {
|
||||||
<button
|
// Insert "高级" separator before first advanced group item
|
||||||
key={item.id}
|
const showAdvancedHeader = item.group === 'advanced'
|
||||||
onClick={() => setActivePage(item.id)}
|
&& (idx === 0 || menuItems[idx - 1]?.group !== 'advanced');
|
||||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
|
|
||||||
activePage === item.id
|
return (
|
||||||
? 'bg-gray-200 text-gray-900 font-medium'
|
<div key={item.id}>
|
||||||
: 'text-gray-500 hover:bg-black/5 hover:text-gray-700'
|
{showAdvancedHeader && (
|
||||||
}`}
|
<div className="flex items-center gap-2 px-3 pt-3 pb-1">
|
||||||
>
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">高级</span>
|
||||||
{item.icon}
|
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||||
<span>{item.label}</span>
|
</div>
|
||||||
</button>
|
)}
|
||||||
))}
|
<button
|
||||||
|
onClick={() => setActivePage(item.id)}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
|
||||||
|
activePage === item.id
|
||||||
|
? 'bg-gray-200 text-gray-900 font-medium'
|
||||||
|
: 'text-gray-500 hover:bg-black/5 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,23 @@ function ToolStepRow({ step, isActive, showConnector }: ToolStepRowProps) {
|
|||||||
const isRunning = step.status === 'running';
|
const isRunning = step.status === 'running';
|
||||||
const isError = step.status === 'error';
|
const isError = step.status === 'error';
|
||||||
|
|
||||||
|
// Parse clarification output for special rendering
|
||||||
|
let clarificationData: { question: string; clarificationType: string; options?: string[] } | null = null;
|
||||||
|
if (step.toolName === 'ask_clarification' && step.output) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(step.output);
|
||||||
|
if (parsed.status === 'clarification_needed' && parsed.question) {
|
||||||
|
clarificationData = {
|
||||||
|
question: parsed.question,
|
||||||
|
clarificationType: parsed.clarification_type || 'missing_info',
|
||||||
|
options: Array.isArray(parsed.options) ? parsed.options : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, show as normal tool output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
@@ -231,15 +248,26 @@ function ToolStepRow({ step, isActive, showConnector }: ToolStepRowProps) {
|
|||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="ml-9 mr-2 mb-1 space-y-1">
|
<div className="ml-9 mr-2 mb-1 space-y-1">
|
||||||
{step.input && (
|
{/* Clarification card — special rendering */}
|
||||||
<div className="text-[11px] text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80 rounded px-2 py-1 font-mono overflow-x-auto">
|
{clarificationData ? (
|
||||||
{truncate(step.input, 500)}
|
<ClarificationCard
|
||||||
</div>
|
question={clarificationData.question}
|
||||||
)}
|
clarificationType={clarificationData.clarificationType}
|
||||||
{step.output && (
|
options={clarificationData.options}
|
||||||
<div className={`text-[11px] font-mono rounded px-2 py-1 overflow-x-auto ${isError ? 'text-red-500 bg-red-50 dark:bg-red-900/10' : 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/10'}`}>
|
/>
|
||||||
{truncate(step.output, 500)}
|
) : (
|
||||||
</div>
|
<>
|
||||||
|
{step.input && (
|
||||||
|
<div className="text-[11px] text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80 rounded px-2 py-1 font-mono overflow-x-auto">
|
||||||
|
{truncate(step.input, 500)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.output && (
|
||||||
|
<div className={`text-[11px] font-mono rounded px-2 py-1 overflow-x-auto ${isError ? 'text-red-500 bg-red-50 dark:bg-red-900/10' : 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/10'}`}>
|
||||||
|
{truncate(step.output, 500)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -253,3 +281,53 @@ function ToolStepRow({ step, isActive, showConnector }: ToolStepRowProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ClarificationCard — structured question display
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const CLARIFICATION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
|
missing_info: { label: '缺少信息', color: 'text-amber-500' },
|
||||||
|
ambiguous_requirement: { label: '需求模糊', color: 'text-blue-500' },
|
||||||
|
approach_choice: { label: '方案选择', color: 'text-purple-500' },
|
||||||
|
risk_confirmation: { label: '风险确认', color: 'text-red-500' },
|
||||||
|
suggestion: { label: '建议', color: 'text-green-500' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function ClarificationCard({ question, clarificationType, options }: {
|
||||||
|
question: string;
|
||||||
|
clarificationType: string;
|
||||||
|
options?: string[];
|
||||||
|
}) {
|
||||||
|
const typeInfo = CLARIFICATION_TYPE_LABELS[clarificationType] || { label: '澄清', color: 'text-gray-500' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-amber-200 dark:border-amber-800/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2.5">
|
||||||
|
{/* Type badge */}
|
||||||
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
|
<HelpCircle className={`w-3.5 h-3.5 ${typeInfo.color}`} />
|
||||||
|
<span className={`text-[11px] font-medium ${typeInfo.color}`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Question */}
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{question}
|
||||||
|
</p>
|
||||||
|
{/* Options */}
|
||||||
|
{options && options.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{options.map((opt, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-start gap-2 text-xs text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-gray-400 dark:text-gray-500 mt-px">{idx + 1}.</span>
|
||||||
|
<span>{opt}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user