fix(context): AGENTS.md 注入仅取根目录,子目录文件只通知相对路径

- _load_agents_md_content 由 rglob 递归改为只读根目录 AGENTS.md
- 新增一级子目录扫描:仅通知相对路径(最多 20 个、30s 超时兜底),不注入内容
- 跳过通用非源码目录(.git/node_modules/.venv/dist 等),不针对本项目特化
- 注入模板新增修改时间与子目录通知占位符
This commit is contained in:
JOJO 2026-08-13 00:39:27 +08:00
parent cedef87dab
commit aac374b6f4
3 changed files with 83 additions and 19 deletions

View File

@ -88,6 +88,21 @@ logger = setup_logger(__name__)
DISABLE_LENGTH_CHECK = True 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: class MessagesMixin:
"""MainTerminalContextMixin messages 能力 mixin。""" """MainTerminalContextMixin messages 能力 mixin。"""
@ -244,10 +259,18 @@ class MessagesMixin:
agents_md_content = self._load_agents_md_content() agents_md_content = self._load_agents_md_content()
if not agents_md_content: if not agents_md_content:
return "" 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() agents_md_template = self.load_prompt("agents_md_inject").strip()
if agents_md_template and "{{AGENTS_MD_CONTENT}}" in agents_md_template: if agents_md_template and "{{AGENTS_MD_CONTENT}}" in agents_md_template:
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content) text = 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 = 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 注入开关可能变化,只在开启时生成并缓存,避免关闭时把空字符串冻住
agents_md_inject_enabled = ( agents_md_inject_enabled = (

View File

@ -1,5 +1,7 @@
import asyncio import asyncio
import json import json
import time
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
@ -86,35 +88,72 @@ from config.model_profiles import (
logger = setup_logger(__name__) logger = setup_logger(__name__)
DISABLE_LENGTH_CHECK = True 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: class PromptMixin:
"""MainTerminalContextMixin prompt 能力 mixin。""" """MainTerminalContextMixin prompt 能力 mixin。"""
def _load_agents_md_content(self) -> Optional[str]: def _load_agents_md_content(self) -> Optional[str]:
"""加载工作区根目录的 AGENTS.md 文件内容。 """加载工作区根目录的 AGENTS.md 文件内容(仅根目录,子目录的不注入)。"""
如果存在多个 AGENTS.md 文件返回最新更新的那一个
"""
try: try:
project_path = Path(self.project_path) 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 return None
content = root_file.read_text(encoding='utf-8')
# 查找所有 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')
return content.strip() if content else None return content.strip() if content else None
except Exception as exc: except Exception as exc:
logger.warning(f"[AGENTS.md] 读取失败: {exc}") logger.warning(f"[AGENTS.md] 读取失败: {exc}")
return None 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: def load_prompt(self, name: str) -> str:
"""加载提示模板""" """加载提示模板"""
prompt_file = Path(PROMPTS_DIR) / f"{name}.txt" prompt_file = Path(PROMPTS_DIR) / f"{name}.txt"

View File

@ -1,8 +1,10 @@
【AGENTS.md 项目规范】 【AGENTS.md 项目规范】
以下是工作区根目录 AGENTS.md 文件的内容,请在回答时参考这些项目规范: 以下是工作区根目录 AGENTS.md 文件的内容{{AGENTS_MD_UPDATED_AT}},请在回答时参考这些项目规范:
{{AGENTS_MD_CONTENT}} {{AGENTS_MD_CONTENT}}
--- ---
注意:以上规范来自工作区根目录的 AGENTS.md 文件,若与未来代码冲突,以实际代码为准并及时更新 AGENTS.md。 注意:以上规范来自工作区根目录的 AGENTS.md 文件,若与未来代码冲突,以实际代码为准并及时更新 AGENTS.md。
{{SUB_AGENTS_MD_NOTICE}}