feat(context): CLAUDE.md 注入开关(默认关)+ .agents/skills 行业目录扫描(含同名冲突报错)
- personalization 新增 claude_md_auto_inject(默认关)与 agents_skills_scan_enabled(默认开),配置存 personalization.json 按工作区隔离 - prompt.py 指令文件加载器泛化,messages.py 新增 CLAUDE.md 冻结注入段(模板 claude_md_inject.txt),子目录只列路径通知 - skills_manager catalog 新增工作区 .agents/skills/ 根(原地读取不同步复制),build_skills_list 按真实路径前缀展示并附来源/冲突说明 - read_skill 支持 .agents/skills 路径解析;与 .astrion/skills 同名重复时报错并引导 read_file 按具体路径读取 - 多智能体 sub_agent_context 同步 CLAUDE.md 开关;顺带修复其 skills 段参数名写错被 except 吞掉恒为空、以及 f-string 打印列表 repr 的存量 bug - 前端个人空间新增两个开关
This commit is contained in:
parent
d5a7c94c8e
commit
6fb5d10542
@ -168,6 +168,8 @@ CUSTOM_ROLES_DIR = str(Path(RUNTIME_ROOT) / _MODE / "mutiagents" / "agents")
|
||||
# web 模式预设角色目录(web 模式下使用,host 模式下不用)
|
||||
WEB_PRESET_ROLES_DIR = str(Path(RUNTIME_ROOT) / "web" / "mutiagents" / "agents")
|
||||
WORKSPACE_SKILLS_DIRNAME = ".astrion/skills"
|
||||
# 行业通用技能目录(Agent Skills 开放标准,工作区根目录下,扫描不同步复制)
|
||||
PROJECT_AGENTS_SKILLS_DIRNAME = ".agents/skills"
|
||||
WORKSPACE_MEMORY_DIRNAME = ".astrion/memory"
|
||||
WORKSPACE_REVIEW_DIRNAME = ".astrion/review"
|
||||
HOST_WORKSPACES_FILE = _resolve_repo_path(
|
||||
@ -193,6 +195,7 @@ __all__ = [
|
||||
"LOGS_DIR",
|
||||
"AGENT_SKILLS_DIR",
|
||||
"WORKSPACE_SKILLS_DIRNAME",
|
||||
"PROJECT_AGENTS_SKILLS_DIRNAME",
|
||||
"WORKSPACE_MEMORY_DIRNAME",
|
||||
"WORKSPACE_REVIEW_DIRNAME",
|
||||
"IS_HOST_MODE",
|
||||
|
||||
@ -88,18 +88,18 @@ logger = setup_logger(__name__)
|
||||
DISABLE_LENGTH_CHECK = True
|
||||
|
||||
|
||||
def _build_sub_agents_md_notice(sub_paths: List[str], total: int) -> str:
|
||||
"""构建子目录 AGENTS.md 路径通知文案(只列相对路径,不注入内容)。无子目录文件时返回空串。"""
|
||||
def _build_sub_md_notice(filename: str, sub_paths: List[str], total: int) -> str:
|
||||
"""构建子目录指令文件路径通知文案(只列相对路径,不注入内容)。无子目录文件时返回空串。"""
|
||||
if not sub_paths:
|
||||
return ""
|
||||
lines = ["另外,工作区以下子目录也存在 AGENTS.md 规范文件:", ""]
|
||||
lines = [f"另外,工作区以下子目录也存在 {filename} 规范文件:", ""]
|
||||
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 了解局部规范后再动手,不得将子目录规范当作全局规范套用。")
|
||||
lines.append(f"这些子目录规范仅适用于其所在目录范围。若你的操作涉及上述目录,请先自行读取对应 {filename} 了解局部规范后再动手,不得将子目录规范当作全局规范套用。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@ -181,7 +181,7 @@ class MessagesMixin:
|
||||
personalization_config = getattr(self.context_manager, "custom_personalization_config", None) or load_personalization_config(self.data_dir)
|
||||
shallow_replace_enabled = bool(personalization_config.get("auto_shallow_compress_enabled", False)) if isinstance(personalization_config, dict) else False
|
||||
|
||||
# 顺序:主prompt → 权限模式 → 执行环境 → 最近对话 → 个性化配置 → 工作区信息 → AGENTS.md → skills → 记忆 → 自定义 → 禁用提示
|
||||
# 顺序:主prompt → 权限模式 → 执行环境 → 最近对话 → 个性化配置 → 工作区信息 → AGENTS.md → CLAUDE.md → skills → 记忆 → 自定义 → 禁用提示
|
||||
|
||||
# 权限模式
|
||||
permission_mode_message = self._get_or_init_frozen_mode_prompt(
|
||||
@ -275,7 +275,7 @@ class MessagesMixin:
|
||||
return ""
|
||||
# 子目录 AGENTS.md 只通知相对路径,不注入内容
|
||||
sub_paths, sub_total = self._load_sub_agents_md_paths()
|
||||
sub_notice = _build_sub_agents_md_notice(sub_paths, sub_total)
|
||||
sub_notice = _build_sub_md_notice("AGENTS.md", 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:
|
||||
text = agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
|
||||
@ -300,8 +300,48 @@ class MessagesMixin:
|
||||
if agents_md_text:
|
||||
messages.append({"role": "system", "content": agents_md_text})
|
||||
|
||||
# CLAUDE.md(默认关闭,开启后与 AGENTS.md 并列注入)
|
||||
def _build_claude_md_prompt() -> str:
|
||||
claude_md_content = self._load_root_md_content("CLAUDE.md")
|
||||
if not claude_md_content:
|
||||
return ""
|
||||
# 子目录 CLAUDE.md 只通知相对路径,不注入内容
|
||||
sub_paths, sub_total = self._load_sub_md_paths("CLAUDE.md")
|
||||
sub_notice = _build_sub_md_notice("CLAUDE.md", sub_paths, sub_total)
|
||||
claude_md_template = self.load_prompt("claude_md_inject").strip()
|
||||
if claude_md_template and "{{CLAUDE_MD_CONTENT}}" in claude_md_template:
|
||||
text = claude_md_template.replace("{{CLAUDE_MD_CONTENT}}", claude_md_content)
|
||||
text = text.replace("{{CLAUDE_MD_UPDATED_AT}}", self._load_root_md_updated_at("CLAUDE.md"))
|
||||
text = text.replace("{{SUB_CLAUDE_MD_NOTICE}}", sub_notice)
|
||||
return text
|
||||
# 模板缺失/无占位符时的兜底格式
|
||||
text = f"【CLAUDE.md 项目规范】{self._load_root_md_updated_at('CLAUDE.md')}\n\n{claude_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 CLAUDE.md 文件,若有冲突请以 CLAUDE.md 为准。"
|
||||
return f"{text}\n\n{sub_notice}" if sub_notice else text
|
||||
|
||||
# 与 AGENTS.md 同理,只在开启时生成并缓存
|
||||
claude_md_inject_enabled = (
|
||||
bool(personalization_config.get("claude_md_auto_inject", False))
|
||||
if isinstance(personalization_config, dict)
|
||||
else False
|
||||
)
|
||||
if claude_md_inject_enabled:
|
||||
claude_md_text = self._get_or_init_frozen_prompt(
|
||||
"frozen_claude_md_prompt",
|
||||
_build_claude_md_prompt,
|
||||
)
|
||||
if claude_md_text:
|
||||
messages.append({"role": "system", "content": claude_md_text})
|
||||
|
||||
# skills 列表
|
||||
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
|
||||
skills_catalog = get_skills_catalog(
|
||||
private_dir=infer_private_skills_dir(self.data_dir),
|
||||
project_path=self.project_path,
|
||||
scan_project_agents=(
|
||||
bool(personalization_config.get("agents_skills_scan_enabled", True))
|
||||
if isinstance(personalization_config, dict)
|
||||
else True
|
||||
),
|
||||
)
|
||||
enabled_skills = merge_enabled_skills(
|
||||
personalization_config.get("enabled_skills") if isinstance(personalization_config, dict) else None,
|
||||
skills_catalog,
|
||||
|
||||
@ -101,23 +101,23 @@ _AGENTS_MD_SKIP_DIRS = {
|
||||
class PromptMixin:
|
||||
"""MainTerminalContextMixin prompt 能力 mixin。"""
|
||||
|
||||
def _load_agents_md_content(self) -> Optional[str]:
|
||||
"""加载工作区根目录的 AGENTS.md 文件内容(仅根目录,子目录的不注入)。"""
|
||||
def _load_root_md_content(self, filename: str) -> Optional[str]:
|
||||
"""加载工作区根目录的指定指令文件内容(仅根目录,子目录的不注入)。"""
|
||||
try:
|
||||
project_path = Path(self.project_path)
|
||||
root_file = project_path / "AGENTS.md"
|
||||
root_file = project_path / filename
|
||||
if not root_file.is_file():
|
||||
return None
|
||||
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}")
|
||||
logger.warning(f"[{filename}] 读取失败: {exc}")
|
||||
return None
|
||||
|
||||
def _load_agents_md_updated_at(self) -> str:
|
||||
"""返回根目录 AGENTS.md 的最后修改时间文案(如「(最后修改:2026-08-12 17:54)」,失败返回空串)。"""
|
||||
def _load_root_md_updated_at(self, filename: str) -> str:
|
||||
"""返回根目录指定指令文件的最后修改时间文案(如「(最后修改:2026-08-12 17:54)」,失败返回空串)。"""
|
||||
try:
|
||||
root_file = Path(self.project_path) / "AGENTS.md"
|
||||
root_file = Path(self.project_path) / filename
|
||||
if not root_file.is_file():
|
||||
return ""
|
||||
mtime = root_file.stat().st_mtime
|
||||
@ -125,8 +125,8 @@ class PromptMixin:
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _load_sub_agents_md_paths(self, max_notice: int = 20, timeout_seconds: float = 30.0) -> tuple:
|
||||
"""扫描工作区一级子目录中的 AGENTS.md,返回 (相对路径列表, 实际总数)。
|
||||
def _load_sub_md_paths(self, filename: str, max_notice: int = 20, timeout_seconds: float = 30.0) -> tuple:
|
||||
"""扫描工作区一级子目录中的指定指令文件,返回 (相对路径列表, 实际总数)。
|
||||
|
||||
- 仅扫描根目录与一级子目录,不递归;跳过通用非源码目录(_AGENTS_MD_SKIP_DIRS)。
|
||||
- 列表最多 max_notice 个(超出截断,总数仍在第二项返回)。
|
||||
@ -141,19 +141,31 @@ class PromptMixin:
|
||||
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)} 个")
|
||||
logger.warning(f"[{filename}] 子目录扫描超过 {timeout_seconds}s,返回已扫描到的 {len(found)} 个")
|
||||
break
|
||||
if not sub.is_dir() or sub.name in _AGENTS_MD_SKIP_DIRS:
|
||||
continue
|
||||
candidate = sub / "AGENTS.md"
|
||||
candidate = sub / filename
|
||||
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}")
|
||||
logger.warning(f"[{filename}] 子目录扫描失败: {exc}")
|
||||
return found, total
|
||||
|
||||
def _load_agents_md_content(self) -> Optional[str]:
|
||||
"""加载工作区根目录的 AGENTS.md 文件内容(仅根目录,子目录的不注入)。"""
|
||||
return self._load_root_md_content("AGENTS.md")
|
||||
|
||||
def _load_agents_md_updated_at(self) -> str:
|
||||
"""返回根目录 AGENTS.md 的最后修改时间文案(如「(最后修改:2026-08-12 17:54)」,失败返回空串)。"""
|
||||
return self._load_root_md_updated_at("AGENTS.md")
|
||||
|
||||
def _load_sub_agents_md_paths(self, max_notice: int = 20, timeout_seconds: float = 30.0) -> tuple:
|
||||
"""扫描工作区一级子目录中的 AGENTS.md,返回 (相对路径列表, 实际总数)。"""
|
||||
return self._load_sub_md_paths("AGENTS.md", max_notice=max_notice, timeout_seconds=timeout_seconds)
|
||||
|
||||
def load_prompt(self, name: str) -> str:
|
||||
"""加载提示模板"""
|
||||
prompt_file = Path(PROMPTS_DIR) / f"{name}.txt"
|
||||
|
||||
@ -119,7 +119,7 @@ class ToolsDefinitionFileToolsMixin:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_skill",
|
||||
"description": "按 skill 名称读取 .astrion/skills/<name>/SKILL.md 内容;内部等价于 read_file 的 read 模式,并返回解析后的 path。",
|
||||
"description": "按 skill 名称读取 SKILL.md 内容(.astrion/skills/<name>/ 或 .agents/skills/<name>/);内部等价于 read_file 的 read 模式,并返回解析后的 path。若技能在 .astrion/skills/ 与 .agents/skills/ 同名重复,会报错并提示改用 read_file 按具体路径读取。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self._inject_intent({
|
||||
|
||||
@ -21,6 +21,7 @@ try:
|
||||
CUSTOM_TOOLS_ENABLED,
|
||||
WORKSPACE_SKILLS_DIRNAME,
|
||||
WORKSPACE_MEMORY_DIRNAME,
|
||||
PROJECT_AGENTS_SKILLS_DIRNAME,
|
||||
)
|
||||
except ImportError:
|
||||
import sys
|
||||
@ -43,6 +44,7 @@ except ImportError:
|
||||
CUSTOM_TOOLS_ENABLED,
|
||||
WORKSPACE_SKILLS_DIRNAME,
|
||||
WORKSPACE_MEMORY_DIRNAME,
|
||||
PROJECT_AGENTS_SKILLS_DIRNAME,
|
||||
)
|
||||
|
||||
from modules.file_manager import FileManager
|
||||
@ -92,9 +94,10 @@ class MainTerminalToolsReadMixin:
|
||||
if not raw:
|
||||
return ""
|
||||
normalized = raw.replace("\\", "/").strip("/")
|
||||
prefix = f"{WORKSPACE_SKILLS_DIRNAME}/"
|
||||
for prefix in (f"{WORKSPACE_SKILLS_DIRNAME}/", f"{PROJECT_AGENTS_SKILLS_DIRNAME}/"):
|
||||
if normalized.lower().startswith(prefix):
|
||||
normalized = normalized[len(prefix):]
|
||||
break
|
||||
if normalized.lower().endswith("/skill.md"):
|
||||
normalized = normalized[: -len("/SKILL.md")]
|
||||
return normalized.strip()
|
||||
@ -108,7 +111,16 @@ class MainTerminalToolsReadMixin:
|
||||
personalization = load_personalization_config(self.data_dir)
|
||||
except Exception:
|
||||
personalization = {}
|
||||
catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
|
||||
scan_project_agents = (
|
||||
bool(personalization.get("agents_skills_scan_enabled", True))
|
||||
if isinstance(personalization, dict)
|
||||
else True
|
||||
)
|
||||
catalog = get_skills_catalog(
|
||||
private_dir=infer_private_skills_dir(self.data_dir),
|
||||
project_path=self.project_path,
|
||||
scan_project_agents=scan_project_agents,
|
||||
)
|
||||
enabled_skills = merge_enabled_skills(
|
||||
personalization.get("enabled_skills") if isinstance(personalization, dict) else None,
|
||||
catalog,
|
||||
@ -119,10 +131,28 @@ class MainTerminalToolsReadMixin:
|
||||
|
||||
normalized_lower = normalized_input.lower()
|
||||
|
||||
def _resolve_item(item: Dict[str, str]) -> Dict[str, Any]:
|
||||
sid = item.get("id")
|
||||
primary_dir = str(item.get("display_dir") or WORKSPACE_SKILLS_DIRNAME).strip("/")
|
||||
conflict_dir = str(item.get("conflict_dir") or "").strip("/")
|
||||
if conflict_dir:
|
||||
# 同名冲突:报错并引导改用 read_file 按具体路径查看
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"技能 '{sid}' 存在同名重复:{primary_dir}/{sid}/SKILL.md 与 "
|
||||
f"{conflict_dir}/{sid}/SKILL.md 均存在,read_skill 无法确定读取哪一份。\n"
|
||||
f"请改用 read_file 工具按具体路径读取查看(可分别读取 "
|
||||
f"{primary_dir}/{sid}/SKILL.md 与 {conflict_dir}/{sid}/SKILL.md 对比),"
|
||||
f"或告知用户删除/重命名其中一份以消除重复。"
|
||||
),
|
||||
}
|
||||
return {"success": True, "skill_id": sid, "display_dir": primary_dir}
|
||||
|
||||
# 1) 优先按 skill id 精确匹配(忽略大小写)
|
||||
id_map = {str(item.get("id", "")).lower(): item.get("id") for item in filtered_catalog if item.get("id")}
|
||||
id_map = {str(item.get("id", "")).lower(): item for item in filtered_catalog if item.get("id")}
|
||||
if normalized_lower in id_map:
|
||||
return {"success": True, "skill_id": id_map[normalized_lower]}
|
||||
return _resolve_item(id_map[normalized_lower])
|
||||
|
||||
# 2) 再按 label 匹配(忽略大小写)
|
||||
label_matches: List[str] = []
|
||||
@ -131,7 +161,7 @@ class MainTerminalToolsReadMixin:
|
||||
if label and label == normalized_lower and item.get("id"):
|
||||
label_matches.append(item["id"])
|
||||
if len(label_matches) == 1:
|
||||
return {"success": True, "skill_id": label_matches[0]}
|
||||
return _resolve_item(id_map[label_matches[0].lower()])
|
||||
if len(label_matches) > 1:
|
||||
return {
|
||||
"success": False,
|
||||
@ -147,8 +177,9 @@ class MainTerminalToolsReadMixin:
|
||||
return resolved
|
||||
|
||||
skill_id = resolved["skill_id"]
|
||||
display_dir = str(resolved.get("display_dir") or WORKSPACE_SKILLS_DIRNAME).strip("/")
|
||||
read_args = {
|
||||
"path": f"{WORKSPACE_SKILLS_DIRNAME}/{skill_id}/SKILL.md",
|
||||
"path": f"{display_dir}/{skill_id}/SKILL.md",
|
||||
"type": "read",
|
||||
}
|
||||
result = self._handle_read_tool(read_args)
|
||||
|
||||
@ -131,27 +131,75 @@ def _build_agents_md_section(workspace_path: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _build_claude_md_section(workspace_path: str, data_dir: str = "") -> str:
|
||||
"""构建 CLAUDE.md 项目规范段(遵循 claude_md_auto_inject 开关,默认关闭)。"""
|
||||
try:
|
||||
from modules.personalization_manager import load_personalization_config
|
||||
|
||||
try:
|
||||
personalization = load_personalization_config(data_dir) if data_dir else {}
|
||||
except Exception:
|
||||
personalization = {}
|
||||
enabled = (
|
||||
bool(personalization.get("claude_md_auto_inject", False))
|
||||
if isinstance(personalization, dict)
|
||||
else False
|
||||
)
|
||||
if not enabled:
|
||||
return ""
|
||||
claude_file = Path(workspace_path) / "CLAUDE.md"
|
||||
if not claude_file.is_file():
|
||||
return ""
|
||||
content = claude_file.read_text(encoding="utf-8").strip()
|
||||
if not content:
|
||||
return ""
|
||||
return (
|
||||
f"## CLAUDE.md 项目规范\n\n"
|
||||
f"> 以下是工作区根目录的 CLAUDE.md 文件内容,请在回答时参考这些项目规范:\n\n"
|
||||
f"{content}\n\n"
|
||||
f"---\n"
|
||||
f"注意:以上规范来自工作区根目录的 CLAUDE.md 文件,若与未来代码冲突,以实际代码为准。\n"
|
||||
)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _build_skills_section(workspace_path: str, data_dir: str = "") -> str:
|
||||
"""构建可用 skill 列表段。"""
|
||||
try:
|
||||
from modules.personalization_manager import load_personalization_config
|
||||
from modules.skills_manager import (
|
||||
get_skills_catalog,
|
||||
build_skills_list,
|
||||
resolve_enabled_skills,
|
||||
infer_private_skills_dir,
|
||||
)
|
||||
|
||||
private_dir = infer_private_skills_dir(data_dir) if data_dir else None
|
||||
catalog = get_skills_catalog(private_dir=private_dir)
|
||||
try:
|
||||
personalization = load_personalization_config(data_dir) if data_dir else {}
|
||||
except Exception:
|
||||
personalization = {}
|
||||
scan_project_agents = (
|
||||
bool(personalization.get("agents_skills_scan_enabled", True))
|
||||
if isinstance(personalization, dict)
|
||||
else True
|
||||
)
|
||||
catalog = get_skills_catalog(
|
||||
private_dir=private_dir,
|
||||
project_path=workspace_path,
|
||||
scan_project_agents=scan_project_agents,
|
||||
)
|
||||
# 子智能体不区分 enabled/disabled,列出全部可用 skill
|
||||
skills_list = build_skills_list(catalog, enabled=None)
|
||||
skills_list = build_skills_list(catalog, resolve_enabled_skills(None, catalog))
|
||||
if not skills_list:
|
||||
return ""
|
||||
return (
|
||||
f"## 可用 AgentSkill\n\n"
|
||||
f"agent skills 系统已启用,以下是可用的 skills(含简要说明):\n\n"
|
||||
f"{skills_list}\n\n"
|
||||
f"使用技能时,优先用 read_skill 通过技能名读取 SKILL.md;"
|
||||
f"需要阅读 skill 目录中的其他文件时再使用 read_file\n"
|
||||
"## 可用 AgentSkill\n\n"
|
||||
"agent skills 系统已启用,以下是可用的 skills(含简要说明):\n\n"
|
||||
+ "\n".join(skills_list)
|
||||
+ "\n\n使用技能时,优先用 read_skill 通过技能名读取 SKILL.md;"
|
||||
"需要阅读 skill 目录中的其他文件时再使用 read_file\n"
|
||||
)
|
||||
except Exception:
|
||||
return ""
|
||||
@ -234,6 +282,11 @@ def build_sub_agent_dynamic_context(
|
||||
if agents_md:
|
||||
sections.append(agents_md)
|
||||
|
||||
# 4.5 CLAUDE.md(遵循 claude_md_auto_inject 开关,默认关闭)
|
||||
claude_md = _build_claude_md_section(workspace_path, data_dir=data_dir)
|
||||
if claude_md:
|
||||
sections.append(claude_md)
|
||||
|
||||
# 5. 可用 skill
|
||||
skills = _build_skills_section(workspace_path, data_dir=data_dir)
|
||||
if skills:
|
||||
|
||||
@ -112,6 +112,8 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
||||
"versioning_backup_mode": "shallow", # 文件备份方式:shallow-浅备份(只备份AI编辑的文件)/ full-完全备份(整个工作区)
|
||||
"versioning_restore_mode": "overwrite", # 版本回溯模式固定为 overwrite
|
||||
"agents_md_auto_inject": False, # AGENTS.md 自动注入开关
|
||||
"claude_md_auto_inject": False, # CLAUDE.md 自动注入开关(默认关闭)
|
||||
"agents_skills_scan_enabled": True, # 自动扫描工作区 .agents/skills/ 行业通用技能目录(默认开启)
|
||||
"new_chat_button_behavior": "route", # 新建对话按钮行为:route-跳转空白新对话页 / blank-立即创建空对话
|
||||
"default_hide_workspace": False, # 默认隐藏工作区
|
||||
"hide_quick_dock": False, # 隐藏快捷窗口(对话区右侧的待办/子智能体/后台指令/文件窗口列)
|
||||
@ -548,6 +550,18 @@ def sanitize_personalization_payload(
|
||||
else:
|
||||
base["agents_md_auto_inject"] = bool(base.get("agents_md_auto_inject", False))
|
||||
|
||||
# CLAUDE.md 自动注入开关(默认关闭)
|
||||
if "claude_md_auto_inject" in data:
|
||||
base["claude_md_auto_inject"] = bool(data.get("claude_md_auto_inject"))
|
||||
else:
|
||||
base["claude_md_auto_inject"] = bool(base.get("claude_md_auto_inject", False))
|
||||
|
||||
# .agents/skills/ 行业通用技能目录扫描开关(默认开启)
|
||||
if "agents_skills_scan_enabled" in data:
|
||||
base["agents_skills_scan_enabled"] = bool(data.get("agents_skills_scan_enabled"))
|
||||
else:
|
||||
base["agents_skills_scan_enabled"] = bool(base.get("agents_skills_scan_enabled", True))
|
||||
|
||||
# 新建对话按钮行为:route-跳转空白新对话页 / blank-立即创建空对话
|
||||
new_chat_behavior = data.get("new_chat_button_behavior", base.get("new_chat_button_behavior"))
|
||||
if isinstance(new_chat_behavior, str) and new_chat_behavior.strip().lower() in ("route", "blank"):
|
||||
|
||||
@ -12,7 +12,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
|
||||
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME
|
||||
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, PROJECT_AGENTS_SKILLS_DIRNAME, WORKSPACE_SKILLS_DIRNAME
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@ -282,8 +282,15 @@ def _scan_skills_catalog(root: Path) -> List[Dict[str, str]]:
|
||||
def get_skills_catalog(
|
||||
base_dir: Optional[str] = None,
|
||||
private_dir: Optional[str | Path] = None,
|
||||
project_path: Optional[str | Path] = None,
|
||||
scan_project_agents: bool = True,
|
||||
) -> List[Dict[str, str]]:
|
||||
"""List available skills from global library plus optional private library."""
|
||||
"""List available skills from global library plus optional private library.
|
||||
|
||||
来源标注:item["source"] = global(仓库 agentskills/)/ private(用户私有)/ agents(工作区 .agents/skills/);
|
||||
item["display_dir"] 为该 skill 在工作区内的可读路径前缀(global/private 经同步复制后在 .astrion/skills/,
|
||||
agents 原地读取在 .agents/skills/)。同名冲突时保留已有条目并打 conflict_dir 标记,由 read_skill 报错引导。
|
||||
"""
|
||||
roots: List[Path] = [Path(base_dir or AGENT_SKILLS_DIR).expanduser().resolve()]
|
||||
if private_dir:
|
||||
private_root = Path(private_dir).expanduser().resolve()
|
||||
@ -292,15 +299,43 @@ def get_skills_catalog(
|
||||
|
||||
merged: Dict[str, Dict[str, str]] = {}
|
||||
order: List[str] = []
|
||||
for root in roots:
|
||||
for index, root in enumerate(roots):
|
||||
source = "global" if index == 0 else "private"
|
||||
for item in _scan_skills_catalog(root):
|
||||
skill_id = item.get("id")
|
||||
if not skill_id:
|
||||
continue
|
||||
if skill_id not in merged:
|
||||
order.append(skill_id)
|
||||
item["source"] = source
|
||||
item["display_dir"] = WORKSPACE_SKILLS_DIRNAME
|
||||
# Later roots (private) override metadata for same id.
|
||||
merged[skill_id] = item
|
||||
|
||||
# 行业通用目录:工作区 .agents/skills/(Agent Skills 开放标准路径;原地读取,不参与同步复制)
|
||||
if project_path and scan_project_agents:
|
||||
try:
|
||||
agents_root = (Path(project_path).expanduser().resolve() / PROJECT_AGENTS_SKILLS_DIRNAME).resolve()
|
||||
except Exception:
|
||||
agents_root = None
|
||||
if agents_root and agents_root not in roots:
|
||||
for item in _scan_skills_catalog(agents_root):
|
||||
skill_id = item.get("id")
|
||||
if not skill_id:
|
||||
continue
|
||||
if skill_id in merged:
|
||||
# 同名冲突:保留已有(.astrion/skills)条目,仅打冲突标记;
|
||||
# prompt 列表与 read_skill 据此提示用户用 read_file 按具体路径查看
|
||||
merged[skill_id]["conflict_dir"] = PROJECT_AGENTS_SKILLS_DIRNAME
|
||||
logger.warning(
|
||||
"[skills] 技能 %s 同时存在于 %s 与 %s,已标记同名冲突",
|
||||
skill_id, WORKSPACE_SKILLS_DIRNAME, PROJECT_AGENTS_SKILLS_DIRNAME,
|
||||
)
|
||||
continue
|
||||
item["source"] = "agents"
|
||||
item["display_dir"] = PROJECT_AGENTS_SKILLS_DIRNAME
|
||||
order.append(skill_id)
|
||||
merged[skill_id] = item
|
||||
return [merged[skill_id] for skill_id in order if skill_id in merged]
|
||||
|
||||
|
||||
@ -353,20 +388,41 @@ def build_skills_list(
|
||||
catalog: Sequence[Dict[str, str]],
|
||||
enabled_skill_ids: Sequence[str],
|
||||
) -> List[str]:
|
||||
"""Build skills list lines / 生成 skills 列表行。"""
|
||||
"""Build skills list lines / 生成 skills 列表行。
|
||||
|
||||
列表行直接使用该 skill 在工作区内的真实路径前缀(.astrion/skills/ 或 .agents/skills/);
|
||||
.agents/skills/ 来源与同名冲突会在列表尾部附说明行。
|
||||
"""
|
||||
if not enabled_skill_ids:
|
||||
return []
|
||||
lookup = {item.get("id"): item for item in catalog if item.get("id")}
|
||||
lines: List[str] = []
|
||||
has_agents_source = False
|
||||
has_conflict = False
|
||||
for skill_id in enabled_skill_ids:
|
||||
meta = lookup.get(skill_id)
|
||||
if not meta:
|
||||
continue
|
||||
display_dir = (meta.get("display_dir") or WORKSPACE_SKILLS_DIRNAME).strip("/")
|
||||
if meta.get("source") == "agents":
|
||||
has_agents_source = True
|
||||
description = (meta.get("description") or "").strip()
|
||||
if description:
|
||||
lines.append(f".astrion/skills/{skill_id}:{description}")
|
||||
else:
|
||||
lines.append(f".astrion/skills/{skill_id}")
|
||||
line = f"{display_dir}/{skill_id}:{description}" if description else f"{display_dir}/{skill_id}"
|
||||
conflict_dir = (meta.get("conflict_dir") or "").strip("/")
|
||||
if conflict_dir:
|
||||
has_conflict = True
|
||||
line += f"(⚠️ 同名重复:{conflict_dir}/{skill_id} 也存在,read_skill 会报错,请改用 read_file 按具体路径读取)"
|
||||
lines.append(line)
|
||||
if has_agents_source:
|
||||
lines.append(
|
||||
f"注:以 {PROJECT_AGENTS_SKILLS_DIRNAME}/ 开头的技能来自工作区 {PROJECT_AGENTS_SKILLS_DIRNAME}/ 目录"
|
||||
"(行业通用技能目录,由项目仓库提供,非 Astrion 管理),同样可用 read_skill 读取。"
|
||||
)
|
||||
if has_conflict:
|
||||
lines.append(
|
||||
f"注:标注同名重复的技能在 {WORKSPACE_SKILLS_DIRNAME}/ 与 {PROJECT_AGENTS_SKILLS_DIRNAME}/ 各有一份,"
|
||||
"内容可能不同;read_skill 对重复名会报错,此时请改用 read_file 读取具体路径查看。"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
|
||||
@ -163,7 +163,7 @@ SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_skill",
|
||||
"description": "按 skill 名称读取 .astrion/skills/<name>/SKILL.md 内容;内部等价于 read_file 的 read 模式,并返回解析后的 path。",
|
||||
"description": "按 skill 名称读取 SKILL.md 内容(.astrion/skills/<name>/ 或 .agents/skills/<name>/);内部等价于 read_file 的 read 模式,并返回解析后的 path。若技能在 .astrion/skills/ 与 .agents/skills/ 同名重复,会报错并提示改用 read_file 按具体路径读取。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
10
prompts/claude_md_inject.txt
Normal file
10
prompts/claude_md_inject.txt
Normal file
@ -0,0 +1,10 @@
|
||||
【CLAUDE.md 项目规范】
|
||||
|
||||
以下是工作区根目录 CLAUDE.md 文件的内容{{CLAUDE_MD_UPDATED_AT}},请在回答时参考这些项目规范:
|
||||
|
||||
{{CLAUDE_MD_CONTENT}}
|
||||
|
||||
---
|
||||
注意:以上规范来自工作区根目录的 CLAUDE.md 文件,若与未来代码冲突,以实际代码为准。
|
||||
|
||||
{{SUB_CLAUDE_MD_NOTICE}}
|
||||
@ -931,6 +931,38 @@
|
||||
})
|
||||
" /><FancyCheck :checked="form.agents_md_auto_inject" /></label>
|
||||
|
||||
<label class="settings-toggle-row"
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">CLAUDE.md 自动注入</span
|
||||
><span class="settings-row-desc"
|
||||
>工作区根目录存在 CLAUDE.md 时自动注入系统提示词(默认关闭,与 AGENTS.md 并列注入)</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
:checked="form.claude_md_auto_inject"
|
||||
@change="
|
||||
personalization.updateField({
|
||||
key: 'claude_md_auto_inject',
|
||||
value: $event.target.checked
|
||||
})
|
||||
" /><FancyCheck :checked="form.claude_md_auto_inject" /></label>
|
||||
|
||||
<label class="settings-toggle-row"
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">扫描 .agents/skills/ 技能目录</span
|
||||
><span class="settings-row-desc"
|
||||
>自动扫描工作区 .agents/skills/ 下的行业通用技能(Agent Skills 开放标准路径),与 .astrion/skills/ 技能一并列出;同名重复时 read_skill 会报错并提示按具体路径读取</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
:checked="form.agents_skills_scan_enabled"
|
||||
@change="
|
||||
personalization.updateField({
|
||||
key: 'agents_skills_scan_enabled',
|
||||
value: $event.target.checked
|
||||
})
|
||||
" /><FancyCheck :checked="form.agents_skills_scan_enabled" /></label>
|
||||
|
||||
<label class="settings-toggle-row"
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">文件修改留痕</span
|
||||
|
||||
@ -132,6 +132,8 @@ interface PersonalForm {
|
||||
deep_compress_trigger_tokens: number | null;
|
||||
deep_compress_form: 'file' | 'inject';
|
||||
agents_md_auto_inject: boolean;
|
||||
claude_md_auto_inject: boolean;
|
||||
agents_skills_scan_enabled: boolean;
|
||||
new_chat_button_behavior: 'route' | 'blank';
|
||||
group_sidebar_by_workspace: boolean;
|
||||
sidebar_pinned_workspaces: string[];
|
||||
@ -336,6 +338,8 @@ const defaultForm = (): PersonalForm => ({
|
||||
deep_compress_trigger_tokens: null,
|
||||
deep_compress_form: 'file',
|
||||
agents_md_auto_inject: false,
|
||||
claude_md_auto_inject: false,
|
||||
agents_skills_scan_enabled: true,
|
||||
new_chat_button_behavior: 'route',
|
||||
group_sidebar_by_workspace: false,
|
||||
sidebar_pinned_workspaces: [],
|
||||
@ -600,6 +604,8 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
),
|
||||
deep_compress_form: data.deep_compress_form === 'inject' ? 'inject' : 'file',
|
||||
agents_md_auto_inject: !!data.agents_md_auto_inject,
|
||||
claude_md_auto_inject: !!data.claude_md_auto_inject,
|
||||
agents_skills_scan_enabled: data.agents_skills_scan_enabled === undefined ? true : !!data.agents_skills_scan_enabled,
|
||||
new_chat_button_behavior: data.new_chat_button_behavior === 'blank' ? 'blank' : 'route',
|
||||
group_sidebar_by_workspace: !!data.group_sidebar_by_workspace,
|
||||
sidebar_pinned_workspaces: Array.isArray(data.sidebar_pinned_workspaces)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user