refactor: 统一项目名称从OpenFang到ZCLAW
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
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
This commit is contained in:
@@ -9,6 +9,7 @@ description = "ZCLAW Hands - autonomous capabilities"
|
||||
|
||||
[dependencies]
|
||||
zclaw-types = { workspace = true }
|
||||
zclaw-runtime = { workspace = true }
|
||||
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
mod whiteboard;
|
||||
mod slideshow;
|
||||
mod speech;
|
||||
mod quiz;
|
||||
pub mod quiz;
|
||||
mod browser;
|
||||
mod researcher;
|
||||
mod collector;
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
use zclaw_types::Result;
|
||||
use zclaw_runtime::driver::{LlmDriver, CompletionRequest};
|
||||
|
||||
use crate::{Hand, HandConfig, HandContext, HandResult, HandStatus};
|
||||
|
||||
@@ -44,29 +45,242 @@ impl QuizGenerator for DefaultQuizGenerator {
|
||||
difficulty: &DifficultyLevel,
|
||||
_question_types: &[QuestionType],
|
||||
) -> Result<Vec<QuizQuestion>> {
|
||||
// Generate placeholder questions
|
||||
// Generate placeholder questions with randomized correct answers
|
||||
let options_pool: Vec<Vec<String>> = vec![
|
||||
vec!["光合作用".into(), "呼吸作用".into(), "蒸腾作用".into(), "运输作用".into()],
|
||||
vec!["牛顿".into(), "爱因斯坦".into(), "伽利略".into(), "开普勒".into()],
|
||||
vec!["太平洋".into(), "大西洋".into(), "印度洋".into(), "北冰洋".into()],
|
||||
vec!["DNA".into(), "RNA".into(), "蛋白质".into(), "碳水化合物".into()],
|
||||
vec!["引力".into(), "电磁力".into(), "强力".into(), "弱力".into()],
|
||||
];
|
||||
|
||||
Ok((0..count)
|
||||
.map(|i| QuizQuestion {
|
||||
id: uuid_v4(),
|
||||
question_type: QuestionType::MultipleChoice,
|
||||
question: format!("Question {} about {}", i + 1, topic),
|
||||
options: Some(vec![
|
||||
"Option A".to_string(),
|
||||
"Option B".to_string(),
|
||||
"Option C".to_string(),
|
||||
"Option D".to_string(),
|
||||
]),
|
||||
correct_answer: Answer::Single("Option A".to_string()),
|
||||
explanation: Some(format!("Explanation for question {}", i + 1)),
|
||||
hints: Some(vec![format!("Hint 1 for question {}", i + 1)]),
|
||||
points: 10.0,
|
||||
difficulty: difficulty.clone(),
|
||||
tags: vec![topic.to_string()],
|
||||
.map(|i| {
|
||||
let pool_idx = i % options_pool.len();
|
||||
let mut opts = options_pool[pool_idx].clone();
|
||||
// Shuffle options to randomize correct answer position
|
||||
let correct_idx = (i * 3 + 1) % opts.len();
|
||||
opts.swap(0, correct_idx);
|
||||
let correct = opts[0].clone();
|
||||
|
||||
QuizQuestion {
|
||||
id: uuid_v4(),
|
||||
question_type: QuestionType::MultipleChoice,
|
||||
question: format!("关于{}的第{}题({}难度)", topic, i + 1, match difficulty {
|
||||
DifficultyLevel::Easy => "简单",
|
||||
DifficultyLevel::Medium => "中等",
|
||||
DifficultyLevel::Hard => "困难",
|
||||
DifficultyLevel::Adaptive => "自适应",
|
||||
}),
|
||||
options: Some(opts),
|
||||
correct_answer: Answer::Single(correct),
|
||||
explanation: Some(format!("第{}题的详细解释", i + 1)),
|
||||
hints: Some(vec![format!("提示:仔细阅读关于{}的内容", topic)]),
|
||||
points: 10.0,
|
||||
difficulty: difficulty.clone(),
|
||||
tags: vec![topic.to_string()],
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// LLM-powered quiz generator that produces real questions via an LLM driver.
|
||||
pub struct LlmQuizGenerator {
|
||||
driver: Arc<dyn LlmDriver>,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl LlmQuizGenerator {
|
||||
pub fn new(driver: Arc<dyn LlmDriver>, model: String) -> Self {
|
||||
Self { driver, model }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QuizGenerator for LlmQuizGenerator {
|
||||
async fn generate_questions(
|
||||
&self,
|
||||
topic: &str,
|
||||
content: Option<&str>,
|
||||
count: usize,
|
||||
difficulty: &DifficultyLevel,
|
||||
question_types: &[QuestionType],
|
||||
) -> Result<Vec<QuizQuestion>> {
|
||||
let difficulty_str = match difficulty {
|
||||
DifficultyLevel::Easy => "简单",
|
||||
DifficultyLevel::Medium => "中等",
|
||||
DifficultyLevel::Hard => "困难",
|
||||
DifficultyLevel::Adaptive => "中等",
|
||||
};
|
||||
|
||||
let type_str = if question_types.is_empty() {
|
||||
String::from("选择题(multiple_choice)")
|
||||
} else {
|
||||
question_types
|
||||
.iter()
|
||||
.map(|t| match t {
|
||||
QuestionType::MultipleChoice => "选择题",
|
||||
QuestionType::TrueFalse => "判断题",
|
||||
QuestionType::FillBlank => "填空题",
|
||||
QuestionType::ShortAnswer => "简答题",
|
||||
QuestionType::Essay => "论述题",
|
||||
_ => "选择题",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
};
|
||||
|
||||
let content_section = match content {
|
||||
Some(c) if !c.is_empty() => format!("\n\n参考内容:\n{}", &c[..c.len().min(3000)]),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let content_note = if content.is_some() && content.map_or(false, |c| !c.is_empty()) {
|
||||
"(基于提供的参考内容出题)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let prompt = format!(
|
||||
r#"你是一个专业的出题专家。请根据以下要求生成测验题目:
|
||||
|
||||
主题: {}
|
||||
难度: {}
|
||||
题目类型: {}
|
||||
数量: {}{}
|
||||
{}
|
||||
|
||||
请严格按照以下 JSON 格式输出,不要添加任何其他文字:
|
||||
```json
|
||||
[
|
||||
{{
|
||||
"question": "题目内容",
|
||||
"options": ["选项A", "选项B", "选项C", "选项D"],
|
||||
"correct_answer": "正确答案(与options中某项完全一致)",
|
||||
"explanation": "答案解释",
|
||||
"hint": "提示信息"
|
||||
}}
|
||||
]
|
||||
```
|
||||
|
||||
要求:
|
||||
1. 题目要有实际内容,不要使用占位符
|
||||
2. 正确答案必须随机分布(不要总在第一个选项)
|
||||
3. 每道题的选项要有区分度,干扰项要合理
|
||||
4. 解释要清晰准确
|
||||
5. 直接输出 JSON,不要有 markdown 包裹"#,
|
||||
topic, difficulty_str, type_str, count, content_section, content_note,
|
||||
);
|
||||
|
||||
let request = CompletionRequest {
|
||||
model: self.model.clone(),
|
||||
system: Some("你是一个专业的出题专家,只输出纯JSON格式。".to_string()),
|
||||
messages: vec![zclaw_types::Message::user(&prompt)],
|
||||
tools: Vec::new(),
|
||||
max_tokens: Some(4096),
|
||||
temperature: Some(0.7),
|
||||
stop: Vec::new(),
|
||||
stream: false,
|
||||
};
|
||||
|
||||
let response = self.driver.complete(request).await.map_err(|e| {
|
||||
zclaw_types::ZclawError::Internal(format!("LLM quiz generation failed: {}", e))
|
||||
})?;
|
||||
|
||||
// Extract text from response
|
||||
let text: String = response
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
zclaw_runtime::driver::ContentBlock::Text { text } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
// Parse JSON from response (handle markdown code fences)
|
||||
let json_str = extract_json(&text);
|
||||
|
||||
let raw_questions: Vec<serde_json::Value> =
|
||||
serde_json::from_str(json_str).map_err(|e| {
|
||||
zclaw_types::ZclawError::Internal(format!(
|
||||
"Failed to parse quiz JSON: {}. Raw: {}",
|
||||
e,
|
||||
&text[..text.len().min(200)]
|
||||
))
|
||||
})?;
|
||||
|
||||
let questions: Vec<QuizQuestion> = raw_questions
|
||||
.into_iter()
|
||||
.take(count)
|
||||
.map(|q| {
|
||||
let options: Vec<String> = q["options"]
|
||||
.as_array()
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let correct = q["correct_answer"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
QuizQuestion {
|
||||
id: uuid_v4(),
|
||||
question_type: QuestionType::MultipleChoice,
|
||||
question: q["question"].as_str().unwrap_or("未知题目").to_string(),
|
||||
options: if options.is_empty() { None } else { Some(options) },
|
||||
correct_answer: Answer::Single(correct),
|
||||
explanation: q["explanation"].as_str().map(String::from),
|
||||
hints: q["hint"].as_str().map(|h| vec![h.to_string()]),
|
||||
points: 10.0,
|
||||
difficulty: difficulty.clone(),
|
||||
tags: vec![topic.to_string()],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if questions.is_empty() {
|
||||
// Fallback to default if LLM returns nothing parseable
|
||||
return DefaultQuizGenerator
|
||||
.generate_questions(topic, content, count, difficulty, question_types)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(questions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract JSON from a string that may be wrapped in markdown code fences.
|
||||
fn extract_json(text: &str) -> &str {
|
||||
let trimmed = text.trim();
|
||||
|
||||
// Try to find ```json ... ``` block
|
||||
if let Some(start) = trimmed.find("```json") {
|
||||
let after_start = &trimmed[start + 7..];
|
||||
if let Some(end) = after_start.find("```") {
|
||||
return after_start[..end].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find ``` ... ``` block
|
||||
if let Some(start) = trimmed.find("```") {
|
||||
let after_start = &trimmed[start + 3..];
|
||||
if let Some(end) = after_start.find("```") {
|
||||
return after_start[..end].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find raw JSON array
|
||||
if let Some(start) = trimmed.find('[') {
|
||||
if let Some(end) = trimmed.rfind(']') {
|
||||
return &trimmed[start..=end];
|
||||
}
|
||||
}
|
||||
|
||||
trimmed
|
||||
}
|
||||
|
||||
/// Quiz action types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
|
||||
Reference in New Issue
Block a user