dsh-github-flow 0.4.0 → 0.6.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 +91 -12
- package/README.md +91 -12
- package/docs/UPGRADE-NOTES.md +9 -12
- package/docs/architecture.md +63 -29
- package/docs/images/1.png +0 -0
- package/lib/api/routes-actions.js +268 -5
- package/lib/api/routes-overview.js +147 -29
- package/lib/cache.d.ts +77 -4
- package/lib/cache.js +114 -7
- package/lib/client.d.ts +9 -4
- package/lib/client.js +5293 -942
- package/lib/commands/gh.d.ts +6 -1
- package/lib/commands/gh.js +144 -71
- package/lib/executor/error-classifier.d.ts +55 -0
- package/lib/executor/error-classifier.js +199 -0
- package/lib/executor/git-parser.d.ts +157 -1
- package/lib/executor/git-parser.js +513 -4
- package/lib/executor.d.ts +147 -4
- package/lib/executor.js +866 -77
- package/lib/main.js +28 -4
- package/lib/renderers/agent-task.d.ts +16 -0
- package/lib/renderers/agent-task.js +107 -0
- package/lib/renderers/codespace.d.ts +16 -0
- package/lib/renderers/codespace.js +103 -0
- package/lib/renderers/discussion.d.ts +16 -0
- package/lib/renderers/discussion.js +101 -0
- package/lib/renderers/markdown.d.ts +4 -0
- package/lib/renderers/markdown.js +4 -0
- package/lib/renderers/project.d.ts +20 -0
- package/lib/renderers/project.js +128 -0
- package/lib/renderers/skill.d.ts +30 -0
- package/lib/renderers/skill.js +180 -0
- package/lib/renderers/status.d.ts +5 -0
- package/lib/renderers/status.js +52 -0
- package/lib/telemetry.d.ts +70 -0
- package/lib/telemetry.js +161 -0
- package/lib/tools/agent-task.d.ts +54 -0
- package/lib/tools/agent-task.js +135 -0
- package/lib/tools/codespace.d.ts +51 -0
- package/lib/tools/codespace.js +154 -0
- package/lib/tools/discussion.d.ts +70 -0
- package/lib/tools/discussion.js +195 -0
- package/lib/tools/help.js +129 -14
- package/lib/tools/image-utils.d.ts +27 -0
- package/lib/tools/image-utils.js +73 -3
- package/lib/tools/project.d.ts +80 -0
- package/lib/tools/project.js +241 -0
- package/lib/tools/repo.js +13 -1
- package/lib/tools/skill.d.ts +24 -1
- package/lib/tools/skill.js +115 -5
- package/lib/tools/status.d.ts +3 -2
- package/lib/tools/status.js +9 -4
- package/lib/types.d.ts +336 -0
- package/lib/ui/commit-modal.js +144 -14
- package/lib/ui/help-modal.d.ts +21 -0
- package/lib/ui/help-modal.js +300 -0
- package/lib/ui/issue-modal.d.ts +14 -0
- package/lib/ui/issue-modal.js +366 -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 +516 -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/storage.d.ts +56 -0
- package/lib/ui/storage.js +163 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +1219 -0
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/upgrade-card.js +39 -14
- package/lib/ui/utils.d.ts +35 -0
- package/lib/ui/utils.js +172 -0
- package/lib/ui/workspace-panel.js +198 -31
- package/lib/upgrade-manager.d.ts +5 -1
- package/lib/upgrade-manager.js +24 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub 关闭原因的状态语义映射。
|
|
3
|
+
* 实测 `stateReason` 取值域:COMPLETED / NOT_PLANNED / REOPENED / ''(空串)。
|
|
4
|
+
* 空串在关闭态下表示「未给出原因」,不渲染徽章即可。
|
|
5
|
+
*/
|
|
6
|
+
var ISSUE_STATE_REASON_LABEL = {
|
|
7
|
+
COMPLETED: '已完成',
|
|
8
|
+
NOT_PLANNED: '不计划处理',
|
|
9
|
+
REOPENED: '已重开',
|
|
10
|
+
};
|
|
11
|
+
/** `#d73a4a` 形态的标签色转 rgba;非法色值退回中性灰,绝不让颜色拼错导致整块渲染失败 */
|
|
12
|
+
function labelRgba(hex, alpha) {
|
|
13
|
+
var raw = typeof hex === 'string' ? hex.replace('#', '').trim() : '';
|
|
14
|
+
if (!/^[0-9a-fA-F]{6}$/.test(raw))
|
|
15
|
+
return 'rgba(140, 149, 159, ' + alpha + ')';
|
|
16
|
+
var r = parseInt(raw.slice(0, 2), 16);
|
|
17
|
+
var g = parseInt(raw.slice(2, 4), 16);
|
|
18
|
+
var b = parseInt(raw.slice(4, 6), 16);
|
|
19
|
+
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 格式化里程碑截止日(未来 / 过去双向)。
|
|
23
|
+
*
|
|
24
|
+
* 为什么不能直接用 formatRelativeTime:它把**所有未来时间**都归一成「刚刚」
|
|
25
|
+
* (见 utils.ts 的 `if (diff < 0) return "刚刚"`,那是为更新时间/创建时间这类
|
|
26
|
+
* 永不为未来的字段设计的)。而里程碑 dueOn 绝大多数都在未来,
|
|
27
|
+
* 直接套用会把「30 天后到期」渲染成「刚刚截止」——语义完全反过来。
|
|
28
|
+
*/
|
|
29
|
+
function formatDueDate(iso) {
|
|
30
|
+
var t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
31
|
+
if (isNaN(t))
|
|
32
|
+
return '';
|
|
33
|
+
var diff = t - Date.now();
|
|
34
|
+
var DAY = 86400000;
|
|
35
|
+
var days = Math.ceil(Math.abs(diff) / DAY);
|
|
36
|
+
if (diff >= 0) {
|
|
37
|
+
if (days <= 0)
|
|
38
|
+
return '今天到期';
|
|
39
|
+
if (days === 1)
|
|
40
|
+
return '明天到期';
|
|
41
|
+
if (days < 30)
|
|
42
|
+
return days + ' 天后到期';
|
|
43
|
+
if (days < 365)
|
|
44
|
+
return Math.round(days / 30) + ' 个月后到期';
|
|
45
|
+
return Math.round(days / 365) + ' 年后到期';
|
|
46
|
+
}
|
|
47
|
+
if (days <= 0)
|
|
48
|
+
return '今天已到期';
|
|
49
|
+
if (days === 1)
|
|
50
|
+
return '昨天已到期';
|
|
51
|
+
if (days < 30)
|
|
52
|
+
return '已逾期 ' + days + ' 天';
|
|
53
|
+
if (days < 365)
|
|
54
|
+
return '已逾期 ' + Math.round(days / 30) + ' 个月';
|
|
55
|
+
return '已逾期 ' + Math.round(days / 365) + ' 年';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 图片附件清单。
|
|
59
|
+
*
|
|
60
|
+
* 为什么不在弹窗里直接 <img src>:图片可能来自任意 https 外域,直连会把
|
|
61
|
+
* 宿主页面的 Referer 与来源暴露给第三方,也绕过插件的域名白名单防线。
|
|
62
|
+
* 这里只给可点外链,由用户显式决定是否打开。
|
|
63
|
+
*/
|
|
64
|
+
function IssueImageList(props) {
|
|
65
|
+
var h = React.createElement;
|
|
66
|
+
var urls = Array.isArray(props.urls) ? props.urls : [];
|
|
67
|
+
var st = React.useState(false);
|
|
68
|
+
var open = st[0];
|
|
69
|
+
var setOpen = st[1];
|
|
70
|
+
if (urls.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
return h("span", { className: "dsh-github-issue-image-badge" }, h("button", {
|
|
73
|
+
type: "button",
|
|
74
|
+
className: "dsh-github-issue-chip dsh-github-issue-chip-btn",
|
|
75
|
+
onClick: function () { setOpen(!open); },
|
|
76
|
+
title: open ? "收起图片清单" : "展开图片链接清单(不在面板内直连渲染)",
|
|
77
|
+
"aria-expanded": open ? "true" : "false",
|
|
78
|
+
}, "📎 " + urls.length + " 张图片"), open
|
|
79
|
+
? h(
|
|
80
|
+
// 用 span 而非 div:整个元数据条本身是 span 级 content 容器,
|
|
81
|
+
// div 嵌在 span 里虽然浏览器会容错,但属于非法嵌套。
|
|
82
|
+
"span", { className: "dsh-github-issue-image-links" }, urls.map(function (u) {
|
|
83
|
+
var name = u.split("?")[0].split("/").pop() || u;
|
|
84
|
+
return h("a", {
|
|
85
|
+
key: u,
|
|
86
|
+
href: u,
|
|
87
|
+
target: "_blank",
|
|
88
|
+
rel: "noopener noreferrer",
|
|
89
|
+
className: "dsh-github-issue-image-link",
|
|
90
|
+
title: u,
|
|
91
|
+
}, "🖼 " + (name.length > 42 ? name.slice(0, 42) + "…" : name));
|
|
92
|
+
}))
|
|
93
|
+
: null);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 单条评论卡片。
|
|
97
|
+
*
|
|
98
|
+
* 为什么提到模块顶层:函数体内定义的组件每次父级渲染都会产生新引用,
|
|
99
|
+
* React 按引用比较类型会卸载重建,CollapsibleBody 的展开状态随之重置。
|
|
100
|
+
* 这是 pr-modal 上已经踩过的坑,Issue 侧不走第二遍。
|
|
101
|
+
*/
|
|
102
|
+
function IssueCommentCard(props) {
|
|
103
|
+
var h = React.createElement;
|
|
104
|
+
var c = props.comment;
|
|
105
|
+
var author = c && c.author ? c.author.login : "ghost";
|
|
106
|
+
var hidden = Boolean(c && c.isMinimized);
|
|
107
|
+
return h("div", { className: "dsh-github-issue-comment-card" }, h("div", { className: "dsh-github-issue-comment-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap", minWidth: 0 } }, h("strong", null, "@" + author), c && c.authorAssociation && c.authorAssociation !== "NONE"
|
|
108
|
+
? h("span", { className: "dsh-github-badge dsh-badge-blue", style: { fontSize: "9px" } }, c.authorAssociation)
|
|
109
|
+
: null, c && c.includesCreatedEdit ? h("span", { style: { fontSize: "10px", opacity: 0.6 } }, "· 已编辑") : null, hidden
|
|
110
|
+
? h("span", {
|
|
111
|
+
className: "dsh-github-badge dsh-badge-red",
|
|
112
|
+
style: { fontSize: "9px" },
|
|
113
|
+
title: "GitHub 判定该评论被折叠(常见原因:垃圾信息 / 攻击性内容)",
|
|
114
|
+
}, "已隐藏" + (c.minimizedReason ? "(" + c.minimizedReason + ")" : ""))
|
|
115
|
+
: null), h("span", { style: { fontSize: "11px", opacity: 0.6, flexShrink: 0 } }, formatRelativeTime(c.createdAt))), h("div", { style: { padding: "10px 12px" } }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "ic-" + c.id })));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Issue 在线查看弹窗(详情 + 元数据 + 评论时间线)。
|
|
119
|
+
*
|
|
120
|
+
* 与 PrDetailModal 的差异:Issue 没有 diff 载荷,因此不做双栏文件区,
|
|
121
|
+
* 单列纵向滚动即可——多出来的宽度全部让给 Markdown 正文,可读性更好。
|
|
122
|
+
*/
|
|
123
|
+
export function IssueDetailModal(props) {
|
|
124
|
+
var issueNumber = props.issueNumber;
|
|
125
|
+
var repoNameWithOwner = props.repoNameWithOwner || "";
|
|
126
|
+
var cwd = props.cwd;
|
|
127
|
+
var onClose = props.onClose;
|
|
128
|
+
var showToast = props.showToast || (function () { });
|
|
129
|
+
// Hooks 必须先于任何 early return 无条件执行(见 pr-modal 的同类注释):
|
|
130
|
+
// 否则「issueNumber 由空变有值」时两次渲染的 Hook 数量不一致,直接崩。
|
|
131
|
+
// 注意 useState 与 useEffect 都必须在这个位置之前——early return 卡在两者
|
|
132
|
+
// 中间时,首帧(无编号)会跳过全部 effect,有编号的首帧却多出两个 effect,
|
|
133
|
+
// 同样违反「每次渲染 Hook 序列一致」。
|
|
134
|
+
var [data, setData] = React.useState(null);
|
|
135
|
+
var [loading, setLoading] = React.useState(true);
|
|
136
|
+
var [error, setError] = React.useState(null);
|
|
137
|
+
var [copied, setCopied] = React.useState(false);
|
|
138
|
+
React.useEffect(function () {
|
|
139
|
+
var onKeyDown = function (e) {
|
|
140
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
141
|
+
onClose();
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("keydown", onKeyDown);
|
|
144
|
+
return function () { window.removeEventListener("keydown", onKeyDown); };
|
|
145
|
+
}, [onClose]);
|
|
146
|
+
React.useEffect(function () {
|
|
147
|
+
if (!issueNumber) {
|
|
148
|
+
setLoading(false);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
setLoading(true);
|
|
152
|
+
setError(null);
|
|
153
|
+
var url = "/api/github/issue-detail?number=" +
|
|
154
|
+
encodeURIComponent(String(issueNumber)) +
|
|
155
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
156
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
157
|
+
fetch(url)
|
|
158
|
+
.then(async function (res) {
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
var errText = await res.text().catch(function () { return ""; });
|
|
161
|
+
var j = null;
|
|
162
|
+
try {
|
|
163
|
+
j = JSON.parse(errText);
|
|
164
|
+
}
|
|
165
|
+
catch (e) { }
|
|
166
|
+
throw new Error((j && j.error) || errText || ("服务响应异常 (" + res.status + ")"));
|
|
167
|
+
}
|
|
168
|
+
return res.json();
|
|
169
|
+
})
|
|
170
|
+
.then(function (json) {
|
|
171
|
+
setLoading(false);
|
|
172
|
+
if (json && json.ok && json.data) {
|
|
173
|
+
setData(json.data);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
setError((json && json.error) || "获取 Issue 详情失败");
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.catch(function (err) {
|
|
180
|
+
setLoading(false);
|
|
181
|
+
setError((err && err.message) || "网络请求异常");
|
|
182
|
+
});
|
|
183
|
+
}, [issueNumber, repoNameWithOwner, cwd]);
|
|
184
|
+
if (!issueNumber)
|
|
185
|
+
return null;
|
|
186
|
+
var h = React.createElement;
|
|
187
|
+
var copyLink = function (text) {
|
|
188
|
+
if (!text)
|
|
189
|
+
return;
|
|
190
|
+
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
191
|
+
navigator.clipboard
|
|
192
|
+
.writeText(text)
|
|
193
|
+
.then(function () {
|
|
194
|
+
setCopied(true);
|
|
195
|
+
showToast("✅ Issue 链接已复制到剪贴板");
|
|
196
|
+
setTimeout(function () { setCopied(false); }, 2000);
|
|
197
|
+
})
|
|
198
|
+
.catch(function () { });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var stateBadge = function (d) {
|
|
202
|
+
if (!d)
|
|
203
|
+
return null;
|
|
204
|
+
var isOpen = String(d.state).toUpperCase() === "OPEN";
|
|
205
|
+
return h("span", {
|
|
206
|
+
className: "dsh-github-badge " + (isOpen ? "dsh-badge-green" : "dsh-badge-red"),
|
|
207
|
+
style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 },
|
|
208
|
+
}, isOpen ? "OPEN" : "CLOSED");
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* 元数据条:只渲染真实存在的字段。
|
|
212
|
+
* 空的「无标签 / 无指派 / 无里程碑」占位没有信息量,只会把正文推得更远。
|
|
213
|
+
*/
|
|
214
|
+
var metaChips = function (d) {
|
|
215
|
+
var chips = [];
|
|
216
|
+
var reason = d.stateReason ? ISSUE_STATE_REASON_LABEL[String(d.stateReason).toUpperCase()] : "";
|
|
217
|
+
if (reason) {
|
|
218
|
+
chips.push(h("span", {
|
|
219
|
+
key: "reason",
|
|
220
|
+
className: "dsh-github-issue-chip",
|
|
221
|
+
title: "关闭原因 (stateReason): " + d.stateReason,
|
|
222
|
+
}, "✓ " + reason));
|
|
223
|
+
}
|
|
224
|
+
if (d.isPinned) {
|
|
225
|
+
chips.push(h("span", { key: "pin", className: "dsh-github-issue-chip" }, "📌 置顶"));
|
|
226
|
+
}
|
|
227
|
+
if (d.issueType) {
|
|
228
|
+
chips.push(h("span", { key: "type", className: "dsh-github-issue-chip" }, "🏷️ " + d.issueType));
|
|
229
|
+
}
|
|
230
|
+
(Array.isArray(d.labels) ? d.labels : []).forEach(function (l, i) {
|
|
231
|
+
chips.push(h("span", {
|
|
232
|
+
key: "label-" + i + "-" + l.name,
|
|
233
|
+
className: "dsh-github-issue-chip dsh-github-issue-label-chip",
|
|
234
|
+
style: {
|
|
235
|
+
background: labelRgba(l.color, 0.14),
|
|
236
|
+
borderColor: labelRgba(l.color, 0.45),
|
|
237
|
+
},
|
|
238
|
+
title: l.description || l.name,
|
|
239
|
+
}, h("span", {
|
|
240
|
+
className: "dsh-github-issue-label-dot",
|
|
241
|
+
style: { background: labelRgba(l.color, 0.9) },
|
|
242
|
+
}), l.name));
|
|
243
|
+
});
|
|
244
|
+
(Array.isArray(d.assignees) ? d.assignees : []).forEach(function (a, i) {
|
|
245
|
+
chips.push(h("span", { key: "asg-" + i, className: "dsh-github-issue-chip" }, "👤 " + a.login));
|
|
246
|
+
});
|
|
247
|
+
if (d.milestone && d.milestone.title) {
|
|
248
|
+
var dueText = d.milestone.dueOn ? formatDueDate(d.milestone.dueOn) : '';
|
|
249
|
+
chips.push(h("span", {
|
|
250
|
+
key: "milestone",
|
|
251
|
+
className: "dsh-github-issue-chip" + (dueText.indexOf('已逾期') >= 0 ? " dsh-github-issue-chip-overdue" : ""),
|
|
252
|
+
title: d.milestone.dueOn ? "截止 " + d.milestone.dueOn : "未设置截止日期",
|
|
253
|
+
}, "🏁 " + d.milestone.title + (dueText ? " · " + dueText : "")));
|
|
254
|
+
}
|
|
255
|
+
if (d.subIssues && Number(d.subIssues.total) > 0) {
|
|
256
|
+
chips.push(h("span", { key: "sub", className: "dsh-github-issue-chip" }, "🧩 子议题 " + d.subIssues.completed + "/" + d.subIssues.total));
|
|
257
|
+
}
|
|
258
|
+
(Array.isArray(d.linkedPrs) ? d.linkedPrs : []).forEach(function (p, i) {
|
|
259
|
+
chips.push(h("a", {
|
|
260
|
+
key: "pr-" + i,
|
|
261
|
+
className: "dsh-github-issue-chip dsh-github-issue-chip-link",
|
|
262
|
+
href: p.url,
|
|
263
|
+
target: "_blank",
|
|
264
|
+
rel: "noopener noreferrer",
|
|
265
|
+
title: "该 Issue 被此 Pull Request 关联/关闭",
|
|
266
|
+
}, "🔗 PR #" + p.number));
|
|
267
|
+
});
|
|
268
|
+
chips.push(h(IssueImageList, { key: "images", urls: d.images || [] }));
|
|
269
|
+
return chips.length > 0 ? h("div", { className: "dsh-github-issue-meta" }, ...chips) : null;
|
|
270
|
+
};
|
|
271
|
+
var bodyCard = function (d) {
|
|
272
|
+
return h("div", { className: "dsh-github-issue-body-card" }, h("div", { className: "dsh-github-issue-comment-header" }, h("strong", null, "@" + d.author.login), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, "提单说明 (Issue Body)")), h("div", { className: "dsh-github-issue-body-content" }, d.body
|
|
273
|
+
? h(CollapsibleBody, { text: d.body, defaultExpanded: true, idPrefix: "issue-body" })
|
|
274
|
+
: h("span", { style: { opacity: 0.5, fontStyle: "italic" } }, "(该 Issue 未填写正文)")));
|
|
275
|
+
};
|
|
276
|
+
return h("div", { className: "dsh-github-modal-overlay", onClick: onClose, style: { zIndex: 100000 } }, h("div", {
|
|
277
|
+
className: "dsh-github-modal-dialog dsh-github-issue-modal-dialog",
|
|
278
|
+
role: "dialog",
|
|
279
|
+
"aria-modal": "true",
|
|
280
|
+
"aria-label": "Issue #" + issueNumber + " 详情",
|
|
281
|
+
onClick: function (e) { e.stopPropagation(); },
|
|
282
|
+
},
|
|
283
|
+
// 头部
|
|
284
|
+
h("div", { className: "dsh-github-modal-header dsh-github-issue-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, renderOctocat(18, "#1f883d"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
|
|
285
|
+
style: {
|
|
286
|
+
margin: 0,
|
|
287
|
+
fontSize: "14px",
|
|
288
|
+
fontWeight: 600,
|
|
289
|
+
overflow: "hidden",
|
|
290
|
+
textOverflow: "ellipsis",
|
|
291
|
+
whiteSpace: "nowrap",
|
|
292
|
+
},
|
|
293
|
+
title: data ? "#" + data.number + " " + data.title : "Issue #" + issueNumber,
|
|
294
|
+
}, data ? "#" + data.number + " " + data.title : "Issue #" + issueNumber), stateBadge(data)), data
|
|
295
|
+
? h("div", {
|
|
296
|
+
style: {
|
|
297
|
+
fontSize: "11px",
|
|
298
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
299
|
+
display: "flex",
|
|
300
|
+
gap: "6px",
|
|
301
|
+
alignItems: "center",
|
|
302
|
+
flexWrap: "wrap",
|
|
303
|
+
},
|
|
304
|
+
}, h("span", null, "👤 @" + data.author.login), data.createdAt ? h("span", null, "· 提单于 " + formatRelativeTime(data.createdAt)) : null, data.closedAt ? h("span", null, "· 关闭于 " + formatRelativeTime(data.closedAt)) : null, repoNameWithOwner
|
|
305
|
+
? h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, repoNameWithOwner)
|
|
306
|
+
: null)
|
|
307
|
+
: null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data && data.url
|
|
308
|
+
? h("button", {
|
|
309
|
+
type: "button",
|
|
310
|
+
className: "dsh-github-btn-sm",
|
|
311
|
+
style: { padding: "4px 8px", fontSize: "11px" },
|
|
312
|
+
onClick: function () { copyLink(data.url); },
|
|
313
|
+
title: "复制该 Issue 网页链接",
|
|
314
|
+
}, copied ? "✓ 已复制" : "📋 复制链接")
|
|
315
|
+
: null, data && data.url
|
|
316
|
+
? h("a", {
|
|
317
|
+
href: data.url,
|
|
318
|
+
target: "_blank",
|
|
319
|
+
rel: "noopener noreferrer",
|
|
320
|
+
className: "dsh-github-btn-sm",
|
|
321
|
+
style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
|
|
322
|
+
title: "在系统默认浏览器中打开该 Issue",
|
|
323
|
+
}, "↗ 浏览器打开")
|
|
324
|
+
: null, h("button", {
|
|
325
|
+
type: "button",
|
|
326
|
+
"aria-label": "关闭 (Esc)",
|
|
327
|
+
style: {
|
|
328
|
+
background: "none",
|
|
329
|
+
border: "none",
|
|
330
|
+
cursor: "pointer",
|
|
331
|
+
fontSize: "18px",
|
|
332
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
333
|
+
padding: "0 6px",
|
|
334
|
+
},
|
|
335
|
+
onClick: onClose,
|
|
336
|
+
title: "关闭 (Esc)",
|
|
337
|
+
}, "✕"))),
|
|
338
|
+
// 主体
|
|
339
|
+
loading
|
|
340
|
+
? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Issue 详情与讨论…")
|
|
341
|
+
: error
|
|
342
|
+
? h("div", {
|
|
343
|
+
style: {
|
|
344
|
+
padding: "24px",
|
|
345
|
+
margin: "16px",
|
|
346
|
+
borderRadius: "8px",
|
|
347
|
+
background: "rgba(248,81,73,0.08)",
|
|
348
|
+
border: "1px solid rgba(248,81,73,0.3)",
|
|
349
|
+
},
|
|
350
|
+
}, h("div", { style: { fontWeight: 600, color: "#f85149", marginBottom: "6px" } }, "⚠️ 读取 Issue 详情失败"), h("div", { style: { fontSize: "12px", opacity: 0.85, lineHeight: 1.6 } }, error), repoNameWithOwner
|
|
351
|
+
? h("a", {
|
|
352
|
+
href: "https://github.com/" + repoNameWithOwner + "/issues/" + issueNumber,
|
|
353
|
+
target: "_blank",
|
|
354
|
+
rel: "noopener noreferrer",
|
|
355
|
+
className: "dsh-github-btn-sm",
|
|
356
|
+
style: { display: "inline-block", marginTop: "12px", padding: "4px 10px", fontSize: "11px", textDecoration: "none" },
|
|
357
|
+
}, "↗ 在浏览器中查看")
|
|
358
|
+
: null)
|
|
359
|
+
: data
|
|
360
|
+
? h("div", { className: "dsh-github-issue-conversation-pane" }, metaChips(data), bodyCard(data), h("div", { className: "dsh-github-issue-timeline-title" }, "💬 讨论与评论时间线 (" + (data.comments ? data.comments.length : 0) + ")"), data.comments && data.comments.length > 0
|
|
361
|
+
? data.comments.map(function (c) {
|
|
362
|
+
return h(IssueCommentCard, { key: c.id || c.createdAt, comment: c });
|
|
363
|
+
})
|
|
364
|
+
: h("div", { className: "dsh-github-issue-empty-timeline" }, "💬 暂无讨论与评论——这是一个还没有人回复的议题。"))
|
|
365
|
+
: null));
|
|
366
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 Markdown 解析与 React 渲染(零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须自己写:本插件没有前端 dependencies,bundle 机制只内联
|
|
5
|
+
* lib/ui/*.js 且不做 import 解析,因此 marked / markdown-it 都无法引入。
|
|
6
|
+
* 服务端的 src/renderers/* 只产出给模型看的 Markdown 文本,不能复用为 DOM。
|
|
7
|
+
*
|
|
8
|
+
* 设计取舍:
|
|
9
|
+
* - 只支持 GitHub PR/Issue 实际会用到的语法子集(标题/粗斜体/行内码/
|
|
10
|
+
* 围栏代码块/列表/引用/分隔线/链接)。表格、图片、内嵌 HTML 明确不做。
|
|
11
|
+
* - 无法识别的语法**降级为纯文本**,绝不抛错——弹窗是只读展示,
|
|
12
|
+
* 因为一段奇怪文本崩掉整块内容是不可接受的。
|
|
13
|
+
* - 全程 React.createElement,**从不使用 dangerouslySetInnerHTML**:
|
|
14
|
+
* 用户写的 <script> 会被当作纯文本渲染,天然免疫 XSS。
|
|
15
|
+
*/
|
|
16
|
+
export interface MdInline {
|
|
17
|
+
type: 'text' | 'code' | 'strong' | 'em' | 'link';
|
|
18
|
+
text?: string;
|
|
19
|
+
children?: MdInline[];
|
|
20
|
+
href?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MdListItem {
|
|
23
|
+
/** 原始标记:'-' / '*' / '1.' 等,用于区分任务列表 */
|
|
24
|
+
marker: string;
|
|
25
|
+
children: MdInline[];
|
|
26
|
+
}
|
|
27
|
+
export interface MdBlock {
|
|
28
|
+
type: 'heading' | 'paragraph' | 'code' | 'list' | 'quote' | 'hr';
|
|
29
|
+
level?: number;
|
|
30
|
+
lang?: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
items?: MdListItem[];
|
|
33
|
+
ordered?: boolean;
|
|
34
|
+
children?: MdInline[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 解析行内语法:`code` / **strong** / *em* / [text](url)。
|
|
38
|
+
*
|
|
39
|
+
* 用单遍扫描而非多次 replace 的原因:
|
|
40
|
+
* 行内代码里的内容**不应再被解析**(`**a**` 是字面量),
|
|
41
|
+
* 分段替换很容易把代码块内的星号也吃掉。手写扫描能保证优先级正确:
|
|
42
|
+
* 行内代码 > 链接 > 粗体 > 斜体。
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseInline(src: string): MdInline[];
|
|
45
|
+
/**
|
|
46
|
+
* 把 Markdown 源文本解析为块级 AST。
|
|
47
|
+
*
|
|
48
|
+
* 纯函数、无 I/O,可被单测直接覆盖——这是把「渲染正确性」从浏览器里
|
|
49
|
+
* 剥离出来断言的关键。任何异常输入都返回空数组,绝不抛错。
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseMarkdownBlocks(src: string): MdBlock[];
|
|
52
|
+
/**
|
|
53
|
+
* 把 AST 渲染为 React 元素数组。
|
|
54
|
+
*
|
|
55
|
+
* @param h React.createElement
|
|
56
|
+
* @param blocks parseMarkdownBlocks 的产物
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderMarkdownBlocks(h: any, blocks: MdBlock[]): any[];
|
|
59
|
+
/**
|
|
60
|
+
* 估算一段 Markdown 渲染后的行数,用于决定是否折叠。
|
|
61
|
+
*
|
|
62
|
+
* 为什么用估算而不是测量 DOM:getBoundingClientRect 需要 ref + 二次渲染,
|
|
63
|
+
* 在弹窗内会造成可见的布局抖动。按换行数 + 长行折行数估算已足够准确,
|
|
64
|
+
* 且成本为零。阈值取保守值——宁可多折一次,也不要漏折导致内容淹没页面。
|
|
65
|
+
*/
|
|
66
|
+
export declare function estimateContentLines(src: string, charsPerLine?: number): number;
|