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) 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): def _parse_skill_frontmatter(content):
"""解析技能 SKILL.md 的 frontmatter(--- 之间),返回 dict。无 frontmatter 返回 {}。""" """解析技能 SKILL.md 的 frontmatter(--- 之间),返回 dict。无 frontmatter 返回 {}。"""
content = (content or "").strip() content = (content or "").strip()
@ -688,23 +701,24 @@ def _parse_skill_frontmatter(content):
return fm return fm
def _collect_module_skills(workspace_dir): def _collect_module_skills(space_dir):
"""扫描项目 workspace 的 repos/*/skill/SKILL.md,收集模块技能(模块怎么用)。 """扫描机构工作空间 modules/*/skill/SKILL.md 和 apps/*/skill/SKILL.md,收集模块技能。
模块仓库自带 skill/SKILL.md(架构/数据模型/挂载函数/坑位),但这些不进 skill_loader 新结构:模块本地仓库在机构工作空间 modules/,应用在 apps/。模块仓库自带
的静态文件树,导致项目角色不知道模块怎么用。这里运行时扫描,作为「项目模块」scope skill/SKILL.md(架构/数据模型/挂载函数/坑位),不进 skill_loader 静态树,
注入技能目录 + 支持 load_skill 加载全文。返回 [{name, description, body, path}]。 这里运行时扫描,作为「项目模块」scope 注入技能目录 + 支持 load_skill 加载全文。
其中 body 是剥离 frontmatter 后的正文(对齐 skill_loader.to_prompt_block 的分层导入规范: 返回 [{name, description, body, path, repo}]。body 是剥离 frontmatter 后的正文。
目录层只放名字+描述,全文层剥离 frontmatter 只返回正文)。
""" """
modules = [] modules = []
repos_dir = os.path.join(workspace_dir, 'repos') skill_files = []
if not os.path.isdir(repos_dir): for sub in ('modules', 'apps'):
return modules base = os.path.join(space_dir, sub)
for entry in sorted(os.listdir(repos_dir)): if os.path.isdir(base):
skill_file = os.path.join(repos_dir, entry, 'skill', 'SKILL.md') for entry in sorted(os.listdir(base)):
if not os.path.isfile(skill_file): sf = os.path.join(base, entry, 'skill', 'SKILL.md')
continue if os.path.isfile(sf):
skill_files.append((sf, entry))
for skill_file, entry in skill_files:
try: try:
with open(skill_file, 'r', encoding='utf-8') as f: with open(skill_file, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
@ -1221,7 +1235,7 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""):
# 项目模块技能:workspace repos/*/skill/SKILL.md(模块怎么用——架构/数据模型/挂载函数/坑位), # 项目模块技能:workspace repos/*/skill/SKILL.md(模块怎么用——架构/数据模型/挂载函数/坑位),
# 运行时扫描注入,让项目角色知道引用了哪些模块、每个模块怎么挂载(load_xxx 入口/库名/坑)。 # 运行时扫描注入,让项目角色知道引用了哪些模块、每个模块怎么挂载(load_xxx 入口/库名/坑)。
try: try:
ws = await _get_workspace_dir(sor, project_id) ws = await _get_space_dir(sor, project_id)
module_skills = _collect_module_skills(ws) module_skills = _collect_module_skills(ws)
if module_skills: if module_skills:
lines.append("\n## 项目模块技能(本项目引用的业务模块,load_skill 加载全文了解模块怎么用)") 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 静态树里,运行时补查。 # 项目模块技能(workspace repos/*/skill/SKILL.md)不在 skill_loader 静态树里,运行时补查。
# 全文层对齐 skill_loader.to_prompt_block:剥离 frontmatter 只返回正文(body)。 # 全文层对齐 skill_loader.to_prompt_block:剥离 frontmatter 只返回正文(body)。
try: try:
ws = await _get_workspace_dir(sor, project_id) ws = await _get_space_dir(sor, project_id)
for m in _collect_module_skills(ws): for m in _collect_module_skills(ws):
if m['name'] == name or m['repo'] == name: if m['name'] == name or m['repo'] == name:
return f"## [项目模块] {m['name']}\n{m['description']}\n\n{m['body']}" 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}") 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} return {"status": "need_info", "task_id": task_id, "question_id": qid}
workspace_dir = await _get_workspace_dir(sor, project_id) workspace_dir = await _get_workspace_dir(sor, project_id)
# 机构工作空间层({space}/):角色工具路径基准,能访问 projects/、apps/、modules/
space_dir = await _get_space_dir(sor, project_id)
try: try:
await sor.sqlExe("COMMIT", {}) 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('__ROLE__', role)
.replace('__TITLE__', title) .replace('__TITLE__', title)
.replace('__QNA__', qna_section) .replace('__QNA__', qna_section)
.replace('__WORKSPACE__', workspace_dir) .replace('__WORKSPACE__', space_dir)
.replace('__PROJECT_NAME__', project_name) .replace('__PROJECT_NAME__', project_name)
.replace('__ROLE_SPECIFIC__', role_specific) .replace('__ROLE_SPECIFIC__', role_specific)
.replace('__ROLE_SKILLS__', role_skills) .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": 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) result = await _load_skill_by_name(sor, project_id, role, org_id, params.get("name", ""), params.get("file_path") or None)
else: 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"): 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)}) msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)})
logger.info(f"role_agent tool_call: {tool} -> {str(result)[:100]}") logger.info(f"role_agent tool_call: {tool} -> {str(result)[:100]}")
if deliverable or ask_question: 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': 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) result = await _load_skill_by_name(sor, project_id, role, org_id, params.get('name', ''), params.get('file_path') or None)
else: 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'): 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": "assistant", "content": raw})
msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"}) msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"})
logger.info(f"role_agent tool_call(text): {tool} -> {str(result)[:100]}") 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): if isinstance(code_files, list):
for f in code_files: for f in code_files:
if isinstance(f, dict) and f.get("path") and f.get("content"): 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"]) ok, msg = await _write_code_file(abs_path, f["content"])
if ok: if ok:
files_written.append(abs_path) 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}") path = os.path.join(base_dir, f"{name}_{short}")
return path 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 = { TEXT_EXTENSIONS = {
'.md', '.py', '.js', '.html', '.css', '.json', '.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.txt',