paseo-req-kit 1.14.0-beta.2 → 1.14.0-beta.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/CHANGELOG.md +11 -0
- package/bin/req.mjs +8 -0
- package/contracts/cli-json.md +1 -1
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +7 -0
- package/engine/src/api.ts +6 -2
- package/engine/src/checklist-cmd.ts +37 -0
- package/engine/src/checklist.ts +254 -0
- package/engine/src/reply.ts +7 -4
- package/engine/src/review-assets/check.css +57 -0
- package/engine/src/review-assets/check.html +35 -0
- package/engine/src/review-assets/check.js +258 -0
- package/engine/src/review.ts +94 -1
- package/engine/src/try.ts +134 -0
- package/package.json +1 -1
- package/paseo-plugin/client/data.ts +3 -3
- package/paseo-plugin/client/detail.tsx +5 -2
- package/paseo-plugin/client/nav.ts +5 -0
- package/paseo-plugin/client/review.tsx +20 -6
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +2 -2
- package/paseo-plugin/shared/contracts.ts +2 -1
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.gI1JF182.js → app.BpC99f5a.js} +1 -1
- package/site-dist/assets/changelog.md.m4cSU5e4.js +1 -0
- package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.DNmhkkfS.js → VPLocalSearchBox.CI43rhTA.js} +1 -1
- package/site-dist/assets/chunks/{theme.CZzkCOAC.js → theme.S6pNEF23.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.zmtXv15G.js → guide_getting-started.md.a7iNP1Ky.js} +1 -1
- package/site-dist/assets/{guide_user.md.2v7b_fzd.js → guide_user.md.D9GpGJ7y.js} +1 -1
- package/site-dist/assets/{guide_user.md.2v7b_fzd.lean.js → guide_user.md.D9GpGJ7y.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.js → reference_contracts_cli-json.md.BV1HtTmr.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.lean.js → reference_contracts_cli-json.md.BV1HtTmr.lean.js} +1 -1
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +4 -4
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +4 -4
- package/site-dist/reference/contracts/usage-record.html +4 -4
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/skills/req/SKILL.md +2 -0
- package/skills/req/playbooks/accept.md +2 -2
- package/site-dist/assets/changelog.md.2xf690VW.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.CiFeI6qC.js +0 -1
- /package/site-dist/assets/{changelog.md.2xf690VW.lean.js → changelog.md.m4cSU5e4.lean.js} +0 -0
- /package/site-dist/assets/{guide_getting-started.md.zmtXv15G.lean.js → guide_getting-started.md.a7iNP1Ky.lean.js} +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// The acceptance checklist page (1.14). Plain script, no framework: the server serves it next to
|
|
2
|
+
// its JSON API (api/info, api/draft, api/try, api/submit).
|
|
3
|
+
(function () {
|
|
4
|
+
"use strict";
|
|
5
|
+
var state = { info: null, answers: {}, reviewer: "", saveTimer: null, busy: false };
|
|
6
|
+
var $ = function (id) {
|
|
7
|
+
return document.getElementById(id);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function el(tag, attrs, children) {
|
|
11
|
+
var node = document.createElement(tag);
|
|
12
|
+
Object.keys(attrs || {}).forEach(function (k) {
|
|
13
|
+
if (attrs[k] === null || attrs[k] === undefined) return;
|
|
14
|
+
if (k === "text") node.textContent = attrs[k];
|
|
15
|
+
else if (k === "className") node.className = attrs[k];
|
|
16
|
+
else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2), attrs[k]);
|
|
17
|
+
else node.setAttribute(k, attrs[k]);
|
|
18
|
+
});
|
|
19
|
+
(children || []).forEach(function (c) {
|
|
20
|
+
if (c) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
|
|
21
|
+
});
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toast(text) {
|
|
26
|
+
var t = $("toast");
|
|
27
|
+
t.textContent = text;
|
|
28
|
+
t.hidden = false;
|
|
29
|
+
clearTimeout(toast.timer);
|
|
30
|
+
toast.timer = setTimeout(function () {
|
|
31
|
+
t.hidden = true;
|
|
32
|
+
}, 6000);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function api(pathname, method, body) {
|
|
36
|
+
return fetch(pathname, { method: method || "GET", headers: body ? { "content-type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined }).then(function (r) {
|
|
37
|
+
return r.json().then(function (data) {
|
|
38
|
+
if (!r.ok && data && data.message) throw new Error(data.message);
|
|
39
|
+
return data;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var PIPELINE = { pass: ["流水线:通过", "pass"], fail: ["流水线:没通过", "fail"], unverified: ["流水线:没验证", "unverified"], unknown: ["流水线:没有记录", ""] };
|
|
45
|
+
|
|
46
|
+
function answersList() {
|
|
47
|
+
return Object.keys(state.answers).map(function (id) {
|
|
48
|
+
return { id: id, mark: state.answers[id].mark, note: state.answers[id].note || "" };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function save() {
|
|
53
|
+
if (state.info.readOnly) return;
|
|
54
|
+
clearTimeout(state.saveTimer);
|
|
55
|
+
state.saveTimer = setTimeout(function () {
|
|
56
|
+
api("api/draft", "PUT", { answers: answersList(), reviewer: state.reviewer }).catch(function () {});
|
|
57
|
+
}, 600);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function summary() {
|
|
61
|
+
var items = state.info.items;
|
|
62
|
+
var c = { pass: 0, fail: 0, skip: 0, open: 0 };
|
|
63
|
+
var missing = 0;
|
|
64
|
+
var failedRequired = 0;
|
|
65
|
+
items.forEach(function (i) {
|
|
66
|
+
var a = state.answers[i.id];
|
|
67
|
+
if (!a) c.open++;
|
|
68
|
+
else c[a.mark]++;
|
|
69
|
+
if (i.required && (!a || a.mark === "skip")) missing++;
|
|
70
|
+
if (i.required && a && a.mark === "fail") failedRequired++;
|
|
71
|
+
});
|
|
72
|
+
var verdict = failedRequired > 0 ? "不通过" : missing === 0 ? "通过" : null;
|
|
73
|
+
$("summary").textContent =
|
|
74
|
+
"共 " + items.length + " 条:通过 " + c.pass + ",不对 " + c.fail + ",先不验 " + c.skip + ",还没看 " + c.open + "。" +
|
|
75
|
+
(verdict === "通过" ? "可以提交:验收通过。" : verdict === "不通过" ? "提交后结论是“不通过”,不对的地方会转给需求的对话。" : "必须验的都核对完才能提交“通过”。");
|
|
76
|
+
$("submit").disabled = state.info.readOnly || state.busy || verdict === null;
|
|
77
|
+
$("submit").textContent = verdict === "不通过" ? "提交:验收不通过" : "提交:验收通过";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderItem(item, n) {
|
|
81
|
+
var a = state.answers[item.id];
|
|
82
|
+
var li = el("li", { className: "item" + (a ? " " + a.mark : "") });
|
|
83
|
+
var p = PIPELINE[item.pipeline] || PIPELINE.unknown;
|
|
84
|
+
li.appendChild(
|
|
85
|
+
el("div", { className: "item-head" }, [
|
|
86
|
+
el("span", { className: "id", text: n + ". " + item.id }),
|
|
87
|
+
el("span", { className: "item-title", text: item.title }),
|
|
88
|
+
item.kind === "criterion" ? el("span", { className: "badge " + p[1], text: p[0] }) : el("span", { className: "badge unverified", text: "请你表态" }),
|
|
89
|
+
item.required ? null : el("span", { className: "badge", text: "可以以后再说" }),
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
if (item.details.length) {
|
|
93
|
+
li.appendChild(
|
|
94
|
+
el(
|
|
95
|
+
"ul",
|
|
96
|
+
{ className: "details" },
|
|
97
|
+
item.details.map(function (d) {
|
|
98
|
+
return el("li", { text: d });
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (item.evidence) li.appendChild(el("div", { className: "evidence", text: "流水线的记录:" + item.evidence }));
|
|
104
|
+
var labels = item.kind === "decision" ? [["pass", "同意"], ["fail", "不同意"], ["skip", "先不表态"]] : [["pass", "通过"], ["fail", "不对"], ["skip", "先不验"]];
|
|
105
|
+
var marks = el(
|
|
106
|
+
"div",
|
|
107
|
+
{ className: "marks" },
|
|
108
|
+
labels.map(function (l) {
|
|
109
|
+
return el("button", {
|
|
110
|
+
className: "btn" + (a && a.mark === l[0] ? " on-" + l[0] : ""),
|
|
111
|
+
text: l[1],
|
|
112
|
+
disabled: state.info.readOnly ? "disabled" : null,
|
|
113
|
+
onclick: function () {
|
|
114
|
+
var cur = state.answers[item.id];
|
|
115
|
+
state.answers[item.id] = { mark: l[0], note: cur ? cur.note : "" };
|
|
116
|
+
save();
|
|
117
|
+
render();
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
if (state.info.readOnly) marks.querySelectorAll("button").forEach(function (b) {
|
|
123
|
+
b.disabled = true;
|
|
124
|
+
});
|
|
125
|
+
li.appendChild(marks);
|
|
126
|
+
if (a && a.mark === "fail") {
|
|
127
|
+
var ta = el("textarea", { placeholder: item.kind === "decision" ? "为什么不同意?希望怎样?" : "看到了什么?应该是什么样?(例如:点导出后提示“失败”,应该下载文件)" });
|
|
128
|
+
ta.value = a.note || "";
|
|
129
|
+
ta.addEventListener("input", function () {
|
|
130
|
+
state.answers[item.id].note = ta.value;
|
|
131
|
+
save();
|
|
132
|
+
});
|
|
133
|
+
li.appendChild(ta);
|
|
134
|
+
}
|
|
135
|
+
return li;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function render() {
|
|
139
|
+
var list = $("items");
|
|
140
|
+
list.innerHTML = "";
|
|
141
|
+
state.info.items.forEach(function (item, i) {
|
|
142
|
+
list.appendChild(renderItem(item, i + 1));
|
|
143
|
+
});
|
|
144
|
+
summary();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function showTry(result) {
|
|
148
|
+
var box = $("try-box");
|
|
149
|
+
box.hidden = false;
|
|
150
|
+
box.innerHTML = "";
|
|
151
|
+
box.appendChild(el("h2", { text: result.ok ? "已经跑起来了" : "没能一键跑起来" }));
|
|
152
|
+
box.appendChild(el("div", { text: (result.message || "").split("\n")[0] }));
|
|
153
|
+
var running = (result.services || []).filter(function (s) {
|
|
154
|
+
return s.running && s.url;
|
|
155
|
+
});
|
|
156
|
+
if (running.length) {
|
|
157
|
+
box.appendChild(
|
|
158
|
+
el(
|
|
159
|
+
"ul",
|
|
160
|
+
{ className: "services" },
|
|
161
|
+
running.map(function (s) {
|
|
162
|
+
return el("li", {}, [s.name + ":", el("a", { href: s.url, target: "_blank", rel: "noopener", text: s.url })]);
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
$("try-stop").hidden = false;
|
|
167
|
+
}
|
|
168
|
+
if (result.howTo && result.howTo.length) box.appendChild(el("pre", { className: "howto", text: result.howTo.join("\n") }));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function tryIt(stop) {
|
|
172
|
+
var button = stop ? $("try-stop") : $("try");
|
|
173
|
+
button.disabled = true;
|
|
174
|
+
button.textContent = stop ? "正在停止…" : "正在启动,可能要一两分钟…";
|
|
175
|
+
api("api/try", "POST", { stop: !!stop })
|
|
176
|
+
.then(function (r) {
|
|
177
|
+
if (stop) {
|
|
178
|
+
toast(r.message);
|
|
179
|
+
$("try-stop").hidden = true;
|
|
180
|
+
$("try-box").hidden = true;
|
|
181
|
+
} else showTry(r);
|
|
182
|
+
})
|
|
183
|
+
.catch(function (e) {
|
|
184
|
+
toast(e.message);
|
|
185
|
+
})
|
|
186
|
+
.then(function () {
|
|
187
|
+
button.disabled = false;
|
|
188
|
+
button.textContent = stop ? "停止试用" : "把它跑起来试用";
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function submit() {
|
|
193
|
+
state.busy = true;
|
|
194
|
+
summary();
|
|
195
|
+
api("api/submit", "POST", { answers: answersList(), reviewer: state.reviewer })
|
|
196
|
+
.then(function (r) {
|
|
197
|
+
toast(r.message);
|
|
198
|
+
var done = el("section", { className: "card done" }, [el("h2", { text: "已提交:验收" + r.verdict }), el("div", { text: r.message })]);
|
|
199
|
+
document.querySelector("main").insertBefore(done, document.querySelector("main").firstChild);
|
|
200
|
+
window.scrollTo(0, 0);
|
|
201
|
+
})
|
|
202
|
+
.catch(function (e) {
|
|
203
|
+
toast(e.message);
|
|
204
|
+
state.busy = false;
|
|
205
|
+
summary();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function start() {
|
|
210
|
+
api("api/info").then(function (info) {
|
|
211
|
+
state.info = info;
|
|
212
|
+
document.title = "验收清单:" + info.title;
|
|
213
|
+
$("title").textContent = "验收清单:" + info.title;
|
|
214
|
+
$("project").textContent = info.project + " · " + info.featureDir;
|
|
215
|
+
$("sub").textContent = [info.reportVerdict ? "流水线结论:" + info.reportVerdict : null, info.accepted ? "上次验收:" + info.accepted : null, info.readOnly ? "只读:可以看,不能提交" : null]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" ");
|
|
218
|
+
(info.draft && info.draft.answers ? info.draft.answers : []).forEach(function (a) {
|
|
219
|
+
state.answers[a.id] = { mark: a.mark, note: a.note || "" };
|
|
220
|
+
});
|
|
221
|
+
state.reviewer = (info.draft && info.draft.reviewer) || "";
|
|
222
|
+
$("reviewer").value = state.reviewer;
|
|
223
|
+
if (info.notes && info.notes.length) {
|
|
224
|
+
var n = $("notes");
|
|
225
|
+
n.hidden = false;
|
|
226
|
+
n.appendChild(el("h2", { text: "先看一下(来自提测说明)" }));
|
|
227
|
+
n.appendChild(
|
|
228
|
+
el(
|
|
229
|
+
"ul",
|
|
230
|
+
{},
|
|
231
|
+
info.notes.map(function (t) {
|
|
232
|
+
return el("li", { text: t });
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (info.readOnly || info.running) $("try").hidden = true;
|
|
238
|
+
if (!info.items.length) $("items").appendChild(el("li", { className: "item", text: "需求说明里没有找到验收标准(AC-编号)。" }));
|
|
239
|
+
render();
|
|
240
|
+
}).catch(function (e) {
|
|
241
|
+
$("title").textContent = "打不开验收清单";
|
|
242
|
+
$("sub").textContent = e.message;
|
|
243
|
+
});
|
|
244
|
+
$("reviewer").addEventListener("input", function (e) {
|
|
245
|
+
state.reviewer = e.target.value;
|
|
246
|
+
save();
|
|
247
|
+
});
|
|
248
|
+
$("try").addEventListener("click", function () {
|
|
249
|
+
tryIt(false);
|
|
250
|
+
});
|
|
251
|
+
$("try-stop").addEventListener("click", function () {
|
|
252
|
+
tryIt(true);
|
|
253
|
+
});
|
|
254
|
+
$("submit").addEventListener("click", submit);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
start();
|
|
258
|
+
})();
|
package/engine/src/review.ts
CHANGED
|
@@ -15,6 +15,7 @@ import os from "node:os";
|
|
|
15
15
|
import http from "node:http";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
18
|
+
import { type Answer, acceptanceFailMessage, acceptanceText, buildChecklist, recordAcceptance, verdictOf } from "./checklist.js";
|
|
18
19
|
import { readIndex } from "./records.js";
|
|
19
20
|
import { readState, statePath } from "./state.js";
|
|
20
21
|
|
|
@@ -203,6 +204,8 @@ export interface ReviewServerOptions {
|
|
|
203
204
|
env?: NodeJS.ProcessEnv;
|
|
204
205
|
/** Runs `req approve|reply …` in the requirement's checkout; tests replace it. */
|
|
205
206
|
run?: (command: "approve" | "reply", repo: string, featureDir: string, extra: string[]) => { ok: boolean; message: string };
|
|
207
|
+
/** 1.14: `req try <需求> --json [--stop]` for the checklist page's "试用" button; tests replace it. */
|
|
208
|
+
tryRun?: (repo: string, featureDir: string, stop: boolean) => { ok: boolean; message: string; services?: unknown[]; howTo?: string[] };
|
|
206
209
|
onRequest?: () => void;
|
|
207
210
|
}
|
|
208
211
|
|
|
@@ -304,9 +307,28 @@ function defaultRun(command: "approve" | "reply", repo: string, featureDir: stri
|
|
|
304
307
|
return { ok: r.status === 0, message: r.error ? r.error.message : `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
305
308
|
}
|
|
306
309
|
|
|
310
|
+
function defaultTry(repo: string, featureDir: string, stop: boolean) {
|
|
311
|
+
const env = { ...process.env };
|
|
312
|
+
delete env.PASEO_AGENT_ID;
|
|
313
|
+
delete env.PASEO_AGENT_CWD;
|
|
314
|
+
const r = spawnSync(process.execPath, [path.join(KIT_ROOT, "bin", "req.mjs"), "try", featureDir, "--json", ...(stop ? ["--stop"] : [])], {
|
|
315
|
+
cwd: repo,
|
|
316
|
+
env,
|
|
317
|
+
encoding: "utf8",
|
|
318
|
+
timeout: 300_000,
|
|
319
|
+
windowsHide: true,
|
|
320
|
+
});
|
|
321
|
+
try {
|
|
322
|
+
return JSON.parse((r.stdout ?? "").trim().split("\n").at(-1) ?? "") as { ok: boolean; message: string; services?: unknown[]; howTo?: string[] };
|
|
323
|
+
} catch {
|
|
324
|
+
return { ok: false, message: r.error ? r.error.message : `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() || "没有启动" };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
308
329
|
const env = opts.env ?? process.env;
|
|
309
330
|
const run = opts.run ?? defaultRun;
|
|
331
|
+
const tryRun = opts.tryRun ?? defaultTry;
|
|
310
332
|
return http.createServer((req, res) => {
|
|
311
333
|
opts.onRequest?.();
|
|
312
334
|
handle(req, res).catch((error: Error) => json(res, 500, { ok: false, message: error.message }));
|
|
@@ -332,6 +354,11 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
332
354
|
res.writeHead(301, { location: `${url.pathname}/` });
|
|
333
355
|
return res.end();
|
|
334
356
|
}
|
|
357
|
+
if (rest === "check") {
|
|
358
|
+
res.writeHead(301, { location: `${url.pathname}/` });
|
|
359
|
+
return res.end();
|
|
360
|
+
}
|
|
361
|
+
if (rest.startsWith("check/")) return handleChecklist(req, res, rest.slice("check/".length), entry, featureAbs, full);
|
|
335
362
|
if (rest === "") return sendFile(res, path.join(ASSETS, "app.html"));
|
|
336
363
|
if (rest === "app.js" || rest === "app.css") return sendFile(res, path.join(ASSETS, rest));
|
|
337
364
|
if (rest.startsWith("proto/cur/")) return sendFile(res, inside(prototypeDir, rest.slice("proto/cur/".length)), PROTOTYPE_CSP);
|
|
@@ -400,6 +427,72 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
400
427
|
}
|
|
401
428
|
return send(res, 404, "没有这个页面");
|
|
402
429
|
}
|
|
430
|
+
|
|
431
|
+
// 1.14: the acceptance checklist, /r/<token>/f/<key>/check/…
|
|
432
|
+
async function handleChecklist(req: http.IncomingMessage, res: http.ServerResponse, rest: string, entry: { repo: string; featureDir: string; key: string; project: string }, featureAbs: string, full: boolean) {
|
|
433
|
+
const checkDir = path.join(featureAbs, ".pipeline", "check");
|
|
434
|
+
if (rest === "") return sendFile(res, path.join(ASSETS, "check.html"));
|
|
435
|
+
if (rest === "check.js" || rest === "check.css") return sendFile(res, path.join(ASSETS, rest));
|
|
436
|
+
if (rest === "api/info" && req.method === "GET") {
|
|
437
|
+
const state = readState(statePath(entry.repo, entry.featureDir));
|
|
438
|
+
let draft: unknown = { answers: [], reviewer: "" };
|
|
439
|
+
try {
|
|
440
|
+
draft = JSON.parse(fs.readFileSync(path.join(checkDir, "draft.json"), "utf8"));
|
|
441
|
+
} catch {
|
|
442
|
+
/* none yet */
|
|
443
|
+
}
|
|
444
|
+
const acceptance = fs.existsSync(path.join(featureAbs, "acceptance.md")) ? fs.readFileSync(path.join(featureAbs, "acceptance.md"), "utf8") : "";
|
|
445
|
+
return json(res, 200, {
|
|
446
|
+
...buildChecklist(featureAbs),
|
|
447
|
+
project: entry.project,
|
|
448
|
+
featureDir: entry.featureDir,
|
|
449
|
+
readOnly: !full,
|
|
450
|
+
running: state?.status === "running",
|
|
451
|
+
accepted: /^[-*]\s*验收结论[::]\s*(.+)$/m.exec(acceptance)?.[1]?.trim() ?? null,
|
|
452
|
+
draft,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (rest === "api/draft" && req.method === "PUT") {
|
|
456
|
+
if (!full) return json(res, 403, { ok: false, message: "这是只读链接" });
|
|
457
|
+
const body = (await readBody(req)) as { answers?: unknown; reviewer?: unknown } | null;
|
|
458
|
+
if (!body || !Array.isArray(body.answers)) return json(res, 400, { ok: false, message: "格式不对" });
|
|
459
|
+
fs.mkdirSync(checkDir, { recursive: true });
|
|
460
|
+
fs.writeFileSync(path.join(checkDir, "draft.json"), JSON.stringify({ answers: body.answers, reviewer: typeof body.reviewer === "string" ? body.reviewer : "", savedAt: new Date().toISOString() }, null, 2));
|
|
461
|
+
return json(res, 200, { ok: true });
|
|
462
|
+
}
|
|
463
|
+
if (rest === "api/try" && req.method === "POST") {
|
|
464
|
+
if (!full) return json(res, 403, { ok: false, message: "这是只读链接" });
|
|
465
|
+
const body = (await readBody(req)) as { stop?: boolean } | null;
|
|
466
|
+
return json(res, 200, tryRun(entry.repo, entry.featureDir, body?.stop === true));
|
|
467
|
+
}
|
|
468
|
+
if (rest === "api/submit" && req.method === "POST") {
|
|
469
|
+
if (!full) return json(res, 403, { ok: false, message: "这是只读链接,不能提交" });
|
|
470
|
+
const body = (await readBody(req)) as { answers?: Answer[]; reviewer?: string } | null;
|
|
471
|
+
const answers = (Array.isArray(body?.answers) ? body.answers : []).filter((a) => a && typeof a.id === "string" && ["pass", "fail", "skip"].includes(a.mark));
|
|
472
|
+
const list = buildChecklist(featureAbs);
|
|
473
|
+
const v = verdictOf(list, answers);
|
|
474
|
+
if (v.verdict === "通过" && v.missing.length > 0) {
|
|
475
|
+
return json(res, 400, { ok: false, message: `还有 ${v.missing.length} 条必须验的没有核对(${v.missing.map((i) => i.id).join("、")})。核对完,或者有不对的标"不对"后再提交。` });
|
|
476
|
+
}
|
|
477
|
+
const file = path.join(featureAbs, "acceptance.md");
|
|
478
|
+
const previous = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined;
|
|
479
|
+
const reviewer = body?.reviewer?.trim() || "需求方(验收页)";
|
|
480
|
+
const text = acceptanceText(list, answers, { verdict: v.verdict, reviewer: reviewer.slice(0, 60), date: new Date().toISOString().slice(0, 10), previous });
|
|
481
|
+
const { committed } = recordAcceptance(entry.repo, entry.featureDir, text);
|
|
482
|
+
fs.rmSync(path.join(checkDir, "draft.json"), { force: true });
|
|
483
|
+
if (v.verdict === "通过") {
|
|
484
|
+
return json(res, 200, { ok: true, verdict: v.verdict, committed, message: `已记录:验收通过${committed ? ",验收记录已提交" : ""}。下一步请开发同事审阅代码并合并。` });
|
|
485
|
+
}
|
|
486
|
+
const told = run("reply", entry.repo, entry.featureDir, ["--acceptance", acceptanceFailMessage(entry.featureDir, list, answers)]);
|
|
487
|
+
return json(res, 200, {
|
|
488
|
+
ok: true,
|
|
489
|
+
verdict: v.verdict,
|
|
490
|
+
committed,
|
|
491
|
+
message: told.ok ? "已记录:验收不通过。不对的地方已经转给这个需求的对话,它会和你确定怎么处理。" : `已记录:验收不通过,但没能转给需求的对话(${told.message.split("\n")[0]})。请在需求的对话里说"验收不通过"。`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return send(res, 404, "没有这个页面");
|
|
495
|
+
}
|
|
403
496
|
}
|
|
404
497
|
|
|
405
498
|
// ---------------------------------------------------------------------------
|
|
@@ -530,7 +623,7 @@ async function serve(env: NodeJS.ProcessEnv): Promise<void> {
|
|
|
530
623
|
// ---------------------------------------------------------------------------
|
|
531
624
|
// req review <需求> [--no-open] [--read-only]
|
|
532
625
|
|
|
533
|
-
function openBrowser(url: string) {
|
|
626
|
+
export function openBrowser(url: string) {
|
|
534
627
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
535
628
|
try {
|
|
536
629
|
const child = spawn(cmd, args as string[], { stdio: "ignore", detached: true, windowsHide: true });
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// `req try <需求> [--stop]` (1.14): runs a finished requirement so the requester can try it,
|
|
2
|
+
// without knowing how to start the project. The project's paseo.json services are started in the
|
|
3
|
+
// requirement's own workspace through Paseo (each on its own port), and the addresses are given
|
|
4
|
+
// with what the handoff note says about test accounts and data. Projects without Paseo-managed
|
|
5
|
+
// services get the start instructions the pipeline wrote down, and the advice to ask the chat.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { section } from "./checklist.js";
|
|
9
|
+
import { DEFAULT_CONFIG, loadConfig, type PipelineConfig } from "./config.js";
|
|
10
|
+
import { defaultConfigPath } from "./locations.js";
|
|
11
|
+
import type { AgentRuntime, ServiceTarget } from "./runtime.js";
|
|
12
|
+
import { isAlive, readState, statePath } from "./state.js";
|
|
13
|
+
|
|
14
|
+
export interface TryResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
message: string;
|
|
17
|
+
services: Array<{ name: string; url: string | null; running: boolean; output: string | null }>;
|
|
18
|
+
/** How to start it and what test data to use, from handoff.md / service-start.md. */
|
|
19
|
+
howTo: string[];
|
|
20
|
+
stopped?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function read(file: string): string {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(file, "utf8");
|
|
26
|
+
} catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tidy = (lines: string[]) => lines.map((l) => l.replace(/\s+$/, "")).filter((l, i, all) => l.trim() || (i > 0 && all[i - 1].trim())).slice(0, 40);
|
|
32
|
+
|
|
33
|
+
/** What the pipeline wrote about running it and the data to use. */
|
|
34
|
+
export function howToTry(featureAbs: string): string[] {
|
|
35
|
+
const handoff = read(path.join(featureAbs, "handoff.md"));
|
|
36
|
+
const run = section(handoff, /怎么跑起来|如何运行|启动/);
|
|
37
|
+
const data = section(handoff, /测试需要的准备|测试数据|账号/);
|
|
38
|
+
const start = run.length ? run : section(read(path.join(featureAbs, "service-start.md")), /启动方法|怎么启动|启动/);
|
|
39
|
+
return tidy([...(start.length ? ["怎么跑起来:", ...start] : []), ...(data.length ? ["", "测试需要的准备:", ...data] : [])]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TryRecord {
|
|
43
|
+
at: string;
|
|
44
|
+
target: ServiceTarget;
|
|
45
|
+
names: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const recordFile = (featureAbs: string) => path.join(featureAbs, ".pipeline", "try.json");
|
|
49
|
+
|
|
50
|
+
export async function tryRequirement(input: { repo: string; featureDir: string; stop?: boolean; runtime: Pick<AgentRuntime, "listServices" | "startServices" | "stopServices">; config: PipelineConfig }): Promise<TryResult> {
|
|
51
|
+
const featureAbs = path.join(input.repo, input.featureDir);
|
|
52
|
+
const state = readState(statePath(input.repo, input.featureDir));
|
|
53
|
+
const howTo = howToTry(featureAbs);
|
|
54
|
+
if (input.stop) {
|
|
55
|
+
let rec: TryRecord | null = null;
|
|
56
|
+
try {
|
|
57
|
+
rec = JSON.parse(fs.readFileSync(recordFile(featureAbs), "utf8")) as TryRecord;
|
|
58
|
+
} catch {
|
|
59
|
+
rec = null;
|
|
60
|
+
}
|
|
61
|
+
if (!rec) return { ok: true, message: "没有由试用启动的服务,不用停止。", services: [], howTo, stopped: false };
|
|
62
|
+
await input.runtime.stopServices(rec.target, rec.names);
|
|
63
|
+
fs.rmSync(recordFile(featureAbs), { force: true });
|
|
64
|
+
return { ok: true, message: `已停止试用的服务:${rec.names.join("、")}。`, services: [], howTo, stopped: true };
|
|
65
|
+
}
|
|
66
|
+
if (state && state.status === "running" && isAlive(state.pid)) {
|
|
67
|
+
return { ok: false, message: "流水线还在运行,它会自己启动和停止服务。等开发结束后再试用。", services: [], howTo };
|
|
68
|
+
}
|
|
69
|
+
const target: ServiceTarget | null = state?.worktree?.workspaceId ? { workspaceId: state.worktree.workspaceId } : state?.originAgentId ? { agentId: state.originAgentId } : null;
|
|
70
|
+
const names = target ? await input.runtime.listServices(target).catch(() => null) : null;
|
|
71
|
+
if (!target || !names || names.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message:
|
|
75
|
+
"这个项目没有用 Paseo 管理的服务脚本(paseo.json),不能一键启动。可以在这个需求的对话里说\"帮我把它跑起来\",或按下面的方法启动;以后请开发同事运行 /req-setup 写好 paseo.json,就能一键试用。",
|
|
76
|
+
services: [],
|
|
77
|
+
howTo,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const started = await input.runtime.startServices(target, names, input.config.serviceReadySeconds * 1000);
|
|
81
|
+
fs.mkdirSync(path.dirname(recordFile(featureAbs)), { recursive: true });
|
|
82
|
+
fs.writeFileSync(recordFile(featureAbs), JSON.stringify({ at: new Date().toISOString(), target, names } satisfies TryRecord, null, 2));
|
|
83
|
+
const failed = started.filter((s) => !s.running);
|
|
84
|
+
const services = started.map((s) => ({ name: s.name, url: s.url, running: s.running, output: s.running ? null : s.output }));
|
|
85
|
+
const ok = failed.length === 0;
|
|
86
|
+
const lines = started.map((s) => (s.running ? `- ${s.name}:${s.url ?? "已启动(没有网页地址)"}` : `- ${s.name}:没有启动成功`));
|
|
87
|
+
return {
|
|
88
|
+
ok,
|
|
89
|
+
message: `${ok ? "已启动,可以试用了" : "有服务没有启动成功"}:\n${lines.join("\n")}\n用完后停止:req try ${path.basename(input.featureDir)} --stop(插件里点"停止试用")。`,
|
|
90
|
+
services,
|
|
91
|
+
howTo,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main(argv: string[]): Promise<number> {
|
|
96
|
+
const stop = argv.includes("--stop");
|
|
97
|
+
const json = argv.includes("--json");
|
|
98
|
+
const feature = argv.find((a) => !a.startsWith("--"));
|
|
99
|
+
if (!feature || argv.includes("--help") || argv.includes("-h")) {
|
|
100
|
+
console.log("用法:req try <需求> [--stop]\n\n把做好的需求在它自己的工作区里跑起来,给出访问地址和测试数据,方便亲手试用;--stop 停止。");
|
|
101
|
+
return feature ? 0 : 2;
|
|
102
|
+
}
|
|
103
|
+
const { locate } = await import("./api.js");
|
|
104
|
+
const { repo, featureDir } = locate(process.cwd(), feature);
|
|
105
|
+
let config: PipelineConfig;
|
|
106
|
+
try {
|
|
107
|
+
config = loadConfig(defaultConfigPath(), repo).config;
|
|
108
|
+
} catch {
|
|
109
|
+
config = DEFAULT_CONFIG;
|
|
110
|
+
}
|
|
111
|
+
const { loadRuntimeFactory } = await import("./runtime-loader.js");
|
|
112
|
+
const runtime = await (await loadRuntimeFactory())({ config, log: () => undefined });
|
|
113
|
+
try {
|
|
114
|
+
const result = await tryRequirement({ repo, featureDir, stop, runtime, config });
|
|
115
|
+
if (json) console.log(JSON.stringify(result));
|
|
116
|
+
else {
|
|
117
|
+
console.log(result.message);
|
|
118
|
+
if (result.howTo.length) console.log(`\n${result.howTo.join("\n")}`);
|
|
119
|
+
}
|
|
120
|
+
return result.ok ? 0 : 1;
|
|
121
|
+
} finally {
|
|
122
|
+
await runtime.close().catch(() => undefined);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
|
127
|
+
main(process.argv.slice(2)).then(
|
|
128
|
+
(code) => process.exit(code),
|
|
129
|
+
(error: Error) => {
|
|
130
|
+
console.error(`没有启动:${error.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.14.0-beta.
|
|
3
|
+
"version": "1.14.0-beta.3",
|
|
4
4
|
"description": "req-kit: a requirement-development workflow for Paseo. /req aligns a requirement in chat; the engine runs design, implementation, review and verification as Paseo agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,11 +74,11 @@ export function useConfigView(repo: string | null) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The prototype review page's address (1.13); it stays valid while its server runs. */
|
|
77
|
-
export function useReviewUrl(repo: string, featureDir: string, enabled: boolean) {
|
|
77
|
+
export function useReviewUrl(repo: string, featureDir: string, enabled: boolean, page: "prototype" | "check" = "prototype") {
|
|
78
78
|
const reviewUrl = useRpc(reviewUrlRpc);
|
|
79
79
|
return useQuery({
|
|
80
|
-
queryKey: ["req", "review-url", repo, featureDir],
|
|
81
|
-
queryFn: () => reviewUrl({ repo, featureDir }),
|
|
80
|
+
queryKey: ["req", "review-url", repo, featureDir, page],
|
|
81
|
+
queryFn: () => reviewUrl({ repo, featureDir, ...(page === "check" ? { page } : {}) }),
|
|
82
82
|
enabled: enabled && Boolean(repo && featureDir),
|
|
83
83
|
retry: false,
|
|
84
84
|
});
|
|
@@ -9,7 +9,7 @@ import type { Action, RequirementDetail, Step } from "../shared/contracts";
|
|
|
9
9
|
import { useAction, useDocument, usePreferences, useRequirement, useRequirements } from "./data";
|
|
10
10
|
import { ACTION_TEXT, actionsFor, ago, ATTENTION_LABELS, facts, FILE_LABELS, minutes, tokens } from "./format";
|
|
11
11
|
import { Markdown, tsvToMarkdown } from "./markdown";
|
|
12
|
-
import { ReviewButton } from "./review";
|
|
12
|
+
import { ChecklistButton, ReviewButton } from "./review";
|
|
13
13
|
import { Badge, Button, Chip, Status, useStyles } from "./ui";
|
|
14
14
|
|
|
15
15
|
type Theme = PluginHostProps["theme"];
|
|
@@ -168,6 +168,8 @@ export function RequirementView({
|
|
|
168
168
|
if (!d) return <Status theme={theme} loading={query.isPending} error={query.error} />;
|
|
169
169
|
const steps = allSteps ? d.steps : d.steps.slice(-12);
|
|
170
170
|
const actions = readOnly ? [] : actionsFor(d);
|
|
171
|
+
// 1.14: finished (it has a report) and not running: the requester can try it and accept it.
|
|
172
|
+
const canAccept = d.files.includes("report.md") && !d.running;
|
|
171
173
|
return (
|
|
172
174
|
<View style={{ gap: 16 }}>
|
|
173
175
|
<View style={{ gap: 4 }}>
|
|
@@ -194,8 +196,9 @@ export function RequirementView({
|
|
|
194
196
|
{d.attention === "business" ? <Text style={s.muted}>需求方的决定请在发起需求的对话中回答,回答后再继续。</Text> : null}
|
|
195
197
|
</View>
|
|
196
198
|
|
|
197
|
-
{actions.length > 0 || d.hasPrototype ? (
|
|
199
|
+
{actions.length > 0 || d.hasPrototype || canAccept ? (
|
|
198
200
|
<View style={s.row}>
|
|
201
|
+
{canAccept ? <ChecklistButton theme={theme} repo={d.sourceRepo ?? d.repo} featureDir={d.featureDir} title={d.title} primary={d.stage.startsWith("待验收") || d.stage === "验收未通过"} /> : null}
|
|
199
202
|
{d.hasPrototype ? <ReviewButton theme={theme} repo={d.sourceRepo ?? d.repo} featureDir={d.featureDir} title={d.title} waiting={d.attention === "approval"} /> : null}
|
|
200
203
|
{actions.map((a) => (
|
|
201
204
|
<Button key={a} theme={theme} primary={a !== "stop" && !(a === "approve" && d.hasPrototype)} label={ACTION_TEXT[a].label} onPress={() => setAction(a)} />
|
|
@@ -10,3 +10,8 @@ export function setScreenOpener(open: Opener): void {
|
|
|
10
10
|
export function openReview(input: { repo: string; featureDir: string; title: string }): void {
|
|
11
11
|
opener?.({ screenId: "review", params: { repo: input.repo, featureDir: input.featureDir, title: input.title } });
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/** 1.14: the acceptance checklist of a requirement (the same screen, another page). */
|
|
15
|
+
export function openChecklist(input: { repo: string; featureDir: string; title: string }): void {
|
|
16
|
+
opener?.({ screenId: "review", params: { repo: input.repo, featureDir: input.featureDir, title: input.title, page: "check" } });
|
|
17
|
+
}
|
|
@@ -4,12 +4,17 @@ import type { PluginHostProps, PluginScreenProps } from "@getpaseo/plugin/client
|
|
|
4
4
|
import { ExternalLink } from "@getpaseo/plugin/client/ui";
|
|
5
5
|
import { Text, View } from "react-native";
|
|
6
6
|
import { useReviewUrl } from "./data";
|
|
7
|
-
import { openReview } from "./nav";
|
|
7
|
+
import { openChecklist, openReview } from "./nav";
|
|
8
8
|
import { Button, Status, useStyles } from "./ui";
|
|
9
9
|
import { canEmbedPages, embeddedPage, reviewUnreachable } from "./web";
|
|
10
10
|
|
|
11
11
|
type Theme = PluginHostProps["theme"];
|
|
12
12
|
|
|
13
|
+
/** 1.14: the acceptance checklist, primary while the requirement waits for acceptance. */
|
|
14
|
+
export function ChecklistButton({ theme, repo, featureDir, title, primary }: { theme: Theme; repo: string; featureDir: string; title: string; primary: boolean }) {
|
|
15
|
+
return <Button theme={theme} primary={primary} label="验收清单(可一键试用)" onPress={() => openChecklist({ repo, featureDir, title })} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
/** The button on a requirement with a prototype: primary while the design waits for confirmation. */
|
|
14
19
|
export function ReviewButton({ theme, repo, featureDir, title, waiting }: { theme: Theme; repo: string; featureDir: string; title: string; waiting: boolean }) {
|
|
15
20
|
return <Button theme={theme} primary={waiting} label={waiting ? "审查原型并确认方案" : "审查原型"} onPress={() => openReview({ repo, featureDir, title })} />;
|
|
@@ -19,7 +24,8 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
19
24
|
const s = useStyles(theme, layout.compact);
|
|
20
25
|
const repo = params.repo ?? "";
|
|
21
26
|
const featureDir = params.featureDir ?? "";
|
|
22
|
-
const
|
|
27
|
+
const page = params.page === "check" ? "check" : "prototype";
|
|
28
|
+
const review = useReviewUrl(repo, featureDir, true, page);
|
|
23
29
|
if (!review.data) {
|
|
24
30
|
return (
|
|
25
31
|
<View style={[s.screen, { padding: 16 }]}>
|
|
@@ -38,17 +44,25 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
38
44
|
</View>
|
|
39
45
|
);
|
|
40
46
|
}
|
|
41
|
-
const frame = canEmbedPages ? embeddedPage(url,
|
|
47
|
+
const frame = canEmbedPages ? embeddedPage(url, `${page === "check" ? "验收清单" : "原型审查"}:${params.title ?? featureDir}`) : null;
|
|
42
48
|
return (
|
|
43
49
|
<View style={[s.screen, { gap: 6 }]}>
|
|
44
50
|
<View style={[s.row, { paddingHorizontal: 12, paddingTop: 8, justifyContent: "space-between" }]}>
|
|
45
|
-
<Text style={s.muted}>
|
|
51
|
+
<Text style={s.muted}>
|
|
52
|
+
{page === "check"
|
|
53
|
+
? readOnly
|
|
54
|
+
? "插件设为只读:可以看清单,不能提交。"
|
|
55
|
+
: "逐条核对:通过、不对(写下看到了什么)、先不验;需要时点右上角把它跑起来。"
|
|
56
|
+
: readOnly
|
|
57
|
+
? "插件设为只读:可以看原型,不能提交意见或确认。"
|
|
58
|
+
: "点原型上要改的地方提意见,双击文字直接改字;看完在右下角提交或确认方案。"}
|
|
59
|
+
</Text>
|
|
46
60
|
<ExternalLink href={url}>在新窗口打开</ExternalLink>
|
|
47
61
|
</View>
|
|
48
62
|
{frame ?? (
|
|
49
63
|
<View style={[s.card, { margin: 12, gap: 8 }]}>
|
|
50
|
-
<Text style={s.text}
|
|
51
|
-
<ExternalLink href={url}
|
|
64
|
+
<Text style={s.text}>{page === "check" ? "验收清单" : "原型审查页"}在浏览器中打开效果最好(手机上屏幕较小,建议用电脑)。</Text>
|
|
65
|
+
<ExternalLink href={url}>{page === "check" ? "打开验收清单" : "打开原型审查页"}</ExternalLink>
|
|
52
66
|
</View>
|
|
53
67
|
)}
|
|
54
68
|
</View>
|