@marsken/dsh-studio 0.1.6 → 0.1.8
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/docs/compatibility.md +1 -1
- package/docs/publish.md +2 -0
- package/package.json +1 -1
- package/src/host/dispatcher.js +4 -1
- package/src/host/executor.js +34 -0
- package/src/host/integrations.js +1 -0
- package/src/host/quality.js +3 -1
- package/src/host/webhooks.js +7 -3
package/docs/compatibility.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| 项 | 值 |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| dsh-studio | `package.json` `version: 0.1.
|
|
9
|
+
| dsh-studio | `package.json` `version: 0.1.8` |
|
|
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 |
|
package/docs/publish.md
CHANGED
|
@@ -127,6 +127,8 @@ 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.8` | 2026-11-14 | 卡进「待评审」时自动核对代码事实,对不上就写进 history 并记 gate/blocked |
|
|
131
|
+
| `0.1.7` | 2026-11-14 | webhook 投递结果进「集成健康」(地址写错了看得见) |
|
|
130
132
|
| `0.1.6` | 2026-11-14 | 回归跑完也发事件(regression/finished);数据库用例可指定断言的列 |
|
|
131
133
|
| `0.1.5` | 2026-11-14 | agent/failed 事件、控制面板(执行开关 / 上限 / webhook 按事件订阅)、抽屉里加数据库用例入口 |
|
|
132
134
|
| `0.1.4` | 2026-11-14 | SQLite 用例、出站 webhook、可配置上限、执行器注册表 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsken/dsh-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|
package/src/host/dispatcher.js
CHANGED
|
@@ -274,7 +274,7 @@ export function runSummary() {
|
|
|
274
274
|
current.ms += run.ms
|
|
275
275
|
map.set(key, current)
|
|
276
276
|
}
|
|
277
|
-
bump(byExpert, run.
|
|
277
|
+
bump(byExpert, run.expertId || run.expert || run.expertName || '未记录')
|
|
278
278
|
bump(byModel, run.model || '跟随默认')
|
|
279
279
|
bump(byPrompt, run.promptVersion || '未记录')
|
|
280
280
|
const day = new Date(run.at).toISOString().slice(0, 10)
|
|
@@ -660,6 +660,9 @@ export class Dispatcher {
|
|
|
660
660
|
taskCode: task.code || task.id,
|
|
661
661
|
sessionId,
|
|
662
662
|
expert: expert.id,
|
|
663
|
+
// 卡上真正指派的专家 id:列负责人是「角色位」时,expert.id 是角色(dev),
|
|
664
|
+
// 只按它统计会把同一个人的记录拆成两行(dev 有派单、expert-xxx 有卡)。
|
|
665
|
+
expertId: String(task.expert ?? ''),
|
|
663
666
|
expertName: expert.name,
|
|
664
667
|
model: String(this.modelFor(expert)?.model ?? ''),
|
|
665
668
|
promptVersion: String(expert.promptVersion ?? ''),
|
package/src/host/executor.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { CODE_PREFIX, ROLE_IDS, STATUS_IDS, STATUSES, nextRequirementCode, nextTaskCode, normalizePatch, sanitizeBaseline, sanitizeReqDoc, sanitizeRequirement, sanitizeTestRun, text,
|
|
5
5
|
canonicalAuthor,
|
|
6
|
+
sanitizeCoding,
|
|
6
7
|
} from './domain.js'
|
|
7
8
|
import { casesOfTasks, impactSummary } from './impact.js'
|
|
8
9
|
import { designGate } from './design.js'
|
|
@@ -30,6 +31,35 @@ export function configureExecutor(services) {
|
|
|
30
31
|
* 核对执行记录和代码事实:agent 自报的提交/文件,分支上到底有没有。
|
|
31
32
|
* 没有 worktree(隔离没开)或还没写执行记录时返回 skipped,不拦流程。
|
|
32
33
|
*/
|
|
34
|
+
/** 自动核对:进「待评审」时跑一次,把结果写回 coding.verified 并在 history 留痕。 */
|
|
35
|
+
export async function autoVerifyCoding(store, taskId) {
|
|
36
|
+
const state = store.snapshot()
|
|
37
|
+
const task = state.tasks.find(entry => entry.id === taskId)
|
|
38
|
+
if (task === undefined || task.coding === null || task.coding === undefined) return null
|
|
39
|
+
const report = await verifyCodingRecord(state, task)
|
|
40
|
+
await store.mutate(draft => {
|
|
41
|
+
const target = draft.tasks.find(entry => entry.id === taskId)
|
|
42
|
+
if (target === undefined || target.coding === null || target.coding === undefined) return
|
|
43
|
+
const skipped = report.skipped !== undefined
|
|
44
|
+
target.coding = sanitizeCoding({ ...target.coding, verified: skipped ? null : report })
|
|
45
|
+
const missingCommits = (report.missingCommits ?? []).length
|
|
46
|
+
const missingFiles = (report.missingFiles ?? []).length
|
|
47
|
+
target.history = [...(target.history ?? []), {
|
|
48
|
+
at: Date.now(),
|
|
49
|
+
by: '系统',
|
|
50
|
+
status: target.status,
|
|
51
|
+
note: skipped
|
|
52
|
+
? '代码事实没法核对:' + report.skipped
|
|
53
|
+
: report.ok === true
|
|
54
|
+
? '代码事实核对通过'
|
|
55
|
+
: '代码事实核对不通过:' + missingCommits + ' 个提交 / ' + missingFiles + ' 个文件在仓库里找不到',
|
|
56
|
+
}]
|
|
57
|
+
})
|
|
58
|
+
if (report.ok !== true && report.skipped === undefined) {
|
|
59
|
+
emitEvent('gate/blocked', { taskId, gate: 'coding', blockers: ['代码事实核对不通过'] })
|
|
60
|
+
}
|
|
61
|
+
return report
|
|
62
|
+
}
|
|
33
63
|
export async function verifyCodingRecord(state, task) {
|
|
34
64
|
const coding = task?.coding ?? null
|
|
35
65
|
if (coding === null || coding === undefined) return { ok: false, skipped: '这张卡还没有执行记录' }
|
|
@@ -1396,6 +1426,10 @@ export async function updateTask(store, id, patch) {
|
|
|
1396
1426
|
host.INTEGRATION.lastPushError = String(error?.message ?? error).slice(0, 300)
|
|
1397
1427
|
}
|
|
1398
1428
|
}
|
|
1429
|
+
// 卡进「待评审」时自动核对一次代码事实:AI 说改了什么,仓库里得真有。
|
|
1430
|
+
if (moved !== null && moved.to === 'review') {
|
|
1431
|
+
await autoVerifyCoding(store, id).catch(() => {})
|
|
1432
|
+
}
|
|
1399
1433
|
return result
|
|
1400
1434
|
}
|
|
1401
1435
|
|
package/src/host/integrations.js
CHANGED
|
@@ -24,6 +24,7 @@ const CHANNELS = {
|
|
|
24
24
|
gitlab: { label: 'GitLab', hint: '拉 Issue、回写评论、对 MR' },
|
|
25
25
|
drive: { label: '飞书云盘归档', hint: '需求文件夹与产物文档(lark-cli)' },
|
|
26
26
|
worktree: { label: '代码隔离', hint: 'git worktree 的创建与回收' },
|
|
27
|
+
webhook: { label: '出站 webhook', hint: '卡片流转/用例失败/门禁/回归结果推给外部系统' },
|
|
27
28
|
}
|
|
28
29
|
const HEALTH = new Map()
|
|
29
30
|
export function healthState(channel) {
|
package/src/host/quality.js
CHANGED
|
@@ -58,10 +58,12 @@ export function buildQuality(state, runs) {
|
|
|
58
58
|
}
|
|
59
59
|
const runRows = (runs?.byExpert ?? []).map(item => ({ name: String(item.key ?? ''), runs: Number(item.runs) || 0, ms: Number(item.ms) || 0 }))
|
|
60
60
|
const runByName = new Map(runRows.map(item => [item.name.toLowerCase(), item]))
|
|
61
|
+
const runById = new Map(runRows.map(item => [item.name, item]))
|
|
61
62
|
const experts = [...perExpert.values()].map(row => {
|
|
62
63
|
const info = (state?.config?.experts ?? []).find(item => item.id === row.key)
|
|
63
64
|
const label = info === undefined ? row.key : `${info.emoji ?? ''} ${info.name}`.trim()
|
|
64
|
-
|
|
65
|
+
// 先按 id 认领(派单日志里带了 expertId),再退回名字。
|
|
66
|
+
const run = runById.get(row.key) ?? runByName.get(String(info?.name ?? row.key).toLowerCase())
|
|
65
67
|
return {
|
|
66
68
|
expert: row.key,
|
|
67
69
|
label,
|
package/src/host/webhooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// webhook 订阅者:把事件 POST 给配置里的地址。失败只记健康,不重试、不阻塞。
|
|
2
2
|
import { emitEvent, subscribe } from './events.js'
|
|
3
|
+
import { markHealth } from './integrations.js'
|
|
3
4
|
|
|
4
5
|
export const WEBHOOK_EVENTS = ['task/moved', 'case/failed', 'gate/blocked', 'agent/failed', 'regression/finished']
|
|
5
6
|
|
|
@@ -23,14 +24,17 @@ export async function deliverWebhooks(config, event) {
|
|
|
23
24
|
const hooks = (config?.webhooks ?? []).filter(hook => hook.enabled !== false && (hook.events ?? []).includes(event.type))
|
|
24
25
|
for (const hook of hooks) {
|
|
25
26
|
try {
|
|
26
|
-
await fetch(hook.url, {
|
|
27
|
+
const response = await fetch(hook.url, {
|
|
27
28
|
method: 'POST',
|
|
28
29
|
headers: { 'content-type': 'application/json' },
|
|
29
30
|
body: JSON.stringify(event),
|
|
30
31
|
signal: AbortSignal.timeout(5000),
|
|
31
32
|
})
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// 投递结果进「集成健康」,否则地址写错了没人知道——事件是 fire-and-forget,不会报错给你。
|
|
34
|
+
if (response.ok) markHealth('webhook', 'ok', hook.id + ' ' + event.type)
|
|
35
|
+
else markHealth('webhook', 'fail', hook.id + ' HTTP ' + response.status)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
markHealth('webhook', 'fail', hook.id + ' ' + String(error?.message ?? error).slice(0, 120))
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
}
|