小程序开放平台

把你的服务,
接进留学生的日常。

会写网页,就会写 Lumo 小程序。几行代码接入登录、相机、定位等原生能力,上线后直接出现在留学生的 App 里。本页是接入方法和 24 个开放 API 的完整参考。

用 AI 写小程序

小程序就是一个 HTML 包,Claude Code、Cursor 这类 agent 都能直接写。把本页链接和下面这段上下文一起丢给你的 agent,它就知道 Lumo 的全部约束。

给 AI 的上下文
你在为「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,等于在微信里用 navigateToswitchTab 使:每切一次拉出一个新页面,返回要一路退回去。

判据只有一条 —— 用户在这个界面按返回,应该去哪?该回到上一个界面,它就是页面,用 location.href;该退出小程序,它就不是页面,在同一个 .html 里切视图。这也是「不要加 vue-router」和「页面间用真实跳转」两条建议的共同出处:前端路由不产生原生页面层,正因为如此,它恰好适合 tab。

你想要的微信里的在 Lumo 里怎么做
打开详情页,用户能返回navigateTolocation.href = 'detail.html?id=1'
切换底部 tab,不留返回栈switchTab同一个 .html 里切视图,不做任何跳转
回到上一页navigateBackhistory.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/ 根目录。

字段必填说明
appIdcreate 命令返回的 appKey,标识这个小程序。
name小程序名称。
versionName版本号,如 1.0.0;同一版本号只能上传一次。
entry入口 HTML 文件,通常是 index.html。
sdkVersionSDK 版本,当前填 "1.0"。
permissions申请的能力范围数组(见下方权限范围)。
requestDomainsLumo.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: string
  • avatar?: 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: string
  • title?: string
  • url?: 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: number
  • headers: Record<string, string>
  • data: stringResponse body as text (capped at 2 MB).

LumoSystemInfo

  • theme: 'light' | 'dark'
  • locale: string
  • safeAreaInsets: { top: number; bottom: number; left: number; right: number }
  • screenWidth: number
  • screenHeight: number
  • statusBarHeight: numberStatus bar height in dp.

LumoMenuButtonRect

  • width: number
  • height: number
  • top: number
  • right: number
  • bottom: number
  • left: number

LumoModalOptions

  • title?: string
  • content?: string
  • confirmText?: string
  • cancelText?: string
  • showCancel?: booleanDefaults to true. Set false for a single-button alert.

LumoModalResult

  • confirm: boolean
  • cancel: 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: number
  • fileType: 'image' | 'video'
  • width?: number
  • height?: number

LumoLocation

  • latitude: number
  • longitude: number
  • accuracy: number | null
  • altitude: number | null
  • speed: number | null

预览与审核

  • 开发版预览 —— lumo-cli miniprogram preview 生成一张 30 分钟有效的二维码,用 Lumo「扫一扫」即可真机运行你上传的版本,哪怕还没过审。
  • 提交审核 —— 在开发者控制台提交版本审核,通过后即对所有用户可见。
  • 运营配置 —— 图标、简介、合法域名等都在控制台管理,改完即时生效。
进入开发者控制台