Agent loop state machine refactor, unified LLM interface, and UI improvements

- Rewrite agent loop as Planning→Executing(N)→Completed state machine with
  per-step context isolation to prevent token explosion
- Split tools and prompts by phase (planning vs execution)
- Add advance_step/save_memo tools for step transitions and cross-step memory
- Unify LLM interface: remove duplicate types, single chat_with_tools path
- Add UTF-8 safe truncation (truncate_str) to prevent panics on Chinese text
- Extract CreateForm component, add auto-scroll to execution log
- Add report generation with app access URL, non-blocking title generation
- Add timer system, file serving, app proxy, exec module
- Update Dockerfile with uv, deployment config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:35:33 +00:00
parent e2d5a6a7eb
commit 2df4e12d30
31 changed files with 3924 additions and 571 deletions

View File

@@ -1,62 +1,122 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import Sidebar from './Sidebar.vue'
import WorkflowView from './WorkflowView.vue'
import ReportView from './ReportView.vue'
import CreateForm from './CreateForm.vue'
import { api } from '../api'
import type { Project } from '../types'
const projects = ref<Project[]>([])
const selectedProjectId = ref('')
const reportWorkflowId = ref('')
const error = ref('')
const creating = ref(false)
const isReportPage = computed(() => !!reportWorkflowId.value)
function parseUrl(): { projectId: string; reportId: string } {
const reportMatch = location.pathname.match(/^\/report\/([^/]+)/)
if (reportMatch) return { projectId: '', reportId: reportMatch[1] ?? '' }
const projectMatch = location.pathname.match(/^\/projects\/([^/]+)/)
return { projectId: projectMatch?.[1] ?? '', reportId: '' }
}
function onPopState() {
const { projectId, reportId } = parseUrl()
selectedProjectId.value = projectId
reportWorkflowId.value = reportId
}
onMounted(async () => {
try {
projects.value = await api.listProjects()
const first = projects.value[0]
if (first) {
selectedProjectId.value = first.id
const { projectId, reportId } = parseUrl()
if (reportId) {
reportWorkflowId.value = reportId
} else if (projectId && projects.value.some(p => p.id === projectId)) {
selectedProjectId.value = projectId
} else if (projects.value[0]) {
selectedProjectId.value = projects.value[0].id
history.replaceState(null, '', `/projects/${projects.value[0].id}`)
}
} catch (e: any) {
error.value = e.message
}
window.addEventListener('popstate', onPopState)
})
onUnmounted(() => {
window.removeEventListener('popstate', onPopState)
})
function onSelectProject(id: string) {
selectedProjectId.value = id
reportWorkflowId.value = ''
creating.value = false
history.pushState(null, '', `/projects/${id}`)
}
async function onCreateProject() {
const name = prompt('项目名称')
if (!name) return
function onStartCreate() {
creating.value = true
selectedProjectId.value = ''
history.pushState(null, '', '/')
}
async function onConfirmCreate(req: string) {
try {
const project = await api.createProject(name)
const project = await api.createProject('新项目')
projects.value.unshift(project)
await api.createWorkflow(project.id, req)
creating.value = false
selectedProjectId.value = project.id
history.pushState(null, '', `/projects/${project.id}`)
} catch (e: any) {
error.value = e.message
}
}
function onProjectUpdate(projectId: string, name: string) {
const p = projects.value.find(p => p.id === projectId)
if (p) p.name = name
}
</script>
<template>
<div class="app-layout">
<div v-if="isReportPage" class="report-fullpage">
<ReportView :workflowId="reportWorkflowId" :key="reportWorkflowId" />
</div>
<div v-else class="app-layout">
<Sidebar
:projects="projects"
:selectedId="selectedProjectId"
@select="onSelectProject"
@create="onCreateProject"
@create="onStartCreate"
/>
<main class="main-content">
<div v-if="error" class="error-banner">{{ error }}</div>
<div v-if="!selectedProjectId" class="empty-state">
<div v-if="error" class="error-banner" @click="error = ''">{{ error }}</div>
<div v-if="creating" class="empty-state">
<CreateForm @submit="onConfirmCreate" @cancel="creating = false" />
</div>
<div v-else-if="!selectedProjectId" class="empty-state">
选择或创建一个项目开始
</div>
<WorkflowView v-else :projectId="selectedProjectId" :key="selectedProjectId" />
<WorkflowView
v-else
:projectId="selectedProjectId"
:key="selectedProjectId"
@projectUpdate="onProjectUpdate"
/>
</main>
</div>
</template>
<style scoped>
.report-fullpage {
height: 100vh;
overflow-y: auto;
}
.app-layout {
display: flex;
height: 100vh;
@@ -84,5 +144,6 @@ async function onCreateProject() {
color: #fff;
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
}
</style>