提交 dfc99e11 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 新增 AI 图片和 AI 视频

上级 d32b7616
...@@ -22,3 +22,5 @@ dist-ssr ...@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*.agents
*.claude
\ No newline at end of file
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@chuangkit/chuangkit-design": "^2.0.8", "@chuangkit/chuangkit-design": "^2.0.8",
"@ezijing/ai-react": "^1.0.37", "@ezijing/ai-react": "^1.0.40",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@reduxjs/toolkit": "^1.9.7", "@reduxjs/toolkit": "^1.9.7",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
...@@ -698,9 +698,9 @@ ...@@ -698,9 +698,9 @@
} }
}, },
"node_modules/@ezijing/ai-core": { "node_modules/@ezijing/ai-core": {
"version": "1.0.37", "version": "1.0.40",
"resolved": "https://registry.npmjs.org/@ezijing/ai-core/-/ai-core-1.0.37.tgz", "resolved": "https://registry.npmjs.org/@ezijing/ai-core/-/ai-core-1.0.40.tgz",
"integrity": "sha512-LyT97TuYIZmgnRcHChJ06PND4d282hwSH5yCr/3DXPjcZPitogirvfC+I/5/dYsj2LpwpmEK65ips59KGtXt8A==", "integrity": "sha512-IusFlzXsrpt/NDxUsXqOt3TNzKnlssgQs5xpx+KIc/UUzcKD32rt6bAEVTvkLr9NAUQF1PcRmqxKhicrVyMirw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.11.0", "axios": "^1.11.0",
...@@ -708,12 +708,12 @@ ...@@ -708,12 +708,12 @@
} }
}, },
"node_modules/@ezijing/ai-react": { "node_modules/@ezijing/ai-react": {
"version": "1.0.37", "version": "1.0.40",
"resolved": "https://registry.npmjs.org/@ezijing/ai-react/-/ai-react-1.0.37.tgz", "resolved": "https://registry.npmjs.org/@ezijing/ai-react/-/ai-react-1.0.40.tgz",
"integrity": "sha512-u1Zywn2U2GHQMVTOqiTBcgib/dqqll41lmqsPAGL1CsRGH1FqfUac8IfZkmcFk9eDFhojf6F7QpSHd9/kNHMsg==", "integrity": "sha512-IxpsKFG+rAdIx/0w1Wjx4tRKPgiZ7//7QS+vzvC4YIVfC5cR4I95fWavzGBujciJB39BoDfcq9JxLLuRbDNXPg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ezijing/ai-core": "1.0.37" "@ezijing/ai-core": "1.0.40"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8.0" "react": ">=16.8.0"
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@chuangkit/chuangkit-design": "^2.0.8", "@chuangkit/chuangkit-design": "^2.0.8",
"@ezijing/ai-react": "^1.0.37", "@ezijing/ai-react": "^1.0.40",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@reduxjs/toolkit": "^1.9.7", "@reduxjs/toolkit": "^1.9.7",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
......
{
"version": 1,
"skills": {
"vercel-react-best-practices": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"computedHash": "3462ec83f862abb2d532953df33a4dbf87f4616849da5d4b5cc7c13601aaf997"
}
}
}
...@@ -57,6 +57,7 @@ import AITranslate from './menu/AITranslate' ...@@ -57,6 +57,7 @@ import AITranslate from './menu/AITranslate'
import AISearch from './menu/AISearch' import AISearch from './menu/AISearch'
import AIWrite from './menu/AIWrite' import AIWrite from './menu/AIWrite'
import AIImage from './menu/AIImage' import AIImage from './menu/AIImage'
import AIVideo from './menu/AIVideo'
import AIBaiduSearch from './menu/AIBaiduSearch' import AIBaiduSearch from './menu/AIBaiduSearch'
import ImageModal from './components/image' import ImageModal from './components/image'
...@@ -128,6 +129,7 @@ const module = { ...@@ -128,6 +129,7 @@ const module = {
AISearch, AISearch,
AIWrite, AIWrite,
AIImage, AIImage,
AIVideo,
AIBaiduSearch, AIBaiduSearch,
], ],
} }
...@@ -347,12 +349,13 @@ const WangEditorCustomer = (props, ref) => { ...@@ -347,12 +349,13 @@ const WangEditorCustomer = (props, ref) => {
'justifyJustify', 'justifyJustify',
'divider', 'divider',
'|', '|',
// 'AIImage',
'ImageAuto', 'ImageAuto',
'AIImage',
// 'ImageAutoOnline', // 'ImageAutoOnline',
'GalleryAuto', 'GalleryAuto',
// 'GalleryAutoOnline', // 'GalleryAutoOnline',
'VideoAuto', 'VideoAuto',
'AIVideo',
'AudioAuto', 'AudioAuto',
'insertTable', 'insertTable',
'|', '|',
......
...@@ -6,7 +6,7 @@ class AIImage extends BaseModalMenu { ...@@ -6,7 +6,7 @@ class AIImage extends BaseModalMenu {
super() super()
this.title = 'AI图片' this.title = 'AI图片'
this.iconSvg = `<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pen"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>` this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 8.12811L20.4668 8.69379C20.2864 9.10792 19.7136 9.10792 19.5331 8.69379L19.2866 8.12811C18.8471 7.11947 18.0555 6.31641 17.0677 5.87708L16.308 5.53922C15.8973 5.35653 15.8973 4.75881 16.308 4.57612L17.0252 4.25714C18.0384 3.80651 18.8442 2.97373 19.2761 1.93083L19.5293 1.31953C19.7058 0.893489 20.2942 0.893489 20.4706 1.31953L20.7238 1.93083C21.1558 2.97373 21.9616 3.80651 22.9748 4.25714L23.6919 4.57612C24.1027 4.75881 24.1027 5.35653 23.6919 5.53922L22.9323 5.87708C21.9445 6.31641 21.1529 7.11947 20.7134 8.12811ZM2.9918 3H14V5H4V19L14 9L20 15V11H22V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"></path></svg>`
} }
getValue(editor) { getValue(editor) {
return <AIImageModal key={Date.now()} editor={editor}></AIImageModal> return <AIImageModal key={Date.now()} editor={editor}></AIImageModal>
......
import BaseModalMenu from './common/BaseModalMenu'
import AIVideoModal from './common/AIVideoModal'
class AIVideo extends BaseModalMenu {
constructor() {
super()
this.title = 'AI视频'
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.7134 8.12811L19.4668 8.69379C19.2864 9.10792 18.7136 9.10792 18.5331 8.69379L18.2866 8.12811C17.8471 7.11947 17.0555 6.31641 16.0677 5.87708L15.308 5.53922C14.8973 5.35653 14.8973 4.75881 15.308 4.57612L16.0252 4.25714C17.0384 3.80651 17.8442 2.97373 18.2761 1.93083L18.5293 1.31953C18.7058 0.893489 19.2942 0.893489 19.4706 1.31953L19.7238 1.93083C20.1558 2.97373 20.9616 3.80651 21.9748 4.25714L22.6919 4.57612C23.1027 4.75881 23.1027 5.35653 22.6919 5.53922L21.9323 5.87708C20.9445 6.31641 20.1529 7.11947 19.7134 8.12811ZM3.9934 3H13V5H5V19H19V11H21V20.0066C21 20.5552 20.5551 21 20.0066 21H3.9934C3.44476 21 3 20.5551 3 20.0066V3.9934C3 3.44476 3.44495 3 3.9934 3ZM10.6219 8.41459L15.5008 11.6672C15.6846 11.7897 15.7343 12.0381 15.6117 12.2219C15.5824 12.2658 15.5447 12.3035 15.5008 12.3328L10.6219 15.5854C10.4381 15.708 10.1897 15.6583 10.0672 15.4745C10.0234 15.4088 10 15.3316 10 15.2526V8.74741C10 8.52649 10.1791 8.34741 10.4 8.34741C10.479 8.34741 10.5562 8.37078 10.6219 8.41459Z"></path></svg>`
}
getValue(editor) {
return <AIVideoModal key={Date.now()} editor={editor}></AIVideoModal>
}
}
export default {
key: 'AIVideo',
factory() {
return new AIVideo()
},
}
// AI图片生成器样式
.ai-image-generator {
.options-container {
margin-top: 16px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.option-group {
display: flex;
flex-direction: column;
gap: 8px;
.option-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
}
.results-container {
margin-top: 24px;
.results-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.loading-container {
min-height: 200px;
}
.empty-results {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px dashed #ddd;
}
}
.generated-image-card {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #1890ff;
}
.image-container {
width: 100%;
height: 200px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.image-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px;
button {
flex: 1;
}
}
}
.error-message {
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 6px;
padding: 12px;
margin-top: 16px;
color: #ff4d4f;
}
}
import { useRef, useState } from 'react'
import { SendOutlined, DownloadOutlined, CheckOutlined, VideoCameraOutlined } from '@ant-design/icons'
import { saveAs } from 'file-saver'
import { ConfigProvider, Modal, Input, Button, message, Row, Col, Card, Spin, Radio, InputNumber } from 'antd'
import { SlateTransforms, SlateEditor, SlateElement } from '@wangeditor/editor'
import { useVideo } from '@ezijing/ai-react'
import { uploadFileByUrl } from '@/utils/oss'
import './AISearchModal.less'
import './AIVideoModal.less'
const { TextArea } = Input
export default function AIVideoModal(props) {
const { editor } = props
const [isModalOpen, setIsModalOpen] = useState(true)
const [prompt, setPrompt] = useState('')
const [ratio, setRatio] = useState('16:9')
const [duration, setDuration] = useState(5)
const [generatedVideos, setGeneratedVideos] = useState([])
const lastGenerateParamsRef = useRef({ prompt: '', ratio: '16:9', duration: 5 })
const ratioOptions = [
{ label: '16:9', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1', value: '1:1' },
{ label: '3:4', value: '3:4' },
{ label: '9:16', value: '9:16' },
{ label: '21:9', value: '21:9' },
]
const { isLoading, error, generateVideo } = useVideo({
provider: 'volcano',
onSuccess: (response) => {
console.log('视频生成成功:', response)
if (response.url) {
const { prompt: usedPrompt, ratio: usedRatio, duration: usedDuration } = lastGenerateParamsRef.current
setGeneratedVideos((prev) => {
const nextVideos = [
{
id: response.id || response.url,
url: response.url,
prompt: usedPrompt,
ratio: usedRatio,
duration: usedDuration,
},
...prev,
]
return nextVideos.slice(0, 8)
})
message.success('视频生成成功!')
}
},
onError: (videoError) => {
console.error('视频生成失败:', videoError)
message.error(`视频生成失败: ${videoError}`)
},
})
const handleEnterGenerate = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (!isLoading) {
handleGenerateVideo()
}
}
}
const handleGenerateVideo = async () => {
if (!prompt.trim()) {
message.error('请输入视频描述!')
return
}
const normalizedDuration = Math.min(12, Math.max(2, Math.round(Number(duration) || 5)))
const normalizedPrompt = prompt.trim()
setDuration(normalizedDuration)
lastGenerateParamsRef.current = {
prompt: normalizedPrompt,
ratio,
duration: normalizedDuration,
}
try {
await generateVideo({
prompt: normalizedPrompt,
ratio,
duration: normalizedDuration,
})
} catch (videoError) {
console.error('生成视频失败:', videoError)
message.error('生成视频失败,请稍后重试')
}
}
// 移除空段落
const removeEmptyParagraph = (editor) => {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
return true
}
}
return false
},
universal: true,
})
for (let nodeEntry of nodeEntries) {
const [node] = nodeEntry
if (node.children && node.children.length === 1 && node.children[0].text === '') {
SlateTransforms.removeNodes(editor)
}
}
}
// 上传并插入视频 (标准视频)
const handleInsertVideo = async (videoUrl, videoPrompt) => {
try {
message.loading('正在上传视频...', 0)
const ossUrl = await uploadFileByUrl(videoUrl)
message.destroy()
if (editor) {
editor.restoreSelection()
removeEmptyParagraph(editor)
const nodes = [
{
type: 'video',
src: ossUrl,
children: [{ text: '' }],
},
{
type: 'paragraph',
textAlign: 'center',
fontSize: '14px',
children: [{ text: videoPrompt || 'AI生成的视频' }],
},
]
SlateTransforms.insertNodes(editor, nodes)
message.success('视频已插入编辑器!')
setIsModalOpen(false)
} else {
message.error('编辑器未找到')
}
} catch (uploadError) {
message.destroy()
console.error('上传视频失败:', uploadError)
message.error('上传视频失败,请稍后重试')
}
}
const handleDownloadVideo = (videoUrl) => {
saveAs(videoUrl, `ai_video_${Date.now()}.mp4`)
}
const GeneratedVideoCard = ({ videoItem, index }) => (
<Card
hoverable
className="generated-video-card"
cover={
<div className="video-container">
<video src={videoItem.url} controls preload="metadata" />
</div>
}
size="small">
<div className="video-meta">{videoItem.prompt || `AI生成视频 ${index + 1}`}</div>
<div className="video-meta">
比例: {videoItem.ratio || '16:9'} | 时长: {videoItem.duration || 5}s
</div>
<div className="video-actions">
<Button size="small" icon={<DownloadOutlined />} onClick={() => handleDownloadVideo(videoItem.url)}>
下载
</Button>
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
onClick={() => handleInsertVideo(videoItem.url, videoItem.prompt)}>
插入
</Button>
</div>
</Card>
)
return (
<ConfigProvider theme={{ components: { Modal: { headerBg: '#f7f8fa', contentBg: '#f7f8fa' } } }}>
<Modal title="AI视频生成" open={isModalOpen} footer={null} onCancel={() => setIsModalOpen(false)} width={1200}>
<div className="ai-video-generator">
<div className="input-container">
<div className="input-box">
<div className="edit-area">
<TextArea
className="content"
autoSize={{ minRows: 3, maxRows: 6 }}
value={prompt}
placeholder="描述你想要生成的视频,例如:海边日落时海浪轻拍沙滩,镜头缓慢推进,氛围安静治愈"
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleEnterGenerate}
/>
</div>
<div className="input-tools">
<Button type="primary" icon={<SendOutlined />} onClick={handleGenerateVideo} loading={isLoading}>
生成视频
</Button>
</div>
</div>
<div className="options-container" style={{ marginTop: 16 }}>
<Row gutter={[16, 16]}>
<Col span={16}>
<div className="option-group">
<div className="option-label">视频比例:</div>
<Radio.Group
options={ratioOptions}
onChange={(e) => setRatio(e.target.value)}
value={ratio}
optionType="button"
buttonStyle="solid"
/>
</div>
</Col>
<Col span={8}>
<div className="option-group">
<div className="option-label">视频时长(2-12秒):</div>
<InputNumber
min={2}
max={12}
step={1}
precision={0}
value={duration}
onChange={(value) => setDuration(Number.isFinite(value) ? Math.round(value) : 5)}
style={{ width: '100%' }}
/>
</div>
</Col>
</Row>
</div>
</div>
{error && (
<div className="error-message" style={{ marginTop: 16, color: '#ff4d4f' }}>
错误: {error}
</div>
)}
<div className="results-container" style={{ marginTop: 24 }}>
<div className="results-title">生成结果</div>
{isLoading ? (
<div className="loading-container">
<Spin tip="AI正在生成视频,请稍候..." size="large">
<div style={{ height: '220px' }} />
</Spin>
</div>
) : generatedVideos.length > 0 ? (
<Row gutter={[16, 16]}>
{generatedVideos.map((videoItem, index) => (
<Col span={8} key={videoItem.id || `${videoItem.url}-${index}`}>
<GeneratedVideoCard videoItem={videoItem} index={index} />
</Col>
))}
</Row>
) : (
<div className="empty-results">
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
<VideoCameraOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<div>输入描述并点击生成按钮,AI将为你创作视频</div>
</div>
</div>
)}
</div>
</div>
</Modal>
</ConfigProvider>
)
}
.ai-video-generator {
.options-container {
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.option-group {
display: flex;
flex-direction: column;
gap: 8px;
.option-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
}
.results-container {
margin-top: 24px;
.results-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.loading-container {
min-height: 220px;
.ant-spin {
width: 100%;
}
.ant-spin-text {
white-space: nowrap;
}
}
.empty-results {
display: flex;
justify-content: center;
align-items: center;
min-height: 220px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px dashed #ddd;
}
}
.generated-video-card {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #1890ff;
}
.video-container {
width: 100%;
height: 220px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #111;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.video-meta {
padding: 12px 12px 0;
color: #666;
font-size: 12px;
min-height: 36px;
line-height: 18px;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.video-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px;
button {
flex: 1;
}
}
}
.error-message {
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 6px;
padding: 12px;
margin-top: 16px;
color: #ff4d4f;
}
}
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import md5 from 'js-md5'
import axios from 'axios' import axios from 'axios'
import { fetchEventSource } from '@fortaine/fetch-event-source'
export function useAI() { export function useAI() {
const options = [ const options = [
{ label: '文心一言', value: 'yiyan' }, { label: '文心一言', value: 'yiyan' },
{ label: 'DeepSeek', value: 'deepseek' }, { label: 'DeepSeek', value: 'deepseek' },
{ label: '通义千问', value: 'qwen' }, { label: '通义千问', value: 'qwen' },
// { label: '天工', value: 'tiangong' },
] ]
const [ai, setAI] = useState(localStorage.getItem('ai') || 'yiyan') const [ai, setAI] = useState(localStorage.getItem('ai') || 'yiyan')
...@@ -34,9 +31,6 @@ export function useAI() { ...@@ -34,9 +31,6 @@ export function useAI() {
case 'qwen': case 'qwen':
await qwen(data) await qwen(data)
break break
case 'tiangong':
await tiangong(data)
break
default: default:
throw new Error('未找到对应的 AI 配置') throw new Error('未找到对应的 AI 配置')
} }
...@@ -46,7 +40,7 @@ export function useAI() { ...@@ -46,7 +40,7 @@ export function useAI() {
setIsLoading(false) setIsLoading(false)
} }
}, },
[ai] // 依赖 `ai`,当 `ai` 变化时,重新创建 `post` [ai],
) )
async function yiyan(data) { async function yiyan(data) {
...@@ -54,7 +48,7 @@ export function useAI() { ...@@ -54,7 +48,7 @@ export function useAI() {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU' const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy' const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
const resp = await axios.post( const resp = await axios.post(
`/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}` `/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`,
) )
return resp.data.access_token return resp.data.access_token
} }
...@@ -62,7 +56,7 @@ export function useAI() { ...@@ -62,7 +56,7 @@ export function useAI() {
`/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${await getAccessToken()}`, `/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${await getAccessToken()}`,
{ {
messages: [{ role: 'user', content: data.content }], messages: [{ role: 'user', content: data.content }],
} },
) )
setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: resp.data.result }]) setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: resp.data.result }])
} }
...@@ -77,7 +71,7 @@ export function useAI() { ...@@ -77,7 +71,7 @@ export function useAI() {
}, },
{ {
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
} },
) )
if (resp.data) { if (resp.data) {
const [choice = {}] = resp.data.choices const [choice = {}] = resp.data.choices
...@@ -95,7 +89,7 @@ export function useAI() { ...@@ -95,7 +89,7 @@ export function useAI() {
}, },
{ {
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
} },
) )
if (resp.data) { if (resp.data) {
const [choice = {}] = resp.data.choices const [choice = {}] = resp.data.choices
...@@ -103,49 +97,9 @@ export function useAI() { ...@@ -103,49 +97,9 @@ export function useAI() {
} }
} }
async function tiangong(data) {
const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000)
const sign = md5(`${appKey}${appSecret}${timestamp}`)
return await fetchEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', app_key: appKey, sign, timestamp, stream: 'true' },
body: JSON.stringify({
chat_history: [{ role: 'user', content: data.content }],
stream_resp_type: 'update',
}),
async onopen(response) {
console.log(response)
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
const message = JSON.parse(res.data)
if (message.type !== 1) return
setMessages((prevMessages) => {
const messageId = message.conversation_id
const messageIndex = prevMessages.findIndex((message) => message.id === messageId)
const content = message?.arguments?.[0]?.messages?.[0]?.text || ''
if (messageIndex === -1) {
return [...prevMessages, { id: messageId, role: 'assistant', content }]
} else {
return prevMessages.map((msg) => (msg.id === messageId ? { ...msg, content } : msg))
}
})
setIsLoading(false)
},
onerror(err) {
setIsLoading(false)
throw err
},
})
}
return { ai, setAI, options, post, messages, isLoading } return { ai, setAI, options, post, messages, isLoading }
} }
export function useGenerateImage() {}
export function useGenerateVideo() {}
import { useEffect, useState, useCallback } from 'react'
import { fetchEventSource } from '@fortaine/fetch-event-source'
// 获取本地存储中的令牌
function getToken() {
return window.localStorage.getItem('kiwi.token') || ''
}
// 生成请求头部信息
const getHeaders = () => {
const token = getToken()
const appId = 'TzEU5jPk2tu80266'
const appSecret = '0a006048a4480481b18fef1405120b83'
const timestamp = Math.floor(Date.now() / 1000)
return {
Authorization: token,
AppId: appId,
AppSecret: appSecret,
Timestamp: timestamp,
'Content-Type': 'application/json'
}
}
// 默认的响应打开处理函数
const defaultOnOpen = async response => {
if (!response.ok) {
throw response
}
return response
}
// 默认的消息处理函数
const defaultOnMessage = setMessages => res => {
const message = JSON.parse(res.data)
setMessages(prevMessages => [...prevMessages, message])
}
// 自定义 Hook,用于使用 Fetch 事件源
export function useFetchEventSource(url, options = {}) {
const [isLoading, setLoading] = useState(false)
const [messages, setMessages] = useState([])
const [error, setError] = useState(null)
// 定义 fetch 函数
const fetch = useCallback(() => {
setLoading(true)
const headers = getHeaders()
const defaultOptions = {
method: 'POST',
headers,
onopen: defaultOnOpen,
onmessage: defaultOnMessage(setMessages),
onerror: err => setError(err)
}
fetchEventSource(url, { ...defaultOptions, ...options })
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url, options])
// 在组件挂载时执行 fetch 函数
useEffect(() => {
fetch()
}, [fetch])
return { isLoading, error, messages, fetch }
}
import md5 from 'js-md5'
import { fetchEventSource } from '@fortaine/fetch-event-source'
import { useState, useCallback } from 'react'
export function useAIChat() {
const authKey = 'f3846153ba784b6d86bdcd5533259c88'
const authSecret = 'HO4IyLEwEOHpeOXBxaLQUOqWslJRGs1M'
const [messages, setMessages] = useState([])
const [chatId, setChatId] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const addMessage = useCallback((message) => {
setMessages((prevMessages) => [...prevMessages, message])
}, [])
const updateMessages = useCallback((newMessage) => {
setMessages((prevMessages) => {
const existingMessage = prevMessages.find((msg) => msg.conversationId === newMessage.conversationId)
const content = newMessage.content === '\n' ? '<br/>' : newMessage.content || ''
if (existingMessage) {
// 更新现有消息
return prevMessages.map((msg) =>
msg.conversationId === newMessage.conversationId ? { ...msg, content: msg.content + content } : msg
)
} else {
// 新增消息
return [...prevMessages, { ...newMessage, content, role_type: 'ai', time: Date.now() }]
}
})
}, [])
const post = useCallback(
async (data) => {
const timestamp = Date.now()
const sign = md5(`${authKey}${authSecret}${timestamp}`)
// 插入用户消息
addMessage({
role_type: 'user',
content: data.content,
conversationId: `user-${timestamp}`, // 生成一个唯一的 conversationId
time: Date.now(),
})
setIsLoading(true)
try {
await fetchEventSource('/api/tiangong/openapi/agent/chat/stream/v1', {
method: 'POST',
headers: {
authKey,
timestamp,
sign,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...data, chatId, agentId: authKey }),
onopen(response) {
if (!response.ok) {
throw new Error('Network response was not ok')
}
},
onmessage(event) {
const message = JSON.parse(event.data)
setChatId(message.chatId)
updateMessages(message)
},
onerror(error) {
console.error('Fetch error:', error)
},
onclose() {
setIsLoading(false)
},
})
} catch (error) {
console.error('Post error:', error)
setIsLoading(false)
}
},
[authKey, authSecret, chatId, addMessage, updateMessages]
)
return { messages, isLoading, post }
}
...@@ -12,7 +12,7 @@ async function getOSSConfig() { ...@@ -12,7 +12,7 @@ async function getOSSConfig() {
...data, ...data,
accessKeyId: data.AccessKeyId, accessKeyId: data.AccessKeyId,
accessKeySecret: data.AccessKeySecret, accessKeySecret: data.AccessKeySecret,
stsToken: data.SecurityToken stsToken: data.SecurityToken,
} }
} catch (error) { } catch (error) {
handleError(error, 'Failed to retrieve STS token') handleError(error, 'Failed to retrieve STS token')
...@@ -32,7 +32,7 @@ async function createOSSInstance() { ...@@ -32,7 +32,7 @@ async function createOSSInstance() {
refreshSTSToken: async () => { refreshSTSToken: async () => {
return await getOSSConfig() return await getOSSConfig()
}, },
refreshSTSTokenInterval: 14 * 60 * 1000 refreshSTSTokenInterval: 14 * 60 * 1000,
}) })
return ossInstance return ossInstance
} catch (error) { } catch (error) {
...@@ -44,9 +44,30 @@ async function createOSSInstance() { ...@@ -44,9 +44,30 @@ async function createOSSInstance() {
export async function uploadFile(file) { export async function uploadFile(file) {
try { try {
const oss = await createOSSInstance() const oss = await createOSSInstance()
const fileExt = file.name?.substring(file.name.lastIndexOf('.')) || '.png' const fileName = typeof file.name === 'string' ? file.name : ''
const fileName = `${new Date().getTime()}-${Math.random() * 1000}${fileExt}` const lastDotIndex = fileName.lastIndexOf('.')
const result = await oss.put(fileName, file) const extFromName = lastDotIndex > -1 ? fileName.slice(lastDotIndex).toLowerCase() : ''
const mimeType = (file.type || '').toLowerCase()
const mimeExtMap = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
'image/svg+xml': '.svg',
'audio/mpeg': '.mp3',
'audio/wav': '.wav',
'audio/ogg': '.ogg',
'audio/mp4': '.m4a',
'video/mp4': '.mp4',
'video/webm': '.webm',
'video/ogg': '.ogv',
'video/quicktime': '.mov',
}
const fileExt = extFromName || mimeExtMap[mimeType] || '.bin'
const generatedFileName = `${new Date().getTime()}-${Math.random() * 1000}${fileExt}`
const result = await oss.put(generatedFileName, file)
return result.url return result.url
} catch (error) { } catch (error) {
handleError(error, 'Failed to upload file') handleError(error, 'Failed to upload file')
...@@ -56,6 +77,7 @@ export async function uploadFile(file) { ...@@ -56,6 +77,7 @@ export async function uploadFile(file) {
// 上传通过URL获取的文件 // 上传通过URL获取的文件
export async function uploadFileByUrl(url) { export async function uploadFileByUrl(url) {
try { try {
url = url.replace('https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com', '/api/ai_file')
const res = await axios.get(url, { responseType: 'blob' }) const res = await axios.get(url, { responseType: 'blob' })
return await uploadFile(res.data) return await uploadFile(res.data)
} catch (error) { } catch (error) {
......
...@@ -37,9 +37,29 @@ export default defineConfig(() => { ...@@ -37,9 +37,29 @@ export default defineConfig(() => {
// changeOrigin: true, // changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api\/wenku/, '/'), // rewrite: (path) => path.replace(/^\/api\/wenku/, '/'),
// }, // },
// '/api/qianfan': {
// target: 'https://qianfan.baidubce.com',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api\/qianfan/, ''),
// },
'/api/volcano': {
target: 'https://ark.cn-beijing.volces.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/volcano/, ''),
},
'/api/xiling': {
target: 'https://open.xiling.baidu.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/xiling/, ''),
},
'/api/ai_file': {
target: 'https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/ai_file/, ''),
},
'/api': { '/api': {
// target: 'https://zijingebook.ezijing.com', target: 'https://zijingebook.ezijing.com',
target: 'http://localhost:7419', // target: 'http://localhost:7419',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论