dsh-github-flow 0.5.0 → 0.7.0
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/README.en.md +112 -17
- package/README.md +115 -17
- package/docs/UPGRADE-NOTES.md +12 -11
- package/docs/architecture.md +81 -22
- package/docs/images/1.png +0 -0
- package/lib/api/route-helpers.d.ts +19 -0
- package/lib/api/route-helpers.js +54 -0
- package/lib/api/routes-actions.js +214 -9
- package/lib/api/routes-lifecycle.js +8 -1
- package/lib/api/routes-overview.d.ts +26 -0
- package/lib/api/routes-overview.js +396 -36
- package/lib/api/routes-proxy.js +8 -1
- package/lib/api/routes.d.ts +11 -1
- package/lib/api/routes.js +15 -2
- package/lib/cache.d.ts +56 -4
- package/lib/cache.js +77 -7
- package/lib/client.d.ts +8 -3
- package/lib/client.js +5542 -1391
- package/lib/destructive.d.ts +59 -0
- package/lib/destructive.js +285 -0
- package/lib/executor/args-guard.d.ts +17 -0
- package/lib/executor/args-guard.js +23 -0
- package/lib/executor/error-classifier.d.ts +55 -0
- package/lib/executor/error-classifier.js +199 -0
- package/lib/executor/failure-anchors.d.ts +52 -0
- package/lib/executor/failure-anchors.js +116 -0
- package/lib/executor/git-parser.d.ts +124 -0
- package/lib/executor/git-parser.js +397 -0
- package/lib/executor/pr-review.d.ts +95 -0
- package/lib/executor/pr-review.js +175 -0
- package/lib/executor/run-log.d.ts +36 -0
- package/lib/executor/run-log.js +78 -0
- package/lib/executor.d.ts +277 -6
- package/lib/executor.js +1265 -94
- package/lib/github-service.d.ts +1 -1
- package/lib/main.d.ts +92 -18
- package/lib/main.js +186 -95
- package/lib/renderers/common.d.ts +0 -3
- package/lib/renderers/common.js +8 -44
- package/lib/renderers/markdown.d.ts +1 -0
- package/lib/renderers/markdown.js +1 -0
- package/lib/renderers/pr.js +26 -1
- package/lib/renderers/task.d.ts +9 -0
- package/lib/renderers/task.js +120 -0
- package/lib/review-rules.d.ts +60 -0
- package/lib/review-rules.js +204 -0
- package/lib/shared/state-badges.d.ts +42 -0
- package/lib/shared/state-badges.js +102 -0
- package/lib/task-primitives.d.ts +109 -0
- package/lib/task-primitives.js +194 -0
- package/lib/tool-tiers.d.ts +57 -0
- package/lib/tool-tiers.js +168 -0
- package/lib/tools/agent-task.js +4 -4
- package/lib/tools/api.js +18 -6
- package/lib/tools/cache.js +42 -40
- package/lib/tools/codespace.js +4 -12
- package/lib/tools/discussion.js +4 -4
- package/lib/tools/gist.d.ts +1 -39
- package/lib/tools/gist.js +4 -14
- package/lib/tools/help.js +72 -18
- package/lib/tools/image-utils.d.ts +27 -0
- package/lib/tools/image-utils.js +73 -3
- package/lib/tools/issue.js +16 -28
- package/lib/tools/label.js +4 -15
- package/lib/tools/pr.d.ts +47 -0
- package/lib/tools/pr.js +170 -33
- package/lib/tools/project.js +4 -13
- package/lib/tools/release.js +11 -22
- package/lib/tools/repo.js +4 -4
- package/lib/tools/ruleset.js +9 -6
- package/lib/tools/run.js +76 -24
- package/lib/tools/secret.js +11 -17
- package/lib/tools/skill.js +18 -22
- package/lib/tools/status.js +4 -4
- package/lib/tools/task.d.ts +219 -0
- package/lib/tools/task.js +457 -0
- package/lib/types.d.ts +514 -1
- package/lib/ui/help-modal.d.ts +6 -0
- package/lib/ui/help-modal.js +124 -16
- package/lib/ui/issue-modal.d.ts +14 -0
- package/lib/ui/issue-modal.js +370 -0
- package/lib/ui/markdown-lite.d.ts +66 -0
- package/lib/ui/markdown-lite.js +312 -0
- package/lib/ui/network-banner.d.ts +12 -0
- package/lib/ui/network-banner.js +35 -0
- package/lib/ui/pr-modal.d.ts +17 -0
- package/lib/ui/pr-modal.js +555 -0
- package/lib/ui/repo-badges.d.ts +43 -0
- package/lib/ui/repo-badges.js +82 -0
- package/lib/ui/repo-selector.d.ts +14 -0
- package/lib/ui/repo-selector.js +112 -0
- package/lib/ui/run-modal.d.ts +26 -0
- package/lib/ui/run-modal.js +486 -0
- package/lib/ui/storage.d.ts +47 -0
- package/lib/ui/storage.js +139 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +1330 -44
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/utils.js +13 -1
- package/lib/ui/workspace-panel.js +226 -29
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { prStateBadge } from '../shared/state-badges.js';
|
|
2
|
+
/**
|
|
3
|
+
* 可折叠的 Markdown 内容块。
|
|
4
|
+
*
|
|
5
|
+
* 【为什么必须定义在模块顶层,不能写在 PrDetailModal 函数体内】
|
|
6
|
+
* 函数体内定义的组件每次父级渲染都会产生**新的函数引用**,
|
|
7
|
+
* React 按引用比较类型,会把 h(CollapsibleBody, ...) 当成**另一个组件**,
|
|
8
|
+
* 于是卸载重建、useState 的展开状态被重置。
|
|
9
|
+
* 表现就是:用户刚点开一条长评论,一拖分隔条(触发父级重渲染)评论又缩回去了。
|
|
10
|
+
* 提到模块级后引用恒定,状态才能跨父级渲染存活。
|
|
11
|
+
*/
|
|
12
|
+
function CollapsibleBody(props) {
|
|
13
|
+
var h = React.createElement;
|
|
14
|
+
var raw = typeof props.text === "string" ? props.text : "";
|
|
15
|
+
var totalLines = estimateContentLines(raw);
|
|
16
|
+
// 阈值取保守值(8 行 / 400 字符):宁可多给一个展开按钮,
|
|
17
|
+
// 也不要让长评论把后续内容淹掉。
|
|
18
|
+
var collapsible = totalLines > 8 || raw.length > 400;
|
|
19
|
+
var st = React.useState(props.defaultExpanded);
|
|
20
|
+
var expanded = st[0];
|
|
21
|
+
var setExpanded = st[1];
|
|
22
|
+
var inner = null;
|
|
23
|
+
if (raw.trim()) {
|
|
24
|
+
inner = renderMarkdownBlocks(h, parseMarkdownBlocks(raw));
|
|
25
|
+
}
|
|
26
|
+
if (!inner || inner.length === 0) {
|
|
27
|
+
return h("div", { style: { padding: "10px 12px", fontSize: "12px", opacity: 0.5, fontStyle: "italic" } }, "(无正文)");
|
|
28
|
+
}
|
|
29
|
+
if (!collapsible) {
|
|
30
|
+
return h("div", { className: "dsh-github-md-body" }, inner);
|
|
31
|
+
}
|
|
32
|
+
return h("div", null, h("div", {
|
|
33
|
+
className: "dsh-github-md-body",
|
|
34
|
+
style: {
|
|
35
|
+
maxHeight: expanded ? "none" : "132px",
|
|
36
|
+
overflow: expanded ? "visible" : "hidden",
|
|
37
|
+
position: "relative",
|
|
38
|
+
},
|
|
39
|
+
}, inner,
|
|
40
|
+
// 折叠时底部渐隐,暗示内容被截断而不是到此为止
|
|
41
|
+
!expanded
|
|
42
|
+
? h("div", {
|
|
43
|
+
style: {
|
|
44
|
+
position: "absolute",
|
|
45
|
+
left: 0,
|
|
46
|
+
right: 0,
|
|
47
|
+
bottom: 0,
|
|
48
|
+
height: "24px",
|
|
49
|
+
pointerEvents: "none",
|
|
50
|
+
background: "linear-gradient(to bottom, transparent, var(--dsw-alias-bg-base, #ffffff))",
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
: null), h("button", {
|
|
54
|
+
type: "button",
|
|
55
|
+
className: "dsh-github-md-expand-btn",
|
|
56
|
+
onClick: function (e) {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setExpanded(!expanded);
|
|
59
|
+
},
|
|
60
|
+
title: expanded ? "收起内容" : "展开全部内容",
|
|
61
|
+
}, expanded ? "⌃ 收起" : "⌄ 展开全部(约 " + totalLines + " 行)"));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* PR 在线完整审阅与代码比对工作台模态弹窗组件。
|
|
65
|
+
*
|
|
66
|
+
* 架构设计:
|
|
67
|
+
* 1. 单次网络往返拉取元数据与 Diff 切片,按文件切换比对 0ms 瞬间响应;
|
|
68
|
+
* 2. 覆盖作者卡片、状态徽章、分支流向、正文描述、CI 与审查决定;
|
|
69
|
+
* 3. 评论回复流 Timeline(Issue Comments + Review 评注);
|
|
70
|
+
* 4. 变动文件清单与行级高保真 Unified Diff 比对(绿色新增、红色删除、灰底 Hunk)。
|
|
71
|
+
*/
|
|
72
|
+
export function PrDetailModal(props) {
|
|
73
|
+
var prNumber = props.prNumber;
|
|
74
|
+
var repoNameWithOwner = props.repoNameWithOwner || "";
|
|
75
|
+
var cwd = props.cwd;
|
|
76
|
+
var onClose = props.onClose;
|
|
77
|
+
var showToast = props.showToast || (() => { });
|
|
78
|
+
// 严格遵守 React Hooks 铁律:所有 Hooks 必须无条件在顶层执行,
|
|
79
|
+
// 严禁在 early return 之后调用。否则「prNumber 由空变有值」时
|
|
80
|
+
// 两次渲染的 Hook 数量不一致,会触发
|
|
81
|
+
// "Rendered fewer hooks than expected" 的致命运行时崩溃。
|
|
82
|
+
var [data, setData] = React.useState(null);
|
|
83
|
+
var [loading, setLoading] = React.useState(true);
|
|
84
|
+
var [error, setError] = React.useState(null);
|
|
85
|
+
var [activeTab, setActiveTab] = React.useState("conversation");
|
|
86
|
+
var [selectedFilePath, setSelectedFilePath] = React.useState(null);
|
|
87
|
+
var [copied, setCopied] = React.useState(false);
|
|
88
|
+
// 文件区布局:宽度可拖拽、可整块折叠。初值从 localStorage 偏好恢复。
|
|
89
|
+
// 同样必须无条件在顶层执行(与上面几个 Hook 一并满足调用顺序铁律)。
|
|
90
|
+
var initialPanePrefs = getPrFilePanePrefs();
|
|
91
|
+
var [filePaneWidth, setFilePaneWidth] = React.useState(initialPanePrefs.width);
|
|
92
|
+
var [filePaneHidden, setFilePaneHidden] = React.useState(initialPanePrefs.hidden);
|
|
93
|
+
var paneContainerRef = React.useRef(null);
|
|
94
|
+
// 宽度镜像 ref:供异步回调(如 setState updater)读取最新值,
|
|
95
|
+
// 避免闭包捕获渲染期快照导致的偏好回跳。
|
|
96
|
+
var widthRef = React.useRef(initialPanePrefs.width);
|
|
97
|
+
widthRef.current = filePaneWidth;
|
|
98
|
+
// 拖拽起点存 ref 而非 state:mousemove 每秒触发几十次,
|
|
99
|
+
// 把起点塞进 state 会引发无意义的重渲染风暴。
|
|
100
|
+
var dragStateRef = React.useRef(null);
|
|
101
|
+
// 拖拽进行中的清理函数:组件卸载(如拖到一半按 Esc)时必须兜底解绑,
|
|
102
|
+
// 否则 document 上的 mousemove 与 body 的 col-resize 光标会永久残留。
|
|
103
|
+
var cleanupDragRef = React.useRef(null);
|
|
104
|
+
// Hooks 执行完毕后再做空值短路
|
|
105
|
+
if (!prNumber)
|
|
106
|
+
return null;
|
|
107
|
+
// 卸载兜底:拖拽中途关闭弹窗(Esc / 点遮罩)时清理 document 监听与 body 光标
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
return () => {
|
|
110
|
+
if (cleanupDragRef.current) {
|
|
111
|
+
cleanupDragRef.current();
|
|
112
|
+
cleanupDragRef.current = null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}, []);
|
|
116
|
+
// Esc 键监听关闭
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
var onKeyDown = (e) => {
|
|
119
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
120
|
+
onClose();
|
|
121
|
+
};
|
|
122
|
+
window.addEventListener("keydown", onKeyDown);
|
|
123
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
124
|
+
}, [onClose]);
|
|
125
|
+
// 拉取 PR 详情与差异数据
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
// 无有效编号时不发请求(Hooks 已无条件执行,这里只做副作用守卫)
|
|
128
|
+
if (!prNumber) {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
setLoading(true);
|
|
133
|
+
setError(null);
|
|
134
|
+
var url = "/api/github/pr-detail?number=" +
|
|
135
|
+
encodeURIComponent(String(prNumber)) +
|
|
136
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
137
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
138
|
+
fetch(url)
|
|
139
|
+
.then(async (res) => {
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
var errText = await res.text().catch(() => "");
|
|
142
|
+
var j = null;
|
|
143
|
+
try {
|
|
144
|
+
j = JSON.parse(errText);
|
|
145
|
+
}
|
|
146
|
+
catch (e) { }
|
|
147
|
+
throw new Error(j?.error || j?.message || errText || `服务响应异常 (${res.status})`);
|
|
148
|
+
}
|
|
149
|
+
return res.json();
|
|
150
|
+
})
|
|
151
|
+
.then((json) => {
|
|
152
|
+
setLoading(false);
|
|
153
|
+
if (json && json.ok && json.data) {
|
|
154
|
+
setData(json.data);
|
|
155
|
+
// 默认选中第一个修改的文件
|
|
156
|
+
if (Array.isArray(json.data.files) && json.data.files.length > 0) {
|
|
157
|
+
setSelectedFilePath(json.data.files[0].path);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
setError(json?.error || "获取 PR 详情失败");
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
setLoading(false);
|
|
166
|
+
setError(err?.message || "网络请求异常");
|
|
167
|
+
});
|
|
168
|
+
}, [prNumber, repoNameWithOwner, cwd]);
|
|
169
|
+
var h = React.createElement;
|
|
170
|
+
/**
|
|
171
|
+
* 开始拖拽分隔条。
|
|
172
|
+
*
|
|
173
|
+
* 为什么把监听挂到 document 而不是分隔条自身:鼠标移动速度快于元素跟随速度时,
|
|
174
|
+
* 指针会脱离分隔条,挂在元素上的 mousemove 立刻丢失,拖拽表现为"卡住"。
|
|
175
|
+
* 这也是必须成对解绑(mouseup 里 removeEventListener)的原因,否则监听泄漏。
|
|
176
|
+
*/
|
|
177
|
+
var beginResize = (e) => {
|
|
178
|
+
if (filePaneHidden)
|
|
179
|
+
return;
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
dragStateRef.current = { startX: e.clientX, startWidth: filePaneWidth };
|
|
182
|
+
var onMove = (ev) => {
|
|
183
|
+
var drag = dragStateRef.current;
|
|
184
|
+
if (!drag)
|
|
185
|
+
return;
|
|
186
|
+
var container = paneContainerRef.current;
|
|
187
|
+
var containerWidth = container ? container.getBoundingClientRect().width : 0;
|
|
188
|
+
// 宽度收敛统一走纯函数,避免在事件回调里散落边界判断
|
|
189
|
+
setFilePaneWidth(clampFilePaneWidth(drag.startWidth + (ev.clientX - drag.startX), containerWidth));
|
|
190
|
+
};
|
|
191
|
+
var onUp = () => {
|
|
192
|
+
dragStateRef.current = null;
|
|
193
|
+
document.removeEventListener("mousemove", onMove);
|
|
194
|
+
document.removeEventListener("mouseup", onUp);
|
|
195
|
+
document.body.style.cursor = "";
|
|
196
|
+
document.body.style.userSelect = "";
|
|
197
|
+
// 拖拽真正的清理函数置空,避免卸载时重复清理
|
|
198
|
+
cleanupDragRef.current = null;
|
|
199
|
+
// 只在松手时落盘一次:拖拽过程中每像素写一次 localStorage 既慢又无意义
|
|
200
|
+
setFilePaneWidth((current) => {
|
|
201
|
+
savePrFilePanePrefs({ width: current, hidden: false });
|
|
202
|
+
return current;
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
document.addEventListener("mousemove", onMove);
|
|
206
|
+
document.addEventListener("mouseup", onUp);
|
|
207
|
+
// 拖拽期间禁用文本选择并统一光标,否则会意外选中 diff 文本、光标闪烁
|
|
208
|
+
document.body.style.cursor = "col-resize";
|
|
209
|
+
document.body.style.userSelect = "none";
|
|
210
|
+
// 登记清理函数供组件卸载时兜底调用。
|
|
211
|
+
// 为什么必须有:拖拽过程中按下 Esc 会直接卸载整个弹窗,此时 mouseup 永远不会触发,
|
|
212
|
+
// 挂在 document 上的 mousemove 与 body 上的 col-resize 光标会永久残留——
|
|
213
|
+
// 表现为「弹窗关了,但整个页面鼠标仍是拖拽光标、选中文本失效」。
|
|
214
|
+
cleanupDragRef.current = () => {
|
|
215
|
+
dragStateRef.current = null;
|
|
216
|
+
document.removeEventListener("mousemove", onMove);
|
|
217
|
+
document.removeEventListener("mouseup", onUp);
|
|
218
|
+
document.body.style.cursor = "";
|
|
219
|
+
document.body.style.userSelect = "";
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* 折叠/展开文件区,并立即持久化(这是低频操作,无需等松手)。
|
|
224
|
+
*
|
|
225
|
+
* 为什么宽度要从 ref 读而不是直接闭包捕获 filePaneWidth:
|
|
226
|
+
* setFilePaneHidden 的回调执行时机由 React 调度,此时闭包里的 filePaneWidth
|
|
227
|
+
* 是**渲染时的快照**。若用户刚拖完宽度、紧接着点折叠,
|
|
228
|
+
* 落盘的会是拖动前的旧宽度,下次打开弹窗宽度回跳。
|
|
229
|
+
* ref 始终指向最新值,避免这类「看起来偶发」的偏好丢失。
|
|
230
|
+
*/
|
|
231
|
+
var toggleFilePane = () => {
|
|
232
|
+
setFilePaneHidden((prev) => {
|
|
233
|
+
var next = !prev;
|
|
234
|
+
savePrFilePanePrefs({ width: widthRef.current, hidden: next });
|
|
235
|
+
return next;
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* 渲染一段 Markdown 为 React 元素。
|
|
240
|
+
*
|
|
241
|
+
* 用 useMemo 按原文缓存:解析结果只依赖输入文本,
|
|
242
|
+
* 而弹窗内的拖拽/折叠会触发大量重渲染,不缓存等于每次都重新解析整个 PR 描述。
|
|
243
|
+
*/
|
|
244
|
+
var renderMd = function (src) {
|
|
245
|
+
var text = typeof src === "string" ? src : "";
|
|
246
|
+
if (!text.trim())
|
|
247
|
+
return null;
|
|
248
|
+
var blocks = parseMarkdownBlocks(text);
|
|
249
|
+
return renderMarkdownBlocks(h, blocks);
|
|
250
|
+
};
|
|
251
|
+
var copyText = (text, label) => {
|
|
252
|
+
if (navigator?.clipboard?.writeText) {
|
|
253
|
+
navigator.clipboard
|
|
254
|
+
.writeText(text)
|
|
255
|
+
.then(() => {
|
|
256
|
+
setCopied(true);
|
|
257
|
+
showToast(`✅ ${label} 已复制到剪贴板`);
|
|
258
|
+
setTimeout(() => setCopied(false), 2000);
|
|
259
|
+
})
|
|
260
|
+
.catch(() => { });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
// 渲染单行 Diff 高亮
|
|
264
|
+
var renderDiffContent = (diffText) => {
|
|
265
|
+
if (!diffText) {
|
|
266
|
+
return h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "该文件未产生可识别的文本差异,或已被清空。");
|
|
267
|
+
}
|
|
268
|
+
return h("div", {
|
|
269
|
+
className: "dsh-github-inline-diff-content",
|
|
270
|
+
style: {
|
|
271
|
+
margin: 0,
|
|
272
|
+
padding: "4px 0",
|
|
273
|
+
fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
|
|
274
|
+
fontSize: "11px",
|
|
275
|
+
lineHeight: "1.5",
|
|
276
|
+
overflowX: "auto",
|
|
277
|
+
borderRadius: "6px",
|
|
278
|
+
border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
|
|
279
|
+
backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
|
|
280
|
+
},
|
|
281
|
+
}, diffText.split("\n").map(function (line, idx) {
|
|
282
|
+
var isAdd = line.startsWith("+") && !line.startsWith("+++");
|
|
283
|
+
var isDel = line.startsWith("-") && !line.startsWith("---");
|
|
284
|
+
var isHunk = line.startsWith("@@");
|
|
285
|
+
var isHeader = line.startsWith("diff --git") ||
|
|
286
|
+
line.startsWith("index ") ||
|
|
287
|
+
line.startsWith("---") ||
|
|
288
|
+
line.startsWith("+++");
|
|
289
|
+
var lineStyle = {
|
|
290
|
+
display: "flex",
|
|
291
|
+
padding: "0 8px",
|
|
292
|
+
whiteSpace: "pre",
|
|
293
|
+
minWidth: "100%",
|
|
294
|
+
boxSizing: "border-box",
|
|
295
|
+
};
|
|
296
|
+
if (isAdd) {
|
|
297
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
|
|
298
|
+
lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
|
|
299
|
+
}
|
|
300
|
+
else if (isDel) {
|
|
301
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
|
|
302
|
+
lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
|
|
303
|
+
}
|
|
304
|
+
else if (isHunk) {
|
|
305
|
+
lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
|
|
306
|
+
lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
|
|
307
|
+
lineStyle.fontWeight = 600;
|
|
308
|
+
lineStyle.margin = "2px 0";
|
|
309
|
+
}
|
|
310
|
+
else if (isHeader) {
|
|
311
|
+
lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
|
|
312
|
+
lineStyle.fontSize = "10px";
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
|
|
316
|
+
}
|
|
317
|
+
return h("div", { key: idx, style: lineStyle }, line || " ");
|
|
318
|
+
}));
|
|
319
|
+
};
|
|
320
|
+
return h("div", {
|
|
321
|
+
className: "dsh-github-modal-overlay",
|
|
322
|
+
onClick: onClose,
|
|
323
|
+
style: { zIndex: 100000 },
|
|
324
|
+
}, h("div", {
|
|
325
|
+
className: "dsh-github-modal-dialog dsh-github-pr-modal-dialog",
|
|
326
|
+
onClick: (e) => e.stopPropagation(),
|
|
327
|
+
},
|
|
328
|
+
// 头部 Header
|
|
329
|
+
h("div", { className: "dsh-github-modal-header dsh-github-pr-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, renderOctocat(18, "#8250df"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
|
|
330
|
+
style: {
|
|
331
|
+
margin: 0,
|
|
332
|
+
fontSize: "14px",
|
|
333
|
+
fontWeight: 600,
|
|
334
|
+
overflow: "hidden",
|
|
335
|
+
textOverflow: "ellipsis",
|
|
336
|
+
whiteSpace: "nowrap",
|
|
337
|
+
},
|
|
338
|
+
}, data ? `#${data.number} ${data.title}` : `PR #${prNumber}`), data
|
|
339
|
+
? h("span", {
|
|
340
|
+
// 色调来自 shared/state-badges(与右侧栏 / CLI 输出同一份映射);
|
|
341
|
+
// 文案保持原样(Draft 用大驼峰、其余直接展示 GitHub 原始 state)。
|
|
342
|
+
// 未知状态沿用旧实现的绿色兜底(旧代码的 else 分支就是 green)。
|
|
343
|
+
className: "dsh-github-badge " +
|
|
344
|
+
(function () {
|
|
345
|
+
var b = prStateBadge(data.state, data.isDraft);
|
|
346
|
+
return "dsh-badge-" + (b && b.tone !== "neutral" ? b.tone : "green");
|
|
347
|
+
})(),
|
|
348
|
+
style: { fontSize: "10px", padding: "1px 6px" },
|
|
349
|
+
}, data.isDraft ? "Draft" : data.state)
|
|
350
|
+
: null), data
|
|
351
|
+
? h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" } }, h("span", null, `👤 @${data.author.login}`), data.createdAt ? h("span", null, `· 提单于 ${formatRelativeTime(data.createdAt)}`) : null, h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, `(${data.headRefName} → ${data.baseRefName})`),
|
|
352
|
+
/*
|
|
353
|
+
* 审查锚点:分支名会移动,SHA 不会。
|
|
354
|
+
* 点开这个弹窗时先看这一枚 chip,就能确认「我看到的是不是最新一次推送」;
|
|
355
|
+
* title 里放完整 SHA,方便复制去精确引用。老服务端没有该字段时不渲染。
|
|
356
|
+
*/
|
|
357
|
+
typeof data.headRefOid === "string" && data.headRefOid.length >= 7
|
|
358
|
+
? h("span", {
|
|
359
|
+
title: `headRefOid: ${data.headRefOid}`,
|
|
360
|
+
style: {
|
|
361
|
+
fontFamily: "monospace",
|
|
362
|
+
fontSize: "10px",
|
|
363
|
+
padding: "1px 6px",
|
|
364
|
+
borderRadius: "10px",
|
|
365
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(31,35,40,0.15))",
|
|
366
|
+
opacity: 0.9,
|
|
367
|
+
},
|
|
368
|
+
}, `🔖 ${data.headRefOid.slice(0, 7)}`)
|
|
369
|
+
: null)
|
|
370
|
+
: null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data?.url
|
|
371
|
+
? h("button", {
|
|
372
|
+
className: "dsh-github-btn-sm",
|
|
373
|
+
style: { padding: "4px 8px", fontSize: "11px" },
|
|
374
|
+
onClick: () => copyText(data.url, "PR 链接"),
|
|
375
|
+
title: "复制该 Pull Request 网页链接",
|
|
376
|
+
}, copied ? "✓ 已复制" : "📋 复制链接")
|
|
377
|
+
: null, data?.url
|
|
378
|
+
? h("a", {
|
|
379
|
+
href: data.url,
|
|
380
|
+
target: "_blank",
|
|
381
|
+
rel: "noopener noreferrer",
|
|
382
|
+
className: "dsh-github-btn-sm",
|
|
383
|
+
style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
|
|
384
|
+
title: "在系统默认浏览器中打开该 PR 官方页面",
|
|
385
|
+
}, "↗ 浏览器打开")
|
|
386
|
+
: null, h("button", {
|
|
387
|
+
style: {
|
|
388
|
+
background: "none",
|
|
389
|
+
border: "none",
|
|
390
|
+
cursor: "pointer",
|
|
391
|
+
fontSize: "18px",
|
|
392
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
393
|
+
padding: "0 6px",
|
|
394
|
+
},
|
|
395
|
+
onClick: onClose,
|
|
396
|
+
title: "关闭 (Esc)",
|
|
397
|
+
}, "✕"))),
|
|
398
|
+
// 状态加载或异常
|
|
399
|
+
loading
|
|
400
|
+
? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 PR 详情与变更代码比对…")
|
|
401
|
+
: error
|
|
402
|
+
? h("div", { style: { padding: "24px", margin: "16px", borderRadius: "8px", background: "rgba(248,81,73,0.08)", border: "1px solid rgba(248,81,73,0.3)" } }, h("div", { style: { fontWeight: 600, color: "#f85149", marginBottom: "6px" } }, "⚠️ 读取 PR 详情失败"), h("div", { style: { fontSize: "12px", opacity: 0.85 } }, error))
|
|
403
|
+
: data
|
|
404
|
+
? h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" } },
|
|
405
|
+
// Tab 切换导航
|
|
406
|
+
h("div", { className: "dsh-github-pr-modal-tabs" }, h("button", {
|
|
407
|
+
className: `dsh-github-pr-tab-btn ${activeTab === "conversation" ? "active" : ""}`,
|
|
408
|
+
onClick: () => setActiveTab("conversation"),
|
|
409
|
+
}, `💬 描述与讨论 (${data.comments ? data.comments.length : 0})`), h("button", {
|
|
410
|
+
className: `dsh-github-pr-tab-btn ${activeTab === "files" ? "active" : ""}`,
|
|
411
|
+
onClick: () => setActiveTab("files"),
|
|
412
|
+
},
|
|
413
|
+
// 截断时显示 N/M:M 来自 changedFiles(权威总数),N 是 gh 实际返回的条数。
|
|
414
|
+
// 只显示 N 会让「被截断」看起来像「就这么多」——这是 cli/cli#12522 的静默截断。
|
|
415
|
+
`📁 变动文件 (${data.files ? data.files.length : 0}${data.filesTruncated && data.changedFiles ? '/' + data.changedFiles : ''} 文件 · +${data.additions || 0} / -${data.deletions || 0})${data.filesTruncated ? ' ⚠️' : ''}`)),
|
|
416
|
+
// Tab 1: 描述与讨论
|
|
417
|
+
activeTab === "conversation"
|
|
418
|
+
? h("div", { className: "dsh-github-modal-body dsh-github-pr-conversation-pane" },
|
|
419
|
+
// 审查与 CI 门禁卡片
|
|
420
|
+
h("div", { className: "dsh-github-pr-checks-bar" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("span", { style: { fontWeight: 600 } }, "审查状态:"), h("span", {
|
|
421
|
+
className: "dsh-github-badge " +
|
|
422
|
+
(data.reviewDecision === "APPROVED"
|
|
423
|
+
? "dsh-badge-green"
|
|
424
|
+
: data.reviewDecision === "CHANGES_REQUESTED"
|
|
425
|
+
? "dsh-badge-red"
|
|
426
|
+
: "dsh-badge-yellow"),
|
|
427
|
+
}, data.reviewDecision || "REVIEW_REQUIRED")), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", fontFamily: "monospace" } }, h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontWeight: 600 } }, `+${data.additions}`), h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600 } }, `-${data.deletions}`))),
|
|
428
|
+
// PR 正文描述
|
|
429
|
+
h("div", { className: "dsh-github-pr-body-card" }, h("div", { className: "dsh-github-pr-comment-header" }, h("strong", null, `@${data.author.login}`), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, "提单说明 (PR Body)")), h("div", { style: { padding: "12px" } }, data.body
|
|
430
|
+
? h(CollapsibleBody, { text: data.body, defaultExpanded: true, idPrefix: "pr-body" })
|
|
431
|
+
: h("span", { style: { opacity: 0.5, fontStyle: "italic" } }, "(该 PR 未填写描述正文)"))),
|
|
432
|
+
// 讨论与评审评论流
|
|
433
|
+
h("h4", { style: { margin: "14px 0 8px 0", fontSize: "13px", fontWeight: 600 } }, `讨论与评论时间线 (${(data.comments?.length || 0) + (data.reviews?.filter((r) => r.body)?.length || 0)})`), data.comments && data.comments.length > 0
|
|
434
|
+
? data.comments.map(function (c) {
|
|
435
|
+
return h("div", { key: c.id, className: "dsh-github-pr-comment-card" }, h("div", { className: "dsh-github-pr-comment-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("strong", null, `@${c.author.login}`), c.authorAssociation && c.authorAssociation !== "NONE"
|
|
436
|
+
? h("span", { className: "dsh-github-badge dsh-badge-blue", style: { fontSize: "9px" } }, c.authorAssociation)
|
|
437
|
+
: null), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, formatRelativeTime(c.createdAt))), h("div", { style: { padding: "10px 12px" } }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "cmt-" + c.id })));
|
|
438
|
+
})
|
|
439
|
+
: h("div", { style: { textAlign: "center", padding: "16px 0", fontSize: "12px", opacity: 0.6 } }, "💬 暂无讨论与评论"))
|
|
440
|
+
: null,
|
|
441
|
+
// Tab 2: 变动文件与 Diff 比对
|
|
442
|
+
activeTab === "files"
|
|
443
|
+
? h("div", { className: "dsh-github-pr-files-pane", ref: paneContainerRef },
|
|
444
|
+
// 左/上侧:变动文件列表(可拖拽调宽、可整块折叠)
|
|
445
|
+
filePaneHidden
|
|
446
|
+
? null
|
|
447
|
+
: h("div", {
|
|
448
|
+
className: "dsh-github-pr-file-list",
|
|
449
|
+
style: { width: filePaneWidth + "px", flexShrink: 0 },
|
|
450
|
+
}, data.files && data.files.length > 0
|
|
451
|
+
? data.files.map(function (f) {
|
|
452
|
+
var isSelected = f.path === selectedFilePath;
|
|
453
|
+
return h("div", {
|
|
454
|
+
key: f.path,
|
|
455
|
+
className: "dsh-github-pr-file-item" + (isSelected ? " selected" : ""),
|
|
456
|
+
onClick: () => setSelectedFilePath(f.path),
|
|
457
|
+
title: `${f.path} (+${f.additions}, -${f.deletions})`,
|
|
458
|
+
}, h("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, minWidth: 0 } }, f.path), h("div", { style: { display: "flex", gap: "4px", fontFamily: "monospace", fontSize: "10px", flexShrink: 0 } }, f.additions > 0 ? h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)" } }, `+${f.additions}`) : null, f.deletions > 0 ? h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)" } }, `-${f.deletions}`) : null));
|
|
459
|
+
})
|
|
460
|
+
: h("div", { style: { padding: "16px", opacity: 0.6 } }, "暂无变动文件记录")),
|
|
461
|
+
// 拖拽分隔条:仅文件区展开时出现。折叠按钮常驻于此,
|
|
462
|
+
// 保证「折叠后一定找得到恢复入口」——否则一旦折叠就再也点不回来。
|
|
463
|
+
h("div", {
|
|
464
|
+
className: "dsh-github-pr-splitter",
|
|
465
|
+
onMouseDown: beginResize,
|
|
466
|
+
title: filePaneHidden ? "点击展开文件列表" : "拖动调整文件列表宽度(双击可折叠)",
|
|
467
|
+
onDoubleClick: toggleFilePane,
|
|
468
|
+
}, h("button", {
|
|
469
|
+
type: "button",
|
|
470
|
+
className: "dsh-github-pr-splitter-toggle",
|
|
471
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
472
|
+
onClick: (e) => { e.stopPropagation(); toggleFilePane(); },
|
|
473
|
+
title: filePaneHidden ? "展开文件列表" : "折叠文件列表",
|
|
474
|
+
}, filePaneHidden ? "▶" : "◀")),
|
|
475
|
+
// 右/下侧:当前选中文件的代码 Diff 视图
|
|
476
|
+
h("div", { className: "dsh-github-pr-diff-viewer" }, selectedFilePath
|
|
477
|
+
? h("div", { style: { display: "flex", flexDirection: "column", height: "100%" } }, h("div", { className: "dsh-github-pr-diff-toolbar" },
|
|
478
|
+
// 文件区折叠后,这里是恢复入口的第一触点(分隔条上的
|
|
479
|
+
// ◀/▶ 按钮是第二触点)。两个入口都保留,避免用户找不到。
|
|
480
|
+
filePaneHidden
|
|
481
|
+
? h("button", {
|
|
482
|
+
className: "dsh-github-btn-sm",
|
|
483
|
+
onClick: toggleFilePane,
|
|
484
|
+
style: { padding: "2px 8px", fontSize: "10px", flexShrink: 0 },
|
|
485
|
+
title: "展开左侧文件列表",
|
|
486
|
+
},
|
|
487
|
+
// 折叠态同样要透出截断事实(N/M),否则展开前的第一眼就被骗了
|
|
488
|
+
`📁 文件列表 (${(data.files && data.files.length) || 0}${data.filesTruncated && data.changedFiles ? '/' + data.changedFiles : ''})`)
|
|
489
|
+
: null, h("span", { style: { fontFamily: "monospace", fontWeight: 600, fontSize: "11px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, selectedFilePath), data.fileDiffs && data.fileDiffs[selectedFilePath]
|
|
490
|
+
? h("button", {
|
|
491
|
+
className: "dsh-github-btn-sm",
|
|
492
|
+
onClick: () => copyText(data.fileDiffs[selectedFilePath], "文件 Diff"),
|
|
493
|
+
style: { padding: "2px 8px", fontSize: "10px" },
|
|
494
|
+
}, copied ? "✓ 已复制" : "📋 复制文件 Diff")
|
|
495
|
+
: null), h("div", { style: { flex: 1, overflowY: "auto", minHeight: 0 } },
|
|
496
|
+
// 两种「看不到 diff」的原因必须分开说,否则用户不知道该做什么:
|
|
497
|
+
// ① diffTruncated:整段 diff 被输出阈值裁掉(去 GitHub 看完整内容)
|
|
498
|
+
// ② diffIncomplete:这个文件在清单里但没取到切片(换一个文件/重试)
|
|
499
|
+
data.diffTruncated
|
|
500
|
+
? h("div", {
|
|
501
|
+
style: {
|
|
502
|
+
margin: "8px",
|
|
503
|
+
padding: "8px 12px",
|
|
504
|
+
borderRadius: "6px",
|
|
505
|
+
fontSize: "11px",
|
|
506
|
+
lineHeight: 1.5,
|
|
507
|
+
background: "rgba(210, 153, 34, 0.12)",
|
|
508
|
+
border: "1px solid rgba(210, 153, 34, 0.35)",
|
|
509
|
+
color: "var(--dsw-alias-state-attention-primary, #9a6700)",
|
|
510
|
+
},
|
|
511
|
+
}, "⚠️ 该 PR 的 diff 体量过大已触达输出上限,部分文件可能无法展示差异。可点击右上角「↗ 浏览器打开」查看完整内容。")
|
|
512
|
+
: null, data.diffIncomplete
|
|
513
|
+
? h("div", {
|
|
514
|
+
style: {
|
|
515
|
+
margin: "8px",
|
|
516
|
+
padding: "8px 12px",
|
|
517
|
+
borderRadius: "6px",
|
|
518
|
+
fontSize: "11px",
|
|
519
|
+
lineHeight: 1.5,
|
|
520
|
+
background: "rgba(210, 153, 34, 0.12)",
|
|
521
|
+
border: "1px solid rgba(210, 153, 34, 0.35)",
|
|
522
|
+
color: "var(--dsw-alias-state-attention-primary, #9a6700)",
|
|
523
|
+
},
|
|
524
|
+
}, "⚠️ 有文件未能取到 diff 正文(二进制、重命名或超出切片范围)。这些文件显示为空不代表没有改动,请到 GitHub 核对。")
|
|
525
|
+
: null, data.fileDiffs && data.fileDiffs[selectedFilePath]
|
|
526
|
+
? renderDiffContent(data.fileDiffs[selectedFilePath])
|
|
527
|
+
: renderDiffContent(""),
|
|
528
|
+
// 行级审查评论:挂在具体代码行上的 review comment。
|
|
529
|
+
// comments 字段只含对话区,这些必须来自独立通道;
|
|
530
|
+
// 放在文件 diff 下方并按行号标注——不嵌进 hunk 中间,
|
|
531
|
+
// 避免多行/折叠上下文下错位到错误的代码行。
|
|
532
|
+
(() => {
|
|
533
|
+
var fileComments = (Array.isArray(data.lineComments) ? data.lineComments : []).filter((c) => c.path === selectedFilePath);
|
|
534
|
+
if (fileComments.length === 0)
|
|
535
|
+
return null;
|
|
536
|
+
return h("div", { className: "dsh-github-pr-line-comments" }, h("div", { className: "dsh-github-pr-line-comments-title" }, `💬 该文件的行级审查评论 (${fileComments.length})`), fileComments.map(function (c) {
|
|
537
|
+
return h("div", { key: c.id, className: "dsh-github-pr-line-comment-item" }, h("div", { className: "dsh-github-pr-line-comment-head" }, h("strong", null, `@${c.author?.login || "unknown"}`), c.line
|
|
538
|
+
? h("span", {
|
|
539
|
+
style: {
|
|
540
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
541
|
+
fontSize: "10px",
|
|
542
|
+
padding: "0 5px",
|
|
543
|
+
borderRadius: "8px",
|
|
544
|
+
background: "rgba(130, 80, 223, 0.12)",
|
|
545
|
+
color: "#8250df",
|
|
546
|
+
},
|
|
547
|
+
title: `评论锚定在${c.side === "LEFT" ? "旧" : "新"}文件第 ${c.line} 行`,
|
|
548
|
+
}, `L${c.line}${c.side === "LEFT" ? " (旧)" : ""}`)
|
|
549
|
+
: null, h("span", { style: { fontSize: "10px", opacity: 0.6 } }, formatRelativeTime(c.createdAt))), h("div", { className: "dsh-github-pr-line-comment-body" }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "lc-" + c.id })));
|
|
550
|
+
}));
|
|
551
|
+
})()))
|
|
552
|
+
: h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.6 } }, "请从左侧选择需要审阅的文件")))
|
|
553
|
+
: null)
|
|
554
|
+
: null));
|
|
555
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仓库卡片徽章推导(纯函数,无 React 依赖,可被单测直接覆盖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么独立成模块:`client.ts` 是近 2000 行的巨石,其中的 UI 判定逻辑
|
|
5
|
+
* 只能靠「bundle 里有没有这个字符串」这类弱断言覆盖。把判定规则抽成纯函数后,
|
|
6
|
+
* 「什么情况出什么徽章」可以被真实断言,而不是靠人工目测。
|
|
7
|
+
*/
|
|
8
|
+
/** 徽章语义色调,映射到既有 .dsh-badge-* 样式 */
|
|
9
|
+
export type RepoBadgeTone = 'green' | 'red' | 'yellow' | 'blue' | 'purple' | 'gray';
|
|
10
|
+
export interface RepoBadge {
|
|
11
|
+
/** 徽章文案(含图标字符) */
|
|
12
|
+
text: string;
|
|
13
|
+
tone: RepoBadgeTone;
|
|
14
|
+
/** hover 提示:说明「是什么」+「该怎么办」 */
|
|
15
|
+
title: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RepoPresentation {
|
|
18
|
+
badges: RepoBadge[];
|
|
19
|
+
/** fork 血缘行;非 fork 或拿不到上游时为 null */
|
|
20
|
+
forkLine: {
|
|
21
|
+
parentNameWithOwner: string;
|
|
22
|
+
parentUrl: string;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 依据仓库元数据推导展示信息。
|
|
27
|
+
*
|
|
28
|
+
* 三条规则(与上位方案 S4 展示契约对齐,但**不含上游 star**):
|
|
29
|
+
* 1. `isFork` 为 true → `⑂ fork` 徽章;有 parent 时附带血缘行
|
|
30
|
+
* 2. `viewerPermission` 为 READ / TRIAGE → `🔒 只读` 徽章
|
|
31
|
+
* 3. 其余一律零徽章(保持现有外观)
|
|
32
|
+
*
|
|
33
|
+
* 为什么权限未知时不显示任何徽章:离线数据没有该字段,此时既不能说
|
|
34
|
+
* 「可写」也不能说「只读」——沉默优于猜测。显示一个可能错误的徽章,
|
|
35
|
+
* 比不显示更糟。
|
|
36
|
+
*
|
|
37
|
+
* 为什么不展示上游 star:`parent` 只返回骨架,拿不到 stargazerCount
|
|
38
|
+
* (`--json` 不支持嵌套子字段,实测);补一次网络往返实测 3~6 秒,
|
|
39
|
+
* 而 star 数不是决策信息。详见 s4 执行方案 §2.1。
|
|
40
|
+
*
|
|
41
|
+
* @param repo RepoMetadata 或 null/undefined(离线、非 Git 工作区)
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveRepoBadges(repo: any): RepoPresentation;
|