feat: 限制用户单日提问次数
This commit is contained in:
+49
-23
@@ -1,4 +1,5 @@
|
|||||||
// pages/wiki/chat/index.js
|
// pages/wiki/chat/index.js
|
||||||
|
import request from '../../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -7,6 +8,8 @@ Page({
|
|||||||
isTyping: false,
|
isTyping: false,
|
||||||
scrollAnchor: '',
|
scrollAnchor: '',
|
||||||
_counter: 0,
|
_counter: 0,
|
||||||
|
quotaRemaining: -1,
|
||||||
|
quotaLimit: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
@@ -25,6 +28,19 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this._fetchQuota();
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchQuota() {
|
||||||
|
request.get('/plant/chat/quota').then(res => {
|
||||||
|
this.setData({
|
||||||
|
quotaRemaining: res.remaining,
|
||||||
|
quotaLimit: res.limit,
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
goToHistory() {
|
goToHistory() {
|
||||||
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
|
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
|
||||||
},
|
},
|
||||||
@@ -42,11 +58,16 @@ Page({
|
|||||||
const query = this.data.inputValue.trim();
|
const query = this.data.inputValue.trim();
|
||||||
if (!query || this.data.isTyping) return;
|
if (!query || this.data.isTyping) return;
|
||||||
|
|
||||||
|
// 额度检查
|
||||||
|
if (this.data.quotaRemaining === 0) {
|
||||||
|
wx.showToast({ title: '今日问答次数已用完,明天再来吧', icon: 'none', duration: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const uid = 'u' + (++this.data._counter);
|
const uid = 'u' + (++this.data._counter);
|
||||||
const aid = 'a' + (++this.data._counter);
|
const aid = 'a' + (++this.data._counter);
|
||||||
const len = this.data.messages.length;
|
const len = this.data.messages.length;
|
||||||
|
|
||||||
// Push user msg + empty AI msg at once
|
|
||||||
this.setData({
|
this.setData({
|
||||||
[`messages[${len}]`]: { id: uid, role: 'user', content: query },
|
[`messages[${len}]`]: { id: uid, role: 'user', content: query },
|
||||||
[`messages[${len + 1}]`]: { id: aid, role: 'ai', content: '' },
|
[`messages[${len + 1}]`]: { id: aid, role: 'ai', content: '' },
|
||||||
@@ -59,31 +80,25 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_streamRequest(query, aiMsgId) {
|
_streamRequest(query, aiMsgId) {
|
||||||
const token = wx.getStorageSync('token');
|
|
||||||
const baseUrl = 'http://192.168.0.184:8889';
|
|
||||||
const url = `${baseUrl}/plant/chat/stream?query=${encodeURIComponent(query)}`;
|
|
||||||
let fullText = '';
|
let fullText = '';
|
||||||
|
|
||||||
const task = wx.request({
|
request.stream('/plant/chat/stream', { query }, {
|
||||||
url,
|
onChunk: (res) => {
|
||||||
method: 'GET',
|
|
||||||
enableChunked: true,
|
|
||||||
header: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Accept': 'text/event-stream',
|
|
||||||
},
|
|
||||||
success: () => {
|
|
||||||
this.setData({ isTyping: false });
|
|
||||||
this.scrollToBottom();
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
this._updateAiMsg(aiMsgId, '网络连接失败,请稍后重试');
|
|
||||||
this.setData({ isTyping: false });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
task.onChunkReceived((res) => {
|
|
||||||
const text = this._decode(res.data);
|
const text = this._decode(res.data);
|
||||||
|
|
||||||
|
// Detect non-SSE JSON error (e.g. quota exceeded returns {code:7, msg:"..."})
|
||||||
|
if (!text.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.code && json.code !== 200 && json.msg) {
|
||||||
|
this._updateAiMsg(aiMsgId, '⚠️ ' + json.msg);
|
||||||
|
this.setData({ isTyping: false });
|
||||||
|
this._fetchQuota();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) { /* not JSON, continue SSE parsing */ }
|
||||||
|
}
|
||||||
|
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -105,6 +120,17 @@ Page({
|
|||||||
this._updateAiMsg(aiMsgId, fullText);
|
this._updateAiMsg(aiMsgId, fullText);
|
||||||
}
|
}
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
},
|
||||||
|
onDone: () => {
|
||||||
|
this.setData({ isTyping: false });
|
||||||
|
this.scrollToBottom();
|
||||||
|
// Delay: backend saves history async (go func) after stream ends
|
||||||
|
setTimeout(() => this._fetchQuota(), 800);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
this._updateAiMsg(aiMsgId, '网络连接失败,请稍后重试');
|
||||||
|
this.setData({ isTyping: false });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,9 @@
|
|||||||
<t-icon name="arrow-up" size="36rpx" color="#fff" />
|
<t-icon name="arrow-up" size="36rpx" color="#fff" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="quota-bar" wx:if="{{quotaLimit > 0}}">
|
||||||
|
<text class="quota-text {{quotaRemaining <= 2 ? 'warn' : ''}}">今日剩余 {{quotaRemaining}}/{{quotaLimit}} 次</text>
|
||||||
|
</view>
|
||||||
<view class="safe-bottom"></view>
|
<view class="safe-bottom"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -263,3 +263,20 @@
|
|||||||
.safe-bottom {
|
.safe-bottom {
|
||||||
height: calc(14rpx + env(safe-area-inset-bottom));
|
height: calc(14rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Quota Bar */
|
||||||
|
.quota-bar {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6rpx 0 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #90A4AE;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-text.warn {
|
||||||
|
color: #EF4444;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
+52
-2
@@ -266,12 +266,62 @@ class WxRequest {
|
|||||||
post(url, data = {}, header = {}) {
|
post(url, data = {}, header = {}) {
|
||||||
return this.request({ url, method: 'POST', data, header });
|
return this.request({ url, method: 'POST', data, header });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream request with chunked transfer.
|
||||||
|
* Reuses baseUrl + token interceptor.
|
||||||
|
* @param {string} url API path, e.g. '/plant/chat/stream'
|
||||||
|
* @param {Object} data Query params (GET) or body (POST)
|
||||||
|
* @param {Object} callbacks { onChunk, onDone, onError }
|
||||||
|
* @returns {Object} wx.request task (call task.abort() to cancel)
|
||||||
|
*/
|
||||||
|
stream(url, data = {}, callbacks = {}) {
|
||||||
|
const fullUrl = url.startsWith('http')
|
||||||
|
? url
|
||||||
|
: this.baseUrl + url;
|
||||||
|
|
||||||
|
// Build query string for GET
|
||||||
|
const qs = Object.keys(data)
|
||||||
|
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
|
||||||
|
.join('&');
|
||||||
|
const reqUrl = qs ? `${fullUrl}?${qs}` : fullUrl;
|
||||||
|
|
||||||
|
// Resolve token via same logic as interceptor
|
||||||
|
let token = wx.getStorageSync('token');
|
||||||
|
|
||||||
|
const mergedHeader = {
|
||||||
|
...this.header,
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
mergedHeader['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = wx.request({
|
||||||
|
url: reqUrl,
|
||||||
|
method: 'GET',
|
||||||
|
enableChunked: true,
|
||||||
|
header: mergedHeader,
|
||||||
|
success: () => {
|
||||||
|
if (callbacks.onDone) callbacks.onDone();
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (callbacks.onError) callbacks.onError(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (callbacks.onChunk) {
|
||||||
|
task.onChunkReceived(callbacks.onChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with default instance
|
// Initialize with default instance
|
||||||
const request = new WxRequest({
|
const request = new WxRequest({
|
||||||
baseUrl: 'http://192.168.0.184:8889',
|
//baseUrl: 'http://192.168.0.184:8889',
|
||||||
//baseUrl: 'https://go.sundynix.cn/api',
|
baseUrl: 'https://go.sundynix.cn/api',
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user