dsh-github-flow 0.4.0 → 0.6.0

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.
Files changed (82) hide show
  1. package/README.en.md +91 -12
  2. package/README.md +91 -12
  3. package/docs/UPGRADE-NOTES.md +9 -12
  4. package/docs/architecture.md +63 -29
  5. package/docs/images/1.png +0 -0
  6. package/lib/api/routes-actions.js +268 -5
  7. package/lib/api/routes-overview.js +147 -29
  8. package/lib/cache.d.ts +77 -4
  9. package/lib/cache.js +114 -7
  10. package/lib/client.d.ts +9 -4
  11. package/lib/client.js +5293 -942
  12. package/lib/commands/gh.d.ts +6 -1
  13. package/lib/commands/gh.js +144 -71
  14. package/lib/executor/error-classifier.d.ts +55 -0
  15. package/lib/executor/error-classifier.js +199 -0
  16. package/lib/executor/git-parser.d.ts +157 -1
  17. package/lib/executor/git-parser.js +513 -4
  18. package/lib/executor.d.ts +147 -4
  19. package/lib/executor.js +866 -77
  20. package/lib/main.js +28 -4
  21. package/lib/renderers/agent-task.d.ts +16 -0
  22. package/lib/renderers/agent-task.js +107 -0
  23. package/lib/renderers/codespace.d.ts +16 -0
  24. package/lib/renderers/codespace.js +103 -0
  25. package/lib/renderers/discussion.d.ts +16 -0
  26. package/lib/renderers/discussion.js +101 -0
  27. package/lib/renderers/markdown.d.ts +4 -0
  28. package/lib/renderers/markdown.js +4 -0
  29. package/lib/renderers/project.d.ts +20 -0
  30. package/lib/renderers/project.js +128 -0
  31. package/lib/renderers/skill.d.ts +30 -0
  32. package/lib/renderers/skill.js +180 -0
  33. package/lib/renderers/status.d.ts +5 -0
  34. package/lib/renderers/status.js +52 -0
  35. package/lib/telemetry.d.ts +70 -0
  36. package/lib/telemetry.js +161 -0
  37. package/lib/tools/agent-task.d.ts +54 -0
  38. package/lib/tools/agent-task.js +135 -0
  39. package/lib/tools/codespace.d.ts +51 -0
  40. package/lib/tools/codespace.js +154 -0
  41. package/lib/tools/discussion.d.ts +70 -0
  42. package/lib/tools/discussion.js +195 -0
  43. package/lib/tools/help.js +129 -14
  44. package/lib/tools/image-utils.d.ts +27 -0
  45. package/lib/tools/image-utils.js +73 -3
  46. package/lib/tools/project.d.ts +80 -0
  47. package/lib/tools/project.js +241 -0
  48. package/lib/tools/repo.js +13 -1
  49. package/lib/tools/skill.d.ts +24 -1
  50. package/lib/tools/skill.js +115 -5
  51. package/lib/tools/status.d.ts +3 -2
  52. package/lib/tools/status.js +9 -4
  53. package/lib/types.d.ts +336 -0
  54. package/lib/ui/commit-modal.js +144 -14
  55. package/lib/ui/help-modal.d.ts +21 -0
  56. package/lib/ui/help-modal.js +300 -0
  57. package/lib/ui/issue-modal.d.ts +14 -0
  58. package/lib/ui/issue-modal.js +366 -0
  59. package/lib/ui/markdown-lite.d.ts +66 -0
  60. package/lib/ui/markdown-lite.js +312 -0
  61. package/lib/ui/network-banner.d.ts +12 -0
  62. package/lib/ui/network-banner.js +35 -0
  63. package/lib/ui/pr-modal.d.ts +17 -0
  64. package/lib/ui/pr-modal.js +516 -0
  65. package/lib/ui/repo-badges.d.ts +43 -0
  66. package/lib/ui/repo-badges.js +82 -0
  67. package/lib/ui/repo-selector.d.ts +14 -0
  68. package/lib/ui/repo-selector.js +112 -0
  69. package/lib/ui/storage.d.ts +56 -0
  70. package/lib/ui/storage.js +163 -0
  71. package/lib/ui/styles.d.ts +1 -1
  72. package/lib/ui/styles.js +1219 -0
  73. package/lib/ui/sync-settings-modal.js +1 -1
  74. package/lib/ui/upgrade-card.js +39 -14
  75. package/lib/ui/utils.d.ts +35 -0
  76. package/lib/ui/utils.js +172 -0
  77. package/lib/ui/workspace-panel.js +198 -31
  78. package/lib/upgrade-manager.d.ts +5 -1
  79. package/lib/upgrade-manager.js +24 -1
  80. package/lib/version.d.ts +1 -1
  81. package/lib/version.js +1 -1
  82. package/package.json +2 -2
