dsh-github-flow 0.1.6 → 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 +6 -6
- package/README.md +6 -6
- package/docs/architecture.md +12 -3
- package/lib/api/routes.js +114 -10
- package/lib/client/panel.js +28 -1
- package/lib/client.js +152 -4
- package/lib/executor.d.ts +6 -5
- package/lib/executor.js +30 -13
- 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 +42 -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,14 +23,14 @@
|
|
|
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
35
|
5. **Robust Environment Diagnosis & Three-State Setup Engine**:
|
|
36
36
|
- Zero-guesswork onboarding: Accurately differentiates `NOT_INSTALLED` (`Missing`), `NOT_LOGGED_IN` (`Required`), and `AUTHENTICATED` (`Active`).
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
## 📸 UI Previews
|
|
48
48
|
|
|
49
49
|
### Global Cockpit (Main Navigation)
|
|
50
|
-
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):
|
|
51
51
|
|
|
52
52
|
<p align="center">
|
|
53
53
|
<img src="./docs/images/1.png" alt="GitHub Global Cockpit" width="100%" />
|
|
54
54
|
</p>
|
|
55
55
|
|
|
56
56
|
### Session Right Sidebar Panel (In-Session Collaboration)
|
|
57
|
-
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:
|
|
58
58
|
|
|
59
59
|
<p align="center">
|
|
60
60
|
<img src="./docs/images/2.png" alt="Session Right Sidebar Panel" width="100%" />
|
package/README.md
CHANGED
|
@@ -23,14 +23,14 @@
|
|
|
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
35
|
5. **健全的环境诊断与三态就绪引导 (Three-State Setup Engine)**:
|
|
36
36
|
- 告别空状态困惑:精准建立「未安装 CLI (`Missing`)」➜「已安装未登录 (`Required`)」➜「已认证就绪 (`Active`)」三态状态机;
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
## 📸 界面预览
|
|
48
48
|
|
|
49
49
|
### 全局全景驾驶舱 (Global Cockpit)
|
|
50
|
-
透视账号云端所有 GitHub 仓库、本地工作区矩阵联动状态,以及待办 PR
|
|
50
|
+
透视账号云端所有 GitHub 仓库、本地工作区矩阵联动状态,以及待办 PR 和多维分类 Issues(支持用户提单、指派待办、我发起的多维药丸切换与状态徽章):
|
|
51
51
|
|
|
52
52
|
<p align="center">
|
|
53
53
|
<img src="./docs/images/1.png" alt="GitHub 全景驾驶舱" width="100%" />
|
|
54
54
|
</p>
|
|
55
55
|
|
|
56
56
|
### 会话右侧栏工作区面板 (Session Workspace Panel)
|
|
57
|
-
自动绑定当前工程关联仓库,常驻呈现 PR
|
|
57
|
+
自动绑定当前工程关联仓库,常驻呈现 PR 状态药丸、当前仓库 Open Issues 任务列表、Actions 构建流水线与快捷操作网格:
|
|
58
58
|
|
|
59
59
|
<p align="center">
|
|
60
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,9 +66,9 @@
|
|
|
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 路由与命令服务在环境就绪后激活;
|
|
@@ -79,3 +80,11 @@
|
|
|
79
80
|
- **平台自适应安装策略**:后端探测到 `ENOENT` 时根据 `process.platform` 组装对应平台的安装命令(Windows: `winget`,macOS: `brew`,Linux: `apt/dnf`)与官方安装包地址;
|
|
80
81
|
- **安全辅助与白名单终端唤起**:Web 端提供一键命令复制,后端 `/api/github/action` 提供 `open_terminal` 安全白名单唤起接口(只允许预设命令),避免在前端模拟复杂交互式 PTY 的脆弱性;
|
|
81
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
|
};
|
|
@@ -279,16 +344,23 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
279
344
|
let userRepos = [];
|
|
280
345
|
let myPrs = [];
|
|
281
346
|
let myIssues = [];
|
|
347
|
+
let issueStats = { total: 0, reported: 0, assigned: 0, created: 0 };
|
|
282
348
|
let workspaceMatrix = [];
|
|
283
349
|
let workspaceError;
|
|
284
350
|
if (auth.loggedIn) {
|
|
285
|
-
// 并发执行:1. 账号下仓库列表 2. 个人 PRs 3.
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
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([
|
|
289
359
|
executor.run(['repo', 'list', '--limit', '20', '--json', 'name,nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,updatedAt,url'], { timeoutMs: 8_000 }),
|
|
290
360
|
executor.run(['search', 'prs', '--author=@me', '--state=open', '--limit', '10', '--json', 'number,title,repository,url,updatedAt'], { timeoutMs: 8_000 }),
|
|
291
|
-
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 }),
|
|
292
364
|
]);
|
|
293
365
|
if (reposRes.ok && Array.isArray(reposRes.data)) {
|
|
294
366
|
userRepos = reposRes.data.map((r) => ({
|
|
@@ -305,9 +377,40 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
305
377
|
if (prsRes.ok && Array.isArray(prsRes.data)) {
|
|
306
378
|
myPrs = prsRes.data;
|
|
307
379
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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);
|
|
311
414
|
}
|
|
312
415
|
// 解析本地所有工作区与 GitHub 关联矩阵
|
|
313
416
|
const wsRead = tryReadWorkspaces();
|
|
@@ -336,6 +439,7 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
|
|
|
336
439
|
workspaceMatrix,
|
|
337
440
|
myPrs,
|
|
338
441
|
myIssues,
|
|
442
|
+
issueStats,
|
|
339
443
|
lastUpdated: new Date().toISOString(),
|
|
340
444
|
...(workspaceError ? { workspaceError } : {}),
|
|
341
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
|
}
|
package/lib/client.js
CHANGED
|
@@ -250,6 +250,16 @@ body[data-ds-dark-theme] .dsh-github-card-subtext {
|
|
|
250
250
|
color: #9a6700;
|
|
251
251
|
border: 1px solid rgba(154, 103, 0, 0.2);
|
|
252
252
|
}
|
|
253
|
+
.dsh-badge-blue {
|
|
254
|
+
background: rgba(9, 105, 218, 0.12);
|
|
255
|
+
color: #0969da;
|
|
256
|
+
border: 1px solid rgba(9, 105, 218, 0.2);
|
|
257
|
+
}
|
|
258
|
+
.dsh-badge-purple {
|
|
259
|
+
background: rgba(137, 87, 229, 0.12);
|
|
260
|
+
color: #8250df;
|
|
261
|
+
border: 1px solid rgba(137, 87, 229, 0.2);
|
|
262
|
+
}
|
|
253
263
|
body[data-ds-dark-theme] .dsh-badge-green {
|
|
254
264
|
background: rgba(46, 160, 67, 0.22);
|
|
255
265
|
color: #3fb950;
|
|
@@ -265,6 +275,63 @@ body[data-ds-dark-theme] .dsh-badge-yellow {
|
|
|
265
275
|
color: #d29922;
|
|
266
276
|
border-color: rgba(210, 153, 34, 0.3);
|
|
267
277
|
}
|
|
278
|
+
body[data-ds-dark-theme] .dsh-badge-blue {
|
|
279
|
+
background: rgba(56, 139, 253, 0.22);
|
|
280
|
+
color: #58a6ff;
|
|
281
|
+
border-color: rgba(56, 139, 253, 0.3);
|
|
282
|
+
}
|
|
283
|
+
body[data-ds-dark-theme] .dsh-badge-purple {
|
|
284
|
+
background: rgba(163, 113, 247, 0.22);
|
|
285
|
+
color: #bc8cff;
|
|
286
|
+
border-color: rgba(163, 113, 247, 0.3);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* 7.5. 分类筛选药丸栏 */
|
|
290
|
+
.dsh-github-pills {
|
|
291
|
+
display: flex;
|
|
292
|
+
align-items: center;
|
|
293
|
+
gap: 6px;
|
|
294
|
+
flex-wrap: wrap;
|
|
295
|
+
margin-bottom: 12px;
|
|
296
|
+
}
|
|
297
|
+
.dsh-github-pill-btn {
|
|
298
|
+
display: inline-flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 4px;
|
|
301
|
+
padding: 4px 10px;
|
|
302
|
+
border-radius: 14px;
|
|
303
|
+
font-size: 11px;
|
|
304
|
+
font-weight: 500;
|
|
305
|
+
border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
|
|
306
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
307
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
transition: all 0.15s ease;
|
|
310
|
+
}
|
|
311
|
+
.dsh-github-pill-btn:hover {
|
|
312
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
313
|
+
border-color: var(--dsw-alias-border-l3, #b0b8c1);
|
|
314
|
+
}
|
|
315
|
+
.dsh-github-pill-btn.active {
|
|
316
|
+
background: var(--dsw-alias-bg-module-platform, rgba(9, 105, 218, 0.1));
|
|
317
|
+
color: var(--dsw-alias-link, #0969da);
|
|
318
|
+
border-color: var(--dsw-alias-link, #0969da);
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
}
|
|
321
|
+
body[data-ds-dark-theme] .dsh-github-pill-btn {
|
|
322
|
+
background: var(--dsw-alias-bg-layer-2, #21262d);
|
|
323
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
324
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
325
|
+
}
|
|
326
|
+
body[data-ds-dark-theme] .dsh-github-pill-btn:hover {
|
|
327
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
328
|
+
border-color: #8b949e;
|
|
329
|
+
}
|
|
330
|
+
body[data-ds-dark-theme] .dsh-github-pill-btn.active {
|
|
331
|
+
background: rgba(56, 139, 253, 0.18);
|
|
332
|
+
color: #58a6ff;
|
|
333
|
+
border-color: #58a6ff;
|
|
334
|
+
}
|
|
268
335
|
|
|
269
336
|
/* 8. 快捷操作网格 */
|
|
270
337
|
.dsh-github-quick-actions {
|
|
@@ -895,6 +962,7 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
895
962
|
var auth = data ? data.auth : null;
|
|
896
963
|
var repo = data ? data.repo : null;
|
|
897
964
|
var prs = (data && data.pullRequests) || [];
|
|
965
|
+
var issues = (data && data.issues) || [];
|
|
898
966
|
var runs = (data && data.runs) || [];
|
|
899
967
|
var headerEl = h("div", { className: "dsh-github-header" }, h("div", { className: "dsh-github-title" }, renderOctocatBadge(isRightbar ? 26 : 30), h("h2", null, isRightbar ? "GitHub 仓库" : "GitHub 工作台")), h("button", {
|
|
900
968
|
className: "dsh-github-refresh-btn",
|
|
@@ -943,7 +1011,17 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
943
1011
|
}
|
|
944
1012
|
},
|
|
945
1013
|
}, "🚀 快速创建 PR"),
|
|
946
|
-
// 4.
|
|
1014
|
+
// 4. 查看所有 Issues (官方 Issues 列表直达)
|
|
1015
|
+
h("button", {
|
|
1016
|
+
className: "dsh-github-action-btn",
|
|
1017
|
+
title: "在系统默认浏览器中查看当前仓库所有 Issues",
|
|
1018
|
+
onClick: () => {
|
|
1019
|
+
if (repo?.url) {
|
|
1020
|
+
window.open(`${repo.url}/issues`, "_blank", "noopener,noreferrer");
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
}, "📋 查看所有 Issues"),
|
|
1024
|
+
// 5. 📝 新建 Issue (官方 Issue 模板页直达)
|
|
947
1025
|
h("button", {
|
|
948
1026
|
className: "dsh-github-action-btn",
|
|
949
1027
|
title: "在系统默认浏览器中打开 GitHub 官方新建 Issue 页面",
|
|
@@ -959,6 +1037,33 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
959
1037
|
: h("div", { className: "dsh-github-list" }, prs.map((pr) => h("div", { key: pr.number, className: "dsh-github-item" }, 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.headRefName} → ${pr.baseRefName}`), pr.mergeable === "CONFLICTING"
|
|
960
1038
|
? h("span", { className: "dsh-github-badge dsh-badge-red" }, "代码冲突")
|
|
961
1039
|
: h("span", { className: "dsh-github-badge dsh-badge-green" }, "可合并"))))));
|
|
1040
|
+
// 仓库 Issues 列表
|
|
1041
|
+
var issueSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `📋 仓库 Issues (${issues.length})`), repo?.url
|
|
1042
|
+
? h("a", {
|
|
1043
|
+
href: `${repo.url}/issues`,
|
|
1044
|
+
target: "_blank",
|
|
1045
|
+
style: { fontSize: "11px", color: "var(--dsw-alias-link, #0969da)", textDecoration: "none" },
|
|
1046
|
+
}, "全部 ↗")
|
|
1047
|
+
: null), issues.length === 0
|
|
1048
|
+
? h("div", { className: "dsh-github-empty" }, "当前无开放中的 Issue")
|
|
1049
|
+
: h("div", { className: "dsh-github-list" }, issues.map((issue) => {
|
|
1050
|
+
var cats = issue.categories || [];
|
|
1051
|
+
var badges = [];
|
|
1052
|
+
if (cats.includes("reported")) {
|
|
1053
|
+
badges.push(h("span", { key: "rep", className: "dsh-github-badge dsh-badge-purple" }, "用户反馈"));
|
|
1054
|
+
}
|
|
1055
|
+
if (cats.includes("assigned")) {
|
|
1056
|
+
badges.push(h("span", { key: "asn", className: "dsh-github-badge dsh-badge-blue" }, "已分配"));
|
|
1057
|
+
}
|
|
1058
|
+
if (cats.includes("created")) {
|
|
1059
|
+
badges.push(h("span", { key: "crt", className: "dsh-github-badge dsh-badge-green" }, "我发起的"));
|
|
1060
|
+
}
|
|
1061
|
+
return h("div", { key: issue.number, className: "dsh-github-item" }, 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
|
|
1062
|
+
? h("span", null, `指派: ${issue.assignees.map((a) => a.login).join(", ")}`)
|
|
1063
|
+
: null, issue.labels && issue.labels.length > 0
|
|
1064
|
+
? issue.labels.map((lbl) => h("span", { key: lbl.name, className: "dsh-github-badge dsh-badge-yellow" }, lbl.name))
|
|
1065
|
+
: null));
|
|
1066
|
+
})));
|
|
962
1067
|
// Actions 流水线
|
|
963
1068
|
var runSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `⚡ 最近 Actions 流水线 (${runs.length})`)), runs.length === 0
|
|
964
1069
|
? h("div", { className: "dsh-github-empty" }, "暂无工作流记录")
|
|
@@ -970,7 +1075,7 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
970
1075
|
var authGuideEl = !auth?.loggedIn
|
|
971
1076
|
? h(AuthGuideCard, { auth, onRefresh, isRightbar })
|
|
972
1077
|
: null;
|
|
973
|
-
return [headerEl, authGuideEl, repoCardEl, actionsBarEl, prSectionEl, runSectionEl].filter(Boolean);
|
|
1078
|
+
return [headerEl, authGuideEl, repoCardEl, actionsBarEl, prSectionEl, issueSectionEl, runSectionEl].filter(Boolean);
|
|
974
1079
|
}
|
|
975
1080
|
// 1. 右侧栏专属视图 (Right Sidebar Tab Body)
|
|
976
1081
|
function GithubRightbarBody(props) {
|
|
@@ -1065,6 +1170,7 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
1065
1170
|
var [error, setError] = React.useState(null);
|
|
1066
1171
|
var [activeTab, setActiveTab] = React.useState("workspaces"); // 'workspaces' | 'repos' | 'mywork'
|
|
1067
1172
|
var [searchQuery, setSearchQuery] = React.useState("");
|
|
1173
|
+
var [issueFilter, setIssueFilter] = React.useState("all"); // 'all' | 'reported' | 'assigned' | 'created'
|
|
1068
1174
|
var loadData = React.useCallback(async (forceRefresh = false) => {
|
|
1069
1175
|
setLoading(true);
|
|
1070
1176
|
setError(null);
|
|
@@ -1149,7 +1255,7 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
1149
1255
|
? h(AuthGuideCard, { auth, onRefresh: () => loadData(true), isRightbar: false })
|
|
1150
1256
|
: null;
|
|
1151
1257
|
// Tab 栏
|
|
1152
|
-
var tabsEl = h("div", { className: "dsh-github-tabs" }, h("button", { className: `dsh-github-tab-btn ${activeTab === 'workspaces' ? 'active' : ''}`, onClick: () => setActiveTab('workspaces') }, `📂 本地工作区矩阵 (${workspaceMatrix.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'repos' ? 'active' : ''}`, onClick: () => setActiveTab('repos') }, `☁️ 我的 GitHub 仓库 (${userRepos.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'mywork' ? 'active' : ''}`, onClick: () => setActiveTab('mywork') }, `📋
|
|
1258
|
+
var tabsEl = h("div", { className: "dsh-github-tabs" }, h("button", { className: `dsh-github-tab-btn ${activeTab === 'workspaces' ? 'active' : ''}`, onClick: () => setActiveTab('workspaces') }, `📂 本地工作区矩阵 (${workspaceMatrix.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'repos' ? 'active' : ''}`, onClick: () => setActiveTab('repos') }, `☁️ 我的 GitHub 仓库 (${userRepos.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'mywork' ? 'active' : ''}`, onClick: () => setActiveTab('mywork') }, `📋 任务与待办 (PR: ${myPrs.length} · Issue: ${myIssues.length})`));
|
|
1153
1259
|
// 内容区
|
|
1154
1260
|
var contentEl = null;
|
|
1155
1261
|
if (activeTab === 'workspaces') {
|
|
@@ -1178,7 +1284,49 @@ body[data-ds-dark-theme] .dsh-github-steps-list {
|
|
|
1178
1284
|
contentEl = h("div", { className: "dsh-github-empty" }, "当前尚未认证登录 GitHub 账号,无法读取个人待办 PR 与 Issues。请先完成上方的安装或登录指引。");
|
|
1179
1285
|
}
|
|
1180
1286
|
else {
|
|
1181
|
-
|
|
1287
|
+
var issueStats = (data && data.issueStats) || {
|
|
1288
|
+
total: myIssues.length,
|
|
1289
|
+
reported: myIssues.filter((i) => (i.categories || []).includes('reported')).length,
|
|
1290
|
+
assigned: myIssues.filter((i) => (i.categories || []).includes('assigned')).length,
|
|
1291
|
+
created: myIssues.filter((i) => (i.categories || []).includes('created')).length,
|
|
1292
|
+
};
|
|
1293
|
+
var filteredIssues = myIssues.filter((is) => {
|
|
1294
|
+
if (issueFilter === 'all')
|
|
1295
|
+
return true;
|
|
1296
|
+
var cats = is.categories || [];
|
|
1297
|
+
return cats.includes(issueFilter);
|
|
1298
|
+
});
|
|
1299
|
+
var issuePillsEl = h("div", { className: "dsh-github-pills" }, h("button", {
|
|
1300
|
+
className: `dsh-github-pill-btn ${issueFilter === 'all' ? 'active' : ''}`,
|
|
1301
|
+
onClick: () => setIssueFilter('all'),
|
|
1302
|
+
}, `全部 (${issueStats.total || myIssues.length})`), h("button", {
|
|
1303
|
+
className: `dsh-github-pill-btn ${issueFilter === 'reported' ? 'active' : ''}`,
|
|
1304
|
+
onClick: () => setIssueFilter('reported'),
|
|
1305
|
+
}, `📥 用户提起的 (${issueStats.reported || 0})`), h("button", {
|
|
1306
|
+
className: `dsh-github-pill-btn ${issueFilter === 'assigned' ? 'active' : ''}`,
|
|
1307
|
+
onClick: () => setIssueFilter('assigned'),
|
|
1308
|
+
}, `🎯 分配给我的 (${issueStats.assigned || 0})`), h("button", {
|
|
1309
|
+
className: `dsh-github-pill-btn ${issueFilter === 'created' ? 'active' : ''}`,
|
|
1310
|
+
onClick: () => setIssueFilter('created'),
|
|
1311
|
+
}, `✍️ 我创建的 (${issueStats.created || 0})`));
|
|
1312
|
+
contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" }, `🔀 我发起的 PR (${myPrs.length})`), myPrs.length === 0 ? h("div", { className: "dsh-github-empty", style: { marginBottom: "16px" } }, "暂无开放中的 PR") : h("div", { className: "dsh-github-list", style: { marginBottom: "16px" } }, myPrs.map((pr) => h("div", { key: pr.number, className: "dsh-github-item" }, h("a", { className: "dsh-github-item-title", href: pr.url, target: "_blank" }, `[${pr.repository?.nameWithOwner || 'Repo'}] #${pr.number} ${pr.title}`)))), h("h3", { className: "dsh-github-subheading" }, `📝 任务与待办 Issues (${filteredIssues.length})`), issuePillsEl, filteredIssues.length === 0 ? h("div", { className: "dsh-github-empty" }, "当前分类下暂无开放中的 Issue") : h("div", { className: "dsh-github-list" }, filteredIssues.map((is) => {
|
|
1313
|
+
var cats = is.categories || [];
|
|
1314
|
+
var badges = [];
|
|
1315
|
+
if (cats.includes("reported")) {
|
|
1316
|
+
badges.push(h("span", { key: "rep", className: "dsh-github-badge dsh-badge-purple" }, "用户提单"));
|
|
1317
|
+
}
|
|
1318
|
+
if (cats.includes("assigned")) {
|
|
1319
|
+
badges.push(h("span", { key: "asn", className: "dsh-github-badge dsh-badge-blue" }, "分配待办"));
|
|
1320
|
+
}
|
|
1321
|
+
if (cats.includes("created")) {
|
|
1322
|
+
badges.push(h("span", { key: "crt", className: "dsh-github-badge dsh-badge-green" }, "我发起的"));
|
|
1323
|
+
}
|
|
1324
|
+
return h("div", { key: is.url || is.number, className: "dsh-github-item" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("a", { className: "dsh-github-item-title", href: is.url, target: "_blank" }, `[${is.repository?.nameWithOwner || 'Repo'}] #${is.number} ${is.title}`), ...badges), h("div", { className: "dsh-github-item-meta" }, is.author ? h("span", null, `由 @${is.author.login} 提出`) : null, is.assignees && is.assignees.length > 0
|
|
1325
|
+
? h("span", null, `指派: ${is.assignees.map((a) => '@' + a.login).join(', ')}`)
|
|
1326
|
+
: null, is.labels && is.labels.length > 0
|
|
1327
|
+
? is.labels.map((lbl) => h("span", { key: lbl.name, className: "dsh-github-badge dsh-badge-yellow" }, lbl.name))
|
|
1328
|
+
: null, is.updatedAt ? h("span", null, `更新于: ${new Date(is.updatedAt).toLocaleDateString()}`) : null));
|
|
1329
|
+
})));
|
|
1182
1330
|
}
|
|
1183
1331
|
}
|
|
1184
1332
|
return h("div", { className: "dsh-github-panel" }, headerEl, errorEl, statsCardsEl, authGuideEl, tabsEl, contentEl);
|
package/lib/executor.d.ts
CHANGED
|
@@ -21,11 +21,12 @@ export declare class GhExecutor {
|
|
|
21
21
|
/**
|
|
22
22
|
* 智能解析最可靠的工作区物理路径:
|
|
23
23
|
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
24
|
-
* 2.
|
|
25
|
-
* 3.
|
|
26
|
-
* 4.
|
|
24
|
+
* 2. 使用显式传入的 sessionId 或环境变量 DSH_SESSION_ID 精确反查工作区
|
|
25
|
+
* 3. 递归探测:若传入了 explicitCwd(如聚合父目录),自动检查其直系子目录中是否包含 Git 仓库
|
|
26
|
+
* 4. 匹配 ~/.dsh/storages/workspace.json 中最近活跃且存在 .git 的仓库工作区
|
|
27
|
+
* 5. 回退到 process.cwd()
|
|
27
28
|
*/
|
|
28
|
-
resolveWorkspaceCwd(explicitCwd?: string): string;
|
|
29
|
+
resolveWorkspaceCwd(explicitCwd?: string, explicitSessionId?: string): string;
|
|
29
30
|
/**
|
|
30
31
|
* 安全执行 gh 命令
|
|
31
32
|
* 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
|
|
@@ -57,5 +58,5 @@ export declare class GhExecutor {
|
|
|
57
58
|
* 获取当前目录(工作区)关联的远程仓库元数据
|
|
58
59
|
* 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
|
|
59
60
|
*/
|
|
60
|
-
getRepoMetadata(cwd?: string): Promise<RepoMetadata | null>;
|
|
61
|
+
getRepoMetadata(cwd?: string, sessionId?: string): Promise<RepoMetadata | null>;
|
|
61
62
|
}
|
package/lib/executor.js
CHANGED
|
@@ -119,22 +119,39 @@ export class GhExecutor {
|
|
|
119
119
|
/**
|
|
120
120
|
* 智能解析最可靠的工作区物理路径:
|
|
121
121
|
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
122
|
-
* 2.
|
|
123
|
-
* 3.
|
|
124
|
-
* 4.
|
|
122
|
+
* 2. 使用显式传入的 sessionId 或环境变量 DSH_SESSION_ID 精确反查工作区
|
|
123
|
+
* 3. 递归探测:若传入了 explicitCwd(如聚合父目录),自动检查其直系子目录中是否包含 Git 仓库
|
|
124
|
+
* 4. 匹配 ~/.dsh/storages/workspace.json 中最近活跃且存在 .git 的仓库工作区
|
|
125
|
+
* 5. 回退到 process.cwd()
|
|
125
126
|
*/
|
|
126
|
-
resolveWorkspaceCwd(explicitCwd) {
|
|
127
|
-
//
|
|
128
|
-
// 此时继续往下走按会话/最近活跃反查,比盲信传入值更准。
|
|
127
|
+
resolveWorkspaceCwd(explicitCwd, explicitSessionId) {
|
|
128
|
+
// 1. 显式路径优先,若有效且直接包含 .git 目录,则直接使用
|
|
129
129
|
if (explicitCwd && hasGitDir(explicitCwd)) {
|
|
130
130
|
return explicitCwd;
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// 2. 使用显式传入的 sessionId 或环境变量中的 DSH_SESSION_ID 反查
|
|
133
|
+
const targetSessionId = explicitSessionId || process.env.DSH_SESSION_ID;
|
|
134
|
+
if (targetSessionId) {
|
|
135
|
+
const bySession = findWorkspacePathBySession(targetSessionId);
|
|
136
|
+
if (bySession && hasGitDir(bySession))
|
|
136
137
|
return bySession;
|
|
137
138
|
}
|
|
139
|
+
// 3. 递归探测:若传入了 explicitCwd(如非 Git 的父工作区目录),检查其一级子目录中是否存在 Git 仓库
|
|
140
|
+
if (explicitCwd && fs.existsSync(explicitCwd)) {
|
|
141
|
+
try {
|
|
142
|
+
const entries = fs.readdirSync(explicitCwd, { withFileTypes: true });
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
const subDir = path.join(explicitCwd, entry.name);
|
|
146
|
+
if (hasGitDir(subDir)) {
|
|
147
|
+
return subDir;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
}
|
|
154
|
+
// 4. 从工作区配置中匹配最近活跃且明确具备 .git 的仓库路径
|
|
138
155
|
const recent = findMostRecentWorkspacePath();
|
|
139
156
|
if (recent)
|
|
140
157
|
return recent;
|
|
@@ -145,7 +162,7 @@ export class GhExecutor {
|
|
|
145
162
|
* 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
|
|
146
163
|
*/
|
|
147
164
|
async run(args, options = {}) {
|
|
148
|
-
const cwd = this.resolveWorkspaceCwd(options.cwd);
|
|
165
|
+
const cwd = this.resolveWorkspaceCwd(options.cwd, options.sessionId);
|
|
149
166
|
const timeout = options.timeoutMs || this.defaultTimeoutMs;
|
|
150
167
|
try {
|
|
151
168
|
const { stdout, stderr } = await execFileAsync(this.ghPath, args, {
|
|
@@ -339,8 +356,8 @@ export class GhExecutor {
|
|
|
339
356
|
* 获取当前目录(工作区)关联的远程仓库元数据
|
|
340
357
|
* 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
|
|
341
358
|
*/
|
|
342
|
-
async getRepoMetadata(cwd) {
|
|
343
|
-
const targetCwd = this.resolveWorkspaceCwd(cwd);
|
|
359
|
+
async getRepoMetadata(cwd, sessionId) {
|
|
360
|
+
const targetCwd = this.resolveWorkspaceCwd(cwd, sessionId);
|
|
344
361
|
// 1. 优先尝试由 gh 直接根据当前上下文识别
|
|
345
362
|
const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
346
363
|
if (res.ok && res.data && res.data.nameWithOwner) {
|
package/lib/tools/api.js
CHANGED
|
@@ -54,6 +54,7 @@ export function createApiTool(executor) {
|
|
|
54
54
|
},
|
|
55
55
|
async execute(args, execContext) {
|
|
56
56
|
const cwd = execContext?.cwd || process.cwd();
|
|
57
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
57
58
|
const cmd = ['api', args.endpoint];
|
|
58
59
|
if (args.method && args.method !== 'GET') {
|
|
59
60
|
cmd.push('-X', args.method);
|
|
@@ -81,7 +82,7 @@ export function createApiTool(executor) {
|
|
|
81
82
|
cmd.push('-f', `${k}=${String(v)}`);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
|
-
const res = await executor.run(cmd, { cwd, rawText: args.raw === true });
|
|
85
|
+
const res = await executor.run(cmd, { cwd, sessionId, rawText: args.raw === true });
|
|
85
86
|
if (!res.ok)
|
|
86
87
|
throw new Error(res.error);
|
|
87
88
|
return normalizeOutput(res.data !== undefined ? res.data : res.rawOutput);
|
package/lib/tools/issue.js
CHANGED
|
@@ -64,17 +64,22 @@ export function createIssueTool(executor) {
|
|
|
64
64
|
},
|
|
65
65
|
async execute(args, execContext) {
|
|
66
66
|
const cwd = execContext?.cwd || process.cwd();
|
|
67
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
67
68
|
let repoTarget = args.repo;
|
|
68
69
|
if (!repoTarget) {
|
|
69
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
70
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
70
71
|
if (meta?.nameWithOwner) {
|
|
71
72
|
repoTarget = meta.nameWithOwner;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
if (!repoTarget) {
|
|
76
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
77
|
+
}
|
|
78
|
+
const repoArgs = ['-R', repoTarget];
|
|
79
|
+
const runOpts = { cwd, sessionId };
|
|
75
80
|
switch (args.action) {
|
|
76
81
|
case 'list': {
|
|
77
|
-
const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'],
|
|
82
|
+
const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], runOpts);
|
|
78
83
|
if (!res.ok)
|
|
79
84
|
throw new Error(res.error);
|
|
80
85
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -82,7 +87,7 @@ export function createIssueTool(executor) {
|
|
|
82
87
|
case 'view': {
|
|
83
88
|
if (!args.issue_number)
|
|
84
89
|
throw new Error('view 操作必须提供 issue_number');
|
|
85
|
-
const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'],
|
|
90
|
+
const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], runOpts);
|
|
86
91
|
if (!res.ok)
|
|
87
92
|
throw new Error(res.error);
|
|
88
93
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -95,7 +100,7 @@ export function createIssueTool(executor) {
|
|
|
95
100
|
if (args.assignees && Array.isArray(args.assignees) && args.assignees.length > 0) {
|
|
96
101
|
cmd.push('--assignee', args.assignees.join(','));
|
|
97
102
|
}
|
|
98
|
-
const res = await executor.run(cmd,
|
|
103
|
+
const res = await executor.run(cmd, runOpts);
|
|
99
104
|
if (!res.ok)
|
|
100
105
|
throw new Error(res.error);
|
|
101
106
|
return { message: 'Issue 创建成功', url: res.rawOutput };
|
|
@@ -103,7 +108,7 @@ export function createIssueTool(executor) {
|
|
|
103
108
|
case 'comment': {
|
|
104
109
|
if (!args.issue_number || !args.body)
|
|
105
110
|
throw new Error('comment 必须提供 issue_number 和 body');
|
|
106
|
-
const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body],
|
|
111
|
+
const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body], runOpts);
|
|
107
112
|
if (!res.ok)
|
|
108
113
|
throw new Error(res.error);
|
|
109
114
|
return { message: '评论发布成功', output: res.rawOutput };
|
|
@@ -111,7 +116,7 @@ export function createIssueTool(executor) {
|
|
|
111
116
|
case 'close': {
|
|
112
117
|
if (!args.issue_number)
|
|
113
118
|
throw new Error('close 操作必须提供 issue_number');
|
|
114
|
-
const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs],
|
|
119
|
+
const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs], runOpts);
|
|
115
120
|
if (!res.ok)
|
|
116
121
|
throw new Error(res.error);
|
|
117
122
|
return { message: `Issue #${args.issue_number} 已成功关闭` };
|
|
@@ -119,7 +124,7 @@ export function createIssueTool(executor) {
|
|
|
119
124
|
case 'reopen': {
|
|
120
125
|
if (!args.issue_number)
|
|
121
126
|
throw new Error('reopen 操作必须提供 issue_number');
|
|
122
|
-
const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs],
|
|
127
|
+
const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs], runOpts);
|
|
123
128
|
if (!res.ok)
|
|
124
129
|
throw new Error(res.error);
|
|
125
130
|
return { message: `Issue #${args.issue_number} 已重新开启` };
|
package/lib/tools/pr.js
CHANGED
|
@@ -84,38 +84,43 @@ export function createPrTool(executor) {
|
|
|
84
84
|
},
|
|
85
85
|
async execute(args, execContext) {
|
|
86
86
|
const cwd = execContext?.cwd || process.cwd();
|
|
87
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
87
88
|
let repoTarget = args.repo;
|
|
88
89
|
if (!repoTarget) {
|
|
89
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
90
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
90
91
|
if (meta?.nameWithOwner) {
|
|
91
92
|
repoTarget = meta.nameWithOwner;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
if (!repoTarget) {
|
|
96
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
97
|
+
}
|
|
98
|
+
const repoArgs = ['-R', repoTarget];
|
|
99
|
+
const runOpts = { cwd, sessionId };
|
|
95
100
|
switch (args.action) {
|
|
96
101
|
case 'list': {
|
|
97
|
-
const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'],
|
|
102
|
+
const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], runOpts);
|
|
98
103
|
if (!res.ok)
|
|
99
104
|
throw new Error(res.error);
|
|
100
105
|
return normalizeOutput(res.data || res.rawOutput);
|
|
101
106
|
}
|
|
102
107
|
case 'view': {
|
|
103
108
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
104
|
-
const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'],
|
|
109
|
+
const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], runOpts);
|
|
105
110
|
if (!res.ok)
|
|
106
111
|
throw new Error(res.error);
|
|
107
112
|
return normalizeOutput(res.data || res.rawOutput);
|
|
108
113
|
}
|
|
109
114
|
case 'checks': {
|
|
110
115
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
111
|
-
const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'],
|
|
116
|
+
const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], runOpts);
|
|
112
117
|
if (!res.ok)
|
|
113
118
|
throw new Error(res.error);
|
|
114
119
|
return normalizeOutput(res.data || res.rawOutput);
|
|
115
120
|
}
|
|
116
121
|
case 'diff': {
|
|
117
122
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
118
|
-
const res = await executor.run(['pr', 'diff', ...target, ...repoArgs],
|
|
123
|
+
const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], runOpts);
|
|
119
124
|
if (!res.ok)
|
|
120
125
|
throw new Error(res.error);
|
|
121
126
|
return { result: res.rawOutput || '无差异内容' };
|
|
@@ -134,7 +139,7 @@ export function createPrTool(executor) {
|
|
|
134
139
|
const dryRun = args.dry_run === true;
|
|
135
140
|
if (dryRun)
|
|
136
141
|
createCmd.push('--dry-run');
|
|
137
|
-
const res = await executor.run(createCmd,
|
|
142
|
+
const res = await executor.run(createCmd, runOpts);
|
|
138
143
|
if (!res.ok)
|
|
139
144
|
throw new Error(res.error);
|
|
140
145
|
return dryRun
|
|
@@ -153,7 +158,7 @@ export function createPrTool(executor) {
|
|
|
153
158
|
reviewCmd.push('--comment');
|
|
154
159
|
if (args.body)
|
|
155
160
|
reviewCmd.push('--body', args.body);
|
|
156
|
-
const res = await executor.run(reviewCmd,
|
|
161
|
+
const res = await executor.run(reviewCmd, runOpts);
|
|
157
162
|
if (!res.ok)
|
|
158
163
|
throw new Error(res.error);
|
|
159
164
|
return { message: 'Review 提交成功', detail: res.rawOutput };
|
|
@@ -167,7 +172,7 @@ export function createPrTool(executor) {
|
|
|
167
172
|
// 而 gh 默认不删。替用户做掉它违背「可撤销的直接做、不可撤销的先问」。
|
|
168
173
|
if (args.delete_branch === true)
|
|
169
174
|
mergeCmd.push('--delete-branch');
|
|
170
|
-
const res = await executor.run(mergeCmd,
|
|
175
|
+
const res = await executor.run(mergeCmd, runOpts);
|
|
171
176
|
if (!res.ok)
|
|
172
177
|
throw new Error(res.error);
|
|
173
178
|
return { message: 'PR 合并完成', output: res.rawOutput };
|
package/lib/tools/repo.js
CHANGED
|
@@ -50,19 +50,21 @@ export function createRepoTool(executor) {
|
|
|
50
50
|
},
|
|
51
51
|
async execute(args, execContext) {
|
|
52
52
|
const cwd = execContext?.cwd || process.cwd();
|
|
53
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
53
54
|
const limitStr = String(args.limit || 10);
|
|
55
|
+
const runOpts = { cwd, sessionId };
|
|
54
56
|
switch (args.action) {
|
|
55
57
|
case 'view': {
|
|
56
58
|
let repoTarget = args.repo;
|
|
57
59
|
let meta = null;
|
|
58
60
|
if (!repoTarget) {
|
|
59
|
-
meta = await executor.getRepoMetadata(cwd);
|
|
61
|
+
meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
60
62
|
if (meta?.nameWithOwner) {
|
|
61
63
|
repoTarget = meta.nameWithOwner;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
const repoArgs = repoTarget ? [repoTarget] : [];
|
|
65
|
-
const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'],
|
|
67
|
+
const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'], runOpts);
|
|
66
68
|
if (!res.ok) {
|
|
67
69
|
// 若执行 gh 失败(如离线或环境限制),但本地已成功识别仓库元数据,进行优雅兜底返回
|
|
68
70
|
if (meta && meta.nameWithOwner) {
|
|
@@ -76,7 +78,7 @@ export function createRepoTool(executor) {
|
|
|
76
78
|
if (!args.query)
|
|
77
79
|
throw new Error('search_code 必须提供 query');
|
|
78
80
|
const repoArgs = args.repo ? ['--repo', args.repo] : [];
|
|
79
|
-
const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr],
|
|
81
|
+
const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr], runOpts);
|
|
80
82
|
if (!res.ok)
|
|
81
83
|
throw new Error(res.error);
|
|
82
84
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -84,7 +86,7 @@ export function createRepoTool(executor) {
|
|
|
84
86
|
case 'search_repos': {
|
|
85
87
|
if (!args.query)
|
|
86
88
|
throw new Error('search_repos 必须提供 query');
|
|
87
|
-
const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr],
|
|
89
|
+
const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr], runOpts);
|
|
88
90
|
if (!res.ok)
|
|
89
91
|
throw new Error(res.error);
|
|
90
92
|
return normalizeOutput(res.data || res.rawOutput);
|
package/lib/tools/run.js
CHANGED
|
@@ -50,17 +50,22 @@ export function createRunTool(executor) {
|
|
|
50
50
|
},
|
|
51
51
|
async execute(args, execContext) {
|
|
52
52
|
const cwd = execContext?.cwd || process.cwd();
|
|
53
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
53
54
|
let repoTarget = args.repo;
|
|
54
55
|
if (!repoTarget) {
|
|
55
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
56
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
56
57
|
if (meta?.nameWithOwner) {
|
|
57
58
|
repoTarget = meta.nameWithOwner;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
if (!repoTarget) {
|
|
62
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
63
|
+
}
|
|
64
|
+
const repoArgs = ['-R', repoTarget];
|
|
65
|
+
const runOpts = { cwd, sessionId };
|
|
61
66
|
switch (args.action) {
|
|
62
67
|
case 'list': {
|
|
63
|
-
const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)],
|
|
68
|
+
const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)], runOpts);
|
|
64
69
|
if (!res.ok)
|
|
65
70
|
throw new Error(res.error);
|
|
66
71
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -68,7 +73,7 @@ export function createRunTool(executor) {
|
|
|
68
73
|
case 'view': {
|
|
69
74
|
if (!args.run_id)
|
|
70
75
|
throw new Error('view 操作必须提供 run_id');
|
|
71
|
-
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'],
|
|
76
|
+
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'], runOpts);
|
|
72
77
|
if (!res.ok)
|
|
73
78
|
throw new Error(res.error);
|
|
74
79
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -78,7 +83,7 @@ export function createRunTool(executor) {
|
|
|
78
83
|
throw new Error('log_failed 操作必须提供 run_id');
|
|
79
84
|
// gh 原生支持仅提取失败的日志!对 Agent 故障诊断价值极高
|
|
80
85
|
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--log-failed'], {
|
|
81
|
-
|
|
86
|
+
...runOpts,
|
|
82
87
|
timeoutMs: 60_000,
|
|
83
88
|
rawText: true,
|
|
84
89
|
});
|
|
@@ -89,7 +94,7 @@ export function createRunTool(executor) {
|
|
|
89
94
|
case 'rerun': {
|
|
90
95
|
if (!args.run_id)
|
|
91
96
|
throw new Error('rerun 操作必须提供 run_id');
|
|
92
|
-
const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'],
|
|
97
|
+
const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'], runOpts);
|
|
93
98
|
if (!res.ok)
|
|
94
99
|
throw new Error(res.error);
|
|
95
100
|
return { message: '已成功重新触发失败任务', output: res.rawOutput };
|
|
@@ -97,7 +102,7 @@ export function createRunTool(executor) {
|
|
|
97
102
|
case 'cancel': {
|
|
98
103
|
if (!args.run_id)
|
|
99
104
|
throw new Error('cancel 操作必须提供 run_id');
|
|
100
|
-
const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs],
|
|
105
|
+
const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs], runOpts);
|
|
101
106
|
if (!res.ok)
|
|
102
107
|
throw new Error(res.error);
|
|
103
108
|
return { message: '已取消该工作流任务', output: res.rawOutput };
|
package/lib/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface PluginConfig {
|
|
|
18
18
|
export interface GhExecutionOptions {
|
|
19
19
|
/** 命令执行的工作目录,优先使用当前 Agent 会话所在的 Workspace 物理路径 */
|
|
20
20
|
cwd?: string;
|
|
21
|
+
/** 当前调用的会话 ID,用于精确关联物理工作区路径 */
|
|
22
|
+
sessionId?: string;
|
|
21
23
|
/** 进程超时限制(毫秒),默认 30,000ms */
|
|
22
24
|
timeoutMs?: number;
|
|
23
25
|
/** 附加环境变量 */
|
|
@@ -86,6 +88,19 @@ export interface PrItem {
|
|
|
86
88
|
url: string;
|
|
87
89
|
updatedAt: string;
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Issue 分类维度契约:
|
|
93
|
+
* - reported: 用户提起的(他人向我名下项目或本仓库提交的 Issue 反馈)
|
|
94
|
+
* - assigned: 分配给我的(指派给当前账号的待办任务,跨仓库通用)
|
|
95
|
+
* - created: 我创建的(由当前账号亲自发起的 Issue)
|
|
96
|
+
*/
|
|
97
|
+
export type IssueCategory = 'reported' | 'assigned' | 'created';
|
|
98
|
+
export interface IssueStats {
|
|
99
|
+
total: number;
|
|
100
|
+
reported: number;
|
|
101
|
+
assigned: number;
|
|
102
|
+
created: number;
|
|
103
|
+
}
|
|
89
104
|
export interface IssueItem {
|
|
90
105
|
number: number;
|
|
91
106
|
title: string;
|
|
@@ -102,6 +117,8 @@ export interface IssueItem {
|
|
|
102
117
|
}>;
|
|
103
118
|
url: string;
|
|
104
119
|
updatedAt: string;
|
|
120
|
+
/** 命中的所有身份分类 */
|
|
121
|
+
categories?: IssueCategory[];
|
|
105
122
|
}
|
|
106
123
|
export interface WorkflowRunItem {
|
|
107
124
|
databaseId: number;
|
|
@@ -118,6 +135,7 @@ export interface GitHubOverviewData {
|
|
|
118
135
|
repo?: RepoMetadata;
|
|
119
136
|
pullRequests: PrItem[];
|
|
120
137
|
issues: IssueItem[];
|
|
138
|
+
issueStats?: IssueStats;
|
|
121
139
|
runs: WorkflowRunItem[];
|
|
122
140
|
lastUpdated: string;
|
|
123
141
|
error?: string;
|
|
@@ -140,6 +158,28 @@ export interface WorkspaceMatrixItem {
|
|
|
140
158
|
repo?: RepoMetadata;
|
|
141
159
|
sessionCount: number;
|
|
142
160
|
}
|
|
161
|
+
export interface GlobalIssueItem {
|
|
162
|
+
number: number;
|
|
163
|
+
title: string;
|
|
164
|
+
repository: {
|
|
165
|
+
name?: string;
|
|
166
|
+
nameWithOwner: string;
|
|
167
|
+
};
|
|
168
|
+
author?: {
|
|
169
|
+
login: string;
|
|
170
|
+
};
|
|
171
|
+
assignees?: Array<{
|
|
172
|
+
login: string;
|
|
173
|
+
}>;
|
|
174
|
+
labels?: Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
color: string;
|
|
177
|
+
}>;
|
|
178
|
+
url: string;
|
|
179
|
+
updatedAt: string;
|
|
180
|
+
state?: 'OPEN' | 'CLOSED';
|
|
181
|
+
categories: IssueCategory[];
|
|
182
|
+
}
|
|
143
183
|
export interface GlobalOverviewData {
|
|
144
184
|
auth: AuthStatus;
|
|
145
185
|
userRepos: UserRepoItem[];
|
|
@@ -153,15 +193,8 @@ export interface GlobalOverviewData {
|
|
|
153
193
|
url: string;
|
|
154
194
|
updatedAt: string;
|
|
155
195
|
}>;
|
|
156
|
-
myIssues:
|
|
157
|
-
|
|
158
|
-
title: string;
|
|
159
|
-
repository: {
|
|
160
|
-
nameWithOwner: string;
|
|
161
|
-
};
|
|
162
|
-
url: string;
|
|
163
|
-
updatedAt: string;
|
|
164
|
-
}>;
|
|
196
|
+
myIssues: GlobalIssueItem[];
|
|
197
|
+
issueStats?: IssueStats;
|
|
165
198
|
lastUpdated: string;
|
|
166
199
|
error?: string;
|
|
167
200
|
/** 工作区矩阵读取失败的原因。区分「没有工作区」与「读不到工作区」,避免把故障显示成空状态 */
|
package/lib/workspace.d.ts
CHANGED
|
@@ -27,9 +27,12 @@ export declare function tryReadWorkspaces(): {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function findWorkspacePathBySession(sessionId: string): string | undefined;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* 匹配最近活跃且包含 Git 仓库的工作区路径。
|
|
31
|
+
*
|
|
32
|
+
* 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
|
|
33
|
+
* 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
|
|
34
|
+
* 进而导致后续 gh 命令报出 "fatal: not a git repository"。
|
|
35
|
+
* 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
|
|
33
36
|
*/
|
|
34
37
|
export declare function findMostRecentWorkspacePath(): string | undefined;
|
|
35
38
|
export declare function hasGitDir(dir: string): boolean;
|
package/lib/workspace.js
CHANGED
|
@@ -79,24 +79,39 @@ export function findWorkspacePathBySession(sessionId) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* 匹配最近活跃且包含 Git 仓库的工作区路径。
|
|
83
|
+
*
|
|
84
|
+
* 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
|
|
85
|
+
* 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
|
|
86
|
+
* 进而导致后续 gh 命令报出 "fatal: not a git repository"。
|
|
87
|
+
* 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
|
|
85
88
|
*/
|
|
86
89
|
export function findMostRecentWorkspacePath() {
|
|
87
90
|
try {
|
|
88
91
|
const { workspaces } = tryReadWorkspaces();
|
|
89
|
-
let
|
|
90
|
-
let
|
|
92
|
+
let bestGitWs = null;
|
|
93
|
+
let bestGitAt = -1;
|
|
94
|
+
let fallbackWs = null;
|
|
95
|
+
let fallbackAt = -1;
|
|
91
96
|
for (const ws of workspaces) {
|
|
97
|
+
if (!ws.path || !fs.existsSync(ws.path))
|
|
98
|
+
continue;
|
|
92
99
|
const at = Date.parse(ws.updatedAt || '');
|
|
93
100
|
const score = Number.isFinite(at) ? at : 0;
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if (hasGitDir(ws.path)) {
|
|
102
|
+
if (score > bestGitAt) {
|
|
103
|
+
bestGitWs = ws;
|
|
104
|
+
bestGitAt = score;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (score > fallbackAt) {
|
|
109
|
+
fallbackWs = ws;
|
|
110
|
+
fallbackAt = score;
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
|
-
return
|
|
114
|
+
return bestGitWs?.path || fallbackWs?.path;
|
|
100
115
|
}
|
|
101
116
|
catch {
|
|
102
117
|
return undefined;
|
package/package.json
CHANGED