fix: resolve 6 remaining defects (P2-18, P2-21, P3-04, P3-05, P3-06, P3-02)
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
P3-03: HTML export now renders key_points in format_scene_content P3-07: SKILL.md/YAML parser handles both single and double quotes P3-09: auto_classify covers 20 categories with keyword matching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -218,11 +218,20 @@ impl HtmlExporter {
|
|||||||
fn format_scene_content(&self, content: &SceneContent) -> String {
|
fn format_scene_content(&self, content: &SceneContent) -> String {
|
||||||
match content.scene_type {
|
match content.scene_type {
|
||||||
SceneType::Slide => {
|
SceneType::Slide => {
|
||||||
|
let mut html = String::new();
|
||||||
if let Some(desc) = content.content.get("description").and_then(|v| v.as_str()) {
|
if let Some(desc) = content.content.get("description").and_then(|v| v.as_str()) {
|
||||||
format!("<p class=\"slide-description\">{}</p>", html_escape(desc))
|
html.push_str(&format!("<p class=\"slide-description\">{}</p>", html_escape(desc)));
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
}
|
||||||
|
if let Some(points) = content.content.get("key_points").and_then(|v| v.as_array()) {
|
||||||
|
let items: String = points.iter()
|
||||||
|
.filter_map(|p| p.as_str().map(|t| format!("<li>{}</li>", html_escape(t))))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
if !items.is_empty() {
|
||||||
|
html.push_str(&format!("<h4>Key Points</h4>\n<ul class=\"key-points\">\n{}\n</ul>", items));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html
|
||||||
}
|
}
|
||||||
SceneType::Quiz => {
|
SceneType::Quiz => {
|
||||||
let questions = content.content.get("questions")
|
let questions = content.content.get("questions")
|
||||||
@@ -744,7 +753,7 @@ mod tests {
|
|||||||
content: SceneContent {
|
content: SceneContent {
|
||||||
title: "Introduction".to_string(),
|
title: "Introduction".to_string(),
|
||||||
scene_type: SceneType::Slide,
|
scene_type: SceneType::Slide,
|
||||||
content: serde_json::json!({"description": "Intro slide"}),
|
content: serde_json::json!({"description": "Intro slide", "key_points": ["Point 1", "Point 2"]}),
|
||||||
actions: vec![SceneAction::Speech {
|
actions: vec![SceneAction::Speech {
|
||||||
text: "Welcome!".to_string(),
|
text: "Welcome!".to_string(),
|
||||||
agent_role: "teacher".to_string(),
|
agent_role: "teacher".to_string(),
|
||||||
@@ -798,6 +807,20 @@ mod tests {
|
|||||||
assert_eq!(format_level(&DifficultyLevel::Expert), "Expert");
|
assert_eq!(format_level(&DifficultyLevel::Expert), "Expert");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_points_rendering() {
|
||||||
|
let exporter = HtmlExporter::new();
|
||||||
|
let classroom = create_test_classroom();
|
||||||
|
let options = ExportOptions::default();
|
||||||
|
|
||||||
|
let result = exporter.export(&classroom, &options).unwrap();
|
||||||
|
let html = String::from_utf8(result.content).unwrap();
|
||||||
|
assert!(html.contains("<h4>Key Points</h4>"));
|
||||||
|
assert!(html.contains("<ul class=\"key-points\">"));
|
||||||
|
assert!(html.contains("<li>Point 1</li>"));
|
||||||
|
assert!(html.contains("<li>Point 2</li>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_include_notes() {
|
fn test_include_notes() {
|
||||||
let exporter = HtmlExporter::new();
|
let exporter = HtmlExporter::new();
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
let mut capabilities = Vec::new();
|
let mut capabilities = Vec::new();
|
||||||
let mut tags = Vec::new();
|
let mut tags = Vec::new();
|
||||||
let mut triggers = Vec::new();
|
let mut triggers = Vec::new();
|
||||||
|
let mut tools: Vec<String> = Vec::new();
|
||||||
let mut category: Option<String> = None;
|
let mut category: Option<String> = None;
|
||||||
let mut in_triggers_list = false;
|
let mut in_triggers_list = false;
|
||||||
|
let mut in_tools_list = false;
|
||||||
|
|
||||||
// Parse frontmatter if present
|
// Parse frontmatter if present
|
||||||
if content.starts_with("---") {
|
if content.starts_with("---") {
|
||||||
@@ -57,21 +59,29 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
|
|
||||||
// Handle triggers list items
|
// Handle triggers list items
|
||||||
if in_triggers_list && line.starts_with("- ") {
|
if in_triggers_list && line.starts_with("- ") {
|
||||||
triggers.push(line[2..].trim().trim_matches('"').to_string());
|
triggers.push(line[2..].trim().trim_matches(|c| c == '"' || c == '\'').to_string());
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
in_triggers_list = false;
|
in_triggers_list = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tools list items
|
||||||
|
if in_tools_list && line.starts_with("- ") {
|
||||||
|
tools.push(line[2..].trim().trim_matches(|c| c == '"' || c == '\'').to_string());
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
in_tools_list = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse category field
|
// Parse category field
|
||||||
if let Some(cat) = line.strip_prefix("category:") {
|
if let Some(cat) = line.strip_prefix("category:") {
|
||||||
category = Some(cat.trim().trim_matches('"').to_string());
|
category = Some(cat.trim().trim_matches(|c| c == '"' || c == '\'').to_string());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((key, value)) = line.split_once(':') {
|
if let Some((key, value)) = line.split_once(':') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let value = value.trim().trim_matches('"');
|
let value = value.trim().trim_matches(|c| c == '"' || c == '\'');
|
||||||
match key {
|
match key {
|
||||||
"name" => name = value.to_string(),
|
"name" => name = value.to_string(),
|
||||||
"description" => description = value.to_string(),
|
"description" => description = value.to_string(),
|
||||||
@@ -93,7 +103,16 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
in_triggers_list = true;
|
in_triggers_list = true;
|
||||||
} else {
|
} else {
|
||||||
triggers = value.split(',')
|
triggers = value.split(',')
|
||||||
.map(|s| s.trim().trim_matches('"').to_string())
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"tools" => {
|
||||||
|
if value.is_empty() {
|
||||||
|
in_tools_list = true;
|
||||||
|
} else {
|
||||||
|
tools = value.split(',')
|
||||||
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +173,9 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
.replace(' ', "-")
|
.replace(' ', "-")
|
||||||
.replace(|c: char| !c.is_alphanumeric() && c != '-', "");
|
.replace(|c: char| !c.is_alphanumeric() && c != '-', "");
|
||||||
|
|
||||||
|
// P3-09: Auto-classify if category not explicitly set
|
||||||
|
let category = category.or_else(|| auto_classify(&id, &description, &tags));
|
||||||
|
|
||||||
Ok(SkillManifest {
|
Ok(SkillManifest {
|
||||||
id: SkillId::new(&id),
|
id: SkillId::new(&id),
|
||||||
name,
|
name,
|
||||||
@@ -167,6 +189,7 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
tags,
|
tags,
|
||||||
category,
|
category,
|
||||||
triggers,
|
triggers,
|
||||||
|
tools,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -191,6 +214,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
let mut tags = Vec::new();
|
let mut tags = Vec::new();
|
||||||
let mut category: Option<String> = None;
|
let mut category: Option<String> = None;
|
||||||
let mut triggers = Vec::new();
|
let mut triggers = Vec::new();
|
||||||
|
let mut tools: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
@@ -199,7 +223,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
}
|
}
|
||||||
if let Some((key, value)) = line.split_once('=') {
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let value = value.trim().trim_matches('"');
|
let value = value.trim().trim_matches(|c| c == '"' || c == '\'');
|
||||||
match key {
|
match key {
|
||||||
"id" => id = value.to_string(),
|
"id" => id = value.to_string(),
|
||||||
"name" => name = value.to_string(),
|
"name" => name = value.to_string(),
|
||||||
@@ -210,27 +234,34 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
// Simple array parsing
|
// Simple array parsing
|
||||||
let value = value.trim_start_matches('[').trim_end_matches(']');
|
let value = value.trim_start_matches('[').trim_end_matches(']');
|
||||||
capabilities = value.split(',')
|
capabilities = value.split(',')
|
||||||
.map(|s| s.trim().trim_matches('"').to_string())
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
"tags" => {
|
"tags" => {
|
||||||
let value = value.trim_start_matches('[').trim_end_matches(']');
|
let value = value.trim_start_matches('[').trim_end_matches(']');
|
||||||
tags = value.split(',')
|
tags = value.split(',')
|
||||||
.map(|s| s.trim().trim_matches('"').to_string())
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
"triggers" => {
|
"triggers" => {
|
||||||
let value = value.trim_start_matches('[').trim_end_matches(']');
|
let value = value.trim_start_matches('[').trim_end_matches(']');
|
||||||
triggers = value.split(',')
|
triggers = value.split(',')
|
||||||
.map(|s| s.trim().trim_matches('"').to_string())
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
"category" => {
|
"category" => {
|
||||||
category = Some(value.to_string());
|
category = Some(value.to_string());
|
||||||
}
|
}
|
||||||
|
"tools" => {
|
||||||
|
let value = value.trim_start_matches('[').trim_end_matches(']');
|
||||||
|
tools = value.split(',')
|
||||||
|
.map(|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,6 +290,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
tags,
|
tags,
|
||||||
category,
|
category,
|
||||||
triggers,
|
triggers,
|
||||||
|
tools,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -298,3 +330,48 @@ pub fn discover_skills(dir: &Path) -> Result<Vec<PathBuf>> {
|
|||||||
|
|
||||||
Ok(skills)
|
Ok(skills)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// P3-09: Auto-classify skill into a category based on ID, description, and tags.
|
||||||
|
/// Covers 20 categories via keyword matching. Returns None if no confident match.
|
||||||
|
fn auto_classify(id: &str, description: &str, tags: &[String]) -> Option<String> {
|
||||||
|
let id_lower = id.to_lowercase();
|
||||||
|
let desc_lower = description.to_lowercase();
|
||||||
|
let tags_joined = tags.join(" ").to_lowercase();
|
||||||
|
let corpus = format!("{} {} {}", id_lower, desc_lower, tags_joined);
|
||||||
|
|
||||||
|
const RULES: &[(&str, &[&str])] = &[
|
||||||
|
("coding", &["code", "program", "develop", "debug", "refactor", "typescript", "python", "rust", "javascript"]),
|
||||||
|
("writing", &["write", "essay", "article", "blog", "copy", "content", "draft", "editor"]),
|
||||||
|
("research", &["research", "search", "analyze", "investigate", "paper", "study", "survey"]),
|
||||||
|
("math", &["math", "calcul", "equation", "algebra", "geometry", "statistic", "formula"]),
|
||||||
|
("translation", &["translat", "language", "i18n", "localize", "chinese", "english", "japanese"]),
|
||||||
|
("data", &["data", "database", "sql", "csv", "excel", "spreadsheet", "table", "chart"]),
|
||||||
|
("education", &["teach", "learn", "tutor", "quiz", "course", "lesson", "classroom", "study"]),
|
||||||
|
("design", &["design", "ui", "ux", "layout", "figma", "color", "font", "style"]),
|
||||||
|
("marketing", &["market", "seo", "advertis", "brand", "campaign", "social media", "promotion"]),
|
||||||
|
("finance", &["financ", "account", "budget", "invest", "stock", "tax", "invoice", "payment"]),
|
||||||
|
("legal", &["legal", "contract", "compliance", "regulation", "law", "policy", "terms"]),
|
||||||
|
("health", &["health", "medical", "fitness", "nutrition", "mental", "wellness", "doctor"]),
|
||||||
|
("travel", &["travel", "flight", "hotel", "itinerary", "tourism", "vacation", "trip"]),
|
||||||
|
("productivity", &["productiv", "task", "todo", "schedule", "calendar", "remind", "organize", "note"]),
|
||||||
|
("communication", &["email", "chat", "message", "meet", "present", "communicate", "slack"]),
|
||||||
|
("security", &["security", "audit", "vulnerab", "encrypt", "auth", "password", "firewall"]),
|
||||||
|
("devops", &["deploy", "docker", "kubernetes", "ci/cd", "pipeline", "infra", "server", "monitor"]),
|
||||||
|
("image", &["image", "photo", "graphic", "visual", "draw", "illustrat", "canvas", "whiteboard"]),
|
||||||
|
("audio", &["audio", "speech", "voice", "music", "sound", "transcrib", "podcast"]),
|
||||||
|
("automation", &["automat", "workflow", "trigger", "schedule", "batch", "script", "pipeline"]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut best_match: Option<(&str, usize)> = None;
|
||||||
|
for (category, keywords) in RULES {
|
||||||
|
let score = keywords.iter().filter(|kw| corpus.contains(*kw)).count();
|
||||||
|
if score > 0 {
|
||||||
|
match best_match {
|
||||||
|
Some((_, best_score)) if score <= best_score => {}
|
||||||
|
_ => best_match = Some((*category, score)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best_match.map(|(cat, _)| cat.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
# ZCLAW 上线前功能审计 — 缺陷清单
|
# ZCLAW 上线前功能审计 — 缺陷清单
|
||||||
|
|
||||||
> **审计日期**: 2026-04-06 | **最后更新**: 2026-04-06 | **审计范围**: T1-T8 模块 | **基线**: V12 审计 | **最新编译状态**: ✅ cargo check 通过
|
> **审计日期**: 2026-04-06 | **最后更新**: 2026-04-06 (深度审计) | **审计范围**: T1-T8 模块 | **基线**: V12 审计 | **最新编译状态**: ✅ cargo check 通过
|
||||||
|
|
||||||
## 统计总览
|
## 统计总览
|
||||||
|
|
||||||
| 严重度 | V12 遗留 | 新发现 | 已修复 | 当前活跃 |
|
| 严重度 | 已修复 | FALSE_POSITIVE | 实际未修(修复中) |
|
||||||
|--------|---------|--------|--------|---------|
|
|--------|--------|---------------|---------|
|
||||||
| **P0** | 1 | 0 | 1 | **0** |
|
| **P0** | 1 | 0 | 0 |
|
||||||
| **P1** | 11 | 2 | 13 | **0** |
|
| **P1** | 14 | 1 (M11-01) | 0 |
|
||||||
| **P2** | 25 | 2 | 26 | **1** |
|
| **P2** | 27 | 0 | 0 |
|
||||||
| **P3** | 10 | 0 | 10 | **0** |
|
| **P3** | 8 | 0 | 3 (P3-03/P3-07/P3-09) |
|
||||||
| **合计** | **47** | **4** | **50** | **1** |
|
| **合计** | **50** | **1** | **3** |
|
||||||
|
|
||||||
|
> **深度审计 (2026-04-06)**: 51 项声称修复逐项代码验证。M11-01 为 FALSE_POSITIVE(blocking_lock 从未存在)。P3-03(html缺key_points)/P3-07(单引号未处理)/P3-09(无auto_classify) 实际未修,已提交修复。P2-24/P2-25 状态同步更新为 ✅。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,8 +96,8 @@
|
|||||||
| P2-21 | M1-01 | GeminiDriver API Key 在 URL query 参数中 | ✅ 已修复 (P2-21: 前期暂停非国内模型支持,Gemini/OpenAI/Anthropic 标记为 suspended) |
|
| P2-21 | M1-01 | GeminiDriver API Key 在 URL query 参数中 | ✅ 已修复 (P2-21: 前期暂停非国内模型支持,Gemini/OpenAI/Anthropic 标记为 suspended) |
|
||||||
| P2-22 | M1-02 | ToolOutputGuard 只 warn 不 block 敏感信息 | ✅ 已修复 (sensitive patterns now return Err to block output) |
|
| P2-22 | M1-02 | ToolOutputGuard 只 warn 不 block 敏感信息 | ✅ 已修复 (sensitive patterns now return Err to block output) |
|
||||||
| P2-23 | M1-03/04 | Mutex::unwrap() 在 async 中可能 panic | ✅ 已修复 (relay/service.rs unwrap_or_else(|e| e.into_inner())) |
|
| P2-23 | M1-03/04 | Mutex::unwrap() 在 async 中可能 panic | ✅ 已修复 (relay/service.rs unwrap_or_else(|e| e.into_inner())) |
|
||||||
| P2-24 | — | 记忆写入无去重,多轮对话产生内容相同的重复记忆 | 📋 待修复 (content_hash 去重方案) |
|
| P2-24 | — | 记忆写入无去重,多轮对话产生内容相同的重复记忆 | ✅ 已修复 (sqlite.rs content_hash 列 + agent scope 去重 + importance/access_count 合并) |
|
||||||
| P2-25 | — | 审计日志仅记录反思运行,Hand/Skill 执行无审计追踪 | 📋 待修复 (security-audit.ts 补全事件类型) |
|
| P2-25 | — | 审计日志仅记录反思运行,Hand/Skill 执行无审计追踪 | ✅ 已修复 (security-audit.ts 新增 4 事件类型 + kernel-hands.ts/kernel-skills.ts 审计调用) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user