actr_cli/commands/
generate.rs1use crate::commands::Command;
9use crate::error::{ActrCliError, Result};
10use async_trait::async_trait;
13use clap::Args;
14use std::path::{Path, PathBuf};
15use std::process::Command as StdCommand;
16use tracing::{debug, info, warn};
17
18#[derive(Args, Debug, Clone)]
19#[command(
20 about = "Generate code from proto files",
21 long_about = "从 proto 文件生成 Rust Actor 代码,包括 protobuf 消息类型、Actor 基础设施代码和用户业务逻辑框架"
22)]
23pub struct GenCommand {
24 #[arg(short, long, default_value = "proto")]
26 pub input: PathBuf,
27
28 #[arg(short, long, default_value = "src/generated")]
30 pub output: PathBuf,
31
32 #[arg(long = "clean")]
34 pub clean: bool,
35
36 #[arg(long = "no-scaffold")]
38 pub no_scaffold: bool,
39
40 #[arg(long)]
42 pub overwrite_user_code: bool,
43
44 #[arg(long = "no-format")]
46 pub no_format: bool,
47
48 #[arg(long)]
50 pub debug: bool,
51}
52
53#[async_trait]
54impl Command for GenCommand {
55 async fn execute(&self) -> Result<()> {
56 info!("🚀 开始代码生成...");
57
58 self.validate_inputs()?;
60
61 self.clean_generated_outputs()?;
63
64 self.prepare_output_dirs()?;
66
67 let proto_files = self.discover_proto_files()?;
69 info!("📁 发现 {} 个 proto 文件", proto_files.len());
70
71 self.generate_infrastructure_code(&proto_files).await?;
73
74 if self.should_generate_scaffold() {
76 self.generate_user_code_scaffold(&proto_files).await?;
77 }
78
79 if self.should_format() {
81 self.format_generated_code().await?;
82 }
83
84 self.validate_generated_code().await?;
86
87 info!("✅ 代码生成完成!");
88 self.set_generated_files_readonly()?;
90 self.print_next_steps();
91
92 Ok(())
93 }
94}
95
96impl GenCommand {
97 fn should_generate_scaffold(&self) -> bool {
99 !self.no_scaffold
100 }
101
102 fn should_format(&self) -> bool {
104 !self.no_format
105 }
106
107 fn clean_generated_outputs(&self) -> Result<()> {
109 use std::fs;
110
111 if !self.clean {
112 return Ok(());
113 }
114
115 if !self.output.exists() {
116 return Ok(());
117 }
118
119 info!("🧹 清理旧的生成结果: {:?}", self.output);
120
121 self.make_writable_recursive(&self.output)?;
122 fs::remove_dir_all(&self.output)
123 .map_err(|e| ActrCliError::config_error(format!("删除生成目录失败: {e}")))?;
124
125 Ok(())
126 }
127
128 #[allow(clippy::only_used_in_recursion)]
130 fn make_writable_recursive(&self, path: &Path) -> Result<()> {
131 use std::fs;
132
133 if path.is_file() {
134 let metadata = fs::metadata(path)
135 .map_err(|e| ActrCliError::config_error(format!("读取文件元数据失败: {e}")))?;
136 let mut permissions = metadata.permissions();
137
138 #[cfg(unix)]
139 {
140 use std::os::unix::fs::PermissionsExt;
141 let mode = permissions.mode();
142 permissions.set_mode(mode | 0o222);
143 }
144
145 #[cfg(not(unix))]
146 {
147 permissions.set_readonly(false);
148 }
149
150 fs::set_permissions(path, permissions)
151 .map_err(|e| ActrCliError::config_error(format!("重置文件权限失败: {e}")))?;
152 } else if path.is_dir() {
153 for entry in fs::read_dir(path)
154 .map_err(|e| ActrCliError::config_error(format!("读取目录失败: {e}")))?
155 {
156 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
157 self.make_writable_recursive(&entry.path())?;
158 }
159 }
160
161 Ok(())
162 }
163
164 fn read_manufacturer(&self) -> Result<String> {
166 use std::fs;
167
168 let config_path = PathBuf::from("Actr.toml");
170 if !config_path.exists() {
171 warn!("Actr.toml not found, using default manufacturer 'acme'");
172 return Ok("acme".to_string());
173 }
174
175 let content = fs::read_to_string(&config_path)
177 .map_err(|e| ActrCliError::config_error(format!("Failed to read Actr.toml: {e}")))?;
178
179 let raw_config: actr_config::RawConfig = toml::from_str(&content)
180 .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
181
182 Ok(raw_config.package.manufacturer)
183 }
184
185 fn validate_inputs(&self) -> Result<()> {
187 if !self.input.exists() {
188 return Err(ActrCliError::config_error(format!(
189 "输入路径不存在: {:?}",
190 self.input
191 )));
192 }
193
194 if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
195 warn!("输入文件不是 .proto 文件: {:?}", self.input);
196 }
197
198 Ok(())
199 }
200
201 fn prepare_output_dirs(&self) -> Result<()> {
203 std::fs::create_dir_all(&self.output)
204 .map_err(|e| ActrCliError::config_error(format!("创建输出目录失败: {e}")))?;
205
206 if self.should_generate_scaffold() {
207 let user_code_dir = self.output.join("../");
208 std::fs::create_dir_all(&user_code_dir)
209 .map_err(|e| ActrCliError::config_error(format!("创建用户代码目录失败: {e}")))?;
210 }
211
212 Ok(())
213 }
214
215 fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
217 let mut proto_files = Vec::new();
218
219 if self.input.is_file() {
220 proto_files.push(self.input.clone());
221 } else {
222 for entry in std::fs::read_dir(&self.input)
224 .map_err(|e| ActrCliError::config_error(format!("读取输入目录失败: {e}")))?
225 {
226 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
227 let path = entry.path();
228
229 if path.extension().unwrap_or_default() == "proto" {
230 proto_files.push(path);
231 }
232 }
233 }
234
235 if proto_files.is_empty() {
236 return Err(ActrCliError::config_error("未找到 proto 文件"));
237 }
238
239 Ok(proto_files)
240 }
241
242 fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
254 const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
256
257 let installed_version = self.check_installed_plugin_version()?;
259
260 match installed_version {
261 Some(version) if version == EXPECTED_VERSION => {
262 info!("✅ Using installed protoc-gen-actrframework v{}", version);
264 let output = StdCommand::new("which")
265 .arg("protoc-gen-actrframework")
266 .output()
267 .map_err(|e| {
268 ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
269 })?;
270
271 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
272 Ok(PathBuf::from(path))
273 }
274 Some(version) => {
275 info!(
277 "🔄 Version mismatch: installed v{}, need v{}",
278 version, EXPECTED_VERSION
279 );
280 info!("🔨 Upgrading plugin...");
281 self.install_or_upgrade_plugin()
282 }
283 None => {
284 info!("📦 protoc-gen-actrframework not found, installing...");
286 self.install_or_upgrade_plugin()
287 }
288 }
289 }
290
291 fn check_installed_plugin_version(&self) -> Result<Option<String>> {
293 let output = StdCommand::new("protoc-gen-actrframework")
294 .arg("--version")
295 .output();
296
297 match output {
298 Ok(output) if output.status.success() => {
299 let version_info = String::from_utf8_lossy(&output.stdout);
300 let version = version_info
302 .lines()
303 .next()
304 .and_then(|line| line.split_whitespace().nth(1))
305 .map(|v| v.to_string());
306
307 debug!("Detected installed version: {:?}", version);
308 Ok(version)
309 }
310 _ => {
311 debug!("Plugin not found in PATH");
312 Ok(None)
313 }
314 }
315 }
316
317 fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
319 let current_dir = std::env::current_dir()?;
321 let workspace_root = current_dir.ancestors().find(|p| {
322 let is_workspace =
323 p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
324 if is_workspace {
325 debug!("Found workspace root: {:?}", p);
326 }
327 is_workspace
328 });
329
330 let workspace_root = workspace_root.ok_or_else(|| {
331 ActrCliError::config_error(
332 "Cannot find actr workspace.\n\
333 Please run this command from within an actr project or workspace.",
334 )
335 })?;
336
337 info!("🔍 Found actr workspace at: {}", workspace_root.display());
338
339 info!("🔨 Building protoc-gen-actrframework...");
341 let mut build_cmd = StdCommand::new("cargo");
342 build_cmd
343 .arg("build")
344 .arg("-p")
345 .arg("actr-framework-protoc-codegen")
346 .arg("--bin")
347 .arg("protoc-gen-actrframework")
348 .current_dir(workspace_root);
349
350 debug!("Running: {:?}", build_cmd);
351 let output = build_cmd
352 .output()
353 .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
354
355 if !output.status.success() {
356 let stderr = String::from_utf8_lossy(&output.stderr);
357 return Err(ActrCliError::command_error(format!(
358 "Failed to build plugin:\n{stderr}"
359 )));
360 }
361
362 info!("📦 Installing to ~/.cargo/bin/...");
364 let mut install_cmd = StdCommand::new("cargo");
365 install_cmd
366 .arg("install")
367 .arg("--path")
368 .arg(workspace_root.join("crates/framework-protoc-codegen"))
369 .arg("--bin")
370 .arg("protoc-gen-actrframework")
371 .arg("--force"); debug!("Running: {:?}", install_cmd);
374 let output = install_cmd
375 .output()
376 .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
377
378 if !output.status.success() {
379 let stderr = String::from_utf8_lossy(&output.stderr);
380 return Err(ActrCliError::command_error(format!(
381 "Failed to install plugin:\n{stderr}"
382 )));
383 }
384
385 info!("✅ Plugin installed successfully");
386
387 let which_output = StdCommand::new("which")
389 .arg("protoc-gen-actrframework")
390 .output()
391 .map_err(|e| {
392 ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
393 })?;
394
395 let path = String::from_utf8_lossy(&which_output.stdout)
396 .trim()
397 .to_string();
398 Ok(PathBuf::from(path))
399 }
400
401 async fn generate_infrastructure_code(&self, proto_files: &[PathBuf]) -> Result<()> {
403 info!("🔧 生成基础设施代码...");
404
405 let plugin_path = self.ensure_protoc_plugin()?;
407
408 let manufacturer = self.read_manufacturer()?;
410 debug!("Using manufacturer from Actr.toml: {}", manufacturer);
411
412 for proto_file in proto_files {
413 debug!("处理 proto 文件: {:?}", proto_file);
414
415 let mut cmd = StdCommand::new("protoc");
417 cmd.arg(format!("--proto_path={}", self.input.display()))
418 .arg("--prost_opt=flat_output_dir")
419 .arg(format!("--prost_out={}", self.output.display()))
420 .arg(proto_file);
421
422 debug!("执行 protoc (prost): {:?}", cmd);
423 let output = cmd.output().map_err(|e| {
424 ActrCliError::command_error(format!("执行 protoc (prost) 失败: {e}"))
425 })?;
426
427 if !output.status.success() {
428 let stderr = String::from_utf8_lossy(&output.stderr);
429 return Err(ActrCliError::command_error(format!(
430 "protoc (prost) 执行失败: {stderr}"
431 )));
432 }
433
434 let mut cmd = StdCommand::new("protoc");
436 cmd.arg(format!("--proto_path={}", self.input.display()))
437 .arg(format!(
438 "--plugin=protoc-gen-actrframework={}",
439 plugin_path.display()
440 ))
441 .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
442 .arg(format!("--actrframework_out={}", self.output.display()))
443 .arg(proto_file);
444
445 debug!("执行 protoc (actrframework): {:?}", cmd);
446 let output = cmd.output().map_err(|e| {
447 ActrCliError::command_error(format!("执行 protoc (actrframework) 失败: {e}"))
448 })?;
449
450 if !output.status.success() {
451 let stderr = String::from_utf8_lossy(&output.stderr);
452 return Err(ActrCliError::command_error(format!(
453 "protoc (actrframework) 执行失败: {stderr}"
454 )));
455 }
456
457 let stdout = String::from_utf8_lossy(&output.stdout);
458 if !stdout.is_empty() {
459 debug!("protoc 输出: {}", stdout);
460 }
461 }
462
463 self.generate_mod_rs(proto_files).await?;
465
466 info!("✅ 基础设施代码生成完成");
467 Ok(())
468 }
469
470 async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
472 let mod_path = self.output.join("mod.rs");
473
474 let mut proto_modules = Vec::new();
476 let mut service_modules = Vec::new();
477
478 use std::fs;
479 for entry in fs::read_dir(&self.output)
480 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
481 {
482 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
483 let path = entry.path();
484
485 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
486 if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
487 if file_name == "mod" {
489 continue;
490 }
491
492 if file_name.ends_with("_service_actor") {
494 service_modules.push(format!("pub mod {file_name};"));
495 } else {
496 proto_modules.push(format!("pub mod {file_name};"));
497 }
498 }
499 }
500 }
501
502 proto_modules.sort();
504 service_modules.sort();
505
506 let mod_content = format!(
507 r#"//! 自动生成的代码模块
508//!
509//! 此模块由 `actr gen` 命令自动生成,包括:
510//! - protobuf 消息类型定义
511//! - Actor 框架代码(路由器、trait)
512//!
513//! ⚠️ 请勿手动修改此目录中的文件
514
515// Protobuf 消息类型(由 prost 生成)
516{}
517
518// Actor 框架代码(由 protoc-gen-actrframework 生成)
519{}
520
521// 常用类型会在各自的模块中定义,请按需导入
522"#,
523 proto_modules.join("\n"),
524 service_modules.join("\n"),
525 );
526
527 std::fs::write(&mod_path, mod_content)
528 .map_err(|e| ActrCliError::config_error(format!("写入 mod.rs 失败: {e}")))?;
529
530 debug!("生成 mod.rs: {:?}", mod_path);
531 Ok(())
532 }
533
534 fn set_generated_files_readonly(&self) -> Result<()> {
536 use std::fs;
537
538 for entry in fs::read_dir(&self.output)
539 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
540 {
541 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
542 let path = entry.path();
543
544 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
545 let metadata = fs::metadata(&path)
547 .map_err(|e| ActrCliError::config_error(format!("获取文件元数据失败: {e}")))?;
548 let mut permissions = metadata.permissions();
549
550 #[cfg(unix)]
552 {
553 use std::os::unix::fs::PermissionsExt;
554 let mode = permissions.mode();
555 permissions.set_mode(mode & !0o222); }
557
558 #[cfg(not(unix))]
559 {
560 permissions.set_readonly(true);
561 }
562
563 fs::set_permissions(&path, permissions)
564 .map_err(|e| ActrCliError::config_error(format!("设置文件权限失败: {e}")))?;
565
566 debug!("设置只读属性: {:?}", path);
567 }
568 }
569
570 Ok(())
571 }
572
573 async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
575 info!("📝 生成用户代码框架...");
576
577 for proto_file in proto_files {
578 let service_name = proto_file
579 .file_stem()
580 .and_then(|s| s.to_str())
581 .ok_or_else(|| ActrCliError::config_error("无效的 proto 文件名"))?;
582
583 self.generate_service_scaffold(service_name).await?;
584 }
585
586 info!("✅ 用户代码框架生成完成");
587 Ok(())
588 }
589
590 async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
592 let user_file_path = self
593 .output
594 .parent()
595 .unwrap_or_else(|| Path::new("src"))
596 .join(format!("{}_service.rs", service_name.to_lowercase()));
597
598 if user_file_path.exists() && !self.overwrite_user_code {
600 info!("⏭️ 跳过已存在的用户代码文件: {:?}", user_file_path);
601 return Ok(());
602 }
603
604 let scaffold_content = self.generate_scaffold_content(service_name);
605
606 std::fs::write(&user_file_path, scaffold_content)
607 .map_err(|e| ActrCliError::config_error(format!("写入用户代码框架失败: {e}")))?;
608
609 info!("📄 生成用户代码框架: {:?}", user_file_path);
610 Ok(())
611 }
612
613 fn generate_scaffold_content(&self, service_name: &str) -> String {
615 let service_name_pascal = service_name
616 .split('_')
617 .map(|s| {
618 let mut chars = s.chars();
619 match chars.next() {
620 None => String::new(),
621 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
622 }
623 })
624 .collect::<String>();
625
626 let template = format!(
627 r#"//! # {service_name_pascal} 用户业务逻辑实现
628//!
629//! 这个文件是由 `actr gen` 命令自动生成的用户代码框架。
630//! 请在这里实现您的具体业务逻辑。
631
632use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
633// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
634// use actr_framework::prelude::*;
635use std::sync::Arc;
636
637/// {service_name_pascal} 服务的具体实现
638///
639/// TODO: 添加您需要的状态字段,例如:
640/// - 数据库连接池
641/// - 配置信息
642/// - 缓存客户端
643/// - 日志记录器等
644pub struct My{service_name_pascal}Service {{
645 // TODO: 添加您的服务状态字段
646 // 例如:
647 // pub db_pool: Arc<DatabasePool>,
648 // pub config: Arc<ServiceConfig>,
649 // pub metrics: Arc<Metrics>,
650}}
651
652impl My{service_name_pascal}Service {{
653 /// 创建新的服务实例
654 ///
655 /// TODO: 根据您的需要修改构造函数参数
656 pub fn new(/* TODO: 添加必要的依赖 */) -> Self {{
657 Self {{
658 // TODO: 初始化您的字段
659 }}
660 }}
661
662 /// 使用默认配置创建服务实例(用于测试)
663 pub fn default_for_testing() -> Self {{
664 Self {{
665 // TODO: 提供测试用的默认值
666 }}
667 }}
668}}
669
670// TODO: 实现 {service_name_pascal}Handler trait 的所有方法
671// 注意:impl_user_code_scaffold! 宏已经为您生成了基础框架,
672// 您需要将其替换为真实的业务逻辑实现。
673//
674// 示例:
675// #[async_trait]
676// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
677// async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
678// // 1. 验证输入
679// // 2. 执行业务逻辑
680// // 3. 返回结果
681// todo!("实现您的业务逻辑")
682// }}
683// }}
684
685#[cfg(test)]
686mod tests {{
687 use super::*;
688
689 #[tokio::test]
690 async fn test_service_creation() {{
691 let _service = My{service_name_pascal}Service::default_for_testing();
692 // TODO: 添加您的测试
693 }}
694
695 // TODO: 添加更多测试用例
696}}
697
698/*
699📚 使用指南
700
701## 🚀 快速开始
702
7031. **实现业务逻辑**:
704 在 `My{service_name_pascal}Service` 中实现 `{service_name_pascal}Handler` trait 的所有方法
705
7062. **添加依赖**:
707 在 `Cargo.toml` 中添加您需要的依赖,例如数据库客户端、HTTP 客户端等
708
7093. **配置服务**:
710 修改 `new()` 构造函数,注入必要的依赖
711
7124. **启动服务**:
713 ```rust
714 #[tokio::main]
715 async fn main() -> ActorResult<()> {{
716 let service = My{service_name_pascal}Service::new(/* 依赖 */);
717
718 ActorSystem::new()
719 .attach(service)
720 .start()
721 .await
722 }}
723 ```
724
725## 🔧 开发提示
726
727- 使用 `tracing` crate 进行日志记录
728- 实现错误处理和重试逻辑
729- 添加单元测试和集成测试
730- 考虑使用配置文件管理环境变量
731- 实现健康检查和指标收集
732
733## 📖 更多资源
734
735- Actor-RTC 文档: [链接]
736- API 参考: [链接]
737- 示例项目: [链接]
738*/
739"# );
741
742 template
743 }
744
745 async fn format_generated_code(&self) -> Result<()> {
747 info!("🎨 格式化生成的代码...");
748
749 let mut cmd = StdCommand::new("rustfmt");
750 cmd.arg("--edition")
751 .arg("2024")
752 .arg("--config")
753 .arg("max_width=100");
754
755 for entry in std::fs::read_dir(&self.output)
757 .map_err(|e| ActrCliError::config_error(format!("读取输出目录失败: {e}")))?
758 {
759 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
760 let path = entry.path();
761
762 if path.extension().unwrap_or_default() == "rs" {
763 cmd.arg(&path);
764 }
765 }
766
767 let output = cmd
768 .output()
769 .map_err(|e| ActrCliError::command_error(format!("执行 rustfmt 失败: {e}")))?;
770
771 if !output.status.success() {
772 let stderr = String::from_utf8_lossy(&output.stderr);
773 warn!("rustfmt 执行警告: {}", stderr);
774 } else {
775 info!("✅ 代码格式化完成");
776 }
777
778 Ok(())
779 }
780
781 async fn validate_generated_code(&self) -> Result<()> {
783 info!("🔍 验证生成的代码...");
784
785 let project_root = self.find_project_root()?;
787
788 let mut cmd = StdCommand::new("cargo");
789 cmd.arg("check").arg("--quiet").current_dir(&project_root);
790
791 let output = cmd
792 .output()
793 .map_err(|e| ActrCliError::command_error(format!("执行 cargo check 失败: {e}")))?;
794
795 if !output.status.success() {
796 let stderr = String::from_utf8_lossy(&output.stderr);
797 warn!("生成的代码存在编译警告或错误:\n{}", stderr);
798 info!("💡 这通常是正常的,因为用户代码框架包含 TODO 标记");
799 } else {
800 info!("✅ 代码验证通过");
801 }
802
803 Ok(())
804 }
805
806 fn find_project_root(&self) -> Result<PathBuf> {
808 let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
809
810 loop {
811 if current.join("Cargo.toml").exists() {
812 return Ok(current);
813 }
814
815 match current.parent() {
816 Some(parent) => current = parent.to_path_buf(),
817 None => break,
818 }
819 }
820
821 std::env::current_dir().map_err(ActrCliError::Io)
823 }
824
825 fn print_next_steps(&self) {
827 println!("\n🎉 代码生成完成!");
828 println!("\n📋 后续步骤:");
829 println!("1. 📖 查看生成的代码: {:?}", self.output);
830 if self.should_generate_scaffold() {
831 println!("2. ✏️ 实现业务逻辑: 在 src/ 目录下的 *_service.rs 文件中");
832 println!("3. 🔧 添加依赖: 在 Cargo.toml 中添加需要的依赖包");
833 println!("4. 🏗️ 编译项目: cargo build");
834 println!("5. 🧪 运行测试: cargo test");
835 println!("6. 🚀 启动服务: cargo run");
836 } else {
837 println!("2. 🏗️ 编译项目: cargo build");
838 println!("3. 🧪 运行测试: cargo test");
839 println!("4. 🚀 启动服务: cargo run");
840 }
841 println!("\n💡 提示: 查看生成的用户代码文件中的详细使用指南");
842 }
843}