Compare commits
7 Commits
2a5114a370
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 848e8a6271 | |||
|
|
3fc26d6996 | ||
|
|
ed04eea481 | ||
|
|
ef50aae9d0 | ||
|
|
c85f211c9c | ||
| c7a37e6816 | |||
|
|
3f2a4a506b |
@@ -3,11 +3,9 @@ import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
|||||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||||
import { MakerDMG } from '@electron-forge/maker-dmg';
|
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||||
import MakerWix from '@electron-forge/maker-wix';
|
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as esbuild from 'esbuild';
|
import * as esbuild from 'esbuild';
|
||||||
@@ -16,28 +14,13 @@ const config: ForgeConfig = {
|
|||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
asar: true,
|
asar: true,
|
||||||
tmpdir: path.resolve(process.cwd(), '..', 'electron-packager-tmp'),
|
tmpdir: path.resolve(process.cwd(), '..', 'electron-packager-tmp'),
|
||||||
name: 'NianXX',
|
|
||||||
icon: path.join(__dirname, 'public/logo'),
|
|
||||||
appCopyright: 'Copyright © 2026 智念科技',
|
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers: [
|
makers: [
|
||||||
new MakerSquirrel({
|
new MakerSquirrel({}),
|
||||||
iconUrl: path.join(__dirname, 'public/logo.ico'), // 快捷方式的图标,需要在线的地址
|
|
||||||
setupIcon: path.join(__dirname, 'public/logo.ico'),
|
|
||||||
setupExe: 'NianXX.exe',
|
|
||||||
// loadingGif: path.join(__dirname, 'public/loading.gif'), // 修改默认安装图片
|
|
||||||
}),
|
|
||||||
new MakerZIP({}, ['darwin']),
|
new MakerZIP({}, ['darwin']),
|
||||||
new MakerRpm({}),
|
new MakerRpm({}),
|
||||||
new MakerDeb({}),
|
new MakerDeb({}),
|
||||||
new MakerDMG({}),
|
|
||||||
new MakerWix({
|
|
||||||
language: 2052,
|
|
||||||
ui: {
|
|
||||||
chooseDirectory: true,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
651
package-lock.json
generated
651
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.3",
|
"@electron-forge/cli": "^7.8.3",
|
||||||
"@electron-forge/maker-deb": "^7.10.2",
|
"@electron-forge/maker-deb": "^7.10.2",
|
||||||
"@electron-forge/maker-dmg": "^7.11.1",
|
|
||||||
"@electron-forge/maker-rpm": "^7.10.2",
|
"@electron-forge/maker-rpm": "^7.10.2",
|
||||||
"@electron-forge/maker-squirrel": "^7.10.2",
|
"@electron-forge/maker-squirrel": "^7.10.2",
|
||||||
"@electron-forge/maker-wix": "^7.11.1",
|
|
||||||
"@electron-forge/maker-zip": "^7.10.2",
|
"@electron-forge/maker-zip": "^7.10.2",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.10.2",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.10.2",
|
||||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
"@electron-forge/plugin-fuses": "^7.10.2",
|
||||||
|
|||||||
BIN
public/logo.ico
BIN
public/logo.ico
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -51,7 +51,8 @@ export function runTaskOperationService() {
|
|||||||
['fzName', 'fg_trace.js'],
|
['fzName', 'fg_trace.js'],
|
||||||
['mtName', 'mt_trace.js'],
|
['mtName', 'mt_trace.js'],
|
||||||
['dyHotelName', 'dy_hotel_trace.js'],
|
['dyHotelName', 'dy_hotel_trace.js'],
|
||||||
['dyHotSpringName', 'dy_hot_spring_trace.js']
|
['dyHotSpringName', 'dy_hot_spring_trace.js'],
|
||||||
|
['xcName', 'xc_trace.js'],
|
||||||
]
|
]
|
||||||
const scriptEntries = pairs.filter(([prop]) => roomType?.[prop])
|
const scriptEntries = pairs.filter(([prop]) => roomType?.[prop])
|
||||||
|
|
||||||
@@ -74,14 +75,9 @@ export function runTaskOperationService() {
|
|||||||
dyHotelName: 'douyin',
|
dyHotelName: 'douyin',
|
||||||
dyHotSpringName: 'douyin',
|
dyHotSpringName: 'douyin',
|
||||||
}
|
}
|
||||||
const defaultTabIndexMap: Record<string, number> = {
|
|
||||||
fliggy: 0,
|
|
||||||
meituan: 1,
|
|
||||||
douyin: 2
|
|
||||||
}
|
|
||||||
const mappedName = channelNameMap[item.channel]
|
const mappedName = channelNameMap[item.channel]
|
||||||
const tabIndex = mappedName ? (openedTabIndexByChannelName.get(mappedName) ?? defaultTabIndexMap[mappedName] ?? i) : i
|
const tabIndex = mappedName ? (openedTabIndexByChannelName.get(mappedName) ?? i) : i
|
||||||
log.info(`Launching script for channel ${item.channel}: ${item.scriptPath} (tabIndex: ${tabIndex})`)
|
log.info(`Launching script for channel ${item.channel}: ${item.scriptPath}`)
|
||||||
const result = await executeScriptServiceInstance.executeScript(item.scriptPath, {
|
const result = await executeScriptServiceInstance.executeScript(item.scriptPath, {
|
||||||
roomType: roomType[item.channel],
|
roomType: roomType[item.channel],
|
||||||
startTime: options.startTime,
|
startTime: options.startTime,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const isSameOrigin = (currentUrl, targetUrl) => {
|
|||||||
const current = normalizeUrl(currentUrl);
|
const current = normalizeUrl(currentUrl);
|
||||||
const target = normalizeUrl(targetUrl);
|
const target = normalizeUrl(targetUrl);
|
||||||
if (!current || !target) return false;
|
if (!current || !target) return false;
|
||||||
|
|
||||||
return current.origin === target.origin;
|
return current.origin === target.origin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
BIN
src/main/scripts/dy.png
Normal file
BIN
src/main/scripts/dy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 245 KiB |
@@ -220,6 +220,5 @@ const navigateToRoomStatusManagement = async (page) => {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await safeDisconnectBrowser(browser);
|
await safeDisconnectBrowser(browser);
|
||||||
process.exit(process.exitCode || 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -220,6 +220,5 @@ const navigateToRoomStatusManagement = async (page) => {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await safeDisconnectBrowser(browser);
|
await safeDisconnectBrowser(browser);
|
||||||
process.exit(process.exitCode || 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -169,6 +169,5 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await safeDisconnectBrowser(browser);
|
await safeDisconnectBrowser(browser);
|
||||||
process.exit(process.exitCode || 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
305
src/main/scripts/fz.md
Normal file
305
src/main/scripts/fz.md
Normal file
File diff suppressed because one or more lines are too long
BIN
src/main/scripts/mt.png
Normal file
BIN
src/main/scripts/mt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -232,6 +232,5 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await safeDisconnectBrowser(browser);
|
await safeDisconnectBrowser(browser);
|
||||||
process.exit(process.exitCode || 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -128,6 +128,5 @@ const isBlankLikePage = (url) => {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
process.exit(process.exitCode || 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
4
src/main/scripts/xc_trace.js
Normal file
4
src/main/scripts/xc_trace.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
log.info('xc_trace.mjs placeholder: not implemented');
|
||||||
|
process.exit(0);
|
||||||
@@ -46,6 +46,6 @@ export async function launchLocalChrome() {
|
|||||||
// 延迟几秒等浏览器起来
|
// 延迟几秒等浏览器起来
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(0);
|
resolve(0);
|
||||||
}, 1000); // 延迟1秒
|
}, 3000); // 延迟3秒
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import Dexie from 'dexie'
|
|
||||||
|
|
||||||
export const db = new Dexie('NianXX') as Dexie & {}
|
|
||||||
|
|
||||||
db.version(1).stores({})
|
|
||||||
@@ -31,6 +31,6 @@ export function createLogo() {
|
|||||||
if (logo != null) {
|
if (logo != null) {
|
||||||
return logo;
|
return logo;
|
||||||
}
|
}
|
||||||
logo = path.join(__dirname, '/public/logo.ico');
|
logo = path.join(__dirname, 'logo.ico');
|
||||||
return logo;
|
return logo;
|
||||||
}
|
}
|
||||||
|
|||||||
122
src/renderer/api/SessionsApi.ts
Normal file
122
src/renderer/api/SessionsApi.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { getRequest, postRequest, patchRequest, deleteRequest, ResponseModel } from '@utils/request'
|
||||||
|
|
||||||
|
// 创建会话 的请求参数和响应数据结构
|
||||||
|
export interface CreateSessionRequest {
|
||||||
|
title?: string
|
||||||
|
tenant_id_query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSessionResponse {
|
||||||
|
session_id: string
|
||||||
|
user_id: string
|
||||||
|
tenant_id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSession = async (params: CreateSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest('/nianxx/api/sessions', params)
|
||||||
|
return res.data as CreateSessionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取会话列表 的请求参数和响应数据结构
|
||||||
|
export interface SessionListRequest {
|
||||||
|
tenant_id_query?: string
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionListResponse {
|
||||||
|
sessions: Array<SessionListRecords>
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionListRecords {
|
||||||
|
session_id: string
|
||||||
|
user_id: string
|
||||||
|
tenant_id: string
|
||||||
|
title: string
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSessionList = async (params: SessionListRequest) => {
|
||||||
|
const res: ResponseModel = await getRequest('/nianxx/api/sessions', params)
|
||||||
|
return res.data as SessionListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// 获取会话消息历史 的请求参数和响应数据结构
|
||||||
|
export interface SessionMessagesRequest {
|
||||||
|
user_id_query?: string
|
||||||
|
tenant_id_query?: number
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
session_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessagesResponse {
|
||||||
|
messages: Array<SessionMessageRecords>
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessageRecords {
|
||||||
|
id: number
|
||||||
|
session_id: string
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
source: string
|
||||||
|
message_id: string | null
|
||||||
|
created_at: string
|
||||||
|
timestamp?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话消息历史 的函数实现
|
||||||
|
export const getSessionMessages = async (params: SessionMessagesRequest) => {
|
||||||
|
const res: ResponseModel = await getRequest(`/nianxx/api/sessions/${params.session_id}/messages`, {
|
||||||
|
limit: params.limit,
|
||||||
|
offset: params.offset,
|
||||||
|
user_id_query: params.user_id_query,
|
||||||
|
tenant_id_query: params.tenant_id_query,
|
||||||
|
})
|
||||||
|
return res.data as SessionMessagesResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// /api/sessions/{session_id} 的请求参数和响应数据结构
|
||||||
|
export interface UpdateSessionRequest {
|
||||||
|
session_id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSessionResponse {
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会话信息 的函数实现
|
||||||
|
export const updateSession = async (params: UpdateSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest(`/nianxx/api/sessions/${params.session_id}/rename`, {
|
||||||
|
title: params.title,
|
||||||
|
})
|
||||||
|
return res.data as UpdateSessionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// /api/sessions/{session_id} 的请求参数和响应数据结构
|
||||||
|
export interface DeleteSessionRequest {
|
||||||
|
session_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSessionResponse {
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话 的函数实现
|
||||||
|
export const deleteSession = async (params: DeleteSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest(`/nianxx/api/sessions/${params.session_id}/delete`, {})
|
||||||
|
return res.data as DeleteSessionResponse
|
||||||
|
}
|
||||||
@@ -8,23 +8,23 @@
|
|||||||
<native-tooltip :content="t('window.minimize')">
|
<native-tooltip :content="t('window.minimize')">
|
||||||
<button v-show="isMinimizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
<button v-show="isMinimizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||||
@click="minimizeWindow">
|
@click="minimizeWindow">
|
||||||
<iconify-icon icon="material-symbols:check-indeterminate-small" :color="color" :width="btnSize"
|
<iconify-icon icon="material-symbols:check-indeterminate-small" color="#525866" :width="btnSize"
|
||||||
:height="btnSize" />
|
:height="btnSize" />
|
||||||
</button>
|
</button>
|
||||||
</native-tooltip>
|
</native-tooltip>
|
||||||
<native-tooltip :content="isMaximized ? t('window.restore') : t('window.maximize')">
|
<native-tooltip :content="isMaximized ? t('window.restore') : t('window.maximize')">
|
||||||
<button v-show="isMaximizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
<button v-show="isMaximizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||||
@click="maximizeWindow">
|
@click="maximizeWindow">
|
||||||
<iconify-icon icon="material-symbols:chrome-maximize-outline-sharp" :color="color" :width="btnSize"
|
<iconify-icon icon="material-symbols:chrome-maximize-outline-sharp" color="#525866" :width="btnSize"
|
||||||
:height="btnSize" v-show="!isMaximized" />
|
:height="btnSize" v-show="!isMaximized" />
|
||||||
<iconify-icon icon="material-symbols:chrome-restore-outline-sharp" :color="color" :width="btnSize"
|
<iconify-icon icon="material-symbols:chrome-restore-outline-sharp" color="#525866" :width="btnSize"
|
||||||
:height="btnSize" v-show="isMaximized" />
|
:height="btnSize" v-show="isMaximized" />
|
||||||
</button>
|
</button>
|
||||||
</native-tooltip>
|
</native-tooltip>
|
||||||
<native-tooltip :content="t('window.close')">
|
<native-tooltip :content="t('window.close')">
|
||||||
<button v-show="isClosable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
<button v-show="isClosable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
|
||||||
@click="handleClose">
|
@click="handleClose">
|
||||||
<iconify-icon icon="material-symbols:close" :color="color" :width="btnSize" :height="btnSize" />
|
<iconify-icon icon="material-symbols:close" color="#525866" :width="btnSize" :height="btnSize" />
|
||||||
</button>
|
</button>
|
||||||
</native-tooltip>
|
</native-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,10 +42,9 @@ interface HeaderBarProps {
|
|||||||
isMaximizable?: boolean;
|
isMaximizable?: boolean;
|
||||||
isMinimizable?: boolean;
|
isMinimizable?: boolean;
|
||||||
isClosable?: boolean;
|
isClosable?: boolean;
|
||||||
color?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({ name: 'HeaderBar', color: '#525866' })
|
defineOptions({ name: 'HeaderBar' })
|
||||||
|
|
||||||
withDefaults(defineProps<HeaderBarProps>(), {
|
withDefaults(defineProps<HeaderBarProps>(), {
|
||||||
isMaximizable: true,
|
isMaximizable: true,
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ export const channels: Item[] = [
|
|||||||
{
|
{
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
channelName: 'douyin',
|
channelName: 'douyin',
|
||||||
channelUrl: 'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
|
channelUrl: 'https://life.douyin.com/p/goods_winetour/physical_room_list?groupid=1816249020842116',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -77,6 +77,7 @@ instance.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 ~ request: \n url:${config.url} \n params:${JSON.stringify(config.data)} \n`)
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -87,7 +88,7 @@ instance.interceptors.request.use(
|
|||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(res) => {
|
(res) => {
|
||||||
console.log(`🚀 ~ response: \n url:${res.config.url} \n params:${JSON.stringify(res.config.params)} \n data:\n ${JSON.stringify(res.data)}`)
|
console.log(`🚀 ~ response: \n url:${res.config.url} \n params:${JSON.stringify(res.config.data)} \n data:\n ${JSON.stringify(res.data)}`)
|
||||||
// 未设置状态码则默认成功状态
|
// 未设置状态码则默认成功状态
|
||||||
const code = res.data.code || 200
|
const code = res.data.code || 200
|
||||||
// 获取错误信息
|
// 获取错误信息
|
||||||
@@ -143,7 +144,7 @@ instance.interceptors.response.use(
|
|||||||
export const postRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
export const postRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
return instance.request({
|
return instance.request({
|
||||||
url,
|
url,
|
||||||
method: 'POST',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -156,12 +157,38 @@ export const postRequest = <ResponseModel>(url: string, data?: any, options?: an
|
|||||||
export const getRequest = <ResponseModel>(url: string, params?: any, options?: any): Promise<ResponseModel> => {
|
export const getRequest = <ResponseModel>(url: string, params?: any, options?: any): Promise<ResponseModel> => {
|
||||||
return instance.request({
|
return instance.request({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'get',
|
||||||
params,
|
params,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
}) as Promise<ResponseModel>
|
}) as Promise<ResponseModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 封装基于 request 的 PATCH 请求
|
||||||
|
export const patchRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'patch',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封装基于 request 的 DELETE 请求
|
||||||
|
export const deleteRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'delete',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
export default instance
|
export default instance
|
||||||
|
|
||||||
/// 响应模型
|
/// 响应模型
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg">
|
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg">
|
||||||
<template #header>
|
<template #header>
|
||||||
<!-- 名字和时间 -->
|
<!-- 名字和时间 -->
|
||||||
<ChatNameTime :showReverse="true" />
|
<ChatNameTime :showReverse="true" :msg="msg" />
|
||||||
</template>
|
</template>
|
||||||
</ChatRoleMe>
|
</ChatRoleMe>
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
||||||
<template #header>
|
<template #header>
|
||||||
<!-- 名字和时间 -->
|
<!-- 名字和时间 -->
|
||||||
<ChatNameTime :showReverse="false" />
|
<ChatNameTime :showReverse="false" :msg="msg" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -69,7 +69,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref, watch, nextTick } from "vue";
|
import { ref, defineProps, defineEmits, watch, nextTick } from 'vue'
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
import { WebSocketManager } from "@common/WebSocketManager";
|
import { WebSocketManager } from "@common/WebSocketManager";
|
||||||
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
||||||
import { IdUtils } from "@common/index";
|
import { IdUtils } from "@common/index";
|
||||||
@@ -83,11 +84,11 @@ import ChatAttach from './components/ChatAttach.vue';
|
|||||||
import ChatInputArea from './components/ChatInputArea.vue';
|
import ChatInputArea from './components/ChatInputArea.vue';
|
||||||
import TaskCenter from './TaskCenter.vue';
|
import TaskCenter from './TaskCenter.vue';
|
||||||
|
|
||||||
import { Session } from '@utils/storage';
|
import { Session } from '../../utils/storage';
|
||||||
|
|
||||||
import userAvatar from '@assets/images/login/user_icon.png';
|
import userAvatar from '@assets/images/login/user_icon.png';
|
||||||
import aiAvatar from '@assets/images/login/blue_logo.png';
|
import aiAvatar from '@assets/images/login/blue_logo.png';
|
||||||
import { createConversation, conversationMessageList } from '@api/ConversationApi';
|
import { createSession, getSessionMessages } from '../../api/SessionsApi';
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
|
|
||||||
// 支持外部通过 prop 控制是否为引导页
|
// 支持外部通过 prop 控制是否为引导页
|
||||||
@@ -109,8 +110,8 @@ watch(isGuidePage, (v) => {
|
|||||||
emit('update:guide', v);
|
emit('update:guide', v);
|
||||||
if (v) {
|
if (v) {
|
||||||
// 当切换到引导页时,重置/清理会话状态
|
// 当切换到引导页时,重置/清理会话状态
|
||||||
|
conversationId.value = '';
|
||||||
resetConversation();
|
resetConversation();
|
||||||
createConversationRequest();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,11 +129,15 @@ const isSendingMessage = ref(false);
|
|||||||
const agentId = ref("1953462165250859011");
|
const agentId = ref("1953462165250859011");
|
||||||
/// 会话ID 历史数据接口中获取
|
/// 会话ID 历史数据接口中获取
|
||||||
const conversationId = ref(props.conversationId);
|
const conversationId = ref(props.conversationId);
|
||||||
|
// 标记 conversationId 是否来自历史消息(由 props.conversationId 提供)
|
||||||
|
const conversationIdFromHistory = ref(!!props.conversationId);
|
||||||
|
|
||||||
// 监听 conversationId prop 变化,只有当有值时(选择历史消息)才请求消息列表
|
// 监听 conversationId prop 变化,只有当有值时(选择历史消息)才请求消息列表
|
||||||
watch(() => props.conversationId, (newId) => {
|
watch(() => props.conversationId, (newId) => {
|
||||||
if (newId) {
|
if (newId) {
|
||||||
conversationId.value = newId;
|
conversationId.value = newId;
|
||||||
|
console.log("外部 conversationId 变化,加载对应消息:", newId);
|
||||||
|
conversationIdFromHistory.value = true;
|
||||||
loadConversationMessages(newId);
|
loadConversationMessages(newId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -274,10 +279,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
// token存在,初始化数据
|
// token存在,初始化数据
|
||||||
const initHandler = async () => {
|
const initHandler = async () => {
|
||||||
console.log("initHandler");
|
console.log("initHandler:检查 token 并初始化数据");
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
await createConversationRequest();
|
|
||||||
await initWebSocket();
|
await initWebSocket();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,11 +299,13 @@ const checkToken = async () => {
|
|||||||
|
|
||||||
// 调用接口创建新会话
|
// 调用接口创建新会话
|
||||||
const createConversationRequest = async (): Promise<string | null> => {
|
const createConversationRequest = async (): Promise<string | null> => {
|
||||||
const res = await createConversation();
|
const res = await createSession({});
|
||||||
if (res && res.conversationId) {
|
if (res && res.session_id) {
|
||||||
conversationId.value = res.conversationId;
|
conversationId.value = res.session_id;
|
||||||
|
// 新创建的 session 不是来源于历史
|
||||||
|
conversationIdFromHistory.value = false;
|
||||||
console.log("创建新会话,ID:", conversationId.value);
|
console.log("创建新会话,ID:", conversationId.value);
|
||||||
return res.conversationId;
|
return res.session_id;
|
||||||
} else {
|
} else {
|
||||||
console.log("创建会话失败,接口返回异常");
|
console.log("创建会话失败,接口返回异常");
|
||||||
return null;
|
return null;
|
||||||
@@ -309,12 +315,14 @@ const createConversationRequest = async (): Promise<string | null> => {
|
|||||||
// 加载历史会话消息
|
// 加载历史会话消息
|
||||||
const loadConversationMessages = async (convId: string) => {
|
const loadConversationMessages = async (convId: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await conversationMessageList({ conversationId: convId, pageSize: 50, pageNum: 1 });
|
const res = await getSessionMessages({ session_id: convId, limit: 50, offset: 0 });
|
||||||
// 将消息转换为 ChatMessage 格式
|
// 将消息转换为 ChatMessage 格式
|
||||||
chatMsgList.value = res.records.map((msg: any) => ({
|
chatMsgList.value = res.messages.map((msg: any) => ({
|
||||||
messageId: msg.messageId,
|
messageId: msg.message_id,
|
||||||
messageRole: msg.messageSenderRole === 'user' ? MessageRole.ME : MessageRole.AI,
|
messageRole: msg.role === 'user' ? MessageRole.ME : MessageRole.AI,
|
||||||
messageContent: msg.messageContent,
|
messageContent: msg.content,
|
||||||
|
messageContentList: [msg.content],
|
||||||
|
timestamp: msg.created_at_ts,
|
||||||
finished: true, // 历史消息已完成
|
finished: true, // 历史消息已完成
|
||||||
}));
|
}));
|
||||||
console.log("加载历史消息:", chatMsgList.value);
|
console.log("加载历史消息:", chatMsgList.value);
|
||||||
@@ -347,7 +355,8 @@ const initWebSocket = async () => {
|
|||||||
|
|
||||||
// 使用配置的WebSocket服务器地址
|
// 使用配置的WebSocket服务器地址
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
const wsUrl = `wss://onefeel.brother7.cn/ingress/agent/ws/chat?access_token=${token}`;
|
// const wsUrl = `wss://onefeel.brother7.cn/ingress/agent/ws/chat?access_token=${token}`;
|
||||||
|
const wsUrl = `wss://onefeel.brother7.cn/ingress/nianxx/ws?token=${token}`;
|
||||||
// 初始化WebSocket管理器
|
// 初始化WebSocket管理器
|
||||||
webSocketManager = new WebSocketManager({
|
webSocketManager = new WebSocketManager({
|
||||||
wsUrl: wsUrl,
|
wsUrl: wsUrl,
|
||||||
@@ -407,6 +416,16 @@ const initWebSocket = async () => {
|
|||||||
// 处理WebSocket消息
|
// 处理WebSocket消息
|
||||||
const handleWebSocketMessage = (data: any) => {
|
const handleWebSocketMessage = (data: any) => {
|
||||||
console.log("收到WebSocket消息:", data);
|
console.log("收到WebSocket消息:", data);
|
||||||
|
|
||||||
|
if (data.type === 'notification' && data.event === 'connected') {
|
||||||
|
console.log("WebSocket连接已建立,服务器消息:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'heartbeat') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 验证关键字段(若服务端传回 conversationId/agentId,则校验是否属于当前会话)
|
// 验证关键字段(若服务端传回 conversationId/agentId,则校验是否属于当前会话)
|
||||||
if (data.conversationId && data.conversationId !== conversationId.value) {
|
if (data.conversationId && data.conversationId !== conversationId.value) {
|
||||||
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
|
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
|
||||||
@@ -427,7 +446,7 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用 messageId 进行匹配
|
// 优先使用 messageId 进行匹配
|
||||||
const msgId = data.messageId || data.id || data.msgId;
|
const msgId = data.messageId || data.reply_message_id || data.id || data.msgId;
|
||||||
let aiMsgIndex = -1;
|
let aiMsgIndex = -1;
|
||||||
if (msgId && pendingMap.has(msgId)) {
|
if (msgId && pendingMap.has(msgId)) {
|
||||||
aiMsgIndex = pendingMap.get(msgId);
|
aiMsgIndex = pendingMap.get(msgId);
|
||||||
@@ -453,16 +472,24 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
if (chatMsgList.value[aiMsgIndex].isLoading) {
|
if (chatMsgList.value[aiMsgIndex].isLoading) {
|
||||||
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
|
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
|
||||||
chatMsgList.value[aiMsgIndex].messageContent = data.content;
|
chatMsgList.value[aiMsgIndex].messageContent = data.content;
|
||||||
|
chatMsgList.value[aiMsgIndex].messageContentList = [data.content];
|
||||||
chatMsgList.value[aiMsgIndex].isLoading = false;
|
chatMsgList.value[aiMsgIndex].isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
// 后续流式内容追加
|
// 后续流式内容追加
|
||||||
chatMsgList.value[aiMsgIndex].messageContent += data.content;
|
chatMsgList.value[aiMsgIndex].messageContent += data.content;
|
||||||
|
chatMsgList.value[aiMsgIndex].messageContentList.push(data.content);
|
||||||
}
|
}
|
||||||
nextTick(() => scrollToBottom());
|
nextTick(() => scrollToBottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 对于通知类消息,如果没有明确的完成状态,默认视为已完成,触发后续处理逻辑(例如心跳、连接建立等事件)
|
||||||
|
if (data.type === 'notification') {
|
||||||
|
data.finish = data.finish || true; // 确保 finish 字段存在
|
||||||
|
}
|
||||||
|
|
||||||
// 处理完成状态
|
// 处理完成状态
|
||||||
if (data.finish) {
|
if (data.finish) {
|
||||||
|
chatMsgList.value[aiMsgIndex].timestamp = Date.now();
|
||||||
chatMsgList.value[aiMsgIndex].finished = data.finish;
|
chatMsgList.value[aiMsgIndex].finished = data.finish;
|
||||||
const msg = chatMsgList.value[aiMsgIndex].messageContent;
|
const msg = chatMsgList.value[aiMsgIndex].messageContent;
|
||||||
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
|
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
|
||||||
@@ -516,6 +543,16 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
|
|
||||||
await checkToken();
|
await checkToken();
|
||||||
|
|
||||||
|
// 如果没有 conversationId(且非历史来源),在发送时按需创建会话
|
||||||
|
if (!conversationId.value) {
|
||||||
|
const sid = await createConversationRequest();
|
||||||
|
if (!sid) {
|
||||||
|
ElMessage({ message: '创建会话失败,请稍后重试', type: 'error' });
|
||||||
|
console.error('createConversationRequest failed before send');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查WebSocket连接状态,如果未连接,尝试重新连接
|
// 检查WebSocket连接状态,如果未连接,尝试重新连接
|
||||||
if (!isWsConnected()) {
|
if (!isWsConnected()) {
|
||||||
console.log("WebSocket未连接,尝试重新连接...");
|
console.log("WebSocket未连接,尝试重新连接...");
|
||||||
@@ -553,6 +590,8 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
messageId: IdUtils.generateMessageId(),
|
messageId: IdUtils.generateMessageId(),
|
||||||
messageRole: MessageRole.ME,
|
messageRole: MessageRole.ME,
|
||||||
messageContent: message,
|
messageContent: message,
|
||||||
|
messageContentList: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
chatMsgList.value.push(newMsg);
|
chatMsgList.value.push(newMsg);
|
||||||
inputMessage.value = "";
|
inputMessage.value = "";
|
||||||
@@ -686,6 +725,8 @@ const sendChat = async (message: string, isInstruct = false) => {
|
|||||||
messageId: currentSessionMessageId,
|
messageId: currentSessionMessageId,
|
||||||
messageRole: MessageRole.AI,
|
messageRole: MessageRole.AI,
|
||||||
messageContent: "加载中",
|
messageContent: "加载中",
|
||||||
|
messageContentList: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -823,6 +864,10 @@ const resetConversation = () => {
|
|||||||
pendingTimeouts.clear();
|
pendingTimeouts.clear();
|
||||||
pendingMap.clear();
|
pendingMap.clear();
|
||||||
|
|
||||||
|
// 如果 conversationId 不是来自历史,重置 conversationId
|
||||||
|
if (!conversationIdFromHistory.value) {
|
||||||
|
conversationId.value = '';
|
||||||
|
}
|
||||||
// 清理消息与状态
|
// 清理消息与状态
|
||||||
chatMsgList.value = [];
|
chatMsgList.value = [];
|
||||||
inputMessage.value = '';
|
inputMessage.value = '';
|
||||||
|
|||||||
@@ -19,20 +19,62 @@
|
|||||||
]">
|
]">
|
||||||
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="truncate text-sm">{{ item.conversationId }}</div>
|
<div class="truncate text-sm">{{ item.conversationTitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="item.conversationId === selectedConversationId"
|
|
||||||
class="bg-transparent border-0 text-gray-500 text-lg px-1 py-0">…</button>
|
<el-dropdown v-if="item.conversationId === selectedConversationId" placement="bottom-end">
|
||||||
|
<el-icon class="el-icon--right">
|
||||||
|
...
|
||||||
|
</el-icon>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="renameHistoryMessage(item.conversationId)">重命名</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="deleteHistoryMessage(item.conversationId)">删除</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- 重命名对话框 -->
|
||||||
|
<el-dialog v-model="renameDialogFormVisible" title="重命名对话" width="500">
|
||||||
|
<el-form :model="newMessageName">
|
||||||
|
<el-form-item label="对话名称" :label-width="formLabelWidth">
|
||||||
|
<el-input v-model="newMessageName" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="renameDialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitNameChange">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<el-dialog v-model="deleteDialogVisible" title="温馨提示" width="500">
|
||||||
|
<span>您确定删除该会话吗?删除后将无法恢复!</span>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="deleteDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitDelete">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, defineEmits } from 'vue'
|
import { ref, onMounted, defineEmits } from 'vue'
|
||||||
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
||||||
import { getConversationList } from '../../api/ConversationApi';
|
import { getSessionList, deleteSession, updateSession } from '../../api/SessionsApi';
|
||||||
|
|
||||||
|
const deleteDialogVisible = ref(false)
|
||||||
|
const renameDialogFormVisible = ref(false)
|
||||||
|
const newMessageName = ref('')
|
||||||
|
const formLabelWidth = '100px'
|
||||||
|
|
||||||
interface HistoryMessage {
|
interface HistoryMessage {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
@@ -51,6 +93,10 @@ const emit = defineEmits(['new-chat', 'select-chat'])
|
|||||||
/// 添加新对话
|
/// 添加新对话
|
||||||
const addNewChat = () => {
|
const addNewChat = () => {
|
||||||
console.log('add new chat')
|
console.log('add new chat')
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNewChat = () => {
|
||||||
// 触发新对话事件
|
// 触发新对话事件
|
||||||
emit('new-chat')
|
emit('new-chat')
|
||||||
// 清空选择的历史消息ID
|
// 清空选择的历史消息ID
|
||||||
@@ -65,6 +111,43 @@ const selectedHistoryMessage = (conversationId: string) => {
|
|||||||
emit('select-chat', conversationId)
|
emit('select-chat', conversationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重命名历史消息
|
||||||
|
const renameHistoryMessage = (conversationId: string) => {
|
||||||
|
console.log('rename message', conversationId)
|
||||||
|
renameDialogFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除历史消息
|
||||||
|
const deleteHistoryMessage = (conversationId: string) => {
|
||||||
|
console.log('delete message', conversationId)
|
||||||
|
deleteDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交重命名
|
||||||
|
const submitNameChange = async () => {
|
||||||
|
console.log('submit name change', newMessageName.value)
|
||||||
|
renameDialogFormVisible.value = false
|
||||||
|
const res = await updateSession({
|
||||||
|
session_id: selectedConversationId.value,
|
||||||
|
title: newMessageName.value
|
||||||
|
})
|
||||||
|
if (res && res.success) {
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交删除
|
||||||
|
const submitDelete = async () => {
|
||||||
|
console.log('submit delete')
|
||||||
|
deleteDialogVisible.value = false
|
||||||
|
const res = await deleteSession({
|
||||||
|
session_id: selectedConversationId.value
|
||||||
|
})
|
||||||
|
if (res && res.success) {
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 页面加载时获取历史会话列表
|
/// 页面加载时获取历史会话列表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getHistoryConversationList()
|
getHistoryConversationList()
|
||||||
@@ -72,12 +155,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
/// 获取历史会话列表
|
/// 获取历史会话列表
|
||||||
const getHistoryConversationList = async () => {
|
const getHistoryConversationList = async () => {
|
||||||
const list = await getConversationList({ pageSize: 20, pageNum: 1 })
|
const list = await getSessionList({ limit: 50, offset: 0 })
|
||||||
if (!list || !list.records) return;
|
if (!list || !list.sessions) return;
|
||||||
groups.value.push(...list.records.map((item: any) => ({
|
// 使用整体赋值替换 push,避免重复累加
|
||||||
conversationId: item.conversationId,
|
groups.value = list.sessions.map((item: any) => ({
|
||||||
conversationTitle: item.conversationTitle
|
conversationId: item.session_id,
|
||||||
})))
|
conversationTitle: item.title
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-start gap-2 pt-0.5 mb-2" :class="showReverse ? 'flex-row-reverse' : 'flex-row'">
|
<div class="flex items-start gap-2 pt-0.5 mb-2" :class="props.showReverse ? 'flex-row-reverse' : 'flex-row'">
|
||||||
<span class="text-xs text-[#4E5969]"> ZHINIAN</span>
|
<span class="text-xs text-[#4E5969]">{{ props.msg?.messageRole === MessageRole.AI ? 'NIANXX' : '我' }}</span>
|
||||||
<span class="text-xs text-[#86909C]"> 20:30</span>
|
<span class="text-xs text-[#86909C]">{{ formattedTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { ChatMessage, MessageRole } from '../model/ChatModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showReverse: boolean
|
msg?: ChatMessage
|
||||||
|
showReverse?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showReverse: false
|
showReverse: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formattedTime = computed(() => {
|
||||||
|
const tsRaw = props.msg?.timestamp
|
||||||
|
if (tsRaw == null) return ''
|
||||||
|
let ts = Number(tsRaw)
|
||||||
|
if (isNaN(ts)) return ''
|
||||||
|
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
|
|
||||||
|
// Heuristic:
|
||||||
|
// - If ts < 1e9, treat as a duration in seconds and convert to dd-hh-mm (legacy)
|
||||||
|
// - If ts looks like an epoch (seconds or ms) format to YYYY年MM月DD日 HH:mm:ss
|
||||||
|
if (ts < 1e9) {
|
||||||
|
const totalSeconds = Math.floor(ts)
|
||||||
|
const days = Math.floor(totalSeconds / 86400)
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||||
|
return `${String(days).padStart(2, '0')}-${pad(hours)}-${pad(minutes)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// epoch handling: convert seconds -> ms when appropriate
|
||||||
|
if (ts < 1e12) ts = ts * 1000
|
||||||
|
const d = new Date(ts)
|
||||||
|
if (isNaN(d.getTime())) return ''
|
||||||
|
const Y = d.getFullYear()
|
||||||
|
const M = pad(d.getMonth() + 1)
|
||||||
|
const D = pad(d.getDate())
|
||||||
|
const h = pad(d.getHours())
|
||||||
|
const m = pad(d.getMinutes())
|
||||||
|
const s = pad(d.getSeconds())
|
||||||
|
|
||||||
|
// If the timestamp is the same calendar day as today, show only time HH:mm:ss
|
||||||
|
const now = new Date()
|
||||||
|
const sameDay = now.getFullYear() === d.getFullYear()
|
||||||
|
&& now.getMonth() === d.getMonth()
|
||||||
|
&& now.getDate() === d.getDate()
|
||||||
|
|
||||||
|
if (sameDay) {
|
||||||
|
return `${h}:${m}:${s}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise show YYYY-MM-DD HH:mm:ss
|
||||||
|
return `${Y}-${M}-${D} ${h}:${m}:${s}`
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-[75%] flex flex-col">
|
<div class="max-w-[75%] flex flex-col">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
<div class="text-sm text-gray-700 flex flex-row">
|
<div v-if="!msg.messageContentList || msg.messageContentList.length === 0"
|
||||||
|
class="flex flex-row text-sm text-gray-700">
|
||||||
<div v-html="compiledMarkdown"></div>
|
<div v-html="compiledMarkdown"></div>
|
||||||
<ChatLoading v-if="msg.isLoading" />
|
<ChatLoading v-if="msg.isLoading" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col p-2 mb-2 text-sm text-gray-700 bg-[#f7f9fc] rounded-md"
|
||||||
|
v-for="(_, index) in msg.messageContentList" :key="index">
|
||||||
|
<div v-html="compiledAt(index)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,6 +30,9 @@ interface Props {
|
|||||||
|
|
||||||
const { msg } = defineProps<Props>()
|
const { msg } = defineProps<Props>()
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
highlight: function (str: string, lang: string) {
|
highlight: function (str: string, lang: string) {
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
try {
|
try {
|
||||||
@@ -33,6 +43,18 @@ const md = new MarkdownIt({
|
|||||||
return hljs.highlightAuto(str).value;
|
return hljs.highlightAuto(str).value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const compiledMarkdown = computed(() => md.render(msg.messageContent))
|
const compiledMarkdown = computed(() => md.render(msg.messageContent))
|
||||||
|
|
||||||
|
const compiledList = computed(() => {
|
||||||
|
return (msg.messageContentList || []).map((m: string) => md.render(m || ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
const compiledAt = (index: number): string => {
|
||||||
|
const list: string[] = (compiledList as any).value || []
|
||||||
|
if (list[index]) return list[index]
|
||||||
|
const raw = msg?.messageContentList?.[index] || ''
|
||||||
|
return md.render(raw || '')
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,43 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<layout>
|
<layout>
|
||||||
<div class="flex h-full w-full flex-col md:flex-row">
|
<div class="flex h-full w-full flex-col md:flex-row">
|
||||||
<ChatHistory class="flex-none w-50" @new-chat="guide = true" @select-chat="handleSelectChat" />
|
<ChatHistory class="flex-none w-50" @new-chat="handleNewChat" @select-chat="handleSelectChat" />
|
||||||
|
|
||||||
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
||||||
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
|
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TaskList />
|
<TaskList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TaskOperationDialog ref="taskOperationDialogRef" />
|
|
||||||
</layout>
|
</layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import TaskList from '@renderer/components/TaskList/index.vue'
|
import TaskList from '@renderer/components/TaskList/index.vue'
|
||||||
import TaskOperationDialog from '@renderer/views/home/components/TaskOperationDialog.vue'
|
|
||||||
import ChatHistory from './ChatHistory.vue'
|
import ChatHistory from './ChatHistory.vue'
|
||||||
import ChatBox from './ChatBox.vue'
|
import ChatBox from './ChatBox.vue'
|
||||||
import emitter from '@utils/emitter'
|
import { ref } from 'vue'
|
||||||
|
/// 是否显示引导页
|
||||||
// 是否显示引导页
|
|
||||||
const guide = ref(true)
|
const guide = ref(true)
|
||||||
// 选择的历史会话ID
|
/// 选择的历史会话ID
|
||||||
const selectedConversationId = ref('')
|
const selectedConversationId = ref('')
|
||||||
// 任务操作弹窗引用
|
|
||||||
const taskOperationDialogRef = ref()
|
|
||||||
|
|
||||||
|
/// 处理新对话事件:切换到引导页并清空选中的历史会话ID
|
||||||
|
const handleNewChat = () => {
|
||||||
|
guide.value = true;
|
||||||
|
selectedConversationId.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
// 选择历史会话
|
/// 选择历史会话
|
||||||
const handleSelectChat = (conversationId: string) => {
|
const handleSelectChat = (conversationId: string) => {
|
||||||
guide.value = false;
|
guide.value = false;
|
||||||
selectedConversationId.value = conversationId;
|
selectedConversationId.value = conversationId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听任务操作弹窗关闭事件
|
|
||||||
emitter.on('OPERATION_CHANNEL', (item) => {
|
|
||||||
taskOperationDialogRef.value?.open(item);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class ChatMessage {
|
|||||||
messageRole: MessageRole;
|
messageRole: MessageRole;
|
||||||
// 消息内容
|
// 消息内容
|
||||||
messageContent: string;
|
messageContent: string;
|
||||||
|
// 消息内容列表(用于流式更新)
|
||||||
|
messageContentList: string[];
|
||||||
// 是否加载中
|
// 是否加载中
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
// 是否完成
|
// 是否完成
|
||||||
@@ -24,22 +26,28 @@ export class ChatMessage {
|
|||||||
toolCall?: any;
|
toolCall?: any;
|
||||||
// 问题信息
|
// 问题信息
|
||||||
question?: string;
|
question?: string;
|
||||||
|
// 时间戳
|
||||||
|
timestamp?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
messageRole: MessageRole,
|
messageRole: MessageRole,
|
||||||
messageContent: string,
|
messageContent: string,
|
||||||
|
messageContentList: string[] = [],
|
||||||
isLoading: boolean = false,
|
isLoading: boolean = false,
|
||||||
finished: boolean = false,
|
finished: boolean = false,
|
||||||
toolCall?: any,
|
toolCall?: any,
|
||||||
question?: any
|
question?: any,
|
||||||
|
timestamp?: number
|
||||||
) {
|
) {
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.messageRole = messageRole;
|
this.messageRole = messageRole;
|
||||||
this.messageContent = messageContent;
|
this.messageContent = messageContent;
|
||||||
|
this.messageContentList = messageContentList;
|
||||||
this.isLoading = isLoading;
|
this.isLoading = isLoading;
|
||||||
this.finished = finished;
|
this.finished = finished;
|
||||||
this.toolCall = toolCall;
|
this.toolCall = toolCall;
|
||||||
this.question = question;
|
this.question = question;
|
||||||
|
this.timestamp = timestamp || Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen login-bg flex flex-col">
|
<div class="h-screen login-bg flex flex-col">
|
||||||
<header-bar color="#fff">
|
<header-bar>
|
||||||
<drag-region class="w-full" />
|
<drag-region class="w-full" />
|
||||||
</header-bar>
|
</header-bar>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user