@marsken/dsh-studio 0.1.16 → 0.1.18

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 CHANGED
@@ -78,7 +78,10 @@ STUDIO_TOKEN=$(sudo journalctl -u dsh-web -n 50 | grep -o 'token=[A-Za-z0-9_-]*'
78
78
  6. `npm test`(条数以 `npm run check` 打印的为准,别再写死数字;覆盖范围是:`tests/repo.test.mjs` 管 manifest / 产物一致 / 宿主接口 / 领域层拆分与导入 / **导入只读绑定不许赋值** / **跨模块符号必须导入** / 迁移 / 文档 / locale / **用了 `React.` 的组件必须从 `client()` 或工厂参数拿到 `React`**;`tests/client-units.test.mjs` 用真实模块跑标签表/负责人/日期/命令面板检索这些纯函数;`tests/notify.test.mjs` 起一个本地 HTTP 桩验证飞书通知(正文归一、`botWants` 的事件矩阵、`feishuBroadcast` 只发给订阅该事件的机器人);`tests/store-boot.test.mjs` 真的起一个 `StudioStore`,把「搬完代码启动就炸」这类问题挡在部署前。想连真数据验证:把生产库三个文件(`.db`/`-wal`/`-shm`)拷到临时目录,用 `new StudioStore()` + `await store.ready` 跑一遍,`tmp/boot-real.mjs` 就是这么写的);`npm run check` = 语法 + 契约一把过
79
79
  7. 构建期守卫:`web/scripts/guard.mjs`(`pnpm build` 自动跑两次)——禁 CSS 里出现 `hsl(var(--x))`、禁 Radix Portal 不挂 `container={portalRoot()}`、产物里必须留下 `data-dsh-studio-portal`。单独跑:`cd web && npm run guard`
80
80
  8. 真库自检:`npm run smoke:store -- ~/.dsh/dsh-studio.db`(把 `.db/-wal/-shm` 拷到临时目录起一遍真实 `StudioStore`,专抓「空库能起、真库就炸」的导入问题)
81
- 9. CI 在 `.github/workflows/ci.yml`:语法 → 契约 → `web/` 里 `pnpm install --frozen-lockfile && pnpm build` → `cmp dist/client.js client.js` → `git diff --exit-code`(产物必须可复现,改了前端忘记构建会直接红)
81
+ 9. 成本数字可疑时跑 `npm run verify:usage [taskId]`:ssh 取原始会话日志现算一遍 token,与 `/costs` 对账(两条路径不同:一条走 session-usage 的 zstd 缓存,一条走 usage.js 直接解析)。
82
+ 10. 发版后跑 `npm run verify:release`:比对 package.json 版本 / 本地 tag / origin main 与 tag / npm registry,不一致直接退出码 1。
83
+ 11. 全闭环自检:`STUDIO_BASE=http://<host>:18080 node tools/e2e-loop.mjs`——真建一条需求和卡,跑完需求评审→拆卡→各列→代码事实→用例→验收→版本发布。**会写入真实数据**,跑完记得清。
84
+ 12. CI 在 `.github/workflows/ci.yml`:语法 → 契约 → `web/` 里 `pnpm install --frozen-lockfile && pnpm build` → `cmp dist/client.js client.js` → `git diff --exit-code`(产物必须可复现,改了前端忘记构建会直接红)
82
85
 
83
86
  ## 5. 禁止事项
84
87
 
@@ -149,4 +152,5 @@ GIT_SSH_COMMAND="ssh -p 443" git push
149
152
  | 点击页面卡顿 | DevTools Performance 找长任务;`keyboardPatch` 之类全量扫描是历史元凶 |
150
153
  | 派单没留言 | 会话日志里找 `turn/end reason.kind=error`;常见是模型无可用通道(503) |
151
154
  | 保存成功但字段为空 | `publicConfig`/`mergeConfig`/`sanitize*` 白名单漏字段 |
155
+ | 测完失败路径改动没生效 | 用 `git checkout -- package.json` 还原测试用的假版本,会把**同一个文件里**刚加的其它改动一起还原。还原后 `grep` 一下你新加的那行还在不在 |
152
156
  | 用脚本改文档后「什么都没变」 | 生成脚本文本里带了反引号,把模板字符串截断了:Node 抛 `ReferenceError: xxx is not defined` 但**编辑一句都没执行**。改完看 `git status`,别只看脚本退出码 |
package/README.md CHANGED
@@ -9,13 +9,29 @@
9
9
 
10
10
  ## 它长什么样
11
11
 
12
- | 看板:八列、角色负责人、卡片带进度/评论/用例 | 用例库:跨卡聚合、套件、最近回归 |
12
+ ## 它长什么样
13
+
14
+ | 工作台:今天该做什么、谁在忙 | 看板:八列、角色负责人、卡片带进度/用例 |
15
+ | --- | --- |
16
+ | ![workbench](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/workbench.png) | ![board](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/board.png) |
17
+
18
+ | 卡片抽屉:状态 / 清单 / 用例 / 执行记录 | 需求:基线、变更影响面、可追溯矩阵 |
13
19
  | --- | --- |
14
- | ![看板](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/board.png) | ![用例库](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/cases.png) |
20
+ | ![drawer](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/drawer.png) | ![requirements](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/requirements.png) |
15
21
 
16
- | 效能:一次通过率、返工率、按专家看质量 |
17
- | --- |
18
- | ![效能](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/quality.png) |
22
+ | 用例库:跨卡聚合、套件、最近回归 | 效能:一次通过率、返工率、按专家 + token 成本 |
23
+ | --- | --- |
24
+ | ![cases](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/cases.png) | ![quality](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/quality.png) |
25
+
26
+ | 版本:发布、灰度、部署记录 | 设置·测试执行:执行开关、数量上限、出站 webhook |
27
+ | --- | --- |
28
+ | ![releases](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/releases.png) | ![settings-execution](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/settings-execution.png) |
29
+
30
+ | 设置·成本与预算:价目表(元/百万 token) | 设置·专家:8 位专家、模型与提示词 |
31
+ | --- | --- |
32
+ | ![settings-cost](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/settings-cost.png) | ![settings-experts](https://raw.githubusercontent.com/marsken88/dsh-studio/main/docs/images/settings-experts.png) |
33
+
34
+ 截图不是手截的:`npm run shots`(`tools/visual/capture.mjs`)会登录面板、逐个页签截一遍,顺手断言没有「渲染失败」。
19
35
 
20
36
 
21
37
  ## 部署
@@ -417,6 +433,8 @@ sudo systemctl restart dsh-web
417
433
  ] } }
