From b93c1796ca8d302e1565b363c3fe7807b34b7ae6 Mon Sep 17 00:00:00 2001 From: ymq Date: Mon, 24 Aug 2026 17:24:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(workspace):=20=E6=9C=BA=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=9B=AE=E5=BD=95=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=96=B0=E5=A2=9E=20build=5Fspace=5Fpath=20+?= =?UTF-8?q?=20=5Fget=5Fspace=5Fdir=EF=BC=8C=E8=A7=92=E8=89=B2=20agent=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B7=AF=E5=BE=84=E5=9F=BA=E5=87=86=E4=BB=8E?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=9B=AE=E5=BD=95=E6=94=B9=E4=B8=BA=E4=BA=A7?= =?UTF-8?q?=E7=BA=BF=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4{space}/=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=20projects/apps/modules=20=E5=8F=AF=E8=BE=BE=EF=BC=9B?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8A=80=E8=83=BD=E6=89=AB=E6=8F=8F=E6=94=B9?= =?UTF-8?q?=E6=89=AB=20modules/=20apps/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_service/agent_loop.py | 60 +++++++++++++++++++++------------- pipeline_service/workspace.py | 10 ++++++ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index bbac1f0..77124da 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -667,6 +667,19 @@ async def _get_workspace_dir(sor, project_id): return _resolve_workspace(ws) +async def _get_space_dir(sor, project_id): + """返回产线工作空间(机构工作空间层){space}/——角色 agent 工具路径基准。 + + 新规下角色在 projects/{项目}/docs/、apps/、modules/ 下读写, + 这些相对路径的基准是 {space}/(不是项目目录 {space}/{项目名})。 + """ + ws = await _get_workspace_dir(sor, project_id) + if not ws: + return ws + # workspace_dir = {base}/{org}/{space}/{项目名},空间目录 = 其父目录 + return os.path.dirname(ws.rstrip('/')) + + def _parse_skill_frontmatter(content): """解析技能 SKILL.md 的 frontmatter(--- 之间),返回 dict。无 frontmatter 返回 {}。""" content = (content or "").strip() @@ -688,23 +701,24 @@ def _parse_skill_frontmatter(content): return fm -def _collect_module_skills(workspace_dir): - """扫描项目 workspace 的 repos/*/skill/SKILL.md,收集模块技能(模块怎么用)。 +def _collect_module_skills(space_dir): + """扫描机构工作空间 modules/*/skill/SKILL.md 和 apps/*/skill/SKILL.md,收集模块技能。 - 模块仓库自带 skill/SKILL.md(架构/数据模型/挂载函数/坑位),但这些不进 skill_loader - 的静态文件树,导致项目角色不知道模块怎么用。这里运行时扫描,作为「项目模块」scope - 注入技能目录 + 支持 load_skill 加载全文。返回 [{name, description, body, path}]。 - 其中 body 是剥离 frontmatter 后的正文(对齐 skill_loader.to_prompt_block 的分层导入规范: - 目录层只放名字+描述,全文层剥离 frontmatter 只返回正文)。 + 新结构:模块本地仓库在机构工作空间 modules/,应用在 apps/。模块仓库自带 + skill/SKILL.md(架构/数据模型/挂载函数/坑位),不进 skill_loader 静态树, + 这里运行时扫描,作为「项目模块」scope 注入技能目录 + 支持 load_skill 加载全文。 + 返回 [{name, description, body, path, repo}]。body 是剥离 frontmatter 后的正文。 """ modules = [] - repos_dir = os.path.join(workspace_dir, 'repos') - if not os.path.isdir(repos_dir): - return modules - for entry in sorted(os.listdir(repos_dir)): - skill_file = os.path.join(repos_dir, entry, 'skill', 'SKILL.md') - if not os.path.isfile(skill_file): - continue + skill_files = [] + for sub in ('modules', 'apps'): + base = os.path.join(space_dir, sub) + if os.path.isdir(base): + for entry in sorted(os.listdir(base)): + sf = os.path.join(base, entry, 'skill', 'SKILL.md') + if os.path.isfile(sf): + skill_files.append((sf, entry)) + for skill_file, entry in skill_files: try: with open(skill_file, 'r', encoding='utf-8') as f: content = f.read() @@ -1221,7 +1235,7 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""): # 项目模块技能:workspace repos/*/skill/SKILL.md(模块怎么用——架构/数据模型/挂载函数/坑位), # 运行时扫描注入,让项目角色知道引用了哪些模块、每个模块怎么挂载(load_xxx 入口/库名/坑)。 try: - ws = await _get_workspace_dir(sor, project_id) + ws = await _get_space_dir(sor, project_id) module_skills = _collect_module_skills(ws) if module_skills: lines.append("\n## 项目模块技能(本项目引用的业务模块,load_skill 加载全文了解模块怎么用)") @@ -1289,7 +1303,7 @@ async def _load_skill_by_name(sor, project_id, role, org_id, name, file_path=Non # 项目模块技能(workspace repos/*/skill/SKILL.md)不在 skill_loader 静态树里,运行时补查。 # 全文层对齐 skill_loader.to_prompt_block:剥离 frontmatter 只返回正文(body)。 try: - ws = await _get_workspace_dir(sor, project_id) + ws = await _get_space_dir(sor, project_id) for m in _collect_module_skills(ws): if m['name'] == name or m['repo'] == name: return f"## [项目模块] {m['name']}\n{m['description']}\n\n{m['body']}" @@ -1339,6 +1353,8 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): logger.warning(f"role_agent_run: org llm missing, task={task_id} org={org_id}") return {"status": "need_info", "task_id": task_id, "question_id": qid} workspace_dir = await _get_workspace_dir(sor, project_id) + # 机构工作空间层({space}/):角色工具路径基准,能访问 projects/、apps/、modules/ + space_dir = await _get_space_dir(sor, project_id) try: await sor.sqlExe("COMMIT", {}) @@ -1386,7 +1402,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): .replace('__ROLE__', role) .replace('__TITLE__', title) .replace('__QNA__', qna_section) - .replace('__WORKSPACE__', workspace_dir) + .replace('__WORKSPACE__', space_dir) .replace('__PROJECT_NAME__', project_name) .replace('__ROLE_SPECIFIC__', role_specific) .replace('__ROLE_SKILLS__', role_skills) @@ -1453,9 +1469,9 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): if tool == "load_skill": result = await _load_skill_by_name(sor, project_id, role, org_id, params.get("name", ""), params.get("file_path") or None) else: - result = await _exec_agent_tool(tool, params, workspace_dir, capability_ctx) + result = await _exec_agent_tool(tool, params, space_dir, capability_ctx) if tool == "write_file" and params.get("path"): - written_files.append(os.path.join(workspace_dir, params["path"])) + written_files.append(os.path.join(space_dir, params["path"])) msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)}) logger.info(f"role_agent tool_call: {tool} -> {str(result)[:100]}") if deliverable or ask_question: @@ -1477,9 +1493,9 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): if tool == 'load_skill': result = await _load_skill_by_name(sor, project_id, role, org_id, params.get('name', ''), params.get('file_path') or None) else: - result = await _exec_agent_tool(tool, params, workspace_dir, capability_ctx) + result = await _exec_agent_tool(tool, params, space_dir, capability_ctx) if tool == 'write_file' and params.get('path'): - written_files.append(os.path.join(workspace_dir, params['path'])) + written_files.append(os.path.join(space_dir, params['path'])) msgs.append({"role": "assistant", "content": raw}) msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"}) logger.info(f"role_agent tool_call(text): {tool} -> {str(result)[:100]}") @@ -1519,7 +1535,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): if isinstance(code_files, list): for f in code_files: if isinstance(f, dict) and f.get("path") and f.get("content"): - abs_path = os.path.join(workspace_dir, f["path"]) + abs_path = os.path.join(space_dir, f["path"]) ok, msg = await _write_code_file(abs_path, f["content"]) if ok: files_written.append(abs_path) diff --git a/pipeline_service/workspace.py b/pipeline_service/workspace.py index d3c885b..b77fb87 100644 --- a/pipeline_service/workspace.py +++ b/pipeline_service/workspace.py @@ -31,6 +31,16 @@ def build_workspace_path(workspace_base, org_id, space, name, project_id=''): path = os.path.join(base_dir, f"{name}_{short}") return path + +def build_space_path(workspace_base, org_id, space): + """构建产线工作空间路径(机构工作空间层):{base}/{org_id}/{space}。 + + 开发产线的机构工作空间(projects/apps/modules 三目录所在层)。 + 角色 agent 的工具路径基准用这个,从而能访问 projects/、apps/、modules/。 + """ + space = (space or GENERAL_SPACE).strip() or GENERAL_SPACE + return os.path.join(workspace_base, str(org_id or '0'), space) + # 可编辑的文本文件扩展名 TEXT_EXTENSIONS = { '.md', '.py', '.js', '.html', '.css', '.json', '.txt',