Skip to content

Commit 7bc8d18

Browse files
author
HelloWorldU
committed
feat: GitHub API integration for PR create/merge
- Add src/api/github.ts: createPR, mergePR, getPR with token auth - Store submitForReview: real GitHub PR creation when token configured - Store mergePr: real GitHub PR merge when token configured - Add SettingsPanel.vue: GitHub PAT input with show/hide toggle - App.vue: integrate SettingsPanel into Settings tab - Fallback to mock mode when token not configured
1 parent b6b198d commit 7bc8d18

7 files changed

Lines changed: 217 additions & 17 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
```
66
UI (Vue) ←→ useSwarmStore (AgentTask[])
77
←→ Tauri IPC ←→ Rust Main Process ←→ git / spawn CLI
8-
←→ GitHub API (PR create/merge)
8+
←→ GitHub API (PR create/merge/review)
9+
←→ localStorage (GitHub Token)
910
```
1011

1112
## 状态分层

docs/COMPONENT_PATTERNS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ const emit = defineEmits<{...}>()
2424

2525
| 类型 | 规则 | 示例 |
2626
|------|------|------|
27-
| 组件 | PascalCase.vue | `TaskCard.vue` |
27+
| 组件 | PascalCase.vue | `TaskCard.vue`, `SettingsPanel.vue` |
2828
| Composable | camelCase.ts | `useSwarmStore.ts` |
2929
| 事件 | kebab-case | `@send-command` |

docs/FRONTEND.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Vue 3 + TypeScript + Vite + Tailwind CSS + lucide-vue-next + Tauri v2
1818

1919
- `store/useSwarmStore.ts` — 状态必须通过方法修改;核心模型 AgentTask 含状态机 + PR 追踪
2020
- `types/index.ts` — AgentTask / LogEntry / CommandCenterStats;修改前检查上下游依赖
21+
- `api/github.ts` — GitHub API 封装(PR 创建/合并/查询)
22+
- `api/ipc.ts` — Tauri IPC 适配层
23+
- `components/SettingsPanel.vue` — 系统设置(GitHub Token 配置)
2124

2225
## 快速启动
2326

