diff --git a/web/package.json b/web/package.json
index 0dc19dac..c145d173 100644
--- a/web/package.json
+++ b/web/package.json
@@ -8,7 +8,8 @@
"lint": "vue-cli-service lint",
"buildLibrary": "node ./scripts/updateVersion.js && vue-cli-service build --mode library --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js",
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
- "createNodeImageList": "node ./scripts/createNodeImageList.js"
+ "createNodeImageList": "node ./scripts/createNodeImageList.js",
+ "ai:serve": "node ./scripts/ai.js"
},
"dependencies": {
"@toast-ui/editor": "^3.1.5",
diff --git a/web/scripts/ai.js b/web/scripts/ai.js
index 6c257f06..67eb6f7e 100644
--- a/web/scripts/ai.js
+++ b/web/scripts/ai.js
@@ -1,8 +1,8 @@
const express = require('express')
const http = require('http')
+const { pipeline } = require('stream')
-const port = 3000
-const baseUrl = 'http://ark.cn-beijing.volces.com'
+const port = 3456
// 起个服务
const app = express()
@@ -19,63 +19,90 @@ app.use((req, res, next) => {
// 监听对话请求
app.get('/ai/test', (req, res) => {
- res.send('/ai/test').end()
+ res
+ .json({
+ code: 0,
+ data: null,
+ msg: '连接成功'
+ })
+ .end()
})
app.post('/ai/chat', (req, res) => {
- try {
- const { a: apiKey, b: model, messages } = req.body
+ // 设置SSE响应头
+ res.setHeader('Content-Type', 'text/event-stream')
+ res.setHeader('Cache-Control', 'no-cache')
+ res.setHeader('Connection', 'keep-alive')
- // res.append('Content-Type', 'text/event-stream')
- // res.append('Cache-Control', 'no-cache')
- // res.append('Connection', 'keep-alive')
- res.send(1).end(200)
- } catch (error) {
- console.log(error)
+ const { api, method, headers, data } = req.body
+
+ // 创建代理请求
+ const proxyReq = http.request(
+ api,
+ {
+ method: method || 'POST',
+ headers: {
+ ...headers
+ }
+ },
+ proxyRes => {
+ // 检查目标服务响应状态
+ if (proxyRes.statusCode !== 200) {
+ proxyRes.resume()
+ return res.status(proxyRes.statusCode).end()
+ }
+
+ // 使用双向流管道
+ const pipelinePromise = new Promise(resolve => {
+ pipeline(proxyRes, res, err => {
+ // 过滤客户端主动断开的情况
+ if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
+ console.error('Pipeline error:', err)
+ }
+ resolve()
+ })
+ })
+
+ // 处理流结束
+ proxyRes.on('end', () => {
+ if (!res.writableEnded) {
+ res.end()
+ }
+ })
+
+ return pipelinePromise
+ }
+ )
+
+ // 错误处理增强
+ const handleError = err => {
+ if (!res.headersSent) {
+ res.status(502).end('Bad Gateway')
+ }
+ cleanupStreams()
}
- // 模拟发送数据
- // const intervalId = setInterval(() => {
- // res.send(`data: ${new Date().toISOString()}\n\n`)
- // }, 1000)
+ // 流清理函数
+ const cleanupStreams = () => {
+ proxyReq.destroy()
+ res.destroy()
+ }
- // // 监听客户端断开连接
- // req.on('close', () => {
- // console.log('Client disconnected.')
- // clearInterval(intervalId)
- // res.end()
- // })
+ // 事件监听器
+ proxyReq.on('error', handleError)
+ res.on('error', handleError)
- // const aiReq = http.request(
- // baseUrl + '/api/v3/chat/completions',
- // {
- // method: 'POST',
- // headers: {
- // Authorization: 'Bearer ' + apiKey
- // }
- // },
- // aiRes => {
- // aiRes.on('data', chunk => {
- // console.log(`BODY: ${chunk}`)
- // res.send(chunk)
- // })
- // aiRes.on('end', () => {
- // console.log('No more data in response.')
- // res.end()
- // })
- // }
- // )
- // const postData = {
- // model,
- // messages,
- // stream: true
- // }
- // aiReq.write(JSON.stringify(postData))
- // aiReq.end()
+ // 处理客户端提前断开
+ req.on('close', () => {
+ if (!res.writableFinished) {
+ console.log('Client disconnected prematurely')
+ cleanupStreams()
+ }
+ })
+
+ proxyReq.write(JSON.stringify(data))
+ proxyReq.end()
})
-// res.writeHead(404)
-// res.end()
-
app.listen(port, () => {
console.log(`app listening on port ${port}`)
})
diff --git a/web/src/App.vue b/web/src/App.vue
index de7034d4..77019da9 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -7,99 +7,7 @@
diff --git a/web/src/assets/icon-font/iconfont.css b/web/src/assets/icon-font/iconfont.css
index 2e741b11..71be3af5 100644
--- a/web/src/assets/icon-font/iconfont.css
+++ b/web/src/assets/icon-font/iconfont.css
@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
- src: url('iconfont.woff2?t=1739152990179') format('woff2'),
- url('iconfont.woff?t=1739152990179') format('woff'),
- url('iconfont.ttf?t=1739152990179') format('truetype');
+ src: url('iconfont.woff2?t=1739843331607') format('woff2'),
+ url('iconfont.woff?t=1739843331607') format('woff'),
+ url('iconfont.ttf?t=1739843331607') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
+.iconAIshengcheng:before {
+ content: "\e6b5";
+}
+
.iconprinting:before {
content: "\ea28";
}
diff --git a/web/src/assets/icon-font/iconfont.ttf b/web/src/assets/icon-font/iconfont.ttf
index 06a6deea..f8154868 100644
Binary files a/web/src/assets/icon-font/iconfont.ttf and b/web/src/assets/icon-font/iconfont.ttf differ
diff --git a/web/src/assets/icon-font/iconfont.woff b/web/src/assets/icon-font/iconfont.woff
index b98983ec..c9ebb1e3 100644
Binary files a/web/src/assets/icon-font/iconfont.woff and b/web/src/assets/icon-font/iconfont.woff differ
diff --git a/web/src/assets/icon-font/iconfont.woff2 b/web/src/assets/icon-font/iconfont.woff2
index 09818b06..16588279 100644
Binary files a/web/src/assets/icon-font/iconfont.woff2 and b/web/src/assets/icon-font/iconfont.woff2 differ
diff --git a/web/src/config/en.js b/web/src/config/en.js
index dae1310f..1f3a3755 100644
--- a/web/src/config/en.js
+++ b/web/src/config/en.js
@@ -444,6 +444,11 @@ export const sidebarTriggerList = [
value: 'setting',
icon: 'iconshezhi'
},
+ {
+ name: 'AI',
+ value: 'ai',
+ icon: 'iconAIshengcheng'
+ },
{
name: 'ShortcutKey',
value: 'shortcutKey',
diff --git a/web/src/config/zh.js b/web/src/config/zh.js
index 75793ad9..3813a5e4 100644
--- a/web/src/config/zh.js
+++ b/web/src/config/zh.js
@@ -534,6 +534,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
+ {
+ name: 'AI',
+ value: 'ai',
+ icon: 'iconAIshengcheng'
+ },
{
name: '设置',
value: 'setting',
diff --git a/web/src/config/zhtw.js b/web/src/config/zhtw.js
index 56dff3d1..106441f7 100644
--- a/web/src/config/zhtw.js
+++ b/web/src/config/zhtw.js
@@ -439,6 +439,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
+ {
+ name: 'AI',
+ value: 'ai',
+ icon: 'iconAIshengcheng'
+ },
{
name: '設置',
value: 'setting',
diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js
index fbabd0cf..6d856a22 100644
--- a/web/src/lang/en_us.js
+++ b/web/src/lang/en_us.js
@@ -134,7 +134,8 @@ export default {
expandNodeChild: 'Expand all sub nodes',
unExpandNodeChild: 'Un expand all sub nodes',
addToDo: 'Add toDo',
- removeToDo: 'Remove toDo'
+ removeToDo: 'Remove toDo',
+ aiCreate: 'AI Continuation'
},
count: {
words: 'Words',
@@ -329,7 +330,8 @@ export default {
newFileTip:
'Please export the currently edited file before creating a new one, Beware of content loss',
openFileTip:
- 'Please export the currently edited file before opening it, Beware of content loss'
+ 'Please export the currently edited file before opening it, Beware of content loss',
+ ai: 'AI'
},
edit: {
newFeatureNoticeTitle: 'New feature reminder',
diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js
index d4b58234..9262cb13 100644
--- a/web/src/lang/zh_cn.js
+++ b/web/src/lang/zh_cn.js
@@ -133,7 +133,8 @@ export default {
expandNodeChild: '展开所有下级节点',
unExpandNodeChild: '收起所有下级节点',
addToDo: '添加待办',
- removeToDo: '删除待办'
+ removeToDo: '删除待办',
+ aiCreate: 'AI续写'
},
count: {
words: '字数',
@@ -323,7 +324,8 @@ export default {
creatingTip: '正在创建文件',
directory: '目录',
newFileTip: '新建文件前请先导出当前编辑的文件,谨防内容丢失',
- openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失'
+ openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失',
+ ai: 'AI'
},
edit: {
newFeatureNoticeTitle: '新特性提醒',
@@ -412,5 +414,8 @@ export default {
nodeTagStyle: {
placeholder: '请输入标签内容',
delete: '删除此标签'
+ },
+ ai: {
+ chatTitle: 'AI对话'
}
}
diff --git a/web/src/lang/zh_tw.js b/web/src/lang/zh_tw.js
index a22f76d8..2bb41eb2 100644
--- a/web/src/lang/zh_tw.js
+++ b/web/src/lang/zh_tw.js
@@ -134,7 +134,8 @@ export default {
expandNodeChild: '展開所有下級節點',
unExpandNodeChild: '收起所有下級節點',
addToDo: '添加待辦',
- removeToDo: '刪除待辦'
+ removeToDo: '刪除待辦',
+ aiCreate: 'AI續寫'
},
count: {
words: '字數',
@@ -323,7 +324,8 @@ export default {
creatingTip: '正在建立檔案',
directory: '目錄',
newFileTip: '新增檔案前,請先匯出目前編輯的檔案,以免內容遺失',
- openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失'
+ openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失',
+ ai: 'AI'
},
edit: {
newFeatureNoticeTitle: '新功能提醒',
diff --git a/web/src/main.js b/web/src/main.js
index 22261c40..cbdb93bd 100644
--- a/web/src/main.js
+++ b/web/src/main.js
@@ -35,4 +35,3 @@ if (window.takeOverApp) {
} else {
initApp()
}
-
diff --git a/web/src/pages/Edit/components/AiChat.vue b/web/src/pages/Edit/components/AiChat.vue
new file mode 100644
index 00000000..f1ad934d
--- /dev/null
+++ b/web/src/pages/Edit/components/AiChat.vue
@@ -0,0 +1,290 @@
+
+ 火山方舟大模型配置:
+ 目前仅支持火山方舟大模型,需要自行去获取key,详细操作步骤见:教程。
+ 思绪思维导图客户端配置: