session-mechanism:两个看板定名(会话看板 / 项目看板)+ 包内代码一律相对路径

用户令(两句,同一轮):
  ①「会话技能中 能用相对路径的都要用相对路径」
  ②「把会话的 项目目标看板 改成 项目看板,
     项目看板 和 会话技能的任务看板 都重新命名 会话看板、项目看板」

## 一、改名:项目管理界面 → 项目看板
* 目录:boards/项目管理界面/ → boards/项目看板/(会话看板不变)
* 包内引用:路径性(3 行)+ 展示名(6 处,含 argparse description、启动横幅、
  错误日志前缀、模块 docstring、页面 <title> 与 .brand)
* HTML 标题:<title>项目管理 · 执行任务</title> → <title>项目看板 · 执行任务</title>

## 二、相对路径(新立第 11 条禁令)
判据=「这个根能不能从『我自己的位置』推出来」:
* 能(本包自己的目录)⇒ 从 __file__ 上跳推导
* 不能(工作区根 / 配置目录 / 文档库根)⇒ 读本包 env/roots.env
* 推不出来 ⇒ 如实报错 + 留痕(⛔ 不许静默扫空)

落点(共 4 处代码级硬编码,全部清掉):
* boards/项目看板/project-board.py:新增 _sm_load_roots();
  技能根改 HERE.parent.parent;WS_ROOTS 从 DSH_AIROOT 推导;
  推不出根时打警告 + 写 logs/project-board.err.log(原为静默扫出 0 个项目)
* scripts/selftest.py:生产区定位改为扫 DSH_AIROOT 下兄弟区(⛔ 不写死区名);
  两处测试假路径去掉盘符

⛔ 文档/注释里的绝对路径保持原样(举例、历史取证照写 —— 那是证据)。

## 验收
* 换机场景实测(技能包拷到 /tmp、不带 env/roots.env):
  无任何配置 ⇒ 如实报错;只给 DSH_AIROOT ⇒ 正常扫到 4 个项目
* selftest --filter 看板:PASS 110 / FAIL 0(报告型 1 条不计入)
* 项目看板服务:pid 40776 · HTTP 200 · <title>项目看板 · 执行任务</title>
  · /api/projects 出数据 · /healthz ok
* py_compile 三份改动文件全过
This commit is contained in:
admin committed 2026-10-14 16:33:28 +08:00
1 parent 52566c79f9
commit fe22f4cebe
5 files changed
+121 -19

