Files
dsh_ai1net_server/归档/技能包快照/session-mechanism-20261004/scripts/hooks/_env.py
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

225 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""**环境定位与环境自检**(2026-10-02 · 用户定案「技能使用时必须先检查环境配置」)。
一句话:把「技能库根 / 配置目录」从**各脚本各自推导**改成**一处判据+一处体检**。
## 为什么要有这个模块(2026-10-02 实测事故)
`reply-style-guard.py` 曾被注册成文档库 `07-scripts/reply-style-guard.py`(D 盘旧副本)。
它沿 `__file__` 上溯 4 级找 `skills/` ⇒ 从 D 盘够不到 ⇒ 落进最后一行
`os.path.expanduser('~/.workbuddy/skills')`。
⚠️🔴 **Windows 上 `~` 不等于真配置目录**(本机真值在 E 盘 `CODEBUDDY_CONFIG_DIR`)⇒ 那一行是**错的**。
后果比报错更坏:该钩子的设计判据是「哪一级都读不到就**静默零输出**、不影响任何人」
⇒ **「机制坏了」和「没配规则」表现完全一样**,日志里只有一行 `core=0 字符`。
(真实代价:另一个工作区为这件事绕了两轮,在「规则文件在不在」上打转。)
## 三条硬规矩(本模块的判据)
1. 🔴 **顺序固定**:`DSH_SKILLS_ROOT` env → `CODEBUDDY_CONFIG_DIR` env(+ `roots.env` 兜底)
→ 沿 `__file__` 上溯找含 `agent-operating-rules` 的那一层 → ⛔ **不再回落到 `~`**。
2. 🔴 **找不到就报错,不静默**:写 **stderr** + 返回 `None`;调用方据此**非零退出**。
⛔ 绝不「静默零输出」—— 那正是把两类事故压成一个表象的元凶。
3. 🔴 **体检与环境标记分离**:`env_check()` 只**读**(体检);`write_env_stamp()` 才**写**标记。
⇒ 体检永远不该有副作用(否则「查一下」就把标记刷新了,标记就永不报警)。
## 标记文件(用户 2026-10-02 要求:「在对应文件夹创建状态变量和时间作为标记」)
落在**配置作用域对应的文件夹**里,文件名 `env-stamp.json`:
- 全局作用域 ⇒ `<配置目录>/env-stamp.json`(本机 `E:/ProgramData/.workbuddy/env-stamp.json`)
- 工作区作用域 ⇒ `<工作区>/.workbuddy/env-stamp.json`
字段:`scope`(global/workspace)|`checked_at`(ISO 到秒)|`ok`(bool)|
`config_dir` / `skills_root`(**判据读数**,便于事后核)|`hooks`(逐条注册的存在性)|
`fixed`(本轮修过的问题清单)
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
_STAMP_NAME = "env-stamp.json"
_ROOTS_ENV = "roots.env" # 技能根的权威落点(install.py 生成)
_SNIFF = "agent-operating-rules" # 技能库的识别特征目录
def _norm(p) -> str:
s = str(p or "").strip()
return os.path.normpath(s).replace("\\", "/") if s else ""
def _load_roots_env() -> dict:
"""读技能包里的 `roots.env`(⛔ 各脚本的 `_sm_load_roots()` 注入 env 前的原始来源)。"""
out = {}
d = Path(__file__).resolve().parent
for _ in range(5):
f = d / _ROOTS_ENV
if f.is_file():
try:
for ln in f.read_text(encoding="utf-8").splitlines():
ln = ln.strip()
if ln and not ln.startswith("#") and "=" in ln:
k, v = ln.split("=", 1)
out[k.strip()] = v.strip()
return out
except Exception:
return {}
n = d.parent
if n == d:
break
d = n
return out
def config_dir() -> str:
"""**真配置目录** —— 顺序见硬规矩①的前三档。"""
v = os.environ.get("CODEBUDDY_CONFIG_DIR")
if v:
return _norm(v)
v = _load_roots_env().get("CODEBUDDY_CONFIG_DIR")
if v:
return _norm(v)
#⛔ 最后一档仍不给 `~`:Windows 上它通常不是真配置目录(实测会指到 C:\Users\...)
return _norm(Path.home() / ".workbuddy")
def skills_root() -> str:
"""**技能库根** —— 顺序见硬规矩①。⛔ 不再回落到 `~`(硬规矩①末档已删)。
🔴 2026-10-02 补:**每一档都要验"真的存在"**,⛔ 不信任 env 值本身。
实测踩过:`DSH_SKILLS_ROOT` 指向一个不存在的目录时,旧写法直接返回它
⇒ 下游 `os.path.isfile(核心块)` 失败 ⇒ **静默零输出**(同款静默失效)。
⇒ env 给错 ⇒ 继续往下找;全都找不到 ⇒ 返回 `""`(=报错,⛔ 不猜)。
"""
v = os.environ.get("DSH_SKILLS_ROOT")
if v and (Path(_norm(v)) / _SNIFF).is_dir():
return _norm(v)
d = Path(__file__).resolve().parent
for _ in range(6): # 沿 __file__ 上溯(换机可用,⛔ 不写死盘符)
cand = d.parent
if (cand / _SNIFF).is_dir():
return _norm(cand)
if d.parent == d:
break
d = d.parent
r = _load_roots_env().get("CODEBUDDY_CONFIG_DIR")
if r:
cand = Path(_norm(r)) / "skills"
if (cand / _SNIFF).is_dir():
return _norm(cand)
return "" # 🔴 空串=找不到(⛔ 不是"猜一个")
def _stamp_path(ws: str = "", scope: str = "global") -> str:
if scope == "workspace" and ws:
return _norm(Path(ws) / ".workbuddy" / _STAMP_NAME)
return _norm(Path(config_dir()) / _STAMP_NAME)
def registered_hooks() -> list:
"""读全局 `settings.json` 里已注册的钩子 ⇒ `[(事件, 命令, timeout, 脚本路径)]`。"""
sp = Path(config_dir()) / "settings.json"
out = []
try:
d = json.loads(sp.read_text(encoding="utf-8"))
except Exception:
return out
for ev, groups in (d.get("hooks") or {}).items():
for g in (groups or []):
for e in (g or {}).get("hooks", []):
c = (e or {}).get("command", "") or ""
# 取最后一个 .py 片段(命令形如`<python>"<脚本>" <参数>`)
parts = [p for p in c.split('"') if p.strip().endswith(".py")]
out.append((ev, c, (e or {}).get("timeout"),
_norm(parts[-1]) if parts else ""))
return out
def env_check(ws: str = "", scope: str = "global") -> dict:
"""🔍 **只读体检**:返回逐项读数,⛔ **绝不写标记**(硬规矩③)。"""
rep = {"scope": scope, "checked_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
"config_dir": config_dir(), "skills_root": skills_root(), "bad": []}
if not rep["skills_root"]:
rep["bad"].append("skills_root 定位失败(⛔ 已不回落~)")
elif not (Path(rep["skills_root"]) / _SNIFF).is_dir():
rep["bad"].append("skills_root 存在但缺 %s:%s" % (_SNIFF, rep["skills_root"]))
hooks = []
for ev, cmd, t, path in registered_hooks():
ok = bool(path) and os.path.isfile(path)
row = {"event": ev, "timeout": t, "path": path, "exists": ok}
hooks.append(row)
if not ok:
rep["bad"].append("钩子目标不存在:%s %s" % (ev, path or cmd[:60]))
rep["hooks"] = hooks
rep["ok"] = not rep["bad"]
return rep
def write_env_stamp(ws: str = "", scope: str = "global", fixed=None) -> str:
"""✍️ **写环境标记**(含状态变量+时间)。`scope` 决定落在哪个文件夹。"""
rep = env_check(ws, scope)
rep["fixed"] = list(fixed or [])
p = Path(_stamp_path(ws, scope))
try:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(rep, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8", newline="\n")
except Exception as e:
sys.stderr.write("env-stamp 写失败:%r\n" % (e,))
return ""
return _norm(p)
def read_env_stamp(ws: str = "", scope: str = "global") -> dict:
try:
return json.loads(Path(_stamp_path(ws, scope)).read_text(encoding="utf-8"))
except Exception:
return {}
def require_env(ws: str = "", what: str = "") -> str:
"""🔴 **调用方统一入口**:定位不到就**报错并返回空串**(硬规矩②)。
⛔ 绝不在这里 `sys.exit` —— 钩子进程退出码由调用方决定(各钩子预算不同)。
"""
root = skills_root()
if root:
return root
sys.stderr.write(
"[env] 技能库根定位失败%s:DSH_SKILLS_ROOT / CODEBUDDY_CONFIG_DIR 均未给,"
"沿 __file__ 上溯也没找到含 %s 的目录。\n"
" ⛔ 已⛔静默跳过(不再回落 ~,因Windows 上 ~ 不是真配置目录)\n"
" ✅ 修法:设环境变量 DSH_SKILLS_ROOT,或跑 "
"`python scripts/hooks/_env.py --stamp` 重建标记并体检\n"
% (("(%s)" % what) if what else "", _SNIFF))
return ""
if __name__ == "__main__":
import argparse
_ap = argparse.ArgumentParser(description="环境自检/环境标记")
_ap.add_argument("--ws", default="", help="工作区绝对路径")
_ap.add_argument("--scope", default="global", choices=["global", "workspace"])
_ap.add_argument("--stamp", action="store_true", help="写环境标记")
_ap.add_argument("--fixed", default="", help="--stamp 时记进 fixed 的说明")
_a = _ap.parse_args()
if _a.stamp:
_p = write_env_stamp(_a.ws, _a.scope,
[_a.fixed] if _a.fixed else None)
print("已写标记:%s" % (_p or "⛔ 写失败"))
raise SystemExit(0 if _p else 1)
_r = env_check(_a.ws, _a.scope)
print("配置目录 = %s" % _r["config_dir"])
print("技能库根 = %s" % (_r["skills_root"] or "⛔ 定位失败"))
print("已注册钩子 = %d 条|不存在的 = %d"
% (len(_r["hooks"]), sum(1 for h in _r["hooks"] if not h["exists"])))
for _b in _r["bad"]:
print(" ⛔ %s" % _b)
print("结论 = %s" % ("✅ 通过" if _r["ok"] else "❌ 不通过"))
raise SystemExit(0 if _r["ok"] else 1)