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 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`. Neither models nor humans need to supply redundant `owner/repo` arguments.
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 & Dynamic Dual-Theme Support**:
32
- - **Global Cockpit** (`sidebar.panellist`, Order 20): Account-wide repository overview, local workspace matrix, and pending personal PRs/issues.
33
- - **Right Sidebar Panel** (`sidebarRightTabs`, Order 40): Shows current repository status, PR list with CI check pills, and a 2x2 quick action grid (Open Repo, View PRs, Create PR, New Issue) with zero popup dependencies.
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 pending personal PRs/issues:
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`,无需 Agent 或人类手动传参 `owner/repo`,零参数直接识别关联仓库。
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):透视账号全局仓库、本地工作区矩阵、个人待办 PR 与 Issues;
33
- - **右侧栏工作区面板**(`sidebarRightTabs`,Order 40):常驻呈现当前仓库的 PR 状态、Checks 门禁药丸与 2x2 黄金快捷网格(浏览器打开、查看 PRs、快速创建 PR、新建 Issue),一键直达官方比对与创建页,零弹窗依赖;
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 和 Issues:
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 状态药丸、Actions 构建流水线与 2x2 黄金操作网格:
57
+ 自动绑定当前工程关联仓库,常驻呈现 PR 状态药丸、当前仓库 Open Issues 任务列表、Actions 构建流水线与快捷操作网格:
58
58
 
59
59
  <p align="center">
60
60
  <img src="./docs/images/2.png" alt="右侧栏工作区面板" width="100%" />
@@ -41,7 +41,8 @@
41
41
  │ │ 全局驾驶舱 (Main Slot) │ │ 右侧栏面板 (Right Sidebar) │ │
42
42
  │ │ - 账号全局所有 GitHub 仓库 │ │ - 当前会话工作区关联仓库 │ │
43
43
  │ │ - 本地工作区矩阵透视 │ │ - PR 列表与 CI Checks 药丸 │ │
44
- │ │ - 个人待办 PRs & Issues │ │ - 快速创建 PR / Issue │ │
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 原生浅色/深色主题自适应与 2x2 黄金操作栏
69
+ ### 2.4 原生浅色/深色主题自适应与快捷操作网格
69
70
  - 全面对齐 DSH 官方 DSW 语义设计变量,基于 `body[data-ds-dark-theme]` 驱动无闪烁双模式切换;
70
- - 右侧栏 2x2 快捷操作栏彻底废弃失效的 Electron `prompt()` 阻塞弹窗,全面采用官方直达模式(Compare / New Issue),享有 100% 官方富文本体验。
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
- issues = issuesRes.data;
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. 个人 Issues
286
- // 超时压到 8 秒:本机实测 `gh search issues --author=@me` 单次就要 10 秒,
287
- // 它是这组的木桶短板,直接决定首屏等待时间。面板查询不值得为它多等 4 秒。
288
- const [reposRes, prsRes, issuesRes] = await Promise.all([
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', '--author=@me', '--state=open', '--limit', '10', '--json', 'number,title,repository,url,updatedAt'], { timeoutMs: 8_000 }),
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
- if (issuesRes.ok && Array.isArray(issuesRes.data)) {
309
- myIssues = issuesRes.data;
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
  };
@@ -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. 📝 新建 Issue (官方 Issue 模板页直达)
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') }, `📋 个人待办 (PR: ${myPrs.length} · Issue: ${myIssues.length})`));
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
- 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 (${myIssues.length})`), myIssues.length === 0 ? h("div", { className: "dsh-github-empty" }, "暂无开放中的 Issue") : h("div", { className: "dsh-github-list" }, myIssues.map((is) => h("div", { key: is.number, className: "dsh-github-item" }, h("a", { className: "dsh-github-item-title", href: is.url, target: "_blank" }, `[${is.repository?.nameWithOwner || 'Repo'}] #${is.number} ${is.title}`)))));
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. 读取当前进程/会话的 DSH_SESSION_ID 并匹配 ~/.dsh/storages/workspace.json
25
- * 3. 匹配 ~/.dsh/storages/workspace.json 中最近活跃的工作区
26
- * 4. 回退到 process.cwd()
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. 读取当前进程/会话的 DSH_SESSION_ID 并匹配 ~/.dsh/storages/workspace.json
123
- * 3. 匹配 ~/.dsh/storages/workspace.json 中最近活跃的工作区
124
- * 4. 回退到 process.cwd()
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
- // 显式路径优先,但只认真正含 .git 的目录:调用方传进来的可能是会话根目录而非仓库目录,
128
- // 此时继续往下走按会话/最近活跃反查,比盲信传入值更准。
127
+ resolveWorkspaceCwd(explicitCwd, explicitSessionId) {
128
+ // 1. 显式路径优先,若有效且直接包含 .git 目录,则直接使用
129
129
  if (explicitCwd && hasGitDir(explicitCwd)) {
130
130
  return explicitCwd;
131
131
  }
132
- const sessionId = process.env.DSH_SESSION_ID;
133
- if (sessionId) {
134
- const bySession = findWorkspacePathBySession(sessionId);
135
- if (bySession)
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);
@@ -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
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
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'], { cwd });
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'], { cwd });
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, { cwd });
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], { cwd });
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], { cwd });
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], { cwd });
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
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
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'], { cwd });
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'], { cwd });
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'], { cwd });
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], { cwd });
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, { cwd });
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, { cwd });
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, { cwd });
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'], { cwd });
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], { cwd });
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], { cwd });
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
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
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)], { cwd });
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'], { cwd });
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
- cwd,
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'], { cwd });
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], { cwd });
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: Array<{
157
- number: number;
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
  /** 工作区矩阵读取失败的原因。区分「没有工作区」与「读不到工作区」,避免把故障显示成空状态 */
@@ -27,9 +27,12 @@ export declare function tryReadWorkspaces(): {
27
27
  */
28
28
  export declare function findWorkspacePathBySession(sessionId: string): string | undefined;
29
29
  /**
30
- * 匹配最近活跃的工作区路径。
31
- * 注意 updatedAt 可能缺失或非 ISO 格式,统一用时间戳比较,无效值视为 0 而非 NaN——
32
- * 后者会让所有比较都返回 false,导致永远选不中任何工作区。
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
- * 注意 updatedAt 可能缺失或非 ISO 格式,统一用时间戳比较,无效值视为 0 而非 NaN——
84
- * 后者会让所有比较都返回 false,导致永远选不中任何工作区。
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 best = null;
90
- let bestAt = -1;
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 (score > bestAt && ws.path && fs.existsSync(ws.path)) {
95
- best = ws;
96
- bestAt = score;
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 best?.path;
114
+ return bestGitWs?.path || fallbackWs?.path;
100
115
  }
101
116
  catch {
102
117
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-github-flow",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "icon": "icon.svg",
5
5
  "description": "GitHub Flow: Native GitHub CLI (gh) integration for DeepSeek Harness — Agent Tools, Slash Commands, and Web UI Workspace",
6
6
  "type": "module",