@marsken/dsh-studio 0.1.21 → 0.1.23
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 +1 -1
- package/client.js +28 -28
- package/dist/client.js +28 -28
- package/docs/compatibility.md +1 -1
- package/docs/publish.md +2 -0
- package/index.js +10 -5
- package/package.json +1 -1
- package/src/host/bitable.js +74 -0
- package/src/host/bugs.js +20 -0
- package/src/host/routes/connect.js +9 -1
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.23` |
|
|
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.23` | 2026-11-15 | 卡片镜像到飞书多维表格(手动触发,失败不谎报) |
|
|
131
|
+
| `0.1.22` | 2026-11-15 | 缺陷严重级按用例优先级(回归升级)+ 角色模板编辑界面 |
|
|
130
132
|
| `0.1.21` | 2026-11-15 | 需求基线可回滚(版本 +1、签字作废、留痕) |
|
|
131
133
|
| `0.1.20` | 2026-11-15 | 拆卡时提示「有后端但功能点没覆盖后端」 |
|
|
132
134
|
| `0.1.19` | 2026-11-15 | 需求澄清三问 + 拆卡门禁;修补答保存不上的白名单漏字段 |
|
package/index.js
CHANGED
|
@@ -29,6 +29,8 @@ export const inject = ['tools', 'systemPrompt']
|
|
|
29
29
|
|
|
30
30
|
import { ACCESS_ROLE_LABEL, ACCESS_ROLES, ADMIN_ROUTES, ARTIFACT_KINDS, ARTIFACT_STATUS, CASE_HTTP_METHODS, CASE_PRIORITIES, CASE_RESULTS, CASE_TYPE_LABEL, CASE_TYPES, cleanId, CODE_PREFIX, CODING_FILE_CHANGES, CODING_REVIEW_STATUS, codingSummary, COLUMN_STRATEGIES, DEFAULT_ROLES, defaultColumns, defaultConfig, defaultDeployEnvs, defaultTemplates, DEPLOY_ENV_KINDS, FALLBACK_AGENT_MODEL, hashLocalPassword, INTAKE_HTML, ISOLATION_MODES, LEGACY_BLOCKED_STATUS, LEVEL_RANK, LOCAL_DEFAULT_PASSWORD, mergeCoding, mergeConfig, mergeSection, MINE_HTML, nextRequirementCode, nextTaskCode, normalizePatch, normalizeStrategy, publicArtifact, publicCases, publicChecklist, publicComments, PRIORITIES, ROLE_LABELS, canonicalAuthor, RELEASE_STATUSES, RELEASE_STATUS_LABEL, REQ_DOC_KIND_LABEL, REQ_DOC_KINDS, REQ_DOC_STATUS, REQ_REVIEW_LABEL, REQ_REVIEW_STATUS, REQ_STATUS_LABEL, REQ_STATUSES, ROLE_IDS, ROLE_LEVEL, SANDBOX_MODES, sanitizeAcceptanceItem, sanitizeAccess, sanitizeAllowHosts, sanitizeArtifact, sanitizeArtifacts, sanitizeBaseline, sanitizeBots, sanitizeBugSource, sanitizeCase, sanitizeCaseHttp, sanitizeCaseRun, sanitizeCases, sanitizeChangedFile, sanitizeChangedFiles, sanitizeChecklist, sanitizeChecklistItem, sanitizeCoding, sanitizeColumns, sanitizeCommit, sanitizeCommits, sanitizeCosts, sanitizeDeployEnvs, sanitizeEnvs, sanitizeExperts, sanitizeIterations, sanitizeLdap, sanitizeLocalAuth, sanitizeProjects, sanitizeReqDoc, sanitizeReqDocs, sanitizeRelease, sanitizeRequirement, sanitizeRequirementChange, sanitizeRequirementHistory, sanitizeRequirementPoint, sanitizeRequirementPoints, sanitizeRequirementPool, sanitizeRequirementQuestions, sanitizeRequirementReview, sanitizeRequirements, sanitizeServices, sanitizeSla, sanitizeTeams, sanitizeTemplates, sanitizeTesting, sanitizeTestRun, sanitizeTestRuns, sanitizeUsers, sanitizeWorkProjects, seedExperts, SEVERITIES, STATUS_HTML, STATUS_IDS, STATUS_SET, STATUSES, TASK_TYPES, text, verifyLocalPassword, worktreeRoot } from './src/host/domain.js'
|
|
31
31
|
import { Dispatcher, RUN_LOG, attachRunLog, claimDispatchLock, clearAgentFailure, configureDispatcher, noteAgentFailure, parseMentions, runSummary } from './src/host/dispatcher.js'
|
|
32
|
+
import { mirrorCards } from './src/host/bitable.js'
|
|
33
|
+
import { bugSeverityFor } from './src/host/bugs.js'
|
|
32
34
|
import { sanitizeLimits } from './src/host/limits.js'
|
|
33
35
|
import { attachWebhooks, sanitizeWebhooks } from './src/host/webhooks.js'
|
|
34
36
|
import { buildDashboard, codingVerifyLine, commentTask, configureExecutor, ensureReqFolder, larkCliReady, invalidateLarkProbe, larkProbeCache, mutateReqDoc, probeEnv, pullFeishuDoc, requirementGate, requirementMatrix, requirementProgress, requirementRollup, runCommand, runSuite, runTaskCases, saveRequirement, syncArtifact, syncRequirementArtifacts, taskExitGate, updateTask, verifyCodingRecord } from './src/host/executor.js'
|
|
@@ -1401,6 +1403,8 @@ function isEnvFailure(text) {
|
|
|
1401
1403
|
* 用例失败 → 自动开一张缺陷卡,挂同一个需求、同一个项目/迭代,父任务是这张测试卡。
|
|
1402
1404
|
* 同一张卡的同一用例只会开一次;重新标失败只是更新证据。
|
|
1403
1405
|
*/
|
|
1406
|
+
const mirrorCardsToBitable = (store, options) => mirrorCards(store, store.snapshot().config, options, { runCommand, markHealth })
|
|
1407
|
+
|
|
1404
1408
|
async function autoBugFromCase(store, task, item, by) {
|
|
1405
1409
|
const snapshot = store.snapshot()
|
|
1406
1410
|
const existing = snapshot.tasks.find(entry => entry.bugSource != null
|
|
@@ -1425,9 +1429,9 @@ async function autoBugFromCase(store, task, item, by) {
|
|
|
1425
1429
|
const bug = await createTask(store, {
|
|
1426
1430
|
title: `[用例失败] ${item.title}`,
|
|
1427
1431
|
type: 'bug',
|
|
1428
|
-
|
|
1432
|
+
// 严重级别跟着用例优先级走:p0 → p0,p3 → p3;已完成/待验收的卡上失败算回归,升一级
|
|
1433
|
+
...bugSeverityFor(item.priority, task.status),
|
|
1429
1434
|
status: 'dev_ready',
|
|
1430
|
-
priority: 'normal',
|
|
1431
1435
|
requirement: [
|
|
1432
1436
|
`来源:${task.code === '' ? task.id : task.code}「${task.title}」的测试用例`,
|
|
1433
1437
|
`用例:${item.title}`,
|
|
@@ -1555,8 +1559,7 @@ function serviceConflicts(state, task) {
|
|
|
1555
1559
|
|
|
1556
1560
|
/** 一卡一分支名:优先用卡号(DEV-001),没编号就用 id。 */
|
|
1557
1561
|
function worktreeBranch(task) {
|
|
1558
|
-
|
|
1559
|
-
const code = String(task?.code ?? '').trim(), id = String(task?.id ?? '').trim()
|
|
1562
|
+
const code = String(task?.code ?? '').trim(), id = String(task?.id ?? '').trim() // 卡号只在需求内唯一,分支要带 id 后缀
|
|
1560
1563
|
const tail = id.replace(/[^a-z0-9]/gi, '').slice(-8)
|
|
1561
1564
|
const label = code === '' ? id : `${code}-${tail}`
|
|
1562
1565
|
return `task/${label}`.slice(0, 120)
|
|
@@ -2625,6 +2628,7 @@ export function apply(ctx) {
|
|
|
2625
2628
|
|
|
2626
2629
|
configureExecutor({
|
|
2627
2630
|
autoBugFromCase,
|
|
2631
|
+
mirrorCardsToBitable,
|
|
2628
2632
|
probeEnv,
|
|
2629
2633
|
publicState,
|
|
2630
2634
|
serviceDependencies,
|
|
@@ -2655,7 +2659,7 @@ export function apply(ctx) {
|
|
|
2655
2659
|
gitlabRequest,
|
|
2656
2660
|
gitlabMergeRequestDetail, applyConfig, ensureWorktree, publicState, readJsonBody, removeWorktree, sendJson, serviceOf, upsertById, workProjectOf })
|
|
2657
2661
|
configureAgentsRoutes({ publicState, readJsonBody, sendJson, getDispatcher: () => activeDispatcher })
|
|
2658
|
-
configureConnectRoutes({ applyConfig, installedPackages, publicState, readJsonBody, resolvePackageName, runPluginCli, sendJson, upsertById, getSyncTimer: () => syncTimer })
|
|
2662
|
+
configureConnectRoutes({ mirrorCardsToBitable, applyConfig, installedPackages, publicState, readJsonBody, resolvePackageName, runPluginCli, sendJson, upsertById, getSyncTimer: () => syncTimer })
|
|
2659
2663
|
configureAdminRoutes({ applyConfig, checkToolAccess, operatorFromSessionId, operatorOf, publicState, readJsonBody, sendJson })
|
|
2660
2664
|
configureSkillsRoutes({ isSkillName, readJsonBody, sendJson, skillFromMarkdown, skillFromZip, skillsRoot, getSkills: () => activeSkills })
|
|
2661
2665
|
configureInsightsRoutes({ getDispatcher: () => activeDispatcher, sendJson, buildMetrics, buildAnalytics, workloadReport, burndownReport })
|
|
@@ -2673,6 +2677,7 @@ export function apply(ctx) {
|
|
|
2673
2677
|
applyConfig,
|
|
2674
2678
|
getDispatcher: () => activeDispatcher,
|
|
2675
2679
|
autoBugFromCase,
|
|
2680
|
+
mirrorCardsToBitable,
|
|
2676
2681
|
publicState,
|
|
2677
2682
|
upsertById,
|
|
2678
2683
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsken/dsh-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 把看板卡片镜像到飞书多维表格:一条 Record = 一张卡。
|
|
2
|
+
// 只负责“卡片 → 记录字段”和分批;真正跑 lark-cli 在 index.js 里(要 markHealth、要复用 runCommand)。
|
|
3
|
+
//
|
|
4
|
+
// 表里需要有这些列(名字要对得上):编号 / 标题 / 状态 / 负责人 / 优先级 / 需求 / 更新时间
|
|
5
|
+
|
|
6
|
+
export const BITABLE_FIELDS = ['编号', '标题', '状态', '负责人', '优先级', '需求', '更新时间']
|
|
7
|
+
export const BATCH_LIMIT = 200
|
|
8
|
+
|
|
9
|
+
export function cardToRecord(task) {
|
|
10
|
+
return {
|
|
11
|
+
'编号': String(task?.code ?? '').trim() || String(task?.id ?? ''),
|
|
12
|
+
'标题': String(task?.title ?? '').slice(0, 200),
|
|
13
|
+
'状态': String(task?.status ?? ''),
|
|
14
|
+
'负责人': String(task?.expert ?? ''),
|
|
15
|
+
'优先级': String(task?.priority ?? ''),
|
|
16
|
+
'需求': String(task?.requirementId ?? ''),
|
|
17
|
+
'更新时间': Number(task?.updatedAt) > 0 ? new Date(Number(task.updatedAt)).toISOString() : '',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 一批最多 200 条,超过就分批(同一张表要串行写,否则会撞并发错误)。 */
|
|
22
|
+
export function recordsPayload(cards) {
|
|
23
|
+
const list = (Array.isArray(cards) ? cards : []).map(cardToRecord)
|
|
24
|
+
const batches = []
|
|
25
|
+
for (let index = 0; index < list.length; index += BATCH_LIMIT) batches.push(list.slice(index, index + BATCH_LIMIT))
|
|
26
|
+
return batches
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 真正镜像:按 200 条一批串行写多维表格。
|
|
31
|
+
* runCommand / markHealth 由 index.js 注入(子模块不反向依赖宿主)。
|
|
32
|
+
*/
|
|
33
|
+
export async function mirrorCards(store, config, options, deps) {
|
|
34
|
+
// 用别名接收注入能力:同名的 runCommand / markHealth 属于宿主模块,子模块不该 import 它们
|
|
35
|
+
const { runCommand: run, markHealth: health } = deps ?? {}
|
|
36
|
+
const gaps = mirrorConfigGaps(config)
|
|
37
|
+
if (gaps.length > 0) return { ok: false, error: '还没配多维表格:' + gaps.join('、') + '(设置 → 飞书机器人)' }
|
|
38
|
+
const requirementId = String(options?.requirementId ?? '')
|
|
39
|
+
const cards = (store.snapshot().tasks ?? []).filter(task => requirementId === '' || task.requirementId === requirementId)
|
|
40
|
+
const batches = recordsPayload(cards)
|
|
41
|
+
if (options?.dryRun === true) return { ok: true, dryRun: true, cards: cards.length, batches: batches.map(batch => batch.length) }
|
|
42
|
+
const probe = await run('lark-cli', ['--version'], 8000)
|
|
43
|
+
if (!probe.ok) {
|
|
44
|
+
health?.('feishu', 'fail', '没有可用的 lark-cli')
|
|
45
|
+
return { ok: false, error: '服务器上没有可用的 lark-cli,镜像需要它在服务器上并完成用户授权' }
|
|
46
|
+
}
|
|
47
|
+
const created = []
|
|
48
|
+
for (const batch of batches) {
|
|
49
|
+
const result = await run('lark-cli', [
|
|
50
|
+
'base', '+record-batch-create',
|
|
51
|
+
'--base-token', String(config.feishu.baseToken).trim(),
|
|
52
|
+
'--table-id', String(config.feishu.tableId).trim(),
|
|
53
|
+
'--json', JSON.stringify({ create_records: batch }),
|
|
54
|
+
'--as', 'user',
|
|
55
|
+
], 60000)
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
health?.('feishu', 'fail', '多维表格写入失败:' + String(result.stderr || '').slice(0, 160))
|
|
58
|
+
return { ok: false, error: '多维表格写入失败:' + String(result.stderr || result.stdout || '').slice(0, 300), created }
|
|
59
|
+
}
|
|
60
|
+
let parsed = {}
|
|
61
|
+
try { parsed = JSON.parse(String(result.stdout || '{}')) } catch { parsed = {} }
|
|
62
|
+
for (const id of (parsed.record_id_list ?? [])) created.push(String(id))
|
|
63
|
+
}
|
|
64
|
+
health?.('feishu', 'ok', '多维表格镜像 ' + created.length + ' 条')
|
|
65
|
+
return { ok: true, cards: cards.length, created: created.length, batches: batches.map(batch => batch.length) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 没配表就直说,别让调用方以为写进去了。 */
|
|
69
|
+
export function mirrorConfigGaps(config) {
|
|
70
|
+
const gaps = []
|
|
71
|
+
if (String(config?.feishu?.baseToken ?? '').trim() === '') gaps.push('baseToken(多维表格 app_token)')
|
|
72
|
+
if (String(config?.feishu?.tableId ?? '').trim() === '') gaps.push('tableId(数据表 id)')
|
|
73
|
+
return gaps
|
|
74
|
+
}
|
package/src/host/bugs.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 自动建缺陷卡时的严重级别:按失败用例自己的优先级来,别再一律 p2。
|
|
2
|
+
// 纯函数,方便单测;建卡在 index.js 的 autoBugFromCase。
|
|
3
|
+
|
|
4
|
+
export const SEVERITY_ORDER = ['p0', 'p1', 'p2', 'p3']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param casePriority 用例优先级(p0-p3,缺省按 p2)
|
|
8
|
+
* @param cardStatus 卡当前所在列;已验收/已完成上失败说明是回归,严重级别升一级
|
|
9
|
+
*/
|
|
10
|
+
export function bugSeverityFor(casePriority, cardStatus) {
|
|
11
|
+
const raw = String(casePriority ?? '').trim().toLowerCase()
|
|
12
|
+
let index = SEVERITY_ORDER.indexOf(raw)
|
|
13
|
+
if (index < 0) index = SEVERITY_ORDER.indexOf('p2')
|
|
14
|
+
// 「已完成/待验收的卡跑失败了」比开发中的失败更要紧:往上升一级(p0 封顶)
|
|
15
|
+
const regression = cardStatus === 'done' || cardStatus === 'accept'
|
|
16
|
+
if (regression && index > 0) index -= 1
|
|
17
|
+
const severity = SEVERITY_ORDER[index]
|
|
18
|
+
const priority = severity === 'p0' ? 'urgent' : severity === 'p3' ? 'relaxed' : 'normal'
|
|
19
|
+
return { severity, priority, regression }
|
|
20
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { INTEGRATION, feishuPush, gitlabReady, gitlabRequest, gitlabSync, healthReport, projectReady } from '../integrations.js'
|
|
2
2
|
import { cleanId } from '../domain.js'
|
|
3
|
-
const INJECTED = ["applyConfig","installedPackages","publicState","readJsonBody","resolvePackageName","runPluginCli","sendJson","getSyncTimer","upsertById"]
|
|
3
|
+
const INJECTED = ['mirrorCardsToBitable', "applyConfig","installedPackages","publicState","readJsonBody","resolvePackageName","runPluginCli","sendJson","getSyncTimer","upsertById"]
|
|
4
4
|
let host = {
|
|
5
|
+
mirrorCardsToBitable: () => ({ ok: false, error: '未注入' }),
|
|
5
6
|
applyConfig: () => {},
|
|
6
7
|
installedPackages: () => {},
|
|
7
8
|
publicState: () => {},
|
|
@@ -140,6 +141,13 @@ if (method === 'POST' && route === '/bots/delete') {
|
|
|
140
141
|
host.sendJson(res, 200, { ok: true, state: host.publicState(store) })
|
|
141
142
|
return true
|
|
142
143
|
}
|
|
144
|
+
if (method === 'POST' && route === '/feishu/mirror') {
|
|
145
|
+
// 把卡片镜像进飞书多维表格;dryRun 只算条数不写
|
|
146
|
+
const body = await host.readJsonBody(req)
|
|
147
|
+
const result = await host.mirrorCardsToBitable(store, { requirementId: body.requirementId ?? '', dryRun: body.dryRun === true })
|
|
148
|
+
host.sendJson(res, 200, { ok: result.ok === true, ...result, state: result.ok === true ? host.publicState(store) : undefined })
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
143
151
|
if (method === 'POST' && route === '/feishu/test') {
|
|
144
152
|
const body = await host.readJsonBody(req).catch(() => ({}))
|
|
145
153
|
const config = store.snapshot().config
|