@@ -0,0 +1,135 @@
1
+ import { renderAgentTaskMarkdown } from '../renderers/markdown.js';
2
+ function normalizeOutput(data) {
3
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
4
+ return data;
5
+ }
6
+ if (Array.isArray(data)) {
7
+ return { items: data, count: data.length };
8
+ }
9
+ return { result: data !== undefined && data !== null ? data : '' };
10
+ }
11
+ const AGENT_TASK_JSON_FIELDS = 'completedAt,createdAt,id,name,pullRequestNumber,pullRequestState,pullRequestTitle,pullRequestUrl,repository,state,updatedAt,user';
12
+ export function createAgentTaskTool(executor) {
13
+ return {
14
+ name: 'github_agent_task',
15
+ description: '管理 GitHub Copilot Coding Agent 云端智能体任务会话:查看已分派任务列表、检视任务状态与会话日志、向云端智能体发起新编码与重构任务。基于 gh CLI preview 特性。',
16
+ parameters: {
17
+ type: 'object',
18
+ properties: {
19
+ action: {
20
+ type: 'string',
21
+ enum: ['list', 'view', 'create'],
22
+ description: 'Agent Task 操作类型。list 查看任务列表、view 检视任务详情与日志、create 新建智能体任务。',
23
+ },
24
+ repo: {
25
+ type: 'string',
26
+ description: '目标仓库 [owner/repo],留空则识别当前工作区仓库。',
27
+ },
28
+ task_id: {
29
+ type: 'string',
30
+ description: '任务标识(view 动作必需,支持 Session ID、PR 编号或任务 URL)。',
31
+ },
32
+ prompt: {
33
+ type: 'string',
34
+ description: '任务描述正文或编码指令(create 动作必需,如 "为用户模块增加单元测试")。',
35
+ },
36
+ base_branch: {
37
+ type: 'string',
38
+ description: 'PR 基准分支(create 动作可选,映射 -b/--base,缺省为主干默认分支)。',
39
+ },
40
+ custom_agent: {
41
+ type: 'string',
42
+ description: '自定义 Agent 规范名称(create 动作可选,映射 -a/--custom-agent,对应 .github/agents/ 规范)。',
43
+ },
44
+ limit: {
45
+ type: 'number',
46
+ description: '返回任务数量上限(list 动作使用,默认 30)。',
47
+ },
48
+ log: {
49
+ type: 'boolean',
50
+ description: '是否一并拉取任务会话执行日志(view 动作可选,映射 --log)。',
51
+ },
52
+ },
53
+ required: ['action'],
54
+ },
55
+ output: {
56
+ schema: { type: 'object' },
57
+ render(args, value) {
58
+ return [{ type: 'text', text: renderAgentTaskMarkdown(args, value) }];
59
+ },
60
+ },
61
+ async execute(args, execContext) {
62
+ const cwd = execContext?.cwd || process.cwd();
63
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
64
+ const repoArgs = args.repo ? ['-R', args.repo] : [];
65
+ const runOpts = { cwd, sessionId };
66
+ switch (args.action) {
67
+ case 'list': {
68
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : 30;
69
+ // 注意:gh agent-task list 属于账号级任务列表命令,上游 gh CLI 官方不接受 -R/--repo 标志
70
+ const listCmd = [
71
+ 'agent-task', 'list',
72
+ '-L', String(limit),
73
+ '--json', AGENT_TASK_JSON_FIELDS,
74
+ ];
75
+ const res = await executor.run(listCmd, runOpts);
76
+ if (!res.ok)
77
+ throw new Error(res.error);
78
+ return normalizeOutput(res.data || res.rawOutput);
79
+ }
80
+ case 'view': {
81
+ // 防线:task_id 必须显式提供,防止 CLI 打开交互式选择器导致无头进程挂死
82
+ if (!args.task_id || typeof args.task_id !== 'string' || !args.task_id.trim()) {
83
+ throw new Error('view 操作必须提供 task_id(支持 Session ID、PR 编号或任务 URL),以防止交互式挂起');
84
+ }
85
+ const cleanId = args.task_id.trim();
86
+ const viewCmd = [
87
+ 'agent-task', 'view', cleanId,
88
+ '--json', AGENT_TASK_JSON_FIELDS,
89
+ ...repoArgs,
90
+ ];
91
+ const res = await executor.run(viewCmd, runOpts);
92
+ if (!res.ok)
93
+ throw new Error(res.error);
94
+ let taskData = res.data;
95
+ if (!taskData && res.rawOutput) {
96
+ try {
97
+ taskData = JSON.parse(res.rawOutput);
98
+ }
99
+ catch {
100
+ taskData = { id: cleanId, result: res.rawOutput };
101
+ }
102
+ }
103
+ // 若调用方需要日志,额外抓取 session log 并挂载
104
+ if (args.log === true && taskData && typeof taskData === 'object') {
105
+ const logCmd = ['agent-task', 'view', cleanId, '--log', ...repoArgs];
106
+ const logRes = await executor.run(logCmd, { ...runOpts, rawText: true, timeoutMs: 45_000 });
107
+ if (logRes.ok && logRes.rawOutput) {
108
+ taskData.logs = logRes.rawOutput;
109
+ }
110
+ }
111
+ return taskData || { id: cleanId };
112
+ }
113
+ case 'create': {
114
+ if (!args.prompt || typeof args.prompt !== 'string' || !args.prompt.trim()) {
115
+ throw new Error('create 操作必须提供 prompt(任务描述正文或编码指令)');
116
+ }
117
+ const createCmd = ['agent-task', 'create', args.prompt.trim(), ...repoArgs];
118
+ if (args.base_branch)
119
+ createCmd.push('-b', args.base_branch.trim());
120
+ if (args.custom_agent)
121
+ createCmd.push('-a', args.custom_agent.trim());
122
+ const res = await executor.run(createCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
123
+ if (!res.ok)
124
+ throw new Error(res.error);
125
+ return {
126
+ message: '智能体任务已成功创建并派发',
127
+ output: res.rawOutput || 'Agent task dispatched',
128
+ };
129
+ }
130
+ default:
131
+ throw new Error(`未知的 Agent Task 操作: ${args.action}`);
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,51 @@
1
+ import type { GhExecutor } from '../executor.js';
2
+ export declare function createCodespaceTool(executor: GhExecutor): {
3
+ name: string;
4
+ description: string;
5
+ parameters: {
6
+ type: string;
7
+ properties: {
8
+ action: {
9
+ type: string;
10
+ enum: string[];
11
+ description: string;
12
+ };
13
+ codespace: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ repo: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ org: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ limit: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ confirm: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ };
34
+ required: string[];
35
+ };
36
+ output: {
37
+ schema: {
38
+ type: string;
39
+ };
40
+ render(args: any, value: any): {
41
+ type: string;
42
+ text: string;
43
+ }[];
44
+ };
45
+ execute(args: any, execContext: any): Promise<Record<string, any> | {
46
+ status: string;
47
+ scope: string;
48
+ command: string;
49
+ tool: string;
50
+ }>;
51
+ };
@@ -0,0 +1,154 @@
1
+ import { renderCodespaceMarkdown } from '../renderers/markdown.js';
2
+ import { checkScopeRequired } from './project.js';
3
+ function normalizeOutput(data) {
4
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
5
+ return data;
6
+ }
7
+ if (Array.isArray(data)) {
8
+ return { items: data, count: data.length };
9
+ }
10
+ return { result: data !== undefined && data !== null ? data : '' };
11
+ }
12
+ const CODESPACE_LIST_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName';
13
+ const CODESPACE_VIEW_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName,devcontainerPath,recentFolders,idleTimeoutMinutes';
14
+ export function createCodespaceTool(executor) {
15
+ return {
16
+ name: 'github_codespace',
17
+ description: '管理 GitHub Codespaces 云端远程开发环境:查阅实例列表与运行状态、检视实例配置详情、停止运行中的环境防计费、安全销毁实例。未授权时提供平台感知自愈命令。',
18
+ parameters: {
19
+ type: 'object',
20
+ properties: {
21
+ action: {
22
+ type: 'string',
23
+ enum: ['list', 'view', 'stop', 'delete'],
24
+ description: 'Codespace 操作类型。list 查看环境列表、view 检视详情、stop 停止运行(防持续计费)、delete 销毁实例(需 confirm)。',
25
+ },
26
+ codespace: {
27
+ type: 'string',
28
+ description: 'Codespace 实例名称(view, stop, delete 动作必需,如 "fuzzy-spoon-69vj";防交互式挂起)。',
29
+ },
30
+ repo: {
31
+ type: 'string',
32
+ description: '按目标仓库筛选 [owner/repo](list 动作可选)。',
33
+ },
34
+ org: {
35
+ type: 'string',
36
+ description: '组织管理员按组织筛选(list, stop, delete 动作可选)。',
37
+ },
38
+ limit: {
39
+ type: 'number',
40
+ description: '返回数量上限(list 动作使用,默认 30)。',
41
+ },
42
+ confirm: {
43
+ type: 'boolean',
44
+ description: '危险操作确认标记(delete 动作必需)。销毁云端环境不可逆,未确认时输出影响评估并拦截。',
45
+ },
46
+ },
47
+ required: ['action'],
48
+ },
49
+ output: {
50
+ schema: { type: 'object' },
51
+ render(args, value) {
52
+ return [{ type: 'text', text: renderCodespaceMarkdown(args, value) }];
53
+ },
54
+ },
55
+ async execute(args, execContext) {
56
+ const cwd = execContext?.cwd || process.cwd();
57
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
58
+ const runOpts = { cwd, sessionId };
59
+ switch (args.action) {
60
+ case 'list': {
61
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : 30;
62
+ const listCmd = [
63
+ 'codespace', 'list',
64
+ '-L', String(limit),
65
+ '--json', CODESPACE_LIST_FIELDS,
66
+ ];
67
+ if (args.repo)
68
+ listCmd.push('-R', String(args.repo).trim());
69
+ if (args.org)
70
+ listCmd.push('-o', String(args.org).trim());
71
+ const res = await executor.run(listCmd, runOpts);
72
+ if (!res.ok) {
73
+ const scopeFix = checkScopeRequired(res.error || '', 'codespace', 'gh auth refresh -h github.com -s codespace', 'github_codespace');
74
+ if (scopeFix)
75
+ return scopeFix;
76
+ throw new Error(res.error);
77
+ }
78
+ return normalizeOutput(res.data || res.rawOutput);
79
+ }
80
+ case 'view': {
81
+ if (!args.codespace || typeof args.codespace !== 'string' || !args.codespace.trim()) {
82
+ throw new Error('view 操作必须提供 codespace 名称,以防止 CLI 打开交互式选择器导致挂起');
83
+ }
84
+ const cleanName = args.codespace.trim();
85
+ const viewCmd = [
86
+ 'codespace', 'view',
87
+ '-c', cleanName,
88
+ '--json', CODESPACE_VIEW_FIELDS,
89
+ ];
90
+ if (args.repo)
91
+ viewCmd.push('-R', String(args.repo).trim());
92
+ const res = await executor.run(viewCmd, runOpts);
93
+ if (!res.ok) {
94
+ const scopeFix = checkScopeRequired(res.error || '', 'codespace', 'gh auth refresh -h github.com -s codespace', 'github_codespace');
95
+ if (scopeFix)
96
+ return scopeFix;
97
+ throw new Error(res.error);
98
+ }
99
+ return normalizeOutput(res.data || res.rawOutput);
100
+ }
101
+ case 'stop': {
102
+ if (!args.codespace || typeof args.codespace !== 'string' || !args.codespace.trim()) {
103
+ throw new Error('stop 操作必须提供 codespace 名称,以防止 CLI 进入交互式挂起');
104
+ }
105
+ const cleanName = args.codespace.trim();
106
+ const stopCmd = ['codespace', 'stop', '-c', cleanName];
107
+ if (args.org)
108
+ stopCmd.push('-o', String(args.org).trim());
109
+ const res = await executor.run(stopCmd, { ...runOpts, rawText: true, timeoutMs: 45_000 });
110
+ if (!res.ok) {
111
+ const scopeFix = checkScopeRequired(res.error || '', 'codespace', 'gh auth refresh -h github.com -s codespace', 'github_codespace');
112
+ if (scopeFix)
113
+ return scopeFix;
114
+ throw new Error(res.error);
115
+ }
116
+ return {
117
+ message: `云端开发环境「${cleanName}」已成功发送停止指令(已避免继续占用算力计费)`,
118
+ output: res.rawOutput || 'Stopped',
119
+ };
120
+ }
121
+ case 'delete': {
122
+ if (!args.codespace || typeof args.codespace !== 'string' || !args.codespace.trim()) {
123
+ throw new Error('delete 操作必须提供 codespace 名称');
124
+ }
125
+ const cleanName = args.codespace.trim();
126
+ if (args.confirm !== true) {
127
+ return {
128
+ status: 'AWAITING_CONFIRMATION',
129
+ warning: `⚠️ 销毁确认拦截:你即将永久删除云端开发环境「${cleanName}」。`,
130
+ impact: '物理销毁 Codespace 将永久释放云端计算资源,环境中任何未提交并推送到远端 Git 的临时修改都将无法找回。',
131
+ requiredAction: '如确认销毁该实例,请核对环境名称并在参数中附加 `confirm: true` 重新调用。',
132
+ };
133
+ }
134
+ const deleteCmd = ['codespace', 'delete', '-c', cleanName, '-f'];
135
+ if (args.org)
136
+ deleteCmd.push('-o', String(args.org).trim());
137
+ const res = await executor.run(deleteCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
138
+ if (!res.ok) {
139
+ const scopeFix = checkScopeRequired(res.error || '', 'codespace', 'gh auth refresh -h github.com -s codespace', 'github_codespace');
140
+ if (scopeFix)
141
+ return scopeFix;
142
+ throw new Error(res.error);
143
+ }
144
+ return {
145
+ message: `云端开发环境「${cleanName}」已成功销毁释放`,
146
+ output: res.rawOutput || 'Deleted',
147
+ };
148
+ }
149
+ default:
150
+ throw new Error(`未知的 Codespace 操作: ${args.action}`);
151
+ }
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,70 @@
1
+ import type { GhExecutor } from '../executor.js';
2
+ export declare function createDiscussionTool(executor: GhExecutor): {
3
+ name: string;
4
+ description: string;
5
+ parameters: {
6
+ type: string;
7
+ properties: {
8
+ action: {
9
+ type: string;
10
+ enum: string[];
11
+ description: string;
12
+ };
13
+ repo: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ discussion_number: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ title: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ body: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ category: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ state: {
34
+ type: string;
35
+ enum: string[];
36
+ description: string;
37
+ };
38
+ labels: {
39
+ type: string;
40
+ items: {
41
+ type: string;
42
+ };
43
+ description: string;
44
+ };
45
+ search: {
46
+ type: string;
47
+ description: string;
48
+ };
49
+ comments: {
50
+ type: string;
51
+ description: string;
52
+ };
53
+ limit: {
54
+ type: string;
55
+ description: string;
56
+ };
57
+ };
58
+ required: string[];
59
+ };
60
+ output: {
61
+ schema: {
62
+ type: string;
63
+ };
64
+ render(args: any, value: any): {
65
+ type: string;
66
+ text: string;
67
+ }[];
68
+ };
69
+ execute(args: any, execContext: any): Promise<Record<string, any>>;
70
+ };
@@ -0,0 +1,195 @@
1
+ import { renderDiscussionMarkdown } from '../renderers/markdown.js';
2
+ function normalizeOutput(data) {
3
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
4
+ return data;
5
+ }
6
+ if (Array.isArray(data)) {
7
+ return { items: data, count: data.length };
8
+ }
9
+ return { result: data !== undefined && data !== null ? data : '' };
10
+ }
11
+ const DISCUSSION_LIST_FIELDS = 'number,title,body,category,author,closed,createdAt,updatedAt,url,labels,answered,stateReason';
12
+ const DISCUSSION_VIEW_FIELDS = 'number,title,body,category,author,state,createdAt,updatedAt,url,labels,answered,comments';
13
+ export function createDiscussionTool(executor) {
14
+ return {
15
+ name: 'github_discussion',
16
+ description: '管理 GitHub Discussions 讨论区:浏览社区讨论、检视讨论详情与回复楼层、发起新讨论议题、编辑现有讨论以及追加评论回复。基于 gh CLI preview 特性。',
17
+ parameters: {
18
+ type: 'object',
19
+ properties: {
20
+ action: {
21
+ type: 'string',
22
+ enum: ['list', 'view', 'create', 'edit', 'comment'],
23
+ description: 'Discussion 操作类型。list 查阅讨论列表、view 查看正文与评论、create 发起新讨论、edit 修改讨论、comment 发表评论。',
24
+ },
25
+ repo: {
26
+ type: 'string',
27
+ description: '目标仓库 [owner/repo],留空则识别当前工作区仓库。',
28
+ },
29
+ discussion_number: {
30
+ type: 'number',
31
+ description: '讨论编号(view, edit, comment 动作必需,如 123)。',
32
+ },
33
+ title: {
34
+ type: 'string',
35
+ description: '讨论标题(create 动作必需,edit 动作修改标题时可选)。',
36
+ },
37
+ body: {
38
+ type: 'string',
39
+ description: '讨论正文或评论正文(create, comment 必需,edit 动作修改正文时可选)。',
40
+ },
41
+ category: {
42
+ type: 'string',
43
+ description: '讨论分类名称(create 动作必需,list 动作按分类筛选使用,如 "General"、"Q&A"、"Ideas")。',
44
+ },
45
+ state: {
46
+ type: 'string',
47
+ enum: ['open', 'closed', 'all'],
48
+ description: '讨论状态过滤(list 动作使用,默认 open)。',
49
+ },
50
+ labels: {
51
+ type: 'array',
52
+ items: { type: 'string' },
53
+ description: '按标签筛选(list 动作使用)。',
54
+ },
55
+ search: {
56
+ type: 'string',
57
+ description: '搜索过滤关键词(list 动作使用,映射 -S/--search)。',
58
+ },
59
+ comments: {
60
+ type: 'boolean',
61
+ description: '是否一并拉取评论列表(view 动作使用,默认 true)。',
62
+ },
63
+ limit: {
64
+ type: 'number',
65
+ description: '返回讨论数量上限(list 动作使用,默认 30)。',
66
+ },
67
+ },
68
+ required: ['action'],
69
+ },
70
+ output: {
71
+ schema: { type: 'object' },
72
+ render(args, value) {
73
+ return [{ type: 'text', text: renderDiscussionMarkdown(args, value) }];
74
+ },
75
+ },
76
+ async execute(args, execContext) {
77
+ const cwd = execContext?.cwd || process.cwd();
78
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
79
+ const repoArgs = args.repo ? ['-R', args.repo] : [];
80
+ const runOpts = { cwd, sessionId };
81
+ switch (args.action) {
82
+ case 'list': {
83
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : 30;
84
+ const listCmd = [
85
+ 'discussion', 'list',
86
+ '-L', String(limit),
87
+ '--json', DISCUSSION_LIST_FIELDS,
88
+ ...repoArgs,
89
+ ];
90
+ if (args.category)
91
+ listCmd.push('-c', String(args.category).trim());
92
+ if (args.state)
93
+ listCmd.push('-s', args.state);
94
+ if (args.search)
95
+ listCmd.push('-S', String(args.search).trim());
96
+ if (Array.isArray(args.labels) && args.labels.length > 0) {
97
+ args.labels.forEach((l) => {
98
+ if (l)
99
+ listCmd.push('-l', String(l).trim());
100
+ });
101
+ }
102
+ const res = await executor.run(listCmd, runOpts);
103
+ if (!res.ok)
104
+ throw new Error(res.error);
105
+ return normalizeOutput(res.data || res.rawOutput);
106
+ }
107
+ case 'view': {
108
+ if (!args.discussion_number || typeof args.discussion_number !== 'number') {
109
+ throw new Error('view 操作必须提供 discussion_number(讨论数字编号),以防止进入交互式挂起');
110
+ }
111
+ const numStr = String(args.discussion_number);
112
+ const viewCmd = [
113
+ 'discussion', 'view', numStr,
114
+ '--json', DISCUSSION_VIEW_FIELDS,
115
+ ...repoArgs,
116
+ ];
117
+ if (args.comments !== false) {
118
+ viewCmd.push('-c');
119
+ }
120
+ const res = await executor.run(viewCmd, runOpts);
121
+ if (!res.ok)
122
+ throw new Error(res.error);
123
+ return normalizeOutput(res.data || res.rawOutput);
124
+ }
125
+ case 'create': {
126
+ if (!args.title || typeof args.title !== 'string' || !args.title.trim()) {
127
+ throw new Error('create 操作必须提供 title(讨论标题)');
128
+ }
129
+ if (!args.body || typeof args.body !== 'string' || !args.body.trim()) {
130
+ throw new Error('create 操作必须提供 body(讨论正文)');
131
+ }
132
+ if (!args.category || typeof args.category !== 'string' || !args.category.trim()) {
133
+ throw new Error('create 操作必须提供 category(讨论分类,例如 "General"、"Q&A"、"Ideas")');
134
+ }
135
+ const createCmd = [
136
+ 'discussion', 'create',
137
+ '-t', args.title.trim(),
138
+ '-b', args.body.trim(),
139
+ '-c', args.category.trim(),
140
+ ...repoArgs,
141
+ ];
142
+ const res = await executor.run(createCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
143
+ if (!res.ok)
144
+ throw new Error(res.error);
145
+ return {
146
+ message: '讨论已成功发起',
147
+ url: res.rawOutput ? res.rawOutput.trim() : undefined,
148
+ };
149
+ }
150
+ case 'edit': {
151
+ if (!args.discussion_number || typeof args.discussion_number !== 'number') {
152
+ throw new Error('edit 操作必须提供 discussion_number(讨论数字编号)');
153
+ }
154
+ if (!args.title && !args.body) {
155
+ throw new Error('edit 操作至少需提供 title 或 body 中的一项进行修改');
156
+ }
157
+ const editCmd = ['discussion', 'edit', String(args.discussion_number), ...repoArgs];
158
+ if (args.title)
159
+ editCmd.push('-t', String(args.title).trim());
160
+ if (args.body)
161
+ editCmd.push('-b', String(args.body).trim());
162
+ const res = await executor.run(editCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
163
+ if (!res.ok)
164
+ throw new Error(res.error);
165
+ return {
166
+ message: `讨论 #${args.discussion_number} 已更新成功`,
167
+ output: res.rawOutput || 'Updated',
168
+ };
169
+ }
170
+ case 'comment': {
171
+ if (!args.discussion_number || typeof args.discussion_number !== 'number') {
172
+ throw new Error('comment 操作必须提供 discussion_number(讨论数字编号)');
173
+ }
174
+ if (!args.body || typeof args.body !== 'string' || !args.body.trim()) {
175
+ throw new Error('comment 操作必须提供 body(评论正文)');
176
+ }
177
+ const commentCmd = [
178
+ 'discussion', 'comment', String(args.discussion_number),
179
+ '-b', args.body.trim(),
180
+ ...repoArgs,
181
+ ];
182
+ const res = await executor.run(commentCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
183
+ if (!res.ok)
184
+ throw new Error(res.error);
185
+ return {
186
+ message: `已向讨论 #${args.discussion_number} 成功发表评论`,
187
+ url: res.rawOutput ? res.rawOutput.trim() : undefined,
188
+ };
189
+ }
190
+ default:
191
+ throw new Error(`未知的 Discussion 操作: ${args.action}`);
192
+ }
193
+ },
194
+ };
195
+ }