feat: implement ERP AI Assistant Phase 1

Backend (FastAPI + SQLAlchemy + Claude API + RAG):
- Config management with Pydantic v2
- Database engine with connection pooling and SQL injection prevention
- AI engine with Claude API integration (support custom base URL)
- RAG engine with ChromaDB and sentence-transformers
- Requirement analysis service
- Config generation service
- Executor engine with SQL validation
- REST API endpoints: /analyze, /generate, /execute

Frontend (Vue 3 + Element Plus + Pinia):
- Complete 3-step workflow: analyze → generate → execute
- Step indicator with progress visualization
- Analysis result display with field table
- SQL preview with monospace font
- Execute confirmation dialog with safety warning
- Execution result display
- State management with Pinia
- API service integration

Security:
- SQL injection prevention with parameterized queries
- Dangerous SQL operation blocking
- Database password URL encoding
- Transaction auto-rollback
- Pydantic config validation

Features:
- Natural language requirement analysis
- Automated SQL configuration generation
- Safe execution with human review
- LAN access support
- Custom Claude API endpoint support

Documentation:
- README with quick start guide
- Quick start guide
- LAN access configuration
- Dependency fixes guide
- Claude API configuration
- Git operation guide
- Implementation report

Dependencies fixed:
- numpy<2.0.0 for chromadb compatibility
- sentence-transformers==2.7.0 for huggingface_hub compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 14:23:20 +00:00
commit acd73431ae
60 changed files with 11284 additions and 0 deletions

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ERP AI Assistant</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1750
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
frontend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "erp-ai-assistant-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"axios": "^1.6.2",
"element-plus": "^2.4.3",
"@element-plus/icons-vue": "^2.3.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.1",
"vite": "^5.0.4"
}
}

22
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,22 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup>
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
font-family: 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
height: 100vh;
margin: 0;
}
</style>

52
frontend/src/api/index.js Normal file
View File

@@ -0,0 +1,52 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api/v1',
timeout: 60000,
headers: {
'Content-Type': 'application/json'
}
})
// 响应拦截器
api.interceptors.response.use(
response => response.data,
error => {
console.error('API Error:', error)
return Promise.reject(error)
}
)
/**
* 分析用户需求
*/
export const analyzeRequirement = (data) => {
return api.post('/analyze', {
input_type: 'natural_language',
content: data.content,
session_id: data.session_id
})
}
/**
* 生成配置
*/
export const generateConfig = (data) => {
return api.post('/generate', {
session_id: data.session_id,
requirements: data.requirements
})
}
/**
* 执行配置
*/
export const executeConfig = (data) => {
return api.post('/execute', {
session_id: data.session_id,
confirmed: data.confirmed || true,
backup_enabled: data.backup_enabled || true
})
}
export default api

14
frontend/src/main.js Normal file
View File

