feat(board/graph): 图模式接项目看板(只读·类型标签);fix(hook): 裸「完成目标」也触发技能闸;fix(launcher): pythonw 兜 stdout
This commit is contained in:
1 parent
12b7dd9b02
commit
efbe128670
5 files changed
+170
-2
No files matched your search
@@ -73,6 +73,8 @@ agent_created: true
|
||||
> **判据(一句话)**:**用户说了触发句 ⇒ 第 0 步是先加载本技能,然后才动手。⛔ 没有"我大概知道"这条捷径。**
|
||||
|
||||
**触发句(命中任一条即须加载)**:🔴 `使用项目机制完成目标:XXXX`(**2026-10-08 用户令的现行写法**)/
|
||||
🔴 **裸「完成目标」**(2026-10-19 用户令,逐字:「把skill 改成 **完成目标** 也能触发」—— 用户口语里常把机制名字省掉,
|
||||
所以 `项目机制完成目标` 与 `完成目标` 都算派活授权)/
|
||||
`使用任务会话完成 X 目标` / `使用执行会话完成 X 目标` / `继续 X 目标` /
|
||||
`用执行会话完成 X` / `派任务会话` / `建任务会话` / 任何"让我去把某个目标做完/接着做"的表述。
|
||||
|
||||
@@ -1046,7 +1048,9 @@ assets/ board.html design-tokens.css
|
||||
命令面:`init`/`check`/`ready`/`start`/`report`/`expand`/`show`/`list`/`approve`/`cancel`/`tick`/`plan`/`progress`。
|
||||
用法与语义见 `graph/README.md`,自测 `python graph/test_graphctl.py`(应 `Ran 122 tests … OK`,约 240 秒)。
|
||||
起一棒的最短路径:`init --ws <ws> --title … --nodes-json -` ⇒ `check` ⇒ `tick`(算完就开)⇒ 执行方 `report` ⇒ 反复 `tick` 直到 `progress` 到 100%。
|
||||
⚠️ 它现在**没有接进看板、也没有排期**(`plan` 只出规范行)⇒ 图模式由会话手工调用,日常流程里看不到它。要不要接入口见工作区待拍板项。
|
||||
✅ **已接项目看板(2026-10-19)**:`boards/项目看板/project-board.py::graph_view()` 只读 `<ws>/.workbuddy/graph/*/graph.json`,
|
||||
页面「图模式」块按**节点类型标签**(普通/子图/展开/子节点/人审/条件)+ 状态色显示,**只画不派活**。
|
||||
⚠️ 仍**没有排期**(`plan` 只出规范行)⇒ 往前推进靠会话敲 `tick`。
|
||||
|
||||
**⛔ 已无必需外部依赖(2026-10-04)**:原先声明「依赖 `agent-operating-rules`」——现已把**排版核心块收进本包并定为权威** ⇒ `references/03-回复排版-核心块.md`
|
||||
(2026-10-06 校正:原措辞是「内联副本」,与 03 号自称「权威源」打架 ⇒ 权威声明分裂)、把 `_env.py` 的技能库识别特征改为**候选数组(首个=本包自己)** ⇒ **本包自包含**。📌 **2026-10-07 订正**:上面末句原写「**那个技能**若同时装着,属可选增强(多了跨项目排版/去 AI 味的完整版),⛔ 不装也能跑」——
|
||||
|
||||
@@ -54,6 +54,28 @@ _sm_load_roots()
|
||||
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||
os.environ.setdefault("DSH_PROJECT_BOARD_UPSTREAM", "http://127.0.0.1:20099/board.json")
|
||||
|
||||
# 🔴🔴 2026-10-19 补:「兜 stdout/stderr」(同族 P0-73 的第六类坑)
|
||||
# `pythonw.exe` 是 GUI 子系统 ⇒ 进程里 **`sys.stdout` 是 None**,看板一 `print()` 就
|
||||
# `AttributeError` ⇒ 进程当场退出、任务显示 `Running` 但端口从没绑上(实测:
|
||||
# 计划任务起的实例静默死掉,而同一份代码用 `python.exe` 跑得起来 ⇒ 极难查)。
|
||||
# ⇒ 起来第一件事把两根流兜到日志文件;日志落本目录 `logs/`(不存在就建)。
|
||||
def _wrap_streams() -> None:
|
||||
if sys.stdout is not None and sys.stderr is not None:
|
||||
return
|
||||
base = os.path.join(HERE, "logs")
|
||||
try:
|
||||
os.makedirs(base, exist_ok=True)
|
||||
f = open(os.path.join(base, "project-board.log"), "a", encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
f = open(os.devnull, "w", encoding="utf-8")
|
||||
if sys.stdout is None:
|
||||
sys.stdout = f
|
||||
if sys.stderr is None:
|
||||
sys.stderr = f
|
||||
|
||||
|
||||
_wrap_streams()
|
||||
|
||||
# 🔴 「本工作区」=**拥有一这层的那个工作区**(用户 2026-10-06 要:本工作区的卡片换色突出)。
|
||||
# ⛔ 不许靠 `Path.cwd()`(计划任务的工作目录不是工作区根)。
|
||||
# 2026-10-14 起:本文件在技能包内 ⇒ 用 `DSH_WS_ROOT`(来自宿主 env 或包内 `env/roots.env`)。
|
||||
@@ -80,6 +102,9 @@ def main() -> int:
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
# ⛔ 不许静默吞:pythonw 下没有 stderr 就没人知道它为什么死(`_wrap_streams` 已兜到日志)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
@@ -261,8 +261,32 @@
|
||||
<div class="summary" id="summary"></div>
|
||||
</header>
|
||||
|
||||
<style id="graphstyle">
|
||||
/* 图模式块(2026-10-19 接入 · 只读展示,节点用「类型标签 + 状态色」区分) */
|
||||
.graphs { margin: 4px 24px 40px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.12); }
|
||||
.ghead { font-size: 16px; font-weight: 700; margin-bottom: 10px; }
|
||||
.gsub { font-weight: 400; opacity: .6; font-size: 12px; margin-left: 8px; }
|
||||
.gcard { border: 1px solid rgba(255,255,255,.12); border-radius: 10px;
|
||||
padding: 10px 12px; margin-bottom: 10px; background: rgba(255,255,255,.03); }
|
||||
.gttl { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
|
||||
.gmeta { font-weight: 400; opacity: .55; font-size: 11px; margin-left: 6px; }
|
||||
.gchips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.gchip { font-size: 11px; padding: 2px 7px; border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,.16); background: rgba(255,255,255,.05);
|
||||
white-space: nowrap; }
|
||||
.gchip b { font-weight: 600; opacity: .75; margin-right: 5px; font-size: 10px; }
|
||||
.gchip.gst-running { border-color: #4a9eff; background: rgba(74,158,255,.16); }
|
||||
.gchip.gst-done { border-color: #3fbf7f; background: rgba(63,191,127,.16); }
|
||||
.gchip.gst-blocked { border-color: #ff6b6b; background: rgba(255,107,107,.16); }
|
||||
.gchip.gst-awaiting { border-color: #ffb454; background: rgba(255,180,84,.16); }
|
||||
.gchip.gst-gating { border-color: #b48bff; background: rgba(180,139,255,.16); }
|
||||
.gchip.gst-skipped, .gchip.gst-cancelled { opacity: .45; }
|
||||
.gempty { opacity: .55; font-size: 12px; padding: 6px 0; }
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div class="cols" id="cols"></div>
|
||||
<div class="graphs" id="graphs"></div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
@@ -798,8 +822,46 @@ function paintGoals(){
|
||||
}
|
||||
|
||||
/* ── 视图分发 ── */
|
||||
/* ── 图模式块(只读)─────────────────────────────────────────────────────
|
||||
数据来自 `/api/projects` 每个工作区的 `p.graph`(后端 `graph_view()`)。
|
||||
🔴 用户 2026-10-19:「图可以接看板,显示类型标签区分下」⇒ 每个节点一枚
|
||||
「类型标签 + 标题」的胶囊,颜色表示状态,标签文字表示节点类型
|
||||
(普通/子图/展开/子节点/人审/条件)。⛔ 这里只画,不派活、不改图。 */
|
||||
function paintGraphs(){
|
||||
var el = document.getElementById("graphs");
|
||||
if(!el) return;
|
||||
var ps = (STATE.data && STATE.data.projects) || [];
|
||||
if(STATE.cur >= 0) ps = ps[STATE.cur] ? [ps[STATE.cur]] : [];
|
||||
var rows = [];
|
||||
ps.forEach(function(p){
|
||||
var gv = p.graph || {};
|
||||
(gv.graphs || []).forEach(function(g){ rows.push({p:p, g:g}); });
|
||||
});
|
||||
if(!rows.length){
|
||||
el.innerHTML = '<div class="gempty">图模式:本机暂无图(会话里用 <code>graphctl.py init</code> 建一张,这里就会显示)</div>';
|
||||
return;
|
||||
}
|
||||
var h = ['<div class="ghead">图模式<span class="gsub">'
|
||||
+ rows.length + ' 张图 · 胶囊上的粗体是节点类型,颜色是状态</span></div>'];
|
||||
rows.forEach(function(r){
|
||||
var g = r.g, p = r.p;
|
||||
h.push('<div class="gcard"><div class="gttl">' + esc(g.title)
|
||||
+ '<span class="gmeta">' + esc(p.dir) + ' · ' + esc(g.gid)
|
||||
+ ' · ' + esc(String(g.percent)) + '%</span></div><div class="gchips">');
|
||||
(g.nodes || []).forEach(function(n){
|
||||
h.push('<span class="gchip gst-' + esc(n.status) + '" title="'
|
||||
+ esc(n.id + ' · ' + n.status) + '"><b>' + esc(n.kind) + '</b>'
|
||||
+ esc(n.title) + '</span>');
|
||||
});
|
||||
if(g.truncated) h.push('<span class="gchip">…还有 ' + g.truncated + ' 个</span>');
|
||||
h.push('</div></div>');
|
||||
});
|
||||
el.innerHTML = h.join("");
|
||||
}
|
||||
|
||||
function render(){
|
||||
if(!STATE.data) return;
|
||||
paintGraphs();
|
||||
var colsEl = document.getElementById("cols");
|
||||
if(STATE.view === "goals"){
|
||||
colsEl.classList.add("aslist"); // 🔴 4 列 grid ⇒ block(否则整页挤进第 1 列)
|
||||
|
||||
@@ -361,6 +361,77 @@ def _region_dir(ws: Path, goal: dict) -> Path:
|
||||
return ws / "执行会话"
|
||||
|
||||
|
||||
# ── 图模式(只读快照 · 2026-10-19 接入看板)─────────────────────────────────
|
||||
# 落点=`<ws>/.workbuddy/graph/<gid>/graph.json`(图模式自己的目录)。
|
||||
# ⛔ 只读:看板不写任何图文件、不派活、不改状态 —— 图模式仍然零侵入。
|
||||
_GRAPH_KINDS = (
|
||||
("sub", "子图"), # 节点的 subgraph 展开出来的父容器
|
||||
("map", "展开"), # map/foreach 展开出来的父容器
|
||||
("child", "子节点"), # 展开出来的子节点
|
||||
("approve", "人审"), # approval: true
|
||||
("cond", "条件"), # 带 when 子句
|
||||
)
|
||||
|
||||
|
||||
def _graph_node_kind(n: dict) -> str:
|
||||
"""给前端一个中文类型标签(用户 2026-10-19:「显示类型标签区分下」)。"""
|
||||
if n.get("parent"):
|
||||
return "子节点"
|
||||
if n.get("subgraph"):
|
||||
return "子图"
|
||||
if n.get("map"):
|
||||
return "展开"
|
||||
if n.get("approval"):
|
||||
return "人审"
|
||||
if n.get("when"):
|
||||
return "条件"
|
||||
return "普通"
|
||||
|
||||
|
||||
def graph_view(ws: Path) -> dict:
|
||||
base = ws / ".workbuddy" / "graph"
|
||||
graphs = []
|
||||
if base.is_dir():
|
||||
for d in sorted(base.iterdir()):
|
||||
gp = d / "graph.json"
|
||||
if not gp.is_file():
|
||||
continue
|
||||
try:
|
||||
g = _load_json(gp, {}) or {}
|
||||
except Exception: # noqa: BLE001 —— 单个坏图不许弄崩整块看板
|
||||
continue
|
||||
nodes = g.get("nodes") or {}
|
||||
if not isinstance(nodes, dict) or not nodes:
|
||||
continue
|
||||
cnt = {}
|
||||
chips = []
|
||||
for nid, n in nodes.items():
|
||||
if not isinstance(n, dict):
|
||||
continue
|
||||
st = str(n.get("status") or "")
|
||||
cnt[st] = cnt.get(st, 0) + 1
|
||||
chips.append({
|
||||
"id": str(nid),
|
||||
"title": str(n.get("title") or nid),
|
||||
"status": st,
|
||||
"kind": _graph_node_kind(n),
|
||||
})
|
||||
active = len(nodes) - cnt.get("skipped", 0) - cnt.get("cancelled", 0)
|
||||
done = cnt.get("done", 0)
|
||||
graphs.append({
|
||||
"gid": str(g.get("gid") or d.name),
|
||||
"title": str(g.get("title") or d.name),
|
||||
"percent": round(100.0 * done / active, 1) if active else 100.0,
|
||||
"counts": cnt,
|
||||
"total": len(nodes),
|
||||
"nodes": chips[:40], # 只发前 40 个,够看进度又不撑爆页面
|
||||
"truncated": max(0, len(chips) - 40),
|
||||
"updated_at": str(g.get("updated_at") or g.get("created_at") or ""),
|
||||
})
|
||||
graphs.sort(key=lambda x: x["updated_at"], reverse=True)
|
||||
return {"graphs": graphs, "count": len(graphs), "dir": str(base).replace("\\", "/")}
|
||||
|
||||
|
||||
def build_project(ws: Path) -> dict:
|
||||
"""把**一个工作区**读成「项目」(纯读,⛔ 不写任何东西)。"""
|
||||
inbox = ws / "tmp" / "supervise-inbox"
|
||||
@@ -551,6 +622,8 @@ def build_project(ws: Path) -> dict:
|
||||
"goal_by_col": by,
|
||||
# 🔴 有目标但**落不进四列**的(状态读不出)⇒ 前端另行提示,⛔ 不硬塞列
|
||||
"life_unknown": life_unknown,
|
||||
# 🔴 图模式快照(只读):`<ws>/.workbuddy/graph/*/graph.json` ⇒ 前端「图模式」块
|
||||
"graph": graph_view(ws),
|
||||
"acc": {"n": len(eff), "pass": acc_pass, "items": acc_items},
|
||||
"region": str(_region_dir(ws, goal)).replace("\\", "/"),
|
||||
"port": cfg.get("singleton_port"),
|
||||
|
||||
@@ -157,6 +157,10 @@ TRIGGERS = (
|
||||
'使用项目机制', '项目机制完成', '项目机制',
|
||||
# 🔴 2026-10-08 用户令:用法=「**使用项目机制完成目标:XXXX**」——**项目机制=机制的名字**;
|
||||
# ⚠️ 它建出来的会话仍叫「执行会话/检查会话」⇒ ⛔ 角色名不进词表、也不改名。
|
||||
# 🔴 2026-10-19 用户令(逐字「把skill 改成 完成目标也能触发」):光写「项目机制完成目标」不够 ——
|
||||
# 用户口语里会把机制名字省掉,只说「完成目标」;词表里必须有**裸「完成目标」**这一条,
|
||||
# 否则那句同样是派活授权,却一条都命不中。
|
||||
'完成目标',
|
||||
'使用任务会话', '使用执行会话', '使用协作会话', '任务会话完成', '执行会话完成',
|
||||
'继续完成目标', '继续目标', '继续完成之前', '继续之前的',
|
||||
'会话技能', '会话机制', '创建任务会话', '建任务会话',
|
||||
@@ -178,7 +182,7 @@ _LOAD_RULES = ('按规则来', '按规则做', '按作业规则', '遵守规则'
|
||||
# 🔴 为什么单列一族:这三族**目标技能不同**,⛔ 不能混在一个 if 里 ——
|
||||
# 命中"使用任务会话完成目标"的人,要的是**会话机制本体**(怎么建排期/怎么派活/
|
||||
# 自决策白名单在 `references/02-功能优先协作协议.md`),⛔ 不是决策方法论。
|
||||
_LOAD_SESSION = ('使用项目机制', '项目机制完成', '项目机制',
|
||||
_LOAD_SESSION = ('使用项目机制', '项目机制完成', '项目机制', '完成目标',
|
||||
'使用任务会话', '使用执行会话', '使用协作会话', '任务会话完成', '执行会话完成',
|
||||
'继续完成目标', '继续目标', '继续完成之前', '继续之前的',
|
||||
'会话技能', '会话机制', '创建任务会话', '建任务会话',
|
||||
|
||||
Reference in new issue
Block a user