diff --git a/miniapp/.gitignore b/miniapp/.gitignore
new file mode 100644
index 0000000..8818d42
--- /dev/null
+++ b/miniapp/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+miniprogram_npm/
+project.private.config.json
+*.local
diff --git a/miniapp/README.md b/miniapp/README.md
new file mode 100644
index 0000000..fa299e1
--- /dev/null
+++ b/miniapp/README.md
@@ -0,0 +1,22 @@
+# TKMind WeChat Mini Program
+
+This directory contains the native WeChat Mini Program client. It is intentionally isolated from the H5/backend code so development can proceed without changing existing services.
+
+## Current MVP
+
+- Native WeChat login entry via `wx.login`.
+- H5 account/password login fallback through `/auth/login`.
+- Chat submission through `/api/agent/runs`.
+- Agent run polling through `/api/agent/runs/:runId`.
+- Session detail refresh through `/api/sessions/:sessionId`.
+- Session list through `/api/sessions`.
+- MindSpace page list through `/api/mindspace/v1/pages`.
+- Page preview through Mini Program `web-view`.
+
+## Backend Assumptions
+
+The H5 APIs are reused as-is. Native Mini Program login posts the `wx.login` code to `MINIAPP_LOGIN_PATH` in `utils/config.js`; the current default is `/auth/wechat-miniapp/login`. If that backend endpoint is not available yet, use the account/password login fallback during local development.
+
+## Isolation Rule
+
+Mini Program work should stay inside `miniapp/`. Do not modify backend, H5, Memory, MindSpace, or production release files for this MVP unless explicitly approved.
diff --git a/miniapp/app.js b/miniapp/app.js
new file mode 100644
index 0000000..1159c30
--- /dev/null
+++ b/miniapp/app.js
@@ -0,0 +1,16 @@
+const { getStoredSession, setApiBaseUrl } = require('./utils/api');
+const { API_BASE_URL } = require('./utils/config');
+
+App({
+ globalData: {
+ user: null,
+ },
+
+ onLaunch() {
+ setApiBaseUrl(API_BASE_URL);
+ const session = getStoredSession();
+ if (session?.user) {
+ this.globalData.user = session.user;
+ }
+ },
+});
diff --git a/miniapp/app.json b/miniapp/app.json
new file mode 100644
index 0000000..fe32a86
--- /dev/null
+++ b/miniapp/app.json
@@ -0,0 +1,36 @@
+{
+ "pages": [
+ "pages/chat/index",
+ "pages/login/index",
+ "pages/sessions/index",
+ "pages/space/index",
+ "pages/webview/index"
+ ],
+ "window": {
+ "navigationBarTitleText": "TKMind",
+ "navigationBarBackgroundColor": "#101820",
+ "navigationBarTextStyle": "white",
+ "backgroundColor": "#f5f7fb"
+ },
+ "tabBar": {
+ "color": "#64748b",
+ "selectedColor": "#0f766e",
+ "backgroundColor": "#ffffff",
+ "borderStyle": "black",
+ "list": [
+ {
+ "pagePath": "pages/chat/index",
+ "text": "聊天"
+ },
+ {
+ "pagePath": "pages/sessions/index",
+ "text": "会话"
+ },
+ {
+ "pagePath": "pages/space/index",
+ "text": "页面"
+ }
+ ]
+ },
+ "sitemapLocation": "sitemap.json"
+}
diff --git a/miniapp/app.wxss b/miniapp/app.wxss
new file mode 100644
index 0000000..33a1e0d
--- /dev/null
+++ b/miniapp/app.wxss
@@ -0,0 +1,45 @@
+page {
+ min-height: 100%;
+ background: #f5f7fb;
+ color: #132028;
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
+}
+
+.page {
+ min-height: 100vh;
+ box-sizing: border-box;
+ padding: 28rpx;
+}
+
+.panel {
+ background: #ffffff;
+ border: 1rpx solid #dbe3eb;
+ border-radius: 16rpx;
+ box-shadow: 0 10rpx 30rpx rgba(15, 23, 42, 0.06);
+}
+
+.muted {
+ color: #64748b;
+}
+
+.primary-button {
+ height: 88rpx;
+ border-radius: 12rpx;
+ background: #0f766e;
+ color: #ffffff;
+ font-weight: 700;
+ line-height: 88rpx;
+}
+
+.secondary-button {
+ height: 78rpx;
+ border-radius: 12rpx;
+ background: #e7f3f1;
+ color: #0f766e;
+ font-weight: 700;
+ line-height: 78rpx;
+}
+
+.danger-text {
+ color: #b42318;
+}
diff --git a/miniapp/pages/chat/index.js b/miniapp/pages/chat/index.js
new file mode 100644
index 0000000..825d821
--- /dev/null
+++ b/miniapp/pages/chat/index.js
@@ -0,0 +1,97 @@
+const {
+ checkAuth,
+ createAgentRun,
+ getAgentRun,
+ loadSession
+} = require('../../utils/api');
+const { AGENT_RUN_MAX_POLLS, AGENT_RUN_POLL_MS } = require('../../utils/config');
+const { createUserMessage, normalizeMessages } = require('../../utils/messages');
+
+function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+Page({
+ data: {
+ sessionId: '',
+ messages: [],
+ draft: '',
+ loading: false,
+ scrollIntoView: ''
+ },
+
+ async onShow() {
+ try {
+ const status = await checkAuth();
+ if (!status?.authenticated) {
+ wx.redirectTo({ url: '/pages/login/index' });
+ }
+ } catch {
+ wx.redirectTo({ url: '/pages/login/index' });
+ }
+ },
+
+ 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 nextMessages = [
+ ...this.data.messages,
+ { id: userMessage.id, role: 'user', text }
+ ];
+ this.setData({ messages: nextMessages, draft: '', loading: true });
+ this.scrollToBottom();
+
+ try {
+ const payload = {
+ session_id: this.data.sessionId || null,
+ user_message: userMessage,
+ source: 'wechat-miniapp'
+ };
+ const run = await createAgentRun(payload);
+ await this.waitForRun(run?.id || run?.runId);
+ } catch (error) {
+ wx.showToast({ title: error?.message || '发送失败', icon: 'none' });
+ } finally {
+ this.setData({ loading: false });
+ this.scrollToBottom();
+ }
+ },
+
+ async waitForRun(runId) {
+ if (!runId) throw new Error('后台任务未返回 runId');
+ let 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 });
+ }
+ if (run?.status === 'succeeded') {
+ if (sessionId) await this.refreshSession(sessionId);
+ return;
+ }
+ if (run?.status === 'failed') {
+ throw new Error(run.error || '后台任务失败');
+ }
+ await delay(AGENT_RUN_POLL_MS);
+ }
+ throw new Error('任务耗时较长,请稍后在会话中查看');
+ },
+
+ async refreshSession(sessionId) {
+ const detail = await loadSession(sessionId);
+ const messages = normalizeMessages(detail?.messages || detail?.data?.messages || []);
+ this.setData({ messages });
+ }
+});
diff --git a/miniapp/pages/chat/index.json b/miniapp/pages/chat/index.json
new file mode 100644
index 0000000..29fc0ed
--- /dev/null
+++ b/miniapp/pages/chat/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "TKMind 聊天"
+}
diff --git a/miniapp/pages/chat/index.wxml b/miniapp/pages/chat/index.wxml
new file mode 100644
index 0000000..db88ba3
--- /dev/null
+++ b/miniapp/pages/chat/index.wxml
@@ -0,0 +1,22 @@
+
+
+
+ {{item.text}}
+
+
+ 正在思考...
+
+
+
+
+
+
+
+
diff --git a/miniapp/pages/chat/index.wxss b/miniapp/pages/chat/index.wxss
new file mode 100644
index 0000000..baa3513
--- /dev/null
+++ b/miniapp/pages/chat/index.wxss
@@ -0,0 +1,68 @@
+.chat-page {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ padding: 0;
+}
+
+.messages {
+ flex: 1;
+ box-sizing: border-box;
+ padding: 28rpx;
+}
+
+.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;
+}
+
+.composer {
+ display: flex;
+ align-items: flex-end;
+ gap: 16rpx;
+ padding: 18rpx;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 0;
+ border-radius: 0;
+}
+
+.input {
+ flex: 1;
+ min-height: 72rpx;
+ max-height: 220rpx;
+ padding: 18rpx 20rpx;
+ border-radius: 12rpx;
+ background: #f8fafc;
+ box-sizing: border-box;
+ font-size: 28rpx;
+}
+
+.send {
+ width: 136rpx;
+ height: 72rpx;
+ line-height: 72rpx;
+ border-radius: 12rpx;
+ background: #0f766e;
+ color: #fff;
+ font-weight: 700;
+ font-size: 28rpx;
+}
diff --git a/miniapp/pages/login/index.js b/miniapp/pages/login/index.js
new file mode 100644
index 0000000..48dbb42
--- /dev/null
+++ b/miniapp/pages/login/index.js
@@ -0,0 +1,57 @@
+const {
+ loginWithPassword,
+ loginWithWechatCode
+} = require('../../utils/api');
+const { MINIAPP_LOGIN_PATH } = require('../../utils/config');
+
+Page({
+ data: {
+ username: '',
+ password: '',
+ error: '',
+ wechatLoading: false,
+ passwordLoading: false
+ },
+
+ handleUsername(event) {
+ this.setData({ username: event.detail.value, error: '' });
+ },
+
+ handlePassword(event) {
+ this.setData({ password: event.detail.value, error: '' });
+ },
+
+ async handleWechatLogin() {
+ this.setData({ wechatLoading: true, error: '' });
+ try {
+ await loginWithWechatCode(MINIAPP_LOGIN_PATH);
+ wx.switchTab({ url: '/pages/chat/index' });
+ } catch (error) {
+ this.setData({
+ error:
+ error?.message ||
+ '微信登录暂不可用。若后端尚未开放小程序登录接口,请先使用 H5 账号登录。'
+ });
+ } finally {
+ this.setData({ wechatLoading: false });
+ }
+ },
+
+ async handlePasswordLogin() {
+ const username = this.data.username.trim();
+ const password = this.data.password;
+ if (!username || !password) {
+ this.setData({ error: '请输入用户名和密码' });
+ return;
+ }
+ this.setData({ passwordLoading: true, error: '' });
+ try {
+ await loginWithPassword(username, password);
+ wx.switchTab({ url: '/pages/chat/index' });
+ } catch (error) {
+ this.setData({ error: error?.message || '登录失败,请重试' });
+ } finally {
+ this.setData({ passwordLoading: false });
+ }
+ }
+});
diff --git a/miniapp/pages/login/index.json b/miniapp/pages/login/index.json
new file mode 100644
index 0000000..261164b
--- /dev/null
+++ b/miniapp/pages/login/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "登录 TKMind"
+}
diff --git a/miniapp/pages/login/index.wxml b/miniapp/pages/login/index.wxml
new file mode 100644
index 0000000..5709503
--- /dev/null
+++ b/miniapp/pages/login/index.wxml
@@ -0,0 +1,22 @@
+
+
+ TKMind
+ 微信小程序工作台
+
+
+
+
+
+ 或使用 H5 账号
+
+
+
+
+
+ {{error}}
+
+
diff --git a/miniapp/pages/login/index.wxss b/miniapp/pages/login/index.wxss
new file mode 100644
index 0000000..31065d4
--- /dev/null
+++ b/miniapp/pages/login/index.wxss
@@ -0,0 +1,52 @@
+.login-page {
+ display: flex;
+ flex-direction: column;
+ gap: 36rpx;
+ padding-top: 96rpx;
+}
+
+.brand {
+ display: flex;
+ flex-direction: column;
+ gap: 12rpx;
+}
+
+.brand-title {
+ font-size: 68rpx;
+ font-weight: 800;
+ color: #101820;
+}
+
+.brand-subtitle {
+ font-size: 28rpx;
+ color: #5b6b7b;
+}
+
+.login-card {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ padding: 32rpx;
+}
+
+.divider {
+ display: flex;
+ justify-content: center;
+ color: #94a3b8;
+ font-size: 24rpx;
+}
+
+.field {
+ height: 84rpx;
+ padding: 0 24rpx;
+ border: 1rpx solid #ccd6e0;
+ border-radius: 12rpx;
+ background: #f8fafc;
+ box-sizing: border-box;
+}
+
+.error {
+ color: #b42318;
+ font-size: 24rpx;
+ line-height: 1.5;
+}
diff --git a/miniapp/pages/sessions/index.js b/miniapp/pages/sessions/index.js
new file mode 100644
index 0000000..aff2b27
--- /dev/null
+++ b/miniapp/pages/sessions/index.js
@@ -0,0 +1,45 @@
+const { listSessions } = require('../../utils/api');
+
+function formatTime(value) {
+ const timestamp = Number(value || 0);
+ if (!timestamp) return '';
+ const date = new Date(timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp);
+ return `${date.getMonth() + 1}/${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
+}
+
+Page({
+ data: {
+ sessions: [],
+ loading: false,
+ error: ''
+ },
+
+ onShow() {
+ this.loadSessions();
+ },
+
+ async loadSessions() {
+ this.setData({ loading: true, error: '' });
+ try {
+ const result = await listSessions({ limit: 30 });
+ const rows = result?.sessions || result?.data || result?.items || [];
+ this.setData({
+ sessions: rows.map((item) => ({
+ ...item,
+ id: item.id || item.sessionId,
+ updatedAtText: formatTime(item.updatedAt || item.updated_at || item.created || item.createdAt)
+ }))
+ });
+ } catch (error) {
+ this.setData({ error: error?.message || '会话加载失败' });
+ } finally {
+ this.setData({ loading: false });
+ }
+ },
+
+ openSession(event) {
+ const id = event.currentTarget.dataset.id;
+ if (!id) return;
+ wx.switchTab({ url: '/pages/chat/index' });
+ }
+});
diff --git a/miniapp/pages/sessions/index.json b/miniapp/pages/sessions/index.json
new file mode 100644
index 0000000..b8655af
--- /dev/null
+++ b/miniapp/pages/sessions/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "历史会话"
+}
diff --git a/miniapp/pages/sessions/index.wxml b/miniapp/pages/sessions/index.wxml
new file mode 100644
index 0000000..60a1f04
--- /dev/null
+++ b/miniapp/pages/sessions/index.wxml
@@ -0,0 +1,17 @@
+
+
+ 历史会话
+
+
+
+ {{error}}
+
+
+ 暂无会话
+
+
+
+ {{item.title || item.name || '未命名会话'}}
+ {{item.updatedAtText || ''}}
+
+
diff --git a/miniapp/pages/sessions/index.wxss b/miniapp/pages/sessions/index.wxss
new file mode 100644
index 0000000..b7d6994
--- /dev/null
+++ b/miniapp/pages/sessions/index.wxss
@@ -0,0 +1,50 @@
+.toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.title {
+ font-size: 42rpx;
+ font-weight: 800;
+}
+
+.reload {
+ width: 132rpx;
+ height: 64rpx;
+ line-height: 64rpx;
+ border-radius: 12rpx;
+ background: #e7f3f1;
+ color: #0f766e;
+ font-size: 26rpx;
+}
+
+.session {
+ display: flex;
+ flex-direction: column;
+ gap: 10rpx;
+ padding: 24rpx;
+ margin-bottom: 18rpx;
+}
+
+.session-title {
+ font-size: 30rpx;
+ font-weight: 700;
+}
+
+.session-meta {
+ color: #64748b;
+ font-size: 24rpx;
+}
+
+.empty {
+ padding: 40rpx;
+ text-align: center;
+ color: #64748b;
+}
+
+.error {
+ margin-bottom: 20rpx;
+ color: #b42318;
+}
diff --git a/miniapp/pages/space/index.js b/miniapp/pages/space/index.js
new file mode 100644
index 0000000..1eb66b4
--- /dev/null
+++ b/miniapp/pages/space/index.js
@@ -0,0 +1,37 @@
+const { listMindSpacePages } = require('../../utils/api');
+
+Page({
+ data: {
+ pages: [],
+ loading: false,
+ error: ''
+ },
+
+ onShow() {
+ this.loadPages();
+ },
+
+ async loadPages() {
+ this.setData({ loading: true, error: '' });
+ try {
+ const result = await listMindSpacePages({ limit: 30 });
+ const pages = result?.data || result?.pages || [];
+ this.setData({ pages });
+ } catch (error) {
+ this.setData({ error: error?.message || '页面加载失败' });
+ } finally {
+ this.setData({ loading: false });
+ }
+ },
+
+ openPage(event) {
+ const url = event.currentTarget.dataset.url;
+ if (!url) {
+ wx.showToast({ title: '该页面暂无可打开链接', icon: 'none' });
+ return;
+ }
+ wx.navigateTo({
+ url: `/pages/webview/index?url=${encodeURIComponent(url)}`
+ });
+ }
+});
diff --git a/miniapp/pages/space/index.json b/miniapp/pages/space/index.json
new file mode 100644
index 0000000..22dc5df
--- /dev/null
+++ b/miniapp/pages/space/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "MindSpace 页面"
+}
diff --git a/miniapp/pages/space/index.wxml b/miniapp/pages/space/index.wxml
new file mode 100644
index 0000000..7573611
--- /dev/null
+++ b/miniapp/pages/space/index.wxml
@@ -0,0 +1,17 @@
+
+
+ MindSpace
+
+
+
+ {{error}}
+
+
+ 暂无页面
+
+
+
+ {{item.title || '未命名页面'}}
+ {{item.status || item.visibility || ''}}
+
+
diff --git a/miniapp/pages/space/index.wxss b/miniapp/pages/space/index.wxss
new file mode 100644
index 0000000..43a32cc
--- /dev/null
+++ b/miniapp/pages/space/index.wxss
@@ -0,0 +1,50 @@
+.toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.title {
+ font-size: 42rpx;
+ font-weight: 800;
+}
+
+.reload {
+ width: 132rpx;
+ height: 64rpx;
+ line-height: 64rpx;
+ border-radius: 12rpx;
+ background: #e7f3f1;
+ color: #0f766e;
+ font-size: 26rpx;
+}
+
+.page-card {
+ display: flex;
+ flex-direction: column;
+ gap: 10rpx;
+ padding: 24rpx;
+ margin-bottom: 18rpx;
+}
+
+.page-title {
+ font-size: 30rpx;
+ font-weight: 700;
+}
+
+.page-meta {
+ color: #64748b;
+ font-size: 24rpx;
+}
+
+.empty {
+ padding: 40rpx;
+ text-align: center;
+ color: #64748b;
+}
+
+.error {
+ margin-bottom: 20rpx;
+ color: #b42318;
+}
diff --git a/miniapp/pages/webview/index.js b/miniapp/pages/webview/index.js
new file mode 100644
index 0000000..f20610c
--- /dev/null
+++ b/miniapp/pages/webview/index.js
@@ -0,0 +1,9 @@
+Page({
+ data: {
+ url: ''
+ },
+
+ onLoad(query) {
+ this.setData({ url: decodeURIComponent(query.url || '') });
+ }
+});
diff --git a/miniapp/pages/webview/index.json b/miniapp/pages/webview/index.json
new file mode 100644
index 0000000..7c24b44
--- /dev/null
+++ b/miniapp/pages/webview/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "页面预览"
+}
diff --git a/miniapp/pages/webview/index.wxml b/miniapp/pages/webview/index.wxml
new file mode 100644
index 0000000..c165ac7
--- /dev/null
+++ b/miniapp/pages/webview/index.wxml
@@ -0,0 +1 @@
+
diff --git a/miniapp/project.config.json b/miniapp/project.config.json
new file mode 100644
index 0000000..8d9a8f8
--- /dev/null
+++ b/miniapp/project.config.json
@@ -0,0 +1,23 @@
+{
+ "description": "TKMind WeChat Mini Program",
+ "packOptions": {
+ "ignore": [
+ {
+ "type": "folder",
+ "value": "node_modules"
+ }
+ ]
+ },
+ "setting": {
+ "urlCheck": true,
+ "es6": true,
+ "enhance": true,
+ "postcss": true,
+ "minified": true
+ },
+ "compileType": "miniprogram",
+ "libVersion": "3.8.8",
+ "appid": "touristappid",
+ "projectname": "tkmind-miniapp",
+ "condition": {}
+}
diff --git a/miniapp/sitemap.json b/miniapp/sitemap.json
new file mode 100644
index 0000000..3869a2e
--- /dev/null
+++ b/miniapp/sitemap.json
@@ -0,0 +1,8 @@
+{
+ "rules": [
+ {
+ "action": "disallow",
+ "page": "*"
+ }
+ ]
+}
diff --git a/miniapp/utils/api.js b/miniapp/utils/api.js
new file mode 100644
index 0000000..a1adc82
--- /dev/null
+++ b/miniapp/utils/api.js
@@ -0,0 +1,188 @@
+const SESSION_KEY = 'tkmind_session';
+
+let apiBaseUrl = '';
+let cookie = '';
+
+function setApiBaseUrl(value) {
+ apiBaseUrl = String(value || '').replace(/\/$/, '');
+}
+
+function getStoredSession() {
+ const session = wx.getStorageSync(SESSION_KEY);
+ if (session && typeof session === 'object') {
+ cookie = session.cookie || cookie;
+ return session;
+ }
+ return null;
+}
+
+function saveSession(session) {
+ cookie = session?.cookie || cookie;
+ wx.setStorageSync(SESSION_KEY, session || {});
+}
+
+function clearSession() {
+ cookie = '';
+ wx.removeStorageSync(SESSION_KEY);
+}
+
+function normalizeSetCookie(headers = {}) {
+ const raw = headers['Set-Cookie'] || headers['set-cookie'];
+ if (!raw) return '';
+ if (Array.isArray(raw)) {
+ return raw.map((item) => String(item).split(';')[0]).join('; ');
+ }
+ return String(raw)
+ .split(/,(?=\s*[^;,]+=)/)
+ .map((item) => item.trim().split(';')[0])
+ .filter(Boolean)
+ .join('; ');
+}
+
+function request(path, options = {}) {
+ const url = /^https?:\/\//i.test(path) ? path : `${apiBaseUrl}${path}`;
+ const header = {
+ Accept: 'application/json',
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
+ ...(cookie ? { Cookie: cookie } : {}),
+ ...(options.header || {})
+ };
+
+ return new Promise((resolve, reject) => {
+ wx.request({
+ url,
+ method: options.method || 'GET',
+ data: options.body || undefined,
+ header,
+ timeout: options.timeout || 20000,
+ success(res) {
+ const nextCookie = normalizeSetCookie(res.header);
+ if (nextCookie) {
+ cookie = nextCookie;
+ const previous = getStoredSession() || {};
+ saveSession({ ...previous, cookie });
+ }
+ const status = Number(res.statusCode || 0);
+ if (status >= 200 && status < 300) {
+ resolve(res.data);
+ return;
+ }
+ const message =
+ res.data?.message ||
+ res.data?.error?.message ||
+ `请求失败 (${status || 'network'})`;
+ const error = new Error(message);
+ error.status = status;
+ error.body = res.data;
+ reject(error);
+ },
+ fail(err) {
+ reject(new Error(err?.errMsg || '网络请求失败'));
+ }
+ });
+ });
+}
+
+function portalRequest(path, options = {}) {
+ return request(path, options);
+}
+
+function apiRequest(path, options = {}) {
+ return request(`/api${path}`, options);
+}
+
+async function checkAuth() {
+ return portalRequest('/auth/status');
+}
+
+async function getMe() {
+ return portalRequest('/auth/me');
+}
+
+async function loginWithPassword(username, password) {
+ const result = await portalRequest('/auth/login', {
+ method: 'POST',
+ body: { username, password }
+ });
+ saveSession({ cookie, user: result?.user || result || null, loginType: 'password' });
+ return result;
+}
+
+function wxLogin() {
+ return new Promise((resolve, reject) => {
+ wx.login({
+ success(res) {
+ if (res.code) resolve(res.code);
+ else reject(new Error('微信登录未返回 code'));
+ },
+ fail(err) {
+ reject(new Error(err?.errMsg || '微信登录失败'));
+ }
+ });
+ });
+}
+
+async function loginWithWechatCode(loginPath) {
+ const code = await wxLogin();
+ const result = await portalRequest(loginPath, {
+ method: 'POST',
+ body: { code }
+ });
+ saveSession({ cookie, user: result?.user || result || null, loginType: 'wechat' });
+ return result;
+}
+
+async function logout() {
+ try {
+ await portalRequest('/auth/logout', { method: 'POST' });
+ } finally {
+ clearSession();
+ }
+}
+
+async function listSessions(options = {}) {
+ const limit = options.limit || 30;
+ const cursor = options.cursor ? `&cursor=${encodeURIComponent(options.cursor)}` : '';
+ return apiRequest(`/sessions?limit=${encodeURIComponent(limit)}${cursor}`);
+}
+
+async function loadSession(sessionId) {
+ return apiRequest(`/sessions/${encodeURIComponent(sessionId)}`);
+}
+
+async function createAgentRun(input) {
+ return apiRequest('/agent/runs', {
+ method: 'POST',
+ body: input,
+ timeout: 60000
+ });
+}
+
+async function getAgentRun(runId) {
+ return apiRequest(`/agent/runs/${encodeURIComponent(runId)}`);
+}
+
+async function listMindSpacePages(options = {}) {
+ const limit = options.limit || 30;
+ const offset = options.offset || 0;
+ return apiRequest(
+ `/mindspace/v1/pages?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`
+ );
+}
+
+module.exports = {
+ setApiBaseUrl,
+ getStoredSession,
+ saveSession,
+ clearSession,
+ checkAuth,
+ getMe,
+ loginWithPassword,
+ loginWithWechatCode,
+ logout,
+ listSessions,
+ loadSession,
+ createAgentRun,
+ getAgentRun,
+ listMindSpacePages
+};
diff --git a/miniapp/utils/config.js b/miniapp/utils/config.js
new file mode 100644
index 0000000..6e9a6f9
--- /dev/null
+++ b/miniapp/utils/config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ API_BASE_URL: 'https://h5.tkmind.cn',
+ MINIAPP_LOGIN_PATH: '/auth/wechat-miniapp/login',
+ AGENT_RUN_POLL_MS: 1200,
+ AGENT_RUN_MAX_POLLS: 120
+};
diff --git a/miniapp/utils/messages.js b/miniapp/utils/messages.js
new file mode 100644
index 0000000..cc48b29
--- /dev/null
+++ b/miniapp/utils/messages.js
@@ -0,0 +1,41 @@
+function createUserMessage(text) {
+ return {
+ id: `mp_${Date.now()}_${Math.random().toString(16).slice(2)}`,
+ role: 'user',
+ created: Math.floor(Date.now() / 1000),
+ content: [{ type: 'text', text }],
+ metadata: {
+ source: 'wechat-miniapp',
+ userVisible: true,
+ agentVisible: true,
+ displayText: text
+ }
+ };
+}
+
+function messageText(message) {
+ if (!message) return '';
+ if (typeof message.text === 'string') return message.text;
+ if (Array.isArray(message.content)) {
+ return message.content
+ .filter((part) => part && part.type === 'text')
+ .map((part) => part.text || '')
+ .join('');
+ }
+ return '';
+}
+
+function normalizeMessages(messages = []) {
+ return messages.map((message) => ({
+ id: message.id || `${message.role}_${message.created || Date.now()}`,
+ role: message.role || 'assistant',
+ text: messageText(message),
+ created: message.created || 0
+ }));
+}
+
+module.exports = {
+ createUserMessage,
+ messageText,
+ normalizeMessages
+};