feat: 百科rag

This commit is contained in:
Blizzard
2026-04-23 11:13:23 +08:00
parent 40f3a8cfa8
commit 9fe2fd42e0
23 changed files with 1129 additions and 69 deletions
+145
View File
@@ -0,0 +1,145 @@
// pages/wiki/chat/index.js
Page({
data: {
messages: [],
inputValue: '',
isTyping: false,
scrollAnchor: '',
_counter: 0,
},
onLoad(options) {
if (options && options.fromHistory === '1') {
const channel = this.getOpenerEventChannel();
channel.on('historyData', (data) => {
const msgs = [
{ id: 'h1', role: 'user', content: data.question },
{ id: 'h2', role: 'ai', content: this._cleanMd(data.answer) },
];
this.setData({ messages: msgs, _counter: 2 }, () => this.scrollToBottom());
});
} else if (options && options.prefillQuestion) {
const q = decodeURIComponent(options.prefillQuestion);
this.setData({ inputValue: q }, () => this.onSend());
}
},
goToHistory() {
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
},
onQuickAsk(e) {
const query = e.currentTarget.dataset.q;
this.setData({ inputValue: query }, () => this.onSend());
},
onInput(e) {
this.setData({ inputValue: e.detail.value });
},
onSend() {
const query = this.data.inputValue.trim();
if (!query || this.data.isTyping) 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: '' },
inputValue: '',
isTyping: true,
}, () => {
this.scrollToBottom();
this._streamRequest(query, aid);
});
},
_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 });
this.scrollToBottom();
},
fail: () => {
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) {
const idx = this.data.messages.findIndex(m => m.id === id);
if (idx !== -1) {
this.setData({ [`messages[${idx}].content`]: this._cleanMd(content) });
}
},
// Strip residual markdown symbols for clean display
_cleanMd(text) {
return text
.replace(/^#{1,6}\s*/gm, '') // ### headers
.replace(/\*\*(.+?)\*\*/g, '【$1】') // **bold** → 【bold】
.replace(/\*(.+?)\*/g, '$1') // *italic*
.replace(/^[\-\*]\s+/gm, '· ') // - list → · list
.replace(/^\d+\.\s+/gm, (m) => m) // keep numbered lists
.replace(/`([^`]+)`/g, '$1') // `code`
.replace(/^---+$/gm, '————') // --- → ————
.replace(/\n{3,}/g, '\n\n'); // collapse blank lines
},
_decode(buffer) {
try {
return new TextDecoder('utf-8').decode(new Uint8Array(buffer));
} catch (e) {
const bytes = new Uint8Array(buffer);
let s = '';
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
try { return decodeURIComponent(escape(s)); } catch (_) { return s; }
}
},
scrollToBottom() {
this.setData({ scrollAnchor: 'scroll-bottom' });
},
});