feat: 开发对话功能

This commit is contained in:
2026-04-11 16:54:09 +08:00
parent 5151379726
commit 8c2ea4488b
8 changed files with 466 additions and 167 deletions

View File

@@ -1,20 +1,21 @@
import { getAction, postAction, deleteAction } from './manage.js'
import url, { getBaseUrl } from './url.js'
import { getAction, postAction, deleteAction } from './manage.js';
import url, { getBaseUrl } from './url.js';
// 健康检查
export const getHealthAction = () => getAction(url.health)
export const getHealthAction = () => getAction(url.health);
// 会话
export const createSessionAction = (data) => postAction(url.session.create, data)
export const getSessionAction = (id) => getAction(url.session.detail(id))
export const listSessionsAction = () => getAction(url.session.list)
export const deleteSessionAction = (id) => deleteAction(url.session.delete(id))
export const createSessionAction = (data) => postAction(url.session.create, data);
export const getSessionAction = (id) => getAction(url.session.detail(id));
export const listSessionsAction = () => getAction(url.session.list);
export const deleteSessionAction = (id) => deleteAction(url.session.delete(id));
// 消息
export const sendMessageAction = (sessionId, data) => postAction(url.message.send(sessionId), data)
export const listMessagesAction = (sessionId) => getAction(url.message.list(sessionId))
export const sendMessageAction = (sessionId, data) => postAction(url.message.send(sessionId), data);
export const promptAsyncAction = (sessionId, data) => postAction(url.message.promptAsync(sessionId), data);
export const listMessagesAction = (sessionId) => getAction(url.message.list(sessionId));
// SSE 事件流(返回 EventSource 实例,由调用方管理生命周期)
export function createEventSource() {
return new EventSource(`${getBaseUrl()}${url.event}`)
return new EventSource(`${getBaseUrl()}${url.event}`);
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getBaseUrl } from './url.js'
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { getBaseUrl } from './url.js';
// baseURL 由主进程动态分配端口,通过 getBaseUrl() 运行时获取
const axiosInstance = axios.create({
@@ -10,131 +10,142 @@ const axiosInstance = axios.create({
'Content-Type': 'application/json;charset=utf-8',
},
responseType: 'json',
})
});
// 每次请求前动态更新 baseURL确保服务启动后端口变更能被感知
axiosInstance.interceptors.request.use((config) => {
config.baseURL = getBaseUrl();
return config;
});
// 请求拦截
axiosInstance.interceptors.request.use((config) => {
config.headers = config.headers || {}
let Authorization = localStorage.getItem('Authorization')
config.headers = config.headers || {};
let Authorization = localStorage.getItem('Authorization');
// 优先使用本地持久化的 Authorization 头(完整值)
config.headers.Authorization = Authorization || ''
config.headers.Authorization = Authorization || '';
if ('get' === config?.method?.toLowerCase()) {
if (config.params) {
config.params.timestamp = new Date().getTime()
config.params.timestamp = new Date().getTime();
}
}
// 移除敏感信息日志
// console.log(config, 'axios request.use config')
return config
})
return config;
});
axiosInstance.interceptors.response.use(
(response) => {
// 移除敏感信息日志
// console.log(response, 'response response')
// 若请求为二进制下载blob直接透传响应交由调用方自行处理
try {
const isBlob = response?.config?.responseType === 'blob'
const isBlob = response?.config?.responseType === 'blob';
if (isBlob) {
// 仍然尝试持久化可能返回的 Authorization
const respHeaders = response?.headers || {}
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization']
const respHeaders = response?.headers || {};
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization'];
if (newAuthorization && typeof newAuthorization === 'string') {
localStorage.setItem('Authorization', newAuthorization)
localStorage.setItem('Authorization', newAuthorization);
}
return response
return response;
}
} catch (e) {
console.log(e)
console.log(e);
// 忽略检查失败
}
// 如果响应头里带有 Authorization则使用 useStorage 持久化到 localStorage
// 以便后续请求自动携带该请求头
try {
const respHeaders = response?.headers || {}
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization']
const respHeaders = response?.headers || {};
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization'];
if (newAuthorization && typeof newAuthorization === 'string') {
localStorage.setItem('Authorization', newAuthorization)
localStorage.setItem('Authorization', newAuthorization);
}
} catch (e) {
// 忽略持久化失败,避免影响主流程
console.warn('持久化 Authorization 失败:', e)
console.warn('持久化 Authorization 失败:', e);
}
// 204 No Content如 prompt_async直接视为成功
if (response.status === 204) {
return Promise.resolve(null);
}
if (response.status === 200) {
const res = response.data || {}
const code = res.code
const msg = res.message || res.msg
const res = response.data || {};
const code = res.code;
const msg = res.message || res.msg;
// 明确的 200 成功,但需要按业务码再判断
if (code === 0) {
// 业务成功
return Promise.resolve(res)
return Promise.resolve(res);
}
// 特殊业务码处理
if (code === 401) {
// 清除持久化的 Authorization避免后续使用失效的头部
localStorage.removeItem('Authorization')
sessionStorage.removeItem('Token')
localStorage.removeItem('Authorization');
sessionStorage.removeItem('Token');
// 延迟跳转,确保消息显示
setTimeout(() => {
window.location.href = '/#/login'
}, 500)
return Promise.reject(new Error('认证失败,请重新登录'))
window.location.href = '/#/login';
}, 500);
return Promise.reject(new Error('认证失败,请重新登录'));
}
// 其余非 0 的业务码统一拦截提示,但不在这里显示 ElMessage
// 交由业务层使用 await-to-js 处理
return Promise.reject(new Error(msg || '请求失败'))
return Promise.reject(new Error(msg || '请求失败'));
}
// 非 2xx 按错误分支处理(通常会进入 error 拦截器)
return Promise.reject(new Error('请求失败'))
return Promise.reject(new Error('请求失败'));
},
(error) => {
console.error('请求错误:', error)
console.error('请求错误:', error);
if (error.response) {
// 服务器响应错误
const status = error.response.status
const message = error.response.data?.message || error.response.data?.msg || '请求失败'
const status = error.response.status;
const message = error.response.data?.message || error.response.data?.msg || '请求失败';
switch (status) {
case 400:
ElMessage.error(`无效的请求参数:${message}`)
break
ElMessage.error(`无效的请求参数:${message}`);
break;
case 401:
// 清除持久化的 Authorization避免后续使用失效的头部
localStorage.removeItem('Authorization')
ElMessage.error('未授权访问或登录已过期,请重新登录')
break
localStorage.removeItem('Authorization');
ElMessage.error('未授权访问或登录已过期,请重新登录');
break;
case 403:
ElMessage.error('访问被拒绝')
break
ElMessage.error('访问被拒绝');
break;
case 404:
ElMessage.error('资源未找到')
break
ElMessage.error('资源未找到');
break;
case 500:
ElMessage.error('服务器内部错误')
break
ElMessage.error('服务器内部错误');
break;
case 502:
case 503:
case 504:
ElMessage.error('服务暂时不可用,请稍后重试')
break
ElMessage.error('服务暂时不可用,请稍后重试');
break;
default:
ElMessage.error(`请求失败: ${message}`)
ElMessage.error(`请求失败: ${message}`);
}
} else if (error.request) {
// 网络错误
ElMessage.error('网络连接失败,请检查网络连接')
ElMessage.error('网络连接失败,请检查网络连接');
} else {
// 其他错误
ElMessage.error('请求发送失败')
ElMessage.error('请求发送失败');
}
return Promise.reject(error)
},
)
return Promise.reject(error);
}
);
export default axiosInstance
export default axiosInstance;

View File

@@ -1,6 +1,6 @@
// OpenCode 服务地址由主进程动态分配端口,通过 getBaseUrl() 获取
export function getBaseUrl() {
return window.__opencodeBaseUrl || 'http://127.0.0.1:4096'
return window.__opencodeBaseUrl || 'http://127.0.0.1:4096';
}
const url = {
@@ -18,11 +18,12 @@ const url = {
// 消息
message: {
send: (sessionId) => `/session/${sessionId}/message`,
promptAsync: (sessionId) => `/session/${sessionId}/prompt_async`,
list: (sessionId) => `/session/${sessionId}/message`,
},
// SSE 事件流
event: '/event',
}
};
export default url
export default url;