kimi-code-swarm/src/App.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script setup lang="ts">
22
import { ref } from 'vue'
3-
import { Plus, Activity, ClipboardList, Coins, CheckCircle } from 'lucide-vue-next'
3+
import { Plus, Activity, ClipboardList, Coins, CheckCircle, Github } from 'lucide-vue-next'
44
import Sidebar from './components/Sidebar.vue'
55
import TaskCard from './components/TaskCard.vue'
66
import CreateTaskModal from './components/CreateTaskModal.vue'
77
import TaskDetail from './components/TaskDetail.vue'
8+
import SettingsPanel from './components/SettingsPanel.vue'
89
import { useSwarmStore } from './store/useSwarmStore'
910
1011
const store = useSwarmStore()
@@ -114,11 +115,15 @@ const statCards = [
114115
<p class="text-sm mt-2">Token 使用趋势、任务负载图表等功能即将上线</p>
115116
</div>
116117
</div>
117-
<div v-else class="flex items-center justify-center h-full text-gray-500">
118-
<div class="text-center">
119-
<ClipboardList class="w-16 h-16 mx-auto mb-4 text-gray-700" />
120-
<p class="text-lg font-medium">系统设置</p>
121-
<p class="text-sm mt-2">CLI 路径配置、Token 限额、通知设置等功能即将上线</p>
118+
<div v-else class="h-full max-w-2xl mx-auto">
119+
<h3 class="text-lg font-semibold text-white mb-6">系统设置</h3>
120+
<div class="space-y-6">
121+
<div class="bg-gray-800/40 border border-gray-700/50 rounded-xl p-6">
122+
<h4 class="text-sm font-semibold text-white mb-4 flex items-center gap-2">
123+
<Github class="w-4 h-4" /> GitHub 配置
124+
</h4>
125+
<SettingsPanel />
126+
</div>
122127
</div>
123128
</div>
124129
</div>

kimi-code-swarm/src/api/github.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const GITHUB_API = 'https://api.github.com'
2+
3+
function getToken(): string | null {
4+
return localStorage.getItem('github-token')
5+
}
6+
7+
export function setToken(token: string): void {
8+
localStorage.setItem('github-token', token)
9+
}
10+
11+
export function hasToken(): boolean {
12+
return !!getToken()
13+
}
14+
15+
function parseRepoUrl(url: string): { owner: string; repo: string } | null {
16+
try {
17+
const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/)
18+
if (!match) return null
19+
return { owner: match[1], repo: match[2] }
20+
} catch {
21+
return null
22+
}
23+
}
24+
25+
async function githubFetch<T = Record<string, unknown>>(path: string, options: RequestInit = {}): Promise<T> {
26+
const token = getToken()
27+
if (!token) throw new Error('GitHub Token 未配置')
28+
29+
const res = await fetch(`${GITHUB_API}${path}`, {
30+
...options,
31+
headers: {
32+
'Authorization': `token ${token}`,
33+
'Accept': 'application/vnd.github.v3+json',
34+
'Content-Type': 'application/json',
35+
...options.headers,
36+
},
37+
})
38+
39+
if (!res.ok) {
40+
const err = await res.json().catch(() => ({}))
41+
throw new Error(err.message || `GitHub API ${res.status}: ${res.statusText}`)
42+
}
43+
44+
return res.json() as Promise<T>
45+
}
46+
47+
export async function createPullRequest(
48+
repoUrl: string,
49+
title: string,
50+
head: string,
51+
base: string = 'main',
52+
body: string = ''
53+
): Promise<{ number: number; html_url: string }> {
54+
const parsed = parseRepoUrl(repoUrl)
55+
if (!parsed) throw new Error('无法解析仓库地址')
56+
57+
return githubFetch(`/repos/${parsed.owner}/${parsed.repo}/pulls`, {
58+
method: 'POST',
59+
body: JSON.stringify({ title, head, base, body }),
60+
})
61+
}
62+
63+
export async function mergePullRequest(
64+
repoUrl: string,
65+
number: number,
66+
method: 'merge' | 'squash' | 'rebase' = 'squash'
67+
): Promise<{ sha: string; message: string }> {
68+
const parsed = parseRepoUrl(repoUrl)
69+
if (!parsed) throw new Error('无法解析仓库地址')
70+
71+
return githubFetch(`/repos/${parsed.owner}/${parsed.repo}/pulls/${number}/merge`, {
72+
method: 'PUT',
73+
body: JSON.stringify({ merge_method: method }),
74+
})
75+
}
76+
77+
export async function getPullRequest(repoUrl: string, number: number): Promise<Record<string, unknown>> {
78+
const parsed = parseRepoUrl(repoUrl)
79+
if (!parsed) throw new Error('无法解析仓库地址')
80+
81+
return githubFetch(`/repos/${parsed.owner}/${parsed.repo}/pulls/${number}`)
82+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted } from 'vue'
3+
import { Github, KeyRound, Check, AlertCircle } from 'lucide-vue-next'
4+
import { setToken, hasToken } from '../api/github'
5+
6+
const token = ref('')
7+
const saved = ref(hasToken())
8+
const showToken = ref(false)
9+
10+
onMounted(() => {
11+
saved.value = hasToken()
12+
})
13+
14+
function handleSave() {
15+
if (!token.value.trim()) return
16+
setToken(token.value.trim())
17+
saved.value = true
18+
token.value = ''
19+
}
20+
21+
function handleClear() {
22+
localStorage.removeItem('github-token')
23+
saved.value = false
24+
}
25+
</script>
26+
27+
<template>
28+
<div class="space-y-4">
29+
<div class="flex items-center gap-3">
30+
<div class="w-10 h-10 rounded-lg bg-gray-700/50 flex items-center justify-center">
31+
<Github class="w-5 h-5 text-gray-400" />
32+
</div>
33+
<div>
34+
<p class="text-sm font-medium text-white">GitHub Personal Access Token</p>
35+
<p class="text-xs text-gray-500">用于创建和合并 Pull Request</p>
36+
</div>
37+
</div>
38+
39+
<div v-if="saved" class="flex items-center gap-2 text-sm text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-lg px-3 py-2">
40+
<Check class="w-4 h-4" />
41+
<span>Token 已配置</span>
42+
<button class="ml-auto text-xs text-gray-400 hover:text-white transition-colors" @click="handleClear">
43+
清除
44+
</button>
45+
</div>
46+
47+
<div v-else class="flex items-start gap-2 text-sm text-amber-400 bg-amber-500/10 border border-amber-500/20 rounded-lg px-3 py-2">
48+
<AlertCircle class="w-4 h-4 mt-0.5 shrink-0" />
49+
<span>未配置 Token,PR 创建/合并将以模拟模式运行</span>
50+
</div>
51+
52+
<div class="flex gap-3">
53+
<div class="flex-1 relative">
54+
<KeyRound class="w-4 h-4 text-gray-500 absolute left-3 top-1/2 -translate-y-1/2" />
55+
<input
56+
v-model="token"
57+
:type="showToken ? 'text' : 'password'"
58+
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
59+
class="w-full pl-9 pr-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-500 focus:outline-none focus:border-swarm-500 focus:ring-1 focus:ring-swarm-500/30 transition-all text-sm"
60+
/>
61+
</div>
62+
<button
63+
type="button"
64+
class="px-3 py-2.5 text-xs text-gray-400 hover:text-white bg-gray-800 border border-gray-700 rounded-lg transition-colors"
65+
@click="showToken = !showToken"
66+
>
67+
{{ showToken ? '隐藏' : '显示' }}
68+
</button>
69+
<button
70+
class="px-4 py-2.5 bg-swarm-600 text-white rounded-lg text-sm font-medium hover:bg-swarm-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
71+
:disabled="!token.trim()"
72+
@click="handleSave"
73+
>
74+
保存
75+
</button>
76+
</div>
77+
78+
<p class="text-xs text-gray-600">
79+
在 GitHub Settings → Developer settings → Personal access tokens → Tokens (classic) 中创建。
80+
需要 <code class="text-gray-400 bg-gray-800 px-1 py-0.5 rounded">repo</code> 权限。
81+
</p>
82+
</div>
83+
</template>