@@ -0,0 +1,14 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,31 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('../views/Layout.vue'),
children: [
{
path: '',
redirect: '/create'
},
{
path: 'create',
name: 'CreateFunction',
component: () => import('../views/CreateFunction.vue')
},
{
path: 'history',
name: 'History',
component: () => import('../views/History.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@@ -0,0 +1,61 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useFunctionStore = defineStore('function', () => {
// 状态
const currentSession = ref(null)
const analysisResult = ref(null)
const configResult = ref(null)
const executeResult = ref(null)
const loading = ref({
analyzing: false,
generating: false,
executing: false
})
// 重置状态
const reset = () => {
currentSession.value = null
analysisResult.value = null
configResult.value = null
executeResult.value = null
loading.value = {
analyzing: false,
generating: false,
executing: false
}
}
// 设置会话
const setSession = (sessionId) => {
currentSession.value = sessionId
}
// 设置分析结果
const setAnalysisResult = (result) => {
analysisResult.value = result
}
// 设置配置结果
const setConfigResult = (result) => {
configResult.value = result
}
// 设置执行结果
const setExecuteResult = (result) => {
executeResult.value = result
}
return {
currentSession,
analysisResult,
configResult,
executeResult,
loading,
reset,
setSession,
setAnalysisResult,
setConfigResult,
setExecuteResult
}
})

View File

@@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export const pinia = createPinia()

View File

@@ -0,0 +1,380 @@
<template>
<div class="create-function">
<!-- 步骤指示器 -->
<el-steps :active="currentStep" finish-status="success" simple class="steps">
<el-step title="需求分析" />
<el-step title="配置生成" />
<el-step title="执行配置" />
</el-steps>
<!-- 步骤 1: 需求输入 -->
<el-card v-if="currentStep === 0" class="section-card">
<template #header>
<div class="card-header">
<span>步骤 1: 输入需求</span>
</div>
</template>
<el-form :model="form" label-width="120px">
<el-form-item label="需求描述">
<el-input
v-model="form.requirement"
type="textarea"
:rows="6"
placeholder="请输入功能需求,例如:创建一个销售订单管理页面,包含订单号、客户、订单日期、金额等字段"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleAnalyze"
:loading="functionStore.loading.analyzing"
:disabled="!form.requirement.trim()"
>
开始分析需求
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 步骤 2: 分析结果 -->
<el-card v-if="functionStore.analysisResult" class="section-card">
<template #header>
<div class="card-header">
<span>需求分析结果</span>
<el-tag type="success">已完成</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="功能名称">
{{ functionStore.analysisResult.功能名称 || '-' }}
</el-descriptions-item>
<el-descriptions-item label="功能类型">
{{ functionStore.analysisResult.功能类型 || '-' }}
</el-descriptions-item>
<el-descriptions-item label="功能号建议">
{{ functionStore.analysisResult.功能号建议 || '-' }}
</el-descriptions-item>
<el-descriptions-item label="窗体类型">
{{ functionStore.analysisResult.窗体类型 || '-' }}
</el-descriptions-item>
</el-descriptions>
<div v-if="functionStore.analysisResult.主表字段" class="field-list">
<h4>主表字段</h4>
<el-table :data="functionStore.analysisResult.主表字段" border size="small">
<el-table-column prop="字段名" label="字段名" />
<el-table-column prop="字段类型" label="字段类型" />
<el-table-column prop="必填" label="必填">
<template #default="{ row }">
<el-tag :type="row.必填 ? 'danger' : 'info'" size="small">
{{ row.必填 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div class="button-group">
<el-button @click="resetAll">重新分析</el-button>
<el-button
type="primary"
@click="handleGenerate"
:loading="functionStore.loading.generating"
>
生成配置方案
</el-button>
</div>
</el-card>
<!-- 步骤 3: 配置方案 -->
<el-card v-if="functionStore.configResult" class="section-card">
<template #header>
<div class="card-header">
<span>配置方案</span>
<el-tag type="success">已生成</el-tag>
</div>
</template>
<el-alert
title="请仔细检查以下 SQL 配置,确认无误后执行"
type="warning"
:closable="false"
show-icon
class="alert-box"
/>
<div v-if="functionStore.configResult.配置方案" class="config-section">
<h4>SQL 配置语句</h4>
<el-input
v-model="sqlDisplay"
type="textarea"
:rows="15"
readonly
class="sql-textarea"
/>
</div>
<div class="button-group">
<el-button @click="backToAnalysis">返回修改</el-button>
<el-button
type="danger"
@click="showExecuteConfirm"
:loading="functionStore.loading.executing"
>
确认并执行
</el-button>
</div>
</el-card>
<!-- 执行结果 -->
<el-card v-if="functionStore.executeResult" class="section-card">
<template #header>
<div class="card-header">
<span>执行结果</span>
<el-tag :type="functionStore.executeResult.status === 'success' ? 'success' : 'danger'">
{{ functionStore.executeResult.status === 'success' ? '执行成功' : '执行失败' }}
</el-tag>
</div>
</template>
<el-result
:icon="functionStore.executeResult.status === 'success' ? 'success' : 'error'"
:title="functionStore.executeResult.message"
>
<template #extra>
<el-button type="primary" @click="resetAll">创建新功能</el-button>
</template>
</el-result>
</el-card>
<!-- 执行确认对话框 -->
<el-dialog
v-model="executeDialogVisible"
title="确认执行"
width="500px"
>
<el-alert
type="warning"
:closable="false"
show-icon
>
<template #title>
<strong>警告此操作将直接修改数据库</strong>
</template>
<div style="margin-top: 10px;">
<p> 请确认已仔细检查所有 SQL 语句</p>
<p> 建议在生产环境执行前先备份数据库</p>
<p> 执行后可通过历史记录查看详情</p>
</div>
</el-alert>
<template #footer>
<el-button @click="executeDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleExecute">
确认执行
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useFunctionStore } from '@/stores/function'
import { analyzeRequirement, generateConfig, executeConfig } from '@/api'
const functionStore = useFunctionStore()
const form = ref({
requirement: ''
})
const executeDialogVisible = ref(false)
// 当前步骤
const currentStep = computed(() => {
if (functionStore.executeResult) return 3
if (functionStore.configResult) return 2
if (functionStore.analysisResult) return 1
return 0
})
// SQL 显示
const sqlDisplay = computed(() => {
if (!functionStore.configResult?.配置方案?.sql_list) return ''
return functionStore.configResult.配置方案.sql_list.join('\n\n')
})
// 分析需求
const handleAnalyze = async () => {
if (!form.value.requirement.trim()) {
ElMessage.warning('请输入需求描述')
return
}
functionStore.loading.analyzing = true
try {
const result = await analyzeRequirement({
content: form.value.requirement,
session_id: null
})
functionStore.setSession(result.session_id)
functionStore.setAnalysisResult(result.data)
ElMessage.success('需求分析成功')
} catch (error) {
console.error('Analysis failed:', error)
ElMessage.error(
'需求分析失败: ' + (error.response?.data?.detail?.message || error.message)
)
} finally {
functionStore.loading.analyzing = false
}
}
// 生成配置
const handleGenerate = async () => {
if (!functionStore.currentSession || !functionStore.analysisResult) {
ElMessage.warning('请先完成需求分析')
return
}
functionStore.loading.generating = true
try {
const result = await generateConfig({
session_id: functionStore.currentSession,
requirements: functionStore.analysisResult
})
functionStore.setConfigResult(result.data)
ElMessage.success('配置生成成功')
} catch (error) {
console.error('Generation failed:', error)
ElMessage.error(
'配置生成失败: ' + (error.response?.data?.detail?.message || error.message)
)
} finally {
functionStore.loading.generating = false
}
}
// 显示执行确认对话框
const showExecuteConfirm = () => {
executeDialogVisible.value = true
}
// 执行配置
const handleExecute = async () => {
if (!functionStore.currentSession) {
ElMessage.warning('会话信息丢失,请重新开始')
return
}
executeDialogVisible.value = false
functionStore.loading.executing = true
try {
const result = await executeConfig({
session_id: functionStore.currentSession,
confirmed: true,
backup_enabled: true
})
functionStore.setExecuteResult(result)
if (result.status === 'success') {
ElMessage.success('配置执行成功')
} else {
ElMessage.error('配置执行失败')
}
} catch (error) {
console.error('Execution failed:', error)
ElMessage.error(
'配置执行失败: ' + (error.response?.data?.detail?.message || error.message)
)
} finally {
functionStore.loading.executing = false
}
}
// 返回分析步骤
const backToAnalysis = () => {
functionStore.setConfigResult(null)
}
// 重置所有状态
const resetAll = () => {
functionStore.reset()
form.value.requirement = ''
}
</script>
<style scoped>
.create-function {
max-width: 1200px;
margin: 0 auto;
}
.steps {
margin-bottom: 20px;
}
.section-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: bold;
}
.field-list {
margin-top: 20px;
}
.field-list h4 {
margin-bottom: 10px;
color: #606266;
}
.alert-box {
margin-bottom: 20px;
}
.config-section {
margin-top: 20px;
}
.config-section h4 {
margin-bottom: 10px;
color: #606266;
}
.sql-textarea {
font-family: 'Courier New', monospace;
}
.sql-textarea :deep(textarea) {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="history">
<el-card>
<template #header>
<div class="card-header">
<span>历史记录</span>
</div>
</template>
<el-empty description="暂无历史记录" />
</el-card>
</div>
</template>
<script setup>
</script>
<style scoped>
.history {
max-width: 1200px;
margin: 0 auto;
}
.card-header {
font-size: 16px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<el-container class="layout-container">
<el-header class="header">
<div class="logo">ERP智能助手</div>
<div class="user-info">用户: Admin</div>
</el-header>
<el-container>
<el-aside width="200px" class="sidebar">
<el-menu
:default-active="$route.path"
router
>
<el-menu-item index="/create">
<el-icon><Edit /></el-icon>
<span>新建功能</span>
</el-menu-item>
<el-menu-item index="/history">
<el-icon><Document /></el-icon>
<span>历史记录</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { Edit, Document } from '@element-plus/icons-vue'
</script>
<style scoped>
.layout-container {
height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
background: #409EFF;
color: white;
padding: 0 20px;
}
.logo {
font-size: 20px;
font-weight: bold;
}
.user-info {
font-size: 14px;
}
.sidebar {
background: #f5f7fa;
overflow-y: auto;
}
.main-content {
background: #f0f2f5;
padding: 20px;
overflow-y: auto;
}
</style>

17
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0', // 允许局域网访问
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
})