Compare commits
2 Commits
4f940e9a86
...
aac374b6f4
| Author | SHA1 | Date | |
|---|---|---|---|
| aac374b6f4 | |||
| cedef87dab |
@ -88,6 +88,21 @@ logger = setup_logger(__name__)
|
||||
DISABLE_LENGTH_CHECK = True
|
||||
|
||||
|
||||
def _build_sub_agents_md_notice(sub_paths: List[str], total: int) -> str:
|
||||
"""构建子目录 AGENTS.md 路径通知文案(只列相对路径,不注入内容)。无子目录文件时返回空串。"""
|
||||
if not sub_paths:
|
||||
return ""
|
||||
lines = ["另外,工作区以下子目录也存在 AGENTS.md 规范文件:", ""]
|
||||
for p in sub_paths:
|
||||
lines.append(f"- `{p}`")
|
||||
if total > len(sub_paths):
|
||||
lines.append("")
|
||||
lines.append(f"...共 {total} 个")
|
||||
lines.append("")
|
||||
lines.append("这些子目录规范仅适用于其所在目录范围。若你的操作涉及上述目录,请先自行读取对应 AGENTS.md 了解局部规范后再动手,不得将子目录规范当作全局规范套用。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class MessagesMixin:
|
||||
"""MainTerminalContextMixin messages 能力 mixin。"""
|
||||
|
||||
@ -244,10 +259,18 @@ class MessagesMixin:
|
||||
agents_md_content = self._load_agents_md_content()
|
||||
if not agents_md_content:
|
||||
return ""
|
||||
# 子目录 AGENTS.md 只通知相对路径,不注入内容
|
||||
sub_paths, sub_total = self._load_sub_agents_md_paths()
|
||||
sub_notice = _build_sub_agents_md_notice(sub_paths, sub_total)
|
||||
agents_md_template = self.load_prompt("agents_md_inject").strip()
|
||||
if agents_md_template and "{{AGENTS_MD_CONTENT}}" in agents_md_template:
|
||||
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
|
||||
return f"【AGENTS.md 项目规范】\n\n{agents_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 AGENTS.md 文件,若有冲突请以 AGENTS.md 为准。"
|
||||
text = agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
|
||||
text = text.replace("{{AGENTS_MD_UPDATED_AT}}", self._load_agents_md_updated_at())
|
||||
text = text.replace("{{SUB_AGENTS_MD_NOTICE}}", sub_notice)
|
||||
return text
|
||||
# 模板缺失/无占位符时的兜底格式
|
||||
text = f"【AGENTS.md 项目规范】{self._load_agents_md_updated_at()}\n\n{agents_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 AGENTS.md 文件,若有冲突请以 AGENTS.md 为准。"
|
||||
return f"{text}\n\n{sub_notice}" if sub_notice else text
|
||||
|
||||
# AGENTS.md 注入开关可能变化,只在开启时生成并缓存,避免关闭时把空字符串冻住
|
||||
agents_md_inject_enabled = (
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
@ -86,35 +88,72 @@ from config.model_profiles import (
|
||||
logger = setup_logger(__name__)
|
||||
DISABLE_LENGTH_CHECK = True
|
||||
|
||||
# 扫描一级子目录 AGENTS.md 时跳过的通用非源码目录(版本控制/依赖/构建产物/工具数据等,不针对本项目特化)
|
||||
_AGENTS_MD_SKIP_DIRS = {
|
||||
".git", ".hg", ".svn",
|
||||
"node_modules", "bower_components",
|
||||
".venv", "venv", "__pycache__",
|
||||
"dist", "build", ".next", ".nuxt", ".output",
|
||||
".astrion", ".claude", ".cursor", ".idea", ".vscode",
|
||||
}
|
||||
|
||||
|
||||
class PromptMixin:
|
||||
"""MainTerminalContextMixin prompt 能力 mixin。"""
|
||||
|
||||
def _load_agents_md_content(self) -> Optional[str]:
|
||||
"""加载工作区根目录的 AGENTS.md 文件内容。
|
||||
|
||||
如果存在多个 AGENTS.md 文件,返回最新更新的那一个。
|
||||
"""
|
||||
"""加载工作区根目录的 AGENTS.md 文件内容(仅根目录,子目录的不注入)。"""
|
||||
try:
|
||||
project_path = Path(self.project_path)
|
||||
if not project_path.exists():
|
||||
root_file = project_path / "AGENTS.md"
|
||||
if not root_file.is_file():
|
||||
return None
|
||||
|
||||
# 查找所有 AGENTS.md 文件
|
||||
agents_md_files = list(project_path.rglob("AGENTS.md"))
|
||||
if not agents_md_files:
|
||||
return None
|
||||
|
||||
# 找到最新更新的文件
|
||||
latest_file = max(agents_md_files, key=lambda p: p.stat().st_mtime)
|
||||
|
||||
# 读取文件内容
|
||||
content = latest_file.read_text(encoding='utf-8')
|
||||
content = root_file.read_text(encoding='utf-8')
|
||||
return content.strip() if content else None
|
||||
except Exception as exc:
|
||||
logger.warning(f"[AGENTS.md] 读取失败: {exc}")
|
||||
return None
|
||||
|
||||
def _load_agents_md_updated_at(self) -> str:
|
||||
"""返回根目录 AGENTS.md 的最后修改时间文案(如「(最后修改:2026-08-12 17:54)」,失败返回空串)。"""
|
||||
try:
|
||||
root_file = Path(self.project_path) / "AGENTS.md"
|
||||
if not root_file.is_file():
|
||||
return ""
|
||||
mtime = root_file.stat().st_mtime
|
||||
return f"(最后修改:{datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M')})"
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _load_sub_agents_md_paths(self, max_notice: int = 20, timeout_seconds: float = 30.0) -> tuple:
|
||||
"""扫描工作区一级子目录中的 AGENTS.md,返回 (相对路径列表, 实际总数)。
|
||||
|
||||
- 仅扫描根目录与一级子目录,不递归;跳过通用非源码目录(_AGENTS_MD_SKIP_DIRS)。
|
||||
- 列表最多 max_notice 个(超出截断,总数仍在第二项返回)。
|
||||
- 扫描超过 timeout_seconds 秒时停止,返回已扫描到的结果。
|
||||
"""
|
||||
found: List[str] = []
|
||||
total = 0
|
||||
try:
|
||||
project_path = Path(self.project_path)
|
||||
if not project_path.is_dir():
|
||||
return found, total
|
||||
start = time.monotonic()
|
||||
for sub in project_path.iterdir():
|
||||
if time.monotonic() - start >= timeout_seconds:
|
||||
logger.warning(f"[AGENTS.md] 子目录扫描超过 {timeout_seconds}s,返回已扫描到的 {len(found)} 个")
|
||||
break
|
||||
if not sub.is_dir() or sub.name in _AGENTS_MD_SKIP_DIRS:
|
||||
continue
|
||||
candidate = sub / "AGENTS.md"
|
||||
if candidate.is_file():
|
||||
total += 1
|
||||
if len(found) < max_notice:
|
||||
found.append(candidate.relative_to(project_path).as_posix())
|
||||
except Exception as exc:
|
||||
logger.warning(f"[AGENTS.md] 子目录扫描失败: {exc}")
|
||||
return found, total
|
||||
|
||||
def load_prompt(self, name: str) -> str:
|
||||
"""加载提示模板"""
|
||||
prompt_file = Path(PROMPTS_DIR) / f"{name}.txt"
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
【AGENTS.md 项目规范】
|
||||
|
||||
以下是工作区根目录 AGENTS.md 文件的内容,请在回答时参考这些项目规范:
|
||||
以下是工作区根目录 AGENTS.md 文件的内容{{AGENTS_MD_UPDATED_AT}},请在回答时参考这些项目规范:
|
||||
|
||||
{{AGENTS_MD_CONTENT}}
|
||||
|
||||
---
|
||||
注意:以上规范来自工作区根目录的 AGENTS.md 文件,若与未来代码冲突,以实际代码为准并及时更新 AGENTS.md。
|
||||
|
||||
{{SUB_AGENTS_MD_NOTICE}}
|
||||
|
||||
@ -35,8 +35,19 @@ def _parse_skill_metadata(content: str, fallback_name: str) -> Dict[str, str]:
|
||||
metadata["name"] = metadata.get("name") or fallback_name
|
||||
return metadata
|
||||
|
||||
def _workspace_project_root(workspace) -> Path:
|
||||
"""工作区根目录(已 resolve 的绝对路径)。"""
|
||||
return Path(workspace.project_path).expanduser().resolve()
|
||||
|
||||
def _workspace_skills_dir(workspace) -> Path:
|
||||
return (Path(workspace.project_path).expanduser().resolve() / WORKSPACE_SKILLS_DIRNAME).resolve()
|
||||
return _workspace_project_root(workspace) / WORKSPACE_SKILLS_DIRNAME
|
||||
|
||||
def _rel_skill_path(skill_file: Path, workspace) -> str:
|
||||
"""把 skill 文件的绝对路径转成相对工作区根的路径(如 `.astrion/skills/xxx/SKILL.md`)。
|
||||
|
||||
前端 / 菜单插入、对话历史、任务快照均使用该相对路径,避免暴露服务器绝对路径。
|
||||
"""
|
||||
return str(skill_file.relative_to(_workspace_project_root(workspace)).as_posix())
|
||||
|
||||
def _resolve_workspace_skill_path(workspace, raw_path: str) -> Path:
|
||||
try:
|
||||
@ -83,7 +94,7 @@ def _list_workspace_skills(workspace) -> List[Dict[str, str]]:
|
||||
result.append({
|
||||
"name": metadata.get("name") or skill_file.parent.name,
|
||||
"description": metadata.get("description") or "",
|
||||
"path": str(skill_file.resolve()),
|
||||
"path": _rel_skill_path(skill_file, workspace),
|
||||
})
|
||||
return result
|
||||
|
||||
@ -102,7 +113,7 @@ def _build_skill_context_messages(workspace, raw_refs: Any) -> List[Dict[str, st
|
||||
if not raw_path:
|
||||
continue
|
||||
skill_file = _resolve_workspace_skill_path(workspace, raw_path)
|
||||
path_key = str(skill_file)
|
||||
path_key = _rel_skill_path(skill_file, workspace)
|
||||
if path_key in seen_paths:
|
||||
debug_log(f"[SkillsAPI] build_skill_context duplicate path skipped: {path_key}")
|
||||
continue
|
||||
|
||||
Loading…
Reference in New Issue
Block a user