技能包:配置与代码分家 —— 每包自建 env/ 放本机专属配置,scripts/ 不再写死本机路径
用户 2026-10-11 原话: · 19:22「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」 · 19:28「不用挪出 用env文件夹区分就行,重点是 脚本是脚本 配置是配置」 · 19:29「每个技能 需要环境配置的 都有自己的 env文件夹」 · 19:30「修改完成后要仔细检查 避免调整后 各技能脚本运行失败」 改动: 1、session-mechanism 新增 env/(roots.env=本机真身不入库 + roots.env.example=样例入库), install.py 的 ROOTS_ENV 改指 env/roots.env;全包脚本/hook 的配置定位统一为 「向上 4 层 × env/roots.env 优先 → 包根 roots.env 兼容 → 技能库那份兜底」。 2、去硬编码:collabctl / keepalive / supervise-launch / board-launch 里的本机路径与本机工作区 清单全清,改为 DSH_AIROOT(兜底 dirname(DSH_WS_ROOT))+ 扫 <AIROOT>/*/.workbuddy/collab/ 自动发现;保活任务名按 collabd-keepalive-<工作区名> 现推;启动器默认值改 ~/.workbuddy 形态。 3、browser-harness:bh_chrome_keepalive.py 默认值全部改 env 驱动,新增 env/bh.env.example。 4、.gitignore:env/ 下只放行 *.example(三行三种层级)+ 兜住旧位置的包根 roots.env。 5、改包纪律.md 新增「四、包结构:脚本是脚本,配置是配置」,立为全仓通用规矩。 6、文档同步:SKILL.md / deploy.md(新增 §3b 配置层级、§6 换机器必查 6 件事)/ rules.md / pitfalls.md / supervise-persistence.md。 验收(本机):py_compile 34 个 py 全过;selftest PASS 109 / FAIL 0; 逐文件实证 8 个 loader 命中 env/roots.env;preflight-lock.sh 同目录实测解出两个根; install.py --manifest 77 份、语法失败 0。 异机验证(包拷成「新克隆」(只有 *.example)+ 另造假工作区 ws-alpha/ws-beta): install.py --apply 自生成该机 env/roots.env;collabctl/keepalive 自动发现的确实是 ws-alpha/ws-beta、任务名现推(写死清单确已消除);部署副本本地无 roots.env 时靠 CODEBUDDY_CONFIG_DIR 兜底读到技能库那份;整套 selftest 有宿主库时 PASS 108 / FAIL 1 (唯一 FAIL=该区没有常驻心跳,环境所致;本机常驻在跑时同一套为 PASS 109 / FAIL 0)。
This commit is contained in:
1 parent
90cc4315d5
commit
d81f75f173
37 files changed
+763
-291
No files matched your search
+15
-2
@@ -43,10 +43,23 @@ Thumbs.db
|
|||||||
# 下面两份是 **install.py 在本机生成**的 ⇒ ⛔ 撤出仓库、改为本机各自生成。
|
# 下面两份是 **install.py 在本机生成**的 ⇒ ⛔ 撤出仓库、改为本机各自生成。
|
||||||
# 为什么必须撤(实测):别的电脑取仓时「同名覆盖」会把它们换成**我们这台机器**的路径
|
# 为什么必须撤(实测):别的电脑取仓时「同名覆盖」会把它们换成**我们这台机器**的路径
|
||||||
# ⇒ 那台机器的机制脚本会去找不存在的目录(`roots.env` 里就是本机绝对路径)。
|
# ⇒ 那台机器的机制脚本会去找不存在的目录(`roots.env` 里就是本机绝对路径)。
|
||||||
# ✅ 样例仍入库:`session-mechanism/roots.env.example`(占位符,无本机路径)。
|
|
||||||
session-mechanism/roots.env
|
|
||||||
session-mechanism/references/manifest.md
|
session-mechanism/references/manifest.md
|
||||||
|
|
||||||
|
# 🔴🔴 2026-10-11 加(用户令:「脚本是脚本 配置是配置」⇒「**每个技能 需要环境配置的 都有自己的 env文件夹**」):
|
||||||
|
# **每台机器各不同**的配置一律放**该技能包自己的 `env/`**;`scripts/` 只留通用代码。
|
||||||
|
# 本仓收录的每个技能 —— 其 `env/` 下**只入库 `*.example` 样例**,真配置一律本机生成、不入库
|
||||||
|
# (理由同上面那条:入库 ⇒ 别的电脑取仓时被本机路径覆盖 ⇒ 机制去找不存在的目录)。
|
||||||
|
# ⚠️ 三行三种层级都写,避免"某个深度漏挡"(`**/env/*` 覆盖任意层,另两行是它的保守冗余)。
|
||||||
|
env/*
|
||||||
|
!env/*.example
|
||||||
|
*/env/*
|
||||||
|
!*/env/*.example
|
||||||
|
**/env/*
|
||||||
|
!**/env/*.example
|
||||||
|
# 兼容旧位置(2026-10-11 之前的布局):包根那份 `roots.env` 也是本机真身 ⇒ 同样不许入库。
|
||||||
|
# ⚠️ `roots.env` 不带斜杠 ⇒ 按**文件名**匹配任意层级,正好把「包根旧位置」一并兜住。
|
||||||
|
roots.env
|
||||||
|
|
||||||
# 📌 刻意**入库**的(⛔ 别误删):
|
# 📌 刻意**入库**的(⛔ 别误删):
|
||||||
# assets/*.png —— 文档配图,是内容不是产物
|
# assets/*.png —— 文档配图,是内容不是产物
|
||||||
|
|
||||||
|
|||||||
+30
-21
@@ -23,7 +23,7 @@ Direct browser control via CDP. For task-specific edits, use `agent-workspace/ag
|
|||||||
症状是 `netstat` 查不到 9223、`curl` 报 `WinError 10061`(2026-10-09 实测踩到一次)。
|
症状是 `netstat` 查不到 9223、`curl` 报 `WinError 10061`(2026-10-09 实测踩到一次)。
|
||||||
做法:**先用常驻守端口把浏览器开着,之后每次操作只连不起**。
|
做法:**先用常驻守端口把浏览器开着,之后每次操作只连不起**。
|
||||||
✅ **起法只有一条:用本会话的后台任务跑那条命令**(启动器在哪、怎么起、起不来怎么查,都在下面「调 AI 工具」§1)—— 那里给的是能整段粘的命令,不是叮嘱。
|
✅ **起法只有一条:用本会话的后台任务跑那条命令**(启动器在哪、怎么起、起不来怎么查,都在下面「调 AI 工具」§1)—— 那里给的是能整段粘的命令,不是叮嘱。
|
||||||
- 🔴🔴 **一类任务只开一个标签页并复用,⛔ 禁止反复开**(2026-10-09 用户令)。
|
- 🔴🔴 **标签页按「页面类型」分:一类页面一个标签、同类只开一个、异类各开一个**(2026-10-09 用户令「一类任务只开一个标签页并复用」;2026-10-11 用户令「不同的网站单独开标签 不要相互抢标签页」+「不同类型的网页 开不同的标签 不要相互覆盖」)。
|
||||||
反复开页会把共享实例堆到**整机卡死**(端口不应答、进程不死,保活脚本也救不回 —— 见下面那条的实测读数)。
|
反复开页会把共享实例堆到**整机卡死**(端口不应答、进程不死,保活脚本也救不回 —— 见下面那条的实测读数)。
|
||||||
细则**只写在下面那颗同名的 🔴🔴 子弹里**,⛔ 本处不复述 —— 但它**优先于任何"顺手再开一个"的念头**。
|
细则**只写在下面那颗同名的 🔴🔴 子弹里**,⛔ 本处不复述 —— 但它**优先于任何"顺手再开一个"的念头**。
|
||||||
- 🔴 **先探测、能复用就复用**(2026-10-08 用户当场点破:「为什么要重新开浏览器,先检查之前是否开过浏览能否复用」)。
|
- 🔴 **先探测、能复用就复用**(2026-10-08 用户当场点破:「为什么要重新开浏览器,先检查之前是否开过浏览能否复用」)。
|
||||||
@@ -35,24 +35,28 @@ Direct browser control via CDP. For task-specific edits, use `agent-workspace/ag
|
|||||||
`curl --noproxy '*' -s http://127.0.0.1:9223/json/version` ⇒ 回 JSON 说明端口后面确实是 CDP。
|
`curl --noproxy '*' -s http://127.0.0.1:9223/json/version` ⇒ 回 JSON 说明端口后面确实是 CDP。
|
||||||
确认在跑之后按下面第 2 条的 env 写法连上去,先 `list_tabs()` 摸清现有页,
|
确认在跑之后按下面第 2 条的 env 写法连上去,先 `list_tabs()` 摸清现有页,
|
||||||
自己需要的页用 `new_tab()` 开 —— ⛔ 不碰别人的页,也⛔ 不为自己方便关掉现有页。
|
自己需要的页用 `new_tab()` 开 —— ⛔ 不碰别人的页,也⛔ 不为自己方便关掉现有页。
|
||||||
- 🔴🔴 **一类任务只开一个标签页并复用,⛔ 禁止反复开**(2026-10-08 用户令「后续调试网页 一个页面开一个标签就行了不要反复开」;2026-10-09 加严到**按任务算**:「一类任务只允许开一个标签页并复用,禁止反复开标签页」)。
|
- 🔴🔴 **标签页按「页面类型」分:一类页面一个标签,各不覆盖**(2026-10-08 用户令「一个页面开一个标签就行了不要反复开」;2026-10-09 加严「一类任务只开一个标签页并复用」;2026-10-11 用户令,逐字:「**不同的网站单独开标签 不要相互抢标签页**」+「**不同类型的网页 开不同的标签 不要相互覆盖**」)。
|
||||||
这条接在上一颗子弹后面:**先复用浏览器**是做对了,但**别在同一个浏览器里反复开页**。
|
🔴 这是**同一条规则的两面,一起读**:**同一类页面只开一个**(别堆)+**不同类页面各开一个**(别合并)。
|
||||||
⚠️ **这不是"标签堆着难看"的问题 —— 它能把整机拖到卡死**(2026-10-09 实测):反复开页把共享实例堆爆后,
|
⛔ 最容易犯的错是**只记住前一半** —— 把「复用同一个标签」当成原则,于是换页面时 `goto_url` 顶掉正在用的页;
|
||||||
Chrome 会进入**端口不应答、进程却还活着**的僵死态,而保活脚本只看端口 ⇒ 查不出、也救不回。
|
用户说的「相互覆盖 / 抢标签页」就是这个。
|
||||||
当天现场读数:保活日志 `06:56 端口 9223 不应答(pid alive=True)`,直到 `07:06` 进程才退出;用户手动关掉全部会话。
|
⚠️ **堆页的代价**(2026-10-09 实测):反复开页把共享实例堆爆后,Chrome 会进入**端口不应答、进程却还活着**的僵死态,
|
||||||
|
而保活脚本只看端口 ⇒ 查不出、也救不回。当天读数:`06:56 端口 9223 不应答(pid alive=True)`,
|
||||||
|
直到 `07:06` 进程才退出;用户手动关掉全部会话。
|
||||||
典型错法:调试一个原型,量一次 `new_tab(原型)` 一次、量完忘了关 ⇒ 一次会话堆出一串同一个页面的标签。
|
典型错法:调试一个原型,量一次 `new_tab(原型)` 一次、量完忘了关 ⇒ 一次会话堆出一串同一个页面的标签。
|
||||||
|
|
||||||
**什么算「一类任务」**:同一件事的一整串动作 —— 同一个原型 / 同一轮站点调研 / 同一批数据抓取。
|
**分页口径**:
|
||||||
这类任务**从头到尾只用一个标签**:
|
- 🔴 **一个用途一个标签**:列表页 / 登录页 / 正文页 / 解析页 / 后台面板 / 原型页 …… **每类页面各占自己的标签**。
|
||||||
- **同一个页面**(同一 URL / 同一份原型)⇒ 就那一个标签。
|
- 🔴 **动手前先 `list_tabs()` 认领本用途那一个**:已有 ⇒ 用它;没有 ⇒ `new_tab()`。
|
||||||
- **换路由** ⇒ 改 hash(`js("location.hash='#/x'")`)或 `goto_url()`,⛔ 不要 `new_tab()`。
|
- 🔴 **绝不对一个不属于本用途的页 `goto_url()`** —— 那会把上面正开着的东西顶掉,就是「覆盖」。
|
||||||
- **换到同任务的另一个页面** ⇒ 也优先在**同一个标签**里 `goto_url()`,⛔ 不要为新页面开新标签。
|
- **同一类页面内部换路由** ⇒ 改 hash(`js("location.hash='#/x'")`)或 `goto_url()`,就在**这一个**标签里做。
|
||||||
|
- **同一类页面反复开**(同一 URL 开两次以上)⇒ 那才是「堆页」,⛔ 禁止。
|
||||||
- **换视口** ⇒ `cdp("Emulation.setDeviceMetricsOverride", width=…, height=…)` 在**同一个标签**里改,
|
- **换视口** ⇒ `cdp("Emulation.setDeviceMetricsOverride", width=…, height=…)` 在**同一个标签**里改,
|
||||||
⛔ 不要为了量另一个宽度再开一个标签;量完 `cdp("Emulation.clearDeviceMetricsOverride")` 还原。
|
⛔ 不要为了量另一个宽度再开一个标签;量完 `cdp("Emulation.clearDeviceMetricsOverride")` 还原。
|
||||||
- **只有必须并排看两个页面时**(参照物 vs 我方)才开第二个,**用完立刻 `close_tab()`**。
|
|
||||||
- **完全不需要页面时**(只取一个数、截一张图)⇒ 复用**当前那个**标签,别再开。
|
- **完全不需要页面时**(只取一个数、截一张图)⇒ 复用**当前那个**标签,别再开。
|
||||||
- 🔴 **收工前必须 `list_tabs()` 复核一遍,把自己开的一个不留**。共享实例是别人的工作现场,
|
- 🔴 **收工按用途清点**:自己为本任务开的页交代去留,⛔ 别留尾巴;反向也一样 —— ⛔ 别顺手关掉不是自己开的页。
|
||||||
⛔ 别留尾巴;反向也一样 —— ⛔ 别顺手关掉不是自己开的页。
|
(依据 2026-10-11 同一目标两轮实测:① 主会话开「小侣坊登录页」等用户登录,任务会话在**同一标签** `goto_url`
|
||||||
|
去 B 站列表 ⇒ 登录页当场消失、用户没法登录;② 任务会话又用**同一标签**从 B 站列表转去小侣坊解析页
|
||||||
|
⇒ B 站列表那页没了。两次都只因「复用同一个标签」做过头,⛔ 不是标签开多了。)
|
||||||
- ✅ **动手前先 `list_tabs()`** 看清有哪些标签页,别盲点。(这条同时是上面那条的**收工复核**用的)
|
- ✅ **动手前先 `list_tabs()`** 看清有哪些标签页,别盲点。(这条同时是上面那条的**收工复核**用的)
|
||||||
- ⛔ **别自建取数通道**:需要 DOM 数值就用 `js(...)`、需要截图就用 harness 的截图helper,
|
- ⛔ **别自建取数通道**:需要 DOM 数值就用 `js(...)`、需要截图就用 harness 的截图helper,
|
||||||
⛔ 不要为了「量一个高度」另写一个脚本去起 Chrome。
|
⛔ 不要为了「量一个高度」另写一个脚本去起 Chrome。
|
||||||
@@ -78,20 +82,25 @@ curl --noproxy '*' -s http://127.0.0.1:9223/json/version
|
|||||||
|
|
||||||
**第 1 步 · 没在跑 ⇒ 用本会话的后台任务起**
|
**第 1 步 · 没在跑 ⇒ 用本会话的后台任务起**
|
||||||
|
|
||||||
两份绝对路径固定,启动器就用**技能包内那一份**(参数化,换工作区不用改代码):
|
启动器就用**技能包内那一份**(参数化,换工作区不用改代码)。🔴 **路径按环境变量拼,⛔ 不写死本机路径**
|
||||||
|
(2026-10-11 用户报「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」):
|
||||||
|
|
||||||
- 启动器:`E:\ProgramData\.workbuddy\skills\browser-harness\agent-workspace\bh_chrome_keepalive.py`
|
- 技能包根 = `$CODEBUDDY_CONFIG_DIR/skills/browser-harness`(`CODEBUDDY_CONFIG_DIR` 由宿主给;本机=`E:/ProgramData/.workbuddy`)
|
||||||
- 解释器:`E:\ProgramData\.workbuddy\binaries\python\versions\3.13.12\pythonw.exe`
|
- 启动器 = `<技能包根>/agent-workspace/bh_chrome_keepalive.py`
|
||||||
|
- 解释器 = 与**当前会话同一个 Python** 的兄弟 `pythonw.exe`(本机例 `<配置目录>/binaries/python/versions/<版本>/pythonw.exe`)
|
||||||
|
- 机器专属值(profile/日志/端口)⇒ 本包 **`env/bh.env`**(不入库;样例 `env/bh.env.example`)
|
||||||
|
|
||||||
命令(整段抄,然后**交给你这个会话的后台任务去跑**):
|
命令(整段抄,然后**交给你这个会话的后台任务去跑**):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/pythonw.exe \
|
SK="$CODEBUDDY_CONFIG_DIR/skills/browser-harness"
|
||||||
"E:/ProgramData/.workbuddy/skills/browser-harness/agent-workspace/bh_chrome_keepalive.py" \
|
PYW="$(dirname "$(readlink -f "$(command -v python)")")/pythonw.exe" # 与当前会话同版本
|
||||||
--log "E:/ProgramData/.workbuddy/tmp/_bh_chrome.log" \
|
"$PYW" "$SK/agent-workspace/bh_chrome_keepalive.py" \
|
||||||
>> "E:/ProgramData/.workbuddy/tmp/_bh_bg.out" 2>&1
|
>> "$CODEBUDDY_CONFIG_DIR/tmp/_bh_bg.out" 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(`--log` 不必给:默认取 `env/bh.env` 的 `BH_LOG`,再兜底到 `<配置目录>/tmp/_bh_chrome.log`。)
|
||||||
|
|
||||||
🔴 **"交给你这个会话的后台任务跑"是这条起法的本体**:后台任务只活到本会话结束
|
🔴 **"交给你这个会话的后台任务跑"是这条起法的本体**:后台任务只活到本会话结束
|
||||||
⇒ 会话在、浏览器在;会话收工、浏览器随之下线。这正是要的语义。
|
⇒ 会话在、浏览器在;会话收工、浏览器随之下线。这正是要的语义。
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,13 @@
|
|||||||
- **只用独立实例**:⛔ 不附着用户日常 Chrome(那是 9222,会弹「允许远程调试?」动到用户浏览器)。
|
- **只用独立实例**:⛔ 不附着用户日常 Chrome(那是 9222,会弹「允许远程调试?」动到用户浏览器)。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
pythonw bh_chrome_keepalive.py --log "E:/ProgramData/.workbuddy/tmp/_bh_chrome.log"
|
pythonw bh_chrome_keepalive.py
|
||||||
(整条命令见 SKILL.md「调 AI 工具」§1;本脚本要放在**会话的后台任务**里跑)
|
(整条命令见 SKILL.md「调 AI 工具」§1;本脚本要放在**会话的后台任务**里跑)
|
||||||
可选:`--port` `--chrome` `--profile` `--proxy` `--log`(都有默认值,见下)
|
可选:`--port` `--chrome` `--profile` `--proxy` `--log`
|
||||||
|
|
||||||
|
🔴 **默认值从哪来(2026-10-11 起)**:先读本包 `env/bh.env`(每台机器一份,不入库),
|
||||||
|
其次宿主 env 的 `BH_*`;再兜底到「WorkBuddy 配置目录(`CODEBUDDY_CONFIG_DIR`,否则 `~/.workbuddy`)
|
||||||
|
+ tmp/…」。⛔ 代码里不写死本机路径 —— 要改就往 `env/bh.env` 里写(样例 `env/bh.env.example`)。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -52,9 +56,39 @@ for _k in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
|
|||||||
os.environ.pop(_k, None)
|
os.environ.pop(_k, None)
|
||||||
os.environ.setdefault("NO_PROXY", "127.0.0.1,localhost")
|
os.environ.setdefault("NO_PROXY", "127.0.0.1,localhost")
|
||||||
|
|
||||||
DEFAULT_CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
# [browser-harness] 环境专属配置外置(🔴 2026-10-11 用户令「脚本是脚本 配置是配置」⇒「每个技能
|
||||||
DEFAULT_PROFILE = r"E:/ProgramData/.workbuddy/tmp/bu-chatgpt-profile"
|
# 需要环境配置的 都有自己的 env文件夹」):**每台机器各不同**的值写本包 `env/bh.env`
|
||||||
DEFAULT_PORT = 9223
|
# (本机生成、不入库;样例 `env/bh.env.example` 入库)。⛔ 代码里不写死本机路径。
|
||||||
|
def _bh_load_env():
|
||||||
|
import os as _os
|
||||||
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
for _up in range(4):
|
||||||
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
|
for _rel in (("env", "bh.env"), ("bh.env",)):
|
||||||
|
_p = _os.path.join(_base, *_rel)
|
||||||
|
if _os.path.isfile(_p):
|
||||||
|
try:
|
||||||
|
with io.open(_p, encoding="utf-8") as _f:
|
||||||
|
for _ln in _f:
|
||||||
|
_ln = _ln.strip()
|
||||||
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
|
_k, _v = _ln.split("=", 1)
|
||||||
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
_bh_load_env()
|
||||||
|
|
||||||
|
# WorkBuddy 配置目录:宿主 env 优先,其次 `~/.workbuddy`(⛔ 不写死本机盘符/盘)
|
||||||
|
_CB_CONF = os.environ.get("CODEBUDDY_CONFIG_DIR") or os.path.expanduser("~/.workbuddy")
|
||||||
|
|
||||||
|
DEFAULT_CHROME = (os.environ.get("BH_CHROME")
|
||||||
|
or r"C:\Program Files\Google\Chrome\Application\chrome.exe")
|
||||||
|
DEFAULT_PROFILE = (os.environ.get("BH_PROFILE")
|
||||||
|
or os.path.join(_CB_CONF, "tmp", "bu-chatgpt-profile"))
|
||||||
|
DEFAULT_LOG = (os.environ.get("BH_LOG")
|
||||||
|
or os.path.join(_CB_CONF, "tmp", "_bh_chrome.log"))
|
||||||
|
DEFAULT_PORT = int(os.environ.get("BH_PORT") or 9223)
|
||||||
|
|
||||||
|
|
||||||
def _log(path, msg):
|
def _log(path, msg):
|
||||||
@@ -95,9 +129,9 @@ def _launch(chrome: str, profile: str, port: int, proxy: str = ""):
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser(add_help=True)
|
ap = argparse.ArgumentParser(add_help=True)
|
||||||
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
|
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||||
ap.add_argument("--chrome", default=os.environ.get("BH_CHROME") or DEFAULT_CHROME)
|
ap.add_argument("--chrome", default=DEFAULT_CHROME)
|
||||||
ap.add_argument("--profile", default=os.environ.get("BH_PROFILE") or DEFAULT_PROFILE)
|
ap.add_argument("--profile", default=DEFAULT_PROFILE)
|
||||||
ap.add_argument("--log", default=os.environ.get("BH_LOG") or "")
|
ap.add_argument("--log", default=DEFAULT_LOG)
|
||||||
ap.add_argument("--proxy", default=os.environ.get("BH_PROXY") or "",
|
ap.add_argument("--proxy", default=os.environ.get("BH_PROXY") or "",
|
||||||
help='目标站点要走代理时才给,例 --proxy socks5://127.0.0.1:10800')
|
help='目标站点要走代理时才给,例 --proxy socks5://127.0.0.1:10800')
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
|||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# browser-harness · 环境专属配置(样例 · ⛔ 请勿当成真文件用)
|
||||||
|
#
|
||||||
|
# 🔴 位置约定(2026-10-11 用户令:「脚本是脚本 配置是配置」⇒「每个技能 需要环境配置的 都有自己的 env文件夹」):
|
||||||
|
# 本技能**每台机器各不同**的值一律放 **`env/`**;脚本从 `env/` 读,代码里不写死。
|
||||||
|
# 真文件=`env/bh.env`(本机生成,⛔ 不入库);本文件=入库样例。仓根 `.gitignore` 只放行 `env/**/*.example`。
|
||||||
|
#
|
||||||
|
# 读取机制:`agent-workspace/bh_chrome_keepalive.py` 头部 `_bh_load_env()` 以 setdefault 注入
|
||||||
|
# ⇒ **宿主 env 优先,本文件兜底**。所有键都可省 —— 省了就按下面的兜底走。
|
||||||
|
|
||||||
|
# Chrome 独立实例的 profile 目录(固定复用 ⇒ 登录态保留)
|
||||||
|
# 兜底:<WorkBuddy 配置目录>/tmp/bu-chatgpt-profile
|
||||||
|
BH_PROFILE=<WorkBuddy 配置目录>/tmp/bu-chatgpt-profile
|
||||||
|
|
||||||
|
# 保活日志路径(本脚本要放在会话后台任务里跑 ⇒ 不落盘就看不到结果)
|
||||||
|
# 兜底:<WorkBuddy 配置目录>/tmp/_bh_chrome.log
|
||||||
|
BH_LOG=<WorkBuddy 配置目录>/tmp/_bh_chrome.log
|
||||||
|
|
||||||
|
# Chrome 可执行体;不填则用 Windows 默认安装位置:
|
||||||
|
# C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
BH_CHROME=<chrome 可执行体的绝对路径>
|
||||||
|
|
||||||
|
# 调试端口(默认 9223;用户日常 Chrome 是 9222,两者别撞)
|
||||||
|
BH_PORT=9223
|
||||||
|
|
||||||
|
# 可选:目标站点要走代理时才填,例 socks5://127.0.0.1:10800
|
||||||
|
BH_PROXY=
|
||||||
@@ -221,8 +221,9 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
|
|||||||
⚠️ 该判据**扫文本**,故必须先剥掉 `SCHED_TASKS` 那种"故意列旧名去禁用它"的**否定语境块**
|
⚠️ 该判据**扫文本**,故必须先剥掉 `SCHED_TASKS` 那种"故意列旧名去禁用它"的**否定语境块**
|
||||||
(同一坑本项目踩过三次)。
|
(同一坑本项目踩过三次)。
|
||||||
|
|
||||||
- ⚠️ **变量名坑**:`roots.env` 给的是 **`COLLABD_PROD_CONFIG`**,而 `board.py`/`collabd.py` 读的是 **`COLLABD_CONFIG`** ⇒ 名字对不上 ⇒ `roots.env` **兜不住**。
|
- ⚠️ **变量名坑**:`env/roots.env` 给的是 **`COLLABD_PROD_CONFIG`**,而 `board.py`/`collabd.py` 读的是 **`COLLABD_CONFIG`** ⇒ 名字对不上 ⇒ `env/roots.env` **兜不住**。
|
||||||
- 🔴 **启动器里写死最稳**(⛔ 别指望 `roots.env` 兜住各区 —— 它只在技能目录,而各区副本在 `<ws>/.workbuddy/collab/`,`_sm_load_roots()` 向上 4 层**找不到**)。
|
- 🔴 **启动器把该设的 env 在进程内设好**(2026-10-11 起由 `env/roots.env` 的 `DSH_WS_ROOT` 供主工作区根;⛔ 兜底值不再写死本机工作区 —— 解析不到就**优雅退出**并留痕,⛔ 不猜路径)。
|
||||||
|
⚠️ 各区副本(`<ws>/.workbuddy/collab/`)里的 `collabd.py` **没有** `env/roots.env` 可读(`_sm_load_roots()` 向上 4 层找不到)⇒ 那一路靠**启动器设 env** + 该区 `collabd.config.json` 兜住。
|
||||||
- 🔴 **重构"起法"时,旧起法里的 env 必须逐条搬过去**(P0-73 就是改成两层形态时**丢了 `CODEBUDDY_CONFIG_DIR` 这一句**)。
|
- 🔴 **重构"起法"时,旧起法里的 env 必须逐条搬过去**(P0-73 就是改成两层形态时**丢了 `CODEBUDDY_CONFIG_DIR` 这一句**)。
|
||||||
- 🔴 **各区 config 的 `host_db` 写死绝对路径最稳**(vibe 一直这么写 ⇒ 只有 ai1net 炸)。
|
- 🔴 **各区 config 的 `host_db` 写死绝对路径最稳**(vibe 一直这么写 ⇒ 只有 ai1net 炸)。
|
||||||
|
|
||||||
@@ -402,7 +403,8 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
|
|||||||
## §0 怎么用(先读这一节)
|
## §0 怎么用(先读这一节)
|
||||||
|
|
||||||
**① 我要装 / 要换机器** ⇒ 跑 `python install.py --dry-run` 看 diff,再 `python install.py --apply`,最后 `python install.py --verify`。
|
**① 我要装 / 要换机器** ⇒ 跑 `python install.py --dry-run` 看 diff,再 `python install.py --apply`,最后 `python install.py --verify`。
|
||||||
(`--apply` 会:写 `roots.env` → 按**声明表**接线全局钩子 → 初始化工作区 → 记 `install.log`。⛔ 不硬编码 python 路径与盘符。)
|
(`--apply` 会:写 `env/roots.env` → 按**声明表**接线全局钩子 → 初始化工作区 → 记 `install.log`。⛔ 不硬编码 python 路径与盘符。)
|
||||||
|
🔴 **包内目录怎么分(2026-10-11 用户令:「脚本是脚本 配置是配置」)**:**每台机器各不同**的配置一律放本包 **`env/`**(`env/roots.env` 本机生成、不入库;`env/roots.env.example` 是入库样例);`scripts/` 只留通用代码。仓根 `.gitignore` 只放行 `env/**/*.example`。
|
||||||
|
|
||||||
**⓪ 我要动手(建判据/写验收/报"已完成"/任何带"现在·已·还差"的结论)** ⇒ **先读 `references/00-动手前必过.md`**(🔴 **动手层**,2026-10-04 建,只有三条动作:① 查状态**问程序自己**,⛔ 不许 glob/手拼路径 ② 引用任何读数**先看它什么时候写的** ③ 写判据**先让基线全绿**再谈抓得住 —— 每条都对应当天真实翻车,且都已落判据不许退化)。
|
**⓪ 我要动手(建判据/写验收/报"已完成"/任何带"现在·已·还差"的结论)** ⇒ **先读 `references/00-动手前必过.md`**(🔴 **动手层**,2026-10-04 建,只有三条动作:① 查状态**问程序自己**,⛔ 不许 glob/手拼路径 ② 引用任何读数**先看它什么时候写的** ③ 写判据**先让基线全绿**再谈抓得住 —— 每条都对应当天真实翻车,且都已落判据不许退化)。
|
||||||
|
|
||||||
@@ -881,8 +883,10 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
|
|||||||
**为什么(2026-10-05 实测)**:直起 ⇒ **崩溃重启循环、20099 从没绑上**。现场输出:
|
**为什么(2026-10-05 实测)**:直起 ⇒ **崩溃重启循环、20099 从没绑上**。现场输出:
|
||||||
`⚠️ collabd:未找到部署配置(COLLABD_CONFIG 未设)⇒ 已拒跑` —— `board.py` 顶层用
|
`⚠️ collabd:未找到部署配置(COLLABD_CONFIG 未设)⇒ 已拒跑` —— `board.py` 顶层用
|
||||||
**`COLLABD_CONFIG`** 定位配置(它要 `import collabd.py`),而**计划任务的动作里没有 env 字段**
|
**`COLLABD_CONFIG`** 定位配置(它要 `import collabd.py`),而**计划任务的动作里没有 env 字段**
|
||||||
⇒ 该变量只能由**启动器在进程内设**。⚠️ **名字别混**:`roots.env` 给的是 `COLLABD_PROD_CONFIG`,
|
⇒ 该变量只能由**启动器在进程内设**。⚠️ **名字别混**:`env/roots.env` 给的是 `COLLABD_PROD_CONFIG`,
|
||||||
而 `board.py` 读的是 `COLLABD_CONFIG` ⇒ 名字对不上,`roots.env` **兜不住它**。
|
而 `board.py` 读的是 `COLLABD_CONFIG` ⇒ 名字对不上,`env/roots.env` **兜不住它**。
|
||||||
|
🔴 2026-10-11:**主工作区根也从 `env/roots.env` 的 `DSH_WS_ROOT` 读**(⛔ 兜底不再写死 ai1net-dsh-server);
|
||||||
|
解析不到 ⇒ 启动器**写 `logs/board-launch.err.log` 留痕 + 退出码 2**,⛔ 不猜路径、不建错任务。
|
||||||
· ⚠️ 启动器的 `WorkingDirectory` =**技能 `scripts/` 目录**(启动器自己定位 `board.py`);
|
· ⚠️ 启动器的 `WorkingDirectory` =**技能 `scripts/` 目录**(启动器自己定位 `board.py`);
|
||||||
看板**配置恒指主工作区**那份(看板是全局共用一份)——这与 `--supervise` 必须指**工作区根**是两回事。
|
看板**配置恒指主工作区**那份(看板是全局共用一份)——这与 `--supervise` 必须指**工作区根**是两回事。
|
||||||
· 🔴 **`ExecutionTimeLimit` 必须 `0`(无时限)** —— 看板是**长驻服务**;
|
· 🔴 **`ExecutionTimeLimit` 必须 `0`(无时限)** —— 看板是**长驻服务**;
|
||||||
@@ -961,7 +965,9 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
|
|||||||
## §3 目录与依赖
|
## §3 目录与依赖
|
||||||
|
|
||||||
```
|
```
|
||||||
SKILL.md install.py roots.env(装后生成) install.log(装后生成)
|
SKILL.md install.py install.log(装后生成)
|
||||||
|
env/ roots.env(装后生成·不入库) roots.env.example(入库样例)
|
||||||
|
🔴 「每台机器各不同的配置」只放这里(脚本是脚本 配置是配置)
|
||||||
references/ architecture collab collab-detail rules deploy pitfalls taskgraph forensics manifest
|
references/ architecture collab collab-detail rules deploy pitfalls taskgraph forensics manifest
|
||||||
scripts/hooks/ 6 份宿主钩子(10 条接线)
|
scripts/hooks/ 6 份宿主钩子(10 条接线)
|
||||||
scripts/lock/ handoff-guard.sh preflight-lock.sh handoff-status.py op-lock.sh
|
scripts/lock/ handoff-guard.sh preflight-lock.sh handoff-status.py op-lock.sh
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Register-ScheduledTask -TaskName 'dsh-board-<端口>' -Action $a -Settings $s -P
|
|||||||
1. **`exec(compile(...))`** —— 从 `C:\Windows\System32`(任务默认 cwd)起时 **rc=1 且零输出**。
|
1. **`exec(compile(...))`** —— 从 `C:\Windows\System32`(任务默认 cwd)起时 **rc=1 且零输出**。
|
||||||
✅ 用 `runpy.run_path(BOARD, run_name="__main__")`:它会正确设好
|
✅ 用 `runpy.run_path(BOARD, run_name="__main__")`:它会正确设好
|
||||||
`__file__`/`__name__`/`sys.path[0]`,`board.py` 顶部的 `_sm_load_roots()`
|
`__file__`/`__name__`/`sys.path[0]`,`board.py` 顶部的 `_sm_load_roots()`
|
||||||
(靠 `__file__` 往上找 `roots.env`)才找得到。
|
(靠 `__file__` 往上找 `env/roots.env`)才找得到。
|
||||||
2. **`pythonw.exe` 下 `sys.stdout/stderr` 可能是 `None`** ⇒ 任何 `print` 抛 `AttributeError` 秒退。
|
2. **`pythonw.exe` 下 `sys.stdout/stderr` 可能是 `None`** ⇒ 任何 `print` 抛 `AttributeError` 秒退。
|
||||||
✅ 起 `board.py` 前先给两个流兜底(本文件已做)。
|
✅ 起 `board.py` 前先给两个流兜底(本文件已做)。
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ PORT = "8788"
|
|||||||
LOG = os.path.join(WS, "tmp", "board-serve.err.log")
|
LOG = os.path.join(WS, "tmp", "board-serve.err.log")
|
||||||
|
|
||||||
# 🔴 必须**先设好 env 再起 board** —— `board.py` 在**模块顶层**读这些名字。
|
# 🔴 必须**先设好 env 再起 board** —— `board.py` 在**模块顶层**读这些名字。
|
||||||
# ⚠️ `roots.env` 只兜底 `COLLABD_PROD_CONFIG`,而 `board.py` 读的是 `COLLABD_CONFIG`
|
# ⚠️ `env/roots.env` 只兜底 `COLLABD_PROD_CONFIG`,而 `board.py` 读的是 `COLLABD_CONFIG`
|
||||||
# ⇒ **名字对不上,不设就落到 `${脚本目录}/collabd.config.json`(不存在)⇒ 起不来**。
|
# ⇒ **名字对不上,不设就落到 `${脚本目录}/collabd.config.json`(不存在)⇒ 起不来**。
|
||||||
os.environ["COLLABD_CONFIG"] = os.path.join(WS, ".workbuddy", "collab", "collabd.config.json")
|
os.environ["COLLABD_CONFIG"] = os.path.join(WS, ".workbuddy", "collab", "collabd.config.json")
|
||||||
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", os.path.expanduser("~/.workbuddy"))
|
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", os.path.expanduser("~/.workbuddy"))
|
||||||
|
|||||||
+13
-7
@@ -1,10 +1,11 @@
|
|||||||
# session-mechanism · 根目录(样例 · ⛔ 请勿当成真文件用)
|
# session-mechanism · 环境专属配置(样例 · ⛔ 请勿当成真文件用)
|
||||||
#
|
#
|
||||||
# 🔴 为什么有这份样例:真正的 `roots.env` 是**本机 `install.py --apply` 生成的**
|
# 🔴 位置约定(2026-10-11 用户令:「脚本是脚本 配置是配置」⇒「每个技能 需要环境配置的 都有自己的 env文件夹」):
|
||||||
# (里面写的是**这台机器**的路径)⇒ ⛔ 不入库(入库会把别的电脑的路径覆盖掉)。
|
# 本包**每台机器各不同**的配置一律放 **本包 `env/`**;`scripts/` 只留通用代码。
|
||||||
# 本文件只示范**有哪些键、各填什么**,值一律是占位符。
|
# 本文件=入库的**样例**(只示范有哪些键、各填什么,值一律占位符)。
|
||||||
#
|
# 真文件=`env/roots.env`,由本机跑一次 `python install.py --apply` 生成,⛔ **不入库**
|
||||||
# 用法:本机跑一次 `python install.py --apply` ⇒ 它会在包根生成真正的 `roots.env`。
|
# (入库 ⇒ 别的电脑取仓时被本机路径覆盖 ⇒ 那台机器的机制会去找不存在的目录)。
|
||||||
|
# 仓根 `.gitignore` 的规则:`env/` 下只放行 `*.example`。
|
||||||
#
|
#
|
||||||
# 读取机制:各脚本头部 `_sm_load_roots()` 以 setdefault 注入
|
# 读取机制:各脚本头部 `_sm_load_roots()` 以 setdefault 注入
|
||||||
# ⇒ **宿主 env 优先,本文件兜底**。
|
# ⇒ **宿主 env 优先,本文件兜底**。
|
||||||
@@ -12,6 +13,11 @@
|
|||||||
# ① 工作区根(`state.py`、台账、心跳都在它下面)
|
# ① 工作区根(`state.py`、台账、心跳都在它下面)
|
||||||
DSH_WS_ROOT=<工作区根>,例:E:/AIProject/<工作区别名>
|
DSH_WS_ROOT=<工作区根>,例:E:/AIProject/<工作区别名>
|
||||||
|
|
||||||
|
# ①b 工作区根的**父目录**(常驻管理脚本靠它自动发现兄弟工作区:
|
||||||
|
# 扫 `<DSH_AIROOT>/*/.workbuddy/collab/` ⇒ ⛔ 不把工作区清单写死在代码里)
|
||||||
|
# 不填 ⇒ 兜底 = ① 的父目录
|
||||||
|
DSH_AIROOT=<工作区根的父目录>,例:E:/AIProject
|
||||||
|
|
||||||
# ② 结果钩子认的工作区(一般与 ① 同值)
|
# ② 结果钩子认的工作区(一般与 ① 同值)
|
||||||
WB_RESULT_HOOK_WS=<同上>
|
WB_RESULT_HOOK_WS=<同上>
|
||||||
|
|
||||||
@@ -19,7 +25,7 @@ WB_RESULT_HOOK_WS=<同上>
|
|||||||
COLLABD_PROD_CONFIG=<工作区根>/.workbuddy/collab/collabd.config.json
|
COLLABD_PROD_CONFIG=<工作区根>/.workbuddy/collab/collabd.config.json
|
||||||
|
|
||||||
# ④ WorkBuddy 的配置目录(宿主库 workbuddy.db 在它下面)
|
# ④ WorkBuddy 的配置目录(宿主库 workbuddy.db 在它下面)
|
||||||
CODEBUDDY_CONFIG_DIR=<WorkBuddy 配置目录>,例:E:/ProgramData/.workbuddy
|
CODEBUDDY_CONFIG_DIR=<WorkBuddy 配置目录>,例:<用户目录>/.workbuddy
|
||||||
|
|
||||||
# ⑤ 文档库根(锁目录 `05-交接单/.locks` 在它下面)
|
# ⑤ 文档库根(锁目录 `05-交接单/.locks` 在它下面)
|
||||||
DSH_DOCS_ROOT=<文档库根>
|
DSH_DOCS_ROOT=<文档库根>
|
||||||
@@ -4,7 +4,7 @@ session-mechanism · 一键配置(换机器只需跑这一个)
|
|||||||
|
|
||||||
用法
|
用法
|
||||||
python install.py --dry-run # 只打印将改什么(settings.json 的 diff),⛔ 不写盘
|
python install.py --dry-run # 只打印将改什么(settings.json 的 diff),⛔ 不写盘
|
||||||
python install.py --apply # 真装:写 roots.env → 按声明表接线全局钩子 → 初始化工作区
|
python install.py --apply # 真装:写 env/roots.env → 按声明表接线全局钩子 → 初始化工作区
|
||||||
python install.py --verify # 装完自检:每个钩子空载荷 rc=0 + collabd --where + selftest
|
python install.py --verify # 装完自检:每个钩子空载荷 rc=0 + collabd --where + selftest
|
||||||
python install.py --uninstall # 还原 settings.json(与装前备份逐字节相同)
|
python install.py --uninstall # 还原 settings.json(与装前备份逐字节相同)
|
||||||
python install.py --manifest # 重算 references/manifest.md 的逐文件表(改完包**必跑**)
|
python install.py --manifest # 重算 references/manifest.md 的逐文件表(改完包**必跑**)
|
||||||
@@ -24,8 +24,11 @@ session-mechanism · 一键配置(换机器只需跑这一个)
|
|||||||
4. **可逆**:首次安装先留**原状**备份 `settings.json.bak-session-mechanism-orig`(⛔ 已存在不覆盖),
|
4. **可逆**:首次安装先留**原状**备份 `settings.json.bak-session-mechanism-orig`(⛔ 已存在不覆盖),
|
||||||
其后每次 `--apply` 另写**带微秒**的时间戳备份(防同秒同名互相覆盖);
|
其后每次 `--apply` 另写**带微秒**的时间戳备份(防同秒同名互相覆盖);
|
||||||
`--uninstall` **优先**用 `-orig`,还原后做**逐字节**比对,不符即报错。
|
`--uninstall` **优先**用 `-orig`,还原后做**逐字节**比对,不符即报错。
|
||||||
5. **外置根目录**:写 `<包根>/roots.env` —— 因为 `settings.json` 的 hook 条目**没有 env 字段**,
|
5. **外置根目录**:写 `<包根>/env/roots.env` —— 因为 `settings.json` 的 hook 条目**没有 env 字段**,
|
||||||
包内脚本搬一次就会静默指错(历史事故:台账写到别处、测试却全绿)。
|
包内脚本搬一次就会静默指错(历史事故:台账写到别处、测试却全绿)。
|
||||||
|
🔴 2026-10-11 用户令(「脚本是脚本 配置是配置」⇒「每个技能 需要环境配置的 都有自己的 env文件夹」):
|
||||||
|
**每台机器各不同**的配置一律放本包 `env/`;`scripts/` 只留通用代码。
|
||||||
|
样例 `env/roots.env.example` 入库,真文件 `env/roots.env` 不入库(见仓根 `.gitignore`)。
|
||||||
6. **清单自算**:`--manifest` 只重写 `references/manifest.md` 的**表 + 计数行 + 重算时间**,
|
6. **清单自算**:`--manifest` 只重写 `references/manifest.md` 的**表 + 计数行 + 重算时间**,
|
||||||
⛔ 不碰上面的「最近改动」散文 —— 那是人写的结论,脚本代笔会把它冲成流水账。
|
⛔ 不碰上面的「最近改动」散文 —— 那是人写的结论,脚本代笔会把它冲成流水账。
|
||||||
包一改就得跑一次,否则表里的字节/md5 立刻过期(本项目已因此误判过两次)。
|
包一改就得跑一次,否则表里的字节/md5 立刻过期(本项目已因此误判过两次)。
|
||||||
@@ -47,7 +50,8 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PKG = Path(__file__).resolve().parent
|
PKG = Path(__file__).resolve().parent
|
||||||
ROOTS_ENV = PKG / "roots.env"
|
# 🔴 2026-10-11:环境专属配置统一落本包 `env/`(⛔ 不再放包根)—— 「脚本是脚本 配置是配置」。
|
||||||
|
ROOTS_ENV = PKG / "env" / "roots.env"
|
||||||
LOG = PKG / "install.log"
|
LOG = PKG / "install.log"
|
||||||
# 🔴 原状备份的**固定名**(首次安装、且当前 settings.json 无本包痕迹时才写;⛔ 已存在不覆盖)。
|
# 🔴 原状备份的**固定名**(首次安装、且当前 settings.json 无本包痕迹时才写;⛔ 已存在不覆盖)。
|
||||||
# 有了它,`--uninstall` 不需要"从一堆时间戳备份里猜哪份是原状"。
|
# 有了它,`--uninstall` 不需要"从一堆时间戳备份里猜哪份是原状"。
|
||||||
@@ -332,10 +336,15 @@ def merge(settings: dict) -> dict:
|
|||||||
|
|
||||||
def write_roots(ws: Path, docs: Path | None, code: str | None, dry: bool) -> None:
|
def write_roots(ws: Path, docs: Path | None, code: str | None, dry: bool) -> None:
|
||||||
lines = [
|
lines = [
|
||||||
"# session-mechanism · 根目录(由 install.py 生成,⛔ 请勿手改)",
|
"# session-mechanism · 环境专属配置(由 install.py 生成,⛔ 请勿手改)",
|
||||||
|
"# 位置:本包 `env/roots.env`(2026-10-11 起 —— 「脚本是脚本 配置是配置」)。",
|
||||||
"# 说明:settings.json 的 hook 条目没有 env 字段 ⇒ 包内脚本靠本文件定位根目录。",
|
"# 说明:settings.json 的 hook 条目没有 env 字段 ⇒ 包内脚本靠本文件定位根目录。",
|
||||||
"# 读取:各脚本头部 `_sm_load_roots()` 以 setdefault 注入 ⇒ **宿主 env 优先,本文件兜底**。",
|
"# 读取:各脚本头部 `_sm_load_roots()` 以 setdefault 注入 ⇒ **宿主 env 优先,本文件兜底**。",
|
||||||
f"DSH_WS_ROOT={ws.as_posix()}",
|
f"DSH_WS_ROOT={ws.as_posix()}",
|
||||||
|
# 🔴 2026-10-11 新键:工作区根(`DSH_WS_ROOT`)的**父目录**,例 `E:/ProgramData/AIProject`。
|
||||||
|
# 常驻管理脚本(collabctl/keepalive)靠它**自动发现**兄弟工作区(扫 `<AIROOT>/*/.workbuddy/collab/`),
|
||||||
|
# ⛔ 不再把工作区清单写死在入库代码里(写死 ⇒ 同步到别的机器会建一批空路径计划任务)。
|
||||||
|
f"DSH_AIROOT={ws.parent.as_posix()}",
|
||||||
f"WB_RESULT_HOOK_WS={ws.as_posix()}",
|
f"WB_RESULT_HOOK_WS={ws.as_posix()}",
|
||||||
# 🔴 2026-10-04 换机演练实测后加:`selftest.py` 的「生产工作区」判据要定位
|
# 🔴 2026-10-04 换机演练实测后加:`selftest.py` 的「生产工作区」判据要定位
|
||||||
# **使用方的部署配置**(它验的是生产数据)。⛔ 不可拿 `DSH_WS_ROOT` 拼 ——
|
# **使用方的部署配置**(它验的是生产数据)。⛔ 不可拿 `DSH_WS_ROOT` 拼 ——
|
||||||
@@ -356,6 +365,7 @@ def write_roots(ws: Path, docs: Path | None, code: str | None, dry: bool) -> Non
|
|||||||
if dry:
|
if dry:
|
||||||
log(f"[dry-run] 将写 {ROOTS_ENV}:\n{text}")
|
log(f"[dry-run] 将写 {ROOTS_ENV}:\n{text}")
|
||||||
return
|
return
|
||||||
|
ROOTS_ENV.parent.mkdir(parents=True, exist_ok=True) # 🔴 env/ 可能还不存在
|
||||||
ROOTS_ENV.write_text(text, encoding="utf-8")
|
ROOTS_ENV.write_text(text, encoding="utf-8")
|
||||||
log(f"已写 {ROOTS_ENV}")
|
log(f"已写 {ROOTS_ENV}")
|
||||||
|
|
||||||
@@ -579,8 +589,10 @@ def cmd_uninstall(args) -> int:
|
|||||||
# 🔴 为什么固化进 install.py:包一改,表里的字节/md5 立刻过期 ⇒ **每轮都得重算**。
|
# 🔴 为什么固化进 install.py:包一改,表里的字节/md5 立刻过期 ⇒ **每轮都得重算**。
|
||||||
# 以前每轮临时手搓一个脚本、跑完即删 —— 既浪费又会漏(本项目已因此误判过两次)。
|
# 以前每轮临时手搓一个脚本、跑完即删 —— 既浪费又会漏(本项目已因此误判过两次)。
|
||||||
MANIFEST = PKG / "references" / "manifest.md"
|
MANIFEST = PKG / "references" / "manifest.md"
|
||||||
# ⛔ 不入表:运行日志 + 表自身(写完即失真)
|
# ⛔ 不入表:运行日志 + 表自身(写完即失真)+ **本机生成的配置**(2026-10-11 加:
|
||||||
MANIFEST_SKIP = {"install.log", "references/manifest.md"}
|
# `env/roots.env` 每台机器一份 ⇒ 入表会让**入库的 manifest.md 随机器变**,
|
||||||
|
# 别的电脑取仓即出现无意义 diff —— 与"配置不入库"同一条口径)
|
||||||
|
MANIFEST_SKIP = {"install.log", "references/manifest.md", "env/roots.env"}
|
||||||
|
|
||||||
|
|
||||||
def _manifest_syntax(p: Path) -> str:
|
def _manifest_syntax(p: Path) -> str:
|
||||||
|
|||||||
@@ -100,6 +100,40 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 3b 🔴 配置层级:技能包级 `env/` vs 工作区级 `collabd.config.json`(2026-10-11 立)
|
||||||
|
|
||||||
|
> 用户令(2026-10-11):19:28「不用挪出 用env文件夹区分就行,重点是 **脚本是脚本 配置是配置**」/
|
||||||
|
> 19:29「**每个技能 需要环境配置的 都有自己的 env文件夹**」。
|
||||||
|
|
||||||
|
**两层,一层一份,各自单一真源**:
|
||||||
|
|
||||||
|
| 层 | 文件 | 装什么 | 谁读 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **L0 技能包级**(每台机器一份) | `<技能包>/env/roots.env`(真文件不入库;样例 `*.example` 入库) | 这台机器的根:`DSH_WS_ROOT`、`DSH_AIROOT`、`WB_RESULT_HOOK_WS`、`COLLABD_PROD_CONFIG`、`CODEBUDDY_CONFIG_DIR`、`DSH_DOCS_ROOT` | **包内脚本**:钩子(`scripts/hooks/*`)、`board.py`、`goalctl.py`、`collabctl.py`、`keepalive.py` |
|
||||||
|
| **L1 工作区级**(每工作区一份) | `<工作区>/.workbuddy/collab/collabd.config.json` | 这个工作区自己的东西:`workspace`/`inbox`/`live`/`taskgraph`/`host_db`(可写本机绝对路径)/`peer_workspaces` | **部署副本**:`<ws>/.workbuddy/collab/` 下的 `collabd.py`、`goalctl.py` |
|
||||||
|
|
||||||
|
**桥接(谁把两层接起来)**:`scripts/supervise-launch.py`(常驻)与 `scripts/board-launch.py`(看板)。
|
||||||
|
计划任务的动作里**没有 env 字段**,所以这两个启动器在**进程内**设
|
||||||
|
`COLLABD_CONFIG=<本区>/collabd.config.json` + `CODEBUDDY_CONFIG_DIR`,再 `runpy` 起本体。
|
||||||
|
|
||||||
|
- **常驻**:启动器装在 `<ws>/.workbuddy/collab/`(由 `deploy_code.py` 分发)⇒ `HERE` 就是本区 ⇒ 配置恒指本区。
|
||||||
|
- **看板**:启动器在技能包内,主工作区根从 L0 的 `DSH_WS_ROOT` 读;读到就用,读不到就**写日志 + 退出码 2**(⛔ 不猜路径)。
|
||||||
|
|
||||||
|
**部署副本怎么拿 L0 的配置**:从 `<ws>/.workbuddy/collab/` 跑**管理入口**(`collabctl.py`/`keepalive.py`)时,
|
||||||
|
`_sm_load_roots()` 向上找不到 `env/roots.env` ⇒ 回落到**本机技能库**那份
|
||||||
|
(`<CODEBUDDY_CONFIG_DIR>/skills/session-mechanism/env/roots.env`;`CODEBUDDY_CONFIG_DIR` 由宿主给)
|
||||||
|
⇒ 全机**只有一份** L0 配置,副本目录里**不放第二份**(避免各区分叉)。
|
||||||
|
🔴 而 `collabd.py` 的副本**不读 L0 的 `DSH_WS_ROOT`**(那会把别的区认成自己 —— P0-85 静默错归属族):
|
||||||
|
它靠**启动器设的 `COLLABD_CONFIG`** 定位自己。
|
||||||
|
|
||||||
|
**日常该改哪一层**:
|
||||||
|
|
||||||
|
- 换机器 / 换工作区根 / 换文档库 ⇒ 改 **L0**(重跑 `install.py --apply`,或手改 `env/roots.env`)
|
||||||
|
- 换本区配置(inbox 名、端口、`host_db`、peer 列表)⇒ 改 **L1**
|
||||||
|
- 两层都**不把**「别台机器才成立的值」写进入库代码(依据:用户 19:22「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4 旧副本退役(⛔ 否则"代码在 A、钩子在 B")
|
## 4 旧副本退役(⛔ 否则"代码在 A、钩子在 B")
|
||||||
|
|
||||||
症状:改了 skill 里的程序却没生效;或 `advance.md` 等产物由**另一个副本**写出(内容与你预期不同)。
|
症状:改了 skill 里的程序却没生效;或 `advance.md` 等产物由**另一个副本**写出(内容与你预期不同)。
|
||||||
@@ -167,13 +201,21 @@ curl -s --noproxy '*' -o /dev/null -w "%{http_code}\n" http://127.0.0.1:20090/
|
|||||||
**⚠️ 遗留单点(如实登记)**:这两条后台任务**挂在"起它的那个会话"名下** ⇒ **起它的会话被回收 / WorkBuddy 退出 ⇒ 链路断**。
|
**⚠️ 遗留单点(如实登记)**:这两条后台任务**挂在"起它的那个会话"名下** ⇒ **起它的会话被回收 / WorkBuddy 退出 ⇒ 链路断**。
|
||||||
⇒ 起它们的会话**在那段时间内不要关**;断了就按上面"② 不在才起"重起一次。
|
⇒ 起它们的会话**在那段时间内不要关**;断了就按上面"② 不在才起"重起一次。
|
||||||
|
|
||||||
## 6 换机器后必查的 5 件事
|
## 6 换机器后必查的 6 件事
|
||||||
|
|
||||||
1. `--where` 的三个路径是否都对;
|
1. `--where` 的三个路径是否都对;
|
||||||
2. 钩子接线是否指向 skill(§4);
|
2. 钩子接线是否指向 skill(§4);
|
||||||
3. 宿主库能否只读打开(`sessions` / `automation_runs` / `automations` 三张表读得到);
|
3. 宿主库能否只读打开(`sessions` / `automation_runs` / `automations` 三张表读得到);
|
||||||
4. 有没有**别的实现**在抢同一个 inbox(§4);
|
4. 有没有**别的实现**在抢同一个 inbox(§4);
|
||||||
5. `guard.py` 是否真的起来了(`--status`)+ 它是否会随开机自启。
|
5. `guard.py` 是否真的起来了(`--status`)+ 它是否会随开机自启;
|
||||||
|
6. 🔴 **计划任务里有没有指向不存在路径的项**(2026-10-11 加)。判据:逐条取出动作里的脚本路径
|
||||||
|
`Test-Path` 一遍;指向已删目录的(例:老工作区改名、`tmp/selftest/…` 这类一次性沙箱)=**静默死任务**。
|
||||||
|
处置:`Disable-ScheduledTask`(`collabctl.py` 的旧任务清单已含本机这几条)——
|
||||||
|
**禁用不删**,可逆。
|
||||||
|
```
|
||||||
|
Get-ScheduledTask | ForEach-Object { $t=$_; $t.Actions | ForEach-Object { "$($t.TaskName)|$($t.State)|$($_.Execute)|$($_.Arguments)" } }
|
||||||
|
```
|
||||||
|
⚠️ 判存在性时先**展开 `%windir%`/`%SystemRoot%`**,否则系统自带任务会被误报成缺失。
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2405,8 +2405,8 @@ INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox'
|
|||||||
| `E://ProgramData//.workbuddy//workbuddy.db`(**活动库**) | 34.9 MB | ✅ 247 行 |
|
| `E://ProgramData//.workbuddy//workbuddy.db`(**活动库**) | 34.9 MB | ✅ 247 行 |
|
||||||
| `C://Users//Administrator//.workbuddy//workbuddy.db`(旧库) | **0 字节** | ❌ 无 |
|
| `C://Users//Administrator//.workbuddy//workbuddy.db`(旧库) | **0 字节** | ❌ 无 |
|
||||||
|
|
||||||
- 🔴 **为什么只有 ai1net 炸**:vibe 的 config 里 `host_db` **写死绝对路径**;ai1net 的 `host_db` **一直是空字符串**。⇒ **同一份代码,两区行为不同,差别只在 config 这一格**。
|
- 🔴 **为什么只有 ai1net 炸**:vibe 的 config 里 `host_db` **写死绝对路径**;ai1net 的 `host_db` **一直空**。⇒ 差别只在 config 这一格。
|
||||||
- 🔴 **"计划任务的『动作』里没有 env 字段"**(P0-72 §二 同一条硬约束)⇒ `CODEBUDDY_CONFIG_DIR` **只能由启动器在进程内设**。
|
- 🔴 **计划任务的动作里没有 env 字段**(P0-72 §二)⇒ 该变量**只能由启动器进程内设**。
|
||||||
- 🔴 **怎么弄丢的(诚实记录)**:旧看守 `start-supervise.ps1` 本有 `$env:CODEBUDDY_CONFIG_DIR = ...`;P0-72 改成"两层形态"时**只保了 `COLLABD_CONFIG`,丢了这一句**。⇒ **重构"起法"时,旧起法里的 env 必须逐条搬**(⛔ 不许凭记忆只搬"我记得的那个")。
|
- 🔴 **怎么弄丢的(诚实记录)**:旧看守 `start-supervise.ps1` 本有 `$env:CODEBUDDY_CONFIG_DIR = ...`;P0-72 改成"两层形态"时**只保了 `COLLABD_CONFIG`,丢了这一句**。⇒ **重构"起法"时,旧起法里的 env 必须逐条搬**(⛔ 不许凭记忆只搬"我记得的那个")。
|
||||||
|
|
||||||
### 四、✅ 修法:新增常驻启动器(与 `board-launch.py` 对称)
|
### 四、✅ 修法:新增常驻启动器(与 `board-launch.py` 对称)
|
||||||
@@ -2414,7 +2414,8 @@ INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox'
|
|||||||
- 新增 **`scripts/supervise-launch.py`**:进程内 `os.environ.setdefault("CODEBUDDY_CONFIG_DIR", ...)` + 设 `COLLABD_CONFIG=<本区>/collabd.config.json` + 兜住 `sys.stdout/stderr`(pythonw 下可能是 `None`)+ `runpy.run_path(collabd.py, run_name="__main__")`。
|
- 新增 **`scripts/supervise-launch.py`**:进程内 `os.environ.setdefault("CODEBUDDY_CONFIG_DIR", ...)` + 设 `COLLABD_CONFIG=<本区>/collabd.config.json` + 兜住 `sys.stdout/stderr`(pythonw 下可能是 `None`)+ `runpy.run_path(collabd.py, run_name="__main__")`。
|
||||||
- 常驻任务动作改指 `pythonw.exe "<区>/.workbuddy/collab/supervise-launch.py"`(⛔ **不再自带 `--supervise`**),`-WorkingDirectory` 仍是**工作区根**,`-ExecutionTimeLimit` 恒 `0`。
|
- 常驻任务动作改指 `pythonw.exe "<区>/.workbuddy/collab/supervise-launch.py"`(⛔ **不再自带 `--supervise`**),`-WorkingDirectory` 仍是**工作区根**,`-ExecutionTimeLimit` 恒 `0`。
|
||||||
- `deploy_code.py` 的 `DEFAULT_FILES` **补入该启动器**(否则各区吃不到,P0-57 同族)。
|
- `deploy_code.py` 的 `DEFAULT_FILES` **补入该启动器**(否则各区吃不到,P0-57 同族)。
|
||||||
- ⚠️ **`roots.env` 位置缺口(连带发现)**:`_sm_load_roots()` 从 `__file__` **向上**找 `roots.env`;工作区副本在 `<ws>/.workbuddy/collab/collabd.py` ⇒ 工作区里没有该文件 ⇒ **找不到** ⇒ **启动器里写死最稳**,⛔ 别指望 `roots.env` 兜住各区。
|
- ⚠️ **`env/roots.env` 位置缺口(连带发现)**:`_sm_load_roots()` 从 `__file__` **向上**找 `env/roots.env`;工作区副本在 `<ws>/.workbuddy/collab/` ⇒ 找不到 ⇒ 那一路靠**启动器进程内设 env**。
|
||||||
|
(原文「启动器里写死最稳」—— 2026-10-11 改为「值放本包 `env/`,启动器照读」。)
|
||||||
|
|
||||||
### 五、✅ 验收(2026-10-05 12:15 实测)
|
### 五、✅ 验收(2026-10-05 12:15 实测)
|
||||||
|
|
||||||
@@ -2426,7 +2427,7 @@ INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox'
|
|||||||
```
|
```
|
||||||
⇒ **`pid=58860` 之后 `no such table` 计数 = 0**(`awk` 分界**实测**),从 fail-safe 的"恒判有会话"变成**真实读库判定**。
|
⇒ **`pid=58860` 之后 `no such table` 计数 = 0**(`awk` 分界**实测**),从 fail-safe 的"恒判有会话"变成**真实读库判定**。
|
||||||
|
|
||||||
**② 那条 `working(a80f300d)` 是真的**(直查活动库):就是**当前这条会话本身** ⇒ 闸②「本区有会话在跑 ⇒ 不建检查会话」是**正确的合法拦截**(⛔ 不是 bug)。
|
**② 那条 `working(a80f300d)` 是真的**(直查活动库):就是**当前这条会话本身** ⇒ 闸②「本区有会话在跑 ⇒ 不建」是**正确的合法拦截**。
|
||||||
|
|
||||||
**③ 变异对照证明判据非恒绿**(两档):
|
**③ 变异对照证明判据非恒绿**(两档):
|
||||||
|
|
||||||
@@ -2438,7 +2439,7 @@ INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox'
|
|||||||
⇒ **A 必 FAIL、B 必 OK** ⇒ 路径来源**真实决定成败**(⛔ 不是"应该绿")。
|
⇒ **A 必 FAIL、B 必 OK** ⇒ 路径来源**真实决定成败**(⛔ 不是"应该绿")。
|
||||||
|
|
||||||
**④ 运行时**(恒 3 进程、父链断 ⇒ 真常驻):两台**启动器**(各指本区)+ 一个**看板**。
|
**④ 运行时**(恒 3 进程、父链断 ⇒ 真常驻):两台**启动器**(各指本区)+ 一个**看板**。
|
||||||
**⑤ 三处 md5 一致**;看板 `curl --noproxy '*' :20099/` ⇒ **HTTP 200**、`netstat` 实测 **LISTENING**。
|
**⑤ 三处 md5 一致**;`curl --noproxy '*' :20099/` ⇒ **HTTP 200**、`netstat` 实测 **LISTENING**。
|
||||||
|
|
||||||
### 六、⛔ 教训(一句话)
|
### 六、⛔ 教训(一句话)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
## 5 钩子与脚本的写法约束
|
## 5 钩子与脚本的写法约束
|
||||||
|
|
||||||
- 🔴 **hook 脚本必须走 `sys.stdout.buffer.write(bytes)`** 输出 —— 否则文案含非 ASCII 符号时抛 `UnicodeEncodeError` ⇒ stdout 为空 ⇒ **静默放行**。
|
- 🔴 **hook 脚本必须走 `sys.stdout.buffer.write(bytes)`** 输出 —— 否则文案含非 ASCII 符号时抛 `UnicodeEncodeError` ⇒ stdout 为空 ⇒ **静默放行**。
|
||||||
- 🔴 **不要按位置推导根目录**:钩子脚本被搬一次就会**静默指错**(历史事故:台账写到别处、测试却全绿)⇒ 用外置的 `roots.env`(本包由 `install.py` 生成)。
|
- 🔴 **不按位置推导根目录**:钩子脚本被搬一次就会**静默指错**(历史事故:台账写到别处、测试却全绿)⇒ 用外置的 **`env/roots.env`**(本包由 `install.py` 生成,每台机器各一份)。
|
||||||
|
- 🔴 **配置与代码分家**(2026-10-11 用户令:「脚本是脚本 配置是配置」):「每台机器各不同」的值 → 本包 **`env/`**(入库的只有 `*.example`);`scripts/` 只放通用代码,读 `env/` 里的值、⛔ 不写死本机路径与本机清单。
|
||||||
- ⚠️ `settings.json` 的 hook 条目**没有 `env` 字段** ⇒ ⛔ 别指望用 env 给钩子传参。
|
- ⚠️ `settings.json` 的 hook 条目**没有 `env` 字段** ⇒ ⛔ 别指望用 env 给钩子传参。
|
||||||
- 🔴 **作用域⛔ 不许静默排除**:被跳过必须**留下痕迹**(写跳过清单 + stderr),否则"没动作"与"坏了"长得一模一样。
|
- 🔴 **作用域⛔ 不许静默排除**:被跳过必须**留下痕迹**(写跳过清单 + stderr),否则"没动作"与"坏了"长得一模一样。
|
||||||
- ⚠️ **钩子脚本内容改动即时生效**;但 `settings.json` 的 hooks 条目是**应用启动时快照** ⇒ 改条目要**完全重启**(关窗 ≠ 退出)。
|
- ⚠️ **钩子脚本内容改动即时生效**;但 `settings.json` 的 hooks 条目是**应用启动时快照** ⇒ 改条目要**完全重启**(关窗 ≠ 退出)。
|
||||||
|
|||||||
@@ -136,16 +136,21 @@
|
|||||||
**`supervise-launch.py` 的四条实现要点**(见该文件头注,⛔ 别自己另写一份):
|
**`supervise-launch.py` 的四条实现要点**(见该文件头注,⛔ 别自己另写一份):
|
||||||
|
|
||||||
1. **它所在目录即本区 `.workbuddy/collab`** —— `collabd.py`/`collabd.config.json` 都在同目录,**用相对自身定位**(⛔ 别写死 `E:\`);
|
1. **它所在目录即本区 `.workbuddy/collab`** —— `collabd.py`/`collabd.config.json` 都在同目录,**用相对自身定位**(⛔ 别写死 `E:\`);
|
||||||
2. `CODEBUDDY_CONFIG_DIR` 用 `setdefault`(宿主 env 优先,兜底才写死);`COLLABD_CONFIG` **必须指向本区**那份配置;
|
2. `CODEBUDDY_CONFIG_DIR` 用 `setdefault`(宿主 env 优先,兜底值取 `os.path.expanduser("~/.workbuddy")` 形态,
|
||||||
|
🔴 2026-10-11 起不再写死本机盘符);`COLLABD_CONFIG` **必须指向本区**那份配置;
|
||||||
3. `pythonw.exe` 下 `sys.stdout/stderr` 可能是 `None` ⇒ **任何 `print` 都会 `AttributeError` 秒退** ⇒ 启动器自己兜到 `logs/_supervise-launch.log`;
|
3. `pythonw.exe` 下 `sys.stdout/stderr` 可能是 `None` ⇒ **任何 `print` 都会 `AttributeError` 秒退** ⇒ 启动器自己兜到 `logs/_supervise-launch.log`;
|
||||||
4. 用 `runpy.run_path(CB, run_name="__main__")`(⛔ **不用 `exec`**)—— `runpy` 会把 `__name__`/`__file__`/`sys.path[0]` 按"真在跑那个脚本"设好;
|
4. 用 `runpy.run_path(CB, run_name="__main__")`(⛔ **不用 `exec`**)—— `runpy` 会把 `__name__`/`__file__`/`sys.path[0]` 按"真在跑那个脚本"设好;
|
||||||
且 `sys.argv` **由启动器自己补 `--supervise`** ⇒ **任务参数里 ⛔ 不要再写**。
|
且 `sys.argv` **由启动器自己补 `--supervise`** ⇒ **任务参数里 ⛔ 不要再写**。
|
||||||
|
|
||||||
> 🔴 **重构"起法"时,旧起法里的 env 必须逐条搬**(P0-73 就是改成两层形态时**丢了 `CODEBUDDY_CONFIG_DIR` 这一句**)。
|
> 🔴 **重构"起法"时,旧起法里的 env 必须逐条搬**(P0-73 就是改成两层形态时**丢了 `CODEBUDDY_CONFIG_DIR` 这一句**)。
|
||||||
> 🔴 **启动器里写死最稳**(⛔ 别指望 `roots.env` 兜住各区 —— 它只在技能目录,而各区副本在
|
> 🔴 **启动器把该设的 env 在进程内设好**(2026-10-11 起配置住本包 **`env/roots.env`**)——
|
||||||
> `<ws>/.workbuddy/collab/`,`_sm_load_roots()` 向上 4 层**找不到**)。
|
> ⛔ 别指望 `env/roots.env` 兜住各区副本:它只在技能包目录,而各区副本在
|
||||||
> ⚠️ **变量名坑**:`roots.env` 给的是 **`COLLABD_PROD_CONFIG`**,而 `board.py`/`collabd.py` 读的是
|
> `<ws>/.workbuddy/collab/`,`_sm_load_roots()` 向上 4 层**找不到**。
|
||||||
> **`COLLABD_CONFIG`** ⇒ 名字对不上 ⇒ `roots.env` **兜不住**。
|
> ✅ 各区那一路的正确供法 = **启动器设 env**(`COLLABD_CONFIG` 指本区那
|
||||||
|
> 份)+ 该区 `collabd.config.json` 兜底(`host_db` 可直接写本机绝对路径 —— 那**属于"每台机器各不同"的配置**,
|
||||||
|
> 本就该落在本区配置里,⛔ 不是写进入库代码)。
|
||||||
|
> ⚠️ **变量名坑**:`env/roots.env` 给的是 **`COLLABD_PROD_CONFIG`**,而 `board.py`/`collabd.py` 读的是
|
||||||
|
> **`COLLABD_CONFIG`** ⇒ 名字对不上 ⇒ `env/roots.env` **兜不住**。
|
||||||
|
|
||||||
|
|
||||||
## 二、装法(⛔ 别自己写 `Register-ScheduledTask`)
|
## 二、装法(⛔ 别自己写 `Register-ScheduledTask`)
|
||||||
|
|||||||
@@ -11,22 +11,68 @@
|
|||||||
⇒ 正解=启动器(本文件):`pythonw.exe board-launch.py`,env 在 Python 进程内设。
|
⇒ 正解=启动器(本文件):`pythonw.exe board-launch.py`,env 在 Python 进程内设。
|
||||||
|
|
||||||
⚠️ **看板是全局共用一份**(用户拍板)⇒ 它的配置**固定指向主工作区**(`DSH_WS_ROOT`)那份,
|
⚠️ **看板是全局共用一份**(用户拍板)⇒ 它的配置**固定指向主工作区**(`DSH_WS_ROOT`)那份,
|
||||||
⛔ 不随工作区变。主工作区由 `roots.env` 的 `DSH_WS_ROOT` 决定,兜底写死 ai1net-dsh-server。
|
⛔ 不随工作区变。主工作区根由**宿主 env → 本包 `env/roots.env`** 决定(2026-10-11 起)。
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
# 🔴 主工作区根 —— 看板配置的真源(与 `roots.env` 的 `DSH_WS_ROOT` 同口径)
|
|
||||||
WS = os.environ.get("DSH_WS_ROOT") or r"E:\ProgramData\AIProject\ai1net-dsh-server"
|
|
||||||
WS = WS.replace("/", "\\")
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
# 🔴 2026-10-11:配置从本包 `env/roots.env` 读(「脚本是脚本 配置是配置」)——
|
||||||
|
# ⛔ 本文件**不写死**本机工作区 / 盘符;旧版兜底写死 ai1net-dsh-server ⇒ 换机器即错。
|
||||||
|
def _sm_load_roots():
|
||||||
|
import os as _os
|
||||||
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
for _up in range(4):
|
||||||
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_p = _os.path.join(_base, *_rel)
|
||||||
|
if _os.path.isfile(_p):
|
||||||
|
try:
|
||||||
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
for _ln in _f:
|
||||||
|
_ln = _ln.strip()
|
||||||
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
|
_k, _v = _ln.split("=", 1)
|
||||||
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
_sm_load_roots()
|
||||||
|
|
||||||
|
|
||||||
|
def _fail(msg: str, code: int = 2):
|
||||||
|
"""⛔ 不静默退出:`pythonw` 下 `stderr` 可能是 `None` ⇒ 先写包内 `logs/`(不入库)留痕,再退出。"""
|
||||||
|
try:
|
||||||
|
_d = os.path.join(os.path.dirname(HERE), "logs")
|
||||||
|
os.makedirs(_d, exist_ok=True)
|
||||||
|
with open(os.path.join(_d, "board-launch.err.log"), "a", encoding="utf-8", errors="replace") as _f:
|
||||||
|
_f.write("%s %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if sys.stderr is not None:
|
||||||
|
sys.stderr.write(msg + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise SystemExit(code)
|
||||||
|
|
||||||
|
|
||||||
|
# 🔴 主工作区根 —— 看板配置的真源(宿主 env → `env/roots.env` 的 `DSH_WS_ROOT`)。
|
||||||
|
WS = (os.environ.get("DSH_WS_ROOT") or "").strip()
|
||||||
|
if not WS:
|
||||||
|
_fail("[board-launch] 未解析到 DSH_WS_ROOT ⇒ 不猜路径、不起看板。"
|
||||||
|
"要什么:设宿主 env,或在包内 `env/roots.env` 里给出主工作区根。")
|
||||||
|
WS = WS.replace("/", "\\")
|
||||||
|
|
||||||
BOARD = os.path.join(HERE, "board.py")
|
BOARD = os.path.join(HERE, "board.py")
|
||||||
PORT = os.environ.get("DSH_BOARD_PORT") or "20099"
|
PORT = os.environ.get("DSH_BOARD_PORT") or "20099"
|
||||||
|
|
||||||
CFG = os.path.join(WS, ".workbuddy", "collab", "collabd.config.json")
|
CFG = os.path.join(WS, ".workbuddy", "collab", "collabd.config.json")
|
||||||
os.environ["COLLABD_CONFIG"] = CFG
|
os.environ["COLLABD_CONFIG"] = CFG
|
||||||
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", r"E:\ProgramData\.workbuddy")
|
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", os.path.expanduser("~/.workbuddy"))
|
||||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
os.environ["PYTHONUNBUFFERED"] = "1"
|
os.environ["PYTHONUNBUFFERED"] = "1"
|
||||||
|
|
||||||
|
|||||||
@@ -25,24 +25,27 @@
|
|||||||
⇒ 现规则:**端口上已有本看板 ⇒ 默认拒绝启动**(提示复用现成那个);要换代码用 `--takeover`。
|
⇒ 现规则:**端口上已有本看板 ⇒ 默认拒绝启动**(提示复用现成那个);要换代码用 `--takeover`。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
|
|||||||
@@ -34,24 +34,27 @@ import socket
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
|
|||||||
@@ -42,6 +42,34 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
# 🔴 2026-10-11:环境配置外置(「脚本是脚本 配置是配置」)⇒ 本包 `env/roots.env`。
|
||||||
|
# ⛔ 本文件**不写死**任何本机路径 / 本机工作区清单(写死 ⇒ 同步到别的机器会按不存在的路径建计划任务)。
|
||||||
|
def _sm_load_roots():
|
||||||
|
import os as _os
|
||||||
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
|
for _up in range(4):
|
||||||
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
# 🔴 本机技能库那份兜底:工作区副本(`<ws>/.workbuddy/collab/`)靠它拿到「这台机器」的配置
|
||||||
|
# ⇒ 全机单一真源(副本目录里⛔ 不放第二份,避免各区分叉)。
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
|
if _os.path.isfile(_p):
|
||||||
|
try:
|
||||||
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
for _ln in _f:
|
||||||
|
_ln = _ln.strip()
|
||||||
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
|
_k, _v = _ln.split("=", 1)
|
||||||
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
_sm_load_roots()
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 常量
|
# ---------------------------------------------------------------- 常量
|
||||||
# 🔴🔴 2026-10-05 **两条旧结论被实测推翻**(都在本机跑出来的,见 skill 的 pitfalls P0-71):
|
# 🔴🔴 2026-10-05 **两条旧结论被实测推翻**(都在本机跑出来的,见 skill 的 pitfalls P0-71):
|
||||||
# ① **`CREATE_BREAKAWAY_FROM_JOB` 在本机被拒** —— 带上它 `subprocess` 直接
|
# ① **`CREATE_BREAKAWAY_FROM_JOB` 在本机被拒** —— 带上它 `subprocess` 直接
|
||||||
@@ -60,18 +88,51 @@ HIDE = (0x08000000 # CREATE_NO_WINDOW(⛔ 没有它 ⇒ 闪黑窗)
|
|||||||
# ⇒ **起进程直接失败**(2026-10-05 实测)。⇒ 起法=**先试带、被拒退回不带**;
|
# ⇒ **起进程直接失败**(2026-10-05 实测)。⇒ 起法=**先试带、被拒退回不带**;
|
||||||
# **只读查询(`_ps`)一律直接用它**,⛔ 别试 breakaway(试了就是白失败一次)。
|
# **只读查询(`_ps`)一律直接用它**,⛔ 别试 breakaway(试了就是白失败一次)。
|
||||||
HIDE_NO_BREAK = 0x08000000 | 0x00000008 | 0x00000200
|
HIDE_NO_BREAK = 0x08000000 | 0x00000008 | 0x00000200
|
||||||
AIROOT = "E:/ProgramData/AIProject"
|
|
||||||
HB_FRESH_S = 90 # 🔴 心跳新鲜阈值(与 `collabd.supervise_alive()` 同口径)
|
HB_FRESH_S = 90 # 🔴 心跳新鲜阈值(与 `collabd.supervise_alive()` 同口径)
|
||||||
|
|
||||||
# 🔴 **登记制**:要管哪些工作区,只在**这一处**登记(⛔ 别在各处各写一份路径清单)。
|
|
||||||
WORKSPACES = [
|
|
||||||
"E:/ProgramData/AIProject/ai1net-dsh-server",
|
|
||||||
"E:/ProgramData/AIProject/vibe-product",
|
|
||||||
"E:/ProgramData/AIProject/contentm_agent",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 看板:全局只一条(⛔ 不是每区一条)
|
def _resolve_airoot() -> str:
|
||||||
BOARD_PY = "E:/ProgramData/.workbuddy/skills/session-mechanism/scripts/board.py"
|
"""工作区根所在的**父目录**。**DSH_AIROOT 优先**(`env/roots.env` 注入);
|
||||||
|
兜底 = `DSH_WS_ROOT` 的父目录。⛔ 不在代码里写死盘符/目录。
|
||||||
|
|
||||||
|
🔴 2026-10-11 用户报(原话):「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」
|
||||||
|
—— 旧版这里写死 `E:/ProgramData/AIProject`,同步到别的机器 ⇒ 路径不存在
|
||||||
|
⇒ `on|ensure` 会**建出一批指向空路径的计划任务**。
|
||||||
|
"""
|
||||||
|
cand = (os.environ.get("DSH_AIROOT") or "").strip()
|
||||||
|
if not cand:
|
||||||
|
ws = (os.environ.get("DSH_WS_ROOT") or "").strip()
|
||||||
|
if ws:
|
||||||
|
cand = os.path.dirname(ws.rstrip("/\\"))
|
||||||
|
return cand.replace("\\", "/") if cand else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_workspaces(airoot: str) -> list:
|
||||||
|
"""**自动发现**要管的工作区:扫 `AIROOT` 下装了 `.workbuddy/collab/` 的目录。
|
||||||
|
|
||||||
|
⛔ 不维护登记清单(登记=写死 ⇒ 别的机器上要么漏、要么指向空路径;本机也踩过
|
||||||
|
「本区不在名单里 ⇒ 改名残留的旧任务谁都没扫到」的判断盲区)。
|
||||||
|
"""
|
||||||
|
if not airoot or not os.path.isdir(airoot):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for name in sorted(os.listdir(airoot)):
|
||||||
|
d = os.path.join(airoot, name)
|
||||||
|
if os.path.isdir(os.path.join(d, ".workbuddy", "collab")):
|
||||||
|
out.append(d.replace("\\", "/"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _keepalive_tasks(workspaces: list) -> dict:
|
||||||
|
"""工作区 → 保活任务名。**由目录名推导**:`collabd-keepalive-<basename>`(⛔ 不写死字典)。"""
|
||||||
|
return {w: "collabd-keepalive-" + os.path.basename(w.rstrip("/\\")) for w in workspaces}
|
||||||
|
|
||||||
|
|
||||||
|
AIROOT = _resolve_airoot()
|
||||||
|
WORKSPACES = _discover_workspaces(AIROOT)
|
||||||
|
|
||||||
|
# 看板:全局只一条(⛔ 不是每区一条);**技能包内相对定位** ⇒ 换机器不用改。
|
||||||
|
BOARD_PY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "board.py")
|
||||||
BOARD_PORT = 20099
|
BOARD_PORT = 20099
|
||||||
|
|
||||||
# 🔴🔴 2026-10-05 改(用户定案:「**常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?**」):
|
# 🔴🔴 2026-10-05 改(用户定案:「**常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?**」):
|
||||||
@@ -81,11 +142,9 @@ BOARD_PORT = 20099
|
|||||||
# ✅ 唯一合格载体=**计划任务**(父链根与会话树无关 —— 实测:父进程已退出、链断在自己身上)。
|
# ✅ 唯一合格载体=**计划任务**(父链根与会话树无关 —— 实测:父进程已退出、链断在自己身上)。
|
||||||
# ✅ 动作=**`pythonw.exe`**(GUI 子系统 ⇒ 不弹窗;⛔ 不是 `powershell.exe`,那是控制台程序)。
|
# ✅ 动作=**`pythonw.exe`**(GUI 子系统 ⇒ 不弹窗;⛔ 不是 `powershell.exe`,那是控制台程序)。
|
||||||
# ⇒ 所以本入口的 `on`/`ensure` 现在**建/启用这两组任务**,`off` **禁用它们+杀进程**。
|
# ⇒ 所以本入口的 `on`/`ensure` 现在**建/启用这两组任务**,`off` **禁用它们+杀进程**。
|
||||||
KEEPALIVE_TASKS = { # 新形态:计划任务 → pythonw collabd.py --supervise
|
# 新形态:计划任务 → pythonw collabd.py --supervise。🔴 2026-10-11:**由工作区名推导**
|
||||||
"E:/ProgramData/AIProject/ai1net-dsh-server": "collabd-keepalive-ai1net-dsh-server",
|
# (`collabd-keepalive-<basename>`)—— 原为写死字典 ⇒ 换机器/改名即失效。
|
||||||
"E:/ProgramData/AIProject/vibe-product": "collabd-keepalive-vibe-product",
|
KEEPALIVE_TASKS = _keepalive_tasks(WORKSPACES)
|
||||||
"E:/ProgramData/AIProject/contentm_agent": "collabd-keepalive-contentm_agent",
|
|
||||||
}
|
|
||||||
BOARD_TASK = "dsh-board-keepalive"
|
BOARD_TASK = "dsh-board-keepalive"
|
||||||
|
|
||||||
# 旧任务:一律**禁用**(⛔ 不删除,可逆)。它们是弹窗事故的来源(动作=powershell.exe)。
|
# 旧任务:一律**禁用**(⛔ 不删除,可逆)。它们是弹窗事故的来源(动作=powershell.exe)。
|
||||||
@@ -551,7 +610,7 @@ def cmd_ensure():
|
|||||||
_p("── ensure:按开关收敛(on⇒补齐缺失的,off⇒全停)──")
|
_p("── ensure:按开关收敛(on⇒补齐缺失的,off⇒全停)──")
|
||||||
on_ws = [w for w in WORKSPACES if switch_of(w) == "on"]
|
on_ws = [w for w in WORKSPACES if switch_of(w) == "on"]
|
||||||
if not on_ws:
|
if not on_ws:
|
||||||
_p(" 两区开关都 off ⇒ 走全停")
|
_p(" 各区开关都 off ⇒ 走全停")
|
||||||
cmd_off()
|
cmd_off()
|
||||||
return
|
return
|
||||||
disable_tasks() # 旧形态任务:见一次禁一次(新形态由 start_all 重建)
|
disable_tasks() # 旧形态任务:见一次禁一次(新形态由 start_all 重建)
|
||||||
@@ -634,6 +693,13 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("⛔ 打不开 --out %s:%r" % (out, e))
|
print("⛔ 打不开 --out %s:%r" % (out, e))
|
||||||
return 1
|
return 1
|
||||||
|
# 🔴 2026-10-11 自适配:解析不到工作区根 ⇒ **优雅报错**,⛔ 不猜路径、
|
||||||
|
# ⛔ 不去建指向空路径的计划任务(那就是用户说的"把环境搞坏了")。
|
||||||
|
if not AIROOT:
|
||||||
|
_p("⚠️ 未解析到工作区根 ⇒ 本入口**不猜路径**。")
|
||||||
|
_p(" 要什么:设 `DSH_AIROOT`(或 `DSH_WS_ROOT`),或先跑 `install.py --apply` 生成 `env/roots.env`。")
|
||||||
|
if a in ("on", "ensure"):
|
||||||
|
return 1
|
||||||
try:
|
try:
|
||||||
if a == "status":
|
if a == "status":
|
||||||
cmd_status()
|
cmd_status()
|
||||||
|
|||||||
@@ -66,24 +66,27 @@
|
|||||||
(无参数仍=常驻,保留兼容;**定案形态以 `--supervise` 为准**)
|
(无参数仍=常驻,保留兼容;**定案形态以 `--supervise` 为准**)
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
|
|||||||
@@ -14,24 +14,27 @@
|
|||||||
⛔ 失败一律**静默退出 0** —— 它是旁路,⛔ 不得影响钩子本身、更不得影响 WorkBuddy 运行。
|
⛔ 失败一律**静默退出 0** —— 它是旁路,⛔ 不得影响钩子本身、更不得影响 WorkBuddy 运行。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
|
|||||||
@@ -63,24 +63,27 @@
|
|||||||
⚠️ 任何写动作前**先备份**(`.bak-goalctl-<日期>/`);⛔ 脚本从不删文件。
|
⚠️ 任何写动作前**先备份**(`.bak-goalctl-<日期>/`);⛔ 脚本从不删文件。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
@@ -728,6 +731,20 @@ def declare() -> int:
|
|||||||
if _is_switch:
|
if _is_switch:
|
||||||
try:
|
try:
|
||||||
GOALS_DIR.mkdir(parents=True, exist_ok=True)
|
GOALS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 🔴 2026-10-11 旧目标收尾:换目标 ⇒ 旧目标整份归档。若它还停在
|
||||||
|
# 「进行中/等待/阻碍」(= 没做完就被换掉),置为「已暂停」——
|
||||||
|
# 否则归档件带着「进行中」进 `goals/`,看板照 `lifecycle` 分列,
|
||||||
|
# 会把这些**已无人推进**的目标一直摆在「执行中」(2026-10-11 实测:
|
||||||
|
# 看板「执行中」4 张里 3 张都是这类)。⛔ 原本「已完成」的保持不动。
|
||||||
|
_old_raw = str(g.get("lifecycle") or "").strip()
|
||||||
|
if not _old_raw.startswith("已完成"):
|
||||||
|
g["lifecycle"] = "已暂停"
|
||||||
|
g["lifecycle_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
g["lifecycle_by"] = "goalctl declare --switch-goal(换目标 ⇒ 旧目标收尾)"
|
||||||
|
g["lifecycle_why"] = ("换目标 ⇒ 旧目标收尾为「已暂停」(改前=%s);"
|
||||||
|
"它不再被推进,归档件可回查" % (_old_raw or "未声明"))
|
||||||
|
print(" 🔴 旧目标已收尾:`lifecycle` %s ⇒ 已暂停(不再推进,看板归「待执行」)"
|
||||||
|
% (_old_raw or "(未声明)"))
|
||||||
_safe = re.sub(r'[\\/:*?"<>|\r\n\t]+', "-", old_title).strip()[:40] or "untitled"
|
_safe = re.sub(r'[\\/:*?"<>|\r\n\t]+', "-", old_title).strip()[:40] or "untitled"
|
||||||
_dst = GOALS_DIR / ("%s__%s.json" % (_safe, time.strftime("%Y%m%d-%H%M")))
|
_dst = GOALS_DIR / ("%s__%s.json" % (_safe, time.strftime("%Y%m%d-%H%M")))
|
||||||
_n = 1
|
_n = 1
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_STAMP_NAME = "env-stamp.json"
|
_STAMP_NAME = "env-stamp.json"
|
||||||
_ROOTS_ENV = "roots.env" # 技能根的权威落点(install.py 生成)
|
_ROOTS_ENV = "roots.env" # 文件名;落点是技能包 `<包>/env/roots.env`(install.py 生成)
|
||||||
# 🔴🔴 2026-10-04 单包自包含改造:识别特征改成**多个候选**,⛔ 不再只认
|
# 🔴🔴 2026-10-04 单包自包含改造:识别特征改成**多个候选**,⛔ 不再只认
|
||||||
# `agent-operating-rules`(那是**另一个**技能 ⇒ 只装本包时会把真技能库判成"不存在")。
|
# `agent-operating-rules`(那是**另一个**技能 ⇒ 只装本包时会把真技能库判成"不存在")。
|
||||||
# 🔴 旧写法的病:`skills_root()` / `env_check()` 三处都拿`(root)/_SNIFF).is_dir()` 当
|
# 🔴 旧写法的病:`skills_root()` / `env_check()` 三处都拿`(root)/_SNIFF).is_dir()` 当
|
||||||
@@ -74,21 +74,24 @@ def _norm(p) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _load_roots_env() -> dict:
|
def _load_roots_env() -> dict:
|
||||||
"""读技能包里的 `roots.env`(⛔ 各脚本的 `_sm_load_roots()` 注入 env 前的原始来源)。"""
|
"""读技能包里的 `env/roots.env`(⛔ 各脚本的 `_sm_load_roots()` 注入 env 前的原始来源)。
|
||||||
|
|
||||||
|
查找顺序:向上逐层 `<d>/env/roots.env`(现行落点)→ `<d>/roots.env`(包根兼容)。
|
||||||
|
"""
|
||||||
out = {}
|
out = {}
|
||||||
d = Path(__file__).resolve().parent
|
d = Path(__file__).resolve().parent
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
f = d / _ROOTS_ENV
|
for f in (d / "env" / _ROOTS_ENV, d / _ROOTS_ENV):
|
||||||
if f.is_file():
|
if f.is_file():
|
||||||
try:
|
try:
|
||||||
for ln in f.read_text(encoding="utf-8").splitlines():
|
for ln in f.read_text(encoding="utf-8").splitlines():
|
||||||
ln = ln.strip()
|
ln = ln.strip()
|
||||||
if ln and not ln.startswith("#") and "=" in ln:
|
if ln and not ln.startswith("#") and "=" in ln:
|
||||||
k, v = ln.split("=", 1)
|
k, v = ln.split("=", 1)
|
||||||
out[k.strip()] = v.strip()
|
out[k.strip()] = v.strip()
|
||||||
return out
|
return out
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
n = d.parent
|
n = d.parent
|
||||||
if n == d:
|
if n == d:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -29,24 +29,27 @@
|
|||||||
⛔ **别给本脚本加 `-E`**:`-E` 会屏蔽 `PYTHONUTF8`/`PYTHONIOENCODING` ⇒ stdin 回退 cp936 ⇒
|
⛔ **别给本脚本加 `-E`**:`-E` 会屏蔽 `PYTHONUTF8`/`PYTHONIOENCODING` ⇒ stdin 回退 cp936 ⇒
|
||||||
含中文的 payload 解析失败且**静默 fail-open**(同目录 `stop-dialog-guard.py` 已因此"白排查一天")。
|
含中文的 payload 解析失败且**静默 fail-open**(同目录 `stop-dialog-guard.py` 已因此"白排查一天")。
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|||||||
@@ -32,24 +32,27 @@ lock-guard-hook.py — 「无锁不许改库」的强制钩子(WorkBuddy PreTo
|
|||||||
|
|
||||||
退出码:始终 0(判定通过 JSON 输出表达);脚本自身异常也放行,绝不误伤。
|
退出码:始终 0(判定通过 JSON 输出表达);脚本自身异常也放行,绝不误伤。
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,19 @@
|
|||||||
(原写「5 条 hook 入口」,2026-10-01 起为 **7 条**:lock-guard ×2 / bash-output-guard / skill-load-guard /
|
(原写「5 条 hook 入口」,2026-10-01 起为 **7 条**:lock-guard ×2 / bash-output-guard / skill-load-guard /
|
||||||
stop-dialog-guard / **session-log-guard ×2**)。
|
stop-dialog-guard / **session-log-guard ×2**)。
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`)
|
||||||
|
# 查找顺序:向上 4 层 × (env/roots.env 优先 → 包根 roots.env 兼容) → 技能库那份。
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
if _os.path.isfile(_p):
|
if _os.path.isfile(_p):
|
||||||
try:
|
try:
|
||||||
with open(_p, encoding="utf-8") as _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
|||||||
@@ -41,13 +41,19 @@ AI 仍按常驻判据把两个**非门禁**问题("106 旧控制面要不要
|
|||||||
18:38 起本钩子与 `stop-dialog-guard` 就在真实用户发言上**热生效**,未重启宿主;`PreToolUse` 同类)。
|
18:38 起本钩子与 `stop-dialog-guard` 就在真实用户发言上**热生效**,未重启宿主;`PreToolUse` 同类)。
|
||||||
⇒ 改完仍应**喂一次模拟载荷端到端自证**,⛔ 别只改配置就宣布"修好了"。
|
⇒ 改完仍应**喂一次模拟载荷端到端自证**,⛔ 别只改配置就宣布"修好了"。
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`)
|
||||||
|
# 查找顺序:向上 4 层 × (env/roots.env 优先 → 包根 roots.env 兼容) → 技能库那份。
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
if _os.path.isfile(_p):
|
if _os.path.isfile(_p):
|
||||||
try:
|
try:
|
||||||
with open(_p, encoding="utf-8") as _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
|||||||
@@ -32,13 +32,19 @@
|
|||||||
含中文的 payload 解码即炸。本脚本现已改为走 `buffer` 显式 UTF-8(读写都加固),但**不要靠加固兜底**,
|
含中文的 payload 解码即炸。本脚本现已改为走 `buffer` 显式 UTF-8(读写都加固),但**不要靠加固兜底**,
|
||||||
装的时候也别再引入新雷。(2026-09-15 实测:`-S -E` 曾让本钩子"看起来从未被调用"整整一天。)
|
装的时候也别再引入新雷。(2026-09-15 实测:`-S -E` 曾让本钩子"看起来从未被调用"整整一天。)
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`)
|
||||||
|
# 查找顺序:向上 4 层 × (env/roots.env 优先 → 包根 roots.env 兼容) → 技能库那份。
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
if _os.path.isfile(_p):
|
if _os.path.isfile(_p):
|
||||||
try:
|
try:
|
||||||
with open(_p, encoding="utf-8") as _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
|||||||
@@ -49,17 +49,21 @@ except Exception:
|
|||||||
# ① stdin 的 `payload.cwd`(宿主一定给)
|
# ① stdin 的 `payload.cwd`(宿主一定给)
|
||||||
# ② `roots.env` 的 `DSH_WS_ROOT`(副本专属,⛔ 不能只靠宿主 env)
|
# ② `roots.env` 的 `DSH_WS_ROOT`(副本专属,⛔ 不能只靠宿主 env)
|
||||||
def _sm_load_roots() -> None:
|
def _sm_load_roots() -> None:
|
||||||
"""把 `roots.env` 以 setdefault 注入 env(宿主 env 优先,⛔ 不覆盖)。"""
|
"""把本包 `env/roots.env` 以 setdefault 注入 env(宿主 env 优先,⛔ 不覆盖)。"""
|
||||||
p = SKILL / "roots.env"
|
cands = [SKILL / "env" / "roots.env", SKILL / "roots.env"]
|
||||||
try:
|
_cfg = os.environ.get("CODEBUDDY_CONFIG_DIR") or os.path.expanduser("~/.workbuddy")
|
||||||
for line in p.read_text(encoding="utf-8").splitlines():
|
cands.append(Path(_cfg) / "skills" / "session-mechanism" / "env" / "roots.env")
|
||||||
line = line.strip()
|
for p in cands:
|
||||||
if not line or line.startswith("#") or "=" not in line:
|
try:
|
||||||
continue
|
for line in p.read_text(encoding="utf-8").splitlines():
|
||||||
k, v = line.split("=", 1)
|
line = line.strip()
|
||||||
os.environ.setdefault(k.strip(), v.strip())
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
except Exception:
|
continue
|
||||||
pass
|
k, v = line.split("=", 1)
|
||||||
|
os.environ.setdefault(k.strip(), v.strip())
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
**已在 ⇒ 新起的立刻让位退出**(幂等,⛔ 不会"叠加成两条")。
|
**已在 ⇒ 新起的立刻让位退出**(幂等,⛔ 不会"叠加成两条")。
|
||||||
|
|
||||||
**一键关掉(新增能力)**:本脚本支持 `off` ⇒ 一个动作做完四件事:
|
**一键关掉(新增能力)**:本脚本支持 `off` ⇒ 一个动作做完四件事:
|
||||||
① 关两区开关 ② 禁用所有相关计划任务 ③ 杀掉所有相关进程 ④ 打印复核结果
|
① 关各区开关 ② 禁用所有相关计划任务 ③ 杀掉所有相关进程 ④ 打印复核结果
|
||||||
⇒ ⛔ 不再需要"禁用任务 + 杀进程"分开记(10-05 就是因为只禁任务没杀进程,以为停了其实没停)。
|
⇒ ⛔ 不再需要"禁用任务 + 杀进程"分开记(10-05 就是因为只禁任务没杀进程,以为停了其实没停)。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
@@ -43,16 +43,78 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
PYW = r"E:\ProgramData\.workbuddy\binaries\python\versions\3.13.12\pythonw.exe"
|
|
||||||
AIROOT = r"E:\ProgramData\AIProject"
|
|
||||||
|
|
||||||
# 🔴 登记制:工作区 → 保活任务名(⛔ 只在这里写一份)
|
# 🔴 2026-10-11:环境配置外置(「脚本是脚本 配置是配置」)⇒ 本包 `env/roots.env`。
|
||||||
WS = {
|
# ⛔ 本文件**不写死**任何本机路径 / 本机工作区清单。
|
||||||
"ai1net-dsh-server": "collabd-keepalive-ai1net-dsh-server",
|
def _sm_load_roots():
|
||||||
"vibe-product": "collabd-keepalive-vibe-product",
|
import os as _os
|
||||||
}
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
# 看板:全局只一条
|
_cands = []
|
||||||
BOARD_PY = r"E:\ProgramData\.workbuddy\skills\session-mechanism\scripts\board.py"
|
for _up in range(4):
|
||||||
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
# 🔴 本机技能库那份兜底:工作区副本(`<ws>/.workbuddy/collab/`)靠它拿到「这台机器」的配置
|
||||||
|
# ⇒ 全机单一真源(副本目录里⛔ 不放第二份,避免各区分叉)。
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
|
if _os.path.isfile(_p):
|
||||||
|
try:
|
||||||
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
for _ln in _f:
|
||||||
|
_ln = _ln.strip()
|
||||||
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
|
_k, _v = _ln.split("=", 1)
|
||||||
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
_sm_load_roots()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_airoot() -> str:
|
||||||
|
"""工作区根所在的**父目录**。**DSH_AIROOT 优先**(`env/roots.env` 注入);
|
||||||
|
兜底 = `DSH_WS_ROOT` 的父目录。⛔ 不在代码里写死盘符/目录。"""
|
||||||
|
cand = (os.environ.get("DSH_AIROOT") or "").strip()
|
||||||
|
if not cand:
|
||||||
|
ws = (os.environ.get("DSH_WS_ROOT") or "").strip()
|
||||||
|
if ws:
|
||||||
|
cand = os.path.dirname(ws.rstrip("/\\"))
|
||||||
|
return os.path.normpath(cand) if cand else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_workspace_names(airoot: str) -> list:
|
||||||
|
"""**自动发现**要管的工作区名:扫 `AIROOT` 下装了 `.workbuddy/collab/` 的目录。"""
|
||||||
|
if not airoot or not os.path.isdir(airoot):
|
||||||
|
return []
|
||||||
|
return sorted(n for n in os.listdir(airoot)
|
||||||
|
if os.path.isdir(os.path.join(airoot, n, ".workbuddy", "collab")))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_pyw() -> str:
|
||||||
|
"""`pythonw.exe`:优先**当前解释器**(`sys.executable`);其次宿主 env;
|
||||||
|
兜底才在同目录**探测**兄弟 `pythonw.exe`。⛔ 不写死版本目录。"""
|
||||||
|
exe = os.path.abspath(sys.executable or "")
|
||||||
|
if exe and exe.lower().endswith("pythonw.exe") and os.path.isfile(exe):
|
||||||
|
return exe
|
||||||
|
for _k in ("DSH_PYTHONW", "PYTHONW", "DSH_PYTHON"):
|
||||||
|
_v = (os.environ.get(_k) or "").strip()
|
||||||
|
if _v and os.path.isfile(_v):
|
||||||
|
return _v
|
||||||
|
_sib = os.path.join(os.path.dirname(exe), "pythonw.exe") if exe else ""
|
||||||
|
if _sib and os.path.isfile(_sib):
|
||||||
|
return _sib
|
||||||
|
return exe or "pythonw.exe"
|
||||||
|
|
||||||
|
|
||||||
|
AIROOT = _resolve_airoot()
|
||||||
|
PYW = _resolve_pyw()
|
||||||
|
|
||||||
|
# 🔴 工作区名 → 保活任务名:**由目录名推导**(`collabd-keepalive-<名>`)—— 原为写死字典 ⇒ 换机器/改名即失效。
|
||||||
|
WS = {n: "collabd-keepalive-" + n for n in _discover_workspace_names(AIROOT)}
|
||||||
|
# 看板:全局只一条;**技能包内相对定位**(⛔ 不写死绝对路径)
|
||||||
|
BOARD_PY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "board.py")
|
||||||
BOARD_TASK = "dsh-board-keepalive"
|
BOARD_TASK = "dsh-board-keepalive"
|
||||||
BOARD_PORT = 20099
|
BOARD_PORT = 20099
|
||||||
|
|
||||||
@@ -245,13 +307,13 @@ def main():
|
|||||||
if not APPLY and CMD in ("on", "off"):
|
if not APPLY and CMD in ("on", "off"):
|
||||||
print("(干跑)将要执行:%s —— 加 --apply 真做" % CMD)
|
print("(干跑)将要执行:%s —— 加 --apply 真做" % CMD)
|
||||||
if CMD == "off":
|
if CMD == "off":
|
||||||
print(" ① 关两区开关 ② 禁用 %d 个计划任务 ③ 杀相关进程 ④ 复核"
|
print(" ① 关各区开关 ② 禁用 %d 个计划任务 ③ 杀相关进程 ④ 复核"
|
||||||
% (len(WS) + 1 + len(OLD_TASKS)))
|
% (len(WS) + 1 + len(OLD_TASKS)))
|
||||||
else:
|
else:
|
||||||
for ws, t in WS.items():
|
for ws, t in WS.items():
|
||||||
print(" 建任务 %s → 每 %d 分钟 pythonw collabd.py --supervise" % (t, INTERVAL_MIN))
|
print(" 建任务 %s → 每 %d 分钟 pythonw collabd.py --supervise" % (t, INTERVAL_MIN))
|
||||||
print(" 建任务 %s → pythonw board.py --serve %d" % (BOARD_TASK, BOARD_PORT))
|
print(" 建任务 %s → pythonw board.py --serve %d" % (BOARD_TASK, BOARD_PORT))
|
||||||
print(" 禁用旧任务 %d 个;两区开关置 on" % len(OLD_TASKS))
|
print(" 禁用旧任务 %d 个;已发现区的开关置 on" % len(OLD_TASKS))
|
||||||
return 0
|
return 0
|
||||||
if CMD == "status":
|
if CMD == "status":
|
||||||
cmd_status()
|
cmd_status()
|
||||||
|
|||||||
@@ -34,10 +34,15 @@
|
|||||||
# 退出码:0 = 放行;1 = 命中硬冲突/硬判定;2 = 环境错误
|
# 退出码:0 = 放行;1 = 命中硬冲突/硬判定;2 = 环境错误
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
|
# 首选 env/roots.env;并兼容旧的包根 roots.env(拷贝未同步时照旧能跑)。
|
||||||
_SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
_SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
||||||
[ -f "$_SM_PKG/roots.env" ] && . "$_SM_PKG/roots.env"
|
if [ -f "$_SM_PKG/env/roots.env" ]; then
|
||||||
|
. "$_SM_PKG/env/roots.env"
|
||||||
|
elif [ -f "$_SM_PKG/roots.env" ]; then
|
||||||
|
. "$_SM_PKG/roots.env"
|
||||||
|
fi
|
||||||
ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"
|
ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"
|
||||||
cd "$ROOT" || exit 2
|
cd "$ROOT" || exit 2
|
||||||
LOCKDIR="$ROOT/05-交接单/.doing-"
|
LOCKDIR="$ROOT/05-交接单/.doing-"
|
||||||
|
|||||||
@@ -8,13 +8,19 @@
|
|||||||
|
|
||||||
用法:python 07-scripts/handoff-status.py
|
用法:python 07-scripts/handoff-status.py
|
||||||
"""
|
"""
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`)
|
||||||
|
# 查找顺序:向上 4 层 × (env/roots.env 优先 → 包根 roots.env 兼容) → 技能库那份。
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
if _os.path.isfile(_p):
|
if _os.path.isfile(_p):
|
||||||
try:
|
try:
|
||||||
with open(_p, encoding="utf-8") as _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
|||||||
@@ -44,9 +44,16 @@ from pathlib import Path
|
|||||||
|
|
||||||
# ── 根目录定位(与包内其它脚本同款:宿主 env > `roots.env` 兜底)────────────
|
# ── 根目录定位(与包内其它脚本同款:宿主 env > `roots.env` 兜底)────────────
|
||||||
def _sm_load_roots() -> None:
|
def _sm_load_roots() -> None:
|
||||||
|
"""把本包 `env/roots.env` 以 setdefault 注入 env(宿主 env 优先,⛔ 不覆盖)。"""
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
cands = []
|
||||||
for up in range(4):
|
for up in range(4):
|
||||||
p = os.path.normpath(os.path.join(here, *([".."] * up), "roots.env"))
|
base = os.path.normpath(os.path.join(here, *([".."] * up)))
|
||||||
|
for rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
cands.append(os.path.join(base, *rel))
|
||||||
|
conf = os.environ.get("CODEBUDDY_CONFIG_DIR") or os.path.expanduser("~/.workbuddy")
|
||||||
|
cands.append(os.path.join(conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for p in cands:
|
||||||
if os.path.isfile(p):
|
if os.path.isfile(p):
|
||||||
try:
|
try:
|
||||||
with open(p, encoding="utf-8") as f:
|
with open(p, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ if [ -z "$ME" ] || [ $# -eq 0 ]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`;兼容包根旧位置)
|
||||||
_SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
_SM_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
_SM_PKG="$(cd "$_SM_HERE/../.." && pwd)"
|
||||||
[ -f "$_SM_PKG/roots.env" ] && . "$_SM_PKG/roots.env"
|
if [ -f "$_SM_PKG/env/roots.env" ]; then . "$_SM_PKG/env/roots.env"
|
||||||
|
elif [ -f "$_SM_PKG/roots.env" ]; then . "$_SM_PKG/roots.env"; fi
|
||||||
DOCS_ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"
|
DOCS_ROOT="${DSH_DOCS_ROOT:-$(cd "$_SM_HERE/.." && pwd)}"
|
||||||
CODE_ROOT="${DSH_CODE_REPO:-${DOCS_ROOT%/*}}" # ⛔ 不留盘符:代码库=文档库的上一级(纯 bash 展开,⛔ 不调 dirname)
|
CODE_ROOT="${DSH_CODE_REPO:-${DOCS_ROOT%/*}}" # ⛔ 不留盘符:代码库=文档库的上一级(纯 bash 展开,⛔ 不调 dirname)
|
||||||
WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用)
|
WS_ROOT="${DSH_WS_ROOT:-$(pwd)}" # ⛔ 不留盘符:默认=当前工作目录(本文件内未被使用)
|
||||||
|
|||||||
@@ -13,24 +13,27 @@
|
|||||||
python selftest.py -k 投递 # 只跑名字含"投递"的用例
|
python selftest.py -k 投递 # 只跑名字含"投递"的用例
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] 环境配置外置(由 install.py 生成);2026-10-11 起落在本包 `env/roots.env`
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
if _os.path.isfile(_p):
|
_p = _os.path.join(_base, *_rel)
|
||||||
try:
|
if _os.path.isfile(_p):
|
||||||
with open(_p, encoding="utf-8") as _f:
|
try:
|
||||||
for _ln in _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
_ln = _ln.strip()
|
for _ln in _f:
|
||||||
if _ln and not _ln.startswith("#") and "=" in _ln:
|
_ln = _ln.strip()
|
||||||
_k, _v = _ln.split("=", 1)
|
if _ln and not _ln.startswith("#") and "=" in _ln:
|
||||||
_os.environ.setdefault(_k.strip(), _v.strip())
|
_k, _v = _ln.split("=", 1)
|
||||||
except Exception:
|
_os.environ.setdefault(_k.strip(), _v.strip())
|
||||||
pass
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
|
||||||
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
|
||||||
@@ -5833,7 +5836,7 @@ def t_hook_reads_ws_from_stdin_cwd():
|
|||||||
|
|
||||||
⚖️ 同源判据的既有正确写法(⛔ 必须对齐,不能自创):
|
⚖️ 同源判据的既有正确写法(⛔ 必须对齐,不能自创):
|
||||||
`wb-result-hook.py` / `lock-guard-hook.py` 都是**先读 stdin 的
|
`wb-result-hook.py` / `lock-guard-hook.py` 都是**先读 stdin 的
|
||||||
`payload.cwd`**,env / `roots.env` 只作兜底。
|
`payload.cwd`**,env / `env/roots.env` 只作兜底。
|
||||||
|
|
||||||
🔴 判据要点:⛔ **不许只查"读没读 cwd"**——还要钉住
|
🔴 判据要点:⛔ **不许只查"读没读 cwd"**——还要钉住
|
||||||
① stdin **只读一次**(`sys.stdin.buffer.read()` 是一次性的)
|
① stdin **只读一次**(`sys.stdin.buffer.read()` 是一次性的)
|
||||||
@@ -6454,7 +6457,9 @@ def t_mirror_drift():
|
|||||||
(fx_cp / "a.py").write_text("A=1\n", encoding="utf-8")
|
(fx_cp / "a.py").write_text("A=1\n", encoding="utf-8")
|
||||||
(fx_cp / "sub" / "b.py").write_text("B=1\n# TAMPER\n", encoding="utf-8") # 待更新
|
(fx_cp / "sub" / "b.py").write_text("B=1\n# TAMPER\n", encoding="utf-8") # 待更新
|
||||||
(fx_cp / "r_stale.py").write_text("STALE\n", encoding="utf-8") # 真残留
|
(fx_cp / "r_stale.py").write_text("STALE\n", encoding="utf-8") # 真残留
|
||||||
(fx_cp / "roots.env").write_text("DSH_WS_ROOT=/fixture/ONLY\n", encoding="utf-8") # 白名单
|
# 🔴 2026-10-11:白名单落点在 `env/roots.env`(配置与代码分家)⇒ 夹具照真实结构摆
|
||||||
|
(fx_cp / "env").mkdir(parents=True, exist_ok=True)
|
||||||
|
(fx_cp / "env" / "roots.env").write_text("DSH_WS_ROOT=/fixture/ONLY\n", encoding="utf-8") # 白名单
|
||||||
|
|
||||||
g, c, only_c, only_g, changed = wm.diff(fx_src, fx_cp)
|
g, c, only_c, only_g, changed = wm.diff(fx_src, fx_cp)
|
||||||
det = [
|
det = [
|
||||||
@@ -6464,7 +6469,7 @@ def t_mirror_drift():
|
|||||||
"c.py" in only_g and len(only_g) == 1),
|
"c.py" in only_g and len(only_g) == 1),
|
||||||
("抓到**待更新**(两边都有但内容不同)",
|
("抓到**待更新**(两边都有但内容不同)",
|
||||||
"sub/b.py" in changed and "a.py" not in changed),
|
"sub/b.py" in changed and "a.py" not in changed),
|
||||||
("**没把白名单当漂移**(roots.env 不该出现在任一差异里)",
|
("**没把白名单当漂移**(env/roots.env 不该出现在任一差异里)",
|
||||||
not any("roots.env" in x for x in (only_c + only_g + changed))),
|
not any("roots.env" in x for x in (only_c + only_g + changed))),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6480,10 +6485,10 @@ def t_mirror_drift():
|
|||||||
and (fx_cp / "c.py").read_bytes() == (fx_src / "c.py").read_bytes()),
|
and (fx_cp / "c.py").read_bytes() == (fx_src / "c.py").read_bytes()),
|
||||||
("同步后**篡改已被覆盖回全局版**",
|
("同步后**篡改已被覆盖回全局版**",
|
||||||
(fx_cp / "sub" / "b.py").read_bytes() == (fx_src / "sub" / "b.py").read_bytes()),
|
(fx_cp / "sub" / "b.py").read_bytes() == (fx_src / "sub" / "b.py").read_bytes()),
|
||||||
("🔴 **白名单内容原样保住**(roots.env 仍写 /fixture/ONLY,"
|
("🔴 **白名单内容原样保住**(env/roots.env 仍写 /fixture/ONLY,"
|
||||||
"⛔ 被冲回全局值就会让副本去读写别人的仓库)",
|
"⛔ 被冲回全局值就会让副本去读写别人的仓库)",
|
||||||
(fx_cp / "roots.env").exists()
|
(fx_cp / "env" / "roots.env").exists()
|
||||||
and "/fixture/ONLY" in (fx_cp / "roots.env").read_text(encoding="utf-8")),
|
and "/fixture/ONLY" in (fx_cp / "env" / "roots.env").read_text(encoding="utf-8")),
|
||||||
("**幂等**:再 sync 一次 rc=0", wm.do_sync(fx_src, fx_cp) == 0),
|
("**幂等**:再 sync 一次 rc=0", wm.do_sync(fx_src, fx_cp) == 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6493,7 +6498,7 @@ def t_mirror_drift():
|
|||||||
wsrc = mp.read_text(encoding="utf-8")
|
wsrc = mp.read_text(encoding="utf-8")
|
||||||
det += [
|
det += [
|
||||||
("覆盖循环里有**白名单守卫** `if k in KEEP: continue`"
|
("覆盖循环里有**白名单守卫** `if k in KEEP: continue`"
|
||||||
"(⛔ 拿掉它 ⇒ roots.env 被全局值覆盖 ⇒ 副本去读写别人的仓库)",
|
"(⛔ 拿掉它 ⇒ env/roots.env 被全局值覆盖 ⇒ 副本去读写别人的仓库)",
|
||||||
re.search(r"for k in sorted\(g\):\s*\n\s*if k in KEEP:\s*\n\s*continue", wsrc) is not None),
|
re.search(r"for k in sorted\(g\):\s*\n\s*if k in KEEP:\s*\n\s*continue", wsrc) is not None),
|
||||||
("`--sync` 先**备份再删**(⛔ 破坏性动作前必须留退路)",
|
("`--sync` 先**备份再删**(⛔ 破坏性动作前必须留退路)",
|
||||||
wsrc.index("copytree") < wsrc.index("unlink()")),
|
wsrc.index("copytree") < wsrc.index("unlink()")),
|
||||||
@@ -6543,30 +6548,31 @@ def t_mirror_drift():
|
|||||||
]
|
]
|
||||||
|
|
||||||
# ③ 有备份时**能**捞回:造一次"先 sync(留备份)→ 再删 → 再 sync"
|
# ③ 有备份时**能**捞回:造一次"先 sync(留备份)→ 再删 → 再 sync"
|
||||||
(fx_cp2 / "roots.env").write_text("DSH_WS_ROOT=/fixture/BACKUPED\n", encoding="utf-8")
|
(fx_cp2 / "env").mkdir(parents=True, exist_ok=True)
|
||||||
|
(fx_cp2 / "env" / "roots.env").write_text("DSH_WS_ROOT=/fixture/BACKUPED\n", encoding="utf-8")
|
||||||
(fx_cp2 / "sub" / "b.py").write_text("B=1\n# DIFF\n", encoding="utf-8") # 制造漂移 ⇒ 触发备份
|
(fx_cp2 / "sub" / "b.py").write_text("B=1\n# DIFF\n", encoding="utf-8") # 制造漂移 ⇒ 触发备份
|
||||||
wm.do_sync(fx_src2, fx_cp2) # ← 这次备份里有 roots.env
|
wm.do_sync(fx_src2, fx_cp2) # ← 这次备份里有 env/roots.env
|
||||||
(fx_cp2 / "roots.env").unlink()
|
(fx_cp2 / "env" / "roots.env").unlink()
|
||||||
rc_bk = wm.do_sync(fx_src2, fx_cp2)
|
rc_bk = wm.do_sync(fx_src2, fx_cp2)
|
||||||
det += [
|
det += [
|
||||||
("🔴 **有备份时,白名单被删后 `--sync` 能捞回**"
|
("🔴 **有备份时,白名单被删后 `--sync` 能捞回**"
|
||||||
"(`roots.env` 丢失 ⇒ 副本按位置推导去读写**别的仓库**,静默且难查)",
|
"(`env/roots.env` 丢失 ⇒ 副本按位置推导去读写**别的仓库**,静默且难查)",
|
||||||
rc_bk == 0 and (fx_cp2 / "roots.env").exists()
|
rc_bk == 0 and (fx_cp2 / "env" / "roots.env").exists()
|
||||||
and "/fixture/BACKUPED" in (fx_cp2 / "roots.env").read_text(encoding="utf-8")),
|
and "/fixture/BACKUPED" in (fx_cp2 / "env" / "roots.env").read_text(encoding="utf-8")),
|
||||||
]
|
]
|
||||||
shutil.rmtree(str(fx_src2.parent), ignore_errors=True)
|
shutil.rmtree(str(fx_src2.parent), ignore_errors=True)
|
||||||
shutil.rmtree(str(fx_src.parent), ignore_errors=True)
|
shutil.rmtree(str(fx_src.parent), ignore_errors=True)
|
||||||
return det
|
return det
|
||||||
|
|
||||||
|
|
||||||
@case("副本同步:`roots.env` 白名单**只按文件名**判(⛔ 换副本不许靠「我记得跳过哪个」)")
|
@case("副本同步:`env/roots.env` 白名单**只按路径**判(⛔ 换副本不许靠「我记得跳过哪个」)")
|
||||||
def t_mirror_keep_by_name():
|
def t_mirror_keep_by_name():
|
||||||
import importlib.util
|
import importlib.util
|
||||||
spec = importlib.util.spec_from_file_location("wm_selftest2", HERE / "workspace_mirror.py")
|
spec = importlib.util.spec_from_file_location("wm_selftest2", HERE / "workspace_mirror.py")
|
||||||
wm = importlib.util.module_from_spec(spec)
|
wm = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(wm)
|
spec.loader.exec_module(wm)
|
||||||
return [
|
return [
|
||||||
("白名单含 `roots.env`(⛔ 覆盖它 ⇒ 副本读写到别的仓库)", "roots.env" in wm.KEEP),
|
("白名单含 `env/roots.env`(⛔ 覆盖它 ⇒ 副本读写到别的仓库)", "env/roots.env" in wm.KEEP),
|
||||||
("白名单含 `副本使用说明.md`(副本独有的口径文档)", "副本使用说明.md" in wm.KEEP),
|
("白名单含 `副本使用说明.md`(副本独有的口径文档)", "副本使用说明.md" in wm.KEEP),
|
||||||
("白名单含 `install.log`(副本自己的安装历史)", "install.log" in wm.KEEP),
|
("白名单含 `install.log`(副本自己的安装历史)", "install.log" in wm.KEEP),
|
||||||
("白名单是**元组**(⛔ 散着写容易漏)", isinstance(wm.KEEP, tuple)),
|
("白名单是**元组**(⛔ 散着写容易漏)", isinstance(wm.KEEP, tuple)),
|
||||||
|
|||||||
@@ -41,21 +41,25 @@ import sqlite3
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# ── 根目录外置(roots.env 由 install.py 生成;宿主 env 优先,本文件兜底) ──────
|
# ── 环境配置外置(由 install.py 生成;宿主 env 优先,本文件兜底)
|
||||||
|
# 🔴 2026-10-11:「脚本是脚本 配置是配置」⇒ 配置落本包 `env/`。
|
||||||
|
# 首选 `env/roots.env`;并兼容旧的包根 `roots.env`(拷贝未同步时照旧能跑)。 ──────
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
for up in range(4):
|
for up in range(4):
|
||||||
p = os.path.normpath(os.path.join(here, *([".."] * up), "roots.env"))
|
base = os.path.normpath(os.path.join(here, *([".."] * up)))
|
||||||
if os.path.isfile(p):
|
for rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
try:
|
p = os.path.join(base, *rel)
|
||||||
for ln in io.open(p, encoding="utf-8"):
|
if os.path.isfile(p):
|
||||||
ln = ln.strip()
|
try:
|
||||||
if ln and not ln.startswith("#") and "=" in ln:
|
for ln in io.open(p, encoding="utf-8"):
|
||||||
k, v = ln.split("=", 1)
|
ln = ln.strip()
|
||||||
os.environ.setdefault(k.strip(), v.strip())
|
if ln and not ln.startswith("#") and "=" in ln:
|
||||||
except Exception:
|
k, v = ln.split("=", 1)
|
||||||
pass
|
os.environ.setdefault(k.strip(), v.strip())
|
||||||
return
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
_sm_load_roots()
|
_sm_load_roots()
|
||||||
|
|
||||||
ARGV = sys.argv[1:]
|
ARGV = sys.argv[1:]
|
||||||
|
|||||||
@@ -20,13 +20,19 @@
|
|||||||
(快照/代码版本互相"打架")。此处按端口逐个探 `/healthz` 签名,把**所有**实例列出来并停掉。
|
(快照/代码版本互相"打架")。此处按端口逐个探 `/healthz` 签名,把**所有**实例列出来并停掉。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
|
# [session-mechanism] roots.env 外置(由 install.py 生成 ⇒ `<包>/env/roots.env`)
|
||||||
|
# 查找顺序:向上 4 层 × (env/roots.env 优先 → 包根 roots.env 兼容) → 技能库那份。
|
||||||
def _sm_load_roots():
|
def _sm_load_roots():
|
||||||
import os as _os
|
import os as _os
|
||||||
_here = _os.path.dirname(_os.path.abspath(__file__))
|
_here = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
|
_cands = []
|
||||||
for _up in range(4):
|
for _up in range(4):
|
||||||
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
|
_base = _os.path.normpath(_os.path.join(_here, *([".."] * _up)))
|
||||||
_p = _os.path.normpath(_p)
|
for _rel in (("env", "roots.env"), ("roots.env",)):
|
||||||
|
_cands.append(_os.path.join(_base, *_rel))
|
||||||
|
_conf = _os.environ.get("CODEBUDDY_CONFIG_DIR") or _os.path.expanduser("~/.workbuddy")
|
||||||
|
_cands.append(_os.path.join(_conf, "skills", "session-mechanism", "env", "roots.env"))
|
||||||
|
for _p in _cands:
|
||||||
if _os.path.isfile(_p):
|
if _os.path.isfile(_p):
|
||||||
try:
|
try:
|
||||||
with open(_p, encoding="utf-8") as _f:
|
with open(_p, encoding="utf-8") as _f:
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ CB = os.path.join(HERE, "collabd.py")
|
|||||||
CFG = os.path.join(HERE, "collabd.config.json")
|
CFG = os.path.join(HERE, "collabd.config.json")
|
||||||
|
|
||||||
# 🔴 活动环境目录(宿主库所在)—— 这是**旧看守里有、新形态里被我漏掉**的那一条。
|
# 🔴 活动环境目录(宿主库所在)—— 这是**旧看守里有、新形态里被我漏掉**的那一条。
|
||||||
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", r"E:\ProgramData\.workbuddy")
|
# 🔴 2026-10-11:默认值改成 `~/.workbuddy` 形态(⛔ 不写死本机盘符)——
|
||||||
|
# 跟着**当前用户**走;宿主 env 里已有的值优先(setdefault)。
|
||||||
|
os.environ.setdefault("CODEBUDDY_CONFIG_DIR", os.path.expanduser("~/.workbuddy"))
|
||||||
os.environ["COLLABD_CONFIG"] = CFG
|
os.environ["COLLABD_CONFIG"] = CFG
|
||||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
os.environ["PYTHONUNBUFFERED"] = "1"
|
os.environ["PYTHONUNBUFFERED"] = "1"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
🔴 为什么必须有这个工具(不是"又写个脚本")
|
🔴 为什么必须有这个工具(不是"又写个脚本")
|
||||||
· 副本是**整目录拷贝**出去的 ⇒ 全局删掉/新增的文件,副本**不知道**,会静默留着
|
· 副本是**整目录拷贝**出去的 ⇒ 全局删掉/新增的文件,副本**不知道**,会静默留着
|
||||||
· 副本的 `roots.env` 是**工作区专属**的 ⇒ ⛔ 不能盲目整目录覆盖(会把工作区指向冲回别人)
|
· 副本的 `env/roots.env` 是**工作区专属**的 ⇒ ⛔ 不能盲目整目录覆盖(会把工作区指向冲回别人)
|
||||||
· 于是每次同步都变成"人肉算差异+人肉挑例外" ⇒ **迟早漏**(实测已漏过:mut_run.py 差点没补上)
|
· 于是每次同步都变成"人肉算差异+人肉挑例外" ⇒ **迟早漏**(实测已漏过:mut_run.py 差点没补上)
|
||||||
⇒ 正确解法:**让"例外的例外"由工具自己知道**,并且**能自证没漂移**。
|
⇒ 正确解法:**让"例外的例外"由工具自己知道**,并且**能自证没漂移**。
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
python workspace_mirror.py --list-keep
|
python workspace_mirror.py --list-keep
|
||||||
|
|
||||||
🔴 三条白名单(**有意保留副本自己的东西,不是残留**)
|
🔴 三条白名单(**有意保留副本自己的东西,不是残留**)
|
||||||
1. `roots.env` —— 工作区专属键。被覆盖 ⇒ 副本会去读写**另一个仓库**
|
1. `env/roots.env` —— 工作区专属键(2026-10-11 起落本包 `env/`)。被覆盖 ⇒ 副本会去读写**另一个仓库**
|
||||||
(`setdefault` 兜底:宿主 env 没设时注入本文件的值)
|
(`setdefault` 兜底:宿主 env 没设时注入本文件的值)
|
||||||
2. `副本使用说明.md` —— 副本独有的口径文档
|
2. `副本使用说明.md` —— 副本独有的口径文档
|
||||||
3. `install.log` —— 副本自己的安装历史(追加式)
|
3. `install.log` —— 副本自己的安装历史(追加式)
|
||||||
@@ -50,7 +50,7 @@ SELF_SUFFIX = (".pyc", ".pyo", ".tmp", ".swp", ".mirtmp")
|
|||||||
# 而 SKILL.md 应当**跟着全局走**(它是版本的一部分)⇒ 只需在残留判定里豁免。
|
# 而 SKILL.md 应当**跟着全局走**(它是版本的一部分)⇒ 只需在残留判定里豁免。
|
||||||
IDENT_MARK = "SKILL.md"
|
IDENT_MARK = "SKILL.md"
|
||||||
NEVER_DELETE = (IDENT_MARK,) # ⛔ 算残留时跳过 ⇒ 删了副本就废了
|
NEVER_DELETE = (IDENT_MARK,) # ⛔ 算残留时跳过 ⇒ 删了副本就废了
|
||||||
KEEP = ("roots.env", "副本使用说明.md", "install.log")
|
KEEP = ("env/roots.env", "副本使用说明.md", "install.log")
|
||||||
# 备份目录名前缀(`_find_in_prior_backups` 靠它认出"这是历史备份" ⇒ ⛔ 改名要同步改那里)
|
# 备份目录名前缀(`_find_in_prior_backups` 靠它认出"这是历史备份" ⇒ ⛔ 改名要同步改那里)
|
||||||
BK_PREFIX = "session-mechanism-副本备份-"
|
BK_PREFIX = "session-mechanism-副本备份-"
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ def _find_in_prior_backups(keep_lost, bk_root: Path):
|
|||||||
原实现把**本次刚建的备份**当兜底源 ⇒ 但 `copytree` 发生在**丢失之后**
|
原实现把**本次刚建的备份**当兜底源 ⇒ 但 `copytree` 发生在**丢失之后**
|
||||||
⇒ 那份备份里**必然也没有**这个文件 ⇒ 兜底永远捞不到。
|
⇒ 那份备份里**必然也没有**这个文件 ⇒ 兜底永远捞不到。
|
||||||
⇒ 正解=翻**先前**留下的备份,按时间**从新到旧**找第一份命中的。
|
⇒ 正解=翻**先前**留下的备份,按时间**从新到旧**找第一份命中的。
|
||||||
⛔⛔ **绝不拿全局那份顶替** —— 全局 `roots.env` 指向**别的仓库**,
|
⛔⛔ **绝不拿全局那份顶替** —— 全局 `env/roots.env` 指向**别的仓库**,
|
||||||
填进去 = 把"副本悄悄指错仓库"变成"副本明确指错仓库",⛔ 更坏。
|
填进去 = 把"副本悄悄指错仓库"变成"副本明确指错仓库",⛔ 更坏。
|
||||||
"""
|
"""
|
||||||
found = {}
|
found = {}
|
||||||
@@ -196,10 +196,10 @@ def do_sync(gpkg: Path, copy_dir: Path) -> int:
|
|||||||
print("⛔ %s 不像技能副本(没有 SKILL.md)⇒ 不动" % copy_dir)
|
print("⛔ %s 不像技能副本(没有 SKILL.md)⇒ 不动" % copy_dir)
|
||||||
return 2
|
return 2
|
||||||
# 🔴 2026-10-04 修:白名单**缺失**也算漂移。
|
# 🔴 2026-10-04 修:白名单**缺失**也算漂移。
|
||||||
# `diff()` 刻意排除 KEEP(免得每次都报它们)⇒ 副本的 `roots.env` 没了
|
# `diff()` 刻意排除 KEEP(免得每次都报它们)⇒ 副本的 `env/roots.env` 没了
|
||||||
# ⇒ 差异表里**一个字段都不动** ⇒ 早先直接走"无漂移,幂等返回",
|
# ⇒ 差异表里**一个字段都不动** ⇒ 早先直接走"无漂移,幂等返回",
|
||||||
# 白名单兜底层**永远没机会跑**。而这恰恰是最该救的场景:
|
# 白名单兜底层**永远没机会跑**。而这恰恰是最该救的场景:
|
||||||
# `roots.env` 丢了 ⇒ 副本按位置推导 ⇒ 去读写**别的仓库**(静默指错)。
|
# `env/roots.env` 丢了 ⇒ 副本按位置推导 ⇒ 去读写**别的仓库**(静默指错)。
|
||||||
# ⇒ 这里单独查一遍。
|
# ⇒ 这里单独查一遍。
|
||||||
# 🔴🔴 修第二版:「**全局侧有**」⛔ 不等于「副本**该**有」。
|
# 🔴🔴 修第二版:「**全局侧有**」⛔ 不等于「副本**该**有」。
|
||||||
# 实测踩到:全新副本本来就没有 `install.log`(从未安装过)⇒ 却被判成
|
# 实测踩到:全新副本本来就没有 `install.log`(从未安装过)⇒ 却被判成
|
||||||
@@ -217,8 +217,8 @@ def do_sync(gpkg: Path, copy_dir: Path) -> int:
|
|||||||
_, _, only_c, only_g, changed = diff(gpkg, copy_dir)
|
_, _, only_c, only_g, changed = diff(gpkg, copy_dir)
|
||||||
if keep_lost:
|
if keep_lost:
|
||||||
print("⛔ **白名单缺失**(副本历史里有过,现在没了):%s" % "、".join(keep_lost)
|
print("⛔ **白名单缺失**(副本历史里有过,现在没了):%s" % "、".join(keep_lost)
|
||||||
+ ("(⛔ `roots.env` 丢 ⇒ 副本会按位置推导去读写**别的仓库**)"
|
+ ("(⛔ `env/roots.env` 丢 ⇒ 副本会按位置推导去读写**别的仓库**)"
|
||||||
if "roots.env" in keep_lost else ""))
|
if "env/roots.env" in keep_lost else ""))
|
||||||
if not (only_c or only_g or changed or keep_lost):
|
if not (only_c or only_g or changed or keep_lost):
|
||||||
print("✅ 无漂移,⛔ 什么也不做(幂等)")
|
print("✅ 无漂移,⛔ 什么也不做(幂等)")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -107,6 +107,52 @@ done
|
|||||||
|
|
||||||
5、**回退后立刻提交推送**,并把体检结果写进提交信息。⛔ 别攒着 —— 攒着就会被下一次覆盖顺手带走。
|
5、**回退后立刻提交推送**,并把体检结果写进提交信息。⛔ 别攒着 —— 攒着就会被下一次覆盖顺手带走。
|
||||||
|
|
||||||
## 四、一句话
|
## 四、包结构:**脚本是脚本,配置是配置**(2026-10-11 立 · 全仓通用)
|
||||||
|
|
||||||
|
**每个需要环境配置的技能,都在自己的包内建一个 `env/` 文件夹**,专放「每台机器各不同」的值;
|
||||||
|
`scripts/`(代码)只放通用逻辑,值从 `env/` 读。
|
||||||
|
|
||||||
|
用户原话(2026-10-11):
|
||||||
|
- 19:22「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」
|
||||||
|
- 19:28「不用挪出 用env文件夹区分就行,重点是 脚本是脚本 配置是配置」
|
||||||
|
- 19:29「每个技能 需要环境配置的 都有自己的 env文件夹」
|
||||||
|
|
||||||
|
### 1、配置放哪
|
||||||
|
|
||||||
|
- **真配置** ⇒ `<技能包>/env/<名>.env`,本机生成、**不入库**
|
||||||
|
- **样例** ⇒ `<技能包>/env/<名>.env.example`,**入库**(只有占位符,无本机路径)
|
||||||
|
- 仓根 `.gitignore` 已有对应规则:`**/env/*` + `!**/env/*.example` ⇒ `env/` 下只放行 `*.example`
|
||||||
|
|
||||||
|
### 2、代码怎么写
|
||||||
|
|
||||||
|
- 脚本头部放一个 `_xx_load_env()`:从 `__file__` 向上找 `<包>/env/<名>.env`,逐行 `KEY=VALUE`
|
||||||
|
以 **setdefault** 注入 ⇒ **宿主 env 优先,文件兜底**(与本仓 `session-mechanism/scripts/**` 的
|
||||||
|
`_sm_load_roots()` 同一套写法,照抄即可)。
|
||||||
|
- 值一律**从 env 读**,代码里只留**通用兜底**(用户目录用
|
||||||
|
`os.environ.get("CODEBUDDY_CONFIG_DIR") or os.path.expanduser("~/.workbuddy")` 这类跟着机器走的写法)。
|
||||||
|
(依据:把本机路径/本机工作区清单写进入库代码 ⇒ 同步到别的机器会按不存在的路径建计划任务,用户 19:22 报的那次。)
|
||||||
|
- **同包内的兄弟文件**用 `Path(__file__).resolve().parent / "x.py"` 定位;解释器用 `sys.executable`
|
||||||
|
或它的同目录兄弟 `pythonw.exe`。
|
||||||
|
- 解析不到必需值时:**把原因写进日志 + 非零退出码**,让人一眼看出缺什么
|
||||||
|
(依据:静默兜底会去建指向空路径的任务,比报错难查得多)。
|
||||||
|
|
||||||
|
### 3、判断一个技能「需不需要建 `env/`」
|
||||||
|
|
||||||
|
问一句:**这个技能里有没有「换一台机器就该变」的值?**
|
||||||
|
- **有**(路径 / 端口 / 用户名 / 工作区清单 / 解释器位置)⇒ **建 `env/`**,把它们全挪进去
|
||||||
|
- **没有**(纯文档、纯算法、只读上游接口)⇒ **不建** —— 空目录只会变成噪声
|
||||||
|
|
||||||
|
### 4、首轮审计(2026-10-11 · 本目录工作区)
|
||||||
|
|
||||||
|
- ✅ **已建 `env/`**:`session-mechanism`(`env/roots.env`)|`browser-harness`(`env/bh.env`)
|
||||||
|
- ✅ **判为不需要**:`humanizer` `humanizer-zh`(纯文档,0 个脚本)|`oil-ui-pro` / `video-capture` /
|
||||||
|
`draw-ui` / `oil-motion` / `ai-daily-report`(有脚本,但零本机值)|`product-planning`(脚本里只有注释提到路径,无实际读取)
|
||||||
|
- ⚠️ **本仓白名单之外的目录里也有同类值**(2026-10-09 已撤出仓库,本轮不动):
|
||||||
|
`dsh-local-env/references/dsh-env-bootstrap/resident-rules.py`(有本机常量)、
|
||||||
|
`dsh-workflow/references/dsh-auto-handoff-chain/audit-mirror.py`(默认根写死)、
|
||||||
|
`dsh-knowledge/references/dsh-knowledge-upkeep/split_skill.py`(仅文档示例里的路径)。
|
||||||
|
⇒ 将来若要收编,按上面第 1~2 条改。
|
||||||
|
|
||||||
|
## 五、一句话
|
||||||
|
|
||||||
**新稿是意图,不是内容。落地 = 把意图逐节落到当前版本上,⛔ 不是把文件换掉。**
|
**新稿是意图,不是内容。落地 = 把意图逐节落到当前版本上,⛔ 不是把文件换掉。**
|
||||||
Reference in new issue
Block a user