From 344fbff7190b33e390953a79d2aac0f0f827a871 Mon Sep 17 00:00:00 2001 From: "bobo.yang" Date: Tue, 11 Mar 2025 13:29:58 +0800 Subject: [PATCH] add /chatflow.ask and /chatflow.gen --- merico/chatflow/ask/command.py | 42 ++ merico/chatflow/ask/command.yml | 4 + merico/chatflow/gen/command.py | 421 +++++++++++++++++++ merico/chatflow/gen/command.yml | 4 + merico/chatflow/util/base_functions_guide.md | 64 +++ merico/chatflow/util/contexts.py | 93 ++++ merico/chatflow/util/ide_service_demo.py | 5 + merico/chatflow/util/workflow_guide.md | 52 +++ 8 files changed, 685 insertions(+) create mode 100644 merico/chatflow/ask/command.py create mode 100644 merico/chatflow/ask/command.yml create mode 100644 merico/chatflow/gen/command.py create mode 100644 merico/chatflow/gen/command.yml create mode 100644 merico/chatflow/util/base_functions_guide.md create mode 100644 merico/chatflow/util/contexts.py create mode 100644 merico/chatflow/util/ide_service_demo.py create mode 100644 merico/chatflow/util/workflow_guide.md diff --git a/merico/chatflow/ask/command.py b/merico/chatflow/ask/command.py new file mode 100644 index 0000000..8ca9d02 --- /dev/null +++ b/merico/chatflow/ask/command.py @@ -0,0 +1,42 @@ +# 在 ask/command.py 中 +import os +import sys + +from devchat.llm import chat +from lib.ide_service import IDEService + +ROOT_WORKFLOW_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_WORKFLOW_DIR) + +from chatflow.util.contexts import CONTEXTS + + + +PROMPT = f""" +{CONTEXTS.replace("{", "{{").replace("}", "}}")} +""" + """ +当前选中代码:{selected_code} +当前打开文件路径:{file_path} +用户要求或问题:{question} +""" +@chat(prompt=PROMPT, stream_out=True) +def ask(question, selected_code, file_path): + pass + + +def get_selected_code(): + """Retrieves the selected lines of code from the user's selection.""" + selected_data = IDEService().get_selected_range().dict() + return selected_data + + +def main(question): + selected_text = get_selected_code() + file_path = selected_text.get("abspath", "") + code_text = selected_text.get("text", "") + + ask(question=question, selected_code=code_text, file_path=file_path) + sys.exit(0) + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/merico/chatflow/ask/command.yml b/merico/chatflow/ask/command.yml new file mode 100644 index 0000000..5d8d5be --- /dev/null +++ b/merico/chatflow/ask/command.yml @@ -0,0 +1,4 @@ +description: 生成工作流命令,输入命令信息 +input: required +steps: + - run: $devchat_python $command_path/command.py "$input" \ No newline at end of file diff --git a/merico/chatflow/gen/command.py b/merico/chatflow/gen/command.py new file mode 100644 index 0000000..0c0dddc --- /dev/null +++ b/merico/chatflow/gen/command.py @@ -0,0 +1,421 @@ +""" +生成工作流命令实现 + +步骤1: 根据用户输入的工作流命令定义信息,生成相关工作流实现步骤描述,展示相关信息,等待用户确认; +步骤2: 根据用户确认的工作流实现步骤描述,生成工作流命令实现代码,并保存到指定文件中; +""" + +#!/usr/bin/env python3 +import os +import sys +import re +import yaml +import subprocess +from pathlib import Path +from lib.chatmark import Button, Form, TextEditor, Step, Radio, Checkbox +from lib.ide_service import IDEService +from devchat.llm import chat, chat_json + +ROOT_WORKFLOW_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_WORKFLOW_DIR) + +from chatflow.util.contexts import CONTEXTS + + +# 工作流命令定义模板 +COMMAND_YML_TEMPLATE = """description: {description} +{input_section} +{help_section}{workflow_python_section} +steps: + - run: ${python_cmd} $command_path/command.py{input_param} +""" + +# 工作流命令实现模板 +COMMAND_PY_TEMPLATE = """#!/usr/bin/env python3 +import os +import sys +from pathlib import Path +{imports} + +def main(): + {main_code} + +if __name__ == "__main__": + main() +""" + +# 从用户输入提取工作流信息的提示 +EXTRACT_INFO_PROMPT = """ +请从以下用户输入中提取创建工作流命令所需的关键信息: + +用户输入: +{user_input} + +工作流上下文信息: +{contexts} + +请提取以下信息并以JSON格式返回: +1. command_name: 工作流命令名称,应以/开头,如"/example"或"/category.command" +2. description: 工作流命令的简短描述 +3. input_required: 工作流是否需要输入参数(true/false) +4. custom_env: 是否需要自定义Python环境(true/false) +5. env_name: 如果需要自定义环境,环境名称是什么 +6. dependencies: 如果需要自定义环境,依赖文件名是什么(如requirements.txt) +7. purpose: 工作流的主要目的和功能 +8. implementation_ideas: 实现思路的简要描述 + +如果某项信息在用户输入中未明确指定,请根据上下文合理推断。 + +返回格式示例: +{{ + "command_name": "/example.command", + "description": "这是一个示例命令", + "input_required": true, + "custom_env": false, + "env_name": "", + "dependencies": "", + "purpose": "这个命令的主要目的是...", + "implementation_ideas": "可以通过以下步骤实现..." +}} +""" + +# 工作流步骤生成提示 +WORKFLOW_STEPS_PROMPT = """ +请为以下工作流命令对应脚本文件生成详细的实现步骤描述: + +工作流命令名称: {command_name} +工作流命令描述: {description} +输入要求: {input_requirement} +工作流目的: {purpose} +实现思路: {implementation_ideas} + +工作流上下文信息: +{contexts} + +请提供清晰的步骤描述,包括: +1. 每个步骤需要完成的具体任务 +2. 每个步骤可能需要的输入和输出 +3. 每个步骤可能需要使用的IDE Service或ChatMark组件 +4. 任何其他实现细节 + +返回格式应为markdown块包裹的步骤列表,每个步骤都有详细描述。输出示例如下: +```steps +步骤1: 获取用户输入的重构任务要求 +步骤2: 调用IDE Service获取选中代码 +步骤3: 根据用户重构任务要求,调用大模型生成选中代码的重构代码 +步骤4: 调用IDE Service,将生成的重构代码通过DIFF VIEW方式展示给用户 +``` + +不要输出工作流命令的其他构建步骤,只需要清洗描述工作流命令对应command.py中对应的步骤实现即可。 +""" + +# 代码实现生成提示 +CODE_IMPLEMENTATION_PROMPT = """ +请为以下工作流命令生成Python实现代码: + +工作流命令名称: {command_name} +工作流命令描述: {description} +输入要求: {input_requirement} +自定义环境: {custom_env} +工作流实现步骤: +{workflow_steps} + +工作流上下文信息: +{contexts} + +请生成完整的Python代码实现,包括: +1. 必要的导入语句 +2. 主函数实现 +3. 按照工作流步骤实现具体功能 +4. 适当的错误处理 +5. 必要的注释说明 +6. 所有markdown代码块都要有明确的语言标识,如python、json、yaml、code等 + +代码应该使用IDE Service接口与IDE交互,使用ChatMark组件与用户交互。 +输出格式应为markdown代码块,语言标识为python。仅输出工作流实现对应的脚本代码块,不需要输出其他逻辑信息。 + +只需要输出最终PYTHON代码块,不需要其他信息。例如: +```python +.... +``` +""" + +@chat_json(prompt=EXTRACT_INFO_PROMPT) +def extract_workflow_info(user_input, contexts): + """从用户输入中提取工作流信息""" + pass + +@chat(prompt=WORKFLOW_STEPS_PROMPT, stream_out=False) +def generate_workflow_steps(command_name, description, input_requirement, purpose, implementation_ideas, contexts): + """生成工作流实现步骤描述""" + pass + +@chat(prompt=CODE_IMPLEMENTATION_PROMPT, stream_out=False) +def generate_workflow_code(command_name, description, input_requirement, custom_env, workflow_steps, contexts): + """生成工作流实现代码""" + pass + +def parse_command_path(command_name): + """ + 解析命令路径,返回目录结构和最终命令名 + 确保工作流创建在custom目录下的有效namespace中 + """ + parts = command_name.strip('/').split('.') + + # 获取custom目录路径 + custom_dir = os.path.join(os.path.expanduser('~'), '.chat', 'scripts', 'custom') + + # 获取custom目录下的有效namespace + valid_namespaces = [] + config_path = os.path.join(custom_dir, 'config.yml') + + if os.path.exists(config_path): + try: + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + if config and 'namespaces' in config: + valid_namespaces = config['namespaces'] + except Exception as e: + print(f"读取custom配置文件失败: {str(e)}") + + # 如果没有找到有效namespace,使用默认namespace + if not valid_namespaces: + print("警告: 未找到有效的custom namespace,将使用默认namespace 'default'") + valid_namespaces = ['default'] + + # 确保default namespace存在于config.yml中 + try: + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w') as f: + yaml.dump({'namespaces': ['default']}, f) + except Exception as e: + print(f"创建默认namespace配置失败: {str(e)}") + + # 使用第一个有效namespace + namespace = valid_namespaces[0] + + # 创建最终命令目录路径 + command_dir = os.path.join(custom_dir, namespace) + + # 创建目录结构 + for part in parts[:-1]: + command_dir = os.path.join(command_dir, part) + + return command_dir, parts[-1] + +def parse_markdown_block(response, block_type="steps"): + """ + 从AI响应中解析指定类型的Markdown代码块内容,支持处理嵌套的代码块。 + + Args: + response (str): AI生成的响应文本 + block_type (str): 要解析的代码块类型,默认为"steps" + + Returns: + str: 解析出的代码块内容 + + Raises: + Exception: 解析失败时抛出异常 + """ + try: + # 处理可能存在的思考过程 + if response.find("") != -1: + response = response.split("")[-1] + + # 构建起始标记 + start_marker = f"```{block_type}" + end_marker = "```" + + # 查找起始位置 + start_pos = response.find(start_marker) + if start_pos == -1: + # 如果没有找到指定类型的标记,直接返回原文本 + return response.strip() + + # 从标记后开始的位置 + content_start = start_pos + len(start_marker) + + # 从content_start开始找到第一个未配对的``` + pos = content_start + open_blocks = 1 # 已经有一个开放的块 + + while True: + # 找到下一个``` + next_marker = response.find(end_marker, pos) + if next_marker == -1: + # 如果没有找到结束标记,返回剩余所有内容 + break + + # 检查这是开始还是结束标记 + # 向后看是否跟着语言标识符 + after_marker = response[next_marker + 3:] + # 检查是否是新的代码块开始 - 只要```后面跟着非空白字符,就认为是新代码块开始 + if after_marker.strip() and not after_marker.startswith("\n"): + first_word = after_marker.split()[0] + if not any(c in first_word for c in ",.;:!?()[]{}"): + open_blocks += 1 + else: + open_blocks -= 1 + else: + open_blocks -= 1 + + if open_blocks == 0: + # 找到匹配的结束标记 + return response[content_start:next_marker].strip() + + pos = next_marker + 3 + + # 如果没有找到匹配的结束标记,返回从content_start到末尾的内容 + return response[content_start:].strip() + + except Exception as e: + import logging + logging.info(f"Response: {response}") + logging.error(f"Exception in parse_markdown_block: {str(e)}") + raise Exception(f"解析{block_type}内容失败: {str(e)}") from e + +def create_workflow_files(command_dir, command_name, description, input_required, + code): + """创建工作流命令文件""" + # 创建命令目录 + os.makedirs(command_dir, exist_ok=True) + + # 创建command.yml + input_section = f"input: {'required' if input_required else 'optional'}" + help_section = "help: README.md" + input_param = ' "$input"' if input_required else '' + + # 添加自定义环境配置 + workflow_python_section = "" + python_cmd = "devchat_python" + + yml_content = COMMAND_YML_TEMPLATE.format( + description=description, + input_section=input_section, + help_section=help_section, + workflow_python_section=workflow_python_section, + python_cmd=python_cmd, + input_param=input_param + ) + + with open(os.path.join(command_dir, 'command.yml'), 'w') as f: + f.write(yml_content) + + # 创建command.py + with open(os.path.join(command_dir, 'command.py'), 'w') as f: + f.write(code) + + # 设置执行权限 + os.chmod(os.path.join(command_dir, 'command.py'), 0o755) + + # 创建README.md + readme_content = f"# {command_name}\n\n{description}\n" + with open(os.path.join(command_dir, 'README.md'), 'w') as f: + f.write(readme_content) + + +def main(): + # 获取用户输入 + user_input = sys.argv[1] if len(sys.argv) > 1 else "" + + # 步骤1: 通过AI分析用户输入,提取必要信息 + with Step("分析用户输入,提取工作流信息..."): + workflow_info = extract_workflow_info(user_input=user_input, contexts=CONTEXTS) + + # 步骤3: 生成工作流实现步骤描述 + with Step("生成工作流实现步骤描述..."): + workflow_steps = generate_workflow_steps( + command_name=workflow_info.get("command_name", ""), + description=workflow_info.get("description", ""), + input_requirement="可选", + purpose=workflow_info.get("purpose", ""), + implementation_ideas=workflow_info.get("implementation_ideas", ""), + contexts=CONTEXTS + ) + + workflow_steps = parse_markdown_block(workflow_steps, block_type="steps") + + # 步骤2: 使用Form组件一次性展示所有信息,让用户编辑 + print("\n## 工作流信息\n") + + # 创建所有编辑组件 + command_name_editor = TextEditor(workflow_info.get("command_name", "/example.command")) + description_editor = TextEditor(workflow_info.get("description", "工作流命令描述")) + input_radio = Radio(["必选 (required)", "可选 (optional)"]) + purpose_editor = TextEditor(workflow_info.get("purpose", "请描述工作流的主要目的和功能")) + # ideas_editor = TextEditor(workflow_info.get("implementation_ideas", "请描述实现思路")) + steps_editor = TextEditor(workflow_steps) + + + # 使用Form组件一次性展示所有编辑组件 + form = Form([ + "命令名称:", + command_name_editor, + "输入要求:", + input_radio, + "描述:", + description_editor, + "工作流目的:", + purpose_editor, + "实现步骤:", + steps_editor + ]) + + form.render() + + # 获取用户编辑后的值 + command_name = command_name_editor.new_text + description = description_editor.new_text + input_required = input_radio.selection == 0 + purpose = purpose_editor.new_text + # implementation_ideas = ideas_editor.new_text + workflow_steps = steps_editor.new_text + + + # 步骤4: 生成工作流实现代码 + with Step("生成工作流实现代码..."): + code = generate_workflow_code( + command_name=command_name, + description=description, + input_requirement="必选" if input_required else "可选", + custom_env=False, + workflow_steps=workflow_steps, + contexts=CONTEXTS + ) + + code = code.strip() + start_index = code.find("```python") + end_index = code.rfind("```") + code = code[start_index + len("```python"):end_index].strip() + # code = parse_markdown_block(code, block_type="python") + + # 解析命令路径 + command_dir, final_command = parse_command_path(command_name) + full_command_dir = os.path.join(command_dir, final_command) + + # 步骤5: 创建工作流文件 + with Step(f"创建工作流文件到 {full_command_dir}"): + create_workflow_files( + full_command_dir, + command_name, + description, + input_required, + code + ) + + # 更新斜杠命令 + with Step("更新斜杠命令..."): + IDEService().update_slash_commands() + + # 在新窗口中打开工作流目录 + try: + subprocess.run(["code", full_command_dir], check=True) + except subprocess.SubprocessError: + print(f"无法自动打开编辑器,请手动打开工作流目录: {full_command_dir}") + + print(f"\n✅ 工作流命令 {command_name} 已成功创建!") + print(f"命令文件位置: {full_command_dir}") + print("你现在可以在DevChat中使用这个命令了。") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/merico/chatflow/gen/command.yml b/merico/chatflow/gen/command.yml new file mode 100644 index 0000000..5d8d5be --- /dev/null +++ b/merico/chatflow/gen/command.yml @@ -0,0 +1,4 @@ +description: 生成工作流命令,输入命令信息 +input: required +steps: + - run: $devchat_python $command_path/command.py "$input" \ No newline at end of file diff --git a/merico/chatflow/util/base_functions_guide.md b/merico/chatflow/util/base_functions_guide.md new file mode 100644 index 0000000..06176c0 --- /dev/null +++ b/merico/chatflow/util/base_functions_guide.md @@ -0,0 +1,64 @@ +工作流开发中封装了部分基础函数,方便开发者在工作流中使用。 + +**大模型调用** +针对大模型调用,封装了几个装饰器函数,分别用于调用大模型生成文本和生成json格式的文本。 +- chat装饰器 +用于调用大模型生成文本,并将生成的文本返回给调用者。具体使用示例如下: +```python +from devchat.llm import chat + +PROMPT = """ +对以下代码段进行解释: +{code} +""" +@chat(prompt=PROMPT, stream_out=True) +# pylint: disable=unused-argument +def explain(code): + """ + call ai to explain selected code + """ + pass + +ai_explanation = explain(code="def foo(): pass") +``` +调用explain函数时,需要使用参数名称=参数值的方式传递参数,参数名称必须与PROMPT中使用的参数名称一致。 + +- chat_json装饰器 +用于调用大模型生成json对象。具体使用示例如下: +```python +from devchat.llm import chat_json +PROMPT = ( + "Give me 5 different git branch names, " + "mainly hoping to express: {task}, " + "Good branch name should looks like: /
," + "the final result is output in JSON format, " + 'as follows: {{"names":["name1", "name2", .. "name5"]}}\n' +) + + +@chat_json(prompt=PROMPT) +def generate_branch_name(task): + pass + +task = "fix bug" +branch_names = generate_branch_name(task=task) +print(branch_names["names"]) +``` +调用generate_branch_name函数时,需要使用参数名称=参数值的方式传递参数,参数名称必须与PROMPT中使用的参数名称一致。 +使用chat, chat_json的注意: +1. 使用chat_json装饰器时,返回的结果是一个字典对象,在PROMPT中描述这个字典对象的结构时需要使用{{}}来表示{}。因为后续操作中会使用类似f-string的方式替代PROMPT中的参数,所以在PROMPT中使用{}时需要使用{{}}来表示{}。 +2. 使用这两个装饰器时,PROMPT中不要使用markdown的代码块语法。 + +**工作流调用** +目的是对已有工作流进行复用,在A工作流中调用B工作流。 +- workflow_call函数 +调用指定的工作流,并将指定的参数传递给被调用的工作流。具体使用示例如下: +```python +from lib.workflow import workflow_call + +ret_code = workflow_call("/helloworld some name") +if ret_code == 0: + print("workflow call success") +else: + print("workflow call failed") +``` \ No newline at end of file diff --git a/merico/chatflow/util/contexts.py b/merico/chatflow/util/contexts.py new file mode 100644 index 0000000..eaa9230 --- /dev/null +++ b/merico/chatflow/util/contexts.py @@ -0,0 +1,93 @@ +""" +为workflow命令提供上下文信息 + +编写工作流实现代码需要的上下文: +1. IDE Service接口定义 +2. ChatMark接口定义; +3. 工作流命令列表; +4. 工作流命令组织、实现规范; +5. 基础可用的函数信息; +""" + +import os +from typing import List + +def load_file_in_user_scripts(filename: str) -> str: + """ + 从用户脚本目录中加载文件内容 + """ + user_path = os.path.expanduser("~/.chat/scripts") + file_path = os.path.join(user_path, filename) + with open(file_path, "r") as f: + return f.read() + +def load_local_file(filename: str) -> str: + """ + 从当前脚本所在目录的相对目录加载文件内容 + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(script_dir, filename) + with open(file_path, "r") as f: + return f.read() + +def load_existing_workflow_defines() -> str: + """ 从user scripts目录遍历找到所有command.yml文件,并加载其内容 """ + merico_path = os.path.expanduser("~/.chat/scripts/merico") + community_path = os.path.expanduser("~/.chat/scripts/community") + custom_path = os.path.expanduser("~/.chat/scripts/custom") + + root_paths = [merico_path] + # 遍历community_path、custom_path下直接子目录,将其添加到root_paths作为下一步的根目录 + for path in [community_path, custom_path]: + for root, dirs, files in os.walk(path): + if root == path: + root_paths.extend([os.path.join(root, d) for d in dirs]) + break + + wrkflow_defines = [] + # 遍历所有根目录,对每个根目录进行递归遍历,找到所有command.yml文件,并加载其内容 + # 将目录名称与command.yml内容拼接成一个字符串,添加到wrkflow_defines列表中 + # 例如:~/.chat/scripts/merico/github/commit/command.yml被找到,那么拼接的字符串为: + # 工作流命令/github.commit的定义:\n\n\n + for root_path in root_paths: + for root, dirs, files in os.walk(root_path): + if "command.yml" in files: + with open(os.path.join(root, "command.yml"), "r") as f: + wrkflow_defines.append(f"工作流命令/{root[len(root_path)+1:].replace(os.sep, '.')}的定义:\n{f.read()}\n\n") + return "\n".join(wrkflow_defines) + + +CONTEXTS = f""" +工作流开发需要的上下文信息: + + +# IDE Service接口定义及使用示例 +IDE Service用于在工作流命令中访问与IDE相关的数据,以及调用IDE提供的功能。 +## IDEService接口定义 +接口定义: +{load_file_in_user_scripts("lib/ide_service/service.py")} +涉及类型定义: +{load_file_in_user_scripts("lib/ide_service/types.py")} + +## IDEService接口示例 +{load_local_file("ide_service_demo.py")} + + +# ChatMark接口使用示例 +ChatMark用于在工作流命令中与用户交互,展示信息,获取用户输入。 +{load_file_in_user_scripts("lib/chatmark/chatmark_example/main.py")} +ChatMark Form组件类似于HTML表单,用于组合多个组件,获取相关设置结果。 + + +# 工作流命令规范 +{load_local_file("workflow_guide.md")} + + +# 已有工作流命令定义 +{load_existing_workflow_defines()} + + +# 工作流内部函数定义 +{load_local_file("base_functions_guide.md")} + +""" diff --git a/merico/chatflow/util/ide_service_demo.py b/merico/chatflow/util/ide_service_demo.py new file mode 100644 index 0000000..1caef0f --- /dev/null +++ b/merico/chatflow/util/ide_service_demo.py @@ -0,0 +1,5 @@ +from lib.ide_service import IDEService + +IDEService().ide_logging( + "debug", "Hello IDE Service!" +) \ No newline at end of file diff --git a/merico/chatflow/util/workflow_guide.md b/merico/chatflow/util/workflow_guide.md new file mode 100644 index 0000000..9e3f79e --- /dev/null +++ b/merico/chatflow/util/workflow_guide.md @@ -0,0 +1,52 @@ +一个工作流对应一个目录,目录可以进行嵌套,每个工作流在UI视图中对应了一个斜杠命令,例如/github.commit。 + +github工作流目录结构简化后示意如下: +github + |-- commit + |-- command.yml + |-- command.py + + |-- README.md + |-- new_pr + |-- command.yml + |-- command.py + ...... + +"command.yml"文件定义了工作流命令的元信息,例如命令的名称、描述、参数等。 +"command.py"文件定义了工作流命令的实现逻辑。 + +拿一个hello world工作流命令为例,目录结构如下: +helloworld + |-- command.yml + |-- command.py + |-- README.md + +"command.yml"文件内容如下: +```yaml +description: Hello Workflow, use as /helloworld +input: required +steps: + - run: $devchat_python $command_path/command.py "$input" +```` +"command.py"文件内容如下: +```python +import sys +print(sys.argv[1]) +``` + +使用一个工作流时,在DevChat AI智能问答视图中输入斜杠命令,例如/helloworld ,即可执行该工作流命令。/helloworld表示对应系统可用的工作流。表示工作流的输入,是可选的,如果工作流定义中input为required,则必须输入,否则可以不输入。 + +工作流命令有重载优先级,merico目录下工作流 < community目录下工作流 < custom目录下工作流。 +例如,如果merico与community目录下都有github工作流,那么在DevChat AI智能问答视图中输入/github命令时,会优先执行community目录下的github工作流命令。 + +在custom目录下,通过config.yml文件定义custom目录下的工作流目录。例如: +namespaces: + - bobo +那么custom/bobo就是一个工作流存储目录,可以在该目录下定义工作流命令。 +例如: +custom/bobo + | ---- hello + |------ command.yml + |------ command.py +那么custom/bobo/hello对应的工作流命令就是/hello. +