kimi-code-swarm/src/store/useSwarmStore.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { reactive, computed } from 'vue'
22
import type { AgentTask, LogEntry } from '../types'
33
import { isTauri, execGit, execCommand, killProcess } from '../api/ipc'
4+
import { createPullRequest, mergePullRequest, hasToken } from '../api/github'
45

56
const generateId = () => Math.random().toString(36).substring(2, 10)
67

@@ -218,19 +219,44 @@ export function useSwarmStore() {
218219
}
219220
}
220221

221-
task.status = 'reviewing'
222-
task.prStatus = 'open'
223-
task.prNumber = Math.floor(Math.random() * 100) + 1
224-
task.prUrl = `https://github.com/HelloWorldU/Kimi-Code-Swarm/pull/${task.prNumber}`
225-
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${task.prNumber} 已创建(${isTauri ? '真实' : '模拟'})` })
222+
if (hasToken()) {
223+
try {
224+
const pr = await createPullRequest(task.repoUrl, `feat: ${task.name}`, task.branch)
225+
task.status = 'reviewing'
226+
task.prStatus = 'open'
227+
task.prNumber = pr.number
228+
task.prUrl = pr.html_url
229+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${pr.number} 已创建: ${pr.html_url}` })
230+
} catch (err) {
231+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'error', content: `创建 PR 失败: ${String(err)}` })
232+
}
233+
} else {
234+
task.status = 'reviewing'
235+
task.prStatus = 'open'
236+
task.prNumber = Math.floor(Math.random() * 100) + 1
237+
task.prUrl = `${task.repoUrl.replace(/\.git$/, '')}/pull/${task.prNumber}`
238+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${task.prNumber} 已创建(模拟,未配置 GitHub Token)` })
239+
}
226240
}
227241

228-
function mergePr(id: string) {
242+
async function mergePr(id: string) {
229243
const task = state.tasks.find(t => t.id === id)
230244
if (!task || task.status !== 'reviewing') return
231-
task.status = 'completed'
232-
task.prStatus = 'merged'
233-
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${task.prNumber} 已合并到 main` })
245+
246+
if (hasToken() && task.prNumber) {
247+
try {
248+
await mergePullRequest(task.repoUrl, task.prNumber)
249+
task.status = 'completed'
250+
task.prStatus = 'merged'
251+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${task.prNumber} 已合并到 main` })
252+
} catch (err) {
253+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'error', content: `合并失败: ${String(err)}` })
254+
}
255+
} else {
256+
task.status = 'completed'
257+
task.prStatus = 'merged'
258+
task.logs.push({ id: generateId(), timestamp: new Date(), type: 'system', content: `PR #${task.prNumber} 已合并到 main(模拟)` })
259+
}
234260
}
235261

236262
function rejectPr(id: string) {

0 commit comments

Comments
 (0)