diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index a8ddadd..8c794b4 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -551,7 +551,7 @@ AGENT_TOOLS = [ {"name":"git_clone","description":"克隆git仓库到机构工作空间(应用→apps/、模块→modules/)","params":{"repo_url":"仓库URL","repo_name":"仓库目录名(可选,默认从URL推断,_app后缀→apps/)","branch":"分支(可选,默认main)"}}, {"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认 apps/modules 下第一个)"}}, {"name":"git_commit_push","description":"git add + commit + push(develop 用它提交模块仓库本地提交作为产出证据;无远程只 commit 不 push,非 git 目录自动 init)","params":{"message":"提交信息","repo_dir":"仓库子目录(可选)"}}, - {"name":"ask_question","description":"向用户提问(缺少信息时使用)","params":{"question":"问题"}}, + {"name":"ask_question","description":"向用户提问(缺少信息时使用)。需要用户提供文件或结构化信息时用 form 声明动态表单,用户可在待办里直接上传/填写完成","params":{"question":"问题","form":"可选,JSON:{\"fields\":[{\"name\":\"字段名\",\"type\":\"file|text|textarea\",\"label\":\"提示\",\"target\":\"文件目标相对路径(如 env/test.json,目录以/结尾保留原文件名)\",\"required\":true}]}"}}, {"name":"deliver","description":"提交最终交付件(代码/文档文件已用write_file写好时调用)","params":{"deliverable_type":"交付件类型(如code_files/design_doc)","summary":"概述","result":"交付件正文","files":"JSON数组[{\"path\":\"modules/{模块}/src/x.py\",\"content\":\"代码内容\"}](可选)"}}, ] @@ -978,13 +978,18 @@ async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missi _own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id}) await sor.sqlExe("COMMIT", {}) owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01' + # env_file 是绝对路径({pdir}/env/{env}.json),target 取项目内相对路径 + _rel = 'env/' + _m = re.search(r'/env/([^/]+)$', env_file or '') + if _m: + _rel = 'env/' + _m.group(1) question = ( f"部署任务「{task_title or task_id[:8]}」无法开始:真实部署环境信息尚未提供。" "这属于外部输入(agent 无法自行产出),因此不回退需求/设计/开发阶段,只等这里补齐。\n\n" f"配置文件:{env_file}\n\n" "缺失字段:\n" + "\n".join(f" · {m}" for m in missing) + - "\n\n请补齐以上字段后提交回答(回答内容会随任务上下文交给执行 agent," - "任务将自动恢复执行、重新校验环境信息)。" + "\n\n**请直接在下方上传该环境配置文件**(含完整部署信息的 JSON;" + "提交后任务自动恢复执行、重新校验环境信息)。" ) await raise_problem( "need_info", question, "system.orchestrator", @@ -992,6 +997,11 @@ async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missi first_handler_role="owner.superuser", first_handler_agentid=owner_id, context={"env_file": env_file, "missing": missing}, suspend_task=True, + form_schema={"fields": [{ + "name": "env_file", "type": "file", + "label": f"环境配置文件({_rel})——需含以下字段:" + "、".join(missing), + "target": _rel, "required": True, "accept": ".json", + }]}, ) logger.warning(f"部署环境信息缺失冒泡(问题通道): task={task_id} missing={missing} owner={owner_id}") @@ -1783,6 +1793,45 @@ async def _rollback_task_chain(sor, project_id, task_id, rollback_role, comment) # ── Agent 工具执行 ── +def _parse_form_schema(v): + """解析 ask_question 的 form 参数 → 规整的 form_schema(非法/空 → None)。 + + LLM 可能传 JSON 字符串或 dict;字段只保留白名单键,防注入。 + 返回 {"fields":[{name,type,label,target,required,accept},...]} 或 None。 + """ + if not v: + return None + try: + schema = json.loads(v) if isinstance(v, str) else v + fields = schema.get('fields') if isinstance(schema, dict) else None + if not isinstance(fields, list) or not fields: + return None + out = [] + for f in fields[:10]: # 最多 10 个字段 + if not isinstance(f, dict): + continue + name = str(f.get('name') or '').strip() + ftype = str(f.get('type') or 'text').strip().lower() + if not name or ftype not in ('file', 'text', 'textarea'): + continue + target = str(f.get('target') or '').strip() + if target: + # target 只允许相对路径字符,禁绝对路径与 .. 穿越 + if target.startswith('/') or '..' in target.split('/'): + target = '' + out.append({ + 'name': name[:40], + 'type': ftype, + 'label': str(f.get('label') or name)[:120], + 'target': target[:200], + 'required': bool(f.get('required')), + 'accept': str(f.get('accept') or '')[:60], + }) + return {"fields": out} if out else None + except Exception: + return None + + def _parse_agent_action(raw): raw = (raw or "").strip() if raw.startswith("```"): @@ -2239,6 +2288,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): deliverable = None ask_question = None + ask_form = None # ask_question 附带的动态表单声明(form_schema) written_files = [] # 本任务执行期间 write_file 实际写入的文件(无 deliver 时的产出兜底) # ── Tool Loop(原生 function calling)── @@ -2287,6 +2337,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): break if tool == "ask_question": ask_question = params.get("question", "") + ask_form = _parse_form_schema(params.get("form")) break if tool == "load_skill": @@ -2309,6 +2360,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): break elif act.get('action') == 'ask': ask_question = act.get('question', '') + ask_form = _parse_form_schema(act.get('form')) break elif act.get('action') == 'tool_call': tool = act.get('tool', '') @@ -2333,7 +2385,8 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): from_agentid=agent_id, tenant_id=project_id, task_id=task_id, first_handler_role="agent.main_agent", - context={"title": title}) + context={"title": title}, + form_schema=ask_form) return {"status": "need_info", "task_id": task_id, "question_id": qid, "question": ask_question} if not deliverable: diff --git a/pipeline_service/communication.py b/pipeline_service/communication.py index 34597fe..3a3e542 100644 --- a/pipeline_service/communication.py +++ b/pipeline_service/communication.py @@ -40,7 +40,7 @@ def _get_db(): async def raise_problem(problem_type, question, from_role, from_agentid="", tenant_id=None, task_id=None, context=None, first_handler_role=None, first_handler_agentid="", - suspend_task=True) -> str: + suspend_task=True, form_schema=None) -> str: """提出问题。首处理方由调用方(读 skill 后)显式指定。 Args: @@ -51,6 +51,10 @@ async def raise_problem(problem_type, question, from_role, from_agentid="", first_handler_role: 首处理角色(不传默认 main_agent) first_handler_agentid: 首处理方具体 agent(空=该角色任意 agent) suspend_task: 是否把任务置 waiting(角色提问挂起等回答) + form_schema: 动态表单声明(可选)。{"fields":[{"name","type":"file|text|textarea", + "label","target","required","accept"}]}——待办详情按它渲染输入界面, + 人类在待办里直接上传文件/填写信息完成任务(文件落项目工作空间, + 路径随回答回流给 agent)。存进 context.form_schema。 Returns: question_id @@ -65,6 +69,10 @@ async def raise_problem(problem_type, question, from_role, from_agentid="", hr = first_handler_role or "agent.main_agent" ha = first_handler_agentid or "" qid = getID() + # form_schema 并入 context 存储(待办详情按它渲染动态表单) + if form_schema: + context = dict(context or {}) + context['form_schema'] = form_schema ctx_json = json.dumps(context, ensure_ascii=False, default=str) if context else None await sor.C('pipeline_agent_questions', { 'id': qid,