feat: 增加ai工作流

This commit is contained in:
qiufeng
2026-07-26 23:28:53 +08:00
parent f0dff2a962
commit dfe510cf57
10 changed files with 332 additions and 38 deletions
@@ -263,6 +263,17 @@ public abstract class BaseAssistant implements IClientAssistant, ToolCapable {
public Flux<ChatResponse> chatWithTools(String sessionId, String prompt, Set<String> serviceIds,
cn.com.mfish.common.ai.agent.TenantContext tenantContext) {
ToolCallbackProvider toolProvider = apiToolEngine.getToolCallbackProvider(serviceIds);
// 诊断日志:输出当前步骤注入的工具数量和名称,便于排查 LLM "无法调用工具" 问题
org.springframework.ai.tool.ToolCallback[] diagnosticCallbacks = toolProvider.getToolCallbacks();
if (log.isInfoEnabled()) {
StringBuilder toolNames = new StringBuilder();
for (int i = 0; i < diagnosticCallbacks.length; i++) {
if (i > 0) toolNames.append(", ");
toolNames.append(diagnosticCallbacks[i].getToolDefinition().name());
}
log.info("[BaseAssistant] chatWithTools sessionId={} serviceIds={} toolCount={} tools=[{}]",
sessionId, serviceIds, diagnosticCallbacks.length, toolNames);
}
Map<String, Object> toolContextMap = tenantContext != null
? buildToolContext(tenantContext)
: buildToolContext();
@@ -112,13 +112,21 @@ public class Executor {
* 构建步骤提示词
* <p>
* 把原始需求和当前步骤描述组合,让 LLM 理解这一步要做什么。
* 强化指导:必须实际调用工具,不得仅描述步骤或声称"无法调用"。
* </p>
*/
private String buildStepPrompt(PlanStep step, int stepIndex, AgentPlan plan) {
return "原始需求:" + plan.getOriginalPrompt() + "\n\n" +
"当前是第 " + (stepIndex + 1) + " 步,共 " + plan.getSteps().size() + " 步。\n" +
"这一步的任务:" + step.getDescription() + "\n\n" +
"请调用合适的工具完成这一步任务,并基于工具返回的数据给出这一步的结论。";
"""
# 执行要求(必须严格遵守)
1. **必须实际调用工具**:你已被注入了完成本步骤所需的全部工具,工具列表已在上方列出。必须从中选择匹配的工具并实际调用,不得仅描述"建议调用""需要调用"而不执行。
2. **工具名格式**:工具名采用 `controllerName.operationId` 格式(如 `demoLeaveApply.add`、`demoLeaveApply.submit`)。请从上方工具列表中找到名称完全匹配的工具调用,不要虚构或猜测工具名。
3. **guide 类型 Skill**:如果工具列表中包含 `skill.{code}` 开头的工具,请优先调用它获取操作指南,然后严格按指南中的步骤调用对应的业务工具(如 `demoLeaveApply.add`)完成实际操作。
4. **禁止虚假执行**:严禁出现"我无法直接调用""请您手动执行""请在后台系统中操作"等表述。工具已就绪,你完全可以调用。
5. **结果反馈**:调用工具后,基于工具返回的真实数据给出这一步的结论。如果工具返回失败,说明失败原因。
""";
}
/**
@@ -52,15 +52,19 @@ public class MfishAssistant extends BaseAssistant {
- codeBuildController_ 开头的工具:代码生成器 HTTP 接口(如 codeBuildController_add 新增代码构建配置)
在执行用户请求时,请遵循以下规则:
1. 先分析需求:理解用户想做什么,判断属于哪个领域(代码生成?工作流?权限?)
1. 先分析需求:理解用户想做什么,判断属于哪个领域(代码生成?工作流?权限?请假?
2. 优先调用对应 skill 工具:如用户要"创建代码",先调用 skill.code-gen-guide 获取代码生成流程指南,
再按指南中的步骤调用具体工具(如 codeBuildController_add 或 sys.getTableList
3. 信息不足时主动提问:如果用户的需求缺少必要信息(如要生成代码但未说明表名、要创建流程但未说明审批节点),
要盲目调用工具先用对话方式向用户询问缺失的关键信息
4. 精确选择工具:仔细阅读工具名和描述,不要把"代码生成"工具和"自助API"工具混淆
3. 【关键】信息不足时禁止调用工具:如果用户的需求缺少必要信息(如要请假但未说明起止时间、
生成代码但未说明表名),**绝对不能盲目调用工具**,必须先用对话方式向用户追问所有必填信息
宁可多问一句,不可错调一次。工具描述中的"必填字段"必须全部齐备才能调用
4. 【关键】多工具编排顺序:部分业务需要多步操作(如请假需先 add 建单据再 submit 提交审批,
代码生成需先配数据库再选表)。必须严格按工具描述中"前置条件""后续步骤"的顺序执行,
不得跳步、不得颠倒顺序。前一步的返回值(如单据 ID)是后一步的入参。
5. 精确选择工具:仔细阅读工具名和描述,不要把"代码生成"工具和"自助API"工具混淆。
工具名前缀代表所属服务:sys=系统服务、oauth=认证服务、nocode=低代码服务、skill=技能包
5. 工具结果反馈:工具执行后,将结果用通俗易懂的方式告诉用户
6. 多步操作引导:对于复杂操作(如代码生成需要先配数据库连接再选表),分步骤引导用户完成
6. 工具结果反馈:工具执行后,将结果用通俗易懂的方式告诉用户
7. 多步操作引导:对于复杂操作,分步骤引导用户完成,每步执行后告知结果再进行下一步
""";
}
@@ -4,6 +4,7 @@ import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.common.ai.agent.TenantContext;
import cn.com.mfish.common.ai.capability.ActionDefinition;
import cn.com.mfish.common.ai.capability.CapabilityEngine;
import cn.com.mfish.common.ai.capability.SkillCapabilityEngine;
import cn.com.mfish.common.ai.entity.AgentPlan;
import cn.com.mfish.common.ai.entity.PlanStep;
import cn.com.mfish.common.ai.memory.ConversationMemory;
@@ -18,7 +19,9 @@ import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -64,13 +67,16 @@ public class Planner {
private final LlmModelRouter llmModelRouter;
private final CapabilityEngine capabilityEngine;
private final ConversationMemoryStore memoryStore;
private final SkillCapabilityEngine skillCapabilityEngine;
public Planner(ChatMemory chatMemory, LlmModelRouter llmModelRouter,
CapabilityEngine capabilityEngine, ConversationMemoryStore memoryStore) {
CapabilityEngine capabilityEngine, ConversationMemoryStore memoryStore,
SkillCapabilityEngine skillCapabilityEngine) {
this.chatMemory = chatMemory;
this.llmModelRouter = llmModelRouter;
this.capabilityEngine = capabilityEngine;
this.memoryStore = memoryStore;
this.skillCapabilityEngine = skillCapabilityEngine;
}
/**
@@ -122,6 +128,8 @@ public class Planner {
.responseEntity(AgentPlan.class);
AgentPlan plan = Objects.requireNonNullElseGet(responseEntity.entity(), () -> fallbackPlan(finalPrompt));
plan.setOriginalPrompt(prompt);
// 合并 guide 类型 Skill 声明的 requires 到各步骤的 serviceIds
mergeGuideRequires(plan);
log.info("[Planner] 规划完成, 步骤数={}, summary={}",
plan.getSteps() != null ? plan.getSteps().size() : 0, plan.getSummary());
return plan;
@@ -240,6 +248,46 @@ public class Planner {
""";
}
/**
* 合并 guide 类型 Skill 声明的 requires 到各步骤的 serviceIds
* <p>
* Planner 生成的步骤可能只包含 skill-{code} 的 serviceId,但 guide 类型 Skill
* 在执行时需要调用被指南引用的业务工具(如 mf-demo 的 add/submit)。
* 此方法扫描每个步骤的 serviceIds,若发现 guide skill,将其 requires 合并进去。
* </p>
* <p>
* 同时在步骤描述中追加提示,告诉 Executor 这一步会调用 guide skill 获取操作指南,
* 拿到指南后必须按指南实际调用业务工具。
* </p>
*/
private void mergeGuideRequires(AgentPlan plan) {
if (plan == null || plan.getSteps() == null || plan.getSteps().isEmpty()) {
return;
}
for (PlanStep step : plan.getSteps()) {
if (step.getServiceIds() == null || step.getServiceIds().isEmpty()) {
continue;
}
Set<String> merged = new LinkedHashSet<>(step.getServiceIds());
for (String serviceId : step.getServiceIds()) {
// 识别 skill 类型的 serviceId(格式为 skill-{code}
if (serviceId == null || !serviceId.startsWith("skill-")) {
continue;
}
// 转换为动作名 skill.{code}
String skillCode = serviceId.substring("skill-".length());
String actionName = "skill." + skillCode;
List<String> requires = skillCapabilityEngine.getGuideRequires(actionName);
if (!requires.isEmpty()) {
merged.addAll(requires);
log.info("[Planner] 步骤合并 guide requires: skill={} requires={} -> serviceIds={}",
skillCode, requires, merged);
}
}
step.setServiceIds(new ArrayList<>(merged));
}
}
/**
* 将动作列表按 serviceId 分组(保持注册顺序)
*/
@@ -52,7 +52,20 @@ public class DemoLeaveApplyController {
* @return 返回添加结果
*/
@Log(title = "请假申请审批Demo-添加", operateType = OperateType.INSERT)
@Operation(summary = "请假申请审批Demo-添加")
@Operation(summary = "新增请假申请草稿",
description = """
【业务流程】新增请假申请草稿单据(auditState=-1 草稿状态,不会启动审批)。
提交审批需另行调用"提交请假审批"接口。
【前置条件】无
【必填字段】
- title: 请假标题
- leaveType: 请假类型(1=事假 2=病假 3=年假)
- startTime: 开始时间(格式 yyyy-MM-dd HH:mm:ss
- endTime: 结束时间(格式 yyyy-MM-dd HH:mm:ss
- reason: 请假原因
【可选字段】leaveDays 可不填,系统按 startTime/endTime 自动计算
【后续步骤】调用成功后,从返回结果中取 id,调用"提交请假审批"接口启动审批流程
""")
@PostMapping
public Result<DemoLeaveApply> add(@RequestBody DemoLeaveApply demoLeaveApply) {
return demoLeaveApplyService.add(demoLeaveApply);
@@ -103,7 +116,12 @@ public class DemoLeaveApplyController {
* @param id 唯一ID
* @return 返回请假申请对象
*/
@Operation(summary = "请假申请审批Demo-通过id查询")
@Operation(summary = "查询请假申请详情",
description = """
【业务流程】根据单据 ID 查询请假申请详情,包含审批状态、请假信息等。
【用途】用于提交审批后查询当前状态(-1=草稿 0=审核中 1=已通过 2=已驳回)
【参数】id 为请假申请单据 ID
""")
@GetMapping("/{id}")
public Result<DemoLeaveApply> queryById(@Parameter(name = "id", description = "唯一ID") @PathVariable String id) {
return demoLeaveApplyService.queryById(id);
@@ -129,7 +147,14 @@ public class DemoLeaveApplyController {
* @return 返回提交结果
*/
@Log(title = "请假申请审批Demo-提交审批", operateType = OperateType.UPDATE)
@Operation(summary = "请假申请审批Demo-提交审批")
@Operation(summary = "提交请假审批",
description = """
【业务流程】提交请假申请进入审批流程,启动工作流。
会将单据状态从草稿(-1)变为审核中(0),并按 BPMN 流程定义流转到第一个审批节点。
【前置条件】必须先调用"新增请假申请草稿"接口创建单据,获取返回的 id
【参数】id 为"新增请假申请草稿"接口返回的单据 ID
【后续步骤】流程启动后,等待审批人在待办列表中处理;可通过"查询请假申请"接口查看状态
""")
@PostMapping("/submit/{id}")
public Result<DemoLeaveApply> submit(@PathVariable String id) {
return demoLeaveApplyService.submit(id);
@@ -142,7 +167,13 @@ public class DemoLeaveApplyController {
* @return 返回撤回结果
*/
@Log(title = "请假申请审批Demo-撤回审批", operateType = OperateType.UPDATE)
@Operation(summary = "请假申请审批Demo-撤回审批")
@Operation(summary = "撤回请假审批",
description = """
【业务流程】撤回已提交的请假审批,终止工作流流程。
会将单据状态从审核中(0)恢复为草稿(-1)。
【前置条件】该请假申请已通过"提交请假审批"接口提交,且当前处于审核中状态
【参数】id 为请假申请单据 ID
""")
@PostMapping("/revoke/{id}")
public Result<DemoLeaveApply> revoke(@PathVariable String id) {
return demoLeaveApplyService.revoke(id);
@@ -154,6 +154,32 @@ public class SkillCapabilityEngine implements CapabilitySubEngine {
return cachedActions;
}
/**
* 查询指定 guide 类型 Skill 声明依赖的业务服务ID列表
* <p>
* 供 Planner 在规划时调用:当 Planner 发现某步骤需要调用 guide 类型 Skill 时,
* 通过此方法获取 Skill 声明的 requires(如 mf-demo),合并到步骤的 serviceIds
* 确保 Executor 执行时 LLM 能看到被指南引用的业务工具。
* </p>
* <p>
* 非 guide 类型或未声明 requires 的 Skill 返回空列表。
* </p>
*
* @param skillActionName Skill 动作名(如 skill.leave-apply
* @return 依赖的业务服务ID列表,可能为空
*/
public List<String> getGuideRequires(String skillActionName) {
if (skillActionName == null) {
return Collections.emptyList();
}
SkillInfo skill = actionRegistry.get(skillActionName);
if (skill == null || !"guide".equalsIgnoreCase(skill.getType())) {
return Collections.emptyList();
}
List<String> requires = skill.getRequires();
return requires != null ? requires : Collections.emptyList();
}
/**
* 执行 Skill 动作(显式调用模式)
* <p>
@@ -269,6 +295,10 @@ public class SkillCapabilityEngine implements CapabilitySubEngine {
/**
* 执行提示词级 Skill:模板填充 + LLM 调用
* <p>
* guide 类型 Skill 不走 LLM 调用,直接返回模板内容给外层 LLM,
* 指导其调用其他工具完成多步编排。
* </p>
*/
private String executePromptSkill(SkillInfo skill, Map<String, Object> params, ExecutionContext ctx) {
String promptTemplate = skill.getPromptTemplate();
@@ -276,7 +306,16 @@ public class SkillCapabilityEngine implements CapabilitySubEngine {
throw new IllegalStateException("Skill " + skill.getSkillCode() + " 无提示词模板");
}
// 填充模板占位符
// guide 类型:直接返回模板内容,不走 LLM 调用
// 避免内部 LLM 无工具时产生幻觉(假装已执行但实际未调工具)
if ("guide".equalsIgnoreCase(skill.getType())) {
String content = fillTemplate(promptTemplate, params != null ? params : Collections.emptyMap());
log.info("[SkillCapabilityEngine] guide 类型 Skill 直接返回内容 skill={} length={}",
skill.getSkillCode(), content.length());
return content;
}
// prompt 类型:填充模板占位符 + 调用 LLM
String filledPrompt = fillTemplate(promptTemplate, params != null ? params : Collections.emptyMap());
// 获取租户 ID(从执行上下文)
@@ -11,6 +11,8 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -18,6 +20,7 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Skill 文件加载器
@@ -228,6 +231,8 @@ public class SkillFileLoader {
.setSkillCode(skillCode)
.setSkillName(meta.getOrDefault("title", skillCode))
.setDescription(meta.getOrDefault("description", skillCode))
.setType(meta.getOrDefault("type", "prompt"))
.setRequires(parseRequires(meta.get("requires")))
.setPromptTemplate(promptBody)
.setModelName(meta.get("model"))
.setParams(params)
@@ -278,6 +283,34 @@ public class SkillFileLoader {
return meta;
}
/**
* 解析 frontmatter 的 requires 字段为服务ID列表
* <p>
* 支持两种格式:
* <ul>
* <li>逗号分隔字符串:{@code mf-demo,mf-sys}</li>
* <li>YAML 数组格式:{@code [mf-demo,mf-sys]}</li>
* </ul>
* </p>
*/
private List<String> parseRequires(String value) {
if (value == null || value.isEmpty()) {
return Collections.emptyList();
}
// 去除方括号
String cleaned = value.trim();
if (cleaned.startsWith("[") && cleaned.endsWith("]")) {
cleaned = cleaned.substring(1, cleaned.length() - 1);
}
if (cleaned.isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(cleaned.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
/**
* 从 prompt 模板提取 {param} 占位符参数名(去重、保序)
*/
@@ -34,6 +34,30 @@ public class SkillInfo {
*/
private String skillName;
/**
* Skill 类型(来自 frontmatter 的 type 字段,缺省 prompt
* <ul>
* <li>{@code prompt}:提示词型,内部调 LLM 处理后返回结果(如翻译、摘要)</li>
* <li>{@code guide}:指南型,直接返回 markdown 内容给外层 LLM
* 指导其调用其他工具完成多步编排(如请假流程需先 add 再 submit)。
* 不走内部 LLM 调用,避免幻觉(LLM 假装已执行但实际未调工具)</li>
* </ul>
*/
private String type = "prompt";
/**
* 依赖的业务服务ID列表(来自 frontmatter 的 requires 字段,逗号分隔)
* <p>
* guide 类型 Skill 声明其编排流程需要调用的业务服务,如 {@code mf-demo,mf-sys}。
* Planner 规划时会将这些 serviceId 与 skill 的 serviceId 合并,
* 确保 Executor 执行时 LLM 能看到被指南引用的业务工具。
* </p>
* <p>
* prompt 类型 Skill 不需要此字段(内部调 LLM,不编排外部工具)。
* </p>
*/
private List<String> requires;
/**
* 动作描述(来自 frontmatter 的 description 字段,供 LLM 理解用途)
*/
@@ -79,7 +79,8 @@ public class OpenApiDocParser {
if (operation == null) continue;
try {
HttpToolCallback callback = parseOperation(path, method, operation, schemas, webClient, serviceId);
// 微服务模式无服务前缀,controllerName 直接从路径首段提取
HttpToolCallback callback = parseOperation(path, method, operation, schemas, webClient, serviceId, null);
callbacks.add(callback);
log.debug("[OpenAPI解析] service={} 生成工具: {} {} {}",
serviceId, method.toUpperCase(), path, callback.getToolName());
@@ -133,8 +134,17 @@ public class OpenApiDocParser {
String path = entry.getKey();
if (shouldSkipPath(path)) continue;
// 根据路径前缀找到对应的 serviceId
String serviceId = resolveServiceId(path, sortedMappings);
// 根据路径前缀找到对应的 serviceId 和匹配的前缀(用于剥离后提取 controllerName
String serviceId = null;
String matchedPrefix = null;
for (Map.Entry<String, String> mapping : sortedMappings) {
String prefix = mapping.getKey();
if (path.equals(prefix) || path.startsWith(prefix + "/")) {
serviceId = mapping.getValue();
matchedPrefix = prefix;
break;
}
}
if (serviceId == null) {
log.debug("[OpenAPI解析] 单实例模式 路径 {} 无匹配的服务前缀,跳过", path);
continue;
@@ -146,7 +156,7 @@ public class OpenApiDocParser {
if (operation == null) continue;
try {
HttpToolCallback callback = parseOperation(path, method, operation, schemas, webClient, serviceId);
HttpToolCallback callback = parseOperation(path, method, operation, schemas, webClient, serviceId, matchedPrefix);
result.computeIfAbsent(serviceId, k -> new ArrayList<>()).add(callback);
log.debug("[OpenAPI解析] 单实例 service={} 生成工具: {} {} {}",
serviceId, method.toUpperCase(), path, callback.getToolName());
@@ -164,34 +174,32 @@ public class OpenApiDocParser {
return result;
}
/**
* 根据路径前缀匹配 serviceId
* <p>
* 路径如 /sys/dict 匹配前缀 /sys,返回对应的 serviceId mf-sys。
* 已按前缀长度降序排列,确保最长前缀优先匹配。
* </p>
*/
private String resolveServiceId(String path, List<Map.Entry<String, String>> sortedMappings) {
for (Map.Entry<String, String> mapping : sortedMappings) {
String prefix = mapping.getKey();
// 精确匹配或前缀匹配(前缀后跟 / 或刚好等于前缀)
if (path.equals(prefix) || path.startsWith(prefix + "/")) {
return mapping.getValue();
}
}
return null;
}
/**
* 解析单个 operation,生成 HttpToolCallback
* <p>
* 工具名采用 {@code controllerName.operationId} 格式(如 {@code demoLeaveApply.add}),
* 从路径首段提取 controllerName(单实例模式需先剥离服务前缀),
* 避免跨服务同名 operationId 冲突,同时与 skill 指南中声明的工具名保持一致。
* </p>
*
* @param path OpenAPI 路径(如 /demoLeaveApply 或 /demo/demoLeaveApply/submit/{id}
* @param method HTTP 方法
* @param operation OpenAPI operation 对象
* @param schemas components.schemas 全集(用于 $ref 解析)
* @param webClient 已配置 baseUrl 的 WebClient
* @param serviceId 服务ID(用于工具描述)
* @param servicePathPrefix 服务路径前缀(单实例模式如 /demo,微服务模式为 null)
*/
private HttpToolCallback parseOperation(String path, String method, JSONObject operation,
JSONObject schemas, WebClient webClient, String serviceId) {
// 1. 工具名:优先用 operationId,否则用 method+path 生成
JSONObject schemas, WebClient webClient, String serviceId,
String servicePathPrefix) {
// 1. 工具名:controllerName.operationId
String operationId = operation.getString("operationId");
if (operationId == null || operationId.isEmpty()) {
operationId = method + path.replaceAll("[{}:/]", "_");
}
String controllerName = extractControllerName(path, servicePathPrefix);
String toolName = controllerName + "." + operationId;
// 2. 工具描述:summary + description
String summary = operation.getString("summary");
@@ -243,7 +251,33 @@ public class OpenApiDocParser {
}
}
return new HttpToolCallback(webClient, operationId, toolDescription, method, path, paramInfos);
return new HttpToolCallback(webClient, toolName, toolDescription, method, path, paramInfos);
}
/**
* 从路径中提取 controllerName 作为工具名前缀
* <p>
* 微服务模式:path=/demoLeaveApply/submit/{id} → 首段 demoLeaveApply
* 单实例模式:path=/demo/demoLeaveApply/submit/{id}servicePathPrefix=/demo
* → 剥离 /demo → /demoLeaveApply/submit/{id} → 首段 demoLeaveApply
* </p>
* <p>
* 跳过 {pathVar} 形式的路径变量段;无可用段时返回 "default"。
* </p>
*/
private String extractControllerName(String path, String servicePathPrefix) {
String remaining = path;
if (servicePathPrefix != null && !servicePathPrefix.isEmpty()
&& path.startsWith(servicePathPrefix + "/")) {
remaining = path.substring(servicePathPrefix.length());
}
String[] segments = remaining.split("/");
for (String seg : segments) {
if (!seg.isEmpty() && !seg.startsWith("{")) {
return seg;
}
}
return "default";
}
/**
@@ -0,0 +1,62 @@
---
name: leave-apply
type: guide
requires: mf-demo
description: 指导 AI 完成请假申请的完整流程:信息收集、创建草稿、提交审批、状态查询
---
【重要】这是一份操作指南,不是执行结果。你尚未执行任何操作!必须按以下步骤实际调用工具完成请假申请,不得假装已执行。
你是摸鱼低代码平台的请假申请助手。当用户表达"请假"、"请假申请"、"提交请假"、"我要请假"等意图时,按以下流程操作。
## 前置信息收集(必须)
在调用任何工具前,必须先向用户确认以下信息,**缺失任何一项都要主动追问**,不得盲目调用工具:
| 信息 | 字段 | 说明 | 格式 |
|------|------|------|------|
| 请假标题 | title | 简短的请假主题 | 字符串,如"家中有事请假" |
| 请假类型 | leaveType | 1=事假 2=病假 3=年假 | 数字 1/2/3 |
| 开始时间 | startTime | 请假开始时间 | yyyy-MM-dd HH:mm:ss |
| 结束时间 | endTime | 请假结束时间 | yyyy-MM-dd HH:mm:ss |
| 请假原因 | reason | 详细请假原因 | 字符串 |
**示例对话**
- 用户:"帮我请假"
- 助手追问:"好的,请提供以下信息:1) 请假标题;2) 请假类型(1=事假 2=病假 3=年假);3) 开始时间(如 2026-07-24 09:00:00);4) 结束时间;5) 请假原因"
## 执行步骤(严格按顺序)
### 步骤 1:创建请假草稿
- 工具:`demoLeaveApply.add`POST /demoLeaveApply
- body 包含:title、leaveType、startTime、endTime、reason
- leaveDays 可不填,系统按 startTime/endTime 自动计算
- 从返回结果中取出 `data.id`,这是单据 ID,后续步骤需要
### 步骤 2:提交审批
- 工具:`demoLeaveApply.submit`POST /demoLeaveApply/submit/{id}
- path 参数 id 为步骤 1 返回的单据 ID
- 提交后单据状态变为审核中(0),工作流流程启动
### 步骤 3:反馈结果
向用户告知:
- 请假申请已提交,等待审批
- 单据 ID
- 当前状态(审核中)
## 可选操作
### 查询请假状态
- 工具:`demoLeaveApply.queryById`GET /demoLeaveApply/{id}
- 返回 auditState 字段:-1=草稿 0=审核中 1=已通过 2=已驳回
### 撤回请假
- 仅当状态为审核中(0)时可撤回
- 工具:`demoLeaveApply.revoke`POST /demoLeaveApply/revoke/{id}
## 禁止行为
- ❌ 不得在未收集完整信息时调用 add 工具
- ❌ 不得跳过 add 直接调用 submit(submit 需要真实存在的单据 ID)
- ❌ 不得使用 workflow.execute_* 工具发起请假(请假流程有业务单据,必须走 add+submit
- ❌ 不得编造单据 ID