diff --git a/pages/wiki/chat/index.js b/pages/wiki/chat/index.js
index dab7c83..db3448b 100644
--- a/pages/wiki/chat/index.js
+++ b/pages/wiki/chat/index.js
@@ -1,4 +1,5 @@
// pages/wiki/chat/index.js
+import request from '../../../utils/request';
Page({
data: {
@@ -7,6 +8,8 @@ Page({
isTyping: false,
scrollAnchor: '',
_counter: 0,
+ quotaRemaining: -1,
+ quotaLimit: 0,
},
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() {
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
},
@@ -42,11 +58,16 @@ Page({
const query = this.data.inputValue.trim();
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 aid = 'a' + (++this.data._counter);
const len = this.data.messages.length;
- // Push user msg + empty AI msg at once
this.setData({
[`messages[${len}]`]: { id: uid, role: 'user', content: query },
[`messages[${len + 1}]`]: { id: aid, role: 'ai', content: '' },
@@ -59,53 +80,58 @@ Page({
},
_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 = '';
- const task = wx.request({
- url,
- method: 'GET',
- enableChunked: true,
- header: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': 'text/event-stream',
- },
- success: () => {
- this.setData({ isTyping: false });
+ request.stream('/plant/chat/stream', { query }, {
+ onChunk: (res) => {
+ 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');
+
+ for (const line of lines) {
+ if (!line.startsWith('data: ')) continue;
+ const chunk = line.substring(6);
+
+ if (chunk === '[DONE]') {
+ this.setData({ isTyping: false });
+ return;
+ }
+ if (chunk.startsWith('[ERROR]')) {
+ fullText += '\n⚠️ ' + (chunk.substring(7) || '服务异常');
+ this._updateAiMsg(aiMsgId, fullText);
+ this.setData({ isTyping: false });
+ return;
+ }
+
+ fullText += chunk;
+ this._updateAiMsg(aiMsgId, fullText);
+ }
this.scrollToBottom();
},
- fail: () => {
+ 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 });
},
});
-
- task.onChunkReceived((res) => {
- const text = this._decode(res.data);
- const lines = text.split('\n');
-
- for (const line of lines) {
- if (!line.startsWith('data: ')) continue;
- const chunk = line.substring(6);
-
- if (chunk === '[DONE]') {
- this.setData({ isTyping: false });
- return;
- }
- if (chunk.startsWith('[ERROR]')) {
- fullText += '\n⚠️ ' + (chunk.substring(7) || '服务异常');
- this._updateAiMsg(aiMsgId, fullText);
- this.setData({ isTyping: false });
- return;
- }
-
- fullText += chunk;
- this._updateAiMsg(aiMsgId, fullText);
- }
- this.scrollToBottom();
- });
},
_updateAiMsg(id, content) {
diff --git a/pages/wiki/chat/index.wxml b/pages/wiki/chat/index.wxml
index 052cd17..9815b07 100644
--- a/pages/wiki/chat/index.wxml
+++ b/pages/wiki/chat/index.wxml
@@ -85,6 +85,9 @@
+
+ 今日剩余 {{quotaRemaining}}/{{quotaLimit}} 次
+
diff --git a/pages/wiki/chat/index.wxss b/pages/wiki/chat/index.wxss
index 6f744b4..13f5bb5 100644
--- a/pages/wiki/chat/index.wxss
+++ b/pages/wiki/chat/index.wxss
@@ -263,3 +263,20 @@
.safe-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;
+}
diff --git a/utils/request.js b/utils/request.js
index ec4f0e2..c085f83 100644
--- a/utils/request.js
+++ b/utils/request.js
@@ -266,12 +266,62 @@ class WxRequest {
post(url, 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
const request = new WxRequest({
- baseUrl: 'http://192.168.0.184:8889',
- //baseUrl: 'https://go.sundynix.cn/api',
+ //baseUrl: 'http://192.168.0.184:8889',
+ baseUrl: 'https://go.sundynix.cn/api',
header: {
'Content-Type': 'application/json'
}