@marsken/dsh-studio 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +148 -0
- package/LICENSE +202 -0
- package/README.md +1367 -3
- package/client.js +952 -0
- package/client.src.js +2466 -0
- package/cordis.patch.yml +9 -0
- package/dist/client.js +952 -0
- package/docs/compatibility.md +101 -0
- package/docs/images/board.png +0 -0
- package/docs/images/cases.png +0 -0
- package/docs/images/quality.png +0 -0
- package/docs/publish.md +123 -0
- package/docs/quickstart.md +53 -0
- package/icon.svg +6 -0
- package/index.js +2674 -0
- package/locale/en.json +11 -0
- package/locale/zh.json +11 -0
- package/package.json +64 -4
- package/scripts/smoke-store.mjs +76 -0
- package/src/host/cases.js +135 -0
- package/src/host/design.js +87 -0
- package/src/host/dispatcher.js +936 -0
- package/src/host/domain.js +1966 -0
- package/src/host/executor.js +1396 -0
- package/src/host/gitlab-facts.js +22 -0
- package/src/host/impact.js +32 -0
- package/src/host/integrations.js +587 -0
- package/src/host/quality.js +106 -0
- package/src/host/routes/admin.js +100 -0
- package/src/host/routes/agents.js +71 -0
- package/src/host/routes/auth.js +155 -0
- package/src/host/routes/coding.js +203 -0
- package/src/host/routes/connect.js +166 -0
- package/src/host/routes/exports.js +122 -0
- package/src/host/routes/insights.js +97 -0
- package/src/host/routes/releases.js +335 -0
- package/src/host/routes/requirements.js +430 -0
- package/src/host/routes/skills.js +75 -0
- package/src/host/routes/tasks.js +573 -0
- package/src/host/store.js +522 -0
- package/src/host/team-memory.js +46 -0
- package/src/host/tools.js +1211 -0
package/README.md
CHANGED
|
@@ -1,3 +1,1367 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# dsh-studio
|
|
2
|
+
|
|
3
|
+
[](https://github.com/marsken88/dsh-studio/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
给一家公司用的「需求&任务工作空间」:一个常驻的 DSH 插件,把**看板 + 角色 agent + 内部市场**装进 DSH Web 的同一个页面里。
|
|
6
|
+
自建 GitLab 管代码、飞书管交流,agent 负责产品 / 架构 / 研发 / 测试四个角色的具体活儿。
|
|
7
|
+
|
|
8
|
+
运行环境:DSH `0.2.2-rc.1`,Node 22 以上,Linux/macOS(Windows 原生不支持 `link:` 安装 + systemd 托管)。
|
|
9
|
+
|
|
10
|
+
## 它长什么样
|
|
11
|
+
|
|
12
|
+
| 看板:八列、角色负责人、卡片带进度/评论/用例 | 用例库:跨卡聚合、套件、最近回归 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
|  |  |
|
|
15
|
+
|
|
16
|
+
| 效能:一次通过率、返工率、按专家看质量 |
|
|
17
|
+
| --- |
|
|
18
|
+
|  |
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## 部署
|
|
22
|
+
|
|
23
|
+
仓库:https://github.com/marsken88/dsh-studio(`main` 分支,含可直接安装的构建产物 `client.js`,clone 下来就能装,不用先构建)。
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1) 拉到宿主机
|
|
27
|
+
# 1) 拉到本机任意目录
|
|
28
|
+
git clone https://github.com/marsken88/dsh-studio.git ~/dsh-studio
|
|
29
|
+
|
|
30
|
+
# 2) 装进目标 profile(会写 profile 的 package.json + bundles,走 pnpm link)
|
|
31
|
+
dsh plugin --profile web add link:~/dsh-studio
|
|
32
|
+
|
|
33
|
+
# 3) 确认挂载
|
|
34
|
+
dsh --profile web --dump-config | grep -A2 'dsh-studio'
|
|
35
|
+
|
|
36
|
+
# 4) 重启服务
|
|
37
|
+
sudo systemctl restart dsh-web
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
页面入口:DSH Web 左侧栏第 4 项「需求&任务工作空间」。三个页签:**看板 / 市场 / 设置**。
|
|
41
|
+
|
|
42
|
+
## 质量闸门
|
|
43
|
+
|
|
44
|
+
两类 HTML/CSS 抓不到的回归,现在由构建期守卫和真库自检挡住:
|
|
45
|
+
|
|
46
|
+
| 命令 | 挡什么 |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `cd web && npm run guard` | CSS 里写回 `hsl(var(--x))`(颜色解析失败)、Radix Portal 忘记 `container={portalRoot()}`(弹层「存在但不可见」)、产物丢失门户作用域属性 |
|
|
49
|
+
| `npm run smoke:store -- ~/.dsh/dsh-studio.db` | 用生产库的 `.db/-wal/-shm` 副本在本地跑真实启动路径,抓「空库能起、真库就炸」的 `ReferenceError` |
|
|
50
|
+
| `npm test` | 57 条契约:manifest、产物哈希、宿主接口、跨模块导入、只读绑定赋值、迁移与文档一致性、快捷键解析 |
|
|
51
|
+
|
|
52
|
+
`pnpm build` 已串上守卫(`make-legacy → guard --pre → vite build → sync-client → guard --post`),CI 也会跑:忘记构建或写出上面两类样式,流水线直接红。
|
|
53
|
+
|
|
54
|
+
## 开发与兼容性
|
|
55
|
+
|
|
56
|
+
- 参与开发(构建链、部署、验证要求、禁区):[AGENTS.md](AGENTS.md)
|
|
57
|
+
上手第一天(三条命令,已在全新 clone 上验证过):[docs/quickstart.md](docs/quickstart.md)
|
|
58
|
+
发布到插件市场(npm / GitHub 发版 + 收录条目 + 自查清单):[docs/publish.md](docs/publish.md)
|
|
59
|
+
- 依赖的 dsh 宿主接口、升级 runbook、兼容矩阵与已知限制:[docs/compatibility.md](docs/compatibility.md)
|
|
60
|
+
|
|
61
|
+
## 看板
|
|
62
|
+
|
|
63
|
+
建卡与建需求的入口在页面右上角统一成一个「+ 新建」:菜单里选「新建需求」进需求表单,选「新建任务」直接在看板上开一张卡(详见文末「新建入口合并」)。
|
|
64
|
+
|
|
65
|
+
想找东西不用一页页翻:按 **⌘K / Ctrl+K** 打开全局搜索,输编号、标题或负责人,↑↓ 选、回车直达(卡片直接开抽屉,需求跳到需求页并选中,版本跳到版本页)。
|
|
66
|
+
|
|
67
|
+
最常用的三件事还给了全局快捷键,不用先打开面板:**Ctrl/⌘+Alt+Shift+N** 新建需求、**Ctrl/⌘+Alt+Shift+T** 新建任务、**Ctrl/⌘+Alt+Shift+D** 把某张卡派给专家——按下去面板直接进到对应那一步(选卡 / 选专家),面板已开着时按另一个键会就地切换动作。为什么是这一档:Ctrl+Shift 被浏览器占了(无痕窗口 / 重开标签页 / 收藏所有标签页),而 Ctrl+Alt 被 dsh 宿主占了(Ctrl+Alt+N = 新会话),两层都不能碰。
|
|
68
|
+
|
|
69
|
+
搜得到的对象:卡片、需求、版本、**专家、专家团、项目、技能库里的技能**(连技能描述与标签一起搜,回车直达设置里的技能库)。
|
|
70
|
+
|
|
71
|
+
还有五条**带参数的动作**——选完动作会进入第二层选择,Esc 逐层返回。最近用过的动作会自动排到最前面(最多记 6 条,存在浏览器本地):
|
|
72
|
+
|
|
73
|
+
| 动作 | 流程 |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| 把卡片推进到下一列 | 选一张卡 → 按状态顺序推进一列(已到最后一列会提示),完成后自动打开这张卡的抽屉 |
|
|
76
|
+
| 推进并留言 | 选一张卡 → 写一句话 → 这句话进讨论、卡片同时推进一列(交接常用) |
|
|
77
|
+
| 整列推进 | 选一列 → 确认一次 → 这一列的卡按顺序一起推进到下一列,逐张汇报成功与失败 |
|
|
78
|
+
| 忘掉最近用过的动作 | 清空这台电脑上记的「最近用过」顺序,面板回到默认排序 |
|
|
79
|
+
| 把卡片派给专家 | 选一张卡 → 选一位专家,当场跑起来 |
|
|
80
|
+
| 给卡片留言 | 选一张卡 → 写一句话,落到卡片讨论里
|
|
81
|
+
|
|
82
|
+
同一个框也能**直接办事**(结果前面带紫色「操作」标签):新建需求 / 新建任务、打开任意页签、导出任务或需求 CSV、切换主题(明亮 / 深色 / 跟随系统)、刷新数据,以及**直接跳到设置里的任意一页**(输入「技能库」「飞书机器人」「权限」等)。
|
|
83
|
+
|
|
84
|
+
**每列先渲染 40 张卡**,超出的折成一行「显示更多(还有 N 张)」,点一次加 40——卡多的时候先卡住的是浏览器 DOM,不是服务端。
|
|
85
|
+
|
|
86
|
+
八列,每列的负责人(专家或专家团)可在「设置 → 每一列由谁负责」里改;卡片进入某列时负责人随之切换,并自动派给它。也可以把某列设成「👤 等人认领」——卡停在这一列、不自动派,由人在卡上点名让谁做(默认「待澄清」就是这一档,因为澄清需求本来就不一定是产品经理的活):
|
|
87
|
+
|
|
88
|
+
| 列 | id | 默认负责人 |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| 待澄清 | `clarify` | 等人认领 |
|
|
91
|
+
| 待方案 | `design` | 架构 |
|
|
92
|
+
| 待开发 | `dev_ready` | 研发 |
|
|
93
|
+
| 开发中 | `dev` | 研发 |
|
|
94
|
+
| 待评审 | `review` | 架构 |
|
|
95
|
+
| 待测试 | `test` | 测试 |
|
|
96
|
+
| 待验收 | `accept` | 产品 |
|
|
97
|
+
| 已完成 | `done` | 产品 |
|
|
98
|
+
|
|
99
|
+
**待评审这一列是给代码评审用的**:研发提了 MR 后,同步任务会把 MR 关联到卡上(`gitlabMR`),只要 MR 还是 `opened` 且卡在「开发中」,就自动把卡推进到「待评审」并派给架构师;评审通过才放行到「待测试」,不通过就 @ 研发退回「开发中」。
|
|
100
|
+
|
|
101
|
+
卡上显示的「负责:」只看一处:正在跑的那个专家 > 卡上指派的专家 > 本列负责人 > 「未指派」。详情页的「当前列 / 负责」跟卡片用同一套口径,不会再出现「卡片说产品、点开说架构」。
|
|
102
|
+
|
|
103
|
+
**卡片上直接看得出它挂在哪**(2026-11-09 补):
|
|
104
|
+
|
|
105
|
+
| 卡片上的元素 | 含义 | 点击行为 |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `DEV-001` 等编号 | 这条需求下自动分配的编号 | —— |
|
|
108
|
+
| `📄 REQ-2026-001`(紫色) | 所属需求;鼠标悬停显示需求标题 | 看板只看这条需求下的卡(再点一次取消) |
|
|
109
|
+
| `ORD`(蓝色) | 所属业务项目(项目的代号) | 看板只看这个项目下的卡(再点一次取消) |
|
|
110
|
+
| `🗓 2026-11` | 所属迭代(悬停显示起止日期) | —— |
|
|
111
|
+
|
|
112
|
+
没挂需求的卡显示一枚虚线灰 chip「未挂需求」(点一下直接开卡片去「计划」里挂),所以「卡片上为什么没有需求」这件事本身也是可读的,不再是空白。
|
|
113
|
+
|
|
114
|
+
**一列可以有多个候选人,靠策略挑一个**:在「设置 → 列的负责人」里给某列「+ 加候选」加第二个人,列就从单人变成候选池,并出现策略下拉:
|
|
115
|
+
|
|
116
|
+
| 策略 | id | 怎么挑 |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| 轮询 | `round_robin` | 谁手上正在跑的卡少给谁(默认) |
|
|
119
|
+
| 按专长匹配 | `best_match` | 同领域 +50、角色位对得上 +20、技能词命中标题/需求 每个 +5,同分再比谁活少 |
|
|
120
|
+
| 团长分派 | `lead` | 池子里有专家团就先把卡交给团长,由团长拆活分给成员 |
|
|
121
|
+
|
|
122
|
+
单人列就是「池子里只有一个人」,行为与以前完全一致。候选池挑出来的人如果没有领域归属、而这张卡标了业务领域,会再自动改派给同领域的对应角色。
|
|
123
|
+
|
|
124
|
+
**「阻塞」不是一列,是卡上的标记**:卡干不下去时打标(必须写原因),它留在原列、不再被自动派单,看板上红边 + ⛔ 标提示;工具栏有「只看阻塞 N」筛选。之后由人决定怎么继续——交回本列负责人、直接推进到下一列、改派给指定专家,或只解除先不派。任何一次「推进到别的列」都等同于解除阻塞。
|
|
125
|
+
|
|
126
|
+
卡片字段:标题、类型、当前角色、状态、优先级、需求描述、GitLab Issue 链接、产出物、评论、历史。
|
|
127
|
+
|
|
128
|
+
点卡片打开右侧详情抽屉(`Esc` 或右上角 × 关闭)。抽屉是「头部固定 + 中间可滚 + 底部评论固定」的三段结构:中间 `.ds-dw-body` 带 `min-height:0`,内容再长也是内部滚动,**不会再把底部评论框顶到屏幕外**(这是早前「详情显示不完整」的根因:flex 子项默认不收缩,body 长过视口后把 footer 挤出去了)。
|
|
129
|
+
|
|
130
|
+
## 专家与专家团
|
|
131
|
+
|
|
132
|
+
「设置 → 专家」是这家公司的 AI 员工名册,每个专家有:图标、名字、一句话定位、**角色位**、**业务领域**、**角色提示词**(作为系统提示)、**技能**、模型、Agent 预设、启用开关。
|
|
133
|
+
|
|
134
|
+
- 默认四位:产品经理、架构师、开发工程师、测试工程师,可按项目增删改(最多 80 个)。
|
|
135
|
+
- **按业务领域拆分**:填「业务领域」后可以点「生成产品/架构/研发/测试」一次建出这条业务线的四位专家;卡片上也可以标领域,当某列的负责人是通用专家而卡有领域时,会自动改派给同领域、角色位匹配的那位专家。
|
|
136
|
+
- **名字就是 @ 名**:评论里写 `@架构师 请给出方案`,服务端解析出被点名的人并立刻派活,不用等卡片换列。接口返回 `mentions` / `dispatched`,界面显示「已通知 …」。
|
|
137
|
+
- 角色提示词会拼进该专家的系统提示,写清楚「先读 README 与 docs/」「不确定先去代码里找证据」这类规矩最有效。
|
|
138
|
+
- **新增/编辑走右侧抽屉**:点「新增专家」或行内的「编辑」,表单从右侧滑出(宽 760px,窄屏铺满),左栏名册保持可见;抽屉里「头部标题固定 / 中间表单可滚 / 底部保存栏固定」,`Esc` 或点遮罩关闭。角色提示词和技能列表都比较长,抽屉比原来的内联表单更适合边看名册边改一个人。
|
|
139
|
+
- **图标是选出来的,不是打的**:抽屉里给 32 个候选 emoji(角色类 + 业务类),点一下就换上、实时预览;下面还有一个输入框可以粘自定义 emoji,留空回落到 🧩。图标会出现在卡片、评论头像、专家列表与派单会话标题 `[图标 名字]` 上。
|
|
140
|
+
- **同一套抽屉用在五处**:专家、专家团、技能库、代码仓库、飞书机器人 的「新增 / 编辑」都是同一个 `SideDrawer` 组件(`client.js` 里的共享组件),行数与按钮位置一致;项目与迭代、列负责人、权限、数据备份这些仍然用内联面板(它们没有「选中一行再编辑」的语义)。
|
|
141
|
+
|
|
142
|
+
### 键盘可达性(2026-11-09)
|
|
143
|
+
|
|
144
|
+
面板里很多可点的是 `div` / `span`(卡片、chip、页签、列表行),原生不可聚焦。没有去改几百处调用点,而是在 DOM 层补:
|
|
145
|
+
|
|
146
|
+
- 启动时(以及 `MutationObserver` 监听到增删后 200ms 去抖)扫一遍 `[data-dsh-studio]` 子树,凡是 `getComputedStyle(el).cursor === 'pointer'` 的元素,补 `tabindex="0"`、`role="button"`、`data-kb="1"`,并绑 `keydown`:`Enter` / 空格 等价于点击。
|
|
147
|
+
- `aria-pressed` 跟着 `data-on` 同步(选中态能被读屏念出来),`aria-label` 取 `title` 或文本(截 80 字)。
|
|
148
|
+
- 跳过输入框/按钮/链接/`contenteditable` 和遮罩层(`.ds-side-mask` 这类),避免把表单和无障碍语义弄乱。
|
|
149
|
+
- 焦点可见:`[data-dsh-studio] :focus-visible { outline:2px solid var(--ds-accent); outline-offset:2px; }`。
|
|
150
|
+
- 代价是手足无措时的「隐形 tab 停点」会变多,但换来的是全键盘可用;要收窄范围只需在 `KB_TAG_SKIP` / `keyboardPatch` 的白名单里加类名。
|
|
151
|
+
|
|
152
|
+
### 显示模式:明亮 / 黑暗 / 跟随系统(2026-11-09)
|
|
153
|
+
|
|
154
|
+
页头右上角、名字按钮右边是三段式开关 `☀ 明亮` / `☾ 黑暗` / `跟随系统`:
|
|
155
|
+
|
|
156
|
+
- **只换一套变量,不逐条改样式**:整套配色从一组表面色变量派生(`--ds-bg` 页面底 / `--ds-surface` 面板与卡片 / `--ds-surface-2` 列与次级块 / `--ds-line` 线 / `--ds-shadow` 投影),两套主题各自赋值,其余线条与浅底仍靠 `color-mix(in srgb, currentColor N%, transparent)` 自动算。明亮:`#eef1f5` 底 / `#ffffff` 面板 / `#dbe1ea` 线;黑暗:`#0e1014` 底 / `#181b22` 面板 / `#1e222a` 次级块 / `#2c323d` 线 / `#e6e8ee` 字 / `#7ba2ff` 强调色。
|
|
157
|
+
- **层次是「底 < 次级 < 表面」**(2026-11-09 调过):原来浅色底是 `#fbfbfc`、面板 `#ffffff`,两者差 2%,整页看着一片白、卡片和背景分不开;现在浅色底压到 `#eef1f5`,并给面板/卡片/KPI 加了 1px 投影(`--ds-shadow`),深色下也是「近黑底 + 稍亮面板 + 更亮列底」。看板列用 `--ds-surface-2`、卡用 `--ds-surface`,卡才浮得起来。
|
|
158
|
+
- **选择记在浏览器里**:`localStorage['dsh-studio:theme']`,刷新、关标签页再回来都还在;非法值回落到 `跟随系统`。
|
|
159
|
+
- **跟随系统先跟宿主**:面板长在宿主页面里,只看操作系统的 `prefers-color-scheme` 会出现「宿主白、工作区黑」的接缝。所以 `跟随系统` 先读宿主 `document.body` 的实际背景亮度(`hostScheme()`),宿主没给出明确信号才退回 `matchMedia`;两条路径都监听变化(`change` 事件 + 1.5s 轮询),不用刷新。
|
|
160
|
+
- 换肤范围是工作空间面板本身(`[data-dsh-studio]` 子树);宿主 GUI 自己的侧栏与顶栏不受插件控制,仍按宿主主题走——所以「强制黑暗 + 宿主是浅色」时,只有工作区这一块变深,这是浏览器里能做到的边界。
|
|
161
|
+
- 深色下单独调过的两处:`需求变更被驳回` 的红色 chip 从 `#a3282b` 提到 `#ff9a9d`,否则在 `#14161b` 上几乎看不见;浅色下则压到 `#b3282c` 保证对比度。
|
|
162
|
+
- **不要用系统颜色关键字(2026-11-09 修的坑)**:抽屉和弹窗原来写的是 `color:CanvasText`、`--ds-surface:Canvas`。`Canvas`/`CanvasText` 跟的是**操作系统**的深浅色,不是我们自己的 `data-theme`——系统是深色时 `CanvasText` 是白字,用户选「明亮」后抽屉就是白底白字,看板详情、专家编辑全都看不见内容。现在改成 `color:inherit` + `--ds-surface:#ffffff`,文字颜色只跟 `data-theme` 走;同时给两套主题加了 `color-scheme:light|dark`,`<select>`、`<input>`、滚动条这些原生控件才会跟着我们的主题画,而不是跟着系统画。
|
|
163
|
+
|
|
164
|
+
「设置 → 专家团」把若干专家编成一支队伍(团长 + 成员,最多 12 人):
|
|
165
|
+
|
|
166
|
+
- 团长负责拆活、给成员派活、汇总结论;成员之间用 `@对方名字` 接棒。
|
|
167
|
+
- 一列可以交给一个专家团,卡片进列时由团长接第一棒。
|
|
168
|
+
- 团队协作靠评论里的 @ 流转:`@架构师` → 架构师回复并 `@开发工程师` → 研发接手,每一步都有会话与评论记录。
|
|
169
|
+
|
|
170
|
+
## 技能库(skills-manager)
|
|
171
|
+
|
|
172
|
+
「设置 → 技能库」是这家公司的可复用做法库,每条技能有:技能名(kebab-case)、什么时候该用、补充触发条件、标签、Markdown 正文、启用开关。
|
|
173
|
+
|
|
174
|
+
- **落盘即生效**:保存时会写 `<DSH_HOME>/skills/<名字>/SKILL.md`(默认 `~/.dsh/skills/…`),DSH 的技能文件系统提供方会扫描这个目录,所以任何 agent 都能用 `skill` 工具加载它——不是只存在面板里的一段文本。
|
|
175
|
+
- **专家挂技能**:在「专家」表单里点技能库的 chip 就能挂上;派单时提示词会写「你可以使用的技能:<名字>(<用途>)」,让专家知道去加载哪条。
|
|
176
|
+
- **专家能自己管理技能**:运行时可用 `studio_skill_list` / `studio_skill_get` / `studio_skill_save` / `studio_skill_delete` 读写技能库。摸出来的做法(命令、坑、检查清单)用 `studio_skill_save` 存下来,下次别的专家直接能用。
|
|
177
|
+
- **上传技能**:面板上「上传技能」接受单个 `SKILL.md`(`.md` / `.markdown` / `.txt`)或包含 `SKILL.md` 的 `.zip` 技能包,走 `POST /studio-api/skills/upload`。
|
|
178
|
+
- 名字优先级:frontmatter `name:` → 文件名 → 首行 `# 标题` → 哈希;frontmatter 里的 `description` / `whenToUse` / `tags` 都会带进库,正文取 `---` 之后的部分。
|
|
179
|
+
- 同名默认**拒绝**(不静默覆盖),勾上「覆盖同名技能」才会替换;上传成功后立即落盘,列表里能看到「N 位在用」。
|
|
180
|
+
- `.zip` 由系统 `unzip` 解包,只取包内任意层级的 `SKILL.md`,其余文件不入库;服务器没有 `unzip` 时会给明确提示让人改传 `.md`。
|
|
181
|
+
- 单文件上限 1.4MB(正文本身截断到 6 万字),失败信息都是可读中文(空文件、坏 zip、名字不合法、同名冲突)。
|
|
182
|
+
- 「重新写入磁盘」用于把库里所有启用的技能重新刷一遍文件(例如有人手删了 `~/.dsh/skills`)。
|
|
183
|
+
- 服务启动时也会自动 sync 一次,库里的技能不会因为磁盘被清理而消失。
|
|
184
|
+
|
|
185
|
+
## 自动派单(专家 agent)
|
|
186
|
+
|
|
187
|
+
在「设置 → 自动派单」里打开开关,并勾选**哪些列**自动派单(默认 待方案 / 待开发 / 待测试)。
|
|
188
|
+
|
|
189
|
+
- 派单实现:一卡一会话(`studio-<taskId>-<专家id>`),会话由 `ctx.agents.create()` 建出来 —— 只有它带驱动循环,`agent.followup()` 才能真正唤醒一轮 turn。
|
|
190
|
+
- 会话必须显式给 `agentOptions.provider/model`,否则首轮直接报 `has no provider/model` 退出。
|
|
191
|
+
- 会话的工具来自 `agentPresets.resolve(id)` + `mount(scope, id)` 的 `setup` 回调;只写 `meta.agentPreset` 不会挂工具(会话里只剩看板工具)。
|
|
192
|
+
- 并发上限 `maxConcurrent`(1–8),槽位在 agent idle 时释放。
|
|
193
|
+
- 卡片抽屉里另有「马上处理 / 停止」,用于手动补派或在跳过自动列的卡上手动触发。
|
|
194
|
+
|
|
195
|
+
角色模型在「设置 → 角色与模型」里配,也可以在「设置 → 专家」里给单个专家指定模型,优先级:专家模型 → 角色模型 → 宿主默认。支持 `provider/model` 或裸模型名(裸名借用默认 provider)。
|
|
196
|
+
|
|
197
|
+
## GitLab 联动(多项目)
|
|
198
|
+
|
|
199
|
+
「设置 → GitLab 项目」可以加任意多个仓库,每行配:名称、项目路径(`group/project`)、服务器地址、Access Token(`api` 权限)、只同步的标签、是否自动同步。
|
|
200
|
+
|
|
201
|
+
- 服务器地址与 Token 留空时回落到全局值(同一台 GitLab 的多个项目只需填一次 Token)。
|
|
202
|
+
- 「测试」按钮单独验证某个仓的连通性;「立即同步」只同步该仓,也可以点全局的「全部同步一次」。
|
|
203
|
+
- 拉取 open Issue 建卡,按 `(项目, iid)` 去重;每仓各自记录 `lastSyncAt` / `lastSyncError`,互不影响。
|
|
204
|
+
- 「每分钟自动同步」按仓开关;卡片状态变化时,会往**这张卡的来源仓**写一条 Issue note(如 `需求&任务工作空间:待方案 → 待开发(架构)`)。
|
|
205
|
+
|
|
206
|
+
## 飞书联动(多机器人)
|
|
207
|
+
|
|
208
|
+
「设置 → 飞书机器人」可以加多个群机器人(飞书群 → 添加机器人 → 自定义机器人,拿 Webhook):
|
|
209
|
+
|
|
210
|
+
- 每个机器人有:名称、Webhook、**收哪些事件**(卡片状态变化 / 缺陷 SLA 超时 / 自动开缺陷 / 版本发布,可多选)、启用开关。
|
|
211
|
+
- 群按需订阅:研发群只勾「自动开缺陷 + SLA 超时」,产品群只勾「版本发布 + 状态变化」,就不会互相刷屏;一项都不勾等于这个群静默(仍可用「测试」发一条自检消息)。
|
|
212
|
+
- 老机器人(改造前建的)默认四类全收,与之前的行为一致;`notifyOnChange: false` 的只关掉「状态变化」这一类。
|
|
213
|
+
- 「发一条测试消息」可以只测某一个,也可以全部测一遍。
|
|
214
|
+
- 自建应用 App ID / App Secret / chat_id 字段保留,用于后续做多维表格镜像。
|
|
215
|
+
- 现在会推的消息:卡片状态变化(含「阻塞 / 解除阻塞」)、缺陷 SLA 超时升级、集成通道连续失败、**用例失败自动开出的新缺陷**、**版本发布(含强制发布原因)**。正文统一走 `feishuText()`,字符串和 `{ text }` 两种写法都能正确落到群里(以前会发成 `[object Object]`)。
|
|
216
|
+
|
|
217
|
+
## 内部市场
|
|
218
|
+
|
|
219
|
+
「市场」页维护公司自己的插件目录:显示名 + 包名/路径(npm spec 或 `link:/abs/path`)+ 说明。
|
|
220
|
+
安装/卸载都委派给 `dsh plugin --profile <本 profile> add|remove`,因此 pnpm lockfile 与 profile 的 bundle 列表保持一致。
|
|
221
|
+
|
|
222
|
+
- 安装成功后需要**重启 DSH Web** 才会加载新插件(界面会提示)。
|
|
223
|
+
- 卸载按包名执行;目录条目里是 `link:` 路径时会回读该目录的 `package.json` 拿包名。
|
|
224
|
+
|
|
225
|
+
## 需求对象与统一编号
|
|
226
|
+
|
|
227
|
+
需求是整条链路的锚点,`需求` 页就是它的工作台。
|
|
228
|
+
|
|
229
|
+
- 需求编号按年递增:`REQ-2026-001`。挂到需求下的卡按类型各自递增:`DEV-001`、`TEST-001`、`BUG-001`、`DES-001`、`OPS-001`,未挂需求的卡没有编号。
|
|
230
|
+
- 需求的字段:标题、正文、业务项目/迭代、负责人、优先级、状态(草稿/进行中/已交付/已关闭)、PRD / 原型 / 验收标准链接。
|
|
231
|
+
- **需求明细按 Markdown 渲染并完整展示**(2026-11-09):右侧「需求明细」面板用和评论、交付物同一套 `renderMarkdown`(标题/列表/粗体/代码块/表格/引用都认),不再塞进 180px 的滚动框里截断;编辑表单的正文框改成 10 行,placeholder 里写了支持的语法,功能点的描述也一样按 Markdown 渲染。
|
|
232
|
+
- 需求页右侧一眼看到:这张需求下有多少卡、几阻塞、几个未结缺陷、验收清单完成度、用例通过率、交付物与 MR 数量,以及每张卡当前在哪个列、谁在处理。
|
|
233
|
+
- 卡片抽屉里会显示 `编号` 与 `所属需求`,反向也能从一张卡追回需求。
|
|
234
|
+
|
|
235
|
+
## 文档与基线(评审通过才生效)
|
|
236
|
+
|
|
237
|
+
需求详情里有「文档与基线」:每份 PRD / 原型 / 验收标准都能登记版本(`v1.0`、`v1.1`…)与正文。
|
|
238
|
+
|
|
239
|
+
- 点「设为基线」= 这一版评审通过、正式生效;同一时间只有一份基线,需求从「草稿」自动转「进行中」。
|
|
240
|
+
- 基线会把当时的正文**快照下来**,之后有人改文档不会影响已经生效的版本。
|
|
241
|
+
- **基线可以逐行对比**(2026-11-13):基线框里点「对比基线」,拿最新一篇 PRD 和基线正文做行级 diff,绿加红减、标题行给出 `+N 行 / −M 行`;纯前端计算,不改任何数据,超过 800 行只给整体说明,避免卡页面。
|
|
242
|
+
- 基线之后新建的卡会记下 `reqBaseline`(版本 + 时间 + 标题),所以「这张卡是照哪一版做的」永远查得到。
|
|
243
|
+
- 「从链接拉正文」会调服务器上的 `lark-cli` 取飞书文档正文(`lark-cli docs +fetch --doc <链接> --doc-format markdown`);本机已装 1.0.99 并绑定了 CLI 应用(bot 身份即可读)。标题栏为空时会把文档标题自动填进去。服务器没装或应用没被加进文档协作者时,页面会如实显示原因,改成手动粘贴即可。
|
|
244
|
+
- 注意:飞书文档里**内嵌的多维表格**在 Markdown 里只有 `<sheet token=…>` 占位,正文能全量拿到,表格要单独按 token 读。
|
|
245
|
+
|
|
246
|
+
## AI Coding 执行记录(谁照着哪版改了什么)
|
|
247
|
+
|
|
248
|
+
研发这一棒不能只留一句「做完了」。每张卡上有一份执行记录,用来回答审计问题:**用了什么 Coding Agent、哪个模型、在哪个分支、改了哪些文件、单测跑成什么样、谁评审、谁合并**。
|
|
249
|
+
|
|
250
|
+
- 研发 agent 改完代码调 `studio_task_coding`(action=update)登记 `agent` / `model` / `branch` / `context`(依据的基线版本或方案)/ `commits` / `files` / 单测总数与通过数;人也可以在抽屉里「手动补录执行记录」。
|
|
251
|
+
- 评审时架构师调 `studio_task_coding`(action=review)写 `approved` 或 `changes_requested` + 说明;抽屉里也有「评审通过 / 要求修改」按钮。
|
|
252
|
+
- 合入后 action=merge 记下谁合的、什么时候合的。
|
|
253
|
+
- 抽屉里的「AI Coding 执行记录」会显示一行摘要(agent · 模型 · 分支 · 单测 x/y · 评审结论)、提交列表、改动文件(新增绿边/修改橙边/删除红边),卡片上显示 `⌥ 文件数`。
|
|
254
|
+
- 项目页与 `/metrics` 的 `coding` 指标汇总:有执行记录的卡数、改动文件总数、单测数量与通过数、评审通过数、已合并数。
|
|
255
|
+
- 卡片是按基线开的,提示词里会带 `reqBaseline`,所以「评审时看的代码是不是照最新 PRD 写的」可以直接对照。
|
|
256
|
+
|
|
257
|
+
## 缺陷自动闭环(用例失败 → 缺陷 → 回归)
|
|
258
|
+
|
|
259
|
+
缺陷不是人工另建的一种「任务类型」,而是自动化测试的直接产物。
|
|
260
|
+
|
|
261
|
+
- 测试同学在卡上用 `studio_task_case`(或抽屉里的「测试用例」)登记用例并回填 `pass` / `fail` / `skip`。
|
|
262
|
+
- **任何一条用例被判 `fail`,服务端立刻自动开一张缺陷卡**:挂同一条需求(编号 `BUG-00n`)、同一个项目/迭代/业务领域,`parent` 指向这张测试卡,`bugSource` 记下是哪条用例、期望、实际结果、谁判的。测试卡上同时留一条评论,缺陷卡上带「复现:执行 TEST-001 的用例 …」。
|
|
263
|
+
- 同一条用例重复判失败**不会重复建卡**,只在原缺陷卡上追加一条「用例再次失败」的评论。
|
|
264
|
+
- 缺陷卡修好推进到「待测试」时,**平台自动把来源用例重置为 `pending`** 并写「回归中(BUG-00n 已修复待验证)」,测试只需复跑这一条;两边都会收到系统评论。
|
|
265
|
+
- 抽屉的「其它信息」里,测试卡显示「关联缺陷」(编号 + 当前列),缺陷卡显示「来源用例」(编号 · 用例 · 判失败时间 · 谁判的)。
|
|
266
|
+
- 测试环节的提示词也改了:明确告诉测试 AI「判 fail 会自动建缺陷,不要手工再建一张」,缺陷卡进测试列时会告诉它该复跑哪一条。
|
|
267
|
+
|
|
268
|
+
## 产物归档到飞书云盘(每个需求一个文件夹)
|
|
269
|
+
|
|
270
|
+
产物不是留在卡片里的一段文字,而是落进飞书云盘、能被同事直接打开的文件。
|
|
271
|
+
|
|
272
|
+
- 根文件夹由「设置 → 飞书机器人 → 云盘归档」里的「云盘根文件夹」决定(默认指向公司共享的 `项目需求&缺陷`)。
|
|
273
|
+
- **新建需求时自动在根文件夹下建一个同名文件夹**,命名是 `<编号> <标题>`,例如 `REQ-2026-001 会员积分抵扣`;已经建过的直接复用,不会重复建。
|
|
274
|
+
- 每件交付物(`studio_task_artifact` 登记的 requirement / design / mr / cases / report / release …)都可以带一段 **Markdown 正文**;登记后平台把它写成文件夹里的一个 `.md` 文件,命名 `<卡片编号>-<类型>-<标题>.md`,并把文档链接回写到交付物上。
|
|
275
|
+
- **同名交付物再次归档是覆盖,不是新建**:文档 token 不变、链接不变,正文更新到最新,所以「持续更新同一份产物」是默认行为。
|
|
276
|
+
- 抽屉「交付物」区块里每行显示文档链接与「归档 / 更新」按钮;需求页有「交付物归档(飞书云盘)」区块,可以一键「归档 N 件交付物」把积压的产物补齐。
|
|
277
|
+
- 授权依赖宿主机上的 `lark-cli`(已装在 `/usr/local/bin/lark-cli`)与**用户身份**授权:`lark-cli auth login --domain drive,docs,markdown,wiki`。bot 身份没有云盘权限,不要用 bot 建文件夹。
|
|
278
|
+
- 用户 token 约 2 小时、refresh token 7 天;过期后归档会失败并在交付物上记 `doc.error`,重新走一次 `auth login` 即可。
|
|
279
|
+
- **没挂需求的卡也能归档**:会归到根文件夹下自动创建的「未挂需求的产物」文件夹(token 缓存在 `state.fallbackFolder`),不再直接报错。
|
|
280
|
+
- **正文可以从文件回读**:交付物只填了 `ref`(比如 `docs/xxx.md`)而没填正文时,归档会去工作区把文件内容读出来(≥2MB 的不读、正文截 6 万字),并把读到的正文回写进交付物——所以看板上能看到「正文 N 字」,下次也不必再读盘。
|
|
281
|
+
- 抽屉里每行有「正文」按钮,可以就地补写/修改正文,保存后自动重新同步到云盘。
|
|
282
|
+
- **用例与验收清单也按需取**(2026-11-13):`/studio-api/state` 里每张卡只带 `caseSummary`(总数/通过/失败)与 `checkSummary`(已勾/总数),正文在抽屉打开时一次取回(`POST /task/parts`,只读);卡片上的 `✅ 0/7`、`☑ 0/3` 和需求的用例覆盖率统计都改读计数。
|
|
283
|
+
- **评论也不随轮询下发**(2026-11-13):`/studio-api/state` 里每张卡只带 `commentCount`,正文等抽屉打开时按卡取一次(`POST /task/comments`,只读)。一条讨论动辄几百字,这一项又砍掉整包的 44%。
|
|
284
|
+
- **正文不随轮询下发**(2026-11-13):`/studio-api/state` 里每件交付物只带 `contentLength`,正文等点「阅读 / 正文」时才单独取(`POST /task/artifact` 的 `get` 动作,只读档位)。实测 21 张卡整包从 1.29 MB 降到约 588 KB(-54.5%),其中交付物正文原本占 733 KB;取过一次会缓存在抽屉里,编辑保存后缓存同步更新。
|
|
285
|
+
- **缺正文会在看板上报警**:任何交付物正文为空时,卡片底部出现红色「● 缺正文」(呼吸闪烁)、卡片边框转红;工具栏出现「只看缺正文 N」,一键筛出这些卡;抽屉「交付物」区块标题旁显示「● N 件缺正文」,缺正文的那一行整行淡红底并标注「缺正文」。新增产物没写正文时立刻就会亮,不需要手动刷新或点检。
|
|
286
|
+
|
|
287
|
+
## 测试执行(接口用例真跑,不是只回填结果)
|
|
288
|
+
|
|
289
|
+
用例不只是一行文字,写成接口调用就能被执行器真跑一遍。
|
|
290
|
+
|
|
291
|
+
- 一条用例可以带 `http`:`method` + `url`(可选 `headers` / `body` / `expectStatus` / `expectContains` / `timeoutMs`)。相对 url 会拼当前环境的 `baseUrl`。
|
|
292
|
+
- 执行器就跑在这台机器上(.218),不依赖外部 CI:测试列专家调 `studio_test_run`,人在抽屉里点「▶ 跑接口用例」或单条用例的 `▶`。
|
|
293
|
+
- 环境与安全在「设置 → 测试执行」:环境列表(名字 + baseUrl)、默认环境、单条超时、**允许访问的域名**(支持 `*.example.com`)。域名不在白名单直接判失败,请求根本不会发出去。
|
|
294
|
+
- 跑完回写:每条用例的 `result`(pass/fail)、最近执行的 `status / 耗时 / 响应片段`;卡片上留一条执行摘要评论;保留最近 10 次执行记录(`testRuns`)。
|
|
295
|
+
- 失败即开缺陷:复用「缺陷自动闭环」的去重逻辑,同一条用例反复失败只追加评论,不会堆卡。
|
|
296
|
+
- 边界:第一版只跑 HTTP 接口,不做 UI 自动化与真机设备农场;用例是**同步串行**跑的,几十条会更慢。
|
|
297
|
+
- 开跑前会先探一次环境(`probeEnv`):连不上/超时/域名不在白名单就整批不跑,免得环境没起却记一堆「用例失败」,把需求准出卡住。
|
|
298
|
+
|
|
299
|
+
### 跨卡用例总览(2026-11-13)
|
|
300
|
+
|
|
301
|
+
用例仍然挂在卡上,但不用一张张点开才能看全:顶部多了「用例」页签(`POST /studio-api/cases`,只读,每次都从台账现算,不另存一份)。
|
|
302
|
+
|
|
303
|
+
- 84 条用例摊成一张表:结果(✅ 通过 / ❌ 不通过 / ⬜ 没跑)、标题、所属卡片编号、所属需求、功能点、接口还是手工、优先级、最近一次跑的时间。
|
|
304
|
+
- 三个筛选可以叠加:关键字(用例 / 卡片 / 需求 / 功能点)、需求下拉、结果下拉;标题条实时显示「N 条 · 通过 / 不通过 / 没跑」。
|
|
305
|
+
- 点任意一行直接打开那张卡的抽屉,接着就能改结果或重跑;行尾的「明细」就地展开期望、最近一次结论、错误与响应片段(`lastError` / `lastPreview` 随 `/cases` 一起下发),不用点进抽屉。
|
|
306
|
+
- 卡片上的用例变了(跑了一条、加了一条、生成一批)页面会自动重取;没变化就不重复请求。
|
|
307
|
+
- **失败补跑一键到位**:工具栏上有「↻ 只跑上次失败的 N 条」——只挑「上次不通过、且有 http 定义」的用例重跑,没失败时按钮是灰的。修完 bug 的回归就点它,不用再翻列表勾选。
|
|
308
|
+
- **批量回归已经能做**:勾几条(或「全选可跑的 N 条」)→「▶ 跑选中的 N 条」,服务端按卡分组、一卡一轮串行跑完,回写各自的卡,再汇总「跑了 X 张卡 / Y 条:通过 a,不通过 b,自动开缺陷 c」;一次最多 60 条。手工用例(没写 `http`)勾不上——跑了也没意义。
|
|
309
|
+
- **最近回归留痕**(2026-11-13):每次批量跑完记一条(时间、谁跑的、几张卡 / 多少条、通过 / 不通过 / 自动开缺陷),保留最近 20 次,列在「用例」页的工具栏下方一行小 chip,有失败的那次标红;夜里定时跑的那批 `by=定时回归`,早上打开页面就知道昨晚跑成什么样。记录放在 SQLite 的 side 键里,不占 state、不涨 revision。
|
|
310
|
+
- **跑之前平台自己先探环境**(2026-11-13):开跑前对 `baseUrl` 发一个请求,连不上就直接回「测试环境「X」不可达(ECONNREFUSED):一条用例都没跑」,**不会把整批用例刷成失败**。任何 HTTP 响应(含 404/500)都算活着——那是服务在跑,只是接口不对。
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
## 用例自动生成(从功能点长出来,不是凭空写)
|
|
314
|
+
|
|
315
|
+
测试用例不用一条条手敲:需求拆了功能点、每个功能点带验收条件,平台就能照着生成。
|
|
316
|
+
|
|
317
|
+
- 抽屉「测试」页签点「✨ 按需求生成」→ `POST /task/cases/generate`;agent 侧用 `studio_task_generate_cases`。
|
|
318
|
+
- 规则:卡挂了需求、需求里有功能点,就按每条**验收条件**生成一条用例(标题 `验证:条件原文`,期望就是条件本身,`result=pending`);这条点没有验收条件时退回「验证《功能点标题》主流程」。
|
|
319
|
+
- 生成时按标题去重,重复点不会堆一遍;返回 `added / total / reason`,界面会提示「已生成 N 条」或「没生成用例:原因」。
|
|
320
|
+
- 生成的用例自动带上 `pointId`,直接进可追溯矩阵的统计口径——生成完就能在需求页看到这条点的覆盖变化。
|
|
321
|
+
|
|
322
|
+
## 缺陷 SLA 与疑似重复
|
|
323
|
+
|
|
324
|
+
缺陷有响应时限,重复的缺陷不该开成两张卡。
|
|
325
|
+
|
|
326
|
+
- SLA 默认按严重级别:`p0` 4 小时、`p1` 24 小时、`p2` 72 小时、`p3` 168 小时;在 `config.sla` 里可改,`bugSla()` 按 `createdAt + hours` 算出 `dueAt`,并给状态 `ok / due_soon / overdue / met / late`。
|
|
327
|
+
- 看板卡片上直接显示 `⏰ 剩 Nh`(临近时限变橙、逾期红底白字 `⏰ 逾期 Nh`),悬停能看到应修时间。
|
|
328
|
+
- 建缺陷时 `findDuplicateBug()` 会比对「同类型 bug + 未完成 + 标题归一后相同」的卡:命中就把新卡的 `duplicateOf` 指向老卡、加 `疑似重复` 标签,并在老卡上留一条评论指路;人可以在抽屉里改用例把两张合成一张(`POST /task/duplicate`、工具 `studio_bug_duplicate`)。
|
|
329
|
+
- 边界:只做标题归一(去 `[用例失败]` 前缀、标点、空格、emoji、大小写),不做语义相似度;也不自动合并,只标出来让人判断。
|
|
330
|
+
|
|
331
|
+
## 发布 tag 与冒烟清单
|
|
332
|
+
|
|
333
|
+
发布不只是「记一笔」:要能对上确定的代码,并且冒烟跑过。
|
|
334
|
+
|
|
335
|
+
- 版本对象新增 `tagName`(发布标签)与 `commitSha`(对应提交,选填),在版本页信息表单里填。
|
|
336
|
+
- 「发布冒烟清单」:逐条勾(`x/y 项已过`),走 `POST /release/smoke`(`add / toggle / remove`)。
|
|
337
|
+
- 门禁新增两条拦截:**没填发布标签**、**还有 N 项冒烟没过**(列出未过的条目名)。也就是说 tag 不填、冒烟没过,`/release/approve` 与 `/release/publish` 都会被拒。
|
|
338
|
+
- 边界:tag 只是记录与校验,平台不会替你打 tag 或推代码;冒烟清单也是人勾,不是自动跑(接口用例另走「测试执行」)。
|
|
339
|
+
|
|
340
|
+
## 执行成本与提示词版本
|
|
341
|
+
|
|
342
|
+
- 每位专家的提示词会算一个 `promptVersion`(提示词内容的 sha1 前 8 位),存进专家配置;专家改了提示词,版本号就变。
|
|
343
|
+
- 每次派单成功(`deliver` 之后)记一条运行日志:任务、会话、专家、模型、提示词版本、开始时间;会话 settle 时补上结束时间与耗时(`RUN_LOG` 最多留 300 条)。
|
|
344
|
+
- `GET /costs`(工具 `studio_costs`)返回总次数、已结束次数、平均耗时,以及按专家 / 按模型 / 按提示词版本 / 按天的次数分布。
|
|
345
|
+
- 边界:宿主没有把 token 用量回传给插件,所以这里统计的是**次数与时长**,不是钱;要算钱得等宿主暴露用量。
|
|
346
|
+
|
|
347
|
+
## 负载与燃尽(里程碑视图的轻量版)
|
|
348
|
+
|
|
349
|
+
- `GET /workload`(工具 `studio_workload`):每位专家 `running / open / done7d / overdue`,外加 `busy` 汇总——谁被压住了、谁闲着,一眼能看出来。
|
|
350
|
+
- `GET /burndown?days=14&project=`(工具 `studio_burndown`):近 N 天每天的 `created / done / remaining` 序列,外加 `now.remaining`;效能页有一排迷你柱状图。
|
|
351
|
+
- 效能页新增三块面板:团队负载、燃尽(近 14 天剩余)、AI 执行成本(平均执行时长 + 按模型/提示词版本的次数)。
|
|
352
|
+
- 边界:燃尽是「剩余卡数」口径,不是故事点;负载只统计专家会话,不含人。
|
|
353
|
+
|
|
354
|
+
插件向主会话注册了 34 个 `studio_*` 工具,任何会话都能读写看板:
|
|
355
|
+
|
|
356
|
+
看板与配置:`studio_config_get` / `studio_config_set` / `studio_task_list` / `studio_task_get` / `studio_task_create` / `studio_task_update` / `studio_task_block` / `studio_task_unblock` / `studio_task_comment`
|
|
357
|
+
|
|
358
|
+
需求:`studio_requirement_list` / `studio_requirement_get` / `studio_requirement_save` / `studio_requirement_folder`(建/复用需求文件夹并批量归档产物)
|
|
359
|
+
|
|
360
|
+
交付与验收:`studio_task_artifact`(登记交付物:需求/方案/MR/用例/报告/发布说明)/ `studio_task_check`(勾验收清单,附证据)/ `studio_task_case`(登记测试用例并回填 pass/fail/skip)/ `studio_test_run`(真跑接口用例,失败自动开缺陷)/ `studio_task_coding`(登记 AI Coding 执行记录与评审结论)
|
|
361
|
+
|
|
362
|
+
项目与技能:`studio_project_list` / `studio_project_save` / `studio_skill_list` / `studio_skill_get` / `studio_skill_save` / `studio_skill_delete`
|
|
363
|
+
|
|
364
|
+
版本与发布:`studio_release_list` / `studio_release_get` / `studio_release_save` / `studio_release_publish`(门禁不过会被拒,返回逐条拦截原因)
|
|
365
|
+
|
|
366
|
+
效能:`studio_analytics`(一次通过率、返工率、周期分布、每列停留、AI 产出采纳情况)/ `studio_workload`(团队负载)/ `studio_burndown`(燃尽)/ `studio_costs`(派单次数与时长)
|
|
367
|
+
|
|
368
|
+
测试与缺陷:`studio_task_generate_cases`(按功能点验收条件生成用例)/ `studio_bug_duplicate`(标记疑似重复缺陷)
|
|
369
|
+
|
|
370
|
+
专家会话通过系统提示词段(order 70)知道自己的身份、职责、@ 规则与技能库用法,并用同一批工具汇报产出、推进状态、沉淀技能。
|
|
371
|
+
|
|
372
|
+
## 沙箱与代码隔离(一卡一会话,一卡一 worktree)
|
|
373
|
+
|
|
374
|
+
两件事是分开的:**沙箱**管「这个会话能碰什么」,**worktree** 管「这个会话在哪写代码」。
|
|
375
|
+
|
|
376
|
+
### 沙箱(DSH permission preset)
|
|
377
|
+
|
|
378
|
+
DSH 自带操作系统级沙箱(`dsh-sandbox-local`,Linux 上走 bwrap,退路是 Landlock)。dsh-studio 以前把它写死成 `danger-full-access`,现在可配:
|
|
379
|
+
|
|
380
|
+
- 「设置 → 自动派单 → 沙箱档位」:`read-only`(能看不能改)/ `workspace-write`(默认,只写自己的工作目录)/ `danger-full-access`(无沙箱,危险)。
|
|
381
|
+
- 单个专家可以覆盖:专家表单里的「沙箱档位」,空 = 跟随全局。
|
|
382
|
+
- 派单时落到 `permissionPresets.set(agent.session, 档位)`,`.218` 上 `/usr/bin/bwrap` 已装,`workspace-write` 实测能挡住往工作区外写文件(拒绝信息原样出现在 agent 的 bash 结果里)。
|
|
383
|
+
- 看当前运行中的会话用了什么档位:`GET /studio-api/agents/policy`。
|
|
384
|
+
|
|
385
|
+
### git worktree 隔离
|
|
386
|
+
|
|
387
|
+
「设置 → 自动派单 → 代码隔离 = worktree」后,每张卡有自己的目录与分支:
|
|
388
|
+
|
|
389
|
+
- 前提:卡挂在**配了「本机代码目录」的业务项目**上(设置 → 项目与迭代 → 本机代码目录 + 基线分支)。
|
|
390
|
+
- **一个项目多个服务(2026-11-09)**:项目表单里可以加多条「服务」,每条有 `服务名 / 本机 git 路径 / 基线分支 / 关联 GitLab 仓库`;卡的「计划」里选一个服务,worktree 就从**那个服务的路径**切,基线也用它自己的分支。卡上不选服务时退回项目级「本机代码目录」;老项目(只有项目级目录)会自动折成一个 `默认服务`,行为不变。
|
|
391
|
+
- 服务在看板上的样子:卡上多一个 `📦 服务名` chip(鼠标悬停看路径),看板顶部多一个「全部服务 / 某个服务」筛选(跟着项目走),卡详情「其它信息」里有一行「服务」;服务被删掉或改名后,卡上残留的 id 会被后端清成「未指定」,chip 上显示「服务已失效」。
|
|
392
|
+
- 服务校验:建卡/改卡时传一个不属于该项目的服务 id,会被**静默清空**(不会让卡挂着一个不存在的路径);保存项目时删掉某个服务,该项目下所有卡上指向它的 `service` 一并清空。
|
|
393
|
+
- 智能体侧:`studio_project_list` 每行列出服务(`服务名→路径`),`studio_project_save` 可以整组写入服务(`services: [{name, repoPath, baseBranch, repoId}]`,部分更新语义);派单提示词里会写「服务:<名字>(本机代码目录 <路径>,基线分支 <分支>)」。
|
|
394
|
+
- 派单时自动 `git worktree add -b task/<编号> <root>/<编号> <基线分支>`,会话的 cwd 就是那个目录,提示词里也写明「只在这个目录里改代码、提交到这条分支」。
|
|
395
|
+
- `worktreeRoot` 留空 = 工作区同级目录 `dsh-worktrees`。
|
|
396
|
+
- 「卡完成时」选 `on_done` 会自动 `git worktree remove --force` + `git branch -D`;默认 `manual`,留着等人看 diff。
|
|
397
|
+
- 抽屉里有「代码隔离」区块:`建 worktree` / `看 git worktree 列表` / `回收 worktree`,也可以走 `POST /studio-api/task/worktree {id, action}`。
|
|
398
|
+
- 真机验证(`tools/verify-worktree.mjs`,14/14 PASS):建卡 → 自动出现 `<workspaceDir>/DEV-001` + 分支 `task/DEV-001` → 主工作区保持干净 → 回收后目录与分支都清掉。
|
|
399
|
+
- 多服务验证(`tools/verify-services.mjs`,20/20 PASS):一个项目挂两个服务(两个真实 git 仓库),卡选服务 B → worktree 只建在 B 的仓库、分支 `task/<编号>` 只存在于 B、A 仓库没有多余 worktree;老项目自动迁移成默认服务;建卡/改卡传非法服务被清空;删掉服务后卡上的 `service` 被清空;回收后配置复原(脚本会先读基线,发现有卡挂着读不到的项目就直接 ABORT,避免用空列表冲掉真项目)。
|
|
400
|
+
- **服务依赖(2026-11-09)**:项目表单里每条服务下面多一行「依赖」chip,勾上它依赖的**同项目内**服务(最多 10 条,自己不能依赖自己,互依赖会自动留一条边去环)。规则:被依赖服务的卡没全部完成时,本服务的卡过不了准出——门禁多一条「依赖服务还没完成:<服务>(N 张:卡号/标题)」;派单提示词写「本服务依赖:A、B(这些服务的改动先落地…)」;项目列表行显示「N 条依赖」,任务详情「服务」行也带「依赖 …」。智能体侧 `studio_project_save` 的 `services[]` 支持 `dependsOn: []`。
|
|
401
|
+
- 服务依赖验证(`tools/verify-service-deps.mjs`,18/18 PASS):保存带依赖的项目 → 依赖落库、自依赖丢弃、未知服务丢弃、互依赖只留一条边 → 建上游/下游/无依赖三张卡 → 只有下游卡的门禁出现「依赖服务还没完成」,上游与无关服务不受影响 → 上游推到完成后下游不再被拦(推完成要过门禁,脚本用 `override` 写明原因是「只用来代表上游服务已完成」)。
|
|
402
|
+
- 踩过的坑(2026-11-09):项目编辑面板里引用了未定义的 `services`,一点「编辑 / 迭代」整个工作区就白屏(渲染期抛错会把整棵插件树卸掉,不是局部报错)。修法是 `const services = draft === null ? [] : (draft.services ?? [])`。教训:这类新加的渲染分支,`node --check` 过得去不代表能跑,改完必须真点开一次看。
|
|
403
|
+
|
|
404
|
+
边界:worktree 只在「本项目本机有 git 仓库」时才有意义;远程仓库、GitLab 上的 MR 仍需 `glab` 或人工推送,平台不替你 push。
|
|
405
|
+
|
|
406
|
+
## 版本与发布管理(门禁 + 发布说明 + 回滚留痕)
|
|
407
|
+
|
|
408
|
+
版本是一个独立对象(`~/.dsh/dsh-studio.json` 的 `releases`),卡通过 `releaseId` 挂上去。
|
|
409
|
+
|
|
410
|
+
- 状态机:`draft` 草稿 → `candidate` 待发布 → `approved` 已审批 → `released` 已发布 → `rolled_back` 已回滚。已发布/已回滚的版本**锁定版本号与状态**,只能通过 `rollback` 改状态。
|
|
411
|
+
- **发布门禁**(`releaseGate`)不是给人看的提示,是硬拦截:卡全做完、没有阻塞标记、缺陷全部关闭、没有 `result=fail` 的用例,才允许 `审批通过` 与 `发布`;否则接口返回 `门禁没过:…`。警告(不拦)只看两类:交付物缺正文、整批卡一个交付物都没登记。
|
|
412
|
+
- **发布说明自动生成**:按需求/方案/研发/测试/缺陷/运维分组列出 `- 编号 标题`,生成后可以手改,失焦自动保存。
|
|
413
|
+
- **回滚**:必须写原因(进复盘记录),只有 `released` 能回滚。
|
|
414
|
+
- 三个入口:看板顶部「版本」页签(左清单 + 右详情 + 门禁面板 + 版本内卡片 + 挂卡)、抽屉「计划 → 所属版本」、agent 工具 `studio_release_*`。
|
|
415
|
+
- 接口:`GET /releases`(每条带 `gate`)、`POST /release/get|save|delete|link|notes|approve|publish|rollback`。
|
|
416
|
+
- 真机验证(`tools/verify-release.mjs`):**15 项全 PASS** —— 新建默认草稿 → 卡没完成时门禁不通过、审批/发布都被拒 → 卡完成后门禁通过 → 发布成功并记时间 → 发布说明自动汇总 → 回滚不带原因被拒、带原因成功且落库。
|
|
417
|
+
|
|
418
|
+
## 权限与审计(谁动了什么)
|
|
419
|
+
|
|
420
|
+
DSH 的 web 只有一张登录 cookie,所有人共用;所以平台自己记「当前身份」:页头 `👤 人` 可以改名字,存在浏览器 `localStorage`,每个请求带 `x-studio-user` 头。
|
|
421
|
+
|
|
422
|
+
- **成员名单**(设置 → 安全 → 权限与审计):三个人档——`管理员`(改配置、删数据、发布/回滚、装插件)、`成员`(建卡、推卡、评论、跑用例、登记交付物)、`只读`(只看)。
|
|
423
|
+
- **启用成员权限**后:名单里没有的人**还能看板**(避免名字对不上直接锁死),但一个写操作都做不了;成员碰管理员动作会被拒并说明原因。
|
|
424
|
+
- 没启用时所有人按管理员算,单机用法完全不变。
|
|
425
|
+
- **审计日志**:每个 `POST` 都记一条——时间、谁、动了哪个接口(有中文动作名)、关键字段摘要(id / title / status …,不记正文与密钥)、成功还是失败、HTTP 码。保留最近 1000 条,面板里可只看失败、按人筛、清空。
|
|
426
|
+
- 接口:`POST /access/save`(`{enabled?}` / `{member:{name,role}}` / `{agents:'inherit'|'bypass'}`)、`POST /access/delete`、`POST /audit/clear`、`POST /access/explain`(`{sessionId, tool}` → 这个会话调这个工具会被放行还是拦下、为什么);`GET /state` 里带最近 200 条 `audit`。
|
|
427
|
+
- 真机验证(`tools/verify-access.mjs`):**13 项全 PASS** —— 未启用时谁都能写、名单外人能读不能写、只读成员被拒、成员能建卡但改配置被拒、管理员能改配置、审计确实记下被拒者与成功动作。
|
|
428
|
+
|
|
429
|
+
### AI 专家也受名单约束(工具层权限)
|
|
430
|
+
|
|
431
|
+
以前这层只管浏览器面板,专家 agent 走宿主工具、谁也拦不住。现在工具调用也过同一张名单:
|
|
432
|
+
|
|
433
|
+
- 每个工具分三档:**读**(`studio_task_list` / `studio_task_get` / `studio_config_get` / `studio_analytics` / `studio_skill_get` …)、**写**(建卡、推卡、评论、登记交付物、跑用例、建 worktree,**没登记的工具一律按写处理**)、**管理员**(`studio_config_set`、`studio_release_save`、`studio_release_publish`、`studio_skill_delete`)。
|
|
434
|
+
- 调用方身份从会话名解析:派单会话固定是 `studio-<taskId>-<expertId>`,于是能判定「是哪个专家在调」。解析不出时按当前页头身份算。
|
|
435
|
+
- 名单里的专家按角色判:只读专家调写工具会被拒,错误里直接写「「架构师」是只读,这一步需要成员权限」。被拒也会进审计(`失败 403`)。
|
|
436
|
+
- **启用权限的那一刻会自动把现有专家登记成「成员」**,否则一开权限所有 AI 立刻停工;新加的专家需要自己去名单里补一条。
|
|
437
|
+
- 设置 → 权限与审计 里的勾选框 **AI 专家也受这份名单约束**:勾着是 `inherit`(默认),取消是 `bypass`(只记审计不拦,适合「全交给 AI」的团队)。
|
|
438
|
+
- 验证(`tools/verify-access2.mjs`,21 项全 PASS):权限关闭放行、开启自动登记专家、只读专家能读不能写、不能改配置、未登记工具按写判、名单外专家被拦、bypass 放行、恢复成员后可写。
|
|
439
|
+
|
|
440
|
+
## 集成健康(外部通道坏了要看得见)
|
|
441
|
+
|
|
442
|
+
飞书通知、GitLab、飞书云盘归档、代码隔离这四条通道,任何一次调用(成功 / 失败 / 跳过)都会记进内存健康表:
|
|
443
|
+
|
|
444
|
+
- 页头在有条异常时亮红:**⚠ 集成异常 N**,点一下直接跳到「设置 → 集成健康」,鼠标悬停能看到每条错误的原文。
|
|
445
|
+
- 「集成健康」页按通道列:状态点、最近一次调用、最近成功、最近失败、连挂次数、错误原文,以及一个 **再试一次** 按钮(GitLab → 立刻同步一次;云盘 → 挑一条带文件夹的需求重新归档;飞书 → 发一条测试消息;代码隔离 → 下次派单自动重试)。
|
|
446
|
+
- 判定口径:飞书通知失败会**自动重发一次**(隔 400ms),两次都失败才算异常;飞书 HTTP 200 但 body `code !== 0` 也按失败记;没配 webhook / 没装 lark-cli 记「跳过」,不算异常。
|
|
447
|
+
- 接口:`GET /integrations/health` → `{channels:[{channel,label,hint,ok,state,at,lastOkAt,lastErrorAt,lastError,failures,attempts,skipped}], broken, checkedAt}`;`GET /state` 里也有同一份 `integrations`。
|
|
448
|
+
- 语义:单条通道失败不会影响别的通道——GitLab 挂了照样能归档交付物,worktree 建失败只让这张卡退回普通工作区。
|
|
449
|
+
|
|
450
|
+
## 效能分析(过程指标,不是拍脑袋)
|
|
451
|
+
|
|
452
|
+
看板顶部「效能」页签,数据来自 `GET /analytics?days=7|30|90&project=<id>`,也可以让 agent 用 `studio_analytics` 读。
|
|
453
|
+
|
|
454
|
+
**版式(2026-11-09 重排,两轮)**:原来是 `repeat(auto-fit,minmax(300px,1fr))`,1180px 宽时四块面板挤一行、每块不到 300px,柱条被压成一条线。现在改成 12 栅格 + 三段分区,并且面板按内容高度排(`align-items:start`),不再为了等高在底部拉出一片空白:
|
|
455
|
+
|
|
456
|
+
| 分区 | 内容 |
|
|
457
|
+
| --- | --- |
|
|
458
|
+
| 质量 | 一次通过率 / 返工率 / 中位周期 / AI 交付采纳率;`repeat(4,minmax(0,1fr))`,900px 折两列、560px 折一列,每张卡左侧一条强调条(`box-shadow:inset 3px 0 0`,warn 时变红) |
|
|
459
|
+
| 流程与产能 | 左列 `ds-span-4` 上下叠「周期分布」「近 14 天完成」两块小面板,右列 `ds-span-8` 放「每列停留」——8 行列表把这一行填满,左边两块就不会被拉高 |
|
|
460
|
+
| 交付、成本与返工 | AI 产出质量 · 团队负载 · 燃尽(各 4 列)+ AI 执行成本(4 列)· 返工最多的卡(8 列) |
|
|
461
|
+
|
|
462
|
+
- 每块面板底部有一句 `.ds-an-foot` 底注(`margin-top:9px` + 虚线上边框),把「所以呢」写出来:周期分布给均值/中位/P90,每列停留直接点名最慢的一列,燃尽给累计建/累计完成/还剩几张。
|
|
463
|
+
- `.ds-an-row` 的列宽从 `84/1fr/64` 放到 `106/1fr/84`,专家名不再折行、右侧数字按列对齐。
|
|
464
|
+
- 两个迷你柱图 `.ds-spark` 高 78px、底边一条基线、柱宽 `70%`,日期标签隔天显示(`index % 2 === 0`),14 个标签不再挤成一排。
|
|
465
|
+
- 窄屏:`@media (max-width:1180px)` 把 4/5 列的面板折成 6 列(两栏),`(max-width:760px)` 全部一栏。
|
|
466
|
+
|
|
467
|
+
- **一次通过率**:已完成卡里,从没被退回过(没在任何列倒退过)的比例。低说明需求没想清楚就往下推。
|
|
468
|
+
- **返工率**:有一定推进过的卡里,出现过「倒退」(状态在流程里往回走)的比例;同时给出评审打回次数、测试打回次数,以及返工最多的卡(可直接点开)。
|
|
469
|
+
- **周期分布**:卡从创建到第一次进「已完成」的时长,按 ≤1 天 / 1–3 天 / 3–7 天 / >7 天分桶,并给中位数、均值、P90(中位数比均值抗极端值)。
|
|
470
|
+
- **每列停留**:每种状态的平均停留时长,用来找瓶颈列(比如「待评审」平均卡 2 天,说明评审是瓶颈)。
|
|
471
|
+
- **近 14 天完成**:每天完成的卡数迷你柱图,看交付节奏。
|
|
472
|
+
- **AI 产出质量**:交付物里由专家登记的比例(按登记人是否命中专家名册判定)、其中被人验收(`accepted`)的比例、缺正文条数;AI Coding 卡的评审通过/要求修改数、单测通过率、已合并数。
|
|
473
|
+
- 口径说明:返工只看 `task.history`(每条记的是**进入的新状态**),所以「同一列连续记录」会被合并,只有真正往回走才算倒退;`days` 决定窗口,但在办的卡永远算进来(否则看不到正在卡住的活)。
|
|
474
|
+
- 真机验证(`tools/verify-analytics.mjs`):**16 项全 PASS** —— 造一张「dev → design → dev → done」的返工卡后,返工率、返工最多的卡、周期分桶求和、每列停留 8 列、AI 交付物计数与缺正文计数、Coding 评审与单测汇总、项目筛选、7 天窗口、非法 `days` 回落 30、删卡后统计仍可用,全部对得上。
|
|
475
|
+
|
|
476
|
+
## 数据与运维
|
|
477
|
+
|
|
478
|
+
- 台账:**`~/.dsh/dsh-studio.db`(SQLite,WAL)**。每次写在同一个 `BEGIN IMMEDIATE` 事务里先插一份快照、再更新 `meta.state`,只保留最近 10 份;多进程同时写靠 `busy_timeout=5000` 排队,不会再互相覆盖。老数据在首次启动时自动从 `~/.dsh/dsh-studio.json` 导入(原文件保留)。
|
|
479
|
+
- 密钥:`~/.dsh/dsh-studio.secrets.json`(`0600`),只存 GitLab token 与飞书 webhook;台账里存的是空串,接口只回 `tokenSet` / `webhookSet`。
|
|
480
|
+
- 技能库:`~/.dsh/dsh-studio-skills.json`(tmp + rename 原子写),并镜像到 `~/.dsh/skills/<名字>/SKILL.md`。
|
|
481
|
+
- 服务:`/etc/systemd/system/dsh-web.service`(`Restart=always`),日志 `journalctl -u dsh-web -f`。
|
|
482
|
+
- 备份与回滚:界面「设置 → 安全 → 数据与备份」列出最近 10 份快照,点「回滚到这版」即恢复(回滚前会先给当前状态再存一份,不会丢)。接口 `GET /studio-api/snapshots`、`POST /studio-api/snapshot/restore {id}`(管理员)。
|
|
483
|
+
- 沙箱兜底:派单时会话默认走 `workspace-write`;只有显式配了 `danger-full-access` 才全开,配了认不出的档位也按 `workspace-write` 处理并记一条日志。
|
|
484
|
+
|
|
485
|
+
## 全模块体检(2026-11-08)
|
|
486
|
+
|
|
487
|
+
`tools/audit-modules.mjs` 在服务器上跑 19 项断言,覆盖:状态接口、配置结构、需求新建/编号、需求文件夹、需求文档与基线、任务新建(模板清单 + 编号)、推进与评论 @、阻塞/解除、验收清单、交付物登记与归档、测试用例执行、AI Coding 记录/评审/合并、项目与迭代指标、专家/专家团/列负责人/技能、仓库与机器人 CRUD、内部市场、派单状态、技能库磁盘、清理。当前 **19/19 PASS**。
|
|
488
|
+
|
|
489
|
+
这一轮发现并修掉的缺陷:
|
|
490
|
+
|
|
491
|
+
1. **交付物全是空的、看板上看不到**:`syncArtifact` 硬性要求「卡必须挂需求」,没挂需求的卡一律抛错 → 从未归档;且老交付物只有 `ref` 路径没有正文。修法:未挂需求归到「未挂需求的产物」文件夹;正文缺失时按 `ref` 从工作区回读文件并回写。
|
|
492
|
+
2. **老数据标题是文件路径、`ref` 丢失**:`sanitizeArtifact` 把路径搬进 `ref` 再把人可读的文件名留给标题;历史 7 条用 `tools/repair-artifacts.mjs` 按文件名规范化匹配补回正文。
|
|
493
|
+
3. **`/repos/save` 只认 `{project}` 字段**:发 `{repo}` 时会静默存下一条空记录。修法:两种字段都认,且名字/项目路径都为空时直接 400。
|
|
494
|
+
4. **纯中文名字每次保存都新建一条**:`cleanId` 对非 ASCII 名字返回随机 id → 改成按名字哈希生成稳定 id(仓库、专家、机器人、项目都受益)。
|
|
495
|
+
5. **保存接口不回实体**:`/experts/save`、`/teams/save`、`/projects/save`、`/repos/save`、`/bots/save` 现在都返回保存后的集合;仓库与机器人返回脱敏视图(只有 `tokenSet` / `webhookHint`,不回传密钥)。
|
|
496
|
+
6. 清掉了体检产生的重复项目、重复缺陷卡与探测数据。
|
|
497
|
+
|
|
498
|
+
## 字段显示规范(2026-11-08 逐字段体检)
|
|
499
|
+
|
|
500
|
+
设计原则:**「没有」要说清楚是什么没有,不能只留一个 `—` 或一片空白。** 人看到空白会以为坏了。
|
|
501
|
+
|
|
502
|
+
| 位置 | 空值时的显示 |
|
|
503
|
+
| --- | --- |
|
|
504
|
+
| 卡片「需求/背景/验收标准」 | 输入框保留可编辑,但空时缩成 4 行并给出填写提示 + 一行红字「这张卡还没有需求描述:专家只能按标题猜」 |
|
|
505
|
+
| 抽屉「其它信息」 | `未挂需求(无编号)` / `未挂需求` / `未归属项目` / `未排期` / `父任务:无` / `来源用例:无(不是用例自动开的缺陷)` / `关联缺陷:无` / `飞书会话:未关联` |
|
|
506
|
+
| 状态历史 | 中文列名(待方案、开发中…)+ 备注,不再显示 `dev@11-08 17:34` 这种原始 id |
|
|
507
|
+
| 评论作者 | 一律显示专家本名(`架构师`、`开发工程师`、`支付架构师`);历史里写成「架构」「🏛️ 支付架构师」的,加载时由 `canonicalAuthor()` 归一后落库 |
|
|
508
|
+
| 评论头像 | 人 = 姓名首字圆点,AI = 专家 emoji;只有专家才有角色/领域 chip |
|
|
509
|
+
| 评论正文 | 上限 20000 字(原 8000 会截断长报告);渲染为空时显示「(这条评论没有正文)」 |
|
|
510
|
+
| 项目卡 | `N 个迭代`、`inProgress`、`checklistRate`、`cycleDays` 都是后端算好的数字,不再出现 `[object Object] 个迭代` 与 `undefined%` |
|
|
511
|
+
| 版本内卡片状态 | 显示中文列名(已完成、待开发),不显示 `done` / `dev_ready` |
|
|
512
|
+
| 市场页 | 显示真实 profile(`profile web`);profile 由 `detectProfile()` 识别(扫描 `~/.dsh/profiles/*/package.json` 里装了 dsh-studio 的那个),因为宿主启动后 argv 里已经没有 `--profile` |
|
|
513
|
+
|
|
514
|
+
一键复验:`node /tmp/verify-fields.mjs`(在 .218 上跑)——11 项:profile、需求描述已补、产物无空正文、作者名归一、评论上限、状态中文名、项目指标三项、需求可读。
|
|
515
|
+
|
|
516
|
+
## AI 质量的口径(2026-11-13)
|
|
517
|
+
|
|
518
|
+
「效能 → 按专家看质量」的数字全部来自台账,不猜:
|
|
519
|
+
|
|
520
|
+
| 指标 | 怎么算 | 数据来源 |
|
|
521
|
+
| --- | --- | --- |
|
|
522
|
+
| 派单次数 | 每次派单成功记一条 | dispatcher 的 `RUN_LOG`(最近 300 条;**落盘在 SQLite side 键 `agent-runs`**,启动恢复,重启不再清零) |
|
|
523
|
+
| 平均耗时 | 会话 settle 后补的毫秒数 | 同上 |
|
|
524
|
+
| 卡数 / 完成 | 按 `task.expert` 归到这位专家 | 台账 `tasks` |
|
|
525
|
+
| 一次通过率 | 已完成的卡里,history 从没往回退过的比例 | 与页首「一次通过率」KPI **同一条定义**(`index.js` 的 `taskTimeline.backward`) |
|
|
526
|
+
| 返工率 | 往回退过的卡 ÷ 名下卡数 | 同上 |
|
|
527
|
+
| AI 失败 / 缺陷重开 | `agentFailures` / `reopenCount` 求和 | 单独列,**不进**返工率 |
|
|
528
|
+
| 成本 | 未接入 | 平台还没有按卡/按项目的 token 记账,接上 usage 才会有数字 |
|
|
529
|
+
|
|
530
|
+
已知偏差:`task.expert` 会在换列时被列负责人覆盖,所以「卡数」看的是**当前负责**的卡,不是历史上派给过它的所有卡。
|
|
531
|
+
|
|
532
|
+
成本为什么还是「未接入」:宿主没有把 token usage 暴露给插件;会话目录里的 `session.v4.jsonl.zstd` 解出来是整份 JSON 快照,没有稳定可依赖的 usage 字段(`dsh-ui-usage-billing` 那类插件是自带解析器 + 价目表的)。要真算钱得先解决这两件事,已单开一张卡跟踪。
|
|
533
|
+
## 已知限制
|
|
534
|
+
|
|
535
|
+
- 多维表格镜像(`baseToken` / `tableId`)只存配置,尚未实现写入。
|
|
536
|
+
- GitLab 同步 Issue 与**关联 MR**(`/issues/:iid/related_merge_requests`),但不同步 MR 的评论与流水线状态;MR 的合并/关闭状态要等下一次同步才会反映到卡上。
|
|
537
|
+
- 用例记录存在卡上(`task.cases`):2026-11-13 起有跨卡的**用例总览**页签,并支持**勾选一批接口用例批量跑**(结果回写到各自的卡、失败照旧开缺陷)。但还没有「回归套件」这个对象——固定的用例集合、一键重跑、历次通过率趋势都还没有,批量跑是临时勾选、跑完即散。
|
|
538
|
+
- 缺陷卡的严重级别默认 `P2`、优先级 `normal`:自动建卡不猜严重程度,由人或在评论里改。
|
|
539
|
+
- 派单失败会记在 `/studio-api/integrations` 与卡片抽屉里,不会自动重试。
|
|
540
|
+
- 集成健康现在会**落盘保留最近一次结果**(SQLite 的 `meta:side:integration-health`,不进 state、不涨 revision、不占快照):重启 dsh-web 后四条通道不再回到「还没用过」。仍然只是「最近一次」,没有按小时/按天的长期趋势(要趋势还得上采集)。
|
|
541
|
+
- 工具层权限只认会话名(`studio-<taskId>-<expertId>`):如果哪个 agent 用了别的会话名,就会被当成「名单外」拦下写操作,需要在审计里看一眼。
|
|
542
|
+
- 同一张卡在同一列只自动派一次(防止来回循环),需要再跑一轮就点「马上处理」,或在评论里 `@` 它。
|
|
543
|
+
- 并发上限是全局的(设置 → 自动派单 → 同时最多几个 agent,默认 2):名额满了新卡就排队,把在跑的会话停掉或把上限调大即可。
|
|
544
|
+
- 删掉卡片会自动收回它占用的名额(原来会留一个幽灵会话把并发名额占死,已修)。
|
|
545
|
+
- 专家团是「团长 + @ 接棒」的协作模式;2026-11-13 起有**团级记忆**:卡推进到「完成」时自动记一条(做了什么、遗留什么),派活时把这个团最近 5 条塞进专家提示词,避免同一批人反复踩同一个坑。记忆存在 SQLite 的 side 键里,最多每个团 20 条,可在「设置 → 专家团」里清空。
|
|
546
|
+
- 新写或改写的技能对**已经开着的会话**不立即生效(技能目录在会话开始时枚举),开新会话或下一次派单即可看到。
|
|
547
|
+
- 「从链接拉飞书正文」与「产物归档到飞书云盘」都依赖宿主机的 `lark-cli` 与有效的用户授权;`auth status` 里 user 不是 ready 时页面会提示,按提示重新授权即可。
|
|
548
|
+
- 归档出来的是云盘里的 Markdown 文件,不是飞书在线文档:同事能打开、能下载、能被搜索,但不能在线协同编辑与评论;正文由平台覆盖更新,人工在飞书上改的内容下次覆盖会丢。
|
|
549
|
+
- 归档是同步写:一次要写很多产物时(比如批量补历史)会挨个调 `lark-cli`,几十份文档需要十几秒到几十秒。
|
|
550
|
+
- 需求基线是快照:2026-11-13 起可以**逐行对比**「基线 vs 最新 PRD」(只读,`web/src/studio/diff.js` 的 LCS),但**不能回滚**——要回到旧版得把旧正文重新贴一份再设为基线;需求的评审仍是人工点按钮,没有独立审批流。
|
|
551
|
+
- AI Coding 执行记录靠 agent/人如实登记:目前**不会自动从 GitLab 拉 MR 的 diff、commit 列表与单测结果**,`commits` / `files` / 单测数都是上报值,不是从仓库校验过的事实。下一步可用 `glab mr diff` + 流水线接口做自动回填。
|
|
552
|
+
- 「业务负责人」与「期望完成」是可选项:没填时抽屉里显示「未指派」/「未排期」,不是数据丢失;需要时补上再点「保存改动」。
|
|
553
|
+
|
|
554
|
+
## 卡片详情(抽屉)布局 与 用例结果展示(2026-11-08)
|
|
555
|
+
|
|
556
|
+
抽屉原来是一条到底的单列,内容一多就要滚很久。现在:
|
|
557
|
+
|
|
558
|
+
- 抽屉宽度 `min(980px,94vw)`,正文按**页签**分组,不用往下滚一屏:概览 / 内容 / 计划 / 交付物 / 测试 / 执行记录 / 讨论。
|
|
559
|
+
- 页签上带计数:交付物件数、测试 `通过/总数`、执行记录是否已评审(`✓ 已评审`)、讨论条数——不用点进去就知道有没有东西。
|
|
560
|
+
- 「讨论」页签里评论**不再套内层滚动条**:内容自然铺开、由抽屉整体滚动,一屏看不完就往下滚,不会出现「只显示一半」的错觉。交付物、测试用例同理(原来各限高 460px / 340px 内滚,已去掉)。
|
|
561
|
+
- 切页签或换卡时抽屉自动滚回顶部,避免从长页面切到「讨论」时停在中间。
|
|
562
|
+
- 底部 @ 输入框始终可见;一点输入框会自动切到「讨论」页签。
|
|
563
|
+
- 头部固定显示 标题 + `编号 · 列 · 负责 AI · 更新时间`;Esc 关闭。
|
|
564
|
+
|
|
565
|
+
测试用例的结果展示改成一眼能判断:
|
|
566
|
+
|
|
567
|
+
| 位置 | 表现 |
|
|
568
|
+
| --- | --- |
|
|
569
|
+
| 区块标题 | `10 条 · ✅ 通过 10 · ❌ 不通过 0`(有没跑的再加 `· ⬜ 没跑 N`) |
|
|
570
|
+
| 每条用例 | 左侧 3px 色条:通过绿 `#0f9d58`、不通过红 `#e5484d`、没跑灰;下拉框可改结果 |
|
|
571
|
+
| 最近执行 | 前缀 `✅` / `❌`,后面是状态码 · 耗时 · 时间 |
|
|
572
|
+
| 卡片上 | 页脚新增 `· ✅ 8/10`(有失败就变红 `❌ 8/10`),鼠标悬停显示「通过 8/10,不通过 2」 |
|
|
573
|
+
|
|
574
|
+
注意「通过」是按用例自己写的期望判的:`expectStatus:400` 的用例返回 400 就是通过,不是失败。
|
|
575
|
+
|
|
576
|
+
## 需求结构化:功能点与可追溯矩阵(2026-11-08)
|
|
577
|
+
|
|
578
|
+
需求不再只是一段描述,而是一组**功能点**(`requirement.points`):
|
|
579
|
+
|
|
580
|
+
- 一条功能点:`{ id, title, description, priority, status, acceptance[] }`,接受条件一行一条。
|
|
581
|
+
- 需求页 →「功能点与可追溯矩阵」:可以增删改功能点,并看到一条纸面矩阵:
|
|
582
|
+
|
|
583
|
+
| 列 | 含义 |
|
|
584
|
+
| --- | --- |
|
|
585
|
+
| 状态 | 已覆盖(`covered`)= 它的接受条件都能对到通过的用例;否则未覆盖 |
|
|
586
|
+
| 卡 | 挂在这条功能点上的卡(`task.pointId`),显示编号;没挂的显示「还没人做」 |
|
|
587
|
+
| 用例 | 挂在这条功能点上的用例(`case.pointId`)总数 / 通过 / 失败 / 没跑 |
|
|
588
|
+
| 验收条件 | 被覆盖的接受条件 / 总数 |
|
|
589
|
+
|
|
590
|
+
- 页面底部会列「未归功能点的卡」,选一个点就能直接挂上(`POST /task/update {pointId}`)。
|
|
591
|
+
- 卡抽屉 →「内容」有「对应功能点」下拉;用例行有「功能点」下拉,选中即写入。
|
|
592
|
+
- 接口:`POST /requirement/matrix {id}` 返回 `{ points, loose, summary }`;`GET /state` 里的需求带 `points`(正文之外的字段都带)。
|
|
593
|
+
- agent 侧:`studio_requirement_get` 返回 `matrix`,`studio_task_create` / `studio_task_update` 支持 `pointId`,`studio_task_case` 的 `cases[]` 也支持 `pointId`。
|
|
594
|
+
|
|
595
|
+
## 准出规则与验收签字(2026-11-08)
|
|
596
|
+
|
|
597
|
+
「已完成」不是拖过去就算完,而是过一道门(`taskExitGate`):
|
|
598
|
+
|
|
599
|
+
**拦截项(blockers,必须清掉)**
|
|
600
|
+
|
|
601
|
+
- 验收清单还有没勾的项;
|
|
602
|
+
- 有用例 `result === 'fail'`;
|
|
603
|
+
- 这张卡还挂着阻塞标记(`blocked`);
|
|
604
|
+
- 没有人签字(`task.signoff` 为空)。
|
|
605
|
+
|
|
606
|
+
**提醒项(warnings,不拦但会列出来)**
|
|
607
|
+
|
|
608
|
+
- 一张用例都没有;
|
|
609
|
+
- 有用例没跑(pending / skip);
|
|
610
|
+
- 没有交付物,或交付物没正文。
|
|
611
|
+
|
|
612
|
+
实现位置:`updateTask` 在 `status = 'done'` 时先跑门禁,没过就抛错并在界面弹出原因;确实要提前收口,就在推进时填 `override`(一句话原因会写进状态历史,审计看得到)。
|
|
613
|
+
|
|
614
|
+
- 抽屉 →「概览」→「准出检查」:点「检查一下」列出 ⛔ / ⚠;签字区填一句结论(「验了什么、遗留什么」)后点「签字确认」,签完按钮变「取消签字」。
|
|
615
|
+
- 接口:`POST /task/gate {id}` 返回 `{ gate, signoff }`;`POST /task/signoff {id, action:'sign'|'clear', note, by}`。
|
|
616
|
+
- agent 侧:`studio_task_signoff`;`studio_task_update` 支持 `override`。
|
|
617
|
+
|
|
618
|
+
## 需求准出与验收签字(管理到每一条需求)
|
|
619
|
+
|
|
620
|
+
卡片门禁只管一张卡;「这条需求能不能交付」是另一道门(`requirementGate`)。
|
|
621
|
+
|
|
622
|
+
**拦截项(blockers)**
|
|
623
|
+
|
|
624
|
+
- 需求下还没有任何卡;
|
|
625
|
+
- 有卡没完成(会列出前 5 个编号);
|
|
626
|
+
- 有卡还挂着阻塞;
|
|
627
|
+
- 有未关闭的缺陷(type=bug 且不是 done);
|
|
628
|
+
- 有用例 `result === 'fail'`;
|
|
629
|
+
- 需求还没签字(`requirement.signoff` 为空)。
|
|
630
|
+
|
|
631
|
+
**提醒项(warnings)**
|
|
632
|
+
|
|
633
|
+
- 没拆功能点;功能点还没有「通过用例」覆盖;卡没归到功能点;没有交付物或交付物没正文。
|
|
634
|
+
|
|
635
|
+
**签字语义**:`POST /requirement/signoff {id, action:'sign', note, by}` —— `note`(交付了什么、遗留什么)必填;签字后若卡已全完成,需求自动落到「已交付」(`delivered`);`action:'clear'` 取消签字并把 delivered 退回 active。
|
|
636
|
+
|
|
637
|
+
**三个入口**
|
|
638
|
+
|
|
639
|
+
- 需求页:列表每项带进度条与「准出通过 / 还差 N 项」,详情顶部「需求准出与验收签字」面板列出 ⛔ / ⚠ 与签字输入框;
|
|
640
|
+
- 看板:工具栏「全部需求」下拉,只看某条需求下的卡;
|
|
641
|
+
- agent 侧:`studio_requirement_get` 返回 `progress`(含 `gate.blockers`),`studio_requirement_signoff` 签字,`studio_task_list` 支持 `requirementId` 过滤。
|
|
642
|
+
|
|
643
|
+
**接口**:`GET /requirements`(每条带 `progress`)、`POST /requirement/get`(带 `matrix` 与 `progress`)、`POST /requirement/signoff`;`publicState()` 的 `requirements[]` 也带 `progress`,所以面板 5s 轮询就能刷新门禁状态。
|
|
644
|
+
|
|
645
|
+
## 测试证据(最近一次响应片段)
|
|
646
|
+
|
|
647
|
+
用例真跑之后,除了状态码与耗时,还会留 `lastRun.response`(响应体前 2000 字),抽屉里显示成「响应片段」,避免「通过了但不知道返回了什么」。接口与工具返回的 `lastRun` 都带这个字段。
|
|
648
|
+
|
|
649
|
+
## 一键复验
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
node /tmp/verify-trace.mjs # 功能点 / 矩阵 / 门禁 / 签字 / override,15 项
|
|
653
|
+
node /tmp/verify-reqgate.mjs # 需求级门禁 / 进度 / 签字 / 状态流转,13 项
|
|
654
|
+
node /tmp/verify-fields.mjs # 字段显示规范,11 项
|
|
655
|
+
node /tmp/verify-final.mjs # 灰度 / 审批留痕 / CSV 导出 / 成本预算 / 批量跑用例 / 需求依赖,14 项
|
|
656
|
+
node /tmp/verify-final2.mjs # 多步用例 / 依赖门禁 / 预算可改 / 升级阈值,7 项
|
|
657
|
+
node /tmp/verify-gitlab.mjs # MR 回写(用 127.0.0.1:18098 的假 GitLab),7 项
|
|
658
|
+
node /tmp/verify-dashboard.mjs # 工作台风险计数 / flaky 不开缺陷 / 缺陷重开,10 项
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## 补完的一批(2026-11-09)
|
|
662
|
+
|
|
663
|
+
这几块是「文档九模块」里剩下、且不依赖真实 GitLab 环境的部分。
|
|
664
|
+
|
|
665
|
+
### 测试执行:多步接口 + 批量并发
|
|
666
|
+
|
|
667
|
+
- 用例的 `http` 现在支持 `steps[]`(多步串联:先登录取 token,再带 token 调业务接口)与 `vars`(变量表);`{{name}}` 在 url / body / header 里替换,每步的 `extract` 用 `pickJsonPath` 取值(支持 `data.token`、`items.0.id`、`header:x-request-id`)。
|
|
668
|
+
- **任何一步失败就停**,后面步骤依赖前面的 token / id,硬跑下去只会得到一串假失败。
|
|
669
|
+
- `POST /studio-api/tests/run`(工具 `studio_test_suite`)批量跑:默认对象是「待测试」列,卡与卡之间并发(1-8,默认取 `testing.concurrency`),同一张卡内部串行。返回逐卡结果与本次开出的缺陷数。
|
|
670
|
+
- 每步的结果(方法 / url / 状态 / 耗时 / 错误 / 响应片段)都写进 `lastRun.steps`,抽屉里能看到是哪一步断的。
|
|
671
|
+
|
|
672
|
+
### 发布:审批留痕 + 灰度批次 + 部署记录
|
|
673
|
+
|
|
674
|
+
- `release.approvals[]`:点「审批通过」时记录谁、什么时候、什么结论(`{by, role, at, note}`)。
|
|
675
|
+
- `release.grayBatches[]`:`{name, percent, status: pending|running|passed|rolled_back}`,`POST /release/gray` 支持 `add / start / advance / rollback`;**灰度回滚会把整个版本置为已回滚**并写 `rollbackReason`。
|
|
676
|
+
- `release.deployLog[]`:审批、发布、灰度动作、回滚都记一条(保留最近 200 条),版本页显示最近 8 条时间线。
|
|
677
|
+
- 门禁新增提醒:没有审批记录、没有灰度批次(将全量发布)、还有灰度批次没走完。
|
|
678
|
+
|
|
679
|
+
### 成本:预算条
|
|
680
|
+
|
|
681
|
+
- `config.costs = { budgetRuns, budgetHours, warnAt }`(默认 500 次 / 80 小时 / 80%)。
|
|
682
|
+
- `GET /costs` 返回 `budget: { runs, hours, runRate, hourRate, level, hint }`,`level` 为 `ok|warn|over`;效能页「AI 执行成本」下方是一条预算进度条,超 `warnAt` 变黄、超 100% 变红。
|
|
683
|
+
- 口径仍是「派单次数 + 机上时长」,宿主没有回传 token 用量。
|
|
684
|
+
|
|
685
|
+
### 集成失败升级
|
|
686
|
+
|
|
687
|
+
- `config.feishu.escalateWebhook` + `escalateAfter`(默认 3):某通道连续失败达到阈值时,**只在跨过阈值那一次**往升级群发一条,包含通道名、连续失败次数、最近错误与时间。
|
|
688
|
+
- 与日常通知分开配,坏消息不会淹在状态流里。
|
|
689
|
+
|
|
690
|
+
### 需求依赖与排序
|
|
691
|
+
|
|
692
|
+
- `requirement.dependsOn[]`:被依赖的需求没到 `delivered` / `closed` 时,**依赖方的门禁多一条 ⛔ 拦截**(`依赖的需求还没交付:REQ-…`),签字前必须等。
|
|
693
|
+
- `requirement.order`:`GET /requirements` 按 `order` 再按编号排序。
|
|
694
|
+
|
|
695
|
+
### 导出
|
|
696
|
+
|
|
697
|
+
- `GET /studio-api/export/tasks.csv`、`/studio-api/export/requirements.csv`:带 BOM 的 CSV(Excel 直接打开不乱码),页头「导出卡 / 导出需求」两个按钮就是下载链接。
|
|
698
|
+
- 卡表包含编号 / 标题 / 类型 / 状态 / 负责人 / 需求 / 项目 / 迭代 / 版本 / 阻塞原因 / 截止 / 用例通过数 / 交付物数;需求表包含功能点覆盖、卡完成数、用例通过率、交付物数、签字人。
|
|
699
|
+
|
|
700
|
+
### GitLab MR 回写(真机待 token)
|
|
701
|
+
|
|
702
|
+
- `POST /studio-api/gitlab/sync` 现在同时同步 **Issue** 与 **MR**:先按 `source_branch` 里的卡编号匹配,匹配不上再用 `related_merge_requests` 兜底。
|
|
703
|
+
- 每个 MR 会拉 `/pipelines` 与 `/changes`:流水线状态写进 `gitlabMR.pipeline`,改动文件写进 `coding.files`(研发没自报时自动补,提交按 MR 的 commits 计)。
|
|
704
|
+
- **`opened` 且卡在「开发中」→ 自动推进到「待评审」**;**`merged` 且在「开发中/待评审」→ 自动推进到「待测试」**,两次都带一条评论说明来源;流水线失败追加一条失败评论。
|
|
705
|
+
- 卡状态流转、阻塞、解除也会回写一条到 MR 的 notes(`broadcastChange`)。
|
|
706
|
+
- 用 `tools/mock-gitlab.mjs`(127.0.0.1:18098)验证过 7/7:匹配、流水线、改动文件、自动推进都通;**真实 GitLab 还需要令牌**(Role Developer+、scope `api`)。
|
|
707
|
+
|
|
708
|
+
### 工具数
|
|
709
|
+
|
|
710
|
+
插件现在提供 **39 个** `studio_*` 工具(新增 `studio_test_suite`、`studio_release_gray`、`studio_dashboard`、`studio_project_save`)。
|
|
711
|
+
|
|
712
|
+
## 工作台(2026-11-09)
|
|
713
|
+
|
|
714
|
+
- **最近回归在第一屏**(2026-11-13):工作台最上面一条横幅显示最近一次批量回归(时间 / 谁跑的 / 几张卡 / 多少条 / 通过 / 不通过 / 自动开缺陷),有失败时整条标红,旁边一个「看用例」直接跳用例页。数据和用例页的「最近回归」是同一份(SQLite 的 side 键),夜间定时跑完早上打开就能看见。
|
|
715
|
+
打开工作空间先落在这里,回答「今天该看什么」,而不是先看一屏幕卡。
|
|
716
|
+
|
|
717
|
+
- **8 个风险 KPI**:在办卡、逾期(过了期望完成)、阻塞、缺陷超时(按 P0–P3 时限)、准出被拦(清单/用例/签字没过)、流水线失败(`gitlabMR.pipeline.status === 'failed'`)、集成异常(飞书/GitLab/云盘/隔离通道坏了)、AI 在跑。
|
|
718
|
+
- **流程与产出**:待评审 / 待测试 / 待验收 / 今天完成 / 近 7 天完成 / 需求卡住 / 版本卡住,一键跳到看板。
|
|
719
|
+
- **六张清单**:逾期、阻塞、缺陷超时、准出被拦、流水线失败、需求与版本卡住;每条带「打开」直接进抽屉。需求与版本那两条会把 `requirementGate` / `releaseGate` 的拦截原因原样列出来。
|
|
720
|
+
- 数据实时算、不落库;接口 `GET /dashboard`,agent 侧对应 `studio_dashboard`(输出中文摘要 + 清单)。
|
|
721
|
+
|
|
722
|
+
### 三个质量信号
|
|
723
|
+
|
|
724
|
+
| 信号 | 怎么来的 | 效果 |
|
|
725
|
+
| --- | --- | --- |
|
|
726
|
+
| CI 拦截 | MR 的 `pipeline.status === 'failed'` | 卡推「已完成」时准出多一条 `⛔ CI 流水线没通过` |
|
|
727
|
+
| 不稳定用例 | 用例行点「标不稳」(`case.flaky = true`) | 判失败**不自动开缺陷**,只记结果与「skipped: flaky」 |
|
|
728
|
+
| 缺陷重开 | BUG 卡从「已完成」被推回 | `reopenCount + 1`,卡片显示「重开 N」 |
|
|
729
|
+
|
|
730
|
+
## 面板上的编辑入口(2026-11-09 补齐)
|
|
731
|
+
|
|
732
|
+
前面几块能力原先只有接口与 agent 工具,面板上没有入口;这一轮补齐了:
|
|
733
|
+
|
|
734
|
+
- **多步接口用例**:卡片详情 → 测试 → 加用例时多一个「多步接口」输入框,一行一步
|
|
735
|
+
`METHOD url | 期望状态码 | 提取变量`,例如
|
|
736
|
+
```
|
|
737
|
+
POST /api/auth/login | 200 | data.token
|
|
738
|
+
GET /api/points | 200 |
|
|
739
|
+
```
|
|
740
|
+
提取变量默认按路径末段命名(`data.token` → `token`),想自己命名写 `token=data.token`;正文里用 `{{token}}` 引用。
|
|
741
|
+
用例行会显示「多步 N」,执行后展开每一步的成功/失败与状态码。
|
|
742
|
+
- **需求依赖与排序**:需求详情 → 「依赖与排序」面板,填排序号、勾选依赖哪些需求,保存后:
|
|
743
|
+
列表按排序号排,被依赖的需求没交付时准出多一条 `⛔ 依赖的需求还没交付:REQ-…`。
|
|
744
|
+
- **成本与预算**:设置 → 运行 → 成本与预算,改每月派单次数、机上时长、警告阈值。
|
|
745
|
+
- **通知升级**:设置 → 接入 → 飞书机器人,下方「通知升级」卡片配升级群 Webhook 与连续失败阈值。
|
|
746
|
+
- **专家提示词**:设置 → 团队 → 专家,每行直接显示提示词摘要(前 150 字,`title` 里有全文),点这行就打开编辑;没写提示词的会标「还没有写,会退回内置角色说明」。
|
|
747
|
+
|
|
748
|
+
### 专家的默认提示词
|
|
749
|
+
|
|
750
|
+
四个内置角色位(产品经理 / 架构师 / 开发工程师 / 测试工程师)出厂就带完整提示词,写的是各自该干什么、动手前读什么、产出必须包含什么、收口前怎么自检。早期版本这几个字段是空的(面板上看着像「没配」),`schema 4` 迁移会给空的补上、`schema 5` 会把「按业务领域批量生成」留下的 13 字一句话提示词换成带业务领域前缀的完整版(已经写过 20 字以上的保持不动)。
|
|
751
|
+
|
|
752
|
+
## 闭环补齐(2026-11-09)
|
|
753
|
+
|
|
754
|
+
「需求 → 研发 → 交付」与「Agent → 改动 → 证据」之间原先有两处断点:需求改完没人被通知要复核,Agent 报的提交/文件也没人跟代码对过。这一轮补上:
|
|
755
|
+
|
|
756
|
+
### 需求变更影响面
|
|
757
|
+
|
|
758
|
+
改需求不再只是覆盖字段,会留下一条「这次改了什么、影响谁」:
|
|
759
|
+
|
|
760
|
+
- 保存时对比 `title / description / points / links` 四项,记成 `change.parts`(标题、需求明细、功能点、文档链接)。
|
|
761
|
+
- 受影响卡 = 这条需求下**已经产出过东西**的卡:状态在 `test/accept/done`,或有验收签字,或有通过的用例,或有执行记录里的提交。
|
|
762
|
+
- 改了「实质内容」(标题/明细/功能点;只改文档链接不算)且受影响卡 > 0 时,把需求上的验收签字清空,`change.invalidated = true`。
|
|
763
|
+
- 需求准出多一条 `⛔ 需求改过(需求明细):N 张已产出的卡要复核(卡号…),验收签字已作废`;只增不删时降级成提醒。
|
|
764
|
+
- 面板:需求详情 →「变更影响」面板,列出改了哪几块、几张卡要复核(点卡号直接打开)、签字是否作废。
|
|
765
|
+
|
|
766
|
+
### 执行记录 vs 代码事实
|
|
767
|
+
|
|
768
|
+
Agent 的「我改了 A、B、C」现在可以和 worktree 里的 git 对账:
|
|
769
|
+
|
|
770
|
+
- `POST /task/coding/verify`(body `{ id }`):读 worktree 的真实分支(`rev-parse --abbrev-ref HEAD`)、最近 200 条提交、`<base>...HEAD` 的改动文件、`status --porcelain` 的未提交改动,和 `coding.branch/commits/files` 逐项比对。
|
|
771
|
+
- 提交只比前缀(完整 sha 或前 8 位都算);文件容忍路径前缀差异;比对结果写回 `coding.verified`。
|
|
772
|
+
- 卡片准出:核对过且 `ok === false` → `⛔ 执行记录和代码不一致:分支对不上 / N 个记录的提交在分支上找不到 / N 个记录的文件没有实际改动`;有 worktree 有执行记录但**从没核对过** → 提醒。
|
|
773
|
+
- 面板:卡片详情 →「执行记录」右上角「核对代码事实」(有 worktree 才出现),结果显示「分支 X · 提交 N 个 · 改动 M 个文件」与缺失项,再次核对显示「再核对一次」。
|
|
774
|
+
|
|
775
|
+
### 版本 ↔ 需求
|
|
776
|
+
|
|
777
|
+
版本不用再手工挑需求:`/release/get` 与看板状态里,每个版本自动带出 `requirements`(由 `task.releaseId` 反查),版本详情顶部显示「这个版本涉及 N 条需求:REQ-…(状态)」,重新生成的发布说明里也多一节 `## 需求(N 条)`。
|
|
778
|
+
|
|
779
|
+
复验:`tools/verify-closed-loop.mjs`(29/29 PASS,覆盖假提交被揪出、真改动核对通过、需求改动清签字、只改链接不清签字、版本带出需求、收尾还原基线)。
|
|
780
|
+
|
|
781
|
+
## 部署环境(2026-11-09,只留痕、不接 CD)
|
|
782
|
+
|
|
783
|
+
环境是「版本上过哪里」的登记维度,**不是部署通道**:平台不调脚本、不发 webhook、不碰 CI,也不会因为「登记了部署」去改任何卡或版本状态。
|
|
784
|
+
|
|
785
|
+
- 环境清单存在 `config.environments`:`{ id, name, kind, note }`,`kind ∈ dev | staging | prod | other`,最多 12 条;出厂 `dev 开发 / staging 预发 / prod 生产` 三条,可以改名、改类型、加说明、增删。
|
|
786
|
+
- 环境 id 由名字生成:英文能拼 slug(`预发UAT → uat`),纯中文落成稳定的 `env-<sha1前8>`,所以同名再存一次命中的是同一条。
|
|
787
|
+
- `POST /release/deploy`(body `{ id, env, note, by }`):只往版本的 `deployLog` 追加一条 `{ at, by, action: 'deploy', env, note }`,返回值带 `mode: 'record-only'`;环境不存在会报「先选一个部署环境」。版本状态**不变**,也不会触发任何外部动作。
|
|
788
|
+
- 派生视图:`releaseDeploys()` 把日志按环境取最新一条投影成 `[{ id, name, kind, at, action, by, note }]`(没上过 `at === 0`),`requirementDeploys()` 再按需求下所有卡关联的版本合并;`/state` 的 `releases[].deploys`、`requirements[].deploys`、`/release/get` 都带这份数据。
|
|
789
|
+
- 界面:
|
|
790
|
+
- 设置 →「部署环境」:行内编辑名字 / 类型 / 说明,底部「+ 加一个环境」「保存环境」,面板上写明「平台不会调用任何部署脚本、webhook 或 CI」。
|
|
791
|
+
- 版本详情 →「上过哪些环境(只登记,不触发部署)」:每个环境一个 chip,上过显示 `名字 ✓`,没上过 `名字 —`;下面选环境 + 填备注 →「登记部署」,成功提示「已登记部署(没有触发任何部署)」;部署记录每行带环境名。
|
|
792
|
+
- 需求详情 →「上线足迹」:这条需求涉及的所有版本分别在哪些环境、什么时候、谁登记的。
|
|
793
|
+
- 和门禁的关系:登记部署**不参与**卡片/需求准出判断,只是记录。真要卡「上线前必须上过预发」,后续可以加一条只看 `deploys` 的提醒(当前没做)。
|
|
794
|
+
|
|
795
|
+
复验:`tools/verify-deploy-envs.mjs`(23/23 PASS:出厂三环境、改名后 id 稳定、登记只留痕不动状态、未知环境被拒、版本与需求都带足迹、收尾还原环境与版本数)。
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
## 评审与闭环补齐(2026-11-09)
|
|
802
|
+
|
|
803
|
+
这一节补的是「体检报告里 A 类:真没写的功能」,六项都是平台自己算的,不需要外部令牌。
|
|
804
|
+
|
|
805
|
+
### 需求评审
|
|
806
|
+
- 需求状态多了 `review`(待评审),状态流:`draft → review → active → delivered → closed`;打回会退回 `draft`。
|
|
807
|
+
- `review = { status: pending | approved | changes_requested, by, at, note }` 存在需求对象上。
|
|
808
|
+
- `POST /requirement/review`:`{ id, decision: submit | approve | reject, by, note }`。
|
|
809
|
+
- `submit` → 状态 `review`、`review.status = pending`;
|
|
810
|
+
- `approve` → 状态 `active`(已交付/已关闭保持)、`approved`;
|
|
811
|
+
- `reject` → 状态 `draft`、`changes_requested`,**必须写 note**,否则接口报错。
|
|
812
|
+
- 门禁:没评审通过(或被打回)时,`requirementGate` 直接给阻塞项,拆卡和「准出」都过不去。
|
|
813
|
+
- 界面:需求详情顶部有「需求评审」面板——状态 chip、提交评审 / 评审通过 / 打回(打回弹窗填意见)、评审时间与人。
|
|
814
|
+
|
|
815
|
+
### 按功能点批量拆卡
|
|
816
|
+
- `POST /requirement/split`:`{ id, pointIds?, status? }`,只为**还没有卡**的功能点建卡(一条功能点一张卡,挂好 `requirementId` + `pointId`),标题是 `<pointId> <功能点标题>`,默认进「待澄清」列。
|
|
817
|
+
- 要求需求已评审通过(或早就开始了),否则报「需求还没评审通过,先过评审再拆卡」;全部覆盖时报「这些功能点都已经有卡了」。
|
|
818
|
+
- 界面:功能点面板底部「按未覆盖的功能点拆卡」,结果直接提示建了哪几张卡。
|
|
819
|
+
|
|
820
|
+
### 卡级 AI 失败重试 / 退避 / 升级
|
|
821
|
+
- 配置 `config.agents.retry = { max: 2, baseMs: 60000, factor: 2 }`:第 N 次失败后等 `min(base * factor^(N-1), 30min)` 再派;超过 `max` 次不再重试。
|
|
822
|
+
- 失败记录在卡上:`agentFailures`(次数)、`retryAt`(下次自动重试的时间戳,0 表示不排了)。
|
|
823
|
+
- 超过上限自动挂阻塞(`blocked.by = '系统'`)并飞书广播;派单器 `tick()` 会跳过 `retryAt > now` 的卡。
|
|
824
|
+
- 接口:`POST /task/agent/fail`(`{ id, error, by }`,一般由派单器或 agent 自己调)、`POST /task/agent/retry`(`{ id, by }`,人工清零失败计数并摘掉系统挂的阻塞)。
|
|
825
|
+
- 界面:卡详情右上角出现「AI 失败 N 次 · 重新派单」,鼠标悬停能看到下次重试时间。
|
|
826
|
+
|
|
827
|
+
### 同服务并行冲突预检
|
|
828
|
+
- `serviceConflicts(state, task)`:同项目、同服务、状态在 `dev / review / test` 的其他卡会算进来。
|
|
829
|
+
- 卡准出时给提醒:`同一个服务「X」还有 N 张卡在改(…),合并前先对齐,避免互相覆盖`(是提醒不是阻塞——并行本身合法,只是要人知道)。
|
|
830
|
+
- 派单提示词里也会写「只改本卡范围,动公共文件前先看别人的分支」。
|
|
831
|
+
|
|
832
|
+
### 用例字段与覆盖率
|
|
833
|
+
- 用例新增 `type`(functional / api / ui / performance / security / other)、`priority`(p0–p3)、`preconditions`(前置条件,≤800 字)、`steps`(手工步骤,一行一步,≤20 步)。
|
|
834
|
+
- 功能点矩阵新增 `pointsWithoutCases` 与 `coverageRate`(有用例的功能点占比);需求准出在「有功能点但一条用例都没有」时给提醒。
|
|
835
|
+
- 界面:用例列表每条显示类型 / 优先级 / 前置条件 / 步骤条数;新增用例那一排多了类型、优先级、前置条件的选择与步骤多行输入。
|
|
836
|
+
|
|
837
|
+
### 发布准入(软校验 + 强制留痕)
|
|
838
|
+
- 配置 `config.releaseRules = { requireStaging: false, requireRequirementSignoff: false }`:
|
|
839
|
+
- `requireStaging`:发布前必须在 `kind === 'staging'` 的环境上登记过一次 `action === 'deploy'` 的部署;
|
|
840
|
+
- `requireRequirementSignoff`:这个版本关联的每条需求都要有验收签字。
|
|
841
|
+
- `/release/publish` 先过版本门禁,再过准入:准入不过且没带 `force: true` 直接拒绝;强制发布必须带 `forceNote`,原因会写进部署日志 `· 强制发布(违例)原因:…`,复盘时能查。
|
|
842
|
+
- 界面:设置 →「部署环境」面板底部两个开关;版本详情「发布」旁边多一个「强制发布」(弹窗填原因)。两个开关默认都不勾,不勾时行为与以前完全一致。
|
|
843
|
+
|
|
844
|
+
### 踩过的坑(同类问题已第二次出现)
|
|
845
|
+
- `publicConfig()` 与 `mergeConfig()` **都是白名单**:新加的配置段(`environments`、`releaseRules`)如果只在 `defaultConfig()` 里加,`/state` 拿不到、`applyConfig` 里 `config.releaseRules.xxx = ...` 会直接 `Cannot set properties of undefined`。加配置段要同时改这三处。
|
|
846
|
+
|
|
847
|
+
复验:`tools/verify-full-loop.mjs`(35/35 PASS:评审流转与门禁、拆卡与 pointId、矩阵覆盖率、用例四个新字段、同服务提醒、失败重试→耗尽阻塞→重新派单、准入拦截→必须写原因→强制发布留痕、收尾还原)。
|
|
848
|
+
|
|
849
|
+
## 统一登录(LDAP,2026-11-09)
|
|
850
|
+
|
|
851
|
+
平台自己不做密码库:账号和密码交给目录服务验证,验过之后把用户名记下来,归属和审计都挂在这个名字上。
|
|
852
|
+
|
|
853
|
+
### 配置
|
|
854
|
+
- `config.ldap = { enabled, url, baseDN, bindDN, bindPassword, userFilter, nameAttr, mailAttr, deptAttr, defaultRole, requireLogin, timeoutMs }`:
|
|
855
|
+
- `url` 必须是 `ldap://` 或 `ldaps://`;`userFilter` 里必须带 `{user}`(登录时替换成输入的用户名,且已经按 RFC 2254 转义 `\ * ( ) \0`);
|
|
856
|
+
- `nameAttr` / `mailAttr` / `deptAttr` 决定显示名、邮箱、部门取哪个属性(AD 一般是 `displayName` / `mail` / `department`);
|
|
857
|
+
- `requireLogin` 才是「锁门」,`enabled` 只表示启用登录(可以先配好、后开门)。
|
|
858
|
+
- **服务账号密码只存在 `~/.dsh/dsh-studio.secrets.json`(0600)**:`/state` 里只有 `bindPasswordSet: true/false`,导出、快照、日志里都没有明文;保存时不传密码就沿用旧密码,要清掉得显式传 `clearBindPassword: true`。
|
|
859
|
+
- 依赖系统里有 `ldapsearch`(Ubuntu:`apt-get install ldap-utils`),实现走 `ldapsearch -y <0600 临时文件>`,密码不出现在进程参数里,用完即删。
|
|
860
|
+
|
|
861
|
+
### 登录与会话
|
|
862
|
+
- `POST /auth/login { username, password, mode }`:`mode` 有三个值——
|
|
863
|
+
- `'ldap'`:只走目录(`userFilter` 查人 → 用这个人的 DN 重新绑一次验密码);
|
|
864
|
+
- `'local'`:只走本地超管(scrypt 哈希比对),失败直接 401 并给出「本地超管的账号或口令不对…」;
|
|
865
|
+
- 不传(老调用方):先本地、后目录,即目录故障时超管能兜底。
|
|
866
|
+
成功后返回 `{ token, expiresAt, user }`。
|
|
867
|
+
- 会话是内存里的 `Map`(12 小时过期),请求头 `x-studio-session: <token>`;重启服务会话即失效,需要重新登录。
|
|
868
|
+
- `GET /auth/me` 查当前会话,`POST /auth/logout` 主动销毁。
|
|
869
|
+
- 首次登录把用户写进 `config.users`(`{ id, name, mail, dept, dn, role, source:'ldap', firstLoginAt, lastLoginAt, loginCount, lastIp }`),之后只更新登录次数与最近时间——这就是「获取正确的用户名并记录」。
|
|
870
|
+
- 登录成功/失败都进审计日志(`登录失败` / `LDAP 登录成功` / `本地超管登录成功`)。
|
|
871
|
+
|
|
872
|
+
### 锁门行为(requireLogin = true)
|
|
873
|
+
- 未登录访问业务接口 → `401 { code: 'login_required' }`。
|
|
874
|
+
- 未登录访问 `/state` → 200 但只回 `{ ok: true, locked: true, state: { revision, config, tasks: [], requirements: [], releases: [] } }`,前端据此渲染登录框(不进看板)。
|
|
875
|
+
- 开放集合只有:`/`、`/state`、`/auth/login`、`/auth/me`、`/auth/logout`、`/auth/ldap/test`。
|
|
876
|
+
|
|
877
|
+
### 界面
|
|
878
|
+
- 登录框顶部是**两档切换**:`LDAP 账号` / `本地超管`,下面只有**一个**「登录」按钮,文案跟着当前档变(「登录(LDAP)」/「登录(本地超管)」)。
|
|
879
|
+
- 切到 `本地超管`:账号自动填成 `localAuth.username`(默认 `admin`),密码框占位提示「默认口令 admin」,回车即提交;
|
|
880
|
+
- LDAP 没启用时 `LDAP 账号` 那一档是禁用态,默认落在 `本地超管`;
|
|
881
|
+
- 失败提示分档:本地侧给「本地超管的账号或口令不对(账号 admin,还没改过口令就是默认的 admin)」,目录侧给中文原因。
|
|
882
|
+
- 登录后页头 `👤` 显示「超级管理员」(本地)或 LDAP 的显示名,旁边是「退出登录」。
|
|
883
|
+
- 设置 →「安全」→「统一登录(LDAP)」:启用、必须登录、服务器、超时、BASE DN、服务账号 DN、密码(已保存则留空表示不改)、过滤器、三个属性名;**「测试连接」**逐条给出「绑定服务账号 / 查人 / 验密码」的结果与耗时;底部列出登录过的人及次数。
|
|
884
|
+
- 登录框只有开启「必须登录」时才强制出现;只开 `enabled` 时行为与以前一致。
|
|
885
|
+
|
|
886
|
+
### 联调记录与网络前提(2026-11-09)
|
|
887
|
+
- 本机用临时 slapd(`dc=test,dc=local`,`alice/Passw0rd!`、`bob/BobPass123`)跑过 `tools/verify-ldap.mjs`:**23/23 PASS**(配置写入、状态里无明文、测试连接、登录成功、显示名/邮箱/部门、users 记录、`/auth/me`、错密码 `badpass`、不存在 `notfound`、锁门 `/state` 与 401、带会话可访问、退出后失效、收尾关开关);联调用的 slapd 与测试账号已卸载清空。
|
|
888
|
+
- **公司 LDAP(`ldap.<公司域>:389`)从目标机不可达**(解析到内网地址,ping 全丢、TCP 389 不通,`ldapsearch` 超时)。真实参数已经写进配置(`url / baseDN / bindDN / 密码`),但 `enabled` 与 `requireLogin` **都关着**:等这台机器能访问目录服务(内网路由 / VPN / 防火墙放行 389),到设置里点「测试连接」通过后再打开开关,否则会把所有人锁在门外。
|
|
889
|
+
- `parseLdif` 支持 RFC 2849 的 `attr:: base64`(AD 的中文 `displayName` 就是这样编码的);`ldapsearch` 的过滤器必须排在属性**前面**,否则会被当成属性名、搜索退化成 `(objectClass=*)` 并触发 `Size limit exceeded`。
|
|
890
|
+
|
|
891
|
+
## 本地超管账号与免登录(2026-11-09)
|
|
892
|
+
|
|
893
|
+
登录界面一直都在(`LoginGate`),但**默认不需要登录**:没有配置 LDAP 的时候,平台直接以本地超管身份运行,写操作和审计都记在它名下。
|
|
894
|
+
|
|
895
|
+
### 默认账号
|
|
896
|
+
|
|
897
|
+
- 账号 `admin`,口令 `admin`(`config.localAuth.password === null` 表示「还是默认口令」,这样默认值稳定、不随服务启动变化)。
|
|
898
|
+
- 改过密码之后,库里只存 `{ salt, hash, at }`(`scryptSync` + 16 字节随机盐),`/state` 只下发 `{ enabled, username, hasPassword, mustChange, updatedAt }`,**永远不下发口令或哈希**。
|
|
899
|
+
- 官方建议:进「设置 → 安全 → 统一登录(LDAP)」把默认口令改掉,或直接点一下「重置为默认口令」(需要知道当前口令)。
|
|
900
|
+
|
|
901
|
+
### 什么时候要登录
|
|
902
|
+
|
|
903
|
+
`locked = ldap.enabled && ldap.requireLogin`:
|
|
904
|
+
|
|
905
|
+
| LDAP 启用 | 必须登录 | 行为 |
|
|
906
|
+
| --- | --- | --- |
|
|
907
|
+
| 否 | 否 | **免登录**,请求算「超管 admin」,页头显示 `👤 超管 admin` + 「登录」按钮(可选用域账号登录) |
|
|
908
|
+
| 是 | 否 | 免登录,但可以主动点「登录」换成个人身份 |
|
|
909
|
+
| 是 | 是 | 弹登录框;LDAP 账号或本地超管都能登 |
|
|
910
|
+
|
|
911
|
+
### 接口
|
|
912
|
+
|
|
913
|
+
- `POST /auth/login { username, password }`:**先试本地超管**(用户名大小写不敏感),不匹配再走 LDAP;本地成功返回 `user.source = 'local'`、`role = 'admin'`。
|
|
914
|
+
- `POST /auth/password { currentPassword, newPassword }`:改超管口令(新口令 ≥ 8 位);`{ currentPassword, reset: true }` 重置回默认口令。
|
|
915
|
+
- `GET /auth/me`、`POST /auth/logout` 不变。
|
|
916
|
+
|
|
917
|
+
### 身份解析(谁写的、记谁)
|
|
918
|
+
|
|
919
|
+
`resolveActor(req, config)`:有会话 → 会话里的显示名;没锁门 → 本地超管(`超管 admin`);锁门且没会话 → 空(由权限检查拦)。请求管线和审计包装层都用它,所以免登录时的写操作不会记成匿名或请求头里的假名字。
|
|
920
|
+
|
|
921
|
+
会话里直接带了 `{ userId, name, role, source }`,`sessionOf()` 不再要求这个人出现在 `config.users` 里——本地超管本来就不在 LDAP 名册中。
|
|
922
|
+
|
|
923
|
+
### 界面
|
|
924
|
+
|
|
925
|
+
- 页头:未登录显示 `👤 超管 admin` + 「登录」;登录后显示 `👤 姓名` + 「退出登录」(退出后回到超管身份)。
|
|
926
|
+
- 登录框:提示「也可以用它自己的超管账号(本地):admin」;LDAP 没启用时底部写「LDAP 还没启用:直接用超管账号登录」,并有「先不登录」按钮(免登录模式下可随时关掉)。
|
|
927
|
+
- 设置 → 安全 →「统一登录(LDAP)」:最上面一块是「本地超管账号」,显示当前账号、是否已设密码,可改密码。
|
|
928
|
+
|
|
929
|
+
### 验证
|
|
930
|
+
|
|
931
|
+
`tools/verify-auth.mjs`(在 `.218` 上 `node /tmp/verify-auth.mjs`)**22/22 PASS**:默认不锁、下发 `localAuth` 不含口令、`admin/admin` 能登录、`/auth/me`、带会话写卡审计记「超级管理员」、不带会话写卡审计记「超管 admin」、错口令 401、改口令后旧口令失效新口令可用、短口令被拒、重置回默认、用户名大小写不敏感、清理测试卡后状态回基线。
|
|
932
|
+
|
|
933
|
+
## 样式对齐 shadcn/ui(2026-11-09)
|
|
934
|
+
|
|
935
|
+
参考 <https://ui.shadcn.com/> 的默认主题(zinc / neutral),把整套界面换成它的视觉语言。
|
|
936
|
+
**没有引入 Tailwind,也没有加构建步骤**——仍然是一个 `client.js`,在原有 `<style>` 末尾追加
|
|
937
|
+
一层「token + 组件规则」,用同名变量覆盖旧样式,所以不会破坏既有功能。
|
|
938
|
+
|
|
939
|
+
### 为什么不是直接装 shadcn 组件
|
|
940
|
+
|
|
941
|
+
shadcn 是「把 React + Tailwind 源码拷进项目」的分发方式,需要构建管线(Tailwind 编译、
|
|
942
|
+
tsconfig 路径别名、Radix 依赖)。这个插件是宿主里的单文件 bundle(`window.__ModuleLoader__`
|
|
943
|
+
+ `React.createElement`),加构建会破坏现在的零依赖部署方式。因此这里对齐的是**设计系统本身**:
|
|
944
|
+
同一套 CSS 变量名、同一套圆角/描边/阴影/焦点环尺度、同样的组件外观。
|
|
945
|
+
|
|
946
|
+
### Token
|
|
947
|
+
|
|
948
|
+
```css
|
|
949
|
+
[data-dsh-studio][data-theme="light"] {
|
|
950
|
+
--background:#fafafa; --foreground:#09090b;
|
|
951
|
+
--card:#ffffff; --primary:#18181b; --primary-foreground:#fafafa;
|
|
952
|
+
--secondary:#f4f4f5; --muted:#f4f4f5; --muted-foreground:#71717a;
|
|
953
|
+
--destructive:#dc2626; --border:#e4e4e7; --input:#e4e4e7; --ring:#a1a1aa;
|
|
954
|
+
}
|
|
955
|
+
[data-dsh-studio][data-theme="dark"] {
|
|
956
|
+
--background:#09090b; --foreground:#fafafa;
|
|
957
|
+
--card:#111113; --primary:#fafafa; --primary-foreground:#18181b;
|
|
958
|
+
--secondary:#1b1b1f; --muted:#1b1b1f; --muted-foreground:#a1a1aa;
|
|
959
|
+
--destructive:#ef4444; --border:#27272a; --input:#27272a; --ring:#52525b;
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
旧的 `--ds-*` 变量保留,但都指向新 token(`--ds-bg→--background`、`--ds-surface→--card`、
|
|
964
|
+
`--ds-surface-2→--muted`、`--ds-line→--border`、`--ds-accent→--primary`、`--ds-shadow`),
|
|
965
|
+
所以几百处既有样式不用改。统一圆角 `--radius:10px`。
|
|
966
|
+
|
|
967
|
+
### 逐项检查过的组件
|
|
968
|
+
|
|
969
|
+
| 组件 | 处理 |
|
|
970
|
+
| --- | --- |
|
|
971
|
+
| 按钮 `.ds-btn` | shadcn `Button` 默认档:32px 高、12px 内边距、1px `--input` 描边、`--card` 底、`--muted` hover、圆角 10、`--ds-shadow` |
|
|
972
|
+
| 主按钮 `.ds-btn-primary` | `bg-primary + text-primary-foreground`(浅色近黑、深色近白) |
|
|
973
|
+
| 幽灵按钮 `.ds-btn-ghost` | 透明底、hover 出 `--muted`,用于「退出登录 / 导出」 |
|
|
974
|
+
| 输入框 `.ds-input/.ds-select/.ds-textarea` | 1px `--input`、32px 高、hover 变 `--ring`、focus 出 `0 0 0 3px ring/35%` 光晕 |
|
|
975
|
+
| 卡片/面板 `.ds-panel/.ds-card/.ds-kpi` | `--card` 底 + 1px `--border` + `--ds-shadow`,圆角 12 |
|
|
976
|
+
| 徽标 `.ds-chip` | 21px 高胶囊,`--secondary` 底 + `--border`,语义色(缺陷/超时/通过)保留色相 |
|
|
977
|
+
| 页签 `.ds-tabs/.ds-tab` | shadcn `Tabs` 胶囊:`--muted` 容器,选中项 `--card` + 阴影 |
|
|
978
|
+
| 分段控件 `.ds-seg` | 同款胶囊,用于主题三档与登录方式切换 |
|
|
979
|
+
| 看板 `.ds-col/.ds-card` | 列底 `--muted` + 描边;卡片白底、13px 标题、11px 元信息 |
|
|
980
|
+
| 抽屉/弹窗 `.ds-side/.ds-drawer/.ds-modal` | `--card` 底、1px 描边、大阴影、遮罩 `rgb(9 9 11 / .5)` |
|
|
981
|
+
| 表格化行 `.ds-listrow/.ds-doc-row/.ds-audit-row` | 行间 1px `--border`,hover 出 `--muted` |
|
|
982
|
+
| 登录卡 | `Card` 同款:描边 + 20px 阴影 + 圆角 14 |
|
|
983
|
+
| 焦点环 | 全局 `:focus-visible` → `2px solid --ring`,offset 2(键盘可达性那层仍然生效) |
|
|
984
|
+
| 滚动条 | 拇指用 `foreground 20%/32%` 透明色,两套主题都可见 |
|
|
985
|
+
|
|
986
|
+
### 验证
|
|
987
|
+
|
|
988
|
+
- 浅色:工作台(KPI、分区、清单)、看板(列/卡片/徽标)、项目卡、设置-专家列表逐一截图核对。
|
|
989
|
+
- 深色:截图整页平均亮度 **23.2**(浅色约 **246**),卡片/描边/文字在 `#09090b` 上可读。
|
|
990
|
+
- 登录页、抽屉、表单沿用同一套 token,未单独写样式分支。
|
|
991
|
+
## 构建链:真实 shadcn/ui(2026-11-09)
|
|
992
|
+
|
|
993
|
+
前面几版是「手写一套 shadcn 风格 CSS」。现在 `dsh-studio/web/` 是一个真正的 Vite + Tailwind + Radix 工程,组件按 shadcn 官网源码 vendored 进来。
|
|
994
|
+
|
|
995
|
+
### 为什么需要构建,以及为什么要桥接
|
|
996
|
+
|
|
997
|
+
- 宿主只提供 `window.__ModuleLoader__.load({ id, factory })` 这个入口,`factory` 里 `require('react')` 拿到的是**宿主的 React**。
|
|
998
|
+
- shadcn/Radix 需要 React 18 的 hooks dispatcher,且组件在自己的树里跑;两棵 React 树不能共享渲染。
|
|
999
|
+
- 所以:React 打进插件自己的 bundle,宿主那边只渲染一个**类组件桥**(`web/src/bridge.jsx`,无 hooks),桥里用一个 `div`(`display:contents`)承载我们自己的 `createRoot`。两棵树互不干扰,元素对象靠 `Symbol.for('react.element')` 互通。
|
|
1000
|
+
|
|
1001
|
+
### 目录
|
|
1002
|
+
|
|
1003
|
+
| 路径 | 作用 |
|
|
1004
|
+
| --- | --- |
|
|
1005
|
+
| `web/vite.config.mjs` | IIFE、单文件 `../dist/client.js`、`cssCodeSplit:false`、`inlineDynamicImports` |
|
|
1006
|
+
| `web/tailwind.config.js` | `corePlugins.preflight=false`(CSS 会注入宿主页面,不能带全局 reset)、`darkMode:['selector','[data-theme="dark"]']`、颜色映射到 shadcn 的 HSL 变量 |
|
|
1007
|
+
| `web/src/index.css` | shadcn token(浅/深两套),作用域限定在 `[data-dsh-studio]` 与 `[data-dsh-studio-portal]`(Radix 浮层会被传送到 `<body>`) |
|
|
1008
|
+
| `web/src/components/ui.jsx` | vendored 原子组件:Button/Input/Textarea/Label/Card/Badge/Tabs/Dialog/Switch/Checkbox |
|
|
1009
|
+
| `web/src/components/login-gate.jsx` | 第一个真正替换的界面:登录卡片(Card + Tabs + Input + Button) |
|
|
1010
|
+
| `web/src/bridge.jsx` | 宿主 React ↔ 自带 React 的类组件桥 |
|
|
1011
|
+
| `web/src/main.jsx` | 入口:注入 Tailwind CSS、注册 `window.__ModuleLoader__.load` |
|
|
1012
|
+
| `web/scripts/make-legacy.mjs` | 把 `dsh-studio/client.src.js`(手写源码)转成 `web/src/legacy.js`(去掉 loader 包装,导出 `createStudio(React, h)`) |
|
|
1013
|
+
| `web/scripts/sync-client.mjs` | 构建后把 `dist/client.js` 复制回包根的 `client.js`,保证「仓库里的 `client.js` = 线上实际加载的那份」 |
|
|
1014
|
+
|
|
1015
|
+
### 构建与部署
|
|
1016
|
+
|
|
1017
|
+
```bash
|
|
1018
|
+
cd dsh-studio/web
|
|
1019
|
+
pnpm install # pnpm 10 默认拦 esbuild 的构建脚本,首次可能提示 approve-builds
|
|
1020
|
+
pnpm build # make-legacy → vite build → sync-client,一次跑完
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
三个文件的关系:
|
|
1024
|
+
|
|
1025
|
+
| 文件 | 是什么 | 谁读它 |
|
|
1026
|
+
| --- | --- | --- |
|
|
1027
|
+
| `client.src.js` | 手写的单文件源码(约 520 KB,前端逻辑全量实现) | 构建第一步 `make-legacy.mjs` 读它生成 `web/src/legacy.js` |
|
|
1028
|
+
| `dist/client.js` | Vite 产物:单文件 IIFE,React 与 CSS 都打进(约 696 KB / gzip 207 KB) | 部署时上传它就是根目录 `client.js` 的来源 |
|
|
1029
|
+
| `client.js`(包根) | 构建产物的副本,由 `sync-client.mjs` 写出 | DSH 按 `package.json` 的 `./client` 导出加载它 |
|
|
1030
|
+
|
|
1031
|
+
所以:**只改前端逻辑 → 改 `client.src.js`;改样式/组件 → 改 `web/src/**`**;两边都要在 `web/` 里跑一次 `pnpm build`,根目录 `client.js` 会自动同步,然后提交。
|
|
1032
|
+
|
|
1033
|
+
部署到另一台机器只需要 `index.js` + `dist/client.js` + 整个 `src/host`:用 `node tools/deploy.mjs`(目标机、账号从环境变量传,脚本会备份、自检、`active` 不对就非零退出),具体命令见 `docs/quickstart.md`。
|
|
1034
|
+
|
|
1035
|
+
### 已验证
|
|
1036
|
+
|
|
1037
|
+
- 构建产物 `526 kB`(gzip 161 kB),单文件、无独立 CSS 资源。
|
|
1038
|
+
- Chrome 实测:刷新后登录卡是新组件(胶囊页签 + 圆角输入框 + 黑色主按钮),切「本地超管」后直接打字即可,`admin`/`admin` 登录进入工作台,主题切换与看板正常。
|
|
1039
|
+
|
|
1040
|
+
---
|
|
1041
|
+
|
|
1042
|
+
## 对话框与列表行(2026-11-10)
|
|
1043
|
+
|
|
1044
|
+
原生 `window.confirm` / `window.prompt` 已全部换成 shadcn 风格的 Radix Dialog,23 处调用点(删除卡/需求/版本/专家/专家团/环境/项目/仓库/机器人/技能/文档/功能点、市场移除、插件装卸、清空审计、快照回滚、worktree 回收、标记阻塞、改领域、强制发布、回滚原因、评审打回)。
|
|
1045
|
+
|
|
1046
|
+
- `web/src/components/studio-ui.jsx` 的 `DialogBox({ open, title, message, danger, okText, cancelText, input, value, placeholder, onChange, onOk, onCancel })`:标题 + 说明 + 「取消 / 确定」,`danger` 时确定按钮用破坏色,`input` 为真时带 3 行文本域(用于「强制发布原因」「回滚原因」这类需要写理由的场合)。
|
|
1047
|
+
- `client.js` 的 `confirmDialog(message, options)` / `promptDialog(message, options)` 返回 Promise,`DialogHost` 挂在工作区根节点上;没有 UIKIT 时自动回落原生弹窗,保证纯单文件产物仍可用。
|
|
1048
|
+
- 列表行统一:`.ds-listrow` 内按钮 26px 高、幽灵按钮 hover 才显形;`[data-danger="1"]` 的按钮红字 + 淡红 hover;`.ds-audit-row` 行高 7/14;窄屏(≤900px)缩小 spark 日期字号、≤760px 隔天显示,避免 14 个标签挤成一排。
|
|
1049
|
+
|
|
1050
|
+
### 踩过的坑:Radix 门户跑出作用域
|
|
1051
|
+
|
|
1052
|
+
Tailwind 产物被 `web/scripts/scope-css.mjs` 改写成 `[data-dsh-studio] .fixed …`;而 Radix 的 Portal 默认挂到 `document.body`,门户里的元素不在这个子树里,定位类全部失效,弹窗会掉到页面末尾变成一段普通文本流(UIA 能读到文字、肉眼看不见)。
|
|
1053
|
+
|
|
1054
|
+
修法两处:① 作用域插件同时产出 `[data-dsh-studio-portal] …` 前缀;② `studio-ui.jsx` 提供 `portalRoot()` 懒创建一个带 `data-dsh-studio-portal` + `data-theme` 的容器,四个 Portal(Dialog ×2、Select、Tooltip)都用 `container={portalRoot()}` 挂进去,明暗主题随工作区同步。
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## 登录会话落库(2026-11-10)
|
|
1059
|
+
|
|
1060
|
+
之前会话只存在服务端内存 `Map` 里,**每次部署重启都会把所有人踢下线**。现在会话同时写进 SQLite:
|
|
1061
|
+
|
|
1062
|
+
- 新表 `sessions(token TEXT PRIMARY KEY, user_id, name, role, source, at, expires_at, ip)`,跟状态库同一个文件(`~/.dsh/dsh-studio.db`)。
|
|
1063
|
+
- `issueSession()` 发牌时既写内存也写库;`/auth/logout`、过期清理都会同时删两边;启动后第一次请求时把未过期的会话读回内存。
|
|
1064
|
+
- 会话仍然 12 小时过期,`source` 区分 `local`(内置超管)与 `ldap`(域账号),审计与卡片归属照旧读会话里的名字。
|
|
1065
|
+
|
|
1066
|
+
### 踩过的坑:`attachSessionStore` 挂得太早
|
|
1067
|
+
|
|
1068
|
+
`registerRoutes()` 是同步调用的,那时 `#boot()` 里的 `await import('node:sqlite')` 还没跑完,`store.db` 仍是 `null`——如果此时就记下「store 已挂载」,之后永远不会再读库,会话看起来「写进去了但恢复不出来」(`sessions` 表里有行、`/auth/me` 仍报 `login_required`)。
|
|
1069
|
+
|
|
1070
|
+
修法:`attachSessionStore()` 见到 `store.db === null` 直接返回,不设标记;请求管线里 `await store.ready` 之后再 `if (SESSION_STORE === null) attachSessionStore(store)`,这样第一次请求一定能把库里的会话装回来。
|
|
1071
|
+
|
|
1072
|
+
验证:登录拿到 token → `systemctl restart dsh-web` → 同一个 token 调 `/auth/me` 仍返回 `{ok:true,user:{id:'admin',role:'admin',source:'local'}}`。
|
|
1073
|
+
|
|
1074
|
+
### 卡片详情页签也换成 shadcn Tabs(2026-11-10)
|
|
1075
|
+
|
|
1076
|
+
抽屉顶部原来是自己画的下划线页签(`.ds-dw-tabs` + `<button>`),现在有 UIKIT 时走 `TabsBar`:
|
|
1077
|
+
|
|
1078
|
+
- `TabsBar` 的 `items` 支持 `{ id, label, count }`,`count` 会渲染成小号计数徽章(「讨论 3」这种),空值不显示;
|
|
1079
|
+
- 抽屉里的 7 个页签(概览 / 内容 / 计划 / 交付物 / 测试 / 执行记录 / 讨论)通过 `kit('TabsBar')` 渲染,键盘左右键可在页签间切换;
|
|
1080
|
+
- 没有 UIKIT 时仍回落原来的 `.ds-dw-tabs` 按钮组,单文件产物不会失样式。
|
|
1081
|
+
|
|
1082
|
+
验证:Chrome 里点开看板任意一张卡,UIA 能枚举到 7 个 `TabItem`(含「讨论 3」的计数),切换页签内容正常刷新。
|
|
1083
|
+
|
|
1084
|
+
### 燃尽改成折线面积图(2026-11-10)
|
|
1085
|
+
|
|
1086
|
+
效能页「燃尽(近 14 天剩余)」原来画柱子,剩余量常年是 0–2 张时柱高被 `min-height:2px` 压成一排小横线,看不出趋势。现在改成内联 SVG:
|
|
1087
|
+
|
|
1088
|
+
- `client.js` 新增 `BurnChart({ series })`:`viewBox="0 0 100 40"` + `preserveAspectRatio="none"`,`y = 38 - (remaining / max) * 32`,折线用 `path` 拼 `M/L`,面积是折线闭合到基线后填充 14% 主色;
|
|
1089
|
+
- 每个点有 `circle` + `<title>`(悬停显示「11-05:还剩 3 张」),底部左右各标一个日期(首日 / 末日);
|
|
1090
|
+
- CSS:`[data-dsh-studio] .ds-burn-svg { height:84px }`、`.ds-burn-area { fill: color-mix(in srgb, var(--ds-accent) 14%, transparent) }`、`.ds-burn-line { stroke: var(--ds-accent); stroke-width:1.6 }`、`.ds-burn-axis` 两端对齐 10.5px muted 文字。
|
|
1091
|
+
- 顺手给卡片详情页签补了轨道外观:`.ds-dw-shadcn-tabs [role="tablist"]` 28px 高、`var(--muted)` 底、`radius 8px`,`[role="tab"]` 24px 高 / 11.5px。
|
|
1092
|
+
|
|
1093
|
+
验证:构建 625.33 kB / gzip 189.42 kB,01:20:01 部署;效能页截图确认折线 + 面积 + 日期轴正常,抽屉里 7 个页签是 Radix TabItem(含「讨论 3」计数),切页正常。
|
|
1094
|
+
|
|
1095
|
+
### 成本预算条换成 Radix Progress(2026-11-10)
|
|
1096
|
+
|
|
1097
|
+
效能页「AI 执行成本」里的预算条原来是自绘的 `.ds-an-track` + `.ds-an-fill`,现在走 UIKIT:
|
|
1098
|
+
|
|
1099
|
+
- `web/src/components/studio-ui.jsx` 新增 `ProgressBar({ value, tone })`:`ProgressPrimitive.Root`(`h-2 rounded-full bg-muted`)+ `Indicator`(`translateX(-${100-pct}%)`),`tone` 取 `over` 用 `bg-destructive`、`warn` 用 `bg-amber-500`、否则 `bg-primary`;
|
|
1100
|
+
- `client.js` 新增适配器 `ProgressField({ value, tone })`:有 `kit('ProgressBar')` 用 Radix,没有就回落原来的 `ds-an-track` 自绘条(单文件产物仍可用);
|
|
1101
|
+
- 预算面板调用改成 `h(ProgressField, { value: Math.min(100, Math.max(runRate, hourRate)), tone: costs.budget.level })`。
|
|
1102
|
+
|
|
1103
|
+
验证:构建 653794 字节部署(01:24),效能页预算条正常显示(3/500 次 · 1/80 小时),产物里 `role="progressbar"` 只出现一次(说明走的是 Radix 版本,回退分支不含该属性)。
|
|
1104
|
+
|
|
1105
|
+
### 关键按钮的 Tooltip 与看板状态点(2026-11-10)
|
|
1106
|
+
|
|
1107
|
+
- `client.js` 新增 `tipOf(label, node)`:有 UIKIT 时用 Radix `Tip`(250ms 延迟、深色气泡 + 箭头、门户挂 `[data-dsh-studio-portal]`),同时**保留原生 `title`** 作为兜底(单文件产物 / 宿主没有 UIKIT 时也有提示)。
|
|
1108
|
+
- 挂上的四处:抽屉里「AI 失败 N 次 · 重新派单」、执行记录「核对代码事实」、版本详情「强制发布」、页头「退出登录」。
|
|
1109
|
+
- 看板卡片状态点改成有语义的颜色:`[data-running="1"]::after` 绿色(浅色 `hsl(152 62% 38%)` / 深色 `hsl(152 60% 56%)`)+ 光晕;`[data-blocked="1"]::after` 红色圆点(与红色描边、淡红底一起表示阻塞);两者同时存在时按阻塞显示红色。
|
|
1110
|
+
- 之前用 `--primary`(近黑)画运行中圆点,浅色下几乎看不见,这次换成绿色后一眼能分辨。
|
|
1111
|
+
|
|
1112
|
+
验证:构建 626.46 kB / gzip 189.82 kB,01:34 部署;看板截图里能看到开发中卡片右上角的绿色运行点,阻塞卡片是红点 + 红描边。
|
|
1113
|
+
## 悬停说明与评论编辑区(2026-11-10)
|
|
1114
|
+
|
|
1115
|
+
### 三种提示写法
|
|
1116
|
+
- `tipOf(label, node)`:给已有按钮/链接补一个 help 光标的气泡,同时保留原生 `title`(浏览器提示是兜底,Radix 气泡是主视觉)。
|
|
1117
|
+
- `tipLabel(text, hint)`:表单标签专用,返回 `.ds-label`(`cursor:help`)再包 `tipOf`;`hint` 为空时退化成普通标签。
|
|
1118
|
+
- `web/src/components/studio-ui.jsx` 里的内部 `Hint`:`TabsBar` 的 `item.hint` 走它——`TooltipProvider delayDuration={260}` + `Portal container={portalRoot()}`,所以抽屉/上下文中都能正常定位。
|
|
1119
|
+
|
|
1120
|
+
覆盖范围:顶部 8 个页签、卡片详情 7 个页签、设置左导航全部条目、12 个行内/关键按钮、9 个容易填错的字段(Agent 预设、沙箱档位、角色位、业务领域、Coding Agent、单测总数、通过数、baseUrl、连续失败几次后升级)。
|
|
1121
|
+
|
|
1122
|
+
### 评论编辑区
|
|
1123
|
+
- 结构:`.ds-composer`(外框 + `focus-within` 时的 3px 淡环)内放 `TextAreaField`,下面是 `.ds-composer-bar`(左提示、中间字数、右发表按钮)。
|
|
1124
|
+
- 快捷键:`Ctrl/⌘ + Enter` 直接发送;`@` 仍会唤起专家联想(↑↓ 选择、Enter 补全、Esc 关闭)。
|
|
1125
|
+
- `.ds-composer .ds-textarea` 去掉了自身边框/阴影/背景,让外框承担视觉。
|
|
1126
|
+
- 提示文案:`Markdown 可用 · 打 @ 派活 · Ctrl/⌘+Enter 发送`;空内容时右侧显示「还没写」,有内容显示字数。
|
|
1127
|
+
|
|
1128
|
+
## 轻提示(toast)(2026-11-10)
|
|
1129
|
+
|
|
1130
|
+
保存、同步、归档这类动作的结果原来只写在各面板自己的小字里,面板一滚或一切页签就看不见了。现在多一层右下角的轻提示:
|
|
1131
|
+
|
|
1132
|
+
- `toast(text, tone = 'success', ttl = 3200)`:右上角勾/感叹号图标区分成功与失败,3.2 秒自动消失;`dismissToast(id)` 可提前关;点提示上的 × 也能关。
|
|
1133
|
+
- 宿主是 `ToastHost`,挂在 `StudioPage` 根部(紧跟 `DialogHost`),只订阅 `TOAST_LISTENERS`,不持有业务状态;同一时刻的多条会纵向堆叠。
|
|
1134
|
+
- 样式在 `web/src/index.css`:`.ds-toasts` 固定右下 `z-index:120`、`pointer-events:none`(只有提示本身可点),`.ds-toast` 用 `--card` 底 + `--border` 描边 + 大阴影,`[data-tone="error"]` 换红描边与红图标。
|
|
1135
|
+
- 已接上的位置:专家/专家团/技能保存、部署环境保存、测试连接结果、云盘归档结果,以及三处 `catch` 分支(失败原因既写面板内小字、也弹一条红提示)。
|
|
1136
|
+
- 版本页的保存/发布/回滚/登记部署/灰度/挂卡全部走 `act(path, body, done)`,`done` 文案会同时进页面小条和 toast。
|
|
1137
|
+
- 需求页:正文保存、状态切换、签字/取消签字、排序与依赖保存。
|
|
1138
|
+
- 面板内的 `.ds-ok` / `.ds-err` 同时改成「图标 + 文字」的药丸(成功绿、失败红,深色主题各有一版),和 toast 的观感一致;`::before` 用 `content` 画 ✓ / !,不需要额外标签。
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
### 发布门禁的操作区(2026-11-10)
|
|
1143
|
+
- 原来四个按钮平铺一行、主次不分;现在分成两段:左边「生成发布说明 / 审批通过」(辅助动作),右边「发布(主按钮)/ 强制发布(琥珀色描边)」,已发布状态右侧多一个「回滚」。
|
|
1144
|
+
- 容器是 `.ds-action-bar`:`space-between` + 顶部细线分隔,`.ds-action-bar-right` 用 `margin-left:auto` 贴右;窄屏自动换行。
|
|
1145
|
+
- 「强制发布」用 `data-force="1"` 单独配色(浅色 #b45309、深色 #fbbf24 文字 + 琥珀描边),不再是 ghost 灰按钮,避免和「取消」类混淆。
|
|
1146
|
+
|
|
1147
|
+
### toast 去重
|
|
1148
|
+
- 同一个动作可能在极短时间内触发两次(按钮重复绑定、`onBlur` 与 `onClick` 各跑一遍等)。`toast()` 现在记住上一条文案与时间,600ms 内相同文案直接吞掉。
|
|
1149
|
+
- 实测:「生成发布说明」原来会在右下角堆两条一模一样的提示,加去重后只弹一条(2026-11-10 02:57 截图确认)。
|
|
1150
|
+
|
|
1151
|
+
### 需求页的操作区(2026-11-10)
|
|
1152
|
+
- 需求抬头右侧从「状态下拉 + 编辑 + 删除(同等份量)」改成:状态下拉(宽度收窄)→ 编辑(常规按钮)→ 删除(红色 ghost)。
|
|
1153
|
+
- 评审面板里的「打回」也改成红色 ghost;「提交评审 / 评审通过」仍是主按钮。这样一屏里只有一个主操作,破坏性动作永远是弱化的红字。
|
|
1154
|
+
- CSS 新增 `.ds-req-actions`(flex + gap 8 + 窄状态下拉)与 `.ds-btn-ghost[data-danger="1"]`(红字红边,深色版用 #fca5a5)。
|
|
1155
|
+
|
|
1156
|
+
### 按钮主次与深色表面(2026-11-10)
|
|
1157
|
+
- 主按钮 `.ds-btn-primary`、次级 `.ds-btn-ghost`、危险 `[data-danger="1"]`、警告 `[data-warn="1"]` 全部改用**实色**(不依赖 `hsl(var(--primary))`)。原因:变量在部分子树(尤其 Radix Portal)里解析不到,主按钮会退回成纯文字,主次看不出来。
|
|
1158
|
+
- 卡片详情「推进到」按列的先后自动分级:下一列是主按钮,往前退的列是 ghost,其余常规;「标记阻塞」是琥珀色警告,解除阻塞里的「只解除,先不派」是 ghost。
|
|
1159
|
+
- **Portal 要同时带两个属性**:抽屉/弹窗挂在 `document.body` 下,只带 `[data-dsh-studio-portal]` 时所有以 `[data-dsh-studio]` 为前缀的既有样式(按钮、chip、表单行)都不生效。现在 `portalRoot()` 会同时设置 `data-dsh-studio` 与 `data-dsh-studio-portal`,并用 `[data-dsh-studio]:not([data-dsh-studio-portal])` 读取主树的主题,避免自引用把主题锁死。
|
|
1160
|
+
- 深色主题下补了 legacy 的表面变量(`--ds-surface:#181b22`、`--ds-surface-2:#1e222a`、`--ds-line:#2c323d`):legacy CSS 把 `--ds-surface` 写死成 `#ffffff`,导致抽屉在暗色下依然是白底。
|
|
1161
|
+
|
|
1162
|
+
### 空状态统一(2026-11-10)
|
|
1163
|
+
- 面板里「还没有… / 暂无… / 没有可用的…」这类说明行统一用 `.ds-empty`:虚线框 + 淡底 + 居中 12px 文案,取代原来 40% 透明度的小灰字(对比度太低,容易被当成没加载出来)。
|
|
1164
|
+
- 本轮把 20 处 `className: 'ds-hint'` 的说明行换成 `.ds-empty`(专家/专家团/技能/机器人/项目/迭代/环境/成员/审计/评论等),加上原有的 5 处,全站 25 处空状态外观一致。
|
|
1165
|
+
- 需要「标题 + 说明」两级时用 `.ds-empty-title`(12.5px/600、前景色),正文仍走 `.ds-empty` 的 muted 色。
|
|
1166
|
+
|
|
1167
|
+
### 项目页与市场页的卡片(2026-11-10)
|
|
1168
|
+
- 项目页原来三张卡被挤成 ~145px 的窄列(外层容器参与父级网格导致),现在用 `.ds-body:has(> .ds-proj-grid){display:block;width:100%}` 回到整行布局,每张项目卡占满一行,统计项与清单进度条不再换行挤压。
|
|
1169
|
+
- 项目卡、市场卡统一 shadcn Card 的解剖:`1px var(--border)` + `calc(var(--radius) - 1px)` 圆角 + `var(--ds-shadow)`,hover 时描边加深 + 抬起阴影;卡头(`.ds-proj-head/.ds-mc-head`)flex 对齐,标题 13px/600。
|
|
1170
|
+
- `.ds-proj-stats` 改成 `flex-wrap` + 6/12 间距,窄屏自动折行;`.ds-proj-bar` 高度 6px、圆角 999px、muted 底。
|
|
1171
|
+
|
|
1172
|
+
### 效能页条形与零值(2026-11-10)
|
|
1173
|
+
- 周期分布 / 每列停留的条形:计数为 0 时不再画满宽的空轨道(会被误读成满格),改成一条虚线基线;非零但很小时保留 2px 最小宽度,避免"看不见"。
|
|
1174
|
+
- 实现:行上带 `data-empty`,CSS ` .ds-an-track[data-empty="1"]{ background:transparent; border-top:1px dashed var(--border) }`,填充条 `min-width:2px`。
|
|
1175
|
+
|
|
1176
|
+
### 多实例派单锁与 SLA 升级(2026-11-10)
|
|
1177
|
+
- **派单租约**:多开一个 dsh-web(或两台机器连同一个库)时,卡片不能被两个进程同时派出去。实现是 SQLite 里的 `locks` 表(`name/owner/at`)+ 12 秒租约:`Dispatcher.tick()` 开头先 `claimDispatchLock()`,抢不到就跳过这一轮,抢到则每轮续租。没有数据库(纯内存模式)时直接放行,单实例行为不变。
|
|
1178
|
+
- `Dispatcher.status()` 现在带 `instance`(`pid-随机串`)与 `lock`(`{owner, at, mine}`),排查「谁在派单」时看这两个字段(`/state` 的 `state.agents`)。实测:进程重启后 `instance` 变化、`lock.owner` 在 12 秒租约过期后自动交接给新进程。
|
|
1179
|
+
- **缺陷 SLA 升级**:`escalateOverdue()` 在每轮 tick 里扫缺陷卡,`bugSla()` 判定 `overdue` 且还没升级过的,写一次 `escalatedAt` + 一条历史「SLA 超时 N 小时,已升级提醒」,并按现有飞书机器人配置广播一条;同一张卡只升一次(重复 tick 不会再写)。
|
|
1180
|
+
- 验证:`tools/test-sla-escalation.mjs`(在 .218 跑,会临时停服改库再起服)——建 p0 缺陷卡 → 把 `createdAt` 回拨 10 小时 → 等一个 tick → 断言 `escalatedAt > 0`、历史正好一条「SLA 超时 6 小时」→ 删测试卡,7/7 PASS。
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
## 审计日志的检索与导出(2026-11-10)
|
|
1184
|
+
|
|
1185
|
+
设置 → 权限与审计 → 审计日志,标题右侧现在有三样东西:**关键字框**(同时匹配动作、操作人、详情)、**按人下拉**、**只看失败**,三者可叠加;标题里的计数是「显示 N / 命中 M / 共 K」,筛选后没有结果时会提示「没有符合筛选条件的记录」,而不是误报「还没有记录」。
|
|
1186
|
+
|
|
1187
|
+
**导出 CSV** 走服务端而不是前端拼 blob:
|
|
1188
|
+
|
|
1189
|
+
```
|
|
1190
|
+
GET /studio-api/audit/export.csv?by=<人>&q=<关键字>&failed=1&token=<会话>
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
- 为什么不用 blob:http 页面里 `a[download]` + `URL.createObjectURL` 生成的下载会被 Chrome 当成不安全下载拦掉(点了没反应、也没文件);服务端返回 `Content-Disposition: attachment` 才是稳的。
|
|
1194
|
+
- 复用 `sendCsv()`:UTF-8 + BOM,含逗号/引号/换行的单元格自动加引号,Excel 直接打开不乱码。
|
|
1195
|
+
- 上限 5000 行,按时间倒序;列是 时间 / 操作人 / 动作 / 详情 / 结果。
|
|
1196
|
+
- 锁门(LDAP 且「必须登录」)时校验 query 里的 `token`,没有会话直接 401「导出审计日志要先登录」——所以这个路由虽然放在开放集合里,数据本身仍要会话。
|
|
1197
|
+
- 验证:`q=登录` 导出 200 / 15,679 字节 / 145 行;不带 token 401;浏览器里点「导出 CSV」会走系统保存对话框,打开后是带引号的正确表格。
|
|
1198
|
+
## 审计与派单日志的检索、导出(2026-11-10)
|
|
1199
|
+
|
|
1200
|
+
### 审计日志(设置 → 权限与审计)
|
|
1201
|
+
|
|
1202
|
+
- 过滤:关键字(在「动作 / 操作人 / 详情」里做包含匹配)、操作人下拉(`所有人` 为不筛选)、`只看失败`。
|
|
1203
|
+
- 标题实时显示「显示 120 / 命中 M / 共 K 条」——`显示` 是当前渲染上限(120),`命中` 是过滤后的总数,`共` 是日志总量。
|
|
1204
|
+
- 导出走服务端:`GET /studio-api/audit/export.csv?by=&q=&failed=1&token=`,最多 5000 行,UTF-8 + BOM,Excel 可直接打开。
|
|
1205
|
+
- 锁门(`ldap.enabled && ldap.requireLogin`)时必须带 `token`(12 小时有效的会话令牌),否则 401「导出审计日志要先登录」。
|
|
1206
|
+
- 不用浏览器 Blob 下载:Chrome 会把 http 页面的 blob 当作不安全下载直接拦掉,点了「导出」却什么都没保存。
|
|
1207
|
+
- 空态分两种:「还没有记录」和「没有符合筛选条件的记录」。
|
|
1208
|
+
|
|
1209
|
+
### 派单日志(设置 → 集成健康 → 派单运行状态)
|
|
1210
|
+
|
|
1211
|
+
- 面板同时显示:实例标识(`pid-hex`)、派单锁(`本进程持有 / 其他实例持有` + 最近续租秒数)、并发占用(`running / maxConcurrent`)、最近一轮时间、最近错误。
|
|
1212
|
+
- 日志区支持关键字、`只看失败`(匹配 `失败|错误|异常|拦|拒绝`)、导出 `GET /studio-api/export/dispatch.csv?q=&failed=1&token=`(上限 2000 行,同样要会话令牌)。
|
|
1213
|
+
- 默认只渲染最近 12 行,超过时提示「只显示最近 N / 命中 M 条,导出可看全量」;没有派单活动时 `agents.log` 为空,这是正常状态。
|
|
1214
|
+
|
|
1215
|
+
### 踩过的坑
|
|
1216
|
+
|
|
1217
|
+
- **Radix Select 不接受空字符串作为可展示项**:`h('option', { value: '' }, '所有人')` 会一直显示 placeholder「请选择」。改成哨兵值 `__all__`,提交时再映射回空字符串。
|
|
1218
|
+
- `agents.log` 是内存环形日志,重启后清空;要留档请用导出。
|
|
1219
|
+
- 两处导出的鉴权都在**服务端**做(`openRoutes` 放行的只是路由本身,数据仍要校验 token),这样即便前端被绕过也拿不到全量日志。
|
|
1220
|
+
|
|
1221
|
+
## 布局与 Markdown 一致性(2026-11-10)
|
|
1222
|
+
|
|
1223
|
+
这一轮修的是「看起来哪里都不对」的根因,而不是逐个页面调参数。
|
|
1224
|
+
|
|
1225
|
+
### 1. 同名 CSS token 被两种格式覆盖
|
|
1226
|
+
- `web/src/index.css` 里 shadcn token 原本是 HSL 三元组(`--border: 240 6% 90%`),约定 `hsl(var(--x))`;
|
|
1227
|
+
legacy `client.js` 又把同名变量定义成 hex(`--border:#e4e4e7`),特异性更高、注入更晚。
|
|
1228
|
+
- 结果:所有 `hsl(var(--x))` 变成 `hsl(#e4e4e7)`,声明非法被丢弃 —— 列底色透明、卡片描边消失、按钮没底色。
|
|
1229
|
+
- 修法:**全站统一为 hex + 裸 `var(--x)`**。`tailwind.config.js` 19 处、`index.css` 92 处、`client.js` 1 处;
|
|
1230
|
+
`hsl(var(--x) / a)` 改写成 `color-mix(in srgb, var(--x) N%, transparent)`。判断依据是页面内
|
|
1231
|
+
`getComputedStyle(el).getPropertyValue('--border')` 返回的是 `#e4e4e7` 而不是三元组。
|
|
1232
|
+
|
|
1233
|
+
### 2. `.ds-row` / `.ds-field` 只有 min-width,没有 display
|
|
1234
|
+
- 全站 97 处 `ds-row`、151 处 `ds-field`,布局原本靠行内 `style={{ gap, alignItems, flex }}`;
|
|
1235
|
+
但 CSS 里这两个类只有 `min-width:0`,于是它们退化成块级堆叠 —— 专家编辑里标签和控件互相压住。
|
|
1236
|
+
- 修法(`index.css`):
|
|
1237
|
+
`[data-dsh-studio] .ds-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;min-width:0}`、
|
|
1238
|
+
`[data-dsh-studio] .ds-field{display:flex;flex-direction:column;gap:5px;min-width:0}`、
|
|
1239
|
+
`.ds-row > .ds-field{flex:1 1 220px}`;字段内的 `.ds-hint` 取消 `margin-left:auto`。
|
|
1240
|
+
|
|
1241
|
+
### 3. uiKit 缺 Input / Textarea,多行输入只剩 32px
|
|
1242
|
+
- `web/src/main.jsx` 的 `uiKit` 只注入了 SheetDrawer/TabsBar/SelectBox/SwitchBox/Tip/ProgressBar/DialogBox/Badge,
|
|
1243
|
+
`kit('Input')`、`kit('Textarea')` 全是 null → 112 处 TextField、14 处 TextAreaField 回落成原生控件。
|
|
1244
|
+
- 原生 textarea 又被表单 reset 清了 padding,`rows=9` 实测只有 32px 高(角色提示词框就是这个)。
|
|
1245
|
+
- 修法:uiKit 补 `Input, Textarea, Label`;`TextAreaField` 不再丢弃调用方 className,
|
|
1246
|
+
并按 `rows` 计算 `minHeight = max(84, rows*21+26)`(9 行 → 215px)。
|
|
1247
|
+
|
|
1248
|
+
### 4. Markdown:输入处给预览,展示处统一渲染
|
|
1249
|
+
- 新增 `MdField`(`client.js`,在 `tipLabel` 之前):工具行「支持 Markdown:标题 / 列表 / 粗体 / 代码块 / 表格」+
|
|
1250
|
+
「预览渲染 / 回到编辑」切换,预览复用 `renderMarkdown()`。
|
|
1251
|
+
- 换用位置:需求正文、文档正文、交付物正文、技能正文、任务需求/背景、功能点说明、发布说明。
|
|
1252
|
+
- 展示处保持渲染 Markdown 的:需求明细、功能点说明、评论、发布说明(复制前可预览)。
|
|
1253
|
+
- 验收方式:无头 Chrome 打开「需求 → 编辑」,`.ds-md-bar` 存在、textarea 219px、点「预览渲染」后
|
|
1254
|
+
`.ds-md-preview` 出现且 `li` 数量与原文条目一致。
|
|
1255
|
+
|
|
1256
|
+
### 5. 看板列与滚轮
|
|
1257
|
+
- `.ds-col` 实色底 + 1px 描边 + `scroll-snap-align:start`;`.ds-col-head` 卡片底色;`.ds-card{min-width:0;max-width:100%}`。
|
|
1258
|
+
- 滚轮改原生 `{ passive:false }` 监听(React 的 onWheel 在 root 上是 passive,`preventDefault` 无效),
|
|
1259
|
+
命中 `.ds-board` 时把 `deltaY` 转成 `scrollLeft`。
|
|
1260
|
+
|
|
1261
|
+
## 直接阅读与暗色巡检(2026-11-10)
|
|
1262
|
+
|
|
1263
|
+
### A. 交付物 / 文档正文可直接展开阅读
|
|
1264
|
+
|
|
1265
|
+
- 交付物:`artRead` 状态记「当前展开的交付物 id」,行按钮「阅读 / 收起」;展开区 `.ds-md-preview.ds-reader`(`max-height:360px; overflow:auto`)用 `renderMarkdown(content)` 渲染,空正文提示「这条交付物还没有正文。」
|
|
1266
|
+
- 文档:`docReader` 状态 + `readDoc(docId)`——点「阅读」时按需 `POST /studio-api/requirement/get` 取回完整 `docs[]`(列表接口只给 `contentLength`),再渲染 Markdown;读取中显示「读取中…」,空正文提示「这条文档还没有正文。」;再点「收起」关闭。
|
|
1267
|
+
- 实测:TEST-002 卡片 → 交付物 → 阅读,`.ds-reader` 高 360、65 个块;需求 `REQ-2026-001` → 文档 → 阅读,`.ds-reader` 高 235、5 个块、正文以「会员积分抵扣背景…」开头。
|
|
1268
|
+
|
|
1269
|
+
### B. 暗色逐页巡检(明亮 + 黑暗各一遍)
|
|
1270
|
+
|
|
1271
|
+
- 巡检方式:`tools/visual/probe-dark-sweep.mjs`(headless Chrome 登录 → 挂载面板 → 逐页签截图 + 采集 `getComputedStyle`),截图落在 `tmp/dark-*.png` / `tmp/dark2-*.png`。
|
|
1272
|
+
- 修复:看板工具栏 `.ds-board-bar` 之前被压成 `padding: 0 0 10px`,第一个搜索框紧贴面板左边缘、看着像被裁掉;改成 `padding: 12px 20px 10px; border-bottom: 1px solid var(--border)`,与看板列的 20px 内边距对齐,工具行与内容之间也有分隔线。
|
|
1273
|
+
- 复核:看板(列底 `--muted`、列头 `--card`、卡片描边与文字 `rgb(230,232,238)`)、版本(面板/操作区/发布按钮)、效能(KPI 卡、柱状与日期轴、燃尽折线)、项目(整行卡片、统计框、服务行 chip)、市场(专家卡与角色 chip、空态虚线框)、需求(评审、矩阵、文档、变更影响)六页在暗色下均无白底突兀、无不可读文字。
|
|
1274
|
+
- 已知边界:无头 Chrome 未装 emoji 字体,截图里 📎 ✅ 之类的图标会显示成空心方块,这不是页面问题。
|
|
1275
|
+
|
|
1276
|
+
## UI 收尾交接(2026-11-10 末尾)
|
|
1277
|
+
|
|
1278
|
+
线上产物 `dsh-studio/dist/client.js` 661.14 kB / gzip 198.65 kB,rev 3767,无 console error。
|
|
1279
|
+
|
|
1280
|
+
### 已完成(本轮)
|
|
1281
|
+
- 卡片详情「推进到」:8 个状态按钮 → 「推进到「下一列」」主按钮 + 「其他状态(N)」下拉(退回项标「AI 会重跑」)。锚点 `dsh-studio/client.js` 的 `ds-listrow-actions` + `forward`/`later`/`earlier` 计算。
|
|
1282
|
+
- 「AI 处理」:负责人下拉提成独立 `.ds-field`(标签「派给谁」),「马上处理 / 停止」单独一行。
|
|
1283
|
+
- 「准出检查」:签字行与未签字输入行用虚线分隔(`.ds-gate-sign`、`.ds-gate .ds-row`)。
|
|
1284
|
+
- 主题三档改为 Radix ToggleGroup(`SegGroup`,`uiKit` 已注入);顶部 8 页签直用 `Tabs/TabsList/TabsTrigger`;两者选中态均为实心主色。
|
|
1285
|
+
- `studio-ui.jsx` 补 `import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'`(此前缺失导致整页白屏,被 `Boundary` 接住)。
|
|
1286
|
+
|
|
1287
|
+
### 待做(新会话从这里接)
|
|
1288
|
+
1. 看板拖拽换列 + 列悬停高亮(`client.js` 的 `function Card` / 列容器 `.ds-col`)。
|
|
1289
|
+
2. 每列 WIP 上限:后端 `sanitizeColumns` 增 `wip`,`ColumnsPanel` 加输入,列头渲染 `done / wip`,超限列头变琥珀。
|
|
1290
|
+
3. 需求文档「全部展开」:`readDoc(docId)` 已有,改成一次 `/requirement/get` 拉全部 docs 并批量展开。
|
|
1291
|
+
4. 验证套路:`node tools/visual/probe-mount.mjs`(挂载 + 报错)、`probe-open-card.mjs`(抽屉段与动作行)、`probe-states.mjs`(页签/radiogroup)。
|
|
1292
|
+
|
|
1293
|
+
## 需求管理补强(2026-11-11)
|
|
1294
|
+
|
|
1295
|
+
### 待确认清单(questions)
|
|
1296
|
+
- 需求对象新增 `questions[]`:`{ id, text, status: open|answered, owner, answer, at, answeredAt }`,最多 50 条。
|
|
1297
|
+
- 接口 `POST /requirement/questions`,`action` 为 `add` / `answer` / `delete`;回答为空视为无效。
|
|
1298
|
+
- 门禁:只要有 `open` 的问题,`requirementGate` 就拦「还有 N 个待确认问题没回答:…」,拆卡前必须先答完。
|
|
1299
|
+
- 界面:需求详情「待确认清单」面板,支持加问题、回答/改答案、删除(危险色),未答完时面板高亮。
|
|
1300
|
+
|
|
1301
|
+
### 需求池 RICE
|
|
1302
|
+
- 需求新增 `pool { reach, impact, confidence, effort, score }`;`score = reach × impact × (confidence/100) / effort`,effort 为 0 时 score 为 0。
|
|
1303
|
+
- 接口 `POST /requirement/pool { id, pool }`,后端统一清洗(reach ≤ 10 万、impact ≤ 3、confidence ≤ 100、effort ≤ 365)。
|
|
1304
|
+
- 门禁:score 为 0 时给 warning「还没做需求池打分(RICE)」,只提醒不拦截。
|
|
1305
|
+
- 界面:需求详情「需求池评分(RICE)」面板,四步弹窗收参数,已打分时显示五个胶囊(含高亮得分)。
|
|
1306
|
+
|
|
1307
|
+
### 变更历史
|
|
1308
|
+
- 需求新增 `history[]`:`{ at, by, field, from, to }`,最多 100 条。
|
|
1309
|
+
- `/requirement/save` 保存前后比对七个字段(标题 / 需求明细 / 优先级 / 状态 / 负责人 / 项目 / 迭代),有差异才记,各值截断 300 字。
|
|
1310
|
+
- 界面:需求详情「变更记录」面板,时间 · 操作人 · 字段 · 旧值 → 新值,最高 260px 内部滚动,新的在上。
|
|
1311
|
+
- 与「变更影响」面板分工:变更影响管「已经产出的卡要不要返工、签字是否作废」,变更记录管「谁在什么时候改了什么」。
|
|
1312
|
+
|
|
1313
|
+
### 澄清技能
|
|
1314
|
+
- 新增技能 `requirement-elicitation`(`~/.dsh/skills/requirement-elicitation/SKILL.md`):五问、5 Whys / JTBD / 故事地图 / 场景走查、RICE 打分、产出模板、收口自检。
|
|
1315
|
+
- 产品角色专家挂上该技能,提示词补「先问五问再动笔、不确定写进待确认清单、涉及金额权限对账合规必须 @架构师」。
|
|
1316
|
+
- 「待澄清」列配置为候选池(`best_match`),卡进入后自动派给产品专家。
|
|
1317
|
+
|
|
1318
|
+
### 验证
|
|
1319
|
+
- `/requirement/questions`:加 → 门禁出现「还有 1 个待确认问题没回答」→ 回答 → 门禁消失 → 删除 → 归零。
|
|
1320
|
+
- `/requirement/pool`:`{1000, 2, 80, 5}` → `score 320`,`/state` 读回一致。
|
|
1321
|
+
- 变更历史:临时需求 description v1 → v2,`history.length = 1`,随后删除需求清理。
|
|
1322
|
+
|
|
1323
|
+
## 泳道按角色、卡片按领域(2026-11-12)
|
|
1324
|
+
|
|
1325
|
+
看板的每一列现在挂的是**角色位**(产品 / 架构 / 研发 / 测试),而不是某一位具体专家:
|
|
1326
|
+
|
|
1327
|
+
- 列配置 `config.columns[status]` 的成员可以是 `{ kind: 'role', id: 'product' | 'architect' | 'dev' | 'qa' }`,界面里显示成「🎯 产品」「🎯 研发」这样的标签。
|
|
1328
|
+
- 派单时(`ownerFor` → `columnCandidates` → `roleCandidates`)先按角色取人,再按卡片的**业务领域**排序:同领域 2 分 > 没写领域 1 分 > 其他领域 0 分,之后再走候选池策略(默认轮流)。
|
|
1329
|
+
- 卡片的领域来自 `task.domain`;没写就继承所属需求的 `domain`(`taskDomain`)。需求的「业务领域」可以在需求表单里填(支付 / 风控 / 会员…),保存时随需求落库。
|
|
1330
|
+
- 迁移 `schema 6` 会把旧配置里「直接指向某个领域专家的列」换成对应的角色位,避免「换人还要改列」。
|
|
1331
|
+
- `/columns/save` 保留成员原本的 `kind`(`team` / `role` / `expert`),不会再被强行洗成 expert。
|
|
1332
|
+
|
|
1333
|
+
实测(.218,看板 20 张卡):把「退款对账」需求的领域设为「支付」后新建一张测试卡,派单落到**支付测试工程师**(`expert-87de8b`);同一列为 `role:qa` 时,没写领域的卡落到**测试工程师**(`expert-d3c58341`)。
|
|
1334
|
+
|
|
1335
|
+
## 新建入口合并为「+ 新建」(2026-11-12)
|
|
1336
|
+
|
|
1337
|
+
看板上的「+ 新建任务」和需求页的「+ 新建需求」原来各管一边,用户要记住「建卡去哪个页面」。现在头部只留一个入口:
|
|
1338
|
+
|
|
1339
|
+
- 右上角「+ 新建」展开一个两项菜单(`role="menu"`、`aria-haspopup`、`aria-expanded`):
|
|
1340
|
+
- **新建需求** → 切到需求页并**直接展开新建表单**(副标题「需求 → 功能点 → 拆成卡」);
|
|
1341
|
+
- **新建任务** → 切到看板并打开新建卡弹窗(副标题「直接在看板上建一张卡」)。
|
|
1342
|
+
- 点菜单外部、或选完任一项自动收起;菜单绝对定位(`zIndex 80`、`minWidth 232`),不撑开头部布局。
|
|
1343
|
+
- 需求页原按钮位置改成一行提示「新建需求请用右上角「+ 新建」」,避免两个入口并存。
|
|
1344
|
+
|
|
1345
|
+
实现要点(`client.js`):
|
|
1346
|
+
|
|
1347
|
+
- `StudioPage` 增 `newMenu`(开合)与 `newReq`(信号计数),`useEffect` 监听 `document.click` 关闭菜单;主按钮与菜单本体都 `stopPropagation`,否则点一次会被自己的关闭逻辑吃掉。
|
|
1348
|
+
- 菜单项「新建需求」执行 `setTab('requirements')` + `setNewReq(n => n + 1)`;「新建任务」执行 `setTab('board')` + `setCreating(true)`。
|
|
1349
|
+
- `RequirementsPage({ …, createSignal })` 用 `useEffect(() => { if (Number(createSignal) > 0) openNew() }, [createSignal])` 响应信号。
|
|
1350
|
+
|
|
1351
|
+
### 顺带修掉的隐藏 bug
|
|
1352
|
+
|
|
1353
|
+
需求页原来的「+ 新建需求」绑的是 `intakeOpen` 这个**只写不读**的 state(`setIntakeOpen('form')` 之后再没有任何地方读它),所以点了没有任何反应——真正的建单面板由 `form` 状态控制(`form === null` 不渲染)。
|
|
1354
|
+
|
|
1355
|
+
现在统一走 `openNew()`:
|
|
1356
|
+
|
|
1357
|
+
```js
|
|
1358
|
+
const openNew = () => {
|
|
1359
|
+
setForm({ title: '', description: '', project: '', iteration: '', owner: '', priority: 'normal', status: 'draft', links: {} })
|
|
1360
|
+
setError(null)
|
|
1361
|
+
}
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
### 验证
|
|
1365
|
+
|
|
1366
|
+
- 构建 `dist/client.js` 696.62 kB / gzip 207.21 kB,`deploy2.py` 部署后 `dsh-web.service` active。
|
|
1367
|
+
- headless(1600×1000)实测:头部文案为「+ 新建」;展开后菜单两项文案正确;点「新建需求」后活动页签为「需求」,面板头出现**新建需求**,字段齐全(标题 / 业务项目 / 状态 / 业务负责人 / 业务领域 / PRD·原型·验收链接 / 正文),`pageerror` 为 0。脚本 `tools/visual/newmenu-check.mjs`、`tools/visual/drawercheck.mjs`。
|