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:
john
2026-07-11 13:01:05 +08:00
parent 04ef1c9105
commit c842119929
16 changed files with 829 additions and 72 deletions
+104 -20
View File
@@ -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 {
+45 -8
View File
@@ -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>
+108 -7
View File
@@ -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;