用 AI 写小程序
小程序就是一个 HTML 包,Claude Code、Cursor 这类 agent 都能直接写。把本页链接和下面这段上下文一起丢给你的 agent,它就知道 Lumo 的全部约束。
你在为「Lumo 小程序开放平台」开发一个小程序。请遵守以下约束:
- 一个小程序 = 一个 HTML 包(入口通常 index.html),运行在 Lumo App 的 WebView 沙箱里,可含多个 .html 页面(页面间用真实跳转,运行时会做栈式转场)。
- 推荐用脚手架起项目:lumo-cli miniprogram init -dir=./app -template=vue(Vue 3 + Vite 多页)。要点:① 每个页面一个 .html 入口,是多页而非 SPA,不要加 vue-router(前端路由不产生原生页面层,右滑会直接退出);② 资源用相对路径(Vite base:'./' 已配好),不要用以 / 开头的绝对路径;③ 页面间跳转用 location.href='detail.html?x=1' 传参。App 会以真实 origin 提供小程序目录,所以 ES module、图片、多份 CSS、代码分包都正常。
- 「页面」= App 页面栈里的一层(进得去、退得回),tab 不是页面。运行时没有导航 API,每一次 location.href 都会入栈,等价于微信的 navigateTo;所以底部 tab 栏必须做成同一个 .html 里的视图切换(等价于 switchTab,不入栈),用 location.href 切 tab 会每切一次拉出一个新页面。判据:用户按返回该回到上一个界面 → 用 location.href;该退出小程序 → 同页切视图。切 tab 时用 history.replaceState 写 ?tab=xxx 记住当前 tab(不入栈),从详情页返回能落回原 tab;tab 视图套 KeepAlive 并自己按 tab 存 scrollTop。
- 调用原生能力:推荐用 SDK —— import Lumo from '@lumate/lumo-sdk',调用 Lumo.xxx();不装包也可直接用全局 window.Lumo。所有方法返回 Promise。常用:login()/getUserInfo()/getOpenId()、request()、storage.get/set()、scanCode()、chooseMedia()/previewImage()/saveImageToPhotosAlbum()、getLocation()/openLocation()、showModal()/showLoading()/showActionSheet()/showToast()、vibrateShort()/vibrateLong()、setClipboardData()/getClipboardData()、getSystemInfo()、getMenuButtonBoundingClientRect()。完整签名见 https://www.lumate.my/developers
- localStorage / sessionStorage 被禁用;持久化一律用 Lumo.storage(需 storage 权限且用户已登录)。
- 网络请求用 Lumo.request({ url, method, headers, body })——由 App 本机发起,没有浏览器跨域(CORS)限制,能跨请求保持 cookie;但目标 host 必须在 manifest.json 的 requestDomains 白名单里,且仅支持 https。不要用 fetch/XHR 直连第三方(会被 CORS 挡)。
- 自定义顶部导航栏时,用 getMenuButtonBoundingClientRect() + getSystemInfo().statusBarHeight 计算高度并避让右上角的胶囊(详情/关闭按钮)。
- manifest.json 需含 appId / name / versionName / entry / sdkVersion;permissions 只能声明封闭目录:openid、userInfo、storage、share、location。
- 推荐用 SDK 开发:import Lumo from '@lumate/lumo-sdk',Lumo.xxx() 带完整 TypeScript 类型提示。不装包也可直接用全局 window.Lumo。
- 开发流程用 @lumate/mp-cli:init → create → upload → preview(扫码真机预览,免审核)。
在写代码前,请先阅读 https://www.lumate.my/developers 获取完整的 API、manifest 规范、权限范围与合法域名说明。
写完用 lumo-cli preview 生成二维码,扫码真机验证。
快速开始
全流程走 CLI,五步从零到真机预览。
# 1. 安装 CLI
npm i -g @lumate/mp-cli
# 2. 登录(复用 Lumo 开发者账号)
lumo-cli login
# 3. 脚手架一个 Vue 3 + Vite 项目(SDK 已内置;想要纯 HTML 单文件用 -template=html)
lumo-cli miniprogram init -dir=./my-app -template=vue
cd my-app && npm install
# 4. 注册,把打印出的 appKey 填进 public/manifest.json 的 appId
lumo-cli miniprogram create -name="我的小程序"
# 5. 构建并上传版本(进入待审核)
npm run build
lumo-cli miniprogram upload -dir=dist -version=1.0.0
# 6. 生成开发版二维码,用 Lumo「扫一扫」即可真机预览(免审核)
lumo-cli miniprogram preview -dir=dist -version=1.0.0
SDK @lumate/lumo-sdk 带完整 TypeScript 类型;不装包也可以直接用全局 window.Lumo。
页面与 tab 栏
一个「页面」= App 页面栈里的一层:进得去,退得回。tab 不是页面。
运行时没有导航 API,跳转全靠 location / history,而 App 的 WebView 会把每一次 location.href 当成一次入栈。拿它切 tab,等于在微信里用 navigateTo 当 switchTab 使:每切一次拉出一个新页面,返回要一路退回去。
判据只有一条 —— 用户在这个界面按返回,应该去哪?该回到上一个界面,它就是页面,用 location.href;该退出小程序,它就不是页面,在同一个 .html 里切视图。这也是「不要加 vue-router」和「页面间用真实跳转」两条建议的共同出处:前端路由不产生原生页面层,正因为如此,它恰好适合 tab。
| 你想要的 | 微信里的 | 在 Lumo 里怎么做 |
|---|---|---|
| 打开详情页,用户能返回 | navigateTo | location.href = 'detail.html?id=1' |
| 切换底部 tab,不留返回栈 | switchTab | 同一个 .html 里切视图,不做任何跳转 |
| 回到上一页 | navigateBack | history.back(),运行时接管 |
// index.html —— 三个 tab 是三个视图,不是三个页面
const TABS = { home: Home, guide: Guide, mine: Mine }
const active = ref('home')
function switchTab(key) {
active.value = key // 只切视图,不跳转,页面栈不动
// replaceState 不入栈,只改写当前这一页的 URL:
// 从详情页 history.back() 回来时读得到 ?tab=,能落回离开时那个 tab
history.replaceState(null, '', key === 'home' ? 'index.html' : `index.html?tab=${key}`)
}
function openDetail(id) {
location.href = `detail.html?id=${id}` // 这一次才该入栈
}
tab 视图建议套 KeepAlive:筛选条件、列表滚动位置切走再切回都还在。滚动位置要自己按 tab 存一份 —— 一个页面只有一根滚动条,不存的话从短页面切回长列表会掉回顶部。切进来才需要刷新的数据(收藏、登录态)放 onActivated,别放 onMounted。
manifest.json 规范
每个小程序包根目录一份 manifest.json。用 Vue 脚手架时它放在 public/manifest.json,构建后会原样落到 dist/ 根目录。
| 字段 | 必填 | 说明 |
|---|---|---|
appId | 是 | create 命令返回的 appKey,标识这个小程序。 |
name | 是 | 小程序名称。 |
versionName | 是 | 版本号,如 1.0.0;同一版本号只能上传一次。 |
entry | 是 | 入口 HTML 文件,通常是 index.html。 |
sdkVersion | 是 | SDK 版本,当前填 "1.0"。 |
permissions | 否 | 申请的能力范围数组(见下方权限范围)。 |
requestDomains | 否 | Lumo.request() 可访问的 HTTPS 域名白名单;也可在控制台/CLI 后配。 |
{
"appId": "mp-xxxxxxxxxxxx",
"name": "我的小程序",
"versionName": "1.0.0",
"entry": "index.html",
"sdkVersion": "1.0",
"permissions": ["openid", "storage"],
"requestDomains": ["api.example.com"]
}
权限范围
permissions 只能从下面的目录里选,未知值会被服务端拒绝。震动、扫码、剪贴板、选图这些能力无需声明,走系统权限弹窗。
| scope | 能力 |
|---|---|
| openid | 获取用户在本小程序内的匿名标识(不暴露账号)。 |
| userInfo | 获取昵称/头像;login 与 getUserInfo 用它(会弹授权半屏)。 |
| storage | 读写本小程序专属的服务端 KV 存储。 |
| share | 调起系统分享面板。 |
| location | 获取设备定位(还需系统定位权限)。 |
合法域名
Lumo.request() 只能请求 requestDomains 里声明过的 HTTPS 域名。请求由 App 本机发起,没有浏览器跨域限制,且跨请求保持 cookie。域名可写在 manifest,也可在控制台或 CLI 后配,即时生效:
lumo-cli miniprogram domains -app=mp-xxxx # 查看
lumo-cli miniprogram domains -app=mp-xxxx -add=api.example.com # 追加
lumo-cli miniprogram domains -app=mp-xxxx -set=a.com,b.com # 替换
开放 API · window.Lumo
import Lumo from '@lumate/lumo-sdk',调用 Lumo.xxx(),全部方法返回 Promise。下表由 SDK 类型定义自动生成。
import Lumo from '@lumate/lumo-sdk'
const { openId, user } = await Lumo.login()
const info = await Lumo.getSystemInfo()
const res = await Lumo.request({ url: 'https://api.example.com/data' })
await Lumo.storage.set('key', 'value')
身份 / 登录
getOpenId(): Promise<string>;
openid
Returns an opaque identifier stable for this (mini-program, user) pair. Never derived from — and never revealing — the user's Lumo account id. Requires the "openid" permission scope.
getUserInfo(): Promise<LumoUserInfo>;
userInfo
Prompts the user with a native consent dialog before returning their nickname/avatar. Rejects if the user declines. Requires the "userInfo" permission scope.
login(): Promise<LumoLoginResult>;
userInfo
"Sign in with Lumo": raises a native authorization sheet, then resolves with the user's openId + profile in one step. Rejects if the user cancels. Requires the "userInfo" permission scope (shares the same on-device grant as getUserInfo — authorizing either won't re-prompt the other).
网络
request(options: LumoRequestOptions): Promise<LumoRequestResult>;
无需权限
Performs the HTTP call natively from the device, so there is no browser CORS restriction and cookies persist across calls (letting you walk a CSRF form flow: GET a token, then POST). The target host must be declared in your manifest.json's "requestDomains" array; https only. Rejects on a disallowed host, non-https URL, or network error. Note the HTTP status is returned in the result — a 4xx/5xx does NOT reject.
交互反馈
showToast(message: string): Promise<void>;
无需权限
Always available — no permission scope required.
showModal(options: LumoModalOptions): Promise<LumoModalResult>;
无需权限
Native confirm dialog. Resolves { confirm, cancel }.
showLoading(options?: { title?: string }): Promise<void>;
无需权限
Show a blocking loading HUD until hideLoading().
hideLoading(): Promise<void>;
无需权限
showActionSheet(options: { itemList: string[] }): Promise<{ tapIndex: number }>;
无需权限
Bottom action sheet. Resolves { tapIndex }; rejects if cancelled.
扫码
scanCode(options?: { onlyFromCamera?: boolean }): Promise<{ result: string; scanType: string }>;
无需权限
Opens Lumo's scanner and resolves the decoded string. Rejects if the user backs out. No scope required (uses the OS camera permission).
媒体
chooseMedia(options?: LumoChooseMediaOptions): Promise<{ tempFiles: LumoMediaFile[] }>;
无需权限
Pick from album or take with the camera. Returns tempFiles whose tempFilePath is a data: URL usable directly in <img src>.
previewImage(options: { urls: string[]; current?: string }): Promise<void>;
无需权限
Full-screen swipeable image viewer.
saveImageToPhotosAlbum(options: { filePath: string }): Promise<void>;
无需权限
Save an image (data: URL, http(s) URL, or file uri) to the photo album.
位置
getLocation(options?: { type?: 'wgs84' | 'gcj02' }): Promise<LumoLocation>;
location
One-shot device location. Requires the "location" permission scope in manifest.json AND the OS location permission at runtime.
openLocation(options: { latitude: number; longitude: number; name?: string; address?: string }): Promise<void>;
无需权限
Opens the given point in the system maps app. No scope required.
设备
vibrateShort(options?: { type?: string }): Promise<void>;
无需权限
Short haptic tap.
vibrateLong(): Promise<void>;
无需权限
Longer haptic buzz.
setClipboardData(options: { data: string }): Promise<void>;
无需权限
getClipboardData(): Promise<{ data: string }>;
无需权限
系统信息
getSystemInfo(): Promise<LumoSystemInfo>;
无需权限
Always available — no permission scope required.
getMenuButtonBoundingClientRect(): Promise<LumoMenuButtonRect>;
无需权限
Returns the on-screen rect of the app's fixed top-right detail/close capsule, so a mini-program using a custom navigation bar (its own page fully controls the screen — see the app's runner, which renders no header) can lay out around it without overlapping it. Always available — no permission scope required.
存储
get(key: string): Promise<string | null>;
storage
Requires the "storage" permission scope.
set(key: string, value: string): Promise<void>;
storage
Requires the "storage" permission scope. Value is capped at 64KB.
类型定义
API 用到的参数与返回结构。
LumoStorageResult
value: string | nullThe stored value, or null if no value was ever set for this key.
LumoUserInfo
name: stringavatar?: string
LumoLoginResult
openId: stringOpaque per-(mini-program, user) identifier, same value getOpenId() returns. May be an empty string if it couldn't be fetched (offline); the profile is still returned.user: LumoUserInfo
LumoShareOptions
message: stringtitle?: stringurl?: stringiOS only.
LumoShareResult
action: string'sharedAction' | 'dismissedAction' (matches React Native's Share.share result).
LumoRequestOptions
url: stringFull https URL. Its host must be listed in the manifest's requestDomains.method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'Defaults to "GET".headers?: Record<string, string>body?: string | objectString sent as-is; a non-string is JSON.stringify'd. Ignored for GET/HEAD.timeout?: numberBridge timeout in ms (default 20000).
LumoRequestResult
status: numberheaders: Record<string, string>data: stringResponse body as text (capped at 2 MB).
LumoSystemInfo
theme: 'light' | 'dark'locale: stringsafeAreaInsets: { top: number; bottom: number; left: number; right: number }screenWidth: numberscreenHeight: numberstatusBarHeight: numberStatus bar height in dp.
LumoMenuButtonRect
width: numberheight: numbertop: numberright: numberbottom: numberleft: number
LumoModalOptions
title?: stringcontent?: stringconfirmText?: stringcancelText?: stringshowCancel?: booleanDefaults to true. Set false for a single-button alert.
LumoModalResult
confirm: booleancancel: boolean
LumoChooseMediaOptions
count?: numberMax items to pick (album), 1–9. Default 9.mediaType?: Array<'image' | 'video'>['image'] | ['video'] | ['image','video']. Default ['image'].sourceType?: Array<'album' | 'camera'>['album'] | ['camera'] | both. Both prompts a chooser. Default both.
LumoMediaFile
tempFilePath: stringA data: URL (base64) — drop straight into <img src> or upload.size: numberfileType: 'image' | 'video'width?: numberheight?: number
LumoLocation
latitude: numberlongitude: numberaccuracy: number | nullaltitude: number | nullspeed: number | null
预览与审核
- 开发版预览 ——
lumo-cli miniprogram preview生成一张 30 分钟有效的二维码,用 Lumo「扫一扫」即可真机运行你上传的版本,哪怕还没过审。 - 提交审核 —— 在开发者控制台提交版本审核,通过后即对所有用户可见。
- 运营配置 —— 图标、简介、合法域名等都在控制台管理,改完即时生效。