AI用 AI 一键接入(推荐)
适用于任意 AI Agent 与编辑器(Claude Code / Cursor / Trae / Codex / Copilot / Windsurf / Cline 等): 复制下面的提示词发给它,它会先阅读官方文档,再按文档改造你的客户端或发布新版本。
1) AI 接入提示词(客户端改造)
2) AI 发布提示词(上传新版安装包)
3) 文档地址(任意 AI 均可直接读取)
{{ORIGIN}}/skills/app-update-integration/SKILL.md
app-update-integration:客户端接入技能。覆盖技术栈识别、版本号来源定位、检测接口封装、更新弹窗、带进度下载、MD5 校验、调起安装与三种场景自测。
{{ORIGIN}}/skills/app-publish/SKILL.md
app-publish:安装包发布技能。覆盖分类匹配、登录或 API Key 认证、multipart 上传与 MD5 比对,用于给客户端造一个「有更新」的版本。
/ 与分类 ID 占位符,
复制后把 <分类 ID>、<当前版本> 换成真实值即可。
1接入概览
整个更新链路只依赖 3 个接口,服务端地址即本站地址:/。
| 接口 | 方法 | 鉴权 | 用途 |
|---|---|---|---|
/api/apps/check-update |
GET | 免登录 | 按分类 ID + 当前版本,检测是否有新版本,并返回安装包地址与 MD5 |
/api/apps/:id/download |
GET | 免登录 | 下载指定安装包,自动累计下载次数并记录下载日志 |
/api/public/categories |
GET | 免登录 | 获取完整分类树,用于查分类 ID(一般只在开发调试时使用) |
买菜APP / android端),
该分类的 ID 即客户端要内置的 categoryId;同一分类下发布的新版本会被自动识别为更新。
分类 ID 可在管理后台「项目管理」的分类树中直接看到,也可通过分类树接口查询。
2快速开始
- 拿到分类 ID:在管理后台「项目管理」查看分类树中标注的 ID(如
4)。 - 内置两个参数:把分类 ID 与当前 APP 版本号(如
1.2.0)写进客户端配置。 - 启动时检测:请求
/api/apps/check-update?categoryId=4&version=1.2.0。 - 有新版本就下载:使用返回的
app.downloadUrl下载,下载后比对app.md5。 - 校验通过再安装:MD5 一致则调起安装,不一致则删除文件并提示重试。
获取分类 ID
方式一:直接在管理后台「项目管理 → 分类树」查看每个分类名称后的 #ID 标识。
方式二:调用分类树接口,返回结构为 id / name / children:
curl "{{ORIGIN}}/api/public/categories"
{
"code": 0,
"message": "ok",
"data": [
{
"id": 1,
"name": "买菜APP",
"parentId": 0,
"children": [
{ "id": 4, "name": "android端", "parentId": 1, "children": [] },
{ "id": 5, "name": "ios端", "parentId": 1, "children": [] }
]
}
]
}
1(买菜APP),
其下各端发布的新版本都会被检测到,并返回其中版本号最高的那一个。
客户端建议直接内置自己所属的最末级分类 ID,避免多端安装包相互串版本。
3检测新版本
这是更新的核心接口,客户端启动或定时调用,服务端比对版本号后返回结果。
{{ORIGIN}}/api/apps/check-update?categoryId=4&version=1.2.0
请求参数(Query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
categoryId |
number | 是 | 分类 ID,支持父分类(含所有子孙分类) |
version |
string | 是 | 客户端当前版本号,如 1.2.0;建议保持 x.y.z 格式 |
响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
code |
number | 0 表示成功,其他值见 message |
data.hasUpdate |
boolean | 是否存在比当前版本更高的版本 |
data.app |
object | null | 无更新时为 null;有更新时为最新版本详情 |
app.id |
number | APP 记录 ID,下载接口使用 |
app.name |
string | 应用名称 |
app.version |
string | 最新版本号 |
app.updateContent |
string | 更新内容,可直接展示在更新弹窗中 |
app.fileName |
string | 安装包原始文件名,可作为保存文件名 |
app.fileSize |
number | 安装包字节数,用于展示体积与进度 |
app.md5 |
string | 安装包 MD5,下载完成后必须比对 |
app.downloadUrl |
string | 下载地址(相对路径),需拼接服务端地址使用 |
{
"code": 0,
"message": "ok",
"data": {
"hasUpdate": true,
"app": {
"id": 12,
"name": "买菜助手",
"version": "1.3.0",
"updateContent": "1. 新增今日特价专区\n2. 优化下单流程",
"fileName": "maicai-1.3.0.apk",
"fileSize": 23801472,
"md5": "ebf3e64a58553bb034a94caeec047ad5",
"downloadUrl": "/api/apps/12/download"
}
}
}
{
"code": 0,
"message": "ok",
"data": { "hasUpdate": false, "app": null }
}
{
"code": 1000,
"message": "缺少 categoryId 或 version 参数",
"data": null
}
版本比较规则
- 按
.拆分主版本号,逐段按数字比较:1.10.0 > 1.9.9、2.0.0 > 1.99.99。 - 不足三段自动补 0:
1.2等价于1.2.0。 - 支持预发布后缀:
1.3.0-beta小于1.3.0;两个预发布版本按后缀字符串比较。 - 只比较
-之前的数字部分,+之后的构建号不参与比较。 - 仅已上架(status=1)的记录参与检测,下架或未发布的版本不会被返回。
4下载安装包
把检测接口返回的 app.downloadUrl 拼在服务端地址后面即可下载,
无需任何请求头与鉴权参数。
{{ORIGIN}}/api/apps/12/download
- 响应为二进制文件流,
Content-Disposition中带有原始文件名。 - 每次请求成功后,服务端会把该 APP 的下载次数 +1,并记录下载日志(IP、时间)。
- 支持 HTTP Range 断点续传,大文件建议使用系统下载器或支持续传的 HTTP 客户端。
- 记录不存在或文件已被清理时返回
404,message为「APP 不存在」或「安装包文件不存在」。
check-update,不要调用下载接口。
5MD5 校验
下载到本地后计算文件 MD5,与 app.md5 比对,一致才允许安装。
这是防止安装包被截断或篡改的唯一手段,请勿省略。
# Windows
certutil -hashfile "D:\downloads\maicai-1.3.0.apk" MD5
# Linux / macOS
md5sum ./maicai-1.3.0.apk
const crypto = require('node:crypto');
const fs = require('node:fs');
function md5File(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
fs.createReadStream(filePath)
.on('data', (chunk) => hash.update(chunk))
.on('end', () => resolve(hash.digest('hex')))
.on('error', reject);
});
}
// 用法
const ok = (await md5File('D:/downloads/maicai-1.3.0.apk')) === remoteMd5;
6完整更新流程
- 读取本地版本:从构建信息或配置中拿到当前版本号,如
1.2.0。 - 检测更新:请求
GET /api/apps/check-update?categoryId={内置ID}&version={本地版本}。 - 判断结果:
hasUpdate=false直接结束;为true则展示更新弹窗(版本号 + 更新内容)。 - 用户确认:展示
app.version、updateContent与安装包体积(fileSize),由用户选择立即更新或稍后。 - 下载安装包:下载
{服务端地址}{app.downloadUrl},展示进度(已下载 / 总大小、速度)。 - 校验 MD5:计算本地文件 MD5 与
app.md5比对;不一致则删除文件并提示重新下载。 - 安装并收尾:调起系统安装;Android 需
REQUEST_INSTALL_PACKAGES权限与 FileProvider,Windows 可调用安装器或自更新程序。
hasUpdate 决定,
这样跨版本(如 1.0.0 → 2.0.0)也能正确升级。
7接入示例
以下示例均把服务端地址替换为本站地址 /,可直接改用。
Android(Kotlin + OkHttp)
data class ApiResult<T>(val code: Int, val message: String?, val data: T?)
data class CheckUpdateData(val hasUpdate: Boolean, val app: UpdateApp?)
data class UpdateApp(
val id: Long,
val name: String,
val version: String,
val updateContent: String?,
val fileName: String?,
val fileSize: Long,
val md5: String,
val downloadUrl: String,
)
private val BASE_URL = "{{ORIGIN}}"
private val CATEGORY_ID = 4 // 管理后台分类树中的 ID
private val CURRENT_VERSION = "1.2.0" // 当前客户端版本
fun checkUpdate(onResult: (UpdateApp?) -> Unit) {
val url = "$BASE_URL/api/apps/check-update?categoryId=$CATEGORY_ID&version=$CURRENT_VERSION"
val request = Request.Builder().url(url).get().build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) = onResult(null)
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string() ?: return onResult(null)
val json = JSONObject(body)
if (json.optInt("code") != 0) return onResult(null)
val data = json.getJSONObject("data")
if (!data.optBoolean("hasUpdate")) return onResult(null)
val app = data.getJSONObject("app")
onResult(
UpdateApp(
id = app.getLong("id"),
name = app.getString("name"),
version = app.getString("version"),
updateContent = app.optString("updateContent"),
fileName = app.optString("fileName"),
fileSize = app.getLong("fileSize"),
md5 = app.getString("md5"),
downloadUrl = app.getString("downloadUrl"),
)
)
}
})
}
// 下载地址 = BASE_URL + app.downloadUrl
// 下载完成后计算文件 MD5 与 app.md5 比对,一致再调起安装
iOS(Swift + URLSession)
struct ApiResult<T: Decodable>: Decodable {
let code: Int
let message: String?
let data: T?
}
struct CheckUpdateData: Decodable {
let hasUpdate: Bool
let app: UpdateApp?
}
struct UpdateApp: Decodable {
let id: Int
let name: String
let version: String
let updateContent: String?
let fileName: String?
let fileSize: Int64
let md5: String
let downloadUrl: String
}
let baseURL = "{{ORIGIN}}"
let categoryId = 4
let currentVersion = "1.2.0"
func checkUpdate() async throws -> UpdateApp? {
var components = URLComponents(string: "\(baseURL)/api/apps/check-update")!
components.queryItems = [
URLQueryItem(name: "categoryId", value: String(categoryId)),
URLQueryItem(name: "version", value: currentVersion),
]
let (data, _) = try await URLSession.shared.data(from: components.url!)
let result = try JSONDecoder().decode(ApiResult<CheckUpdateData>.self, from: data)
guard result.code == 0, let payload = result.data, payload.hasUpdate else { return nil }
return payload.app
}
Flutter(Dart)
import 'dart:convert';
import 'package:http/http.dart' as http;
const baseUrl = '{{ORIGIN}}';
const categoryId = 4;
const currentVersion = '1.2.0';
Future<Map<String, dynamic>?> checkUpdate() async {
final uri = Uri.parse('$baseUrl/api/apps/check-update')
.replace(queryParameters: {
'categoryId': '$categoryId',
'version': currentVersion,
});
final res = await http.get(uri);
if (res.statusCode != 200) return null;
final body = jsonDecode(res.body);
if (body['code'] != 0) return null;
if (body['data']['hasUpdate'] != true) return null;
return body['data']['app'] as Map<String, dynamic>;
// app['downloadUrl'] 拼接 baseUrl 后下载,app['md5'] 用于校验
}
Electron / Windows 客户端(Node.js)
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const BASE_URL = '{{ORIGIN}}';
const CATEGORY_ID = 4;
const CURRENT_VERSION = '1.2.0';
async function checkUpdate() {
const url = `${BASE_URL}/api/apps/check-update?categoryId=${CATEGORY_ID}&version=${CURRENT_VERSION}`;
const res = await fetch(url);
const body = await res.json();
if (body.code !== 0 || !body.data.hasUpdate) return null;
return body.data.app;
}
async function download(app, saveDir, onProgress) {
const res = await fetch(BASE_URL + app.downloadUrl);
if (!res.ok) throw new Error(`下载失败:${res.status}`);
const total = Number(res.headers.get('content-length')) || app.fileSize;
const filePath = path.join(saveDir, app.fileName || 'update.bin');
const file = fs.createWriteStream(filePath);
let received = 0;
for await (const chunk of res.body) {
received += chunk.length;
file.write(chunk);
onProgress?.({ received, total, percent: Math.round((received / total) * 100) });
}
await new Promise((resolve) => file.end(resolve));
const localMd5 = crypto.createHash('md5')
.update(fs.readFileSync(filePath))
.digest('hex');
if (localMd5 !== app.md5) {
fs.unlinkSync(filePath);
throw new Error('安装包 MD5 校验失败,请重新下载');
}
return filePath;
}
uni-app / H5(JavaScript)
const BASE_URL = '{{ORIGIN}}';
const CATEGORY_ID = 4;
const CURRENT_VERSION = '1.2.0';
// 检测更新
async function checkUpdate() {
const url = `${BASE_URL}/api/apps/check-update?categoryId=${CATEGORY_ID}&version=${CURRENT_VERSION}`;
const res = await fetch(url);
const { code, data } = await res.json();
if (code !== 0 || !data.hasUpdate) return null;
return data.app;
}
// 下载并显示进度(浏览器 / H5)
async function downloadWithProgress(app, onProgress) {
const res = await fetch(BASE_URL + app.downloadUrl);
if (!res.ok) throw new Error('下载失败');
const total = Number(res.headers.get('content-length')) || app.fileSize;
const reader = res.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
onProgress?.({ received, total, percent: Math.round((received / total) * 100) });
}
return new Blob(chunks); // H5 可直接 saveBlob,原生端请使用下载插件
}
8常见问题
检测接口需要登录或 API Key 吗?
不需要。检测与下载都是公开接口,只需分类 ID 与版本号,可在客户端硬编码。
为什么 hasUpdate 一直是 false?
- 分类 ID 不对:确认内置 ID 与发布时选择的分类一致,可在管理后台分类树核对。
- 版本号没有变大:例如客户端写的是
1.3.0,而服务端最新也是1.3.0。 - 发布的是草稿或已下架:只有
status=1(已上架)的版本参与检测。 - 客户端版本号格式异常,例如
v1.2.0带着前缀字母,会被按数字解析为 0。
同一分类下有多个历史版本会怎样?
服务端会在所有比当前版本高的记录中取版本号最大的一个返回,历史版本不会被删除,仍可在下载中心看到。
下载次数是怎么统计的?
每次成功请求下载接口即 +1,并写入下载日志(APP ID、IP、时间)。管理后台「APP 管理」可查看累计次数。
可以在 APP 内直接展示下载中心页面吗?
可以,直接用 WebView / 内置浏览器打开 {{ORIGIN}}/,页面已适配移动端,支持分类浏览、搜索与进度下载。
服务端地址换了怎么办?
检测与下载都用相对路径返回(downloadUrl),客户端只需修改内置的 BASE_URL。建议域名保持稳定,或通过配置下发。