地区选择开发,AI接口联调

This commit is contained in:
francis_fh
2026-01-22 12:57:01 +08:00
parent 7dcfbd35fe
commit f73d3f7a8c
7 changed files with 813 additions and 134 deletions

View File

@@ -6,9 +6,9 @@
*/ */
export default { export default {
// baseUrl: 'http://39.98.44.136:8080', // 测试 // baseUrl: 'http://39.98.44.136:8080', // 测试
baseUrl: 'https://www.xjksly.cn/api/ks', // 测试
// baseUrl: 'https://www.xjksly.cn/api/ks', // 测试 // baseUrl: 'https://www.xjksly.cn/api/ks', // 测试
// baseUrl: 'http://ks.zhaopinzao8dian.com/api/ks', // 测试 // baseUrl: 'https://www.xjksly.cn/api/ks', // 测试
baseUrl: 'http://ks.zhaopinzao8dian.com/api/ks', // 测试
// LCBaseUrl:'http://10.110.145.145:9100',//内网端口 // LCBaseUrl:'http://10.110.145.145:9100',//内网端口
// LCBaseUrlInner:'http://10.110.145.145:10100',//招聘、培训、帮扶 // LCBaseUrlInner:'http://10.110.145.145:10100',//招聘、培训、帮扶
@@ -20,11 +20,11 @@ export default {
trainVideoImgUrl:'https://www.xjksly.cn/prod-api/file/file/minio', trainVideoImgUrl:'https://www.xjksly.cn/prod-api/file/file/minio',
// sseAI+ // sseAI+
// StreamBaseURl: 'http://39.98.44.136:8000', // StreamBaseURl: 'http://39.98.44.136:8000',
StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai', StreamBaseURl: 'https://www.xjksly.cn/api/ks/app/chat',
// StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai/test', // StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai/test',
// 语音转文字 // 语音转文字
// vioceBaseURl: 'ws://39.98.44.136:8080/speech-recognition', vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/ks/app/speech/asr',
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition', // vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
// 语音合成 // 语音合成
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis', speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
// indexedDB // indexedDB

View File

@@ -6,6 +6,12 @@
"navigationBarTitleText": "喀什智慧就业平台" "navigationBarTitleText": "喀什智慧就业平台"
} }
}, },
{
"path": "pages/city-select/index",
"style": {
"navigationBarTitleText": "选择城市"
}
},
{ {
"path": "pages/mine/mine", "path": "pages/mine/mine",
"style": { "style": {

View File

@@ -551,7 +551,9 @@ const scrollToBottom = throttle(function () {
}, 500); }, 500);
function getGuess() { function getGuess() {
$api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => { // $api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
$api.chatRequest('/guest', undefined, 'POST').then((res) => {
console.log('getGuess ---- res:', res);
guessList.value = res.data; guessList.value = res.data;
showGuess.value = true; showGuess.value = true;
nextTick(() => { nextTick(() => {
@@ -804,22 +806,22 @@ function refreshMarkdown(index) {
} }
const jobSearchQueries = [ const jobSearchQueries = [
'青岛有哪些薪资 12K 以上的岗位适合我?', '喀什地区有哪些薪资 12K 以上的岗位适合我?',
'青岛 3 年工作经验能找到哪些 12K 以上的工作?', '喀什地区 3 年工作经验能找到哪些 12K 以上的工作?',
'青岛哪些公司在招聘,薪资范围在 12K 以上?', '喀什地区哪些公司在招聘,薪资范围在 12K 以上?',
'青岛有哪些企业提供 15K 以上的岗位?', '喀什地区有哪些企业提供 15K 以上的岗位?',
'青岛哪些公司在招 3-5 年经验的岗位?', '喀什地区哪些公司在招 3-5 年经验的岗位?',
'我有三年的工作经验,能否推荐一些适合我的青岛的国企 岗位?', '我有三年的工作经验,能否推荐一些适合我的喀什地区的国企 岗位?',
'青岛国企目前在招聘哪些岗位?', '喀什地区国企目前在招聘哪些岗位?',
'青岛有哪些适合 3 年经验的国企岗位?', '喀什地区有哪些适合 3 年经验的国企岗位?',
'青岛国企招聘的岗位待遇如何?', '喀什地区国企招聘的岗位待遇如何?',
'青岛国企岗位的薪资水平是多少?', '喀什地区国企岗位的薪资水平是多少?',
'青岛哪些国企支持双休 & 五险一金完善?', '喀什地区哪些国企支持双休 & 五险一金完善?',
'青岛有哪些公司支持远程办公?', '喀什地区有哪些公司支持远程办公?',
'青岛有哪些外企的岗位,薪资 12K 以上的多吗?', '喀什地区有哪些外企的岗位,薪资 12K 以上的多吗?',
'青岛哪些企业在招聘 Web3.0 相关岗位?', '喀什地区哪些企业在招聘 Web3.0 相关岗位?',
'青岛哪些岗位支持海外远程?薪资如何?', '喀什地区哪些岗位支持海外远程?薪资如何?',
'青岛招聘 AI/大数据相关岗位的公司有哪些?', '喀什地区招聘 AI/大数据相关岗位的公司有哪些?',
]; ];
function changeQueries(value) { function changeQueries(value) {

425
pages/city-select/index.vue Normal file
View File

@@ -0,0 +1,425 @@
<template>
<view class="city-select-container">
<!-- 顶部导航栏 -->
<!-- <view class="nav-bar">
<view class="nav-left" @click="navBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">选择城市</view>
<view class="nav-right"></view>
</view> -->
<!-- 搜索框 -->
<view class="search-box">
<uni-icons class="search-icon" type="search" size="20" color="#999"></uni-icons>
<input
class="search-input"
placeholder="搜索城市名/拼音"
placeholder-style="color: #999"
v-model="searchText"
@input="handleSearch"
>
</view>
<!-- 定位城市 -->
<!-- <view class="location-section">
<view class="section-title">定位城市</view>
<view class="location-city" :class="{ active: selectedCity.code === locationCity.code }" @click="selectCity(locationCity)">
{{ locationCity.name }}
</view>
</view> -->
<!-- 热门城市 -->
<!-- <view class="hot-section">
<view class="section-title">热门城市</view>
<view class="hot-cities">
<view
class="city-item"
:class="{ active: selectedCity.code === item.code }"
v-for="item in hotCities"
:key="item.code"
@click="selectCity(item)"
>
{{ item.name }}
</view>
</view>
</view> -->
<!-- 城市数量统计 -->
<!-- <view class="city-count" v-if="allCities.length > 0">
共找到 {{ allCities.length }} 个城市
</view> -->
<!-- 城市列表 -->
<view class="city-list-section">
<scroll-view
class="city-list-content"
scroll-y
:scroll-into-view="currentScrollId"
:scroll-with-animation="true"
>
<view
class="city-group"
v-for="group in cityGroups"
:key="group.letter"
:id="`city-group-${group.letter}`"
>
<view class="group-title">{{ group.letter }}</view>
<view
class="city-item"
:class="{ active: selectedCity.code === item.code }"
v-for="item in group.cities"
:key="item.code"
@click="selectCity(item)"
>
{{ item.name }}
</view>
</view>
</scroll-view>
<!-- 右侧字母索引 -->
<view class="letter-index">
<view
class="letter-item"
v-for="letter in letters"
:key="letter"
@click="scrollToLetter(letter)"
:class="{ active: currentLetter === letter }"
>
{{ letter }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed, inject } from 'vue';
const { $api, navTo } = inject('globalFunction');
// 搜索文本
const searchText = ref('');
// 定位城市
const locationCity = ref({ code: '110100', name: '北京' });
// 选中的城市
const selectedCity = ref({ code: '', name: '' });
// 当前显示的字母
const currentLetter = ref('');
// 当前滚动到的城市组ID
const currentScrollId = ref('');
// 城市数据
const allCities = ref([]);
// 字母列表
const letters = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']);
// 按字母分组的城市
const cityGroups = computed(() => {
const groups = {};
const filteredCities = allCities.value.filter(city => {
if (!searchText.value) return true;
return city.name.includes(searchText.value) || city.pinyin?.includes(searchText.value.toUpperCase());
});
console.log('过滤后用于分组的城市:', filteredCities);
// 初始化字母分组
letters.value.forEach(letter => {
groups[letter] = { letter, cities: [] };
});
// 将城市分配到对应字母组
filteredCities.forEach(city => {
const firstLetter = city.pinyin || '#';
// 如果字母不在预定义列表中,将其添加到分组中
if (!groups[firstLetter]) {
groups[firstLetter] = { letter: firstLetter, cities: [] };
}
groups[firstLetter].cities.push(city);
});
const result = Object.values(groups).filter(group => group.cities.length > 0);
// 对城市组进行排序
result.sort((a, b) => {
// '#' 符号应该排在最前面
if (a.letter === '#') return -1;
if (b.letter === '#') return 1;
// 其他字母按字母顺序排序
return a.letter.localeCompare(b.letter);
});
console.log('最终分组结果:', result);
return result;
});
// 获取城市数据
const getCityData = async () => {
try {
// 直接获取所有城市数据,新接口已经返回了所有层级的数据
const res = await $api.createRequest('/cms/dict/sysarea/listCity', {});
console.log('城市数据接口返回:', res);
if (res.code === 200 && res.data) {
console.log('原始城市数据:', res.data);
// 显示接口返回的所有城市数据
const filteredCities = res.data;
console.log('过滤后城市数据:', filteredCities);
// 直接使用后端返回的zm字段作为拼音首字母并转换为大写
allCities.value = filteredCities.map(city => ({
...city,
pinyin: (city.zm || '#').toUpperCase()
}));
console.log('使用后端zm字段的城市数据:', allCities.value);
// 按拼音首字母排序
allCities.value.sort((a, b) => {
// 首先按拼音首字母排序
if (a.pinyin !== b.pinyin) {
// '#' 符号应该排在最前面
if (a.pinyin === '#') return -1;
if (b.pinyin === '#') return 1;
return a.pinyin.localeCompare(b.pinyin);
}
// 首字母相同时,按城市名称排序
return a.name.localeCompare(b.name);
});
}
} catch (error) {
console.error('获取城市数据失败:', error);
}
};
// 获取拼音首字母(使用更准确的映射表)
const getPinyinFirstLetter = (name) => {
const firstChar = name.charAt(0);
// 查找对应的拼音首字母
for (const [letter, chars] of Object.entries(pinyinMap)) {
if (chars.includes(firstChar)) {
return letter;
}
}
// 如果没有找到,返回#
return '#';
};
// 处理搜索
const handleSearch = () => {
// 搜索逻辑已在cityGroups计算属性中处理
};
// 选择城市
const selectCity = (city) => {
selectedCity.value = city;
// 返回上一页并传递选择的城市
uni.navigateBack({
delta: 1,
success: () => {
// 发送事件通知首页选择了城市
uni.$emit('citySelected', city);
}
});
};
// 滚动到指定字母
const scrollToLetter = (letter) => {
currentLetter.value = letter;
// 更新滚动ID触发scroll-view滚动
currentScrollId.value = `city-group-${letter}`;
};
// 返回上一页
const navBack = () => {
uni.navigateBack({ delta: 1 });
};
// 组件挂载时获取城市数据
onMounted(() => {
getCityData();
});
</script>
<style scoped>
.city-select-container {
background-color: #f5f5f5;
min-height: 100vh;
padding-top: 16px;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background-color: #fff;
border-bottom: 1px solid #e5e5e5;
}
.nav-left,
.nav-right {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 18px;
font-weight: 500;
color: #333;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
margin: 12px 16px;
padding:8px 16px;
background-color: #fff;
border-radius: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-icon {
margin-right: 8px;
}
.search-input {
flex: 1;
font-size: 14px;
color: #333;
border: none;
outline: none;
}
/* 定位城市 */
.location-section {
padding: 16px;
background-color: #fff;
margin-bottom: 8px;
}
.section-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.location-city {
display: inline-block;
padding: 8px 20px;
background-color: #f0f9ff;
color: #007aff;
border-radius: 20px;
font-size: 14px;
}
.location-city.active {
background-color: #007aff;
color: #fff;
}
/* 热门城市 */
.hot-section {
padding: 16px;
background-color: #fff;
margin-bottom: 8px;
}
.city-count {
padding: 0 16px 12px;
background-color: #fff;
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.hot-cities {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.city-item {
padding: 8px 20px;
background-color: #f5f5f5;
color: #333;
border-radius: 20px;
font-size: 14px;
}
.city-item.active {
background-color: #007aff;
color: #fff;
}
/* 城市列表 */
.city-list-section {
display: flex;
background-color: #fff;
height: calc(100vh - 180px);
}
.city-list-content {
flex: 1;
overflow-y: auto;
}
.city-group {
padding: 0 16px;
}
.group-title {
font-size: 14px;
color: #666;
margin: 12px 0 8px 0;
padding-left: 4px;
}
.city-list-content .city-item {
display: block;
padding: 12px 4px;
margin-bottom: 0;
background-color: transparent;
border-radius: 0;
border-bottom: 1px solid #f0f0f0;
}
.city-list-content .city-item.active {
background-color: transparent;
color: #007aff;
}
/* 右侧字母索引 */
.letter-index {
width: 30px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.letter-item {
width: 24px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #666;
margin: 2px 0;
}
.letter-item.active {
color: #007aff;
font-weight: bold;
}
</style>

View File

@@ -250,10 +250,16 @@
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')"> <view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons> <uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text> <text>添加</text>
</view> </view>
<view class="jobs-add button-click" @click="navTo('/pages/city-select/index')" style="padding-right:0;">
<!-- <uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons> -->
<text>{{ selectedCity.name || '地区' }}</text>
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view> </view>
<view class="filter-bottom"> <view class="filter-bottom">
<view class="btm-left"> <view class="btm-left">
@@ -317,7 +323,7 @@
<view class="falls-card-company" v-show="isShowJw !== 3"> <view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }} {{ config.appInfo.areaName }}
<!-- {{ job.jobLocation }} --> <!-- {{ job.jobLocation }} -->
<dict-Label dictType="area" :value="job.jobLocationAreaCode"></dict-Label> <dict-Label dictType="jobLocationAreaCode" :value="job.jobLocationAreaCode"></dict-Label>
</view> </view>
<view class="falls-card-pepleNumber"> <view class="falls-card-pepleNumber">
<view> <view>
@@ -394,7 +400,7 @@
<view class="falls-card-company" v-show="isShowJw !== 3"> <view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }} {{ config.appInfo.areaName }}
<!-- {{ job.jobLocation }} --> <!-- {{ job.jobLocation }} -->
<dict-Label dictType="area" :value="job.jobLocationAreaCode"></dict-Label> <dict-Label dictType="jobLocationAreaCode" :value="job.jobLocationAreaCode"></dict-Label>
</view> </view>
<view class="falls-card-pepleNumber"> <view class="falls-card-pepleNumber">
<view> <view>
@@ -631,6 +637,8 @@ const inputText = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const selectFilterModel = ref(null); const selectFilterModel = ref(null);
const showModel = ref(false); const showModel = ref(false);
// 选中的城市
const selectedCity = ref({ code: '', name: '' });
const rangeOptions = ref([ const rangeOptions = ref([
{ value: 0, text: '推荐' }, { value: 0, text: '推荐' },
{ value: 1, text: '最热' }, { value: 1, text: '最热' },
@@ -723,6 +731,7 @@ onMounted(() => {
// 在组件挂载时绑定事件监听,确保只绑定一次 // 在组件挂载时绑定事件监听,确保只绑定一次
// 先移除可能存在的旧监听,避免重复绑定 // 先移除可能存在的旧监听,避免重复绑定
uni.$off('showLoginModal'); uni.$off('showLoginModal');
uni.$off('citySelected');
// 绑定新的监听 // 绑定新的监听
uni.$on('showLoginModal', () => { uni.$on('showLoginModal', () => {
@@ -731,6 +740,15 @@ onMounted(() => {
pageNull.value = 0; pageNull.value = 0;
}); });
// 监听城市选择事件
uni.$on('citySelected', (city) => {
console.log('收到citySelected事件选择的城市:', city);
selectedCity.value = city;
// 可以在这里添加根据城市筛选职位的逻辑
conditionSearch.value.jobLocationAreaCode = city.code;
getJobRecommend('refresh');
});
// 获取企业信息 // 获取企业信息
getCompanyInfo(); getCompanyInfo();
}); });
@@ -738,6 +756,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
// 组件销毁时移除事件监听 // 组件销毁时移除事件监听
uni.$off('showLoginModal'); uni.$off('showLoginModal');
uni.$off('citySelected');
}); });
onShow(() => { onShow(() => {
@@ -1728,6 +1747,9 @@ defineExpose({ loadData });
min-width: 80rpx; min-width: 80rpx;
padding: 8rpx 12rpx; padding: 8rpx 12rpx;
white-space: nowrap; white-space: nowrap;
.right-sx
width: 28rpx;
height: 28rpx;
.filter-bottom .filter-bottom
display: flex display: flex
justify-content: space-between justify-content: space-between

View File

@@ -107,102 +107,108 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
return await baseDB.db.add(massageName.value, payload); return await baseDB.db.add(massageName.value, payload);
} }
async function getStearm(text, fileUrls = [], progress, options = {}) { async function getStearm(text, fileUrls = [], progress, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
toggleTyping(true); toggleTyping(true);
const customDataID = 'message_' + UUID.generate() const customDataID = 'message_' + UUID.generate()
const params = {
data: text, // 对话历史管理只保留最近的N条消息防止token超限
sessionId: chatSessionID.value, // 计算消息数量只保留最近的10条消息可根据实际情况调整
dataId: customDataID const MAX_HISTORY_MESSAGES = 10;
}; const historyMessages = messages.value.slice(-MAX_HISTORY_MESSAGES);
if (fileUrls && fileUrls.length) {
params['fileUrl'] = fileUrls.map((item) => item.url); const params = {
} data: text,
// ------> sessionId: chatSessionID.value,
const MsgData = { dataId: customDataID
text: text, };
self: true, if (fileUrls && fileUrls.length) {
displayText: text, params['fileUrl'] = fileUrls.map((item) => item.url);
files: fileUrls }
}; // ------>
addMessage(MsgData); // 添加message数据 const MsgData = {
// <------ text: text,
const newMsg = { self: true,
text: '', // 存储原始结构化内容 displayText: text,
self: false, files: fileUrls
displayText: '', // 用于流式渲染展示 };
dataId: customDataID addMessage(MsgData); // 添加message数据
}; // <------
const index = messages.value.length; const newMsg = {
messages.value.push(newMsg); text: '', // 存储原始结构化内容
self: false,
const rawParts = Array.isArray(text) ? text : [text]; // 统一处理 displayText: '', // 用于流式渲染展示
dataId: customDataID
// 用于追加每个部分的流式数据 };
let partIndex = 0; const index = messages.value.length;
messages.value.push(newMsg);
function handleUnload() {
newMsg.parentGroupId = chatSessionID.value; const rawParts = Array.isArray(text) ? text : [text]; // 统一处理
baseDB.db.add(massageName.value, newMsg).then((id) => {
messages.value[index] = { // 用于追加每个部分的流式数据
...newMsg, let partIndex = 0;
id
}; function handleUnload() {
}); newMsg.parentGroupId = chatSessionID.value;
} baseDB.db.add(massageName.value, newMsg).then((id) => {
// #ifdef H5 messages.value[index] = {
if (typeof window !== 'undefined') { ...newMsg,
window.addEventListener("unload", handleUnload); id
} };
// #endif });
}
function onDataReceived(data) { // #ifdef H5
// 支持追加多个部分 if (typeof window !== 'undefined') {
newMsg.text += data; window.addEventListener("unload", handleUnload);
newMsg.displayText += data; }
messages.value[index] = { // #endif
...newMsg
}; function onDataReceived(data) {
progress && progress(); // 支持追加多个部分
newMsg.text += data;
// 调用外部传入的onDataReceived回调 newMsg.displayText += data;
if (options.onDataReceived) { messages.value[index] = {
options.onDataReceived(data, newMsg, index); ...newMsg
} };
} progress && progress();
function onError(error) { // 调用外部传入的onDataReceived回调
msg('服务响应异常'); if (options.onDataReceived) {
reject(error); options.onDataReceived(data, newMsg, index);
} }
}
function onComplete() {
messages.value[index] = { function onError(error) {
...newMsg msg('服务响应异常');
}; reject(error);
toggleTyping(false); }
// #ifdef H5
if (typeof window !== 'undefined') { function onComplete() {
window.removeEventListener("unload", handleUnload); messages.value[index] = {
} ...newMsg
// #endif };
handleUnload(); toggleTyping(false);
// 调用外部传入的onComplete回调 // #ifdef H5
if (options.onComplete) { if (typeof window !== 'undefined') {
options.onComplete(); window.removeEventListener("unload", handleUnload);
} }
resolve(); // #endif
} handleUnload();
// 调用外部传入的onComplete回调
$api.streamRequest('/chat', params, onDataReceived, onError, onComplete); if (options.onComplete) {
} catch (err) { options.onComplete();
console.log(err); }
reject(err); resolve();
} }
});
$api.streamRequest('/chat', params, onDataReceived, onError, onComplete);
} catch (err) {
console.log(err);
reject(err);
}
});
} }
// 状态控制 // 状态控制

View File

@@ -27,6 +27,7 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let buffer = ''; let buffer = '';
let hasReceivedContent = false;
const requestTask = uni.request({ const requestTask = uni.request({
url: config.StreamBaseURl + url, url: config.StreamBaseURl + url,
@@ -55,15 +56,33 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
try { try {
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8');
const chunk = decoder.decode(new Uint8Array(res.data)); const chunk = decoder.decode(new Uint8Array(res.data));
console.log('📦 收到分块数据:', chunk);
buffer += chunk; buffer += chunk;
let lines = buffer.split("\n"); let lines = buffer.split("\n");
buffer = lines.pop() || ''; // 保留不完整的行 buffer = lines.pop() || ''; // 保留不完整的行
console.log('📝 解析到行:', lines.length, '行,缓冲区剩余:', buffer.length, '字符');
for (let line of lines) { for (let line of lines) {
if (line.startsWith("data: ")) { console.log('🔍 处理行:', line);
const jsonData = line.slice(6).trim(); // 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的JSON数据:', jsonData);
if (jsonData === "[DONE]") { if (jsonData === "[DONE]") {
console.log('✅ 收到结束标记 [DONE]');
onComplete && onComplete(); onComplete && onComplete();
resolve(); resolve();
return; return;
@@ -72,11 +91,35 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
if (jsonData && jsonData.trim()) { if (jsonData && jsonData.trim()) {
try { try {
const parsedData = JSON.parse(jsonData); const parsedData = JSON.parse(jsonData);
console.log('🔧 解析后的JSON:', parsedData);
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
// 处理标准的choices格式 // 处理标准的choices格式
if (parsedData?.choices?.[0]?.delta?.content) { else if (parsedData?.choices?.[0]?.delta?.content) {
const content = parsedData.choices[0].delta.content; const content = parsedData.choices[0].delta.content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -84,6 +127,8 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) { else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content; const content = parsedData.choices[0].delta.reasoning_content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -91,14 +136,42 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
else if (parsedData?.tool?.response) { else if (parsedData?.tool?.response) {
const content = parsedData.tool.response; const content = parsedData.tool.response;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
onDataReceived && onDataReceived(content);
}
}
else {
console.warn('⚠️ 未匹配到任何内容格式:', parsedData);
}
} catch (e) { } catch (e) {
console.error("JSON 解析失败:", e.message); console.error("JSON 解析失败:", e.message, "原始数据:", jsonData);
} }
} }
} }
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的行:', processedLine);
}
} }
} catch (error) { } catch (error) {
console.error('处理分块数据失败:', error); console.error('处理分块数据失败:', error);
@@ -135,6 +208,7 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
let buffer = ""; let buffer = "";
let retryCount = 0; let retryCount = 0;
const maxRetries = 3; const maxRetries = 3;
let hasReceivedContent = false;
while (true) { while (true) {
const { const {
@@ -157,9 +231,25 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
console.log(`📦 Processing ${lines.length} lines, buffer length: ${buffer.length}`); console.log(`📦 Processing ${lines.length} lines, buffer length: ${buffer.length}`);
for (let line of lines) { for (let line of lines) {
if (line.startsWith("data: ")) { console.log('🔍 处理行:', line);
const jsonData = line.slice(6).trim(); // 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的JSON数据:', jsonData);
if (jsonData === "[DONE]") { if (jsonData === "[DONE]") {
console.log('✅ 收到结束标记 [DONE]');
onComplete && onComplete(); onComplete && onComplete();
resolve(); resolve();
return; return;
@@ -169,11 +259,35 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
// 检查JSON数据是否完整 // 检查JSON数据是否完整
if (jsonData && jsonData.trim() && jsonData !== "[DONE]") { if (jsonData && jsonData.trim() && jsonData !== "[DONE]") {
const parsedData = JSON.parse(jsonData); const parsedData = JSON.parse(jsonData);
console.log('🔧 解析后的JSON:', parsedData);
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
// 处理标准的choices格式 // 处理标准的choices格式
if (parsedData?.choices?.[0]?.delta?.content) { else if (parsedData?.choices?.[0]?.delta?.content) {
const content = parsedData.choices[0].delta.content; const content = parsedData.choices[0].delta.content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -181,6 +295,8 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) { else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content; const content = parsedData.choices[0].delta.reasoning_content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -188,6 +304,27 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
else if (parsedData?.tool?.response) { else if (parsedData?.tool?.response) {
const content = parsedData.tool.response; const content = parsedData.tool.response;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -201,6 +338,10 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
// 不抛出错误,继续处理下一个数据块 // 不抛出错误,继续处理下一个数据块
} }
} }
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的行:', processedLine);
}
} }
} }
@@ -209,16 +350,55 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
console.log("📦 Processing remaining buffer:", buffer.substring(0, 100) + "..."); console.log("📦 Processing remaining buffer:", buffer.substring(0, 100) + "...");
const lines = buffer.split("\n"); const lines = buffer.split("\n");
for (let line of lines) { for (let line of lines) {
if (line.startsWith("data: ")) { console.log('🔍 处理剩余缓冲区行:', line);
const jsonData = line.slice(6).trim(); // 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的剩余JSON数据:', jsonData);
if (jsonData && jsonData !== "[DONE]") { if (jsonData && jsonData !== "[DONE]") {
try { try {
const parsedData = JSON.parse(jsonData); const parsedData = JSON.parse(jsonData);
console.log('🔧 解析后的剩余JSON:', parsedData);
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
// 处理标准的choices格式 // 处理标准的choices格式
if (parsedData?.choices?.[0]?.delta?.content) { else if (parsedData?.choices?.[0]?.delta?.content) {
const content = parsedData.choices[0].delta.content; const content = parsedData.choices[0].delta.content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -226,6 +406,8 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) { else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content; const content = parsedData.choices[0].delta.reasoning_content;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -233,6 +415,27 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
else if (parsedData?.tool?.response) { else if (parsedData?.tool?.response) {
const content = parsedData.tool.response; const content = parsedData.tool.response;
if (content) { if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
onDataReceived && onDataReceived(content); onDataReceived && onDataReceived(content);
} }
} }
@@ -241,6 +444,10 @@ function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
} }
} }
} }
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的剩余行:', processedLine);
}
} }
} }
@@ -295,16 +502,27 @@ export function chatRequest(url, data = {}, method = 'GET', loading = false, hea
return return
} }
uni.showToast({ uni.showToast({
title: msg, title: msg || '请求失败',
icon: 'none' icon: 'none'
}) })
// 拒绝Promise并提供详细错误信息
const err = new Error(msg || '请求失败,服务器返回错误码: ' + code)
err.error = resData
reject(err)
} else {
// 处理非200状态码
const errorMsg = `请求失败HTTP状态码: ${resData.statusCode}`
uni.showToast({
title: errorMsg,
icon: 'none'
})
const err = new Error(errorMsg)
err.error = resData
reject(err)
} }
if (resData.data?.code === 401 || resData.data?.code === 402) { if (resData.data?.code === 401 || resData.data?.code === 402) {
useUserStore().logOut() useUserStore().logOut()
} }
const err = new Error('请求出现异常,请联系工作人员')
err.error = resData
reject(err)
}, },
fail: (err) => { fail: (err) => {
reject(err) reject(err)