@boostkit-dsh/job-workbench 0.3.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/CHANGELOG.md +23 -0
- package/README.md +36 -0
- package/client.js +269 -0
- package/cordis.patch.yml +4 -0
- package/index.js +117 -0
- package/package.json +63 -0
- package/specs.js +138 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
## 0.3.1 - 2026-09-22
|
|
4
|
+
|
|
5
|
+
- 首次发布前将包名统一为 `@boostkit-dsh/job-workbench`,去掉 scope 下重复的 `plugin-` 前缀。
|
|
6
|
+
- 将 DSH 精确兼容基线升级为 `0.1.7-alpha.1`,并同步鉴权依赖到 `@boostkit-dsh/auth-management@0.1.1`。
|
|
7
|
+
|
|
8
|
+
## 0.3.0 - 2026-09-22
|
|
9
|
+
|
|
10
|
+
- 对鉴权与审计管理增加精确硬依赖。
|
|
11
|
+
- SPEC 页面与读取接口在没有有效 GitCode ActorRef 时统一返回 `AUTH_REQUIRED`。
|
|
12
|
+
|
|
13
|
+
## 0.2.0 - 2026-09-20
|
|
14
|
+
|
|
15
|
+
- 新增右侧栏工作台面板:会话标题栏入口按钮 + `sidebar.right.pane.tab` 两阶段注册(`extension` 优先带)。
|
|
16
|
+
- 面板展示特性分组(类型/生命周期/负责人/标题、Spec ID/路径/上游引用详情)、校验问题列表、概览统计,支持工作区选择与刷新。
|
|
17
|
+
- 新增同源只读接口 `GET /kunpeng-workbench/state?workspaceId=...`。
|
|
18
|
+
- `kunpengSpecs` 服务在未配置 `root` 时改为多根 facade(`forRoot`/`roots`);显式 `root` 保持 0.1.0 行为。
|
|
19
|
+
|
|
20
|
+
## 0.1.0 - 2026-09-19
|
|
21
|
+
|
|
22
|
+
- 提供项目级 SPEC 扫描、类型注册和按 Feature ID 聚合能力。
|
|
23
|
+
- 校验生命周期、类型、必填标识及重复 Spec ID。
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @boostkit-dsh/job-workbench
|
|
2
|
+
|
|
3
|
+
## 用途
|
|
4
|
+
|
|
5
|
+
提供鲲鹏特性作业工作台的 SPEC 服务与右侧栏面板。插件扫描项目的 `.agents/specs/`,校验最小头部与目录一致性,并按稳定 Feature ID 聚合不同角色交付的 SPEC。
|
|
6
|
+
|
|
7
|
+
该插件不保存共享作业状态,不负责跨 Session 同步,也不替代 Git 历史。
|
|
8
|
+
|
|
9
|
+
## 用法
|
|
10
|
+
|
|
11
|
+
通过计算插件市场安装,或使用 DSH `0.1.7-alpha.1` 的原生插件管理能力安装精确版本:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
@boostkit-dsh/job-workbench@0.3.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Host 服务 `ctx.kunpengSpecs`
|
|
18
|
+
|
|
19
|
+
- 显式配置 `root` 时:单根服务,含 `scan()`、`listTypes()` 和 `registerType()`(0.1.0 行为);
|
|
20
|
+
- 未配置 `root` 时:多根 facade,`forRoot(projectRoot)` 返回指定项目根的服务(UI 按工作区解析项目根),`roots()` 列出已创建的根。
|
|
21
|
+
|
|
22
|
+
当前版本精确依赖 `@boostkit-dsh/auth-management@0.1.1`。右侧栏和 `GET /kunpeng-workbench/state` 在扫描任何 SPEC 前验证当前 GitCode ActorRef;未鉴权或 PAT 失效返回 `AUTH_REQUIRED`,客户端会引导到鉴权页。
|
|
23
|
+
|
|
24
|
+
### UI(0.2.0 新增)
|
|
25
|
+
|
|
26
|
+
- 会话标题栏出现“鲲鹏工作台”入口按钮,点击在右侧栏打开工作台 tab(`extension` 优先带,两阶段注册);
|
|
27
|
+
- 面板内选择 DSH 工作区后展示:特性分组(类型/生命周期/负责人/标题,展开见 Spec ID、路径与上游引用)、校验问题列表(重复 ID、状态/类型不一致等)、概览统计与刷新;
|
|
28
|
+
- 数据来自同源接口 `GET /kunpeng-workbench/state?workspaceId=...`,只读不拦截 Agent 运行。
|
|
29
|
+
|
|
30
|
+
## 配置
|
|
31
|
+
|
|
32
|
+
- `root`:可选的项目根目录;缺省按所选工作区解析(`.git` 向上发现项目根)。
|
|
33
|
+
|
|
34
|
+
## 维护人
|
|
35
|
+
|
|
36
|
+
@lujinxing
|
package/client.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Generated from src/client.ts by scripts/build-clients.mjs. Do not edit.
|
|
2
|
+
window.__ModuleLoader__.load({ id: "@boostkit-dsh/job-workbench", factory: (require2) => {
|
|
3
|
+
const module = { exports: {} };
|
|
4
|
+
const exports = module.exports;
|
|
5
|
+
const { createElement: h, useCallback, useEffect, useMemo, useState, useSyncExternalStore } = require2("react");
|
|
6
|
+
const NS = "kunpeng-job-workbench";
|
|
7
|
+
const TAB_ID = "@boostkit-dsh/job-workbench";
|
|
8
|
+
const TAB_KIND = "kunpeng-workbench";
|
|
9
|
+
const dictionaries = {
|
|
10
|
+
zh: {
|
|
11
|
+
tabTitle: "\u9CB2\u9E4F\u5DE5\u4F5C\u53F0",
|
|
12
|
+
entryButton: "\u6253\u5F00\u9CB2\u9E4F\u5DE5\u4F5C\u53F0",
|
|
13
|
+
targetWorkspace: "\u5DE5\u4F5C\u533A",
|
|
14
|
+
noWorkspace: "\u8BF7\u5148\u5728\u5DE6\u4FA7\u6DFB\u52A0\u4E00\u4E2A DSH \u5DE5\u4F5C\u533A\uFF0C\u518D\u67E5\u770B\u9879\u76EE SPEC\u3002",
|
|
15
|
+
refresh: "\u5237\u65B0",
|
|
16
|
+
loading: "\u6B63\u5728\u626B\u63CF SPEC\u2026",
|
|
17
|
+
failed: "\u626B\u63CF\u5931\u8D25",
|
|
18
|
+
featureUnit: "\u4E2A\u7279\u6027",
|
|
19
|
+
specUnit: "\u4E2A SPEC",
|
|
20
|
+
typeUnit: "\u4E2A\u7C7B\u578B",
|
|
21
|
+
issueUnit: "\u4E2A\u95EE\u9898",
|
|
22
|
+
features: "\u7279\u6027",
|
|
23
|
+
issues: "\u6821\u9A8C\u95EE\u9898",
|
|
24
|
+
emptySpecs: "\u672A\u53D1\u73B0 SPEC",
|
|
25
|
+
emptySpecsBody: "\u5728\u9879\u76EE\u6839\u7684 .agents/specs/<\u751F\u547D\u5468\u671F>/<\u7C7B\u578B>/ \u4E0B\u521B\u5EFA Markdown SPEC\uFF08\u6807\u9898 \u201C# Feature SPEC: \u2026\u201D\uFF0C\u5934\u90E8\u542B Feature/Spec/Type/Status/Owner \u5B57\u6BB5\uFF09\u540E\uFF0C\u5237\u65B0\u5373\u53EF\u770B\u5230\u3002",
|
|
26
|
+
owner: "\u8D1F\u8D23\u4EBA",
|
|
27
|
+
specId: "Spec ID",
|
|
28
|
+
status: "\u72B6\u6001",
|
|
29
|
+
path: "\u8DEF\u5F84",
|
|
30
|
+
upstream: "\u4E0A\u6E38\u5F15\u7528",
|
|
31
|
+
copy: "\u590D\u5236",
|
|
32
|
+
copied: "\u5DF2\u590D\u5236",
|
|
33
|
+
showDetails: "\u67E5\u770B\u8BE6\u60C5",
|
|
34
|
+
hideDetails: "\u6536\u8D77"
|
|
35
|
+
},
|
|
36
|
+
en: {
|
|
37
|
+
tabTitle: "Kunpeng Workbench",
|
|
38
|
+
entryButton: "Open the Kunpeng workbench",
|
|
39
|
+
targetWorkspace: "Workspace",
|
|
40
|
+
noWorkspace: "Add a DSH workspace in the sidebar before browsing project SPECs.",
|
|
41
|
+
refresh: "Refresh",
|
|
42
|
+
loading: "Scanning SPECs\u2026",
|
|
43
|
+
failed: "Scan failed",
|
|
44
|
+
featureUnit: "features",
|
|
45
|
+
specUnit: "SPECs",
|
|
46
|
+
typeUnit: "types",
|
|
47
|
+
issueUnit: "issues",
|
|
48
|
+
features: "Features",
|
|
49
|
+
issues: "Validation issues",
|
|
50
|
+
emptySpecs: "No SPECs found",
|
|
51
|
+
emptySpecsBody: 'Create Markdown SPECs under .agents/specs/<lifecycle>/<type>/ in the project root (title "# Feature SPEC: \u2026", header fields Feature/Spec/Type/Status/Owner), then refresh.',
|
|
52
|
+
owner: "Owner",
|
|
53
|
+
specId: "Spec ID",
|
|
54
|
+
status: "Status",
|
|
55
|
+
path: "Path",
|
|
56
|
+
upstream: "Upstream",
|
|
57
|
+
copy: "Copy",
|
|
58
|
+
copied: "Copied",
|
|
59
|
+
showDetails: "Details",
|
|
60
|
+
hideDetails: "Collapse"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const styles = {
|
|
64
|
+
pane: { flex: 1, minHeight: 0, overflow: "auto", boxSizing: "border-box", padding: "12px 12px 20px", display: "flex", flexDirection: "column", gap: 10, color: "var(--dsw-alias-label-primary)" },
|
|
65
|
+
row: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
|
|
66
|
+
grow: { flex: 1 },
|
|
67
|
+
title: { margin: 0, fontSize: 14, fontWeight: 700 },
|
|
68
|
+
muted: { fontSize: 12, color: "var(--dsw-alias-label-tertiary)", wordBreak: "break-word" },
|
|
69
|
+
select: { minWidth: 120, maxWidth: "100%", padding: "5px 8px", borderRadius: 7, border: "1px solid var(--dsw-alias-border-l2)", color: "var(--dsw-alias-label-primary)", background: "var(--dsw-alias-bg-layer-1)" },
|
|
70
|
+
button: { padding: "4px 10px", borderRadius: 7, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)", cursor: "pointer", font: "inherit" },
|
|
71
|
+
iconButton: { display: "inline-flex", alignItems: "center", gap: 6, padding: "4px 8px", borderRadius: 7, border: "1px solid transparent", background: "none", color: "var(--dsw-alias-label-secondary)", cursor: "pointer", font: "inherit" },
|
|
72
|
+
badge: { padding: "1px 6px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2)", fontSize: 11, color: "var(--dsw-alias-label-secondary)", whiteSpace: "nowrap" },
|
|
73
|
+
lifecycleBadge: { padding: "1px 6px", borderRadius: 999, border: "1px solid var(--dsw-alias-state-business-primary)", fontSize: 11, color: "var(--dsw-alias-state-business-primary)", whiteSpace: "nowrap" },
|
|
74
|
+
issueBadge: { padding: "1px 6px", borderRadius: 999, border: "1px solid var(--dsw-alias-state-error-primary)", fontSize: 11, color: "var(--dsw-alias-state-error-primary)", whiteSpace: "nowrap" },
|
|
75
|
+
sectionHeader: { display: "flex", alignItems: "center", gap: 8, fontSize: 12, fontWeight: 700, color: "var(--dsw-alias-label-secondary)", textTransform: "uppercase", letterSpacing: 0.4 },
|
|
76
|
+
feature: { display: "flex", flexDirection: "column", gap: 4, padding: "8px 10px", borderRadius: 9, border: "1px solid var(--dsw-alias-border-l1)", background: "var(--dsw-alias-bg-layer-1)" },
|
|
77
|
+
featureTitle: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, fontWeight: 700, wordBreak: "break-all" },
|
|
78
|
+
specRow: { display: "flex", flexDirection: "column", gap: 3 },
|
|
79
|
+
specLine: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 },
|
|
80
|
+
specTitle: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", cursor: "pointer", border: 0, padding: 0, background: "none", color: "inherit", font: "inherit", textAlign: "left" },
|
|
81
|
+
specDetail: { margin: 0, paddingLeft: 4, fontSize: 11.5, lineHeight: 1.6, color: "var(--dsw-alias-label-secondary)", borderLeft: "2px solid var(--dsw-alias-border-l2)", display: "flex", flexDirection: "column", gap: 2 },
|
|
82
|
+
detailLine: { display: "flex", gap: 6, alignItems: "baseline", wordBreak: "break-all" },
|
|
83
|
+
detailKey: { flex: "0 0 auto", color: "var(--dsw-alias-label-tertiary)" },
|
|
84
|
+
detailValue: { flex: 1, minWidth: 0, fontFamily: "var(--ds-font-family-code)", fontSize: 11 },
|
|
85
|
+
copyLink: { border: 0, padding: 0, background: "none", color: "var(--dsw-alias-state-business-primary)", cursor: "pointer", font: "inherit", fontSize: 11 },
|
|
86
|
+
issue: { display: "flex", flexDirection: "column", gap: 2, padding: "6px 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l1)" },
|
|
87
|
+
issuePath: { fontSize: 11.5, fontFamily: "var(--ds-font-family-code)", wordBreak: "break-all", color: "var(--dsw-alias-label-secondary)" },
|
|
88
|
+
issueText: { fontSize: 12, color: "var(--dsw-alias-label-secondary)" },
|
|
89
|
+
error: { padding: 10, borderRadius: 9, border: "1px solid var(--dsw-alias-state-error-primary)", color: "var(--dsw-alias-state-error-primary)", fontSize: 12, wordBreak: "break-all" },
|
|
90
|
+
empty: { padding: 12, borderRadius: 9, border: "1px dashed var(--dsw-alias-border-l2)", fontSize: 12, color: "var(--dsw-alias-label-secondary)", lineHeight: 1.6, wordBreak: "break-word" }
|
|
91
|
+
};
|
|
92
|
+
async function responseJson(response) {
|
|
93
|
+
const value = await response.json();
|
|
94
|
+
const code = value?.error?.code;
|
|
95
|
+
if (code === "AUTH_REQUIRED") window.dispatchEvent(new CustomEvent("kunpeng-auth-required"));
|
|
96
|
+
if (!response.ok || value?.ok === false) throw Object.assign(new Error(value?.error?.message ?? value?.error ?? `HTTP ${response.status}`), { code });
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function WorkbenchIcon({ size = 16, active = false }) {
|
|
100
|
+
const color = active ? "var(--dsw-alias-state-business-primary)" : "currentColor";
|
|
101
|
+
return h(
|
|
102
|
+
"svg",
|
|
103
|
+
{ width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" },
|
|
104
|
+
h("path", { d: "M4 5.5h16v4H4v-4Zm0 5.5h9v8H4v-8Zm11 0h5v8h-5v-8Z", stroke: color, strokeWidth: 1.7, strokeLinejoin: "round" })
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function SpecRow({ spec, t, language }) {
|
|
108
|
+
const [open, setOpen] = useState(false);
|
|
109
|
+
const [copied, setCopied] = useState(false);
|
|
110
|
+
const copy = useCallback(async () => {
|
|
111
|
+
try {
|
|
112
|
+
await navigator.clipboard?.writeText(spec.path);
|
|
113
|
+
setCopied(true);
|
|
114
|
+
setTimeout(() => setCopied(false), 1400);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}, [spec.path]);
|
|
118
|
+
return h(
|
|
119
|
+
"div",
|
|
120
|
+
{ style: styles.specRow },
|
|
121
|
+
h(
|
|
122
|
+
"div",
|
|
123
|
+
{ style: styles.specLine },
|
|
124
|
+
h("span", { style: styles.badge }, spec.type),
|
|
125
|
+
h("span", { style: styles.lifecycleBadge }, spec.lifecycle),
|
|
126
|
+
h(
|
|
127
|
+
"button",
|
|
128
|
+
{ type: "button", style: styles.specTitle, title: t(open ? "hideDetails" : "showDetails"), onClick: () => setOpen(!open) },
|
|
129
|
+
spec.title || spec.path
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
open ? h(
|
|
133
|
+
"div",
|
|
134
|
+
{ style: styles.specDetail },
|
|
135
|
+
h("div", { style: styles.detailLine }, h("span", { style: styles.detailKey }, `${t("specId")}\uFF1A`), h("span", { style: styles.detailValue }, spec.specId || "\u2014")),
|
|
136
|
+
h("div", { style: styles.detailLine }, h("span", { style: styles.detailKey }, `${t("owner")}\uFF1A`), h("span", { style: styles.detailValue }, spec.owner || "\u2014")),
|
|
137
|
+
h("div", { style: styles.detailLine }, h("span", { style: styles.detailKey }, `${t("status")}\uFF1A`), h("span", { style: styles.detailValue }, spec.status || "\u2014")),
|
|
138
|
+
h(
|
|
139
|
+
"div",
|
|
140
|
+
{ style: styles.detailLine },
|
|
141
|
+
h("span", { style: styles.detailKey }, `${t("path")}\uFF1A`),
|
|
142
|
+
h("span", { style: styles.detailValue }, spec.path),
|
|
143
|
+
h("button", { type: "button", style: styles.copyLink, onClick: () => void copy() }, copied ? t("copied") : t("copy"))
|
|
144
|
+
),
|
|
145
|
+
spec.upstream ? h("div", { style: styles.detailLine }, h("span", { style: styles.detailKey }, `${t("upstream")}\uFF1A`), h("span", { style: styles.detailValue }, spec.upstream)) : null
|
|
146
|
+
) : null
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function WorkbenchPane({ t, workspaceSource, load }) {
|
|
150
|
+
const [state, setState] = useState(null);
|
|
151
|
+
const [error, setError] = useState(null);
|
|
152
|
+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(null);
|
|
153
|
+
const subscribeWorkspaces = useCallback((listener) => workspaceSource.subscribe(listener), [workspaceSource]);
|
|
154
|
+
const readWorkspaces = useCallback(() => workspaceSource.getSnapshot(), [workspaceSource]);
|
|
155
|
+
const workspaceSnapshot = useSyncExternalStore(subscribeWorkspaces, readWorkspaces);
|
|
156
|
+
const workspaces = workspaceSnapshot.items ?? [];
|
|
157
|
+
const selectedWorkspace = workspaces.find((workspace) => workspace.workspaceId === selectedWorkspaceId) ?? workspaces[0] ?? null;
|
|
158
|
+
const workspaceId = selectedWorkspace?.workspaceId ?? null;
|
|
159
|
+
const loadState = useCallback(async () => {
|
|
160
|
+
setError(null);
|
|
161
|
+
if (workspaceId === null) {
|
|
162
|
+
setState(null);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
setState(await load(workspaceId));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
setState(null);
|
|
169
|
+
setError(err?.message ?? String(err));
|
|
170
|
+
}
|
|
171
|
+
}, [load, workspaceId]);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
void loadState();
|
|
174
|
+
}, [loadState]);
|
|
175
|
+
const counts = useMemo(() => ({
|
|
176
|
+
features: state?.features?.length ?? 0,
|
|
177
|
+
specs: state?.specs?.length ?? 0,
|
|
178
|
+
types: state?.types?.length ?? 0,
|
|
179
|
+
issues: state?.issues?.length ?? 0
|
|
180
|
+
}), [state]);
|
|
181
|
+
return h(
|
|
182
|
+
"div",
|
|
183
|
+
{ style: styles.pane },
|
|
184
|
+
h(
|
|
185
|
+
"div",
|
|
186
|
+
{ style: styles.row },
|
|
187
|
+
h("h3", { style: styles.title }, t("tabTitle")),
|
|
188
|
+
h("span", { style: styles.grow }),
|
|
189
|
+
h("button", { type: "button", style: styles.button, disabled: workspaceId === null, onClick: () => void loadState() }, t("refresh"))
|
|
190
|
+
),
|
|
191
|
+
h(
|
|
192
|
+
"label",
|
|
193
|
+
{ style: styles.muted },
|
|
194
|
+
`${t("targetWorkspace")}\uFF1A`,
|
|
195
|
+
h(
|
|
196
|
+
"select",
|
|
197
|
+
{ value: workspaceId ?? "", disabled: workspaces.length === 0, style: styles.select, onChange: (event) => setSelectedWorkspaceId(event.target.value) },
|
|
198
|
+
...workspaces.map((workspace) => h("option", { key: workspace.workspaceId, value: workspace.workspaceId }, workspace.title))
|
|
199
|
+
)
|
|
200
|
+
),
|
|
201
|
+
workspaceId === null ? h("div", { style: styles.empty }, t("noWorkspace")) : error ? h("div", { style: styles.error }, `${t("failed")}\uFF1A${error}`) : state === null ? h("div", { style: styles.muted }, t("loading")) : h(
|
|
202
|
+
"div",
|
|
203
|
+
{ style: { display: "flex", flexDirection: "column", gap: 10 } },
|
|
204
|
+
h("div", { style: styles.muted }, `${counts.features} ${t("featureUnit")} \xB7 ${counts.specs} ${t("specUnit")} \xB7 ${counts.types} ${t("typeUnit")} \xB7 ${counts.issues} ${t("issueUnit")}`),
|
|
205
|
+
counts.specs === 0 ? h("div", { style: styles.empty }, h("strong", null, t("emptySpecs")), h("div", null, t("emptySpecsBody"))) : h(
|
|
206
|
+
"div",
|
|
207
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
208
|
+
h("div", { style: styles.sectionHeader }, t("features")),
|
|
209
|
+
...state.features.map((feature) => h(
|
|
210
|
+
"section",
|
|
211
|
+
{ key: feature.featureId, style: styles.feature },
|
|
212
|
+
h("div", { style: styles.featureTitle }, feature.featureId, h("span", { style: styles.badge }, String(feature.specs.length))),
|
|
213
|
+
...feature.specs.map((spec) => h(SpecRow, { key: spec.path, spec, t }))
|
|
214
|
+
))
|
|
215
|
+
),
|
|
216
|
+
counts.issues > 0 ? h(
|
|
217
|
+
"div",
|
|
218
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
219
|
+
h("div", { style: styles.sectionHeader }, t("issues"), h("span", { style: styles.issueBadge }, String(counts.issues))),
|
|
220
|
+
...state.issues.map((issue, index) => h(
|
|
221
|
+
"div",
|
|
222
|
+
{ key: `${issue.path}:${issue.code}:${index}`, style: styles.issue },
|
|
223
|
+
h("span", { style: styles.issuePath }, issue.path),
|
|
224
|
+
h("span", { style: styles.issueText }, `${issue.code} \xB7 ${issue.message}`)
|
|
225
|
+
))
|
|
226
|
+
) : null
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function WorkbenchTitle({ t }) {
|
|
231
|
+
return h("span", null, t("tabTitle"));
|
|
232
|
+
}
|
|
233
|
+
function WorkbenchEntryButton({ t, open }) {
|
|
234
|
+
return h(
|
|
235
|
+
"button",
|
|
236
|
+
{ type: "button", style: styles.iconButton, title: t("entryButton"), "aria-label": t("entryButton"), onClick: open },
|
|
237
|
+
h(WorkbenchIcon, { size: 15 }),
|
|
238
|
+
t("tabTitle")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
function apply(ctx) {
|
|
242
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), "kunpeng-job-workbench: dictionaries");
|
|
243
|
+
const t = ctx.locale.bind(NS);
|
|
244
|
+
ctx.effect(() => ctx.sidebarRightTabs.register({
|
|
245
|
+
id: TAB_ID,
|
|
246
|
+
kind: TAB_KIND,
|
|
247
|
+
priority: "extension",
|
|
248
|
+
title: () => t("tabTitle")
|
|
249
|
+
}), "kunpeng-job-workbench: sidebar tab type");
|
|
250
|
+
const inject = () => ({
|
|
251
|
+
t,
|
|
252
|
+
workspaceSource: ctx.workspaces.list,
|
|
253
|
+
load: (workspaceId) => fetch(`/kunpeng-workbench/state?workspaceId=${encodeURIComponent(workspaceId)}`, { cache: "no-store" }).then(responseJson)
|
|
254
|
+
});
|
|
255
|
+
ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({ name: "sidebar.right.pane.tab", key: TAB_ID, locale: NS, inject }, WorkbenchPane));
|
|
256
|
+
ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({ name: "sidebar.right.pane.tab.title", key: TAB_ID, locale: NS, inject: () => ({ t }) }, WorkbenchTitle));
|
|
257
|
+
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
258
|
+
name: "conversation.session.header.utilities",
|
|
259
|
+
id: "kunpeng-job-workbench",
|
|
260
|
+
order: 46,
|
|
261
|
+
locale: NS,
|
|
262
|
+
inject: () => ({ t, open: () => ctx.sidebarRight.openTab(TAB_KIND, {}) })
|
|
263
|
+
}, WorkbenchEntryButton));
|
|
264
|
+
}
|
|
265
|
+
exports.name = "kunpeng-job-workbench-client";
|
|
266
|
+
exports.inject = ["slots", "locale", "workspaces", "sidebarRight", "sidebarRightTabs", "authManagementClient"];
|
|
267
|
+
exports.apply = apply;
|
|
268
|
+
return module.exports;
|
|
269
|
+
} });
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { createSpecService } from './specs.js'
|
|
5
|
+
|
|
6
|
+
export const name = 'kunpeng-job-workbench'
|
|
7
|
+
export const inject = ['authManagement']
|
|
8
|
+
|
|
9
|
+
const API_PREFIX = '/kunpeng-workbench'
|
|
10
|
+
|
|
11
|
+
/** Match the filesystem Skill provider's upward `.git` project-root discovery. */
|
|
12
|
+
function dshProjectRoot(cwd) {
|
|
13
|
+
let current = cwd
|
|
14
|
+
while (true) {
|
|
15
|
+
if (existsSync(join(current, '.git'))) return current
|
|
16
|
+
const parent = dirname(current)
|
|
17
|
+
if (parent === current) return cwd
|
|
18
|
+
current = parent
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolve a registered workspace id to the project root DSH scans. */
|
|
23
|
+
function workspaceProjectRoot(ctx, workspaceId) {
|
|
24
|
+
let registry
|
|
25
|
+
try { registry = ctx.get?.('workspaceRegistry') } catch { registry = undefined }
|
|
26
|
+
const workspace = registry?.get?.(workspaceId)
|
|
27
|
+
if (workspace === undefined || typeof workspace.path !== 'string') throw new Error(`所选 DSH 工作区不存在:${workspaceId}`)
|
|
28
|
+
return dshProjectRoot(workspace.path)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sendJson(response, status, payload) {
|
|
32
|
+
if (response.destroyed || response.writableEnded) return
|
|
33
|
+
response.writeHead(status, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' })
|
|
34
|
+
response.end(JSON.stringify(payload))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sendProtectedError(response, error) {
|
|
38
|
+
if (error?.code === 'AUTH_REQUIRED') return sendJson(response, 401, { ok: false, error: { code: 'AUTH_REQUIRED', message: String(error.message) } })
|
|
39
|
+
if (error?.code === 'AUTH_UNAVAILABLE') return sendJson(response, 503, { ok: false, error: { code: 'AUTH_UNAVAILABLE', message: String(error.message) } })
|
|
40
|
+
sendJson(response, 400, { ok: false, error: String(error?.message ?? error) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requestedWorkspaceId(value) {
|
|
44
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 200 ? value : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register the SPEC service, the workbench state endpoint and lifecycle cleanup.
|
|
49
|
+
* An explicit `config.root` keeps the 0.1.0 single-root service shape; without
|
|
50
|
+
* it, a `forRoot(projectRoot)` facade lazily creates one service per project
|
|
51
|
+
* root (the UI resolves roots from DSH workspaces).
|
|
52
|
+
*/
|
|
53
|
+
export function apply(ctx, config = {}) {
|
|
54
|
+
const auth = ctx.get?.('authManagement') ?? ctx.authManagement
|
|
55
|
+
if (auth === undefined) throw new Error('kunpeng-job-workbench 需要 authManagement 服务')
|
|
56
|
+
const services = new Map()
|
|
57
|
+
let disposed = false
|
|
58
|
+
|
|
59
|
+
function serviceFor(projectRoot) {
|
|
60
|
+
const root = resolve(projectRoot)
|
|
61
|
+
let service = services.get(root)
|
|
62
|
+
if (service === undefined) {
|
|
63
|
+
service = createSpecService({ root })
|
|
64
|
+
services.set(root, service)
|
|
65
|
+
}
|
|
66
|
+
return service
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function disposeAll() {
|
|
70
|
+
disposed = true
|
|
71
|
+
for (const service of services.values()) service.dispose()
|
|
72
|
+
services.clear()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (config.root !== undefined) ctx.provide('kunpengSpecs', serviceFor(config.root))
|
|
76
|
+
else {
|
|
77
|
+
ctx.provide('kunpengSpecs', {
|
|
78
|
+
forRoot(projectRoot) {
|
|
79
|
+
if (disposed) throw new Error('kunpengSpecs service 已释放')
|
|
80
|
+
return serviceFor(projectRoot)
|
|
81
|
+
},
|
|
82
|
+
roots() { return [...services.keys()] },
|
|
83
|
+
dispose: disposeAll,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.effect?.(() => () => disposeAll(), 'kunpeng-job-workbench.dispose')
|
|
88
|
+
|
|
89
|
+
ctx.inject(['webServer'], (host) => {
|
|
90
|
+
host.effect(() => {
|
|
91
|
+
const route = {
|
|
92
|
+
kind: 'exact',
|
|
93
|
+
path: `${API_PREFIX}/state`,
|
|
94
|
+
handler: (request, response) => {
|
|
95
|
+
if (request.method !== 'GET') { response.writeHead(405, { allow: 'GET' }); response.end(); return }
|
|
96
|
+
void auth.requireActor().then(() => {
|
|
97
|
+
let workspaceId = null
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL(request.url ?? '', 'http://localhost')
|
|
100
|
+
workspaceId = requestedWorkspaceId(url.searchParams.get('workspaceId'))
|
|
101
|
+
} catch { /* invalid request URL */ }
|
|
102
|
+
if (workspaceId === null) return sendJson(response, 400, { ok: false, error: 'invalid workspaceId' })
|
|
103
|
+
const projectRoot = config.root === undefined ? workspaceProjectRoot(ctx, workspaceId) : resolve(config.root)
|
|
104
|
+
const state = serviceFor(projectRoot).scan()
|
|
105
|
+
sendJson(response, 200, { ok: true, workspaceId, ...state })
|
|
106
|
+
}).catch((error) => sendProtectedError(response, error))
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
const dispose = host.webServer.register(route)
|
|
110
|
+
return () => dispose()
|
|
111
|
+
}, 'kunpeng-job-workbench.http')
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { createSpecService } from './specs.js'
|
|
116
|
+
|
|
117
|
+
export const __testing = { dshProjectRoot, workspaceProjectRoot }
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boostkit-dsh/job-workbench",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "扫描项目 SPEC 并派生特性、交付件和校验问题的工作台插件(服务 + 右侧栏面板)",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"CHANGELOG.md",
|
|
9
|
+
"README.md",
|
|
10
|
+
"client.js",
|
|
11
|
+
"cordis.patch.yml",
|
|
12
|
+
"index.js",
|
|
13
|
+
"specs.js"
|
|
14
|
+
],
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://gitcode.com/lujinxing/kunpeng-dsh-demo.git",
|
|
19
|
+
"directory": "plugins/job-workbench"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test test/*.test.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@boostkit-dsh/auth-management": "0.1.1"
|
|
30
|
+
},
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"market": {
|
|
36
|
+
"displayName": {
|
|
37
|
+
"zh": "鲲鹏特性作业工作台",
|
|
38
|
+
"en": "Kunpeng Feature Workbench"
|
|
39
|
+
},
|
|
40
|
+
"category": "kunpeng-rd",
|
|
41
|
+
"maintainers": [
|
|
42
|
+
"@lujinxing"
|
|
43
|
+
],
|
|
44
|
+
"compatibility": {
|
|
45
|
+
"dsh": "0.1.7-alpha.1"
|
|
46
|
+
},
|
|
47
|
+
"allowedBuilds": []
|
|
48
|
+
},
|
|
49
|
+
"client": {
|
|
50
|
+
"platform": "web",
|
|
51
|
+
"inject": [
|
|
52
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
53
|
+
"@deepseek-ai/dsh-client-locale",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"exports": {
|
|
59
|
+
".": "./index.js",
|
|
60
|
+
"./client": "./client.js",
|
|
61
|
+
"./package.json": "./package.json"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/specs.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const TYPE_RE = /^[a-z][a-z0-9-]*$/
|
|
5
|
+
const LIFECYCLES = ['proposed', 'approved', 'rejected', 'archived']
|
|
6
|
+
const BUILTIN_TYPES = [
|
|
7
|
+
'requirement',
|
|
8
|
+
'design',
|
|
9
|
+
'implementation',
|
|
10
|
+
'verification',
|
|
11
|
+
'review',
|
|
12
|
+
'retrospective',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function filesUnder(directory) {
|
|
16
|
+
if (!existsSync(directory)) return []
|
|
17
|
+
const files = []
|
|
18
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
19
|
+
const path = join(directory, entry.name)
|
|
20
|
+
if (entry.isDirectory()) files.push(...filesUnder(path))
|
|
21
|
+
else if (entry.name.endsWith('.md')) files.push(path)
|
|
22
|
+
}
|
|
23
|
+
return files.sort()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseHeader(text) {
|
|
27
|
+
const lines = text.split(/\r?\n/)
|
|
28
|
+
const title = /^# Feature SPEC:\s*(.+)$/.exec(lines[0] ?? '')?.[1]?.trim() ?? ''
|
|
29
|
+
const fields = {}
|
|
30
|
+
for (const line of lines.slice(1, 24)) {
|
|
31
|
+
const match = /^([A-Za-z][A-Za-z-]*):\s*(.*)$/.exec(line)
|
|
32
|
+
if (match) fields[match[1].toLowerCase()] = match[2].trim()
|
|
33
|
+
}
|
|
34
|
+
return { title, fields }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function relativePath(root, file) {
|
|
38
|
+
return relative(root, file).split(sep).join('/')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function issue(code, path, message) {
|
|
42
|
+
return { code, path, message }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createSpecService(options = {}) {
|
|
46
|
+
const projectRoot = resolve(options.root ?? process.cwd())
|
|
47
|
+
const specsRoot = join(projectRoot, '.agents', 'specs')
|
|
48
|
+
const customTypes = new Map()
|
|
49
|
+
let disposed = false
|
|
50
|
+
|
|
51
|
+
function knownTypes() {
|
|
52
|
+
return [...BUILTIN_TYPES, ...customTypes.keys()].sort()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function registerType(type, definition = {}) {
|
|
56
|
+
if (disposed) throw new Error('kunpengSpecs service 已释放')
|
|
57
|
+
if (!TYPE_RE.test(type)) throw new Error(`SPEC 类型非法:${type}`)
|
|
58
|
+
if (BUILTIN_TYPES.includes(type) || customTypes.has(type)) throw new Error(`SPEC 类型已存在:${type}`)
|
|
59
|
+
customTypes.set(type, { type, ...definition })
|
|
60
|
+
let active = true
|
|
61
|
+
return () => {
|
|
62
|
+
if (!active) return
|
|
63
|
+
active = false
|
|
64
|
+
customTypes.delete(type)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scan() {
|
|
69
|
+
if (disposed) throw new Error('kunpengSpecs service 已释放')
|
|
70
|
+
const specs = []
|
|
71
|
+
const issues = []
|
|
72
|
+
const seenSpecIds = new Map()
|
|
73
|
+
const allowedTypes = new Set(knownTypes())
|
|
74
|
+
|
|
75
|
+
for (const lifecycle of LIFECYCLES) {
|
|
76
|
+
const lifecycleRoot = join(specsRoot, lifecycle)
|
|
77
|
+
if (!existsSync(lifecycleRoot)) continue
|
|
78
|
+
for (const typeEntry of readdirSync(lifecycleRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
79
|
+
const type = typeEntry.name
|
|
80
|
+
for (const file of filesUnder(join(lifecycleRoot, type))) {
|
|
81
|
+
const path = relativePath(projectRoot, file)
|
|
82
|
+
const { title, fields } = parseHeader(readFileSync(file, 'utf8'))
|
|
83
|
+
const record = {
|
|
84
|
+
path,
|
|
85
|
+
title,
|
|
86
|
+
lifecycle,
|
|
87
|
+
type,
|
|
88
|
+
status: fields.status ?? '',
|
|
89
|
+
featureId: fields.feature ?? '',
|
|
90
|
+
specId: fields.spec ?? '',
|
|
91
|
+
owner: fields.owner ?? '',
|
|
92
|
+
upstream: fields.upstream ?? '',
|
|
93
|
+
}
|
|
94
|
+
specs.push(record)
|
|
95
|
+
|
|
96
|
+
if (!allowedTypes.has(type)) issues.push(issue('unknown-type', path, `未注册的 SPEC 类型:${type}`))
|
|
97
|
+
if (record.status !== lifecycle) issues.push(issue('status-mismatch', path, `Status ${record.status || '<missing>'} 与目录 ${lifecycle} 不一致`))
|
|
98
|
+
if (fields.type !== type) issues.push(issue('type-mismatch', path, `Type ${fields.type || '<missing>'} 与目录 ${type} 不一致`))
|
|
99
|
+
if (title === '') issues.push(issue('missing-title', path, '缺少 # Feature SPEC 标题'))
|
|
100
|
+
if (record.featureId === '') issues.push(issue('missing-feature-id', path, '缺少 Feature 字段'))
|
|
101
|
+
if (record.specId === '') issues.push(issue('missing-spec-id', path, '缺少 Spec 字段'))
|
|
102
|
+
else if (seenSpecIds.has(record.specId)) issues.push(issue('duplicate-spec-id', path, `Spec ID 与 ${seenSpecIds.get(record.specId)} 重复`))
|
|
103
|
+
else seenSpecIds.set(record.specId, path)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const featureMap = new Map()
|
|
109
|
+
for (const spec of specs) {
|
|
110
|
+
if (spec.featureId === '') continue
|
|
111
|
+
if (!featureMap.has(spec.featureId)) featureMap.set(spec.featureId, [])
|
|
112
|
+
featureMap.get(spec.featureId).push(spec)
|
|
113
|
+
}
|
|
114
|
+
const features = [...featureMap.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([featureId, featureSpecs]) => ({
|
|
115
|
+
featureId,
|
|
116
|
+
specs: featureSpecs.sort((left, right) => left.path.localeCompare(right.path)),
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
root: relativePath(projectRoot, specsRoot),
|
|
121
|
+
types: knownTypes(),
|
|
122
|
+
specs: specs.sort((left, right) => left.path.localeCompare(right.path)),
|
|
123
|
+
features,
|
|
124
|
+
issues: issues.sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
root: specsRoot,
|
|
130
|
+
registerType,
|
|
131
|
+
scan,
|
|
132
|
+
listTypes: knownTypes,
|
|
133
|
+
dispose() {
|
|
134
|
+
disposed = true
|
|
135
|
+
customTypes.clear()
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|