@marsken/dsh-studio 0.1.2 → 0.1.3
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 +1 -1
- package/README.md +17 -0
- package/client.js +17 -17
- package/dist/client.js +17 -17
- package/docs/compatibility.md +1 -1
- package/docs/publish.md +1 -0
- package/package.json +1 -1
- package/src/host/domain.js +9 -1
- package/src/host/executor.js +4 -2
- package/src/host/shell-case.js +16 -0
- package/src/host/shell-runner.js +42 -0
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.3` |
|
|
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,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.3` | 2026-11-13 | shell 用例(命令 + 退出码/输出断言,默认关闭) |
|
|
130
131
|
| `0.1.2` | 2026-11-13 | 修 5 个缺陷:卡片挂不上 MR、换列清空负责人、三处静默截断、还原度无历史、规范体检不定位元素 |
|
|
131
132
|
|
|
132
133
|
两条硬规矩,每次发版都过一遍:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsken/dsh-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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/domain.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module dsh-studio/src/host/domain
|
|
9
9
|
*/
|
|
10
10
|
import { createHash, randomBytes } from 'node:crypto'
|
|
11
|
+
import { sanitizeCaseShell } from './shell-case.js'
|
|
11
12
|
import { homedir } from 'node:os'
|
|
12
13
|
import { dirname, join } from 'node:path'
|
|
13
14
|
|
|
@@ -1257,6 +1258,7 @@ export function sanitizeCaseHttp(raw) {
|
|
|
1257
1258
|
}
|
|
1258
1259
|
|
|
1259
1260
|
/** 一条测试用例:做什么、期望什么、实际结果,以及最近一次执行的样子。 */
|
|
1261
|
+
|
|
1260
1262
|
export function sanitizeCase(raw, index) {
|
|
1261
1263
|
if (typeof raw === 'string') {
|
|
1262
1264
|
const title = raw.trim().slice(0, 200)
|
|
@@ -1275,6 +1277,7 @@ export function sanitizeCase(raw, index) {
|
|
|
1275
1277
|
by: '',
|
|
1276
1278
|
at: 0,
|
|
1277
1279
|
http: null,
|
|
1280
|
+
shell: null,
|
|
1278
1281
|
lastRun: null,
|
|
1279
1282
|
}
|
|
1280
1283
|
}
|
|
@@ -1287,6 +1290,7 @@ export function sanitizeCase(raw, index) {
|
|
|
1287
1290
|
return {
|
|
1288
1291
|
id: String(raw.id ?? '').trim() || `c${index + 1}-${randomBytes(2).toString('hex')}`,
|
|
1289
1292
|
title,
|
|
1293
|
+
shell: sanitizeCaseShell(raw.shell),
|
|
1290
1294
|
expected: String(raw.expected ?? '').trim().slice(0, 300),
|
|
1291
1295
|
pointId: String(raw.pointId ?? '').trim().slice(0, 24),
|
|
1292
1296
|
result,
|
|
@@ -1445,7 +1449,9 @@ export function sanitizeAllowHosts(input) {
|
|
|
1445
1449
|
|
|
1446
1450
|
/** 测试执行配置:环境、默认环境、超时、允许的域名。 */
|
|
1447
1451
|
export function sanitizeTesting(raw) {
|
|
1448
|
-
|
|
1452
|
+
// allowShell 默认关:开了等于把「在宿主机执行任意命令」的能力交给看板。
|
|
1453
|
+
|
|
1454
|
+
const base = { envs: [], defaultEnv: '', timeoutMs: 15000, allowHosts: ['127.0.0.1', 'localhost'], concurrency: 4, allowShell: false, shellCwd: '' }
|
|
1449
1455
|
if (raw === null || typeof raw !== 'object') return base
|
|
1450
1456
|
const envs = sanitizeEnvs(raw.envs)
|
|
1451
1457
|
const defaultEnv = String(raw.defaultEnv ?? '').trim()
|
|
@@ -1454,6 +1460,8 @@ export function sanitizeTesting(raw) {
|
|
|
1454
1460
|
defaultEnv: envs.some(env => env.id === defaultEnv) ? defaultEnv : (envs[0]?.id ?? ''),
|
|
1455
1461
|
timeoutMs: Math.max(1000, Math.min(120000, Number(raw.timeoutMs) || base.timeoutMs)),
|
|
1456
1462
|
allowHosts: sanitizeAllowHosts(raw.allowHosts ?? base.allowHosts),
|
|
1463
|
+
allowShell: raw.allowShell === true,
|
|
1464
|
+
shellCwd: String(raw.shellCwd ?? '').slice(0, 300),
|
|
1457
1465
|
concurrency: Math.max(1, Math.min(8, Number(raw.concurrency) || base.concurrency)),
|
|
1458
1466
|
}
|
|
1459
1467
|
}
|
package/src/host/executor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { casesOfTasks, impactSummary } from './impact.js'
|
|
|
8
8
|
import { designGate } from './design.js'
|
|
9
9
|
import { RUN_LOG, parseMentions } from './dispatcher.js'
|
|
10
10
|
import { appendTeamMemory } from './team-memory.js'
|
|
11
|
+
import { runShellCase } from './shell-runner.js'
|
|
11
12
|
import { homedir } from 'node:os'
|
|
12
13
|
import { join, resolve } from 'node:path'
|
|
13
14
|
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
@@ -183,6 +184,7 @@ export async function probeEnv(env, config, timeoutMs = 5000) {
|
|
|
183
184
|
|
|
184
185
|
export async function runOneCase(item, env, config, seedVars) {
|
|
185
186
|
const started = Date.now()
|
|
187
|
+
if (item.shell !== null && item.shell !== undefined) return runShellCase(item, config)
|
|
186
188
|
if (item.http == null) {
|
|
187
189
|
return { ok: false, status: 0, ms: 0, error: '这条用例还没有可执行的请求定义(method/url)', preview: '' }
|
|
188
190
|
}
|
|
@@ -297,7 +299,7 @@ export async function runTaskCases(store, taskId, options = {}) {
|
|
|
297
299
|
? new Set(options.caseIds.map(id => String(id)))
|
|
298
300
|
: null
|
|
299
301
|
const targets = (Array.isArray(task.cases) ? task.cases : [])
|
|
300
|
-
.filter(item => (wanted === null ? item.http != null : wanted.has(item.id)))
|
|
302
|
+
.filter(item => (wanted === null ? item.http != null || item.shell != null : wanted.has(item.id)))
|
|
301
303
|
if (targets.length === 0) throw new Error('没有可执行的用例:用例上要填 method + url(相对地址会拼环境 baseUrl)')
|
|
302
304
|
|
|
303
305
|
const probe = await probeEnv(env, config)
|
|
@@ -940,7 +942,7 @@ export async function pullFeishuDoc(url) {
|
|
|
940
942
|
}
|
|
941
943
|
text = String(parsed.data?.document?.content ?? '')
|
|
942
944
|
if (text.trim() === '') return { ok: false, tool: true, text: '', error: '拉到了空正文:这个文档可能是空的,或者应用没有阅读权限' }
|
|
943
|
-
const title = /<title>([\s\S]*?)<\/title
|
|
945
|
+
const title = text.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? ''
|
|
944
946
|
const body = text.replace(/<title>[\s\S]*?<\/title>\s*/, '')
|
|
945
947
|
return { ok: true, tool: true, title, text: body.slice(0, 60000), error: '' }
|
|
946
948
|
} catch {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// shell 用例的数据形状。纯函数,放在这里是为了不把 domain.js 撑过 2000 行的闸门。
|
|
2
|
+
|
|
3
|
+
/** shell 用例:跑一条命令,看退出码与输出。默认**关**,要在设置里显式打开(见 config.testing.allowShell)。 */
|
|
4
|
+
export function sanitizeCaseShell(raw) {
|
|
5
|
+
if (raw === null || typeof raw !== 'object') return null
|
|
6
|
+
const command = String(raw.command ?? '').trim().slice(0, 2000)
|
|
7
|
+
if (command === '') return null
|
|
8
|
+
return {
|
|
9
|
+
command,
|
|
10
|
+
cwd: String(raw.cwd ?? '').trim().slice(0, 300),
|
|
11
|
+
timeoutMs: Math.max(1000, Math.min(120000, Number(raw.timeoutMs) || 30000)),
|
|
12
|
+
expectExit: Number.isInteger(Number(raw.expectExit)) ? Number(raw.expectExit) : 0,
|
|
13
|
+
expectContains: String(raw.expectContains ?? '').slice(0, 500),
|
|
14
|
+
expectNotContains: String(raw.expectNotContains ?? '').slice(0, 500),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// shell 用例执行器:跑一条命令,比对退出码与输出。
|
|
2
|
+
// 默认关闭(config.testing.allowShell)——打开它等于把「在宿主机执行任意命令」的能力交给看板。
|
|
3
|
+
import { exec } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
/** shell 用例:跑一条命令,比对退出码与输出。默认关闭,见 config.testing.allowShell。 */
|
|
6
|
+
export async function runShellCase(item, config) {
|
|
7
|
+
const started = Date.now()
|
|
8
|
+
const shell = item.shell
|
|
9
|
+
if (config?.testing?.allowShell !== true) {
|
|
10
|
+
return { ok: false, status: 0, ms: 0, error: '这台机器没开 shell 用例:设置 → 测试执行 里打开「允许 shell 用例」', preview: '' }
|
|
11
|
+
}
|
|
12
|
+
const cwd = shell.cwd !== '' ? shell.cwd : String(config?.testing?.shellCwd ?? '')
|
|
13
|
+
const timeout = shell.timeoutMs > 0 ? shell.timeoutMs : 30000
|
|
14
|
+
let stdout = ''
|
|
15
|
+
let stderr = ''
|
|
16
|
+
let code = 0
|
|
17
|
+
try {
|
|
18
|
+
const out = await new Promise(finish => {
|
|
19
|
+
exec(shell.command, { cwd: cwd === '' ? undefined : cwd, timeout, maxBuffer: 1024 * 1024 }, (error, outText, errText) => {
|
|
20
|
+
finish({ error, out: String(outText ?? ''), err: String(errText ?? '') })
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
stdout = out.out
|
|
24
|
+
stderr = out.err
|
|
25
|
+
code = out.error === null || out.error === undefined ? 0 : (Number(out.error.code) || 1)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
stderr = String(error?.message ?? error)
|
|
28
|
+
code = 1
|
|
29
|
+
}
|
|
30
|
+
const combined = stdout + stderr
|
|
31
|
+
const problems = []
|
|
32
|
+
if (code !== shell.expectExit) problems.push('退出码 ' + code + '(期望 ' + shell.expectExit + ')')
|
|
33
|
+
if (shell.expectContains !== '' && !combined.includes(shell.expectContains)) problems.push('输出里没有「' + shell.expectContains + '」')
|
|
34
|
+
if (shell.expectNotContains !== '' && combined.includes(shell.expectNotContains)) problems.push('输出里有不该出现的「' + shell.expectNotContains + '」')
|
|
35
|
+
return {
|
|
36
|
+
ok: problems.length === 0,
|
|
37
|
+
status: code,
|
|
38
|
+
ms: Date.now() - started,
|
|
39
|
+
error: problems.join(';').slice(0, 300),
|
|
40
|
+
preview: combined.trim().slice(-600),
|
|
41
|
+
}
|
|
42
|
+
}
|