No files matched your search

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""项目管理界面的**启动器**(计划任务动作指向它,⛔ 不许直起 `project-board.py`)。
"""项目看板的**启动器**(计划任务动作指向它,⛔ 不许直起 `project-board.py`)。
## 为什么必须有这一层(同族 P0-73/74)
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>项目管理 · 执行任务</title>
<title>项目看板 · 执行任务</title>
<style>
:root{
--bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --line:#2a2f3a;
@@ -249,7 +249,7 @@
<body>
<header>
<div class="hrow">
<div class="brand">项目管理</div>
<div class="brand">项目看板</div>
<select id="psel" aria-label="选择工作区" title="选择工作区"></select>
<div class="spacer"></div>
<span class="stamp" id="stamp">加载中…</span>
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""项目管理界面 —— 管**本机所有工作区**的项目(照 dsh-task-board 的「项目 × 卡片」模式)。
"""项目看板 —— 管**本机所有工作区**的项目(照 dsh-task-board 的「项目 × 卡片」模式)。
## 这是什么(用户原话)
@@ -64,18 +64,71 @@ from pathlib import Path
HERE = Path(__file__).resolve().parent
PAGE = HERE / "project-board.html"
# 🔴 扫哪些根目录找工作区(`;` 分隔;可用环境变量覆盖)
WS_ROOTS = [Path(p) for p in (
os.environ.get("DSH_PM_BOARD_ROOTS") or "E:/ProgramData/AIProject"
).split(";") if p.strip()]
def _sm_load_roots():
"""把本包 `env/roots.env` 注入进程 env(宿主已有的**不动** ⇒ `setdefault`)。
🔴🔴 2026-10-14(用户令:「会话技能中 能用相对路径的都要用相对路径」):
本文件原先把两处根写死成 `E:/ProgramData/...` 绝对路径 ⇒ 换机 / 换用户目录就全错。
正解分两半(判据=**这个根能不能从"我自己的位置"推出来**):
· **能**(本包自己的目录,如 `scripts/`、`boards/`)⇒ 从 `__file__` 上跳推导,⛔ 不写死;
· **不能**(工作区根 —— 技能包在配置目录下,跟工作区没有任何相对关系)
⇒ 属"本机各不同的配置" ⇒ 读本包 `env/roots.env`(`install.py` 生成,不入库)。
⚠️ 从 `boards/项目看板/` 到包根要上跳**两级**。
"""
import os as _os
_here = _os.path.dirname(_os.path.abspath(__file__))
for _up in range(5):
_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 _p
return ""
_sm_load_roots()
# 🔴 扫哪些根目录找工作区(`;` 分隔;可用环境变量覆盖)。
# ⚠️ 2026-10-14:默认值原写死 `E:/ProgramData/AIProject` ⇒ 改为**按序推导**(全部相对,⛔ 无盘符):
# ① 宿主 env / `env/roots.env` 的 `DSH_AIROOT`(工作区根的父目录 —— 唯一权威来源)
# ② `DSH_PM_BOARD_SELF_WS`(本看板服务的那个工作区)的父目录
# ③ 都取不到 ⇒ **空**,并由下面的 `_ws_roots()` 在真正要用时**如实报错**,
# ⛔ 不做"兜底猜一个盘符"那种事(猜错 ⇒ 扫到别人的工作区,静默出错数据)。
def _ws_roots() -> list:
raw = (os.environ.get("DSH_PM_BOARD_ROOTS") or "").strip()
if not raw:
raw = (os.environ.get("DSH_AIROOT") or "").strip()
if not raw:
_self = (os.environ.get("DSH_PM_BOARD_SELF_WS") or "").strip()
if _self:
try:
raw = str(Path(_self).resolve().parent)
except Exception:
raw = ""
return [Path(p.strip()) for p in raw.split(";") if p.strip()]
WS_ROOTS = _ws_roots()
# 技能自带的新建入口(⛔ 不自己另写一套建区逻辑 —— 同族"一条规则两份实现"必漂)
# ⚠️ 2026-10-14:看板整包搬进 `boards/`(用户令「开个 boards 文件夹放两个看板」)⇒
# **看板真身**与**机制脚本**不再同目录,⛔ 别再用一个常量指两处。
# · `SKILL_BOARD_DIR` = 会话看板(`board.py::acc_is_pass` 的取用处)
# · `SKILL_SCRIPTS` = 机制脚本(`init_workspace.py` 的取用处)
_SKILL_ROOT = Path(os.environ.get("DSH_SKILL_ROOT")
or r"E:/ProgramData/.workbuddy/skills/session-mechanism")
# 🔴 技能根=**从本文件位置上跳两级**(`boards/项目看板/` → `boards/` → 包根)——
# 纯相对,⛔ 不再写死盘符(2026-10-14 用户令「能用相对路径的都要用相对路径」)。
# 宿主 env `DSH_SKILL_ROOT` 仍优先(部署方显式指定时以它为准)。
_SKILL_ROOT = Path(os.environ.get("DSH_SKILL_ROOT") or HERE.parent.parent)
SKILL_BOARD_DIR = Path(os.environ.get("DSH_SKILL_BOARD_DIR")
or str(_SKILL_ROOT / "boards" / "会话看板"))
SKILL_SCRIPTS = Path(os.environ.get("DSH_SKILL_SCRIPTS")
@@ -258,6 +311,24 @@ def scan_workspaces() -> list:
"""
installed, skipped = [], []
seen = set()
if not WS_ROOTS:
# 🔴 2026-10-14:默认值去掉了写死盘符 ⇒ "推不出来"成了一种真实可能
# (换机 / 没跑过 `install.py` / `env/roots.env` 丢了)。
# ⇒ **如实报错 + 留痕**,⛔ 不许静默扫出"0 个项目"
# (那与"本机真的没有工作区"表现完全一样)。
_say("⛔ 推不出工作区根 ⇒ 一个区都扫不到。请设 `DSH_AIROOT`(工作区根的父目录),"
"或跑一次 `python install.py --apply --workspace <本区>`。")
try:
_d = _SKILL_ROOT / "logs"
_d.mkdir(parents=True, exist_ok=True)
with open(_d / "project-board.err.log", "a",
encoding="utf-8", errors="replace") as _f:
_f.write("[项目看板] ⛔ 推不出工作区根(`DSH_AIROOT` / "
"`DSH_PM_BOARD_ROOTS` / `DSH_PM_BOARD_SELF_WS` 全空,"
"且 `env/roots.env` 缺 `DSH_AIROOT`)\n")
except Exception:
pass
return installed, skipped
for root in WS_ROOTS:
if not root.is_dir():
continue
@@ -717,7 +788,7 @@ class Handler(BaseHTTPRequestHandler):
def main() -> int:
ap = argparse.ArgumentParser(description="项目管理界面(本机所有工作区 · 增删查)")
ap = argparse.ArgumentParser(description="项目看板(本机所有工作区 · 增删查)")
ap.add_argument("--serve", type=int, default=0, metavar="PORT",
help="起 HTTP 服务(默认不启,只做一次扫描并打印摘要)")
ap.add_argument("--once", action="store_true", help="扫一次并打印摘要")
@@ -725,7 +796,7 @@ def main() -> int:
if a.serve:
srv = ThreadingHTTPServer(("127.0.0.1", a.serve), Handler)
_say("项目管理界面已起:http://127.0.0.1:%d/ (根目录 %s)"
_say("项目看板已起:http://127.0.0.1:%d/ (根目录 %s)"
% (a.serve, ", ".join(str(r) for r in WS_ROOTS)))
try:
srv.serve_forever()