dsh-github-flow 0.1.6 → 0.1.8

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.
@@ -1,2 +1,3 @@
1
1
  import type { GhExecutor } from '../executor.js';
2
- export declare function registerGhCommand(commandsService: any, executor: GhExecutor): void;
2
+ import type { UpdateChecker } from '../updater.js';
3
+ export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker): void;
@@ -1,4 +1,5 @@
1
- export function registerGhCommand(commandsService, executor) {
1
+ import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
2
+ export function registerGhCommand(commandsService, executor, updateChecker) {
2
3
  if (!commandsService || typeof commandsService.register !== 'function')
3
4
  return;
4
5
  commandsService.register({
@@ -11,7 +12,14 @@ export function registerGhCommand(commandsService, executor) {
11
12
  // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
12
13
  const auth = await executor.checkAuth(undefined, true);
13
14
  const repo = await executor.getRepoMetadata();
15
+ const updateInfo = updateChecker?.getQuickStatus();
14
16
  let md = `### GitHub 集成状态\n\n`;
17
+ // 若检测到新版本,在最上方展示醒目的升级指引横幅
18
+ if (updateInfo?.hasUpdate) {
19
+ md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
20
+ md += `> 输入 \`/gh upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
21
+ }
22
+ md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
15
23
  if (auth.loggedIn) {
16
24
  md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
17
25
  md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
@@ -35,15 +43,44 @@ export function registerGhCommand(commandsService, executor) {
35
43
  text: md,
36
44
  };
37
45
  }
46
+ if (trimmed === 'upgrade') {
47
+ const info = updateChecker ? await updateChecker.check(true) : null;
48
+ const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
49
+ const latestVer = info?.latestVersion || curVer;
50
+ const hasUpdate = info?.hasUpdate ?? false;
51
+ const isMac = process.platform === 'darwin';
52
+ const isWin = process.platform === 'win32';
53
+ const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
54
+ let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
55
+ md += `- **当前运行版本**: \`v${curVer}\`\n`;
56
+ md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
57
+ md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
58
+ md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
59
+ md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
60
+ md += `1. **DSH 官方标准工程升级(推荐)**:\n\`\`\`bash\npnpm dsh plugin add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
61
+ md += `2. **DSH 桌面版 Profile 环境升级**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
62
+ if (isMac) {
63
+ md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
64
+ }
65
+ else {
66
+ md += `\n`;
67
+ }
68
+ md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
69
+ md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
70
+ return {
71
+ kind: 'success',
72
+ text: md,
73
+ };
74
+ }
38
75
  if (trimmed === 'help') {
39
76
  return {
40
77
  kind: 'success',
41
- text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号与仓库连接\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
78
+ text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号、仓库连接与插件版本\n- \`/gh upgrade\`: 检查新版本并显示全场景升级命令指引\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
42
79
  };
43
80
  }
44
81
  return {
45
82
  kind: 'error',
46
- text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh help`,
83
+ text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh upgrade, /gh help`,
47
84
  };
48
85
  },
49
86
  });
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, {
@@ -252,7 +269,18 @@ export class GhExecutor {
252
269
  async probeAuth(cwd) {
253
270
  const res = await this.run(['auth', 'status'], { cwd, timeoutMs: 10_000, rawText: true });
254
271
  const raw = (res.rawOutput || '') + '\n' + (res.error || '');
255
- const loggedIn = raw.includes('Logged in to') || raw.includes('✓');
272
+ // 多重维度强健判定登录态 (容错 Windows Unicode 乱码、Keyring 告警与不同版本 CLI 输出差分)
273
+ const hasLoggedInFlag = /logged\s+in\s+to/i.test(raw) || raw.includes('✓') || raw.includes('√');
274
+ const hasActiveFlag = /active\s+account:\s*true/i.test(raw);
275
+ const hasTokenFlag = raw.includes('Token:') || raw.includes('Token scopes:');
276
+ const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
277
+ const hasAccount = Boolean(accountMatch && accountMatch[1] && accountMatch[1].toLowerCase() !== 'none');
278
+ // 显式未登录特征排查
279
+ const isExplicitlyLoggedOut = /not\s+logged\s+in/i.test(raw) ||
280
+ raw.includes('gh auth login') ||
281
+ raw.includes('authentication required') ||
282
+ raw.includes('No accounts found');
283
+ const loggedIn = !isExplicitlyLoggedOut && (hasLoggedInFlag || hasActiveFlag || (hasTokenFlag && hasAccount));
256
284
  // 只要已登录或有常规输出,则必然安装了 gh
257
285
  const isNotInstalled = Boolean(res.error && (res.error.includes('系统未检测到 GitHub CLI') || res.error.includes('ENOENT')));
258
286
  const installed = loggedIn || !isNotInstalled;
@@ -275,7 +303,6 @@ export class GhExecutor {
275
303
  }
276
304
  // 解析用户名
277
305
  let user;
278
- const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
279
306
  if (accountMatch) {
280
307
  user = accountMatch[1];
281
308
  }
@@ -339,8 +366,8 @@ export class GhExecutor {
339
366
  * 获取当前目录(工作区)关联的远程仓库元数据
340
367
  * 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
341
368
  */
342
- async getRepoMetadata(cwd) {
343
- const targetCwd = this.resolveWorkspaceCwd(cwd);
369
+ async getRepoMetadata(cwd, sessionId) {
370
+ const targetCwd = this.resolveWorkspaceCwd(cwd, sessionId);
344
371
  // 1. 优先尝试由 gh 直接根据当前上下文识别
345
372
  const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
346
373
  if (res.ok && res.data && res.data.nameWithOwner) {
package/lib/main.d.ts CHANGED
@@ -2,6 +2,13 @@ import { GhExecutor } from './executor.js';
2
2
  import './github-service.js';
3
3
  import type { PluginConfig } from './types.js';
4
4
  export declare const name = "dsh-github-flow";
5
+ /**
6
+ * 核心服务依赖声明
7
+ *
8
+ * 必须显式声明 webServer 与 workspaceRegistry:
9
+ * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
10
+ * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
11
+ */
5
12
  export declare const inject: readonly ['tools', 'commands', 'webServer', 'workspaceRegistry'];
6
13
  export type Config = PluginConfig;
7
14
  /**
@@ -19,4 +26,8 @@ export declare const Config: {
19
26
  export declare function apply(ctx: any, config?: PluginConfig): void;
20
27
  export type GitHubService = GhExecutor;
21
28
  export * from './types.js';
29
+ export * from './version.js';
30
+ export * from './updater.js';
31
+ export * from './upgrade-manager.js';
32
+ export * from './executor.js';
22
33
  export * from './github-service.js';
package/lib/main.js CHANGED
@@ -6,10 +6,20 @@ import { createRepoTool } from './tools/repo.js';
6
6
  import { createApiTool } from './tools/api.js';
7
7
  import { registerGhCommand } from './commands/gh.js';
8
8
  import { registerApiRoutes } from './api/routes.js';
9
+ import { UpdateChecker } from './updater.js';
10
+ import { UpgradeManager } from './upgrade-manager.js';
11
+ import { NPM_PACKAGE_URL } from './version.js';
9
12
  // 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
10
13
  // 并随下面 `export * from './github-service.js'` 一起传给消费方。
11
14
  import './github-service.js';
12
15
  export const name = 'dsh-github-flow';
16
+ /**
17
+ * 核心服务依赖声明
18
+ *
19
+ * 必须显式声明 webServer 与 workspaceRegistry:
20
+ * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
21
+ * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
22
+ */
13
23
  export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
14
24
  /**
15
25
  * 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
@@ -28,6 +38,9 @@ export const Config = {
28
38
  maxOutputBytes: typeof cfg.maxOutputBytes === 'number' ? cfg.maxOutputBytes : undefined,
29
39
  cacheTtlMs: typeof cfg.cacheTtlMs === 'number' ? cfg.cacheTtlMs : 15_000,
30
40
  defaultListLimit: typeof cfg.defaultListLimit === 'number' ? cfg.defaultListLimit : 20,
41
+ checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
42
+ updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
43
+ registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
31
44
  }
32
45
  };
33
46
  }
@@ -35,6 +48,11 @@ export const Config = {
35
48
  };
36
49
  export function apply(ctx, config = {}) {
37
50
  const executor = new GhExecutor(config);
51
+ const updateChecker = new UpdateChecker({
52
+ intervalMs: config.updateCheckIntervalMs,
53
+ customRegistry: config.registryUrl,
54
+ });
55
+ const upgradeManager = new UpgradeManager();
38
56
  // 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
39
57
  if (ctx.provide && typeof ctx.provide === 'function') {
40
58
  ctx.provide('github', executor);
@@ -42,9 +60,9 @@ export function apply(ctx, config = {}) {
42
60
  else {
43
61
  ctx.github = executor;
44
62
  }
45
- // 2. 注册人类 Slash 命令 (/gh status, /gh help)
63
+ // 2. 注册人类 Slash 命令 (/gh status, /gh upgrade, /gh help)
46
64
  if (ctx.commands) {
47
- registerGhCommand(ctx.commands, executor);
65
+ registerGhCommand(ctx.commands, executor, updateChecker);
48
66
  }
49
67
  // 3. 注册 Agent 模型工具集(5 大核心领域工具)
50
68
  if (ctx.tools) {
@@ -58,9 +76,29 @@ export function apply(ctx, config = {}) {
58
76
  if (ctx.webServer) {
59
77
  // 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
60
78
  // cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
61
- registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000);
79
+ registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager);
80
+ }
81
+ // 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
82
+ if (config.checkUpdate !== false) {
83
+ setTimeout(() => {
84
+ updateChecker.check(false).then((info) => {
85
+ if (info.hasUpdate) {
86
+ console.log(`
87
+ ┌────────────────────────────────────────────────────────────────────────┐
88
+ │ 🚀 dsh-github-flow 插件有新版本可用: ${info.currentVersion} -> ${info.latestVersion} │
89
+ │ 升级命令: ${info.upgradeCommand.padEnd(58, ' ')}│
90
+ │ 更新主页: ${NPM_PACKAGE_URL.padEnd(58, ' ')}│
91
+ └────────────────────────────────────────────────────────────────────────┘
92
+ `);
93
+ }
94
+ }).catch(() => { });
95
+ }, 3_000);
62
96
  }
63
97
  }
64
98
  export * from './types.js';
99
+ export * from './version.js';
100
+ export * from './updater.js';
101
+ export * from './upgrade-manager.js';
102
+ export * from './executor.js';
65
103
  // 让 `ctx.github` 的类型声明随包一起被消费方加载(声明合并在 './github-service.js' 内)
66
104
  export * from './github-service.js';
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 };