dsh-github-flow 0.1.2 → 0.1.4
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 +7 -5
- package/README.md +7 -5
- package/cordis.patch.yml +2 -2
- package/docs/architecture.md +14 -8
- package/lib/client.js +107 -34
- package/lib/executor.d.ts +8 -0
- package/lib/executor.js +61 -14
- package/lib/index.d.ts +2 -4
- package/lib/index.js +2 -30
- package/lib/main.d.ts +17 -1
- package/lib/main.js +33 -5
- package/lib/tools/api.d.ts +2 -2
- package/lib/tools/api.js +19 -2
- package/lib/tools/issue.d.ts +2 -2
- package/lib/tools/issue.js +28 -4
- package/lib/tools/pr.d.ts +2 -2
- package/lib/tools/pr.js +30 -6
- package/lib/tools/repo.d.ts +2 -2
- package/lib/tools/repo.js +36 -6
- package/lib/tools/run.d.ts +2 -2
- package/lib/tools/run.js +29 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
|
|
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
|
-
2. **Deep Workspace Affinity
|
|
26
|
-
- Automatically scopes commands to the active session workspace
|
|
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.
|
|
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**:
|
|
31
32
|
- **Global Cockpit** (`sidebar.panellist`, Order 20): Account-wide repository overview, local workspace matrix, and pending personal PRs/issues.
|
|
32
|
-
- **Right Sidebar Panel** (`sidebarRightTabs`, Order 40): Shows current repository status, PR list with CI check pills, and quick action
|
|
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.
|
|
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.
|
|
33
35
|
5. **Standard Cordis Microkernel Compliance**:
|
|
34
36
|
- Exposes native `GitHubService` (`ctx.github`) for ecosystem extensibility;
|
|
35
|
-
- Strongly typed runtime validation via
|
|
37
|
+
- Strongly typed runtime validation via Standard Schema (`Config`);
|
|
36
38
|
- Domain event broadcasting (`github/pr:create`, `github/issue:create`).
|
|
37
39
|
|
|
38
40
|
---
|
package/README.md
CHANGED
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
|
|
23
23
|
1. **零鉴权配置,开箱即用**:
|
|
24
24
|
- 自动复用宿主机已通过 `gh auth login` 认证的登录凭据(OAuth Token / 系统 Keyring),无需在 DSH 中存储任何明文 GitHub PAT。
|
|
25
|
-
2.
|
|
26
|
-
-
|
|
25
|
+
2. **工作区目录深度亲和与零参数探测 (CWD & Git Auto-Binding)**:
|
|
26
|
+
- 自动绑定当前会话所在的物理工作区目录,原生直读本地 `.git/config` 并映射 `workspace.json`,无需 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 双重视图与双模式自适应**:
|
|
31
32
|
- **全局驾驶舱**(主导航栏 `sidebar.panellist`,Order 20):透视账号全局仓库、本地工作区矩阵、个人待办 PR 与 Issues;
|
|
32
|
-
- **右侧栏工作区面板**(`sidebarRightTabs`,Order 40
|
|
33
|
+
- **右侧栏工作区面板**(`sidebarRightTabs`,Order 40):常驻呈现当前仓库的 PR 状态、Checks 门禁药丸与 2x2 黄金快捷网格(浏览器打开、查看 PRs、快速创建 PR、新建 Issue),一键直达官方比对与创建页,零弹窗依赖;
|
|
34
|
+
- **全场景双模式原生自适应**:深度契合 DSH 官方 DSW(DeepSeek Web)设计变量规范,通过 `body[data-ds-dark-theme]` 驱动浅色与深色主题实时零延迟平滑切换。
|
|
33
35
|
5. **符合 Cordis 官方微内核架构**:
|
|
34
36
|
- 暴露原生 `GitHubService`(`ctx.github`),允许生态其他插件复用;
|
|
35
|
-
-
|
|
37
|
+
- 具备符合 Standard Schema 规范的运行时配置校验(`Config`);
|
|
36
38
|
- 支持领域事件系统(`github/pr:create`, `github/issue:create`)。
|
|
37
39
|
|
|
38
40
|
---
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- insert:
|
|
2
|
-
- id: github-flow
|
|
3
|
-
name:
|
|
2
|
+
- id: github-flow
|
|
3
|
+
name: dsh-github-flow
|
package/docs/architecture.md
CHANGED
|
@@ -55,15 +55,21 @@
|
|
|
55
55
|
- 直接复用宿主机由 `gh auth login` 认证的安全凭证(系统 Keychain / Windows Credential Manager / 系统的加密存储);
|
|
56
56
|
- 天然支持多账号、企业 GitHub (GH Enterprise) 以及各种 OAuth 作用域。
|
|
57
57
|
|
|
58
|
-
### 2.2
|
|
59
|
-
-
|
|
60
|
-
-
|
|
58
|
+
### 2.2 工作区深度亲和与零参数探测 (Workspace Affinity & Auto-Binding)
|
|
59
|
+
- 结合 `$env:DSH_SESSION_ID` 逆向解析 `~/.dsh/storages/workspace.json`,自动锁定当前活跃会话物理工作区;
|
|
60
|
+
- 原生免子进程直接读取工作区 `.git/config`,自动识别远程仓库,免去大模型或人类每次传递 `owner/repo` 的繁重上下文负担;
|
|
61
|
+
- 具备离线优雅兜底机制,无论在何种受限环境下调用均不崩溃。
|
|
61
62
|
|
|
62
|
-
### 2.3 上下文 Token
|
|
63
|
+
### 2.3 上下文 Token 保护与防御性输出包装 (Token Budget Guard)
|
|
63
64
|
- 所有的 `gh` 查询原生采用 `--json <fields>` 结构化投影;
|
|
64
|
-
- 对长文本输出(如大型 PR 的完整 `diff`、CI 失败步骤日志)设置安全截断阈值(默认 24KB
|
|
65
|
+
- 对长文本输出(如大型 PR 的完整 `diff`、CI 失败步骤日志)设置安全截断阈值(默认 24KB),保留头尾关键上下文并添加显式截断标记,杜绝撑爆模型上下文;
|
|
66
|
+
- 所有 Agent 工具的输出统一经过 `normalizeOutput` 归一化,列表自动打包为 `{ items, count }`,标量打包为 `{ result }`,100% 严丝合缝契合 DSH 工具网关模式约束。
|
|
65
67
|
|
|
66
|
-
### 2.4
|
|
67
|
-
-
|
|
68
|
+
### 2.4 原生浅色/深色主题自适应与 2x2 黄金操作栏
|
|
69
|
+
- 全面对齐 DSH 官方 DSW 语义设计变量,基于 `body[data-ds-dark-theme]` 驱动无闪烁双模式切换;
|
|
70
|
+
- 右侧栏 2x2 快捷操作栏彻底废弃失效的 Electron `prompt()` 阻塞弹窗,全面采用官方直达模式(Compare / New Issue),享有 100% 官方富文本体验。
|
|
71
|
+
|
|
72
|
+
### 2.5 Cordis 微内核标准规范
|
|
73
|
+
- **服务依赖就绪**:声明 `['tools', 'commands', 'webServer', 'workspaceRegistry']`,确保 Web 路由与命令服务在环境就绪后激活;
|
|
68
74
|
- **生命周期 Effect 闭环**:所有 HTTP 路由与命令注册均持有 Disposer 清理函数,插件热重载或重新挂载时自动彻底注销,零内存泄漏;
|
|
69
|
-
-
|
|
75
|
+
- **配置自校验与服务暴露**:基于 Standard Schema 规范实现强类型校验与默认值注入,通过 `ctx.provide('github', executor)` 向生态插件暴露单例服务。
|
package/lib/client.js
CHANGED
|
@@ -304,6 +304,80 @@ body[data-ds-dark-theme] .dsh-github-action-btn:hover {
|
|
|
304
304
|
color: #58a6ff;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/* 组合式快捷操作按钮 (AI 智能装填主操作 + 官方外链直达) */
|
|
308
|
+
.dsh-github-split-btn {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: stretch;
|
|
311
|
+
border-radius: 6px;
|
|
312
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
313
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
transition: all 0.15s ease;
|
|
316
|
+
}
|
|
317
|
+
.dsh-github-split-btn:hover {
|
|
318
|
+
border-color: var(--dsw-alias-link, #0969da);
|
|
319
|
+
}
|
|
320
|
+
body[data-ds-dark-theme] .dsh-github-split-btn {
|
|
321
|
+
border-color: var(--dsw-alias-border-l1, #444);
|
|
322
|
+
background: var(--dsw-alias-bg-layer-2, #28282b);
|
|
323
|
+
}
|
|
324
|
+
body[data-ds-dark-theme] .dsh-github-split-btn:hover {
|
|
325
|
+
border-color: #58a6ff;
|
|
326
|
+
}
|
|
327
|
+
.dsh-github-split-main {
|
|
328
|
+
flex: 1;
|
|
329
|
+
display: flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
justify-content: center;
|
|
332
|
+
gap: 5px;
|
|
333
|
+
padding: 8px 8px;
|
|
334
|
+
border: 0;
|
|
335
|
+
background: transparent;
|
|
336
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
337
|
+
font-size: 12px;
|
|
338
|
+
font-weight: 500;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
transition: all 0.15s ease;
|
|
341
|
+
white-space: nowrap;
|
|
342
|
+
}
|
|
343
|
+
.dsh-github-split-main:hover {
|
|
344
|
+
background: var(--dsw-alias-interactive-bg-hover, #ebeef2);
|
|
345
|
+
color: var(--dsw-alias-link, #0969da);
|
|
346
|
+
}
|
|
347
|
+
body[data-ds-dark-theme] .dsh-github-split-main {
|
|
348
|
+
color: var(--dsw-alias-label-primary, #fff);
|
|
349
|
+
}
|
|
350
|
+
body[data-ds-dark-theme] .dsh-github-split-main:hover {
|
|
351
|
+
background: var(--dsw-alias-interactive-bg-hover, #343438);
|
|
352
|
+
color: #58a6ff;
|
|
353
|
+
}
|
|
354
|
+
.dsh-github-split-link {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
padding: 0 8px;
|
|
359
|
+
border-left: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
360
|
+
background: transparent;
|
|
361
|
+
color: var(--dsw-alias-label-tertiary, #656d76);
|
|
362
|
+
font-size: 12px;
|
|
363
|
+
font-weight: bold;
|
|
364
|
+
text-decoration: none !important;
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
transition: all 0.15s ease;
|
|
367
|
+
}
|
|
368
|
+
.dsh-github-split-link:hover {
|
|
369
|
+
background: var(--dsw-alias-interactive-bg-hover, #ebeef2);
|
|
370
|
+
color: var(--dsw-alias-link, #0969da);
|
|
371
|
+
}
|
|
372
|
+
body[data-ds-dark-theme] .dsh-github-split-link {
|
|
373
|
+
border-left-color: var(--dsw-alias-border-l1, #444);
|
|
374
|
+
color: var(--dsw-alias-label-secondary, #9ca3af);
|
|
375
|
+
}
|
|
376
|
+
body[data-ds-dark-theme] .dsh-github-split-link:hover {
|
|
377
|
+
background: var(--dsw-alias-interactive-bg-hover, #343438);
|
|
378
|
+
color: #58a6ff;
|
|
379
|
+
}
|
|
380
|
+
|
|
307
381
|
/* 9. 模块分区与列表 */
|
|
308
382
|
.dsh-github-section {
|
|
309
383
|
margin-bottom: 20px;
|
|
@@ -609,23 +683,6 @@ body[data-ds-dark-theme] .dsh-github-subheading {
|
|
|
609
683
|
onClick: onRefresh,
|
|
610
684
|
disabled: loading,
|
|
611
685
|
}, loading ? "刷新中..." : "🔄 刷新"));
|
|
612
|
-
// 操作执行函数
|
|
613
|
-
const runAction = async (action, extra = {}) => {
|
|
614
|
-
try {
|
|
615
|
-
const res = await fetch("/api/github/action", {
|
|
616
|
-
method: "POST",
|
|
617
|
-
headers: { "Content-Type": "application/json" },
|
|
618
|
-
body: JSON.stringify({ action, ...extra }),
|
|
619
|
-
});
|
|
620
|
-
const json = await res.json();
|
|
621
|
-
alert(json.ok ? `✅ ${json.message || "操作已完成"}` : `❌ ${json.error || "操作失败"}`);
|
|
622
|
-
if (json.ok)
|
|
623
|
-
onRefresh();
|
|
624
|
-
}
|
|
625
|
-
catch (err) {
|
|
626
|
-
alert(`网络异常: ${err.message}`);
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
686
|
// 仓库基础卡片
|
|
630
687
|
var repoCardEl = h("div", { className: "dsh-github-card" }, h("div", { className: "dsh-github-card-title" }, "当前工作区 GitHub 仓库"), h("div", { className: "dsh-github-card-value" }, repo
|
|
631
688
|
? h("a", { href: repo.url, target: "_blank", className: "dsh-github-repo-link" }, repo.nameWithOwner)
|
|
@@ -636,40 +693,46 @@ body[data-ds-dark-theme] .dsh-github-subheading {
|
|
|
636
693
|
: null, !repo
|
|
637
694
|
? h("div", { className: "dsh-github-card-subtext" }, "若本地已有 git 仓库,请运行 `git remote add github <url>` 或 `git remote add origin <url>` 关联远程仓库。")
|
|
638
695
|
: null);
|
|
639
|
-
// 快捷操作栏
|
|
640
|
-
var actionsBarEl = h("div", { className: "dsh-github-quick-actions" },
|
|
696
|
+
// 快捷操作栏 (方案 1: 官方黄金功能直达)
|
|
697
|
+
var actionsBarEl = h("div", { className: "dsh-github-quick-actions" },
|
|
698
|
+
// 1. 浏览器打开仓库主页
|
|
699
|
+
h("button", {
|
|
641
700
|
className: "dsh-github-action-btn",
|
|
701
|
+
title: "在系统默认浏览器中打开当前仓库主页",
|
|
642
702
|
onClick: () => {
|
|
643
703
|
if (repo?.url) {
|
|
644
704
|
window.open(repo.url, "_blank", "noopener,noreferrer");
|
|
645
705
|
}
|
|
646
|
-
else {
|
|
647
|
-
alert("当前工作区尚未关联 GitHub 仓库");
|
|
648
|
-
}
|
|
649
706
|
},
|
|
650
|
-
}, "🌐 浏览器打开"),
|
|
707
|
+
}, "🌐 浏览器打开"),
|
|
708
|
+
// 2. 查看所有 PRs
|
|
709
|
+
h("button", {
|
|
651
710
|
className: "dsh-github-action-btn",
|
|
711
|
+
title: "在系统默认浏览器中查看所有 Pull Requests",
|
|
652
712
|
onClick: () => {
|
|
653
713
|
if (repo?.url) {
|
|
654
714
|
window.open(`${repo.url}/pulls`, "_blank", "noopener,noreferrer");
|
|
655
715
|
}
|
|
656
|
-
else {
|
|
657
|
-
alert("当前工作区尚未关联 GitHub 仓库");
|
|
658
|
-
}
|
|
659
716
|
},
|
|
660
|
-
}, "🔀 查看所有 PRs"),
|
|
717
|
+
}, "🔀 查看所有 PRs"),
|
|
718
|
+
// 3. 🚀 快速创建 PR (官方对比与创建页直达)
|
|
719
|
+
h("button", {
|
|
661
720
|
className: "dsh-github-action-btn",
|
|
721
|
+
title: "在系统默认浏览器中打开 GitHub 官方对比与创建 Pull Request 页面",
|
|
662
722
|
onClick: () => {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
723
|
+
if (repo?.url) {
|
|
724
|
+
window.open(`${repo.url}/compare`, "_blank", "noopener,noreferrer");
|
|
725
|
+
}
|
|
666
726
|
},
|
|
667
|
-
}, "🚀 快速创建 PR"),
|
|
727
|
+
}, "🚀 快速创建 PR"),
|
|
728
|
+
// 4. 📝 新建 Issue (官方 Issue 模板页直达)
|
|
729
|
+
h("button", {
|
|
668
730
|
className: "dsh-github-action-btn",
|
|
731
|
+
title: "在系统默认浏览器中打开 GitHub 官方新建 Issue 页面",
|
|
669
732
|
onClick: () => {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
733
|
+
if (repo?.url) {
|
|
734
|
+
window.open(`${repo.url}/issues/new`, "_blank", "noopener,noreferrer");
|
|
735
|
+
}
|
|
673
736
|
},
|
|
674
737
|
}, "📝 新建 Issue"));
|
|
675
738
|
// PR 列表
|
|
@@ -720,6 +783,11 @@ body[data-ds-dark-theme] .dsh-github-subheading {
|
|
|
720
783
|
url += "?" + params.join("&");
|
|
721
784
|
}
|
|
722
785
|
var res = await fetch(url);
|
|
786
|
+
if (!res.ok) {
|
|
787
|
+
var errText = await res.text().catch(() => "");
|
|
788
|
+
setError(`后端服务未就绪 (${res.status}): ${errText || res.statusText}`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
723
791
|
var json = await res.json();
|
|
724
792
|
if (json.ok && json.data) {
|
|
725
793
|
setData(json.data);
|
|
@@ -773,6 +841,11 @@ body[data-ds-dark-theme] .dsh-github-subheading {
|
|
|
773
841
|
if (forceRefresh)
|
|
774
842
|
await fetch("/api/github/refresh", { method: "POST" }).catch(() => { });
|
|
775
843
|
var res = await fetch("/api/github/global-overview");
|
|
844
|
+
if (!res.ok) {
|
|
845
|
+
var errText = await res.text().catch(() => "");
|
|
846
|
+
setError(`后端服务未就绪或接口不存在 (${res.status}): ${errText || res.statusText}`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
776
849
|
var json = await res.json();
|
|
777
850
|
if (json.ok && json.data)
|
|
778
851
|
setData(json.data);
|
package/lib/executor.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export declare class GhExecutor {
|
|
|
5
5
|
maxOutputChars: number;
|
|
6
6
|
constructor(config?: PluginConfig);
|
|
7
7
|
updateConfig(config?: PluginConfig): void;
|
|
8
|
+
/**
|
|
9
|
+
* 智能解析最可靠的工作区物理路径:
|
|
10
|
+
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
11
|
+
* 2. 读取当前进程/会话的 DSH_SESSION_ID 并匹配 ~/.dsh/storages/workspace.json
|
|
12
|
+
* 3. 匹配 ~/.dsh/storages/workspace.json 中最近活跃的工作区
|
|
13
|
+
* 4. 回退到 process.cwd()
|
|
14
|
+
*/
|
|
15
|
+
resolveWorkspaceCwd(explicitCwd?: string): string;
|
|
8
16
|
/**
|
|
9
17
|
* 安全执行 gh 命令
|
|
10
18
|
* 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
|
package/lib/executor.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
3
6
|
const execFileAsync = promisify(execFile);
|
|
4
7
|
export class GhExecutor {
|
|
5
8
|
ghPath;
|
|
@@ -18,12 +21,61 @@ export class GhExecutor {
|
|
|
18
21
|
if (config.maxOutputChars)
|
|
19
22
|
this.maxOutputChars = config.maxOutputChars;
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 智能解析最可靠的工作区物理路径:
|
|
26
|
+
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
27
|
+
* 2. 读取当前进程/会话的 DSH_SESSION_ID 并匹配 ~/.dsh/storages/workspace.json
|
|
28
|
+
* 3. 匹配 ~/.dsh/storages/workspace.json 中最近活跃的工作区
|
|
29
|
+
* 4. 回退到 process.cwd()
|
|
30
|
+
*/
|
|
31
|
+
resolveWorkspaceCwd(explicitCwd) {
|
|
32
|
+
if (explicitCwd) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(path.join(explicitCwd, '.git'))) {
|
|
35
|
+
return explicitCwd;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const wsJsonPath = path.join(os.homedir(), '.dsh', 'storages', 'workspace.json');
|
|
42
|
+
if (fs.existsSync(wsJsonPath)) {
|
|
43
|
+
const wsData = JSON.parse(fs.readFileSync(wsJsonPath, 'utf8'));
|
|
44
|
+
const workspaces = wsData.tables?.workspaces || {};
|
|
45
|
+
// A. 优先通过 DSH 会话 ID 精准匹配物理工作区
|
|
46
|
+
const sessionId = process.env.DSH_SESSION_ID;
|
|
47
|
+
if (sessionId) {
|
|
48
|
+
for (const wsId of Object.keys(workspaces)) {
|
|
49
|
+
const item = workspaces[wsId];
|
|
50
|
+
if (item.sessionIds && Array.isArray(item.sessionIds) && item.sessionIds.includes(sessionId)) {
|
|
51
|
+
if (item.path && fs.existsSync(item.path)) {
|
|
52
|
+
return item.path;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// B. 兜底匹配最近活跃更新的工作区
|
|
58
|
+
let latestItem = null;
|
|
59
|
+
for (const wsId of Object.keys(workspaces)) {
|
|
60
|
+
const item = workspaces[wsId];
|
|
61
|
+
if (!latestItem || new Date(item.updatedAt || 0) > new Date(latestItem.updatedAt || 0)) {
|
|
62
|
+
latestItem = item;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (latestItem && latestItem.path && fs.existsSync(latestItem.path)) {
|
|
66
|
+
return latestItem.path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
return explicitCwd || process.cwd();
|
|
72
|
+
}
|
|
21
73
|
/**
|
|
22
74
|
* 安全执行 gh 命令
|
|
23
75
|
* 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
|
|
24
76
|
*/
|
|
25
77
|
async run(args, options = {}) {
|
|
26
|
-
const cwd = options.cwd
|
|
78
|
+
const cwd = this.resolveWorkspaceCwd(options.cwd);
|
|
27
79
|
const timeout = options.timeoutMs || this.defaultTimeoutMs;
|
|
28
80
|
try {
|
|
29
81
|
const { stdout, stderr } = await execFileAsync(this.ghPath, args, {
|
|
@@ -144,7 +196,7 @@ export class GhExecutor {
|
|
|
144
196
|
* 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
|
|
145
197
|
*/
|
|
146
198
|
async getRepoMetadata(cwd) {
|
|
147
|
-
const targetCwd =
|
|
199
|
+
const targetCwd = this.resolveWorkspaceCwd(cwd);
|
|
148
200
|
// 1. 优先尝试由 gh 直接根据当前上下文识别
|
|
149
201
|
const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
150
202
|
if (res.ok && res.data && res.data.nameWithOwner) {
|
|
@@ -159,20 +211,17 @@ export class GhExecutor {
|
|
|
159
211
|
url: d.url || `https://github.com/${d.nameWithOwner}`,
|
|
160
212
|
};
|
|
161
213
|
}
|
|
162
|
-
// 2.
|
|
214
|
+
// 2. 原生文件解析:直接读取本地 .git/config 文件(0 子进程开销、免疫沙箱 EPERM)
|
|
163
215
|
try {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (remoteOut) {
|
|
169
|
-
// 匹配 github.com 格式:git@github.com:owner/repo.git 或 https://github.com/owner/repo.git
|
|
170
|
-
const match = remoteOut.match(/github\.com[:/]([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?(?:\s|\()/i);
|
|
216
|
+
const gitConfigPath = path.join(targetCwd, '.git', 'config');
|
|
217
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
218
|
+
const configText = fs.readFileSync(gitConfigPath, 'utf8');
|
|
219
|
+
const match = configText.match(/github\.com[:/]([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?(?:\s|\r|\n|$)/i);
|
|
171
220
|
if (match) {
|
|
172
221
|
const owner = match[1];
|
|
173
222
|
const repoName = match[2];
|
|
174
223
|
const nameWithOwner = `${owner}/${repoName}`;
|
|
175
|
-
//
|
|
224
|
+
// 使用精确的 owner/repo 向 gh 获取元数据(显式传参,不依赖无上下文的探测)
|
|
176
225
|
const directRes = await this.run(['repo', 'view', nameWithOwner, '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
177
226
|
if (directRes.ok && directRes.data && directRes.data.nameWithOwner) {
|
|
178
227
|
const d = directRes.data;
|
|
@@ -199,9 +248,7 @@ export class GhExecutor {
|
|
|
199
248
|
}
|
|
200
249
|
}
|
|
201
250
|
}
|
|
202
|
-
catch {
|
|
203
|
-
// 本地无 git 或无 remote 配置
|
|
204
|
-
}
|
|
251
|
+
catch { }
|
|
205
252
|
return null;
|
|
206
253
|
}
|
|
207
254
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export declare function apply(ctx: any): void;
|
|
4
|
-
export * from './types.js';
|
|
1
|
+
export * from './main.js';
|
|
2
|
+
export { apply as default } from './main.js';
|
package/lib/index.js
CHANGED
|
@@ -1,30 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createIssueTool } from './tools/issue.js';
|
|
4
|
-
import { createRunTool } from './tools/run.js';
|
|
5
|
-
import { createRepoTool } from './tools/repo.js';
|
|
6
|
-
import { createApiTool } from './tools/api.js';
|
|
7
|
-
import { registerGhCommand } from './commands/gh.js';
|
|
8
|
-
import { registerApiRoutes } from './api/routes.js';
|
|
9
|
-
export const name = 'dsh-github-flow';
|
|
10
|
-
export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
|
|
11
|
-
export function apply(ctx) {
|
|
12
|
-
const executor = new GhExecutor();
|
|
13
|
-
// 1. 注册人类 Slash 命令 (/gh status, /gh help)
|
|
14
|
-
if (ctx.commands) {
|
|
15
|
-
registerGhCommand(ctx.commands, executor);
|
|
16
|
-
}
|
|
17
|
-
// 2. 注册 Agent 模型工具集(5 大核心领域工具)
|
|
18
|
-
if (ctx.tools) {
|
|
19
|
-
ctx.tools.register(createPrTool(executor));
|
|
20
|
-
ctx.tools.register(createIssueTool(executor));
|
|
21
|
-
ctx.tools.register(createRunTool(executor));
|
|
22
|
-
ctx.tools.register(createRepoTool(executor));
|
|
23
|
-
ctx.tools.register(createApiTool(executor));
|
|
24
|
-
}
|
|
25
|
-
// 3. 注册面向 Client 端的 WebServer HTTP 路由
|
|
26
|
-
if (ctx.webServer) {
|
|
27
|
-
registerApiRoutes(ctx.webServer, executor, ctx.workspaceRegistry);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export * from './types.js';
|
|
1
|
+
export * from './main.js';
|
|
2
|
+
export { apply as default } from './main.js';
|
package/lib/main.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import { GhExecutor } from './executor.js';
|
|
2
|
+
import type { PluginConfig } from './types.js';
|
|
1
3
|
export declare const name = "dsh-github-flow";
|
|
2
4
|
export declare const inject: readonly ['tools', 'commands', 'webServer', 'workspaceRegistry'];
|
|
3
|
-
export
|
|
5
|
+
export type Config = PluginConfig;
|
|
6
|
+
/**
|
|
7
|
+
* 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
|
|
8
|
+
*/
|
|
9
|
+
export declare const Config: {
|
|
10
|
+
'~standard': {
|
|
11
|
+
version: number;
|
|
12
|
+
vendor: string;
|
|
13
|
+
validate(value: unknown): {
|
|
14
|
+
value: PluginConfig;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function apply(ctx: any, config?: PluginConfig): void;
|
|
19
|
+
export type GitHubService = GhExecutor;
|
|
4
20
|
export * from './types.js';
|
package/lib/main.js
CHANGED
|
@@ -8,13 +8,41 @@ import { registerGhCommand } from './commands/gh.js';
|
|
|
8
8
|
import { registerApiRoutes } from './api/routes.js';
|
|
9
9
|
export const name = 'dsh-github-flow';
|
|
10
10
|
export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
|
|
13
|
+
*/
|
|
14
|
+
export const Config = {
|
|
15
|
+
'~standard': {
|
|
16
|
+
version: 1,
|
|
17
|
+
vendor: 'dsh',
|
|
18
|
+
validate(value) {
|
|
19
|
+
const cfg = (typeof value === 'object' && value !== null) ? value : {};
|
|
20
|
+
return {
|
|
21
|
+
value: {
|
|
22
|
+
ghPath: typeof cfg.ghPath === 'string' ? cfg.ghPath : 'gh',
|
|
23
|
+
defaultTimeoutMs: typeof cfg.defaultTimeoutMs === 'number' ? cfg.defaultTimeoutMs : 30_000,
|
|
24
|
+
maxOutputChars: typeof cfg.maxOutputChars === 'number' ? cfg.maxOutputChars : 24_000,
|
|
25
|
+
cacheTtlMs: typeof cfg.cacheTtlMs === 'number' ? cfg.cacheTtlMs : 15_000,
|
|
26
|
+
defaultListLimit: typeof cfg.defaultListLimit === 'number' ? cfg.defaultListLimit : 20,
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export function apply(ctx, config = {}) {
|
|
33
|
+
const executor = new GhExecutor(config);
|
|
34
|
+
// 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
|
|
35
|
+
if (ctx.provide && typeof ctx.provide === 'function') {
|
|
36
|
+
ctx.provide('github', executor);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
ctx.github = executor;
|
|
40
|
+
}
|
|
41
|
+
// 2. 注册人类 Slash 命令 (/gh status, /gh help)
|
|
14
42
|
if (ctx.commands) {
|
|
15
43
|
registerGhCommand(ctx.commands, executor);
|
|
16
44
|
}
|
|
17
|
-
//
|
|
45
|
+
// 3. 注册 Agent 模型工具集(5 大核心领域工具)
|
|
18
46
|
if (ctx.tools) {
|
|
19
47
|
ctx.tools.register(createPrTool(executor));
|
|
20
48
|
ctx.tools.register(createIssueTool(executor));
|
|
@@ -22,7 +50,7 @@ export function apply(ctx) {
|
|
|
22
50
|
ctx.tools.register(createRepoTool(executor));
|
|
23
51
|
ctx.tools.register(createApiTool(executor));
|
|
24
52
|
}
|
|
25
|
-
//
|
|
53
|
+
// 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
|
|
26
54
|
if (ctx.webServer) {
|
|
27
55
|
registerApiRoutes(ctx.webServer, executor, ctx.workspaceRegistry);
|
|
28
56
|
}
|
package/lib/tools/api.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export declare function createApiTool(executor: GhExecutor): {
|
|
|
31
31
|
};
|
|
32
32
|
render(args: any, value: any): {
|
|
33
33
|
type: string;
|
|
34
|
-
text:
|
|
34
|
+
text: any;
|
|
35
35
|
}[];
|
|
36
36
|
};
|
|
37
|
-
execute(args: any, execContext: any): Promise<
|
|
37
|
+
execute(args: any, execContext: any): Promise<Record<string, any>>;
|
|
38
38
|
};
|
package/lib/tools/api.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function normalizeOutput(data) {
|
|
2
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(data)) {
|
|
6
|
+
return { items: data, count: data.length };
|
|
7
|
+
}
|
|
8
|
+
return { result: data !== undefined && data !== null ? data : '' };
|
|
9
|
+
}
|
|
1
10
|
export function createApiTool(executor) {
|
|
2
11
|
return {
|
|
3
12
|
name: 'github_api',
|
|
@@ -28,7 +37,15 @@ export function createApiTool(executor) {
|
|
|
28
37
|
output: {
|
|
29
38
|
schema: { type: 'object' },
|
|
30
39
|
render(args, value) {
|
|
31
|
-
|
|
40
|
+
if (!value)
|
|
41
|
+
return [{ type: 'text', text: '无返回内容' }];
|
|
42
|
+
if (typeof value.result === 'string') {
|
|
43
|
+
return [{ type: 'text', text: value.result }];
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value.items)) {
|
|
46
|
+
return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
|
|
47
|
+
}
|
|
48
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
32
49
|
},
|
|
33
50
|
},
|
|
34
51
|
async execute(args, execContext) {
|
|
@@ -48,7 +65,7 @@ export function createApiTool(executor) {
|
|
|
48
65
|
const res = await executor.run(cmd, { cwd });
|
|
49
66
|
if (!res.ok)
|
|
50
67
|
throw new Error(res.error);
|
|
51
|
-
return res.data
|
|
68
|
+
return normalizeOutput(res.data !== undefined ? res.data : res.rawOutput);
|
|
52
69
|
},
|
|
53
70
|
};
|
|
54
71
|
}
|
package/lib/tools/issue.d.ts
CHANGED
|
@@ -49,8 +49,8 @@ export declare function createIssueTool(executor: GhExecutor): {
|
|
|
49
49
|
};
|
|
50
50
|
render(args: any, value: any): {
|
|
51
51
|
type: string;
|
|
52
|
-
text:
|
|
52
|
+
text: any;
|
|
53
53
|
}[];
|
|
54
54
|
};
|
|
55
|
-
execute(args: any, execContext: any): Promise<
|
|
55
|
+
execute(args: any, execContext: any): Promise<Record<string, any>>;
|
|
56
56
|
};
|
package/lib/tools/issue.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function normalizeOutput(data) {
|
|
2
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(data)) {
|
|
6
|
+
return { items: data, count: data.length };
|
|
7
|
+
}
|
|
8
|
+
return { result: data !== undefined && data !== null ? data : '' };
|
|
9
|
+
}
|
|
1
10
|
export function createIssueTool(executor) {
|
|
2
11
|
return {
|
|
3
12
|
name: 'github_issue',
|
|
@@ -42,18 +51,33 @@ export function createIssueTool(executor) {
|
|
|
42
51
|
output: {
|
|
43
52
|
schema: { type: 'object' },
|
|
44
53
|
render(args, value) {
|
|
45
|
-
|
|
54
|
+
if (!value)
|
|
55
|
+
return [{ type: 'text', text: '无返回内容' }];
|
|
56
|
+
if (typeof value.result === 'string') {
|
|
57
|
+
return [{ type: 'text', text: value.result }];
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value.items)) {
|
|
60
|
+
return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
|
|
61
|
+
}
|
|
62
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
46
63
|
},
|
|
47
64
|
},
|
|
48
65
|
async execute(args, execContext) {
|
|
49
66
|
const cwd = execContext?.cwd || process.cwd();
|
|
50
|
-
|
|
67
|
+
let repoTarget = args.repo;
|
|
68
|
+
if (!repoTarget) {
|
|
69
|
+
const meta = await executor.getRepoMetadata(cwd);
|
|
70
|
+
if (meta?.nameWithOwner) {
|
|
71
|
+
repoTarget = meta.nameWithOwner;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const repoArgs = repoTarget ? ['-R', repoTarget] : [];
|
|
51
75
|
switch (args.action) {
|
|
52
76
|
case 'list': {
|
|
53
77
|
const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], { cwd });
|
|
54
78
|
if (!res.ok)
|
|
55
79
|
throw new Error(res.error);
|
|
56
|
-
return res.data || res.rawOutput;
|
|
80
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
57
81
|
}
|
|
58
82
|
case 'view': {
|
|
59
83
|
if (!args.issue_number)
|
|
@@ -61,7 +85,7 @@ export function createIssueTool(executor) {
|
|
|
61
85
|
const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], { cwd });
|
|
62
86
|
if (!res.ok)
|
|
63
87
|
throw new Error(res.error);
|
|
64
|
-
return res.data || res.rawOutput;
|
|
88
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
65
89
|
}
|
|
66
90
|
case 'create': {
|
|
67
91
|
const cmd = ['issue', 'create', ...repoArgs, '--title', args.title || '', '--body', args.body || ''];
|
package/lib/tools/pr.d.ts
CHANGED
|
@@ -53,8 +53,8 @@ export declare function createPrTool(executor: GhExecutor): {
|
|
|
53
53
|
};
|
|
54
54
|
render(args: any, value: any): {
|
|
55
55
|
type: string;
|
|
56
|
-
text:
|
|
56
|
+
text: any;
|
|
57
57
|
}[];
|
|
58
58
|
};
|
|
59
|
-
execute(args: any, execContext: any): Promise<
|
|
59
|
+
execute(args: any, execContext: any): Promise<Record<string, any>>;
|
|
60
60
|
};
|
package/lib/tools/pr.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function normalizeOutput(data) {
|
|
2
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(data)) {
|
|
6
|
+
return { items: data, count: data.length };
|
|
7
|
+
}
|
|
8
|
+
return { result: data !== undefined && data !== null ? data : '' };
|
|
9
|
+
}
|
|
1
10
|
export function createPrTool(executor) {
|
|
2
11
|
return {
|
|
3
12
|
name: 'github_pr',
|
|
@@ -50,39 +59,54 @@ export function createPrTool(executor) {
|
|
|
50
59
|
output: {
|
|
51
60
|
schema: { type: 'object' },
|
|
52
61
|
render(args, value) {
|
|
53
|
-
|
|
62
|
+
if (!value)
|
|
63
|
+
return [{ type: 'text', text: '无返回内容' }];
|
|
64
|
+
if (typeof value.result === 'string') {
|
|
65
|
+
return [{ type: 'text', text: value.result }];
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value.items)) {
|
|
68
|
+
return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
|
|
69
|
+
}
|
|
70
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
54
71
|
},
|
|
55
72
|
},
|
|
56
73
|
async execute(args, execContext) {
|
|
57
74
|
const cwd = execContext?.cwd || process.cwd();
|
|
58
|
-
|
|
75
|
+
let repoTarget = args.repo;
|
|
76
|
+
if (!repoTarget) {
|
|
77
|
+
const meta = await executor.getRepoMetadata(cwd);
|
|
78
|
+
if (meta?.nameWithOwner) {
|
|
79
|
+
repoTarget = meta.nameWithOwner;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const repoArgs = repoTarget ? ['-R', repoTarget] : [];
|
|
59
83
|
switch (args.action) {
|
|
60
84
|
case 'list': {
|
|
61
85
|
const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], { cwd });
|
|
62
86
|
if (!res.ok)
|
|
63
87
|
throw new Error(res.error);
|
|
64
|
-
return res.data || res.rawOutput;
|
|
88
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
65
89
|
}
|
|
66
90
|
case 'view': {
|
|
67
91
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
68
92
|
const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], { cwd });
|
|
69
93
|
if (!res.ok)
|
|
70
94
|
throw new Error(res.error);
|
|
71
|
-
return res.data || res.rawOutput;
|
|
95
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
72
96
|
}
|
|
73
97
|
case 'checks': {
|
|
74
98
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
75
99
|
const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], { cwd });
|
|
76
100
|
if (!res.ok)
|
|
77
101
|
throw new Error(res.error);
|
|
78
|
-
return res.data || res.rawOutput;
|
|
102
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
79
103
|
}
|
|
80
104
|
case 'diff': {
|
|
81
105
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
82
106
|
const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], { cwd });
|
|
83
107
|
if (!res.ok)
|
|
84
108
|
throw new Error(res.error);
|
|
85
|
-
return res.rawOutput || '无差异内容';
|
|
109
|
+
return { result: res.rawOutput || '无差异内容' };
|
|
86
110
|
}
|
|
87
111
|
case 'create': {
|
|
88
112
|
const createCmd = ['pr', 'create', ...repoArgs, '--title', args.title || '', '--body', args.body || ''];
|
package/lib/tools/repo.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export declare function createRepoTool(executor: GhExecutor): {
|
|
|
31
31
|
};
|
|
32
32
|
render(args: any, value: any): {
|
|
33
33
|
type: string;
|
|
34
|
-
text:
|
|
34
|
+
text: any;
|
|
35
35
|
}[];
|
|
36
36
|
};
|
|
37
|
-
execute(args: any, execContext: any): Promise<
|
|
37
|
+
execute(args: any, execContext: any): Promise<Record<string, any>>;
|
|
38
38
|
};
|
package/lib/tools/repo.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function normalizeOutput(data) {
|
|
2
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(data)) {
|
|
6
|
+
return { items: data, count: data.length };
|
|
7
|
+
}
|
|
8
|
+
return { result: data !== undefined && data !== null ? data : '' };
|
|
9
|
+
}
|
|
1
10
|
export function createRepoTool(executor) {
|
|
2
11
|
return {
|
|
3
12
|
name: 'github_repo',
|
|
@@ -28,7 +37,15 @@ export function createRepoTool(executor) {
|
|
|
28
37
|
output: {
|
|
29
38
|
schema: { type: 'object' },
|
|
30
39
|
render(args, value) {
|
|
31
|
-
|
|
40
|
+
if (!value)
|
|
41
|
+
return [{ type: 'text', text: '无返回内容' }];
|
|
42
|
+
if (typeof value.result === 'string') {
|
|
43
|
+
return [{ type: 'text', text: value.result }];
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value.items)) {
|
|
46
|
+
return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
|
|
47
|
+
}
|
|
48
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
32
49
|
},
|
|
33
50
|
},
|
|
34
51
|
async execute(args, execContext) {
|
|
@@ -36,11 +53,24 @@ export function createRepoTool(executor) {
|
|
|
36
53
|
const limitStr = String(args.limit || 10);
|
|
37
54
|
switch (args.action) {
|
|
38
55
|
case 'view': {
|
|
39
|
-
|
|
56
|
+
let repoTarget = args.repo;
|
|
57
|
+
let meta = null;
|
|
58
|
+
if (!repoTarget) {
|
|
59
|
+
meta = await executor.getRepoMetadata(cwd);
|
|
60
|
+
if (meta?.nameWithOwner) {
|
|
61
|
+
repoTarget = meta.nameWithOwner;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const repoArgs = repoTarget ? [repoTarget] : [];
|
|
40
65
|
const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'], { cwd });
|
|
41
|
-
if (!res.ok)
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
// 若执行 gh 失败(如离线或环境限制),但本地已成功识别仓库元数据,进行优雅兜底返回
|
|
68
|
+
if (meta && meta.nameWithOwner) {
|
|
69
|
+
return normalizeOutput(meta);
|
|
70
|
+
}
|
|
42
71
|
throw new Error(res.error);
|
|
43
|
-
|
|
72
|
+
}
|
|
73
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
44
74
|
}
|
|
45
75
|
case 'search_code': {
|
|
46
76
|
if (!args.query)
|
|
@@ -49,7 +79,7 @@ export function createRepoTool(executor) {
|
|
|
49
79
|
const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr], { cwd });
|
|
50
80
|
if (!res.ok)
|
|
51
81
|
throw new Error(res.error);
|
|
52
|
-
return res.data || res.rawOutput;
|
|
82
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
53
83
|
}
|
|
54
84
|
case 'search_repos': {
|
|
55
85
|
if (!args.query)
|
|
@@ -57,7 +87,7 @@ export function createRepoTool(executor) {
|
|
|
57
87
|
const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr], { cwd });
|
|
58
88
|
if (!res.ok)
|
|
59
89
|
throw new Error(res.error);
|
|
60
|
-
return res.data || res.rawOutput;
|
|
90
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
61
91
|
}
|
|
62
92
|
default:
|
|
63
93
|
throw new Error(`未知的 Repo 操作: ${args.action}`);
|
package/lib/tools/run.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export declare function createRunTool(executor: GhExecutor): {
|
|
|
31
31
|
};
|
|
32
32
|
render(args: any, value: any): {
|
|
33
33
|
type: string;
|
|
34
|
-
text:
|
|
34
|
+
text: any;
|
|
35
35
|
}[];
|
|
36
36
|
};
|
|
37
|
-
execute(args: any, execContext: any): Promise<
|
|
37
|
+
execute(args: any, execContext: any): Promise<Record<string, any>>;
|
|
38
38
|
};
|
package/lib/tools/run.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function normalizeOutput(data) {
|
|
2
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(data)) {
|
|
6
|
+
return { items: data, count: data.length };
|
|
7
|
+
}
|
|
8
|
+
return { result: data !== undefined && data !== null ? data : '' };
|
|
9
|
+
}
|
|
1
10
|
export function createRunTool(executor) {
|
|
2
11
|
return {
|
|
3
12
|
name: 'github_run',
|
|
@@ -28,18 +37,33 @@ export function createRunTool(executor) {
|
|
|
28
37
|
output: {
|
|
29
38
|
schema: { type: 'object' },
|
|
30
39
|
render(args, value) {
|
|
31
|
-
|
|
40
|
+
if (!value)
|
|
41
|
+
return [{ type: 'text', text: '无返回数据' }];
|
|
42
|
+
if (typeof value.result === 'string') {
|
|
43
|
+
return [{ type: 'text', text: value.result }];
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value.items)) {
|
|
46
|
+
return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
|
|
47
|
+
}
|
|
48
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
32
49
|
},
|
|
33
50
|
},
|
|
34
51
|
async execute(args, execContext) {
|
|
35
52
|
const cwd = execContext?.cwd || process.cwd();
|
|
36
|
-
|
|
53
|
+
let repoTarget = args.repo;
|
|
54
|
+
if (!repoTarget) {
|
|
55
|
+
const meta = await executor.getRepoMetadata(cwd);
|
|
56
|
+
if (meta?.nameWithOwner) {
|
|
57
|
+
repoTarget = meta.nameWithOwner;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const repoArgs = repoTarget ? ['-R', repoTarget] : [];
|
|
37
61
|
switch (args.action) {
|
|
38
62
|
case 'list': {
|
|
39
63
|
const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)], { cwd });
|
|
40
64
|
if (!res.ok)
|
|
41
65
|
throw new Error(res.error);
|
|
42
|
-
return res.data || res.rawOutput;
|
|
66
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
43
67
|
}
|
|
44
68
|
case 'view': {
|
|
45
69
|
if (!args.run_id)
|
|
@@ -47,7 +71,7 @@ export function createRunTool(executor) {
|
|
|
47
71
|
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'], { cwd });
|
|
48
72
|
if (!res.ok)
|
|
49
73
|
throw new Error(res.error);
|
|
50
|
-
return res.data || res.rawOutput;
|
|
74
|
+
return normalizeOutput(res.data || res.rawOutput);
|
|
51
75
|
}
|
|
52
76
|
case 'log_failed': {
|
|
53
77
|
if (!args.run_id)
|
|
@@ -60,7 +84,7 @@ export function createRunTool(executor) {
|
|
|
60
84
|
});
|
|
61
85
|
if (!res.ok)
|
|
62
86
|
throw new Error(res.error);
|
|
63
|
-
return res.rawOutput || '未找到失败步骤的日志';
|
|
87
|
+
return { result: res.rawOutput || '未找到失败步骤的日志' };
|
|
64
88
|
}
|
|
65
89
|
case 'rerun': {
|
|
66
90
|
if (!args.run_id)
|
package/package.json
CHANGED