dsh-github-flow 0.1.5 → 0.1.7
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 +12 -7
- package/README.md +12 -7
- package/docs/architecture.md +18 -3
- package/lib/api/routes.js +149 -10
- package/lib/client/panel.js +28 -1
- package/lib/client.js +424 -13
- package/lib/executor.d.ts +8 -5
- package/lib/executor.js +58 -14
- package/lib/tools/api.js +2 -1
- package/lib/tools/issue.js +13 -8
- package/lib/tools/pr.js +14 -9
- package/lib/tools/repo.js +6 -4
- package/lib/tools/run.js +12 -7
- package/lib/types.d.ts +53 -9
- package/lib/workspace.d.ts +6 -3
- package/lib/workspace.js +24 -9
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -23,16 +23,21 @@
|
|
|
23
23
|
1. **Zero-Configuration Authentication**:
|
|
24
24
|
- Seamlessly reuses existing host credentials from `gh auth login` (OAuth tokens / system Keyrings) without exposing plain-text Personal Access Tokens (PATs) in DSH.
|
|
25
25
|
2. **Deep Workspace Affinity & Zero-Config Detection**:
|
|
26
|
-
- Automatically scopes commands to the active session workspace by natively reading local `.git/config` and mapping DSH `workspace.json`.
|
|
26
|
+
- Automatically scopes commands to the active session workspace by natively reading local `.git/config` and mapping DSH `workspace.json`. Enhanced with Git repository validation, parent-child directory auto-penetration, and session ID propagation to eliminate misidentification, so neither models nor humans need to supply redundant `owner/repo` arguments.
|
|
27
27
|
3. **High Signal-to-Noise Ratio & Token Budget Protection**:
|
|
28
28
|
- Consolidates over 70+ scattered micro-tools into **5 focused domain tools**.
|
|
29
29
|
- Queries use `--json <fields>` projections by default; long diffs and failed CI logs are safely truncated (default 24KB) to protect the model's context window.
|
|
30
30
|
- All tool outputs are defensively normalized into root objects to strictly comply with DSH Tool Gateway schema validation.
|
|
31
|
-
4. **Dual Native Web UI Views &
|
|
32
|
-
- **Global Cockpit** (`sidebar.panellist`, Order 20): Account-wide repository overview, local workspace matrix, and
|
|
33
|
-
- **Right Sidebar Panel** (`sidebarRightTabs`, Order 40): Shows current repository status, PR list with CI check pills, and
|
|
31
|
+
4. **Dual Native Web UI Views & Multi-Dimensional Task Governance**:
|
|
32
|
+
- **Global Cockpit** (`sidebar.panellist`, Order 20): Account-wide repository overview, local workspace matrix mapping, and a reconstructed **3-dimensional Issue aggregation engine** ("User Reported" / "Assigned to Me" / "Created by Me") featuring 4-state filter pills and role badges.
|
|
33
|
+
- **Right Sidebar Panel** (`sidebarRightTabs`, Order 40): Shows current repository status, PR list with CI check pills, **active workspace issues list** (with author attribution, role badges, labels, and external links), Actions pipelines, and quick action shortcuts (Open Repo, View All Issues, View All PRs, Create PR, New Issue) with zero popup dependencies.
|
|
34
34
|
- **Native Light/Dark Dual-Theme Adaptation**: Deeply aligned with DSH official DSW (DeepSeek Web) design tokens, seamlessly reacting to `body[data-ds-dark-theme]` without lag or flickering.
|
|
35
|
-
5. **
|
|
35
|
+
5. **Robust Environment Diagnosis & Three-State Setup Engine**:
|
|
36
|
+
- Zero-guesswork onboarding: Accurately differentiates `NOT_INSTALLED` (`Missing`), `NOT_LOGGED_IN` (`Required`), and `AUTHENTICATED` (`Active`).
|
|
37
|
+
- OS-aware installation guides: Auto-detects platform (Windows/macOS/Linux) and suggests native package manager commands (`winget install --id GitHub.cli`, `brew install gh`) with official installer links.
|
|
38
|
+
- Interactive helpers: One-click command copying and security-whitelisted terminal launcher.
|
|
39
|
+
- **Focus Auto-Recovery**: Automatically detects authorization completion when returning to the DSH window, transitioning to full workspace view without manual page reloads.
|
|
40
|
+
6. **Standard Cordis Microkernel Compliance**:
|
|
36
41
|
- Exposes native `GitHubService` (`ctx.github`) for ecosystem extensibility;
|
|
37
42
|
- Strongly typed runtime validation via Standard Schema (`Config`);
|
|
38
43
|
- Domain event broadcasting (`github/pr:create`, `github/issue:create`).
|
|
@@ -42,14 +47,14 @@
|
|
|
42
47
|
## 📸 UI Previews
|
|
43
48
|
|
|
44
49
|
### Global Cockpit (Main Navigation)
|
|
45
|
-
Overview of account-wide repositories, local workspace matrix mapping, and
|
|
50
|
+
Overview of account-wide repositories, local workspace matrix mapping, and multi-dimensional categorized issues (supports filter pills across All, User Reported, Assigned, and Created with status badges):
|
|
46
51
|
|
|
47
52
|
<p align="center">
|
|
48
53
|
<img src="./docs/images/1.png" alt="GitHub Global Cockpit" width="100%" />
|
|
49
54
|
</p>
|
|
50
55
|
|
|
51
56
|
### Session Right Sidebar Panel (In-Session Collaboration)
|
|
52
|
-
Automatically detects current workspace repository, displaying live PR checks, Actions pipelines, and quick action shortcuts:
|
|
57
|
+
Automatically detects current workspace repository, displaying live PR checks, workspace open issues list, Actions pipelines, and quick action shortcuts:
|
|
53
58
|
|
|
54
59
|
<p align="center">
|
|
55
60
|
<img src="./docs/images/2.png" alt="Session Right Sidebar Panel" width="100%" />
|
package/README.md
CHANGED
|
@@ -23,16 +23,21 @@
|
|
|
23
23
|
1. **零鉴权配置,开箱即用**:
|
|
24
24
|
- 自动复用宿主机已通过 `gh auth login` 认证的登录凭据(OAuth Token / 系统 Keyring),无需在 DSH 中存储任何明文 GitHub PAT。
|
|
25
25
|
2. **工作区目录深度亲和与零参数探测 (CWD & Git Auto-Binding)**:
|
|
26
|
-
- 自动绑定当前会话所在的物理工作区目录,原生直读本地 `.git/config` 并映射 `workspace.json
|
|
26
|
+
- 自动绑定当前会话所在的物理工作区目录,原生直读本地 `.git/config` 并映射 `workspace.json`;具备 Git 强校验、父子级目录智能穿透与会话 ID 上下文透传机制,杜绝非 Git 目录误选,无需 Agent 或人类手动传参 `owner/repo`,零参数直接识别关联仓库。
|
|
27
27
|
3. **高信噪比与 Token 防溢出治理**:
|
|
28
28
|
- 拒绝 70+ 个碎片化微型工具撑爆上下文,收敛聚合为 **5 大领域核心 Tool**。
|
|
29
29
|
- 所有查询原生采用 `--json <fields>` 按需返回;对 `diff`、`log_failed` 设置安全截断保护(默认 24KB),防止模型上下文窗口溢出崩溃。
|
|
30
30
|
- 工具层已通过防御性输出包装,列表与标量 100% 契合 DSH 工具网关模式约束。
|
|
31
|
-
4. **原生 Web UI
|
|
32
|
-
- **全局驾驶舱**(主导航栏 `sidebar.panellist`,Order 20
|
|
33
|
-
- **右侧栏工作区面板**(`sidebarRightTabs`,Order 40
|
|
31
|
+
4. **原生 Web UI 双重视图与多维任务治理**:
|
|
32
|
+
- **全局驾驶舱**(主导航栏 `sidebar.panellist`,Order 20):透视账号全局仓库、本地工作区矩阵联动状态;重构并支持**三维 Issue 智能聚合**(「📥 用户提单」/「🎯 分配待办」/「✍️ 我发起的」),提供四态响应式药丸过滤与身份徽章;
|
|
33
|
+
- **右侧栏工作区面板**(`sidebarRightTabs`,Order 40):自动绑定当前工程,常驻呈现 PR 门禁状态、**当前仓库 Open Issues 任务列表**(带提出人、分类徽章、标签与直达链接)、CI 构建流水线,以及快捷黄金网格(浏览器打开、查看所有 Issues、查看所有 PRs、快速创建 PR、新建 Issue),零弹窗依赖;
|
|
34
34
|
- **全场景双模式原生自适应**:深度契合 DSH 官方 DSW(DeepSeek Web)设计变量规范,通过 `body[data-ds-dark-theme]` 驱动浅色与深色主题实时零延迟平滑切换。
|
|
35
|
-
5.
|
|
35
|
+
5. **健全的环境诊断与三态就绪引导 (Three-State Setup Engine)**:
|
|
36
|
+
- 告别空状态困惑:精准建立「未安装 CLI (`Missing`)」➜「已安装未登录 (`Required`)」➜「已认证就绪 (`Active`)」三态状态机;
|
|
37
|
+
- 自动识别宿主机操作系统(Windows/macOS/Linux),按平台提供包管理器安装命令(Windows 下 `winget install --id GitHub.cli`、macOS 下 `brew install gh`)与官方安装包直达;
|
|
38
|
+
- 支持一键复制命令与安全白名单系统终端一键唤起;
|
|
39
|
+
- 具备**窗口焦点自愈(Focus Auto-Recovery)**机制:切到外部终端或浏览器授权完毕后,切回 DSH 界面自动探测刷新,无感恢复。
|
|
40
|
+
6. **符合 Cordis 官方微内核架构**:
|
|
36
41
|
- 暴露原生 `GitHubService`(`ctx.github`),允许生态其他插件复用;
|
|
37
42
|
- 具备符合 Standard Schema 规范的运行时配置校验(`Config`);
|
|
38
43
|
- 支持领域事件系统(`github/pr:create`, `github/issue:create`)。
|
|
@@ -42,14 +47,14 @@
|
|
|
42
47
|
## 📸 界面预览
|
|
43
48
|
|
|
44
49
|
### 全局全景驾驶舱 (Global Cockpit)
|
|
45
|
-
透视账号云端所有 GitHub 仓库、本地工作区矩阵联动状态,以及待办 PR
|
|
50
|
+
透视账号云端所有 GitHub 仓库、本地工作区矩阵联动状态,以及待办 PR 和多维分类 Issues(支持用户提单、指派待办、我发起的多维药丸切换与状态徽章):
|
|
46
51
|
|
|
47
52
|
<p align="center">
|
|
48
53
|
<img src="./docs/images/1.png" alt="GitHub 全景驾驶舱" width="100%" />
|
|
49
54
|
</p>
|
|
50
55
|
|
|
51
56
|
### 会话右侧栏工作区面板 (Session Workspace Panel)
|
|
52
|
-
自动绑定当前工程关联仓库,常驻呈现 PR
|
|
57
|
+
自动绑定当前工程关联仓库,常驻呈现 PR 状态药丸、当前仓库 Open Issues 任务列表、Actions 构建流水线与快捷操作网格:
|
|
53
58
|
|
|
54
59
|
<p align="center">
|
|
55
60
|
<img src="./docs/images/2.png" alt="右侧栏工作区面板" width="100%" />
|
package/docs/architecture.md
CHANGED
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
│ │ 全局驾驶舱 (Main Slot) │ │ 右侧栏面板 (Right Sidebar) │ │
|
|
42
42
|
│ │ - 账号全局所有 GitHub 仓库 │ │ - 当前会话工作区关联仓库 │ │
|
|
43
43
|
│ │ - 本地工作区矩阵透视 │ │ - PR 列表与 CI Checks 药丸 │ │
|
|
44
|
-
│ │ -
|
|
44
|
+
│ │ - 三维待办 Issues (药丸筛选) │ │ - 仓库 Open Issues 任务列表│ │
|
|
45
|
+
│ │ - 用户提单 / 分配待办 / 我发起 │ │ - 快捷操作网格 (直达入口) │ │
|
|
45
46
|
│ └──────────────────────────────────┘ └─────────────────────────────┘ │
|
|
46
47
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
47
48
|
```
|
|
@@ -65,11 +66,25 @@
|
|
|
65
66
|
- 对长文本输出(如大型 PR 的完整 `diff`、CI 失败步骤日志)设置安全截断阈值(默认 24KB),保留头尾关键上下文并添加显式截断标记,杜绝撑爆模型上下文;
|
|
66
67
|
- 所有 Agent 工具的输出统一经过 `normalizeOutput` 归一化,列表自动打包为 `{ items, count }`,标量打包为 `{ result }`,100% 严丝合缝契合 DSH 工具网关模式约束。
|
|
67
68
|
|
|
68
|
-
### 2.4
|
|
69
|
+
### 2.4 原生浅色/深色主题自适应与快捷操作网格
|
|
69
70
|
- 全面对齐 DSH 官方 DSW 语义设计变量,基于 `body[data-ds-dark-theme]` 驱动无闪烁双模式切换;
|
|
70
|
-
-
|
|
71
|
+
- 右侧栏快捷操作栏彻底废弃失效的 Electron `prompt()` 阻塞弹窗,全面采用官方直达模式(查看所有 Issues、查看 PRs、Compare / New Issue),享有 100% 官方富文本体验。
|
|
71
72
|
|
|
72
73
|
### 2.5 Cordis 微内核标准规范
|
|
73
74
|
- **服务依赖就绪**:声明 `['tools', 'commands', 'webServer', 'workspaceRegistry']`,确保 Web 路由与命令服务在环境就绪后激活;
|
|
74
75
|
- **生命周期 Effect 闭环**:所有 HTTP 路由与命令注册均持有 Disposer 清理函数,插件热重载或重新挂载时自动彻底注销,零内存泄漏;
|
|
75
76
|
- **配置自校验与服务暴露**:基于 Standard Schema 规范实现强类型校验与默认值注入,通过 `ctx.provide('github', executor)` 向生态插件暴露单例服务。
|
|
77
|
+
|
|
78
|
+
### 2.6 三态环境诊断与交互引导状态机 (Setup & Authentication Lifecycle Guard)
|
|
79
|
+
- **三态状态模型 (Three-State Machine)**:将环境就绪判定严格划分为 `NOT_INSTALLED`(未安装 CLI)、`NOT_LOGGED_IN`(已安装未登录)、`AUTHENTICATED`(已认证)三态,彻底消除“把未安装当作未登录”的状态屏蔽隐患;
|
|
80
|
+
- **平台自适应安装策略**:后端探测到 `ENOENT` 时根据 `process.platform` 组装对应平台的安装命令(Windows: `winget`,macOS: `brew`,Linux: `apt/dnf`)与官方安装包地址;
|
|
81
|
+
- **安全辅助与白名单终端唤起**:Web 端提供一键命令复制,后端 `/api/github/action` 提供 `open_terminal` 安全白名单唤起接口(只允许预设命令),避免在前端模拟复杂交互式 PTY 的脆弱性;
|
|
82
|
+
- **焦点感知自愈 (Focus Auto-Recovery)**:前端挂载 `window.focus` 事件监听,用户在外部浏览器或终端完成认证后切回 DSH 界面,自动静默重新探测并无缝更新 UI,实现零摩擦闭环体验。
|
|
83
|
+
|
|
84
|
+
### 2.7 多维 Issue 身份判定与并发聚合引擎 (Multi-Dimensional Issue Engine)
|
|
85
|
+
- **多重身份决策矩阵**:基于当前登录用户 `currentUser`(`auth.user`)对 Issue 执行身份归类:
|
|
86
|
+
- `reported`(用户提单):`repoOwner === currentUser && author != currentUser`(外部访客在本人维护的仓库中提出的反馈);
|
|
87
|
+
- `assigned`(分配待办):`assignees.includes(currentUser)`(指派给本人的任务,跨仓库通用);
|
|
88
|
+
- `created`(我发起的):`author === currentUser`(本人亲自创建的 Issue)。
|
|
89
|
+
- **三路并发搜索与 URL 去重**:后端在 `/api/github/global-overview` 中通过 `Promise.all` 同时发起 `--owner=@me`、`--assignee=@me`、`--author=@me` 检索,按 URL 唯一性合并去重并按最后更新时间统一降序排列;
|
|
90
|
+
- **双端视图无缝打标**:驾驶舱提供四态响应式药丸筛选(全部/用户提单/指派待办/我发起的),右侧栏直接常驻展现当前工作区所属 Issue 任务清单与状态徽章。
|
package/lib/api/routes.js
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { TtlCache } from '../cache.js';
|
|
3
3
|
import { mapLimit, tryReadWorkspaces, findWorkspacePathBySession, findMostRecentWorkspacePath, hasGitDir } from '../workspace.js';
|
|
4
|
+
/**
|
|
5
|
+
* 依据当前登录账号判定 Issue 的归属身份维度 (可多选):
|
|
6
|
+
* - reported: 用户提起的 (属于当前账号名下仓库,但作者不是本人)
|
|
7
|
+
* - assigned: 被分配的 (指派列表中包含当前账号)
|
|
8
|
+
* - created: 我创建的 (作者是当前账号)
|
|
9
|
+
*
|
|
10
|
+
* 为什么这样划分:
|
|
11
|
+
* 在真实工程实践中,Maintainer 不仅要关注自己创建的任务,更要对社区/用户提交的 Bug 反馈负责,
|
|
12
|
+
* 同时还要处理他人指派的待办任务。这三个维度互补且可能重叠。
|
|
13
|
+
*/
|
|
14
|
+
function resolveIssueCategories(issue, currentUser, explicitRepoOwner) {
|
|
15
|
+
const categories = [];
|
|
16
|
+
if (!currentUser)
|
|
17
|
+
return categories;
|
|
18
|
+
const currentLower = currentUser.toLowerCase();
|
|
19
|
+
const authorLogin = issue.author?.login?.toLowerCase();
|
|
20
|
+
const isAuthor = Boolean(authorLogin && authorLogin === currentLower);
|
|
21
|
+
const isAssignee = Array.isArray(issue.assignees) &&
|
|
22
|
+
issue.assignees.some((a) => a.login?.toLowerCase() === currentLower);
|
|
23
|
+
// 仓库 owner 判定:优先从 repository.nameWithOwner 提取,兜底使用 explicitRepoOwner
|
|
24
|
+
let repoOwner = explicitRepoOwner?.toLowerCase();
|
|
25
|
+
if (!repoOwner && issue.repository?.nameWithOwner) {
|
|
26
|
+
repoOwner = issue.repository.nameWithOwner.split('/')[0]?.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
const isOwnerRepo = Boolean(repoOwner && repoOwner === currentLower);
|
|
29
|
+
if (isAuthor)
|
|
30
|
+
categories.push('created');
|
|
31
|
+
if (isAssignee)
|
|
32
|
+
categories.push('assigned');
|
|
33
|
+
// 如果是当前用户维护的仓库,且作者不是本人,则明确归为「用户提起的」
|
|
34
|
+
if (isOwnerRepo && !isAuthor)
|
|
35
|
+
categories.push('reported');
|
|
36
|
+
return categories;
|
|
37
|
+
}
|
|
38
|
+
function calculateIssueStats(issues) {
|
|
39
|
+
let reported = 0;
|
|
40
|
+
let assigned = 0;
|
|
41
|
+
let created = 0;
|
|
42
|
+
for (const item of issues) {
|
|
43
|
+
const cats = item.categories || [];
|
|
44
|
+
if (cats.includes('reported'))
|
|
45
|
+
reported++;
|
|
46
|
+
if (cats.includes('assigned'))
|
|
47
|
+
assigned++;
|
|
48
|
+
if (cats.includes('created'))
|
|
49
|
+
created++;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
total: issues.length,
|
|
53
|
+
reported,
|
|
54
|
+
assigned,
|
|
55
|
+
created,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
4
58
|
/** path.basename 在空串/异常输入上会抛错,工作区路径来自磁盘文件,必须防御 */
|
|
5
59
|
function basenameSafe(p) {
|
|
6
60
|
try {
|
|
@@ -125,16 +179,27 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
125
179
|
]);
|
|
126
180
|
if (prsRes.ok && Array.isArray(prsRes.data))
|
|
127
181
|
pullRequests = prsRes.data;
|
|
128
|
-
if (issuesRes.ok && Array.isArray(issuesRes.data))
|
|
129
|
-
|
|
182
|
+
if (issuesRes.ok && Array.isArray(issuesRes.data)) {
|
|
183
|
+
const currentUser = auth.user;
|
|
184
|
+
const repoOwner = repo?.owner || repo?.nameWithOwner?.split('/')[0];
|
|
185
|
+
issues = issuesRes.data.map((item) => {
|
|
186
|
+
const categories = resolveIssueCategories(item, currentUser, repoOwner);
|
|
187
|
+
return {
|
|
188
|
+
...item,
|
|
189
|
+
categories,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
130
193
|
if (runsRes.ok && Array.isArray(runsRes.data))
|
|
131
194
|
runs = runsRes.data;
|
|
132
195
|
}
|
|
196
|
+
const issueStats = calculateIssueStats(issues);
|
|
133
197
|
const freshData = {
|
|
134
198
|
auth,
|
|
135
199
|
repo: repo || undefined,
|
|
136
200
|
pullRequests,
|
|
137
201
|
issues,
|
|
202
|
+
issueStats,
|
|
138
203
|
runs,
|
|
139
204
|
lastUpdated: new Date().toISOString(),
|
|
140
205
|
};
|
|
@@ -212,6 +277,41 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
212
277
|
sendJson(res, 200, { ok: true, message: '已在浏览器打开' });
|
|
213
278
|
break;
|
|
214
279
|
}
|
|
280
|
+
case 'open_terminal': {
|
|
281
|
+
const command = typeof payload.command === 'string' ? payload.command.trim() : 'gh auth login';
|
|
282
|
+
// 严格白名单过滤,严防任意命令注入
|
|
283
|
+
const allowedCommands = [
|
|
284
|
+
'gh auth login',
|
|
285
|
+
'winget install --id GitHub.cli',
|
|
286
|
+
'brew install gh',
|
|
287
|
+
'sudo apt install gh',
|
|
288
|
+
'sudo dnf install gh',
|
|
289
|
+
];
|
|
290
|
+
if (!allowedCommands.includes(command)) {
|
|
291
|
+
throw new Error(`不支持唤起该指令: ${command}`);
|
|
292
|
+
}
|
|
293
|
+
const { execFile } = await import('node:child_process');
|
|
294
|
+
let bin = 'cmd';
|
|
295
|
+
let args = [];
|
|
296
|
+
if (process.platform === 'win32') {
|
|
297
|
+
bin = 'cmd';
|
|
298
|
+
args = ['/c', 'start', 'powershell', '-NoExit', '-Command', command];
|
|
299
|
+
}
|
|
300
|
+
else if (process.platform === 'darwin') {
|
|
301
|
+
bin = 'osascript';
|
|
302
|
+
args = ['-e', `tell application "Terminal" to do script "${command}"`, '-e', 'tell application "Terminal" to activate'];
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
bin = 'x-terminal-emulator';
|
|
306
|
+
args = ['-e', command];
|
|
307
|
+
}
|
|
308
|
+
execFile(bin, args, (err) => {
|
|
309
|
+
if (err)
|
|
310
|
+
console.warn('[dsh-github-flow] 唤起终端失败:', err?.message || err);
|
|
311
|
+
});
|
|
312
|
+
sendJson(res, 200, { ok: true, message: '已请求唤起系统终端' });
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
215
315
|
default:
|
|
216
316
|
sendJson(res, 400, { ok: false, message: `未知的操作类型: ${payload.action}` });
|
|
217
317
|
}
|
|
@@ -244,16 +344,23 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
244
344
|
let userRepos = [];
|
|
245
345
|
let myPrs = [];
|
|
246
346
|
let myIssues = [];
|
|
347
|
+
let issueStats = { total: 0, reported: 0, assigned: 0, created: 0 };
|
|
247
348
|
let workspaceMatrix = [];
|
|
248
349
|
let workspaceError;
|
|
249
350
|
if (auth.loggedIn) {
|
|
250
|
-
// 并发执行:1. 账号下仓库列表 2. 个人 PRs 3.
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
351
|
+
// 并发执行:1. 账号下仓库列表 2. 个人 PRs 3. 三维 Issue 检索(名下仓库用户提单 + 指派待办 + 我创建的)
|
|
352
|
+
// 为什么三路并发搜索:
|
|
353
|
+
// - --owner=@me: 捕获用户/访客在当前用户所有仓库下提的 Issue (如反馈 Bug、Feature Request),解决外部提单看不见的问题
|
|
354
|
+
// - --assignee=@me: 捕获团队成员或自己指派给当前账号的待办任务 (含跨组织/第三方项目)
|
|
355
|
+
// - --author=@me: 捕获当前账号亲自发起的任务与向外部提起的议题
|
|
356
|
+
// 3 个轻量进程与 repo/prs 同步 spawn,经 OS 并发调度实测仅耗时 3 秒出头,完全满足 8 秒超时门限。
|
|
357
|
+
const issueFields = 'number,title,repository,author,assignees,labels,url,updatedAt';
|
|
358
|
+
const [reposRes, prsRes, ownerIssuesRes, assignedIssuesRes, authorIssuesRes] = await Promise.all([
|
|
254
359
|
executor.run(['repo', 'list', '--limit', '20', '--json', 'name,nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,updatedAt,url'], { timeoutMs: 8_000 }),
|
|
255
360
|
executor.run(['search', 'prs', '--author=@me', '--state=open', '--limit', '10', '--json', 'number,title,repository,url,updatedAt'], { timeoutMs: 8_000 }),
|
|
256
|
-
executor.run(['search', 'issues', '--
|
|
361
|
+
executor.run(['search', 'issues', '--owner=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
|
|
362
|
+
executor.run(['search', 'issues', '--assignee=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
|
|
363
|
+
executor.run(['search', 'issues', '--author=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
|
|
257
364
|
]);
|
|
258
365
|
if (reposRes.ok && Array.isArray(reposRes.data)) {
|
|
259
366
|
userRepos = reposRes.data.map((r) => ({
|
|
@@ -270,9 +377,40 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
270
377
|
if (prsRes.ok && Array.isArray(prsRes.data)) {
|
|
271
378
|
myPrs = prsRes.data;
|
|
272
379
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
380
|
+
// 多源 Issues 聚合、按 URL 唯一去重并排序
|
|
381
|
+
const issueMap = new Map();
|
|
382
|
+
const collectIssues = (res) => {
|
|
383
|
+
if (res.ok && Array.isArray(res.data)) {
|
|
384
|
+
for (const item of res.data) {
|
|
385
|
+
if (item.url && !issueMap.has(item.url)) {
|
|
386
|
+
issueMap.set(item.url, item);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
collectIssues(ownerIssuesRes);
|
|
392
|
+
collectIssues(assignedIssuesRes);
|
|
393
|
+
collectIssues(authorIssuesRes);
|
|
394
|
+
const rawList = Array.from(issueMap.values());
|
|
395
|
+
// 统一按最后更新时间降序排列
|
|
396
|
+
rawList.sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
|
|
397
|
+
const currentUser = auth.user;
|
|
398
|
+
myIssues = rawList.map((raw) => {
|
|
399
|
+
const categories = resolveIssueCategories(raw, currentUser);
|
|
400
|
+
return {
|
|
401
|
+
number: raw.number,
|
|
402
|
+
title: raw.title || '',
|
|
403
|
+
repository: raw.repository || { nameWithOwner: '' },
|
|
404
|
+
author: raw.author ? { login: raw.author.login } : undefined,
|
|
405
|
+
assignees: Array.isArray(raw.assignees) ? raw.assignees.map((a) => ({ login: a.login })) : [],
|
|
406
|
+
labels: Array.isArray(raw.labels) ? raw.labels.map((l) => ({ name: l.name, color: l.color })) : [],
|
|
407
|
+
url: raw.url || '',
|
|
408
|
+
updatedAt: raw.updatedAt || '',
|
|
409
|
+
state: raw.state || 'OPEN',
|
|
410
|
+
categories,
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
issueStats = calculateIssueStats(myIssues);
|
|
276
414
|
}
|
|
277
415
|
// 解析本地所有工作区与 GitHub 关联矩阵
|
|
278
416
|
const wsRead = tryReadWorkspaces();
|
|
@@ -301,6 +439,7 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
301
439
|
workspaceMatrix,
|
|
302
440
|
myPrs,
|
|
303
441
|
myIssues,
|
|
442
|
+
issueStats,
|
|
304
443
|
lastUpdated: new Date().toISOString(),
|
|
305
444
|
...(workspaceError ? { workspaceError } : {}),
|
|
306
445
|
};
|
package/lib/client/panel.js
CHANGED
|
@@ -62,6 +62,33 @@ export function createGithubPanelComponent() {
|
|
|
62
62
|
: h('div', { className: 'dsh-github-list' }, prs.map((pr) => h('div', { key: pr.number, className: 'dsh-github-item' }, h('div', { className: 'dsh-github-item-main' }, h('a', { className: 'dsh-github-item-title', href: pr.url, target: '_blank' }, `#${pr.number} ${pr.title}`), h('div', { className: 'dsh-github-item-meta' }, h('span', null, `由 ${pr.author?.login || '未知'} 提交`), h('span', null, `${pr.headRefName} → ${pr.baseRefName}`), pr.mergeable === 'CONFLICTING'
|
|
63
63
|
? h('span', { className: 'dsh-github-badge dsh-badge-red' }, '冲突 Conflicting')
|
|
64
64
|
: h('span', { className: 'dsh-github-badge dsh-badge-green' }, '可合并 Mergeable'))), h('div', { className: 'dsh-github-item-actions' }, h('a', { className: 'dsh-github-btn-sm', href: pr.url, target: '_blank', style: { textDecoration: 'none' } }, '在 GitHub 打开'))))));
|
|
65
|
+
// Issues 区域
|
|
66
|
+
const issuesSectionEl = h('div', { className: 'dsh-github-section' }, h('div', { className: 'dsh-github-section-header' }, h('h3', null, `📋 仓库 Issues (${issues.length})`), repo?.url
|
|
67
|
+
? h('a', {
|
|
68
|
+
href: `${repo.url}/issues`,
|
|
69
|
+
target: '_blank',
|
|
70
|
+
style: { fontSize: '11px', color: 'var(--dsw-alias-link, #0969da)', textDecoration: 'none' },
|
|
71
|
+
}, '全部 ↗')
|
|
72
|
+
: null), issues.length === 0
|
|
73
|
+
? h('div', { className: 'dsh-github-empty' }, '当前无开放中的 Issue')
|
|
74
|
+
: h('div', { className: 'dsh-github-list' }, issues.map((issue) => {
|
|
75
|
+
const cats = issue.categories || [];
|
|
76
|
+
const badges = [];
|
|
77
|
+
if (cats.includes('reported')) {
|
|
78
|
+
badges.push(h('span', { key: 'rep', className: 'dsh-github-badge dsh-badge-purple' }, '用户反馈'));
|
|
79
|
+
}
|
|
80
|
+
if (cats.includes('assigned')) {
|
|
81
|
+
badges.push(h('span', { key: 'asn', className: 'dsh-github-badge dsh-badge-blue' }, '已分配'));
|
|
82
|
+
}
|
|
83
|
+
if (cats.includes('created')) {
|
|
84
|
+
badges.push(h('span', { key: 'crt', className: 'dsh-github-badge dsh-badge-green' }, '我发起的'));
|
|
85
|
+
}
|
|
86
|
+
return h('div', { key: issue.number, className: 'dsh-github-item' }, h('div', { className: 'dsh-github-item-main' }, h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, h('a', { className: 'dsh-github-item-title', href: issue.url, target: '_blank' }, `#${issue.number} ${issue.title}`), ...badges), h('div', { className: 'dsh-github-item-meta' }, h('span', null, `由 ${issue.author?.login || '未知'} 提出`), issue.assignees && issue.assignees.length > 0
|
|
87
|
+
? h('span', null, `指派: ${issue.assignees.map((a) => a.login).join(', ')}`)
|
|
88
|
+
: null, issue.labels && issue.labels.length > 0
|
|
89
|
+
? issue.labels.map((lbl) => h('span', { key: lbl.name, className: 'dsh-github-badge dsh-badge-yellow' }, lbl.name))
|
|
90
|
+
: null)), h('div', { className: 'dsh-github-item-actions' }, h('a', { className: 'dsh-github-btn-sm', href: issue.url, target: '_blank', style: { textDecoration: 'none' } }, '在 GitHub 打开')));
|
|
91
|
+
})));
|
|
65
92
|
// GitHub Actions 区域
|
|
66
93
|
const runsSectionEl = h('div', { className: 'dsh-github-section' }, h('div', { className: 'dsh-github-section-header' }, h('h3', null, `⚡ GitHub Actions 最近流水线 (${runs.length})`)), runs.length === 0
|
|
67
94
|
? h('div', { className: 'dsh-github-empty' }, '暂无工作流运行记录')
|
|
@@ -70,6 +97,6 @@ export function createGithubPanelComponent() {
|
|
|
70
97
|
: r.conclusion === 'failure'
|
|
71
98
|
? h('span', { className: 'dsh-github-badge dsh-badge-red' }, '失败 Failure')
|
|
72
99
|
: h('span', { className: 'dsh-github-badge dsh-badge-yellow' }, r.status || '进行中'))), h('div', { className: 'dsh-github-item-actions' }, h('a', { className: 'dsh-github-btn-sm', href: r.url, target: '_blank', style: { textDecoration: 'none' } }, '查看流水线'))))));
|
|
73
|
-
return h('div', { className: 'dsh-github-panel' }, headerEl, cardsEl, prsSectionEl, runsSectionEl);
|
|
100
|
+
return h('div', { className: 'dsh-github-panel' }, headerEl, cardsEl, prsSectionEl, issuesSectionEl, runsSectionEl);
|
|
74
101
|
};
|
|
75
102
|
}
|