Files
memind/miniapp/pages/chat/index.js
john 048f25f580 feat(miniapp): add privacy consent and legal pages
Show privacy popup on launch, gate login on legal consent, and link
user agreement and privacy policy pages in the mini program.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 12:18:54 +08:00

257 lines
7.2 KiB
JavaScript

const {
checkAuth,
createAgentRun,
getAgentRun,
getMe,
loadSession,
logout
} = require('../../utils/api');
const {
AGENT_RUN_MAX_POLLS,
AGENT_RUN_POLL_MS,
SELECTED_SESSION_KEY,
API_BASE_URL
} = require('../../utils/config');
const { createUserMessage, normalizeMessages } = require('../../utils/messages');
const { buildMessageView, resolveWorkspacePublicUrl } = require('../../utils/message-display');
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Page({
data: {
sessionId: '',
messages: [],
draft: '',
loading: false,
error: '',
scrollIntoView: ''
},
async onShow() {
if (this._authCheckPromise) {
await this._authCheckPromise;
return;
}
this._authCheckPromise = this.verifyAccess();
try {
await this._authCheckPromise;
} finally {
this._authCheckPromise = null;
}
},
async verifyAccess() {
if (this._redirectingToLogin) return;
try {
const status = await checkAuth();
if (!status?.authenticated) {
this._redirectingToLogin = true;
wx.redirectTo({
url: '/pages/login/index',
complete: () => {
this._redirectingToLogin = false;
},
});
return;
}
await this.ensureCurrentUser();
const selectedSessionId = wx.getStorageSync(SELECTED_SESSION_KEY);
if (selectedSessionId) {
wx.removeStorageSync(SELECTED_SESSION_KEY);
if (selectedSessionId !== this.data.sessionId) {
await this.openSession(selectedSessionId);
}
} else if (this.data.sessionId && this.shouldRefreshRenderedMessages()) {
await this.refreshSession(this.data.sessionId);
}
} catch {
if (this._redirectingToLogin) return;
this._redirectingToLogin = true;
wx.redirectTo({
url: '/pages/login/index',
complete: () => {
this._redirectingToLogin = false;
},
});
}
},
getMessageRenderOptions() {
const app = getApp();
const user = app?.globalData?.user || {};
return {
publishKey: user.id || user.publishSlug || '',
apiBaseUrl: API_BASE_URL,
};
},
shouldRefreshRenderedMessages() {
return this.data.messages.some(
(item) =>
!Array.isArray(item.parts) ||
(/\bpublic\/[A-Za-z0-9._-]+\.html\b/i.test(item.text || '') &&
(!item.pageLinks || item.pageLinks.length === 0))
);
},
async ensureCurrentUser() {
const app = getApp();
if (app?.globalData?.user?.id) return app.globalData.user;
const result = await getMe();
if (result?.user) {
app.globalData.user = result.user;
}
return result?.user || null;
},
handleInput(event) {
this.setData({ draft: event.detail.value });
},
scrollToBottom() {
const index = Math.max(0, this.data.messages.length - 1);
this.setData({ scrollIntoView: `msg-${index}` });
},
async sendMessage() {
const text = this.data.draft.trim();
if (!text || this.data.loading) return;
const userMessage = createUserMessage(text);
const userView = buildMessageView(userMessage, this.getMessageRenderOptions());
const nextMessages = [...this.data.messages, userView];
this.setData({ messages: nextMessages, draft: '', loading: true, error: '' });
this.scrollToBottom();
try {
const payload = {
session_id: this.data.sessionId || undefined,
user_message: userMessage
};
console.info('[TKMind miniapp] createAgentRun start', {
sessionId: payload.session_id || null
});
const run = await createAgentRun(payload);
console.info('[TKMind miniapp] agent run created', run?.id, run?.status);
await this.waitForRun(run);
} catch (error) {
console.error('[TKMind miniapp] send failed', error);
this.setData({ error: error?.message || '发送失败' });
wx.showToast({ title: error?.message || '发送失败', icon: 'none' });
} finally {
this.setData({ loading: false });
this.scrollToBottom();
}
},
async waitForRun(run) {
const runId = run?.id;
if (!runId) throw new Error('后台任务未返回 runId');
let sessionId = run?.sessionId || this.data.sessionId;
for (let i = 0; i < AGENT_RUN_MAX_POLLS; i += 1) {
const latest = i === 0 ? run : await getAgentRun(runId);
if (latest?.sessionId) {
sessionId = latest.sessionId;
if (sessionId !== this.data.sessionId) {
this.setData({ sessionId });
}
}
if (latest?.status === 'succeeded') {
if (sessionId) {
await this.refreshSession(sessionId);
} else {
throw new Error('任务已完成,但未返回会话 ID');
}
return;
}
if (latest?.status === 'failed') {
throw new Error(latest.error || '后台任务失败');
}
await delay(AGENT_RUN_POLL_MS);
}
throw new Error('任务耗时较长,请稍后在会话中查看');
},
async refreshSession(sessionId) {
const detail = await loadSession(sessionId);
const conversation =
detail?.conversation ||
detail?.messages ||
detail?.data?.conversation ||
detail?.data?.messages ||
[];
const visible = conversation.filter(
(message) => message?.metadata?.userVisible !== false
);
const messages = normalizeMessages(visible, this.getMessageRenderOptions());
this.setData({ sessionId, messages, error: '' });
},
async openSession(sessionId) {
this.setData({ loading: true, error: '' });
try {
await this.refreshSession(sessionId);
} catch (error) {
this.setData({ error: error?.message || '会话加载失败' });
} finally {
this.setData({ loading: false });
this.scrollToBottom();
}
},
startNewChat() {
wx.removeStorageSync(SELECTED_SESSION_KEY);
this.setData({ sessionId: '', messages: [], draft: '', error: '' });
},
openLegalCenter() {
wx.navigateTo({ url: '/pages/legal/index/index' });
},
openLink(event) {
let url = String(event.currentTarget.dataset.url || '').trim();
if (!url) {
wx.showToast({ title: '链接无效', icon: 'none' });
return;
}
const { publishKey, apiBaseUrl } = this.getMessageRenderOptions();
if (/^public\//i.test(url)) {
const resolved = resolveWorkspacePublicUrl(url, publishKey, apiBaseUrl);
if (resolved) url = resolved;
} else if (url.startsWith('/')) {
url = `${String(apiBaseUrl).replace(/\/$/, '')}${url}`;
}
wx.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(url)}`
});
},
copyMessage(event) {
const text = String(event.currentTarget.dataset.text || '').trim();
if (!text) return;
wx.setClipboardData({
data: text,
success() {
wx.showToast({ title: '已复制', icon: 'success' });
},
fail() {
wx.showToast({ title: '复制失败', icon: 'none' });
}
});
},
async handleLogout() {
this.setData({ loading: true, error: '' });
try {
await logout();
wx.redirectTo({ url: '/pages/login/index' });
} catch (error) {
this.setData({ error: error?.message || '退出失败' });
} finally {
this.setData({ loading: false });
}
}
});