fix(miniapp): add missing uuid and message-display utils
Commit the utility modules required by api.js, messages.js, and chat page
so WeChat DevTools can resolve require('./uuid') at runtime.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+104
-20
@@ -2,15 +2,18 @@ const {
|
||||
checkAuth,
|
||||
createAgentRun,
|
||||
getAgentRun,
|
||||
getMe,
|
||||
loadSession,
|
||||
logout
|
||||
} = require('../../utils/api');
|
||||
const {
|
||||
AGENT_RUN_MAX_POLLS,
|
||||
AGENT_RUN_POLL_MS,
|
||||
SELECTED_SESSION_KEY
|
||||
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));
|
||||
@@ -33,18 +36,49 @@ Page({
|
||||
wx.redirectTo({ url: '/pages/login/index' });
|
||||
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 {
|
||||
wx.redirectTo({ url: '/pages/login/index' });
|
||||
}
|
||||
},
|
||||
|
||||
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 });
|
||||
},
|
||||
@@ -59,22 +93,24 @@ Page({
|
||||
if (!text || this.data.loading) return;
|
||||
|
||||
const userMessage = createUserMessage(text);
|
||||
const nextMessages = [
|
||||
...this.data.messages,
|
||||
{ id: userMessage.id, role: 'user', 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 || null,
|
||||
user_message: userMessage,
|
||||
source: 'wechat-miniapp'
|
||||
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);
|
||||
await this.waitForRun(run?.id || run?.runId);
|
||||
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 {
|
||||
@@ -83,21 +119,28 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async waitForRun(runId) {
|
||||
async waitForRun(run) {
|
||||
const runId = run?.id;
|
||||
if (!runId) throw new Error('后台任务未返回 runId');
|
||||
let sessionId = this.data.sessionId;
|
||||
let sessionId = run?.sessionId || this.data.sessionId;
|
||||
for (let i = 0; i < AGENT_RUN_MAX_POLLS; i += 1) {
|
||||
const run = await getAgentRun(runId);
|
||||
if (run?.sessionId && run.sessionId !== sessionId) {
|
||||
sessionId = run.sessionId;
|
||||
this.setData({ sessionId });
|
||||
const latest = i === 0 ? run : await getAgentRun(runId);
|
||||
if (latest?.sessionId) {
|
||||
sessionId = latest.sessionId;
|
||||
if (sessionId !== this.data.sessionId) {
|
||||
this.setData({ sessionId });
|
||||
}
|
||||
}
|
||||
if (run?.status === 'succeeded') {
|
||||
if (sessionId) await this.refreshSession(sessionId);
|
||||
if (latest?.status === 'succeeded') {
|
||||
if (sessionId) {
|
||||
await this.refreshSession(sessionId);
|
||||
} else {
|
||||
throw new Error('任务已完成,但未返回会话 ID');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (run?.status === 'failed') {
|
||||
throw new Error(run.error || '后台任务失败');
|
||||
if (latest?.status === 'failed') {
|
||||
throw new Error(latest.error || '后台任务失败');
|
||||
}
|
||||
await delay(AGENT_RUN_POLL_MS);
|
||||
}
|
||||
@@ -106,7 +149,16 @@ Page({
|
||||
|
||||
async refreshSession(sessionId) {
|
||||
const detail = await loadSession(sessionId);
|
||||
const messages = normalizeMessages(detail?.messages || detail?.data?.messages || []);
|
||||
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: '' });
|
||||
},
|
||||
|
||||
@@ -127,6 +179,38 @@ Page({
|
||||
this.setData({ sessionId: '', messages: [], draft: '', error: '' });
|
||||
},
|
||||
|
||||
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 {
|
||||
|
||||
@@ -14,23 +14,60 @@
|
||||
<view wx:if="{{messages.length === 0 && !loading}}" class="empty-state">
|
||||
<text>发一条消息开始使用 TKMind。</text>
|
||||
</view>
|
||||
<view wx:for="{{messages}}" wx:key="id" id="msg-{{index}}" class="message {{item.role}}">
|
||||
<text>{{item.text}}</text>
|
||||
<view wx:for="{{messages}}" wx:key="id" id="msg-{{index}}" class="message-row {{item.role}}">
|
||||
<view class="message {{item.role}}">
|
||||
<view class="message-body">
|
||||
<block wx:for="{{item.parts}}" wx:for-item="part" wx:for-index="partIndex" wx:key="partIndex">
|
||||
<text wx:if="{{part.type === 'text'}}" class="message-text">{{part.text}}</text>
|
||||
<text
|
||||
wx:elif="{{part.type === 'link'}}"
|
||||
class="message-link"
|
||||
bindtap="openLink"
|
||||
data-url="{{part.url}}"
|
||||
>{{part.text}}</text>
|
||||
</block>
|
||||
</view>
|
||||
<view wx:if="{{item.pageLinks.length}}" class="page-links">
|
||||
<view
|
||||
wx:for="{{item.pageLinks}}"
|
||||
wx:for-item="page"
|
||||
wx:key="publicUrl"
|
||||
class="page-link-card"
|
||||
bindtap="openLink"
|
||||
data-url="{{page.publicUrl}}"
|
||||
>
|
||||
<text class="page-link-title">{{page.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="message-copy"
|
||||
bindtap="copyMessage"
|
||||
data-text="{{item.copyText}}"
|
||||
aria-role="button"
|
||||
aria-label="复制"
|
||||
>
|
||||
<view class="copy-icon"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{loading}}" class="message assistant">
|
||||
<text>正在思考...</text>
|
||||
<view wx:if="{{loading}}" class="message-row assistant">
|
||||
<view class="message assistant">
|
||||
<text class="message-text">正在思考...</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="composer panel">
|
||||
<textarea
|
||||
<input
|
||||
class="input"
|
||||
value="{{draft}}"
|
||||
placeholder="输入你的问题"
|
||||
auto-height
|
||||
placeholder="输入你的问题,回车发送"
|
||||
maxlength="2000"
|
||||
confirm-type="send"
|
||||
disabled="{{loading}}"
|
||||
bindinput="handleInput"
|
||||
bindconfirm="sendMessage"
|
||||
/>
|
||||
<button class="send" loading="{{loading}}" bindtap="sendMessage">发送</button>
|
||||
<button class="send" loading="{{loading}}" disabled="{{loading}}" bindtap="sendMessage">发送</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -62,29 +62,131 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.message-row.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 84%;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 29rpx;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: auto;
|
||||
background: #0f766e;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
margin-right: auto;
|
||||
background: #ffffff;
|
||||
color: #132028;
|
||||
border: 1rpx solid #dbe3eb;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-link {
|
||||
color: #0ea5e9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message.user .message-link {
|
||||
color: #d1fae5;
|
||||
}
|
||||
|
||||
.page-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.page-link-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #0f766e;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.message.user .page-link-card {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.page-link-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.user .page-link-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.message-copy {
|
||||
flex: 0 0 auto;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10rpx;
|
||||
background: #f8fafc;
|
||||
border: 1rpx solid #e2e8f0;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
position: relative;
|
||||
width: 22rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.copy-icon::before,
|
||||
.copy-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 2rpx solid #64748b;
|
||||
border-radius: 3rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.copy-icon::before {
|
||||
top: 0;
|
||||
left: 6rpx;
|
||||
width: 16rpx;
|
||||
height: 18rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.copy-icon::after {
|
||||
top: 6rpx;
|
||||
left: 0;
|
||||
width: 16rpx;
|
||||
height: 18rpx;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -98,9 +200,8 @@
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-height: 72rpx;
|
||||
max-height: 220rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f8fafc;
|
||||
box-sizing: border-box;
|
||||
|
||||
Reference in New Issue
Block a user