418
434
  ```
419
435
 
436
+ 对账命令:`npm run verify:usage [taskId]`——ssh 取原始日志现算一遍,和 `/costs` 的数字比。
437
+
420
438
  **实测**(线上 3045 次调用 / 2.42 亿 token):配价目表后算出 ¥81.47,其中 719 次调用因模型没配价目记「未知」;清空价目表后花费显示 `null`(未知),不会按 0 算。
421
439
  - 口径见上一节七条;订阅通道(`*-coding` / `*-token-plan`)记 token、不计钱。
422
440
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  | 项 | 值 |
8
8
  | --- | --- |
9
- | dsh-studio | `package.json` `version: 0.1.16` |
9
+ | dsh-studio | `package.json` `version: 0.1.18` |
10
10
  | DeepSeek Harness | `0.2.2-rc.1`(`dsh --version`) |
11
11
  | Node | `v24.22.2`(Linux 目标机);本机 Windows `v24.19.0` |
12
12
  | 目标系统 | Ubuntu 24.04,`dsh web` 监听 `18080`,nginx `:80` 反代并注入 `dsh-auth-*` cookie |
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/docs/publish.md CHANGED
@@ -127,6 +127,7 @@ token 只写进临时 npmrc(放在仓库外或 `tmp/` 且不提交),发完
127
127
  | --- | --- | --- |
128
128
  | `0.1.0` | 2026-11-13 | 首个公开版:看板 / 需求 / 用例回归 / 效能 / 设计评审四期 |
129
129
  | `0.1.1` | 2026-11-13 | 补三张 README 截图(npm 页可看图);默认值与文档去掉公司内网信息 |
130
+ | `0.1.17` | 2026-11-15 | 截图补到 10 张,且有脚本可重跑 |
130
131
  | `0.1.16` | 2026-11-15 | quickstart 命令修正 + GitLab 打桩验证配方 |
131
132
  | `0.1.15` | 2026-11-15 | 清掉三处过期的「已知限制」 |
132
133
  | `0.1.14` | 2026-11-15 | 修正成本口径的过期文案 |
@@ -147,4 +148,10 @@ token 只写进临时 npmrc(放在仓库外或 `tmp/` 且不提交),发完
147
148
 
148
149
  1. `npm run check` + `pnpm build` + `cmp -s dist/client.js client.js`(产物必须逐字节一致);
149
150
  2. 发完**在临时 profile 里装一遍**并跑 `dsh --profile <tmp> --dump-config | grep dsh-studio`,确认宿主真认到了,然后把 profile 删掉;
150
- 3. 部署脚本退出码 0 **不等于服务活着**——它打印的 `state=` 要自己看一眼,再单独 `curl -o /dev/null -w '%{http_code}' <host>/studio-api/state` 确认 200(2026-11-13 有过一次 `state=404` 被当成成功,线上挂了十分钟)。
151
+ 3. **发版一致性用命令查,不靠记性**:`npm run verify:release`(= `node tools/verify-release.mjs`)比对五处——
152
+ package.json 版本、本地 tag、tag 里的 package.json、origin/main 与 origin 上的 tag、npm registry 的版本与 latest。
153
+ 全绿输出「发版一致」,有不一致退出码 1。
154
+ **刚 publish 完那几分钟 registry 还看不到新版本**,这时用 `npm run verify:release -- --wait 180` 让它自己轮询等待,别把「还没同步」当成「没发」。
155
+ 这条是因为 0.1.15/0.1.16 真的发到 npm 了、git 却没推上去:`git tag -f vX.Y.Z && git push ... | tail -1` 会把 push 的失败吞掉。
156
+ npm latest 滞后(刚发几分钟)只算 warn,不算失败。
157
+ 4. 部署脚本退出码 0 **不等于服务活着**——它打印的 `state=` 要自己看一眼,再单独 `curl -o /dev/null -w '%{http_code}' <host>/studio-api/state` 确认 200(2026-11-13 有过一次 `state=404` 被当成成功,线上挂了十分钟)。
package/index.js CHANGED
@@ -1555,7 +1555,10 @@ function serviceConflicts(state, task) {
1555
1555
 
1556
1556
  /** 一卡一分支名:优先用卡号(DEV-001),没编号就用 id。 */
1557
1557
  function worktreeBranch(task) {
1558
- const label = String(task?.code ?? '').trim() || String(task?.id ?? '').trim()
1558
+ // 卡号只在一条需求内唯一(两条需求都会拆出 REQ-001):带上 id 后半段,否则 git 不让同一分支挂两个 worktree
1559
+ const code = String(task?.code ?? '').trim(), id = String(task?.id ?? '').trim()
1560
+ const tail = id.replace(/[^a-z0-9]/gi, '').slice(-8)
1561
+ const label = code === '' ? id : `${code}-${tail}`
1559
1562
  return `task/${label}`.slice(0, 120)
1560
1563
  }
1561
1564
 
@@ -1582,9 +1585,17 @@ async function ensureWorktree(store, task, config) {
1582
1585
  if (alive.ok && String(alive.stdout).includes(`worktree ${existing.path}`)) return existing
1583
1586
  }
1584
1587
  const branch = worktreeBranch(task)
1585
- const path = join(worktreeRoot(config), String(task?.code ?? '').trim() || String(task?.id ?? 'card'))
1588
+ const path = join(worktreeRoot(config), branch.replace(/^[a-z]+\//, ''))
1586
1589
  await mkdir(dirname(path), { recursive: true })
1587
- const base = String(service?.baseBranch ?? project.baseBranch ?? '').trim() || 'main'
1590
+ // 基准分支没有显式配就用仓库当前检出的分支:默认写死 main 会在用 master 的仓库上直接失败
1591
+ // 配置里默认写的是 main(sanitizeProject 的默认值),但很多仓库还在用 master:
1592
+ // 先确认这个分支真的存在,不存在就退回仓库当前检出的分支。
1593
+ let base = String(service?.baseBranch ?? project.baseBranch ?? '').trim()
1594
+ const branchOk = async (name) => name !== '' && (await runCommand('git', ['-C', repo, 'rev-parse', '--verify', 'refs/heads/' + name], 20000)).ok
1595
+ if (!(await branchOk(base))) {
1596
+ const current = await runCommand('git', ['-C', repo, 'rev-parse', '--abbrev-ref', 'HEAD'], 20000)
1597
+ base = (current.ok ? String(current.stdout).trim() : '') || base || 'main'
1598
+ }
1588
1599
  const branches = await runCommand('git', ['-C', repo, 'branch', '--list', branch], 20000)
1589
1600
  const exists = branches.ok && String(branches.stdout).trim().length > 0
1590
1601
  const added = exists
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marsken/dsh-studio",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "description": "DSH Studio: role-based multi-agent workspace (product / architect / developer / QA) with a live work board, GitLab + Feishu connection settings, all configurable from the web GUI.",
6
6
  "main": "index.js",
@@ -13,7 +13,10 @@
13
13
  "check": "node --check index.js && node --check client.src.js && node --test tests/repo.test.mjs tests/store-boot.test.mjs tests/client-units.test.mjs tests/notify.test.mjs",
14
14
  "smoke:store": "node scripts/smoke-store.mjs",
15
15
  "mock:gitlab": "node tools/mock-gitlab.mjs",
16
- "verify:gitlab": "node tools/verify-gitlab-facts.mjs"
16
+ "verify:gitlab": "node tools/verify-gitlab-facts.mjs",
17
+ "verify:release": "node tools/verify-release.mjs",
18
+ "verify:usage": "node tools/verify-usage.mjs",
19
+ "shots": "node tools/visual/capture.mjs"
17
20
  },
18
21
  "exports": {
19
22
  ".": "./index.js",
@@ -342,7 +342,6 @@ export async function runTaskCases(store, taskId, options = {}) {
342
342
  if (task === undefined) throw new Error('任务不存在')
343
343
  const config = state.config
344
344
  const envs = config.testing.envs
345
- if (envs.length === 0) throw new Error('还没有配置测试环境(设置 → 测试执行)')
346
345
  const envId = String(options.env ?? config.testing.defaultEnv ?? '')
347
346
  const env = envs.find(item => item.id === envId) ?? envs[0]
348
347
  const wanted = Array.isArray(options.caseIds) && options.caseIds.length > 0
@@ -350,11 +349,15 @@ export async function runTaskCases(store, taskId, options = {}) {
350
349
  : null
351
350
  const targets = (Array.isArray(task.cases) ? task.cases : [])
352
351
  .filter(item => (wanted === null ? item.http != null || item.shell != null : wanted.has(item.id)))
353
- if (targets.length === 0) throw new Error('没有可执行的用例:用例上要填 method + url(相对地址会拼环境 baseUrl)')
352
+ if (targets.length === 0) throw new Error('没有可执行的用例:接口用例要填 method + url(相对地址拼环境 baseUrl),shell / 数据库用例要填 command / sql')
354
353
 
355
- const probe = await probeEnv(env, config)
356
- if (probe.ok !== true) {
357
- throw new Error(`测试环境「${env.name || env.id}」不可达(${probe.error}):一条用例都没跑,先把 ${probe.baseUrl || 'baseUrl'} 起来`)
354
+ // 只有接口用例才需要测试环境;纯 shell / 数据库用例不该被「环境不可达」挡住。
355
+ if (targets.some(item => item.http != null)) {
356
+ if (env === undefined) throw new Error('还没有配置测试环境(设置 → 测试执行)')
357
+ const probe = await probeEnv(env, config)
358
+ if (probe.ok !== true) {
359
+ throw new Error(`测试环境「${env.name || env.id}」不可达(${probe.error}):一条用例都没跑,先把 ${probe.baseUrl || 'baseUrl'} 起来`)
360
+ }
358
361
  }
359
362
 
360
363
  const by = String(options.by ?? '系统').slice(0, 40)