Compare commits
2 Commits
4f940e9a86
...
aac374b6f4
| Author | SHA1 | Date | |
|---|---|---|---|
| aac374b6f4 | |||
| cedef87dab |
@ -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 = (
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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}}
|
||||||
|
|||||||
@ -35,8 +35,19 @@ def _parse_skill_metadata(content: str, fallback_name: str) -> Dict[str, str]:
|
|||||||
metadata["name"] = metadata.get("name") or fallback_name
|
metadata["name"] = metadata.get("name") or fallback_name
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
def _workspace_project_root(workspace) -> Path:
|
||||||
|
"""工作区根目录(已 resolve 的绝对路径)。"""
|
||||||
|
return Path(workspace.project_path).expanduser().resolve()
|
||||||
|
|
||||||
def _workspace_skills_dir(workspace) -> Path:
|
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:
|
def _resolve_workspace_skill_path(workspace, raw_path: str) -> Path:
|
||||||
try:
|
try:
|
||||||
@ -83,7 +94,7 @@ def _list_workspace_skills(workspace) -> List[Dict[str, str]]:
|
|||||||
result.append({
|
result.append({
|
||||||
"name": metadata.get("name") or skill_file.parent.name,
|
"name": metadata.get("name") or skill_file.parent.name,
|
||||||
"description": metadata.get("description") or "",
|
"description": metadata.get("description") or "",
|
||||||
"path": str(skill_file.resolve()),
|
"path": _rel_skill_path(skill_file, workspace),
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -102,7 +113,7 @@ def _build_skill_context_messages(workspace, raw_refs: Any) -> List[Dict[str, st
|
|||||||
if not raw_path:
|
if not raw_path:
|
||||||
continue
|
continue
|
||||||
skill_file = _resolve_workspace_skill_path(workspace, raw_path)
|
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:
|
if path_key in seen_paths:
|
||||||
debug_log(f"[SkillsAPI] build_skill_context duplicate path skipped: {path_key}")
|
debug_log(f"[SkillsAPI] build_skill_context duplicate path skipped: {path_key}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user