本页是「教学讲义 + 课堂沙盒」一体化单文件。包含完整 5 天主线内容、可交互大模型调用沙盒(Key 仅存本机 localStorage)、HTTP 调试工具、微信小程序界面原型交互演示,以及完整代码示例。
开发工具 · 项目结构 · 首页静态布局
导航系统 · Flex布局 · rpx单位
wx.request · juhe API · Mock兜底
OpenAI兼容 · 4厂商配置 · 对话展示
合法域名 · 发布流程 · 演示答辩
| 层次 | 技术 |
|---|---|
| 前端框架 | 微信小程序原生 WXML/WXSS/JS |
| 网络层 | wx.request + Promise封装 |
| 数据服务 | 聚合数据 juhe.cn(新闻/天气/笑话/星座) |
| AI 对话 | DeepSeek / 阿里千问 / Kimi / 字节豆包 |
| 本地存储 | wx.setStorageSync(Key不写死在代码) |
| AI辅助开发 | VibeCoding / Cursor AI |
ai-assistant/ ├── app.js # 全局逻辑 ├── app.json # 页面注册/tabBar ├── app.wxss # 全局样式 ├── utils/ │ ├── request.js # wx.request封装 │ └── aiChat.js # AI 对话工具函数 ├── pages/ │ ├── home/ # 首页宫格 │ ├── news/ # 新闻列表 │ ├── joke/ # 笑话 │ ├── weather/ # 天气 │ ├── chat/ # AI 对话 │ └── setting/ # 配置 Key └── images/ # 图标资源
每天包含教学目标、课堂流程建议、关键代码片段、验收标准。
让所有学生在第一天结束时,手机上能用微信扫码预览到自己修改过的页面。建立「能跑 → 能改 → 能解释」的信心。
微信开发者工具下载安装,注册小程序测试账号(无需企业认证),创建项目选「不使用云开发」。
app.js 全局逻辑 · app.json 注册页面/配置窗口 · app.wxss 全局样式 · pages/index/index.wxml 首页模板
改标题颜色、加一行文字、换一张 image。然后点「预览」用手机扫码验证效果。
在 .js 中定义 data,在 WXML 中用 {'{{'}变量{'}}'} 渲染,按钮触发 setData 更新视图。
用 Cursor 或 Copilot 输入「帮我写一个微信小程序首页,宫格布局,6个入口」,观察 AI 生成 WXML。
{
"pages": [
"pages/home/home",
"pages/news/news",
"pages/joke/joke",
"pages/weather/weather",
"pages/chat/chat",
"pages/setting/setting"
],
"window": {
"navigationBarBackgroundColor": "#4527A0",
"navigationBarTitleText": "AI 生活助手",
"navigationBarTextStyle": "white",
"backgroundColor": "#F5F7FA"
},
"tabBar": {
"color": "#78909C",
"selectedColor": "#4527A0",
"list": [
{ "pagePath": "pages/home/home", "text": "首页", "iconPath": "images/home.png", "selectedIconPath": "images/home-s.png" },
{ "pagePath": "pages/chat/chat", "text": "AI", "iconPath": "images/ai.png", "selectedIconPath": "images/ai-s.png" },
{ "pagePath": "pages/setting/setting", "text": "我的", "iconPath": "images/me.png", "selectedIconPath": "images/me-s.png" }
]
}
}
<!-- pages/home/home.wxml -->
<view class="header">
<text class="title">AI 生活助手</text>
<text class="sub">你的智能生活工具箱</text>
</view>
<view class="grid">
<view class="item" bindtap="goNews">
<text class="icon">📰</text>
<text class="label">今日新闻</text>
</view>
<view class="item" bindtap="goJoke">
<text class="icon">😄</text>
<text class="label">每日笑话</text>
</view>
<view class="item" bindtap="goWeather">
<text class="icon">🌤️</text>
<text class="label">天气查询</text>
</view>
<view class="item" bindtap="goConstellation">
<text class="icon">⭐</text>
<text class="label">星座运势</text>
</view>
<view class="item" bindtap="goChat">
<text class="icon">🤖</text>
<text class="label">AI 助手</text>
</view>
<view class="item" bindtap="goSetting">
<text class="icon">⚙️</text>
<text class="label">配置中心</text>
</view>
</view>
项目能在开发者工具模拟器正常运行,且真机预览通过
能说出 app.js / app.json / .wxml / .wxss / .js 各自的作用
改了标题/颜色/文字后,模拟器立即反映变化
把「一个页面」扩展成「一个有结构的产品骨架」——能导航,能切 Tab,布局自适应屏幕宽度。
wx.navigateTo(有返回按钮,入栈)vs wx.switchTab(tabBar 页,不入栈)。讲清楚哪些页面用哪种。
在 app.json 配置 3 个 Tab,准备 icon 图标(建议 AI 生成 40×40 PNG),讲解常见报错:路径不存在、图标尺寸过大。
rpx 是小程序专用单位:750rpx = 屏幕宽度。用 display:flex 做宫格,保证在不同机型自适应。
wx.navigateTo({'{'}url:'/pages/detail/detail?id=123&name=xx'{'}'}),目标页在 onLoad(options) 里拿参数。
// pages/home/home.js
Page({
goNews() {
wx.navigateTo({ url: '/pages/news/news?type=top' })
},
goChat() {
wx.switchTab({ url: '/pages/chat/chat' }) // tabBar 页必须用 switchTab
}
})
// pages/news/news.js - 接收参数
Page({
data: { type: 'top', list: [] },
onLoad(options) {
const type = options.type || 'top'
this.setData({ type })
this.loadNews(type)
},
loadNews(type) {
// Day3 填充网络请求
wx.showLoading({ title: '加载中' })
// ...
}
})
/* pages/home/home.wxss */
.grid {
display: flex;
flex-wrap: wrap;
padding: 20rpx;
gap: 20rpx;
}
.item {
width: calc(33.33% - 14rpx);
background: #fff;
border-radius: 16rpx;
padding: 30rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,.08);
}
.icon { font-size: 52rpx; margin-bottom: 12rpx; }
.label { font-size: 26rpx; color: #455A64; font-weight: bold; }
让学生理解:接口是产品的心脏,但课堂要有「不断电」的备份方案(mock 数据)。掌握封装 request 的工程思维。
抽出 utils/request.js,统一处理 loading、错误提示、超时,让业务代码只关心数据。
juhe.cn 注册 → 申请「今日头条新闻」免费接口 → 在设置页保存 Key → 新闻页调用展示。
当 Key 申请未通过或网络异常时,从本地 JSON 文件读取 mock 数据,保证课堂演示不卡壳。
picker 选择星座 → 请求运势接口 → 展示多字段。笑话列表 → 每日随机一条。
// utils/request.js
const BASE_JUHE = 'https://v.juhe.cn'
function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
wx.showNavigationBarLoading()
wx.request({
url,
method,
data,
timeout: 10000,
success(res) {
wx.hideNavigationBarLoading()
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(new Error('HTTP ' + res.statusCode))
}
},
fail(err) {
wx.hideNavigationBarLoading()
wx.showToast({ title: '网络异常', icon: 'error' })
reject(err)
}
})
})
}
// 新闻接口封装
function getNews(type = 'top', key) {
if (!key) return Promise.resolve(MOCK_NEWS) // 无 Key 走 mock
return request(`${BASE_JUHE}/toutiao/index?type=${type}&key=${key}`)
.catch(() => MOCK_NEWS) // 接口失败也走 mock
}
// 笑话接口封装
function getJoke(key) {
if (!key) return Promise.resolve(MOCK_JOKE)
return request(`${BASE_JUHE}/joke/content/list.php?sort=rand&page=1&pagesize=1&key=${key}`)
.catch(() => MOCK_JOKE)
}
// Mock 数据
const MOCK_NEWS = {
reason: 'mock',
result: {
data: [
{ title: '【演示】今日科技头条:AI大模型持续演进', category: '科技', date: '2025-05-14', url: 'https://example.com' },
{ title: '【演示】全球气候峰会:各国承诺碳中和目标', category: '国际', date: '2025-05-14', url: 'https://example.com' },
{ title: '【演示】本地餐饮业恢复强劲,夜经济持续升温', category: '本地', date: '2025-05-14', url: 'https://example.com' }
]
}
}
const MOCK_JOKE = {
reason: 'mock',
result: {
data: [{ content: '演示笑话:程序员最喜欢的运动是什么?—— 跑(bug)步!' }]
}
}
module.exports = { request, getNews, getJoke }
// pages/news/news.js
const { getNews } = require('../../utils/request')
Page({
data: { list: [], loading: true },
onLoad() {
const key = wx.getStorageSync('juhe_key') || ''
getNews('top', key).then(res => {
const list = (res.result && res.result.data) || []
this.setData({ list, loading: false })
})
},
goDetail(e) {
const url = encodeURIComponent(e.currentTarget.dataset.url)
wx.navigateTo({ url: `/pages/webview/webview?url=${url}` })
}
})
v.juhe.cn。开发阶段可勾选「不校验合法域名」跳过。理解 OpenAI 兼容接口的统一格式,实现可复用的 AI 调用工具函数,在小程序中完整展示「配置 Key → 发送消息 → 查看模型信息」全流程。
POST /v1/chat/completions,body:{ model, messages:[{role,content}], temperature },response:choices[0].message.content + usage。
豆包 model 填 ep-xxxx(Endpoint ID,非模型名),其余3个填模型名字符串即可。
通用 chat() 函数,传入 provider / messages → 返回 { text, usage, duration }。
消息列表渲染 + 输入框 + 发送 + loading 气泡 + 本次调用信息展开卡片。
picker 选厂商 → 输入 Key → 输入 model → wx.setStorageSync 保存。
| 厂商 | Endpoint | model 示例 | 官网 |
|---|---|---|---|
| DeepSeek | https://api.deepseek.com/v1/chat/completions | deepseek-chat | platform.deepseek.com |
| 阿里千问 | https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions | qwen-turboqwen-plus | dashscope.console.aliyun.com |
| Kimi | https://api.moonshot.cn/v1/chat/completions | moonshot-v1-8k | platform.moonshot.cn |
| 字节豆包 | https://ark.cn-beijing.volces.com/api/v3/chat/completions | ep-xxxxxxxx(Endpoint ID) | console.volcengine.com/ark |
// utils/aiChat.js
const PROVIDERS = {
deepseek: { url: 'https://api.deepseek.com/v1/chat/completions', defaultModel: 'deepseek-chat' },
qwen: { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', defaultModel: 'qwen-turbo' },
moonshot: { url: 'https://api.moonshot.cn/v1/chat/completions', defaultModel: 'moonshot-v1-8k' },
doubao: { url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', defaultModel: '' }
}
function chat(provider, apiKey, model, messages, systemPrompt = '你是一个智能助手,回答简洁清晰。') {
const cfg = PROVIDERS[provider]
if (!cfg) return Promise.reject(new Error('未知厂商'))
const t0 = Date.now()
const allMessages = [
{ role: 'system', content: systemPrompt },
...messages
]
return new Promise((resolve, reject) => {
wx.request({
url: cfg.url,
method: 'POST',
timeout: 30000,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
data: {
model: model || cfg.defaultModel,
messages: allMessages,
temperature: 0.7
},
success(res) {
const duration = Date.now() - t0
if (res.statusCode === 200 && res.data.choices) {
resolve({
text: res.data.choices[0].message.content,
usage: res.data.usage || {},
duration,
model: res.data.model || model,
status: res.statusCode
})
} else {
reject({
status: res.statusCode,
msg: (res.data && res.data.error && res.data.error.message) || JSON.stringify(res.data)
})
}
},
fail(err) {
reject({ status: -1, msg: err.errMsg || '网络异常' })
}
})
})
}
function loadConfig() {
return {
provider: wx.getStorageSync('ai_provider') || 'deepseek',
key: wx.getStorageSync('ai_key') || '',
model: wx.getStorageSync('ai_model') || ''
}
}
function saveConfig(provider, key, model) {
wx.setStorageSync('ai_provider', provider)
wx.setStorageSync('ai_key', key)
wx.setStorageSync('ai_model', model)
}
module.exports = { chat, loadConfig, saveConfig, PROVIDERS }
// pages/chat/chat.js
const { chat, loadConfig } = require('../../utils/aiChat')
Page({
data: {
messages: [], // 对话历史 [{role:'user'|'assistant', content}]
inputText: '',
sending: false,
lastMeta: null // 最后一次调用信息
},
onLoad() {
const cfg = loadConfig()
this.setData({ cfg })
},
onInput(e) {
this.setData({ inputText: e.detail.value })
},
async send() {
const text = this.data.inputText.trim()
if (!text || this.data.sending) return
const cfg = loadConfig()
if (!cfg.key) {
wx.showModal({ title: '提示', content: '请先在「我的」页面配置 API Key', showCancel: false })
return
}
// 加入用户消息
const messages = [...this.data.messages, { role: 'user', content: text }]
this.setData({ messages, inputText: '', sending: true })
try {
const res = await chat(cfg.provider, cfg.key, cfg.model, messages)
// 加入 AI 回复
const updated = [...messages, { role: 'assistant', content: res.text }]
this.setData({
messages: updated,
sending: false,
lastMeta: {
provider: cfg.provider,
model: res.model,
duration: res.duration + 'ms',
promptTokens: res.usage.prompt_tokens || '-',
completionTokens: res.usage.completion_tokens || '-',
totalTokens: res.usage.total_tokens || '-'
}
})
} catch(err) {
const updated = [...messages, { role: 'assistant', content: `[错误 ${err.status}] ${err.msg}` }]
this.setData({ messages: updated, sending: false })
}
},
clearHistory() {
wx.showModal({
title: '确认', content: '清空对话历史?',
success: (res) => { if(res.confirm) this.setData({ messages: [], lastMeta: null }) }
})
}
})
| 检查项 | 方法 | 说明 |
|---|---|---|
| ✅ tabBar 可正常切换 | 模拟器点击 | 3个Tab都能到达 |
| ✅ 所有页面路径存在 | app.json核对 | 无404页面 |
| ✅ 网络请求有loading提示 | 真机测试 | 避免空白等待 |
| ✅ 无Key时有友好提示 | 清空storage测试 | 提示去配置页填Key |
| ✅ Key不写死在代码里 | 全局搜索sk-/ep- | 用wx.getStorageSync读取 |
| ✅ API Key已清理(演示用) | 查看storage | 提交代码前必做 |
登录 mp.weixin.qq.com → 开发 → 开发管理 → 服务器域名,添加:v.juhe.cn api.deepseek.com api.moonshot.cn dashscope.aliyuncs.com ark.cn-beijing.volces.com
点「上传」→ 填写版本号(如 1.0.0)和版本说明 → 提交。登录公众平台 → 版本管理 → 将版本设为体验版。
管理员扫体验版二维码,验证合法域名模式下所有接口正常(不勾选「不校验合法域名」)。
宫格布局,说明每个入口的功能
进入新闻页,展示真实juhe数据
发送一条消息,展示模型信息卡片
推荐:utils/aiChat.js 的chat()函数
选择厂商 → 填写 Key 和模型名 → 保存到本机 → 在对话框测试。支持多轮对话,展示模型信息和 token 用量。
课堂演示 juhe 接口、公开测试接口,或调试自建后端转发服务。浏览器可能遇到 CORS,这正好解释「小程序为何要配合法域名」。
{}
用于课堂讲解信息架构:底部 Tab 导航、首页宫格入口、AI 对话、配置中心。点击底部 Tab 切换页面,AI 对话框可模拟发送消息。
| 页面 | 路径 | 核心技术 |
|---|---|---|
| 首页宫格 | pages/home | Flex Grid + navigateTo |
| 新闻列表 | pages/news | wx.request + scroll-view |
| AI 对话 | pages/chat | aiChat.js + 消息列表 |
| 配置中心 | pages/setting | wx.setStorageSync |
| WebView | pages/webview | web-view 组件 |
// utils/aiChat.js — 完整版
const PROVIDERS = {
deepseek: {
label: 'DeepSeek',
url: 'https://api.deepseek.com/v1/chat/completions',
defaultModel: 'deepseek-chat'
},
qwen: {
label: '阿里千问',
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
defaultModel: 'qwen-turbo'
},
moonshot: {
label: 'Kimi',
url: 'https://api.moonshot.cn/v1/chat/completions',
defaultModel: 'moonshot-v1-8k'
},
doubao: {
label: '字节豆包',
url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
defaultModel: '' // 必须填 ep-xxxx,无默认值
}
}
/**
* 发送 AI 对话请求
* @param {string} provider - 厂商 key(deepseek/qwen/moonshot/doubao)
* @param {string} apiKey - API Key
* @param {string} model - 模型名或 Endpoint ID
* @param {Array} messages - 历史消息 [{role:'user'|'assistant', content:'...'}]
* @param {string} systemPrompt - 系统提示词
* @returns Promise<{text, usage, duration, model, status}>
*/
function chat(provider, apiKey, model, messages, systemPrompt) {
const cfg = PROVIDERS[provider]
if (!cfg) return Promise.reject({ status: -1, msg: '未知厂商: ' + provider })
if (!apiKey) return Promise.reject({ status: -1, msg: '未配置 API Key' })
systemPrompt = systemPrompt || '你是一个智能生活助手,回答简洁清晰,使用中文。'
const t0 = Date.now()
const allMessages = [
{ role: 'system', content: systemPrompt },
...messages.slice(-20) // 最多保留最近20条,避免超 token
]
return new Promise((resolve, reject) => {
wx.request({
url: cfg.url,
method: 'POST',
timeout: 30000,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
data: {
model: model || cfg.defaultModel,
messages: allMessages,
temperature: 0.7,
max_tokens: 1000
},
success(res) {
const duration = Date.now() - t0
if (res.statusCode === 200 && res.data && res.data.choices) {
resolve({
text: res.data.choices[0].message.content || '',
usage: res.data.usage || {},
duration,
model: res.data.model || (model || cfg.defaultModel),
status: 200
})
} else {
const errMsg = res.data && res.data.error
? res.data.error.message
: JSON.stringify(res.data).slice(0, 200)
reject({ status: res.statusCode, msg: errMsg })
}
},
fail(err) {
reject({ status: -1, msg: err.errMsg || '网络请求失败' })
}
})
})
}
function loadConfig() {
return {
provider: wx.getStorageSync('ai_provider') || 'deepseek',
key: wx.getStorageSync('ai_key') || '',
model: wx.getStorageSync('ai_model') || ''
}
}
function saveConfig(provider, key, model) {
wx.setStorageSync('ai_provider', provider)
wx.setStorageSync('ai_key', key)
wx.setStorageSync('ai_model', model)
}
module.exports = { chat, loadConfig, saveConfig, PROVIDERS }
// utils/request.js — Promise 封装 wx.request
const JUHE_BASE = 'https://v.juhe.cn'
function request(url, method, data, options) {
method = method || 'GET'
data = data || {}
options = options || {}
return new Promise((resolve, reject) => {
if (options.showLoading !== false) {
wx.showNavigationBarLoading()
}
wx.request({
url, method, data,
timeout: options.timeout || 10000,
success(res) {
wx.hideNavigationBarLoading()
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(new Error('HTTP ' + res.statusCode))
}
},
fail(err) {
wx.hideNavigationBarLoading()
if (options.silent !== true) {
wx.showToast({ title: '网络异常,请重试', icon: 'error', duration: 2000 })
}
reject(err)
}
})
})
}
// --- juhe 接口封装 ---
const MOCK = {
news: {
error_code: 0, result: {
data: [
{ title: 'AI大模型竞争白热化', category: '科技', date: '2025-05-14', url: 'https://example.com' },
{ title: '全球气候峰会新进展', category: '国际', date: '2025-05-14', url: 'https://example.com' }
]
}
},
joke: {
error_code: 0, result: {
data: [{ content: '程序员最喜欢的运动是什么?跑(bug)步!' }]
}
}
}
function getNews(type, key) {
if (!key) return Promise.resolve(MOCK.news)
return request(`${JUHE_BASE}/toutiao/index?type=${type || 'top'}&key=${key}`)
.catch(() => MOCK.news)
}
function getJoke(key) {
if (!key) return Promise.resolve(MOCK.joke)
return request(`${JUHE_BASE}/joke/content/list.php?sort=rand&page=1&pagesize=3&key=${key}`)
.catch(() => MOCK.joke)
}
function getWeather(city, key) {
if (!key || !city) return Promise.reject(new Error('缺少city或key'))
return request(`${JUHE_BASE}/weather/index?city=${encodeURIComponent(city)}&dtype=json&format=2&key=${key}`)
}
module.exports = { request, getNews, getJoke, getWeather }
<!-- pages/chat/chat.wxml -->
<view class="page">
<!-- 消息列表 -->
<scroll-view class="msg-list" scroll-y scroll-into-view="{{scrollTo}}" scroll-with-animation>
<view wx:for="{{messages}}" wx:key="index" class="msg-wrap">
<view class="bubble {{item.role}}">{{item.content}}</view>
</view>
<!-- loading 气泡 -->
<view wx:if="{{sending}}" class="msg-wrap">
<view class="bubble assistant loading">
<text>思考中 </text>
<view class="dots"><view/><view/><view/></view>
</view>
</view>
<view id="bottom"></view>
</scroll-view>
<!-- 调用信息(可展开) -->
<view wx:if="{{lastMeta}}" class="meta-bar" bindtap="toggleMeta">
<text>{{metaOpen ? '▲' : '▼'}} 本次调用:{{lastMeta.model}} · {{lastMeta.duration}} · {{lastMeta.totalTokens}} tokens</text>
</view>
<view wx:if="{{lastMeta && metaOpen}}" class="meta-detail">
<view>厂商:{{lastMeta.provider}}</view>
<view>模型:{{lastMeta.model}}</view>
<view>耗时:{{lastMeta.duration}}</view>
<view>提示词 tokens:{{lastMeta.promptTokens}}</view>
<view>回复 tokens:{{lastMeta.completionTokens}}</view>
</view>
<!-- 输入框 -->
<view class="input-bar">
<textarea
class="input"
value="{{inputText}}"
bindinput="onInput"
placeholder="输入消息,Enter 换行,点发送…"
auto-height
maxlength="2000"
/>
<button class="send-btn" bindtap="send" disabled="{{sending}}">发送</button>
<button class="clear-btn" bindtap="clearHistory">清空</button>
</view>
</view>
// pages/chat/chat.js
const { chat, loadConfig } = require('../../utils/aiChat')
Page({
data: {
messages: [],
inputText: '',
sending: false,
lastMeta: null,
metaOpen: false,
scrollTo: ''
},
onLoad() {
const cfg = loadConfig()
if (!cfg.key) {
wx.showModal({
title: '还没配置 Key',
content: '请先去「我的」页面配置 AI 厂商和 API Key',
showCancel: false,
success: () => wx.switchTab({ url: '/pages/setting/setting' })
})
}
},
onInput(e) {
this.setData({ inputText: e.detail.value })
},
toggleMeta() {
this.setData({ metaOpen: !this.data.metaOpen })
},
async send() {
const text = this.data.inputText.trim()
if (!text || this.data.sending) return
const cfg = loadConfig()
if (!cfg.key) {
wx.showToast({ title: '请先配置 API Key', icon: 'none' })
return
}
const messages = [
...this.data.messages,
{ role: 'user', content: text }
]
this.setData({ messages, inputText: '', sending: true, scrollTo: 'bottom' })
try {
const res = await chat(cfg.provider, cfg.key, cfg.model, messages)
const updated = [...messages, { role: 'assistant', content: res.text }]
this.setData({
messages: updated,
sending: false,
scrollTo: 'bottom',
lastMeta: {
provider: cfg.provider,
model: res.model,
duration: res.duration + 'ms',
promptTokens: res.usage.prompt_tokens ?? '-',
completionTokens: res.usage.completion_tokens ?? '-',
totalTokens: res.usage.total_tokens ?? '-'
}
})
} catch(err) {
const errText = `[错误 ${err.status}] ${err.msg}`
const updated = [...messages, { role: 'assistant', content: errText }]
this.setData({ messages: updated, sending: false })
wx.showToast({ title: err.msg.slice(0, 20), icon: 'none' })
}
},
clearHistory() {
wx.showModal({
title: '确认清空', content: '清空后无法恢复',
success: res => {
if (res.confirm) this.setData({ messages: [], lastMeta: null, metaOpen: false })
}
})
}
})
// pages/setting/setting.js
const { saveConfig, loadConfig, PROVIDERS } = require('../../utils/aiChat')
Page({
data: {
providerList: [],
providerIndex: 0,
apiKey: '',
model: '',
juheKey: '',
saved: false
},
onLoad() {
const providerList = Object.entries(PROVIDERS).map(([k, v]) => v.label)
const cfg = loadConfig()
const providerIndex = Object.keys(PROVIDERS).indexOf(cfg.provider)
this.setData({
providerList,
providerIndex: providerIndex >= 0 ? providerIndex : 0,
apiKey: cfg.key,
model: cfg.model,
juheKey: wx.getStorageSync('juhe_key') || ''
})
this._providerKeys = Object.keys(PROVIDERS)
},
onProviderChange(e) {
const idx = e.detail.value
const key = this._providerKeys[idx]
const defaultModel = PROVIDERS[key].defaultModel
this.setData({ providerIndex: idx, model: defaultModel })
},
onKeyInput(e) { this.setData({ apiKey: e.detail.value }) },
onModelInput(e) { this.setData({ model: e.detail.value }) },
onJuheInput(e) { this.setData({ juheKey: e.detail.value }) },
save() {
const provider = this._providerKeys[this.data.providerIndex]
saveConfig(provider, this.data.apiKey, this.data.model)
wx.setStorageSync('juhe_key', this.data.juheKey)
this.setData({ saved: true })
wx.showToast({ title: '已保存到本机', icon: 'success' })
setTimeout(() => this.setData({ saved: false }), 3000)
},
clearAll() {
wx.showModal({
title: '确认清除', content: '清除所有已保存的 Key?',
success: res => {
if (!res.confirm) return
wx.clearStorageSync()
this.setData({ apiKey: '', model: '', juheKey: '' })
wx.showToast({ title: '已清除', icon: 'success' })
}
})
}
})
| 用途 | 域名(加入 request 合法域名) | 备注 |
|---|---|---|
| 聚合数据新闻/笑话/天气 | v.juhe.cn | 需 juhe.cn 账号申请接口 Key |
| DeepSeek | api.deepseek.com | OpenAI 兼容,/v1/chat/completions |
| 阿里千问 DashScope | dashscope.aliyuncs.com | 兼容模式路径:/compatible-mode/v1/chat/completions |
| Kimi Moonshot | api.moonshot.cn | OpenAI 兼容,/v1/chat/completions |
| 字节豆包火山方舟 | ark.cn-beijing.volces.com | /api/v3/chat/completions,model 填 ep-xxxx |
| 接口 | URL | 参数 |
|---|---|---|
| 今日头条新闻 | https://v.juhe.cn/toutiao/index | type=top&key=YOUR_KEY |
| 笑话大全 | https://v.juhe.cn/joke/content/list.php | sort=rand&page=1&pagesize=5&key=YOUR_KEY |
| 天气查询 | https://v.juhe.cn/weather/index | city=北京&dtype=json&format=2&key=YOUR_KEY |
| 星座运势 | https://web.juhe.cn/constellation/getAll | consName=白羊座&key=YOUR_KEY |
| 用途 | URL |
|---|---|
| 文章列表(GET) | https://jsonplaceholder.typicode.com/posts |
| 单条记录(GET) | https://jsonplaceholder.typicode.com/posts/1 |
| 创建记录(POST) | https://jsonplaceholder.typicode.com/posts |