mirror of
https://gitee.com/ssssssss-team/magic-api.git
synced 2026-05-07 11:00:53 +08:00
feat(ai): 升级 AI 对话界面并支持多模型选择
- 实现多 AI 模型选择功能 - 集成流式响应提升用户体验 - 重构样式增加动画效果 - 添加代码块高亮和复制功能 - 优化响应式布局和交互体验 - 更新 .gitignore 配置文件
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,3 +9,6 @@ bin/
|
||||
.myeclipse
|
||||
node_modules/
|
||||
dist/
|
||||
.gitnexus
|
||||
/.claude/skills
|
||||
|
||||
|
||||
@@ -2,19 +2,46 @@
|
||||
<teleport to=".magic-editor">
|
||||
<div v-if="visible" class="magic-ai-panel">
|
||||
<div class="magic-ai-panel-header">
|
||||
<span>AI Coding 助手</span>
|
||||
<span class="magic-ai-close" @click="onClose"><magic-icon icon="close"/></span>
|
||||
<div class="magic-ai-header-left">
|
||||
<span class="magic-ai-header-icon">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2a4 4 0 0 1 4 4v1a3 3 0 0 1 3 3v1a2 2 0 0 1-2 2h-1l-2 5H10l-2-5H7a2 2 0 0 1-2-2v-1a3 3 0 0 1 3-3V6a4 4 0 0 1 4-4z"/>
|
||||
<circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="magic-ai-header-title">AI Coding 助手</span>
|
||||
</div>
|
||||
<span class="magic-ai-close" @click="onClose">
|
||||
<magic-icon icon="close"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="magic-ai-container">
|
||||
<!-- 对话消息区域 -->
|
||||
<div class="magic-ai-messages" ref="messagesContainer">
|
||||
<div v-if="messages.length === 0" class="magic-ai-welcome">
|
||||
<div class="magic-ai-welcome-icon">✨</div>
|
||||
<div class="magic-ai-welcome-icon-wrap">
|
||||
<svg class="magic-ai-welcome-svg" viewBox="0 0 64 64" width="52" height="52" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="aiGrad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#5b7ee5"/>
|
||||
<stop offset="100%" stop-color="#4da6e8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect rx="16" width="64" height="64" fill="url(#aiGrad)" opacity="0.12"/>
|
||||
<path d="M32 14a8 8 0 0 1 8 8v2a6 6 0 0 1 6 6v2a4 4 0 0 1-4 4h-2l-4 10h-8l-4-10h-2a4 4 0 0 1-4-4v-2a6 6 0 0 1 6-6v-2a8 8 0 0 1 8-8z" stroke="url(#aiGrad)" stroke-width="2.5" fill="none"/>
|
||||
<circle cx="26" cy="26" r="2" fill="#5b7ee5"/>
|
||||
<circle cx="38" cy="26" r="2" fill="#4da6e8"/>
|
||||
<path d="M28 31c1.5 2 6.5 2 8 0" stroke="url(#aiGrad)" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="magic-ai-welcome-title">AI Coding 助手</div>
|
||||
<div class="magic-ai-welcome-desc">我可以帮你按照 magic-api 脚本语法生成代码,请描述你的需求。</div>
|
||||
<div class="magic-ai-suggestions">
|
||||
<div class="magic-ai-suggestion" v-for="s in suggestions" :key="s" @click="sendSuggestion(s)">
|
||||
{{ s }}
|
||||
<span class="magic-ai-suggestion-icon">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M6 3l5 5-5 5"/></svg>
|
||||
</span>
|
||||
<span>{{ s }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,32 +52,48 @@
|
||||
:class="['magic-ai-message', msg.role === 'user' ? 'magic-ai-message--user' : 'magic-ai-message--assistant']"
|
||||
>
|
||||
<div class="magic-ai-message-avatar">
|
||||
<span v-if="msg.role === 'user'">我</span>
|
||||
<span v-else>AI</span>
|
||||
<span v-if="msg.role === 'user'">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" fill="currentColor"><path d="M10 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm-7 8a7 7 0 0 1 14 0H3z"/></svg>
|
||||
</span>
|
||||
<span v-else>
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3a3.5 3.5 0 0 1 3.5 3.5V8A2.5 2.5 0 0 1 16 10.5V12a2 2 0 0 1-2 2h-.5L12 17H8l-1.5-3H6a2 2 0 0 1-2-2v-1.5A2.5 2.5 0 0 1 6.5 8v-1.5A3.5 3.5 0 0 1 10 3z"/><circle cx="8" cy="9" r="0.8" fill="currentColor"/><circle cx="12" cy="9" r="0.8" fill="currentColor"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="magic-ai-message-content">
|
||||
<div v-if="msg.role === 'assistant'" v-html="renderMarkdown(msg.content)"></div>
|
||||
<div v-else>{{ msg.content }}</div>
|
||||
<div class="magic-ai-message-body">
|
||||
<div class="magic-ai-message-content">
|
||||
<div v-if="msg.role === 'assistant'" v-html="renderMarkdown(msg.content)"></div>
|
||||
<div v-else>{{ msg.content }}</div>
|
||||
<span v-if="streaming && msg.role === 'assistant' && index === messages.length - 1" class="magic-ai-cursor">▍</span>
|
||||
</div>
|
||||
<!-- 代码操作按钮 -->
|
||||
<div class="magic-ai-code-actions" v-if="msg.role === 'assistant' && extractCode(msg.content)">
|
||||
<div class="magic-ai-code-actions" v-if="msg.role === 'assistant' && !streaming && extractCode(msg.content)">
|
||||
<button class="magic-ai-btn magic-ai-btn--insert" @click="insertCode(extractCode(msg.content))">
|
||||
插入到编辑器
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
插入代码
|
||||
</button>
|
||||
<button class="magic-ai-btn magic-ai-btn--replace" @click="replaceCode(extractCode(msg.content))">
|
||||
替换编辑器内容
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 8h12M12 4l4 4-4 4"/></svg>
|
||||
替换代码
|
||||
</button>
|
||||
<button class="magic-ai-btn magic-ai-btn--copy" @click="copyCode(extractCode(msg.content))">
|
||||
复制代码
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5v-7A1.5 1.5 0 0 1 3.5 1h7A1.5 1.5 0 0 1 12 2.5V5"/></svg>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 正在生成 -->
|
||||
<div v-if="loading" class="magic-ai-message magic-ai-message--assistant">
|
||||
<div class="magic-ai-message-avatar"><span>AI</span></div>
|
||||
<div class="magic-ai-message-content">
|
||||
<div class="magic-ai-loading">
|
||||
<span></span><span></span><span></span>
|
||||
<!-- 正在等待(非流式阶段才显示loading动画) -->
|
||||
<div v-if="loading && !streaming" class="magic-ai-message magic-ai-message--assistant">
|
||||
<div class="magic-ai-message-avatar">
|
||||
<span>
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3a3.5 3.5 0 0 1 3.5 3.5V8A2.5 2.5 0 0 1 16 10.5V12a2 2 0 0 1-2 2h-.5L12 17H8l-1.5-3H6a2 2 0 0 1-2-2v-1.5A2.5 2.5 0 0 1 6.5 8v-1.5A3.5 3.5 0 0 1 10 3z"/><circle cx="8" cy="9" r="0.8" fill="currentColor"/><circle cx="12" cy="9" r="0.8" fill="currentColor"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="magic-ai-message-body">
|
||||
<div class="magic-ai-message-content">
|
||||
<div class="magic-ai-loading">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,13 +102,29 @@
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="magic-ai-input-area">
|
||||
<!-- 模型选择器 -->
|
||||
<div class="magic-ai-model-selector" v-if="configLoaded && selectableProviders.length > 0">
|
||||
<div class="magic-ai-select-wrap">
|
||||
<select class="magic-ai-select" :value="activeProvider" @change="onProviderChange">
|
||||
<option v-for="p in selectableProviders" :key="p.key" :value="p.key">{{ p.label }}</option>
|
||||
</select>
|
||||
<svg class="magic-ai-select-arrow" viewBox="0 0 12 12" width="10" height="10"><path d="M3 5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div class="magic-ai-select-wrap magic-ai-select-wrap--model">
|
||||
<select class="magic-ai-select" :value="activeModel" @change="onModelChange">
|
||||
<option v-for="m in availableModels" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
<svg class="magic-ai-select-arrow" viewBox="0 0 12 12" width="10" height="10"><path d="M3 5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="magic-ai-input-toolbar">
|
||||
<label class="magic-ai-context-toggle">
|
||||
<input type="checkbox" v-model="useCurrentCode" />
|
||||
携带当前代码作为上下文
|
||||
<span class="magic-ai-toggle-text">携带当前代码上下文</span>
|
||||
</label>
|
||||
<button class="magic-ai-clear-btn" @click="clearMessages" :disabled="messages.length === 0">
|
||||
清空对话
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 4h12M5 4V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1M6 7v5M10 7v5M4 4l1 10h6l1-10"/></svg>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<div class="magic-ai-input-wrapper">
|
||||
@@ -73,12 +132,15 @@
|
||||
ref="inputRef"
|
||||
v-model="inputText"
|
||||
class="magic-ai-textarea"
|
||||
placeholder="描述你需要实现的功能,按 Enter 发送,Shift+Enter 换行..."
|
||||
placeholder="描述你需要实现的功能... Enter 发送,Shift+Enter 换行"
|
||||
@keydown="onKeydown"
|
||||
:disabled="loading"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<button class="magic-ai-send-btn" @click="sendMessage" :disabled="loading || !inputText.trim()">
|
||||
<magic-icon icon="run" />
|
||||
<svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 10l6-6 6 6M10 4v12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +150,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ref, nextTick, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
bus: Object,
|
||||
@@ -100,21 +162,110 @@ const visible = ref(false)
|
||||
const messages = ref([])
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const streaming = ref(false) // 流式输出中
|
||||
const useCurrentCode = ref(false)
|
||||
const messagesContainer = ref(null)
|
||||
const inputRef = ref(null)
|
||||
|
||||
// 模型选择相关状态
|
||||
const activeProvider = ref('')
|
||||
const activeModel = ref('')
|
||||
const configuredProviders = ref([]) // [{key, label, models}]
|
||||
const configLoaded = ref(false)
|
||||
|
||||
const STORAGE_KEY_PROVIDER = 'magic-ai-provider'
|
||||
const STORAGE_KEY_MODEL = 'magic-ai-model'
|
||||
|
||||
// 从 localStorage 读取上次选择
|
||||
const loadLocalSelection = () => {
|
||||
try {
|
||||
const p = localStorage.getItem(STORAGE_KEY_PROVIDER)
|
||||
const m = localStorage.getItem(STORAGE_KEY_MODEL)
|
||||
if (p) activeProvider.value = p
|
||||
if (m) activeModel.value = m
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// 保存选择到 localStorage
|
||||
const saveLocalSelection = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY_PROVIDER, activeProvider.value)
|
||||
localStorage.setItem(STORAGE_KEY_MODEL, activeModel.value)
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// 过滤出后端已配置的提供商
|
||||
const selectableProviders = computed(() => {
|
||||
return configuredProviders.value.map(p => ({ key: p.key, label: p.label || p.key }))
|
||||
})
|
||||
|
||||
// 当前提供商下的可用模型
|
||||
const availableModels = computed(() => {
|
||||
const provider = activeProvider.value
|
||||
const found = configuredProviders.value.find(p => p.key === provider)
|
||||
return found && found.models ? found.models : []
|
||||
})
|
||||
|
||||
// 从后端加载已配置的提供商列表,并恢复本地选择
|
||||
const loadAiConfig = () => {
|
||||
props.request.sendJson('/ai/config/get', {}).success(data => {
|
||||
if (Array.isArray(data)) {
|
||||
configuredProviders.value = data
|
||||
}
|
||||
// 恢复 localStorage 中的选择
|
||||
loadLocalSelection()
|
||||
// 如果本地没有选择或选择的提供商已不存在,使用第一个提供商
|
||||
const providerKeys = configuredProviders.value.map(p => p.key)
|
||||
if (!activeProvider.value || !providerKeys.includes(activeProvider.value)) {
|
||||
if (configuredProviders.value.length > 0) {
|
||||
activeProvider.value = configuredProviders.value[0].key
|
||||
const models = configuredProviders.value[0].models || []
|
||||
activeModel.value = models[0] || ''
|
||||
saveLocalSelection()
|
||||
}
|
||||
} else {
|
||||
// 验证当前模型是否仍在可用列表中
|
||||
const found = configuredProviders.value.find(p => p.key === activeProvider.value)
|
||||
const models = found && found.models ? found.models : []
|
||||
if (activeModel.value && !models.includes(activeModel.value)) {
|
||||
activeModel.value = models[0] || ''
|
||||
saveLocalSelection()
|
||||
}
|
||||
}
|
||||
configLoaded.value = true
|
||||
}).error(() => {
|
||||
configLoaded.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const onProviderChange = (e) => {
|
||||
const newProvider = e.target.value
|
||||
activeProvider.value = newProvider
|
||||
// 自动选择新提供商的第一个模型
|
||||
const found = configuredProviders.value.find(p => p.key === newProvider)
|
||||
const models = found && found.models ? found.models : []
|
||||
activeModel.value = models[0] || ''
|
||||
saveLocalSelection()
|
||||
}
|
||||
|
||||
const onModelChange = (e) => {
|
||||
activeModel.value = e.target.value
|
||||
saveLocalSelection()
|
||||
}
|
||||
|
||||
const suggestions = [
|
||||
'查询用户列表,支持按名称模糊搜索和分页',
|
||||
'根据ID查询用户详情并返回',
|
||||
'新增用户,接收POST请求体参数',
|
||||
'更新用户信息,根据ID修改',
|
||||
'删除用户,根据ID删除',
|
||||
]
|
||||
|
||||
// 从bus事件打开对话框
|
||||
props.bus.$on('ai-dialog-open', () => {
|
||||
visible.value = true
|
||||
if (!configLoaded.value) {
|
||||
loadAiConfig()
|
||||
}
|
||||
nextTick(() => inputRef.value && inputRef.value.focus())
|
||||
})
|
||||
|
||||
@@ -127,6 +278,14 @@ const sendSuggestion = (text) => {
|
||||
sendMessage()
|
||||
}
|
||||
|
||||
// 获取请求的 baseURL(从当前页面路径推导)
|
||||
const getBaseURL = () => {
|
||||
const path = window.location.pathname
|
||||
// 页面路径如 /magic/web/index.html 或 /magic/web/,取目录部分
|
||||
const dir = path.endsWith('/') ? path.slice(0, -1) : path.substring(0, path.lastIndexOf('/'))
|
||||
return window.location.origin + dir
|
||||
}
|
||||
|
||||
const sendMessage = async () => {
|
||||
const text = inputText.value.trim()
|
||||
if (!text || loading.value) return
|
||||
@@ -142,6 +301,7 @@ const sendMessage = async () => {
|
||||
messages.value.push({ role: 'user', content: text })
|
||||
inputText.value = ''
|
||||
loading.value = true
|
||||
streaming.value = false
|
||||
scrollToBottom()
|
||||
|
||||
const history = messages.value.slice(0, -1).slice(-10).map(m => ({
|
||||
@@ -149,19 +309,88 @@ const sendMessage = async () => {
|
||||
content: m.content
|
||||
}))
|
||||
|
||||
props.request.sendJson('/ai/chat', {
|
||||
message: text,
|
||||
history,
|
||||
currentCode: useCurrentCode.value ? currentCode : ''
|
||||
}).success(response => {
|
||||
messages.value.push({ role: 'assistant', content: response.message })
|
||||
// 添加空的 assistant 消息占位,用于流式追加
|
||||
messages.value.push({ role: 'assistant', content: '' })
|
||||
const msgIndex = messages.value.length - 1
|
||||
scrollToBottom()
|
||||
|
||||
const token = localStorage.getItem('magic-token') || 'unauthorization'
|
||||
const baseURL = getBaseURL()
|
||||
|
||||
try {
|
||||
const resp = await fetch(baseURL + '/ai/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'magic-token': token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
history,
|
||||
currentCode: useCurrentCode.value ? currentCode : '',
|
||||
provider: activeProvider.value,
|
||||
model: activeModel.value
|
||||
})
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
messages.value[msgIndex].content = '请求失败,状态码: ' + resp.status
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
|
||||
streaming.value = true
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
// 按行解析 SSE 数据
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() // 保留不完整的最后一行
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith(':')) continue
|
||||
if (trimmed.startsWith('event:done') || trimmed.startsWith('event: done')) {
|
||||
// 流结束
|
||||
continue
|
||||
}
|
||||
if (trimmed.startsWith('data:')) {
|
||||
const data = trimmed.substring(5)
|
||||
if (data.trim() === '[DONE]') continue
|
||||
// SSE data 是 JSON 编码的字符串,parse 还原换行等特殊字符
|
||||
try {
|
||||
const text = JSON.parse(data)
|
||||
messages.value[msgIndex].content += text
|
||||
} catch (e) {
|
||||
// 解析失败则直接追加原始文本
|
||||
messages.value[msgIndex].content += data
|
||||
}
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果最终内容为空,显示提示
|
||||
if (!messages.value[msgIndex].content.trim()) {
|
||||
messages.value[msgIndex].content = 'AI返回了空内容,请检查配置。'
|
||||
}
|
||||
} catch (e) {
|
||||
if (!messages.value[msgIndex].content) {
|
||||
messages.value[msgIndex].content = '请求失败,请检查AI配置是否正确。'
|
||||
} else {
|
||||
messages.value[msgIndex].content += '\n\n连接中断'
|
||||
}
|
||||
scrollToBottom()
|
||||
}).error(() => {
|
||||
messages.value.push({ role: 'assistant', content: '❌ 请求失败,请检查AI配置是否正确。' })
|
||||
scrollToBottom()
|
||||
}).end(() => {
|
||||
} finally {
|
||||
loading.value = false
|
||||
})
|
||||
streaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onKeydown = (e) => {
|
||||
@@ -242,101 +471,183 @@ defineExpose({ visible })
|
||||
top: var(--magic-header-height);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 380px;
|
||||
width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--main-background-color);
|
||||
border-left: 1px solid var(--main-border-color);
|
||||
z-index: 100;
|
||||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.08);
|
||||
animation: slideInRight 0.2s ease-out;
|
||||
}
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.magic-ai-panel-header {
|
||||
height: var(--magic-header-height);
|
||||
line-height: var(--magic-header-height);
|
||||
padding: 0 10px;
|
||||
height: 44px;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: var(--main-color);
|
||||
background: var(--main-background-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.magic-ai-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.magic-ai-header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #5b7ee5 0%, #4da6e8 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.magic-ai-header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--main-color);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.magic-ai-close {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.6;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.magic-ai-close:hover {
|
||||
opacity: 1;
|
||||
background: var(--select-option-hover-background-color);
|
||||
}
|
||||
.magic-ai-close:hover { opacity: 1; }
|
||||
.magic-ai-close :deep(.magic-icon) {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.magic-ai-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--main-background-color);
|
||||
}
|
||||
|
||||
/* Messages area */
|
||||
.magic-ai-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
.magic-ai-messages::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.magic-ai-messages::-webkit-scrollbar-thumb {
|
||||
background: var(--main-border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.magic-ai-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Welcome */
|
||||
.magic-ai-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--main-color);
|
||||
opacity: 0.8;
|
||||
padding: 20px;
|
||||
}
|
||||
.magic-ai-welcome-icon-wrap {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.magic-ai-welcome-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--main-color);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.magic-ai-welcome-icon { font-size: 48px; margin-bottom: 10px; }
|
||||
.magic-ai-welcome-title { font-size: 18px; font-weight: bold; margin-bottom: 8px; }
|
||||
.magic-ai-welcome-desc {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--empty-color);
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
max-width: 280px;
|
||||
}
|
||||
.magic-ai-suggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-width: 360px;
|
||||
}
|
||||
.magic-ai-suggestion {
|
||||
padding: 8px 14px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--main-color);
|
||||
transition: background 0.15s;
|
||||
transition: all 0.15s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.magic-ai-suggestion-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.4;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.magic-ai-suggestion:hover {
|
||||
background: var(--select-option-hover-background-color);
|
||||
color: var(--select-option-hover-color);
|
||||
border-color: #5b7ee5;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.magic-ai-suggestion:hover .magic-ai-suggestion-icon {
|
||||
opacity: 0.8;
|
||||
color: #5b7ee5;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.magic-ai-message {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
animation: fadeInMsg 0.2s ease-out;
|
||||
}
|
||||
@keyframes fadeInMsg {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.magic-ai-message--user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.magic-ai-message--user { flex-direction: row-reverse; }
|
||||
.magic-ai-message-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -344,24 +655,37 @@ defineExpose({ visible })
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.magic-ai-message--user .magic-ai-message-avatar { background: #1976d2; color: #fff; }
|
||||
.magic-ai-message--assistant .magic-ai-message-avatar { background: #43a047; color: #fff; }
|
||||
.magic-ai-message--user .magic-ai-message-avatar {
|
||||
background: #5b7ee5;
|
||||
color: #fff;
|
||||
}
|
||||
.magic-ai-message--assistant .magic-ai-message-avatar {
|
||||
background: linear-gradient(135deg, #5b7ee5 0%, #4da6e8 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.magic-ai-message-body {
|
||||
max-width: 82%;
|
||||
min-width: 0;
|
||||
}
|
||||
.magic-ai-message-content {
|
||||
max-width: 85%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.65;
|
||||
color: var(--main-color);
|
||||
background: var(--select-option-hover-background-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
.magic-ai-message--user .magic-ai-message-content {
|
||||
background: #1565c0;
|
||||
color: #fff;
|
||||
border-radius: 8px 2px 8px 8px;
|
||||
background: var(--select-option-hover-background-color);
|
||||
color: var(--main-color);
|
||||
border-radius: 12px 4px 12px 12px;
|
||||
}
|
||||
.magic-ai-message--assistant .magic-ai-message-content { border-radius: 2px 8px 8px 8px; }
|
||||
.magic-ai-message--assistant .magic-ai-message-content {
|
||||
border-radius: 4px 12px 12px 12px;
|
||||
}
|
||||
|
||||
/* Code action buttons */
|
||||
.magic-ai-code-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
@@ -369,74 +693,162 @@ defineExpose({ visible })
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.magic-ai-btn {
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
border: 1px solid;
|
||||
border: none;
|
||||
transition: all 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.magic-ai-btn--insert { background: #43a047; color: #fff; border-color: #43a047; }
|
||||
.magic-ai-btn--insert:hover { background: #388e3c; }
|
||||
.magic-ai-btn--replace { background: #1976d2; color: #fff; border-color: #1976d2; }
|
||||
.magic-ai-btn--replace:hover { background: #1565c0; }
|
||||
.magic-ai-btn--copy { background: transparent; color: var(--main-color); border-color: var(--main-border-color); }
|
||||
.magic-ai-btn--copy:hover { background: var(--select-option-hover-background-color); }
|
||||
.magic-ai-btn--insert {
|
||||
background: var(--select-option-hover-background-color);
|
||||
color: var(--main-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
.magic-ai-btn--insert:hover {
|
||||
background: #5b7ee5;
|
||||
color: #fff;
|
||||
border-color: #5b7ee5;
|
||||
}
|
||||
.magic-ai-btn--replace {
|
||||
background: var(--select-option-hover-background-color);
|
||||
color: var(--main-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
.magic-ai-btn--replace:hover {
|
||||
background: #5b7ee5;
|
||||
color: #fff;
|
||||
border-color: #5b7ee5;
|
||||
}
|
||||
.magic-ai-btn--copy {
|
||||
background: transparent;
|
||||
color: var(--main-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
.magic-ai-btn--copy:hover {
|
||||
background: var(--select-option-hover-background-color);
|
||||
}
|
||||
|
||||
/* Loading dots */
|
||||
.magic-ai-loading {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.magic-ai-loading span {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--main-icon-color);
|
||||
animation: aiDot 1.2s infinite;
|
||||
background: var(--main-icon-color, #999);
|
||||
animation: aiDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.magic-ai-loading span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.magic-ai-loading span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes aiDot {
|
||||
0%, 60%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
30% { transform: scale(1); opacity: 1; }
|
||||
0%, 80%, 100% { transform: scale(0.5); opacity: 0.3; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Input area */
|
||||
.magic-ai-input-area {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding: 8px 12px 10px;
|
||||
padding: 10px 14px 12px;
|
||||
background: var(--main-background-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Model selector */
|
||||
.magic-ai-model-selector {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.magic-ai-select-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.magic-ai-select-wrap--model {
|
||||
flex: 1.5;
|
||||
}
|
||||
.magic-ai-select {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 22px 0 8px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--main-background-color);
|
||||
color: var(--main-color);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.magic-ai-select:focus {
|
||||
border-color: #5b7ee5;
|
||||
}
|
||||
.magic-ai-select-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.magic-ai-input-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--empty-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.magic-ai-context-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.magic-ai-context-toggle input { cursor: pointer; accent-color: #1976d2; }
|
||||
.magic-ai-context-toggle input {
|
||||
cursor: pointer;
|
||||
accent-color: #5b7ee5;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.magic-ai-toggle-text {
|
||||
font-size: 11px;
|
||||
color: var(--empty-color);
|
||||
}
|
||||
.magic-ai-clear-btn {
|
||||
padding: 2px 8px;
|
||||
padding: 3px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: var(--empty-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.magic-ai-clear-btn:hover:not(:disabled) {
|
||||
background: var(--select-option-hover-background-color);
|
||||
color: var(--main-color);
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
.magic-ai-clear-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.magic-ai-clear-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
/* Input wrapper */
|
||||
.magic-ai-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -444,11 +856,11 @@ defineExpose({ visible })
|
||||
}
|
||||
.magic-ai-textarea {
|
||||
flex: 1;
|
||||
min-height: 60px;
|
||||
min-height: 56px;
|
||||
max-height: 120px;
|
||||
padding: 8px 10px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
border-radius: 10px;
|
||||
resize: none;
|
||||
font-size: 13px;
|
||||
background: var(--main-background-color);
|
||||
@@ -456,70 +868,111 @@ defineExpose({ visible })
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.magic-ai-textarea:focus { border-color: #1976d2; }
|
||||
.magic-ai-textarea:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.magic-ai-textarea:focus {
|
||||
border-color: #5b7ee5;
|
||||
box-shadow: 0 0 0 2px rgba(91, 126, 229, 0.12);
|
||||
}
|
||||
.magic-ai-textarea:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.magic-ai-textarea::placeholder { color: var(--empty-color); font-size: 12px; }
|
||||
|
||||
/* Send button */
|
||||
.magic-ai-send-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 6px;
|
||||
background: #43a047;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #5b7ee5 0%, #4da6e8 100%);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s;
|
||||
transition: all 0.2s;
|
||||
color: #fff;
|
||||
}
|
||||
.magic-ai-send-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px rgba(91, 126, 229, 0.3);
|
||||
}
|
||||
.magic-ai-send-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.magic-ai-send-btn:hover:not(:disabled) { background: #388e3c; }
|
||||
.magic-ai-send-btn:disabled {
|
||||
background: var(--main-border-color);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Streaming cursor */
|
||||
.magic-ai-cursor {
|
||||
display: inline;
|
||||
animation: blink 1s step-end infinite;
|
||||
color: var(--main-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
.magic-ai-send-btn :deep(.magic-icon) { fill: #fff; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Code block global styles */
|
||||
.magic-ai-message-content .magic-ai-code-block {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
background: #1a1b26;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.magic-ai-message-content .magic-ai-code-block pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
padding: 14px 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.magic-ai-message-content .magic-ai-code-block pre::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.magic-ai-message-content .magic-ai-code-block pre::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.magic-ai-message-content .magic-ai-code-block code {
|
||||
color: #d4d4d4;
|
||||
font-family: 'JetBrainsMono', Consolas, 'Courier New', monospace;
|
||||
color: #c0caf5;
|
||||
font-family: 'JetBrainsMono', 'Fira Code', Consolas, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
}
|
||||
.magic-ai-message-content .magic-ai-inline-code {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
background: var(--select-option-hover-background-color);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrainsMono', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.magic-ai-message--user .magic-ai-message-content .magic-ai-inline-code {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: var(--main-color);
|
||||
}
|
||||
.magic-ai-message-content h2,
|
||||
.magic-ai-message-content h3,
|
||||
.magic-ai-message-content h4 {
|
||||
margin: 8px 0 4px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 4px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.magic-ai-message-content h2 { font-size: 15px; }
|
||||
.magic-ai-message-content h3 { font-size: 14px; }
|
||||
.magic-ai-message-content h4 { font-size: 13px; }
|
||||
.magic-ai-message-content ul {
|
||||
margin: 4px 0;
|
||||
padding-left: 20px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
.magic-ai-message-content li {
|
||||
margin: 3px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.magic-ai-message-content strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.magic-ai-message-content li { margin: 2px 0; }
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
copy: 'Copy Code',
|
||||
copySuccess: 'Code copied to clipboard',
|
||||
user: 'Me',
|
||||
assistant: 'AI'
|
||||
assistant: 'AI',
|
||||
provider: 'Provider',
|
||||
model: 'Model',
|
||||
switchFailed: 'Failed to switch model, please check configuration'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
copy: '复制代码',
|
||||
copySuccess: '代码已复制到剪贴板',
|
||||
user: '我',
|
||||
assistant: 'AI'
|
||||
assistant: 'AI',
|
||||
provider: '提供商',
|
||||
model: '模型',
|
||||
switchFailed: '切换模型失败,请检查配置'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<svg class="icon" style="width: 1em; height: 1em; vertical-align: middle; fill: currentColor; overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#83D3E7"/>
|
||||
<stop offset="50%" stop-color="#6950BE"/>
|
||||
<stop offset="100%" stop-color="#C53B77"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M512 128C300.32 128 128 292.32 128 496C128 619.36 190.88 729.44 287.84 793.28L319.36 757.28C239.04 702.56 192 606.24 192 496C192 327.52 331.04 192 512 192C692.96 192 832 327.52 832 496C832 606.24 784.96 702.56 704.64 757.28L736.16 793.28C833.12 729.44 896 619.36 896 496C896 292.32 723.68 128 512 128ZM512 256C366.24 256 256 362.08 256 496C256 586.4 299.68 666.08 370.4 715.36L401.92 679.36C348.64 640.16 320 570.72 320 496C320 397.92 398.24 320 512 320C625.76 320 704 397.92 704 496C704 570.72 675.36 640.16 622.08 679.36L653.6 715.36C724.32 666.08 768 586.4 768 496C768 362.08 657.76 256 512 256ZM512 384C432.64 384 384 436.64 384 496C384 539.36 403.36 580.32 442.24 605.28L473.76 569.28C446.56 551.2 448 523.04 448 496C448 469.28 473.76 448 512 448C550.24 448 576 469.28 576 496C576 523.04 577.44 551.2 550.24 569.28L581.76 605.28C620.64 580.32 640 539.36 640 496C640 436.64 591.36 384 512 384Z" fill="url(#logoGradient)"/>
|
||||
</svg>
|
||||
<svg width="50" height="50" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#83D3E7"/>
|
||||
<stop offset="50%" stop-color="#6950BE"/>
|
||||
<stop offset="100%" stop-color="#C53B77"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M512 128C300.32 128 128 292.32 128 496C128 619.36 190.88 729.44 287.84 793.28L319.36 757.28C239.04 702.56 192 606.24 192 496C192 327.52 331.04 192 512 192C692.96 192 832 327.52 832 496C832 606.24 784.96 702.56 704.64 757.28L736.16 793.28C833.12 729.44 896 619.36 896 496C896 292.32 723.68 128 512 128ZM512 256C366.24 256 256 362.08 256 496C256 586.4 299.68 666.08 370.4 715.36L401.92 679.36C348.64 640.16 320 570.72 320 496C320 397.92 398.24 320 512 320C625.76 320 704 397.92 704 496C704 570.72 675.36 640.16 622.08 679.36L653.6 715.36C724.32 666.08 768 586.4 768 496C768 362.08 657.76 256 512 256ZM512 384C432.64 384 384 436.64 384 496C384 539.36 403.36 580.32 442.24 605.28L473.76 569.28C446.56 551.2 448 523.04 448 496C448 469.28 473.76 448 512 448C550.24 448 576 469.28 576 496C576 523.04 577.44 551.2 550.24 569.28L581.76 605.28C620.64 580.32 640 539.36 640 496C640 436.64 591.36 384 512 384Z" fill="url(#logoGradient)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -6,7 +6,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.ssssssss.magicapi.ai.service.*;
|
||||
import org.ssssssss.magicapi.ai.service.AiServiceManager;
|
||||
import org.ssssssss.magicapi.ai.web.MagicAiController;
|
||||
import org.ssssssss.magicapi.core.config.MagicConfiguration;
|
||||
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
|
||||
@@ -27,38 +27,17 @@ public class MagicAiConfiguration implements MagicPluginConfiguration {
|
||||
|
||||
public MagicAiConfiguration(MagicAiProperties properties) {
|
||||
this.properties = properties;
|
||||
logger.info("Magic AI Coding 模块已启用,提供商: {}", properties.getProvider());
|
||||
logger.info("Magic AI Coding 模块已启用");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AiService aiService() {
|
||||
String provider = properties.getProvider();
|
||||
String apiUrl = properties.getApiUrl();
|
||||
String apiKey = properties.getApiKey();
|
||||
String model = properties.getModel();
|
||||
|
||||
logger.info("初始化AI服务,提供商: {}, 模型: {}", provider, model);
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case "dashscope":
|
||||
case "aliyun":
|
||||
return new DashScopeService(apiUrl, apiKey, model);
|
||||
case "zhipu":
|
||||
case "glm":
|
||||
return new ZhipuAiService(apiUrl, apiKey, model);
|
||||
case "minimax":
|
||||
return new MiniMaxService(apiUrl, apiKey, model);
|
||||
case "deepseek":
|
||||
return new DeepSeekService(apiUrl, apiKey, model);
|
||||
case "openai":
|
||||
default:
|
||||
return new OpenAiService(apiUrl, apiKey, model);
|
||||
}
|
||||
public AiServiceManager aiServiceManager() {
|
||||
return new AiServiceManager(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MagicAiController magicAiController(MagicConfiguration configuration, AiService aiService) {
|
||||
return new MagicAiController(configuration, aiService);
|
||||
public MagicAiController magicAiController(MagicConfiguration configuration, AiServiceManager aiServiceManager) {
|
||||
return new MagicAiController(configuration, aiServiceManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +48,7 @@ public class MagicAiConfiguration implements MagicPluginConfiguration {
|
||||
@Override
|
||||
public MagicControllerRegister controllerRegister() {
|
||||
return (mapping, configuration) -> {
|
||||
MagicAiController controller = magicAiController(configuration, aiService());
|
||||
MagicAiController controller = magicAiController(configuration, aiServiceManager());
|
||||
mapping.registerController(controller);
|
||||
logger.info("AI Coding 控制器已注册");
|
||||
};
|
||||
|
||||
@@ -2,6 +2,12 @@ package org.ssssssss.magicapi.ai.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI配置属性
|
||||
*/
|
||||
@@ -14,7 +20,24 @@ public class MagicAiProperties {
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* AI服务提供商类型:openai, azure, custom
|
||||
* 默认活跃提供商(无持久化选择时使用)
|
||||
*/
|
||||
private String activeProvider = "openai";
|
||||
|
||||
/**
|
||||
* 默认活跃模型(无持久化选择时使用)
|
||||
*/
|
||||
private String activeModel = "";
|
||||
|
||||
/**
|
||||
* 多提供商配置,key为提供商名称(如 deepseek、openai、dashscope、zhipu、minimax)
|
||||
*/
|
||||
private Map<String, ProviderConfig> providers = new LinkedHashMap<>();
|
||||
|
||||
// --- 向下兼容:旧的单提供商字段 ---
|
||||
|
||||
/**
|
||||
* AI服务提供商类型:openai, deepseek, dashscope, zhipu, minimax
|
||||
*/
|
||||
private String provider = "openai";
|
||||
|
||||
@@ -33,6 +56,29 @@ public class MagicAiProperties {
|
||||
*/
|
||||
private String model = "gpt-3.5-turbo";
|
||||
|
||||
/**
|
||||
* 获取有效的提供商配置。
|
||||
* 如果配置了 providers map 则直接返回;
|
||||
* 否则从旧的单提供商字段合成一个单条目 map,实现向下兼容。
|
||||
*/
|
||||
public Map<String, ProviderConfig> getEffectiveProviders() {
|
||||
if (!providers.isEmpty()) {
|
||||
return providers;
|
||||
}
|
||||
if (provider != null && apiKey != null) {
|
||||
ProviderConfig config = new ProviderConfig();
|
||||
config.setApiUrl(apiUrl);
|
||||
config.setApiKey(apiKey);
|
||||
config.setModel(model);
|
||||
Map<String, ProviderConfig> map = new LinkedHashMap<>();
|
||||
map.put(provider.toLowerCase(), config);
|
||||
return map;
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// --- Getters / Setters ---
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
@@ -41,6 +87,30 @@ public class MagicAiProperties {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getActiveProvider() {
|
||||
return activeProvider;
|
||||
}
|
||||
|
||||
public void setActiveProvider(String activeProvider) {
|
||||
this.activeProvider = activeProvider;
|
||||
}
|
||||
|
||||
public String getActiveModel() {
|
||||
return activeModel;
|
||||
}
|
||||
|
||||
public void setActiveModel(String activeModel) {
|
||||
this.activeModel = activeModel;
|
||||
}
|
||||
|
||||
public Map<String, ProviderConfig> getProviders() {
|
||||
return providers;
|
||||
}
|
||||
|
||||
public void setProviders(Map<String, ProviderConfig> providers) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
@@ -72,4 +142,60 @@ public class MagicAiProperties {
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个提供商的配置
|
||||
*/
|
||||
public static class ProviderConfig {
|
||||
|
||||
private String label;
|
||||
|
||||
private String apiUrl;
|
||||
|
||||
private String apiKey;
|
||||
|
||||
private String model;
|
||||
|
||||
private List<String> models = new ArrayList<>();
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getApiUrl() {
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
public void setApiUrl(String apiUrl) {
|
||||
this.apiUrl = apiUrl;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<String> getModels() {
|
||||
return models;
|
||||
}
|
||||
|
||||
public void setModels(List<String> models) {
|
||||
this.models = models;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ public class AiChatRequest {
|
||||
*/
|
||||
private String currentCode;
|
||||
|
||||
/**
|
||||
* 指定使用的提供商(由前端传入)
|
||||
*/
|
||||
private String provider;
|
||||
|
||||
/**
|
||||
* 指定使用的模型(由前端传入)
|
||||
*/
|
||||
private String model;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
@@ -45,4 +55,20 @@ public class AiChatRequest {
|
||||
public void setCurrentCode(String currentCode) {
|
||||
this.currentCode = currentCode;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void setProvider(String provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* AI服务接口
|
||||
@@ -12,101 +13,31 @@ import java.util.List;
|
||||
public interface AiService {
|
||||
|
||||
/**
|
||||
* 发送对话消息,获取AI回复
|
||||
*
|
||||
* @param request 请求对象,包含消息和历史记录
|
||||
* @return AI回复
|
||||
* 发送对话消息,获取AI回复(同步)
|
||||
*/
|
||||
AiChatResponse chat(AiChatRequest request);
|
||||
|
||||
/**
|
||||
* 构建包含magic-script语法说明的系统提示词
|
||||
* 流式对话,逐块推送AI回复内容
|
||||
* 默认实现降级为同步一次推送,子类可覆盖实现真正的流式
|
||||
*/
|
||||
default void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
AiChatResponse resp = chat(request);
|
||||
onToken.accept(resp.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建系统提示词(根据用户消息动态匹配 Skill 模块)
|
||||
*/
|
||||
default String buildSystemPrompt(String userMessage) {
|
||||
return SkillPromptLoader.buildPrompt(userMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建系统提示词(无参兼容版本,加载默认 Skill)
|
||||
*/
|
||||
default String buildSystemPrompt() {
|
||||
return "你是一个magic-api脚本专家,专门帮助用户编写magic-script脚本代码。\n\n" +
|
||||
"magic-script 是magic-api框架使用的脚本引擎,语法类似JavaScript,但有以下特点:\n\n" +
|
||||
"## 基础语法\n" +
|
||||
"```\n" +
|
||||
"// 变量定义\n" +
|
||||
"var name = 'hello'\n" +
|
||||
"var age = 18\n\n" +
|
||||
"// 字符串插值\n" +
|
||||
"var message = `Hello ${name}, you are ${age} years old`\n\n" +
|
||||
"// 条件语句\n" +
|
||||
"if (age > 18) {\n" +
|
||||
" return 'adult'\n" +
|
||||
"} else {\n" +
|
||||
" return 'minor'\n" +
|
||||
"}\n\n" +
|
||||
"// 循环\n" +
|
||||
"for (var item in list) {\n" +
|
||||
" // 处理每个元素\n" +
|
||||
"}\n" +
|
||||
"```\n\n" +
|
||||
"## 数据库操作 (db模块)\n" +
|
||||
"```\n" +
|
||||
"// 查询列表\n" +
|
||||
"return db.select('select * from user where status = ?', [1])\n\n" +
|
||||
"// 分页查询\n" +
|
||||
"return db.page('select * from user')\n\n" +
|
||||
"// 查询单条记录\n" +
|
||||
"return db.selectOne('select * from user where id = ?', [id])\n\n" +
|
||||
"// 新增\n" +
|
||||
"return db.insert('insert into user(name, age) values(?, ?)', [name, age])\n\n" +
|
||||
"// 更新\n" +
|
||||
"return db.update('update user set name = ? where id = ?', [name, id])\n\n" +
|
||||
"// 删除\n" +
|
||||
"return db.delete('delete from user where id = ?', [id])\n\n" +
|
||||
"// 动态SQL(MyBatis风格)\n" +
|
||||
"return db.select(\"\"\"\n" +
|
||||
" select * from user\n" +
|
||||
" <where>\n" +
|
||||
" <if test=\"name != null\">and name like concat('%', #{name}, '%')</if>\n" +
|
||||
" <if test=\"status != null\">and status = #{status}</if>\n" +
|
||||
" </where>\n" +
|
||||
"\"\"\")\n" +
|
||||
"```\n\n" +
|
||||
"## 请求参数获取\n" +
|
||||
"```\n" +
|
||||
"// 获取路径参数(需在API路径中定义 /user/{id})\n" +
|
||||
"var userId = id\n\n" +
|
||||
"// 获取请求体参数(POST请求)\n" +
|
||||
"var body = body\n\n" +
|
||||
"// 获取请求头\n" +
|
||||
"var token = request.getHeader('Authorization')\n" +
|
||||
"```\n\n" +
|
||||
"## 常用内置对象\n" +
|
||||
"```\n" +
|
||||
"// request 对象 - 获取请求信息\n" +
|
||||
"request.getHeader('name') // 获取请求头\n" +
|
||||
"request.getParameter('name') // 获取请求参数\n" +
|
||||
"\n" +
|
||||
"// response 对象 - 设置响应信息\n" +
|
||||
"response.setHeader('name', 'value') // 设置响应头\n" +
|
||||
"\n" +
|
||||
"// session 对象 - 操作session\n" +
|
||||
"session.setAttribute('key', value) // 设置session\n" +
|
||||
"session.getAttribute('key') // 获取session\n" +
|
||||
"```\n\n" +
|
||||
"## 使用Spring Bean\n" +
|
||||
"```\n" +
|
||||
"// 导入Spring Bean\n" +
|
||||
"import {userService} from 'spring'\n" +
|
||||
"var user = userService.findById(id)\n" +
|
||||
"```\n\n" +
|
||||
"## 导入Java类\n" +
|
||||
"```\n" +
|
||||
"import java.util.Date\n" +
|
||||
"var now = new Date()\n" +
|
||||
"```\n\n" +
|
||||
"## HTTP请求\n" +
|
||||
"```\n" +
|
||||
"import {http} from 'magic'\n" +
|
||||
"var result = http.get('https://api.example.com/data')\n" +
|
||||
"```\n\n" +
|
||||
"请根据用户的需求,生成符合magic-script语法的代码。\n" +
|
||||
"生成代码时,使用代码块标记(```)包裹代码,并指定语言类型为 `magicscript`。\n" +
|
||||
"代码要简洁、易懂,并添加必要的注释。";
|
||||
return buildSystemPrompt("");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,14 @@ import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 阿里百炼(DashScope)AI服务实现
|
||||
@@ -37,13 +44,36 @@ public class DashScopeService implements AiService {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
|
||||
ObjectNode model = requestBody.putObject("model");
|
||||
model.put("name", this.model);
|
||||
requestBody.put("model", this.model);
|
||||
|
||||
ObjectNode input = requestBody.putObject("input");
|
||||
ArrayNode messages = input.putArray("messages");
|
||||
@@ -51,7 +81,7 @@ public class DashScopeService implements AiService {
|
||||
// 系统提示词
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt());
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
|
||||
// 代码上下文
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
@@ -109,4 +139,51 @@ public class DashScopeService implements AiService {
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
// 流式使用 DashScope 的 OpenAI 兼容端点
|
||||
String streamUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(streamUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) {
|
||||
break;
|
||||
}
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
String content = chunk.path("choices").path(0)
|
||||
.path("delta").path("content").asText(null);
|
||||
if (content != null) {
|
||||
onToken.accept(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("DashScope流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@ import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* DeepSeek AI服务实现(兼容OpenAI接口格式)
|
||||
@@ -37,41 +44,37 @@ public class DeepSeekService implements AiService {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", false);
|
||||
|
||||
ArrayNode messages = requestBody.putArray("messages");
|
||||
|
||||
// 系统提示词
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt());
|
||||
|
||||
// 代码上下文
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
|
||||
// 历史消息
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户消息
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
@@ -103,4 +106,49 @@ public class DeepSeekService implements AiService {
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) {
|
||||
break;
|
||||
}
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
String content = chunk.path("choices").path(0)
|
||||
.path("delta").path("content").asText(null);
|
||||
if (content != null) {
|
||||
onToken.accept(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("DeepSeek流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@ import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* MiniMax AI服务实现
|
||||
@@ -37,40 +44,37 @@ public class MiniMaxService implements AiService {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
|
||||
ArrayNode messages = requestBody.putArray("messages");
|
||||
|
||||
// 系统提示词
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt());
|
||||
|
||||
// 代码上下文
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
|
||||
// 历史消息
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户消息
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
requestBody.put("stream", false);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
@@ -82,12 +86,26 @@ public class MiniMaxService implements AiService {
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, entity, String.class);
|
||||
|
||||
logger.info("MiniMax响应状态码: {}, 响应体: {}", response.getStatusCodeValue(), response.getBody());
|
||||
|
||||
if (response.getStatusCodeValue() != 200) {
|
||||
logger.error("MiniMax接口请求失败,状态码: {}", response.getStatusCodeValue());
|
||||
return new AiChatResponse("AI服务请求失败,状态码: " + response.getStatusCodeValue());
|
||||
}
|
||||
|
||||
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
||||
|
||||
// 检查 MiniMax 业务层错误
|
||||
JsonNode baseResp = responseJson.path("base_resp");
|
||||
if (baseResp != null && !baseResp.isMissingNode()) {
|
||||
int statusCode = baseResp.path("status_code").asInt(0);
|
||||
if (statusCode != 0) {
|
||||
String statusMsg = baseResp.path("status_msg").asText("未知错误");
|
||||
logger.error("MiniMax业务错误: code={}, msg={}", statusCode, statusMsg);
|
||||
return new AiChatResponse("MiniMax错误: " + statusMsg);
|
||||
}
|
||||
}
|
||||
|
||||
String content = responseJson
|
||||
.path("choices")
|
||||
.path(0)
|
||||
@@ -95,6 +113,11 @@ public class MiniMaxService implements AiService {
|
||||
.path("content")
|
||||
.asText("");
|
||||
|
||||
if (content.isEmpty()) {
|
||||
logger.warn("MiniMax返回内容为空,完整响应: {}", response.getBody());
|
||||
return new AiChatResponse("MiniMax返回了空内容,请检查模型配置或稍后重试。");
|
||||
}
|
||||
|
||||
return new AiChatResponse(content);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -102,4 +125,57 @@ public class MiniMaxService implements AiService {
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) {
|
||||
break;
|
||||
}
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
// 检查业务错误
|
||||
JsonNode baseResp = chunk.path("base_resp");
|
||||
if (!baseResp.isMissingNode() && baseResp.path("status_code").asInt(0) != 0) {
|
||||
String errMsg = baseResp.path("status_msg").asText("未知错误");
|
||||
logger.error("MiniMax流式业务错误: {}", errMsg);
|
||||
onToken.accept("\n\nMiniMax错误: " + errMsg);
|
||||
break;
|
||||
}
|
||||
String content = chunk.path("choices").path(0)
|
||||
.path("delta").path("content").asText(null);
|
||||
if (content != null) {
|
||||
onToken.accept(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("MiniMax流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@ import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* OpenAI兼容接口的AI服务实现(支持OpenAI、Azure OpenAI、本地大模型等)
|
||||
@@ -31,47 +38,43 @@ public class OpenAiService implements AiService {
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public OpenAiService(String apiUrl, String apiKey, String model) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.apiUrl = apiUrl != null && !apiUrl.isEmpty() ? apiUrl : "https://api.openai.com/v1/chat/completions";
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
this.model = model != null && !model.isEmpty() ? model : "gpt-4o";
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", false);
|
||||
|
||||
ArrayNode messages = requestBody.putArray("messages");
|
||||
|
||||
// 系统提示词
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt());
|
||||
|
||||
// 如果有当前代码上下文,加入 system 消息
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
|
||||
// 历史消息
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户消息
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
@@ -103,4 +106,49 @@ public class OpenAiService implements AiService {
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) {
|
||||
break;
|
||||
}
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
String content = chunk.path("choices").path(0)
|
||||
.path("delta").path("content").asText(null);
|
||||
if (content != null) {
|
||||
onToken.accept(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("OpenAI流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@ import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 智谱AI(GLM)服务实现
|
||||
@@ -37,40 +44,37 @@ public class ZhipuAiService implements AiService {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
|
||||
ArrayNode messages = requestBody.putArray("messages");
|
||||
|
||||
// 系统提示词
|
||||
ObjectNode systemMsg = messages.addObject();
|
||||
systemMsg.put("role", "system");
|
||||
systemMsg.put("content", buildSystemPrompt());
|
||||
|
||||
// 代码上下文
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
ObjectNode codeContextMsg = messages.addObject();
|
||||
codeContextMsg.put("role", "system");
|
||||
codeContextMsg.put("content", "当前编辑器中的代码:\n```magicscript\n" + request.getCurrentCode() + "\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
|
||||
// 历史消息
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", msg.getRole());
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户消息
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
requestBody.put("stream", false);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
@@ -102,4 +106,49 @@ public class ZhipuAiService implements AiService {
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) {
|
||||
break;
|
||||
}
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
String content = chunk.path("choices").path(0)
|
||||
.path("delta").path("content").asText(null);
|
||||
if (content != null) {
|
||||
onToken.accept(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("智谱AI流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,48 @@
|
||||
package org.ssssssss.magicapi.ai.web;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
import org.ssssssss.magicapi.ai.service.AiService;
|
||||
import org.ssssssss.magicapi.ai.service.AiServiceManager;
|
||||
import org.ssssssss.magicapi.core.annotation.Valid;
|
||||
import org.ssssssss.magicapi.core.config.MagicConfiguration;
|
||||
import org.ssssssss.magicapi.core.model.JsonBean;
|
||||
import org.ssssssss.magicapi.core.web.MagicController;
|
||||
import org.ssssssss.magicapi.core.web.MagicExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* AI Coding 控制器
|
||||
*/
|
||||
public class MagicAiController extends MagicController implements MagicExceptionHandler {
|
||||
|
||||
private final AiService aiService;
|
||||
private static final Logger logger = LoggerFactory.getLogger(MagicAiController.class);
|
||||
|
||||
public MagicAiController(MagicConfiguration configuration, AiService aiService) {
|
||||
private final AiServiceManager aiServiceManager;
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public MagicAiController(MagicConfiguration configuration, AiServiceManager aiServiceManager) {
|
||||
super(configuration);
|
||||
this.aiService = aiService;
|
||||
this.aiServiceManager = aiServiceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI对话接口
|
||||
* AI对话接口(同步),provider和model由前端每次请求携带
|
||||
*/
|
||||
@PostMapping("/ai/chat")
|
||||
@ResponseBody
|
||||
@@ -34,7 +51,53 @@ public class MagicAiController extends MagicController implements MagicException
|
||||
if (request.getMessage() == null || request.getMessage().trim().isEmpty()) {
|
||||
return new JsonBean<>(400, "消息内容不能为空");
|
||||
}
|
||||
AiChatResponse response = aiService.chat(request);
|
||||
AiChatResponse response = aiServiceManager.getService(request.getProvider(), request.getModel()).chat(request);
|
||||
return new JsonBean<>(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI对话接口(流式SSE),逐块推送AI回复
|
||||
*/
|
||||
@PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@ResponseBody
|
||||
@Valid
|
||||
public SseEmitter chatStream(@RequestBody AiChatRequest request) {
|
||||
SseEmitter emitter = new SseEmitter(120_000L);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
AiService service = aiServiceManager.getService(request.getProvider(), request.getModel());
|
||||
service.chatStream(request, token -> {
|
||||
try {
|
||||
// 手动 JSON 编码,确保换行符等被转义为 \n
|
||||
// StringHttpMessageConverter 优先级高于 Jackson,直接写已编码的字符串
|
||||
String jsonToken = objectMapper.writeValueAsString(token);
|
||||
emitter.send(SseEmitter.event().data(jsonToken));
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
emitter.send(SseEmitter.event().name("done").data("[DONE]"));
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
logger.error("流式AI服务调用异常", e);
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
|
||||
emitter.complete();
|
||||
} catch (IOException ignored) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已配置的提供商列表(含模型列表),供前端选择器使用
|
||||
*/
|
||||
@PostMapping("/ai/config/get")
|
||||
@ResponseBody
|
||||
@Valid
|
||||
public JsonBean<List<Map<String, Object>>> getProviders() {
|
||||
return new JsonBean<>(aiServiceManager.getConfiguredProviders());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user