feat(agents): Phase 8.4-10.5 built-in plugins, bundled skills, coordinator
This commit is contained in:
364
development-doc/plan/code-update/phase-5-frontend-integration.md
Normal file
364
development-doc/plan/code-update/phase-5-frontend-integration.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Phase 5:前端集成
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
依赖:Phase 4 完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
Vue 前端新增代码指挥官 UI:
|
||||
- 页面组件
|
||||
- 终端显示组件
|
||||
- WebSocket 服务
|
||||
- 路由配置
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 页面组件
|
||||
|
||||
**新文件**: `frontend/src/pages/chat/CodeCommander.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="code-commander">
|
||||
<!-- AI 提供商选择器 -->
|
||||
<div class="provider-selector">
|
||||
<div class="label">选择 AI 助手</div>
|
||||
<div class="providers">
|
||||
<button
|
||||
v-for="p in providers"
|
||||
:key="p.id"
|
||||
:class="{ active: selectedProvider === p.id }"
|
||||
@click="selectedProvider = p.id"
|
||||
>
|
||||
<img :src="p.icon" :alt="p.name" />
|
||||
{{ p.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务输入 -->
|
||||
<div class="task-input">
|
||||
<textarea
|
||||
v-model="taskPrompt"
|
||||
placeholder="描述你想让 AI 帮你做什么..."
|
||||
rows="4"
|
||||
/>
|
||||
<button @click="executeTask" :disabled="isExecuting">
|
||||
{{ isExecuting ? '执行中...' : '开始执行' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 终端输出 -->
|
||||
<TerminalDisplay
|
||||
ref="terminalRef"
|
||||
:session-id="currentSessionId"
|
||||
@input="handleUserInput"
|
||||
/>
|
||||
|
||||
<!-- 交互输入框 -->
|
||||
<div v-if="isWaitingForInput" class="interactive-input">
|
||||
<span>{{ inputPrompt }}</span>
|
||||
<input v-model="userInput" @keyup.enter="sendUserInput" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions">
|
||||
<button @click="downloadFiles" :disabled="!canDownload">
|
||||
下载文件
|
||||
</button>
|
||||
<button @click="cleanup" :disabled="!canCleanup">
|
||||
清理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import TerminalDisplay from '@/components/TerminalDisplay.vue'
|
||||
import { terminalWsService } from '@/services/terminalWs'
|
||||
|
||||
const providers = [
|
||||
{ id: 'claude', name: 'Claude', icon: '/icons/claude.png' },
|
||||
{ id: 'gemini', name: 'Gemini', icon: '/icons/gemini.png' },
|
||||
{ id: 'codex', name: 'Codex', icon: '/icons/codex.png' },
|
||||
{ id: 'opencode', name: 'OpenCode', icon: '/icons/opencode.png' },
|
||||
]
|
||||
|
||||
const selectedProvider = ref('claude')
|
||||
const taskPrompt = ref('')
|
||||
const isExecuting = ref(false)
|
||||
const currentSessionId = ref<string | null>(null)
|
||||
const isWaitingForInput = ref(false)
|
||||
const inputPrompt = ref('')
|
||||
const userInput = ref('')
|
||||
const terminalRef = ref<InstanceType<typeof TerminalDisplay> | null>(null)
|
||||
|
||||
const canDownload = computed(() => currentSessionId.value !== null)
|
||||
const canCleanup = computed(() => currentSessionId.value !== null)
|
||||
|
||||
async function executeTask() {
|
||||
if (!taskPrompt.value.trim()) return
|
||||
|
||||
isExecuting.value = true
|
||||
currentSessionId.value = await terminalWsService.connect(selectedProvider.value)
|
||||
|
||||
// 订阅消息
|
||||
terminalWsService.onMessage((msg) => {
|
||||
if (msg.type === 'output') {
|
||||
terminalRef.value?.write(msg.data)
|
||||
} else if (msg.type === 'waiting_input') {
|
||||
isWaitingForInput.value = true
|
||||
inputPrompt.value = msg.data
|
||||
} else if (msg.type === 'complete') {
|
||||
isExecuting.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 发送任务
|
||||
await terminalWsService.sendTask(currentSessionId.value, taskPrompt.value)
|
||||
}
|
||||
|
||||
function handleUserInput(data: string) {
|
||||
terminalWsService.sendInput(currentSessionId.value!, data)
|
||||
}
|
||||
|
||||
function sendUserInput() {
|
||||
terminalWsService.sendInput(currentSessionId.value!, userInput.value)
|
||||
userInput.value = ''
|
||||
isWaitingForInput.value = false
|
||||
}
|
||||
|
||||
async function downloadFiles() {
|
||||
// TODO: 调用下载 API
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
if (currentSessionId.value) {
|
||||
await terminalWsService.disconnect(currentSessionId.value)
|
||||
currentSessionId.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2.2 终端显示组件
|
||||
|
||||
**新文件**: `frontend/src/components/TerminalDisplay.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="terminal-display" ref="containerRef">
|
||||
<div class="terminal-output" ref="outputRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import 'xterm/css/xterm.css'
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
input: [data: string]
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const outputRef = ref<HTMLElement | null>(null)
|
||||
let terminal: Terminal | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
|
||||
onMounted(() => {
|
||||
terminal = new Terminal({
|
||||
theme: { background: '#1e1e1e' },
|
||||
cursorBlink: true,
|
||||
})
|
||||
|
||||
fitAddon = new FitAddon()
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
terminal.open(outputRef.value!)
|
||||
fitAddon.fit()
|
||||
|
||||
// 用户输入
|
||||
terminal.onData((data) => {
|
||||
emit('input', data)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
terminal?.dispose()
|
||||
})
|
||||
|
||||
function write(data: string) {
|
||||
terminal?.write(data)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
terminal?.clear()
|
||||
}
|
||||
|
||||
defineExpose({ write, clear })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
padding: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 2.3 WebSocket 服务
|
||||
|
||||
**新文件**: `frontend/src/services/terminalWs.ts`
|
||||
|
||||
```typescript
|
||||
type MessageHandler = (msg: StreamMessage) => void
|
||||
|
||||
interface StreamMessage {
|
||||
type: 'output' | 'error' | 'status' | 'waiting_input' | 'complete'
|
||||
session_id: string
|
||||
data: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
class TerminalWsService {
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null = null
|
||||
private handlers: MessageHandler[] = []
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
|
||||
async connect(provider: string): Promise<string> {
|
||||
// 创建会话
|
||||
const response = await fetch('/api/code-commander/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ provider }),
|
||||
})
|
||||
const { session_id } = await response.json()
|
||||
|
||||
// 建立 WebSocket
|
||||
this.ws = new WebSocket(`ws://localhost:8000/ws/terminal/${session_id}`)
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const msg: StreamMessage = JSON.parse(event.data)
|
||||
this.handlers.forEach((h) => h(msg))
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.attemptReconnect()
|
||||
}
|
||||
|
||||
this.sessionId = session_id
|
||||
return session_id
|
||||
}
|
||||
|
||||
async sendTask(sessionId: string, prompt: string) {
|
||||
await fetch(`/api/code-commander/sessions/${sessionId}/task`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt }),
|
||||
})
|
||||
}
|
||||
|
||||
sendInput(sessionId: string, input: string) {
|
||||
this.ws?.send(JSON.stringify({ type: 'input', data: input }))
|
||||
}
|
||||
|
||||
onMessage(handler: MessageHandler) {
|
||||
this.handlers.push(handler)
|
||||
}
|
||||
|
||||
removeHandler(handler: MessageHandler) {
|
||||
this.handlers = this.handlers.filter((h) => h !== handler)
|
||||
}
|
||||
|
||||
async disconnect(sessionId: string) {
|
||||
await fetch(`/api/code-commander/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
this.ws?.close()
|
||||
this.ws = null
|
||||
this.sessionId = null
|
||||
}
|
||||
|
||||
private async attemptReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
return
|
||||
}
|
||||
this.reconnectAttempts++
|
||||
await new Promise((r) => setTimeout(r, 1000 * this.reconnectAttempts))
|
||||
// 重新连接
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalWsService = new TerminalWsService()
|
||||
```
|
||||
|
||||
### 2.4 路由配置
|
||||
|
||||
**文件**: `frontend/src/router/index.ts`
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/code-commander',
|
||||
name: 'CodeCommander',
|
||||
component: () => import('@/pages/chat/CodeCommander.vue'),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `CodeCommander.vue` | 新增 | 主页面组件 |
|
||||
| `TerminalDisplay.vue` | 新增 | 终端显示组件(xterm.js) |
|
||||
| `terminalWs.ts` | 新增 | WebSocket 服务 |
|
||||
| `router/index.ts` | 修改 | 新增路由 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] 用户可以选择 AI 提供商
|
||||
- [ ] 可以输入任务描述并执行
|
||||
- [ ] 终端实时显示 AI 输出
|
||||
- [ ] 用户可以输入交互(如 "y")
|
||||
- [ ] 可以下载和清理文件
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖
|
||||
|
||||
| 依赖 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| xterm | ^5.x | 终端渲染 |
|
||||
| xterm-addon-fit | ^0.8.x | 自适应大小 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖关系
|
||||
|
||||
```
|
||||
Phase 4(流式交互)
|
||||
↓
|
||||
本阶段(前端集成)→ 端到端测试
|
||||
```
|
||||
Reference in New Issue
Block a user