feat(workspace): 机构工作空间目录重构——新增 build_space_path + _get_space_dir,角色 agent 工具路径基准从项目目录改为产线工作空间{space}/,使 projects/apps/modules 可达;模块技能扫描改扫 modules/ apps/

This commit is contained in:
ymq 2026-08-24 17:24:41 +08:00
parent f8fef7a7d5
commit b93c1796ca
2 changed files with 48 additions and 22 deletions

View File

@ -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)

View File

@ -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',