Files
JARVIS/development-doc/plan/code-update/phase-5-frontend-integration.md

8.2 KiB
Raw Blame History

Phase 5前端集成

日期2026-04-04 状态:待实施

依赖Phase 4 完成


1. 本阶段目的

Vue 前端新增代码指挥官 UI

  • 页面组件
  • 终端显示组件
  • WebSocket 服务
  • 路由配置

2. 详细任务

2.1 页面组件

新文件: frontend/src/pages/chat/CodeCommander.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

<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

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

{
  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流式交互
       ↓
本阶段(前端集成)→ 端到端测试