享 享耍插件仓库

享耍插件开发文档

本页公开可读,覆盖插件包结构、manifest 字段、路由与后端分层、独立前端页、打包上传、安装生命周期、仓库签名接口、开发规范、SQL 防注入与提示词模板。按左侧目录可直接跳转。

无需登录 14 节完整规范 附 5 段通用提示词 4 个插件专属文档 与后台文档同源

01分发模型

仓库只负责「存放与分发」,业务站负责「安装与运行」。插件的主代码全部放在仓库的 zip 包里,业务站拉取后解包到自己的插件目录,前端页面随之部署到 /app/{slug}/。

  • 一次上传:包上传到仓库后自动读取清单,记录版本、大小与 SHA-256。
  • 多站安装:每个业务站用独立的接入账号签名请求,安装状态按会员单独保存。
  • 逐步解耦:插件有自己的文件夹与表前缀,未来可以整包、整表迁移到独立库。
仓库与业务站是两个独立部署:仓库使用 phub_ 前缀,业务站的插件表使用各自 manifest 中声明的 p_{slug}_ 前缀。

02插件包结构

插件包解压后,根目录必须直接包含 manifest.json(允许外面多包一层目录,但只有一层)。

myapp-1.0.0.zip
├── manifest.json              清单(必须)
├── routes.php                 该插件的路由表(可选)
├── migrations/
│   ├── 001.sql                建表脚本,按文件名顺序执行
│   └── 002.sql                后续增量
├── src/
│   ├── Controller.php         接口入口,命名空间 OOO\Plugins\Myapp
│   ├── Service.php            业务逻辑
│   └── Model.php              数据访问(只用自己的表前缀)
├── docs/
│   ├── README.md              插件详细简介(上传时写入仓库数据库)
│   └── PROMPT.md              本插件专属提示词
└── web/
    └── index.html             独立前端页,安装后部署到 /app/myapp/

解包规则:src/ 会自动映射命名空间 OOO\Plugins\{Studly},web/ 会整体复制到业务站的 app/{slug}/。只提供前端页的轻量插件可以不写 src/ 与 migrations/。

文件 / 目录是否必须作用
manifest.json必须插件身份、版本、表前缀、导航与权限声明。
routes.php可选该插件专属 API 路由,未提供则只跑前端页。
migrations/*.sql可选按文件名顺序执行,必须幂等。
src/可选Controller / Service / Model,命名空间自动注册。
docs/README.md可选插件详细简介。清单未写 intro 时,仓库上传会自动读取它并写入数据库。
docs/PROMPT.md可选该插件的专属提示词,便于后续维护时直接复用。
web/可选独立前端页,部署到 /app/{slug}/。

03文档结构

仓库源码包内的文档与本站内容保持同一口径,修改规范时请同步更新,避免「页面一套、文件一套」。

plugin-hub/
├── README.md                 部署、接入与账号说明
├── docs/
│   ├── ARCHITECTURE.md       仓库架构、数据表与签名接口
│   ├── PLUGIN-SPEC.md        插件开发规范(对应 02–12 节)
│   ├── PROMPTS.md            通用提示词模板(对应 14 节)
│   └── plugins/              内置插件文档,每个插件一个子目录
│       ├── wangzhi/          README.md(简介)+ PROMPT.md(提示词)
│       ├── biji/
│       ├── wangpan/
│       └── caishui/
├── app/                      MVC 控制器、视图与路由
├── lib/                      内核、SqlGuard、验证码
├── assets/                   后台、介绍页与文档样式
├── packages/                 插件 zip 存放目录(Web 禁止访问)
├── schema.sql                仓库表结构
└── index.php                 唯一入口
  • 在线文档:/docs 无需登录即可阅读;/admin/docs 在后台内嵌同一份内容。
  • 文档即规范:PLUGIN-SPEC.md 与在线文档冲突时,以在线文档为准,并回头修正文件。
  • 提示词独立:PROMPTS.md 维护可直接复制的通用提示词;单个插件的提示词放在 docs/plugins/{slug}/PROMPT.md。
  • 一插件一目录:docs/plugins/{slug}/README.md 是插件简介的权威版本,打包时一并放进 zip 的 docs/。
  • 版本记录:规范发生破坏性变化时,在 README 顶部标注日期与不兼容项。

04manifest.json

清单决定插件能否被扫描到。缺少 name、version,或 slug、table_prefix 不合规的目录会被直接忽略。

字段必填说明
slug是插件唯一标识,小写字母开头,仅 a-z0-9_,长度 2–32。必须与目录名、请求参数一致。
name是展示名称,如「网址」。
version是语义化版本,如 1.0.0。仓库以它作为包版本。
description否一句话介绍,显示在插件中心卡片上。
intro否插件详细简介(纯文本,可换行,建议 300–1500 字)。上传时写入仓库 phub_packages.intro,在「插件包」列表点「简介」可查看;留空则自动读取包内 docs/README.md。
icon否图标标识,默认 grid,由前端图标库匹配。
table_prefix否数据表前缀,^[a-z0-9_]{2,20}$,默认 p_{slug}_。
nav否{ label, order, path };path 默认 /app/{slug}/。
permissions否权限声明数组,如 ["notes.read","notes.write"]。
requires否依赖的其它插件 slug 数组。
builtin否是否随业务站内置,仓库分发的外部插件保持 false。
{
  "slug": "myapp",
  "name": "我的应用",
  "version": "1.0.0",
  "description": "一句话说清楚它解决什么问题。",
  "intro": "详细简介:解决什么问题、主要功能、数据表与安全约定、安装后的使用步骤。",
  "icon": "grid",
  "table_prefix": "p_myapp_",
  "nav": { "label": "我的应用", "order": 60, "path": "/app/myapp/" },
  "permissions": ["myapp.read", "myapp.write"],
  "requires": [],
  "repository": { "type": "remote", "package": "myapp-1.0.0.zip" }
}
同一个 slug 的 table_prefix 一经发布不要修改,否则升级后会读不到旧数据。

05路由表

routes.php 返回数组,每项为 [METHOD, 路径, '完整控制器类@方法', 中间件数组]。路径不带前导斜杠,内核会自动追加 plugin:{slug} 中间件,用于校验会员是否已安装并启用。

<?php
return array(
    array('GET',    'myapp/items',      'OOO\\Plugins\\Myapp\\Controller@items',  array('auth')),
    array('POST',   'myapp/items',      'OOO\\Plugins\\Myapp\\Controller@create', array('auth')),
    array('PUT',    'myapp/items/{id}', 'OOO\\Plugins\\Myapp\\Controller@update', array('auth')),
    array('DELETE', 'myapp/items/{id}', 'OOO\\Plugins\\Myapp\\Controller@delete', array('auth')),
);
  • {id} 会被编译成正整数占位,控制器方法按顺序接收 (Request $request, $id)。
  • 请求地址为 /api/index.php?r=myapp/items,静态化后等价于 /api/v1/myapp/items。
  • 未安装:返回 403 plugin_not_installed;已安装但停用:403 plugin_disabled。
  • 路径与其它插件重名会被拒绝,统一使用 {slug}/... 前缀。
不需要自定义路由的插件可以不放 routes.php,只提供前端页也能正常安装。

06后端分层

Controller 继承 BaseController,方法签名固定为 (Request $request) 或 (Request $request, $id);统一用 $this->ok() 返回 JSON,用 $this->user($request) 取当前会员。

<?php
namespace OOO\Plugins\Myapp;

use OOO\Core\HttpException;
use OOO\Core\Request;
use OOO\Http\Controllers\BaseController;

class Controller extends BaseController
{
    public function items(Request $request)
    {
        $user = $this->user($request);
        $page = max(1, (int) $request->queryInt('page', 1));
        $items = (new Model())->allByUser((int) $user['id'], 30, ($page - 1) * 30);
        return $this->ok(array('items' => $items));
    }

    public function create(Request $request)
    {
        $user = $this->user($request);
        $title = trim($request->inputString('title'));
        if ($title === '') {
            throw new HttpException('标题不能为空', 400);
        }
        $id = (new Model())->create((int) $user['id'], $title);
        return $this->ok(array('id' => $id), 201);
    }
}

Model 继承 BaseModel,只使用 manifest 里声明的表前缀,禁止跨插件读写:

<?php
namespace OOO\Plugins\Myapp;

use OOO\Models\BaseModel;

class Model extends BaseModel
{
    const TABLE = 'p_myapp_items';

    public function allByUser($userId, $limit = 30, $offset = 0)
    {
        $limit = max(1, min(200, (int) $limit));
        $offset = max(0, (int) $offset);
        return $this->fetchAll(
            'SELECT * FROM ' . self::TABLE . ' WHERE user_id = ? ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset,
            array((int) $userId)
        );
    }
}
PDO 关闭了模拟预处理,LIMIT ? OFFSET ? 不能绑定参数;分页请先把值转成整数再拼接。

分层约定

  • Controller 只做参数校验、权限判断与响应组装,不写 SQL。
  • Service 负责跨步骤业务逻辑,例如「校验归属 → 写入 → 记录事件」。
  • Model 只访问自己的表,返回数组或标量,不抛业务文案。
  • 错误统一抛 HttpException,由内核转换为 {ok:false,message}。

07建表与表前缀

migrations/*.sql 按文件名顺序执行,单文件内以 ; 加换行分隔语句。执行异常会被忽略,所以语句必须写成幂等形式。

CREATE TABLE IF NOT EXISTS p_myapp_items (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id INT UNSIGNED NOT NULL,
  title VARCHAR(120) NOT NULL DEFAULT '',
  created_at DATETIME NOT NULL,
  PRIMARY KEY (id),
  KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 所有表统一带 table_prefix,不要读写公共表。
  • 时间字段用 DATETIME,写入当前时间后再按时间排序或查询。
  • 增量变更新增 002.sql,不要回头改已发布的脚本。
  • 卸载默认不删表,避免误删数据;确需清理请单独提供管理入口。
  • 索引至少覆盖 user_id 与常用排序字段,列表查询不要全表扫描。

08独立前端页

web/ 里的内容会在安装时复制到业务站的 app/{slug}/,形成 https://站点/app/{slug}/ 独立链路,不依赖主应用打包。

  • 入口文件固定为 web/index.html,相对引用业务站公共脚本 ../js/config.js 与 ../js/api.js。
  • 登录态由 api.js 统一从 sessionStorage / localStorage 读取并带上 Authorization: Bearer,页面无需自己拼请求头。
  • 窄屏要有一行两个图标的面包屑菜单,与主应用 H5 顶栏保持一致。
  • 深链统一使用 ?view=xxx 或 hash,刷新后不丢状态。
  • 空态、加载态、错误态必须齐全;错误提示用中文,不直接暴露堆栈。
<script src="../js/config.js"></script>
<script src="../js/api.js?v=1"></script>
<script>
window.OOOApi.request('myapp/items').then(function (res) {
  render(res.items);
}).catch(function (err) {
  toast(err.message);
});
</script>

09打包与上传

  1. 确认 manifest.json 的 slug、version 与本次发布一致。
  2. 把插件目录内容(不含最外层目录)打成 zip,文件名建议 {slug}-{version}.zip。
  3. 不要把 node_modules、.git、测试脚本、临时文件打进包里。
  4. 到「插件包」页上传,仓库会自动读取清单并计算 SHA-256;清单里的 intro(或包内 docs/README.md)会一并写入数据库,列表里的「简介」按钮可查看。
  5. 上传后用「下载」自测一次,确认解压后的目录就是 manifest.json 所在层级。
同名同版本重复上传会覆盖文件与 SHA-256,并同步刷新简介;需要保留旧包时请先提升 version。

09A更新包制作与发布

更新包就是版本号更高的完整插件包,不是差量补丁。业务站按版本号判断是否有更新,下载后整目录替换,因此更新包里必须包含完整的 manifest.json、后端代码、前端页面与迁移脚本。

版本号规则

类型示例适用场景
修订版1.1.0 → 1.1.1修 Bug、改文案、调整样式
功能版1.1.1 → 1.2.0新增页面、接口或配置项
不兼容版1.2.0 → 2.0.0数据结构或接口不向下兼容,需要迁移说明
新版本号必须大于仓库中该插件当前最新版本,否则业务站不会提示更新(同版本重传只用于修复包文件本身)。

制作步骤

  1. 复制当前插件目录,修改 manifest.json 的 version(slug、name 保持不变)。
  2. 结构变更时在 migrations/ 下新增 SQL,文件按名称顺序执行,语句必须幂等(CREATE TABLE IF NOT EXISTS / 先判空再 ALTER)。
  3. 更新 docs/README.md 或 manifest.intro,写清本次更新内容(会同步到插件简介)。
  4. 本地跑一遍 php -l 与核心流程自测,确认 web/ 内资源自包含,不引用包外路径。
  5. 把插件目录内容(不含最外层目录)打成 {slug}-{version}.zip,不要带 node_modules、.git、测试脚本。

上传位置

  1. 打开「插件包」页,列表已按插件归类折叠,展开目标插件。
  2. 在该插件分组内点「上传更新包」,选择 ZIP;分组表单会带上 slug,服务端校验 manifest 的 slug 必须一致,防止把 A 插件的包传到 B 插件下面。
  3. 上传成功后新版本出现在该插件版本列表顶部并标记「最新」;旧版本保留在同一分组内,可随时下架或删除。
  4. 首次发布的全新插件,仍使用页面顶部「上传新插件包」。

完整示例:shop 1.1.1 → 1.2.0

下面以「自营网店」新增订单备注为例,给出更新包需要改动的全部文件与打包命令,可直接照抄替换。

1. 目录变化

shop-1.2.0/                     打包时只打这一层内容,不要带 shop-1.2.0 外层目录
├── manifest.json               version 1.1.1 → 1.2.0
├── routes.php                  不变
├── migrations/
│   ├── 001_init.sql            历史文件,禁止修改
│   ├── 002_orders.sql          历史文件,禁止修改
│   └── 003_order_remarks.sql   ← 本次新增
├── src/
│   ├── Controller.php          新增备注接口
│   ├── Service/RemarkService.php  本次新增
│   └── Model/OrderModel.php    增加备注读写
├── docs/
│   ├── README.md               追加 1.2.0 更新说明
│   └── PROMPT.md               不变
└── web/
    └── index.html              订单详情页增加备注输入框

2. manifest.json

{
  "slug": "shop",
  "name": "自营网店",
  "version": "1.2.0",
  "description": "商品、订单、活动与对账一体化的自营网店。",
  "intro": "1.2.0:新增订单备注,支持在订单详情编辑、列表检索;优化订单列表首屏加载。",
  "icon": "bag",
  "table_prefix": "p_shop_",
  "nav": { "label": "网店", "order": 30, "path": "/app/shop/" },
  "permissions": ["shop.read", "shop.write"],
  "requires": []
}
slug 与 table_prefix 必须和上一版完全一致;只改 version、intro 以及本次确实变动的内容。

3. migrations/003_order_remarks.sql

-- 新增表:可重复执行,重复安装不会报错
CREATE TABLE IF NOT EXISTS p_shop_order_remarks (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id INT UNSIGNED NOT NULL DEFAULT 0,
  order_id INT UNSIGNED NOT NULL DEFAULT 0,
  remark VARCHAR(200) NOT NULL DEFAULT '',
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  PRIMARY KEY (id),
  KEY idx_user_order (user_id, order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

如果必须给已有表加列,MySQL 5.7 不支持 ADD COLUMN IF NOT EXISTS,按下面的存在性判断写法:

-- 给 p_shop_orders 加 remark 列(存在则跳过)
SET @exists := (
  SELECT COUNT(*) FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA = DATABASE()
    AND TABLE_NAME = 'p_shop_orders'
    AND COLUMN_NAME = 'remark'
);
SET @ddl := IF(@exists = 0,
  'ALTER TABLE p_shop_orders ADD COLUMN remark VARCHAR(200) NOT NULL DEFAULT '''' AFTER status',
  'DO 0');
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
业务站每次安装 / 更新都会把 migrations/*.sql 全部重跑一遍,且执行器会忽略 SQL 报错。所以每个迁移文件都必须幂等;改结构前先在测试库手工执行一遍确认生效。

4. docs/README.md 追加更新说明

# 自营网店

## 1.2.0(2026-10-28)

- 新增:订单备注,支持在详情页编辑、列表按备注检索
- 优化:订单列表首屏加载速度
- 数据:新增表 p_shop_order_remarks(幂等建表,无需手工迁移)
- 注意:升级前系统会自动备份 p_shop_ 前缀下的全部表,
  可在会员中心「数据备份」恢复;插件数据全站共享,恢复会影响所有会员。

5. 打包与本地校验

cd /path/to/shop-1.2.0

# 只打当前目录内容,排除依赖、版本库、测试与临时文件
zip -r ../shop-1.2.0.zip . \
  -x "node_modules/*" ".git/*" "*.DS_Store" "*.log" "tests/*"

# 校验 1:根目录必须直接是 manifest.json,不能多套一层目录
unzip -l ../shop-1.2.0.zip | head -20

# 校验 2:记录 SHA-256,上传后与仓库显示值比对
sha256sum ../shop-1.2.0.zip        # macOS: shasum -a 256 ../shop-1.2.0.zip

# 校验 3:PHP 语法
find . -name "*.php" -print0 | xargs -0 -n1 php -l

6. 上传与线上验证

  1. 后台「插件包」→ 展开 shop 分组 → 「上传更新包」→ 选择 shop-1.2.0.zip。
  2. 确认 1.2.0 出现在该分组顶部并标记「最新」,1.1.1 仍在列表中。
  3. 业务站会员中心等待索引刷新(≤5 分钟)→ 卡片显示「有更新 v1.2.0」。
  4. 点「更新到最新版」→ 确认弹窗 → 成功提示中会带升级前备份编号。
  5. 打开插件验证新功能;在「数据备份」中可见一条 update 记录(表数、行数、版本区间)。
  6. 需要回退数据时,在备份列表对升级前那份点「恢复」;代码回滚由升级失败流程自动完成, 因此上线新版后先观察一段时间,再考虑删除旧版本。
本地可先用 curl 直接下载验证包可访问性,但签名接口需要接入账号,正常以「后台下载」或业务站安装结果为准。

业务站更新行为

  • 提示:业务站每 5 分钟刷新仓库索引,会员中心卡片在版本号后显示「有更新 vX」,并在操作区最前出现「更新到最新版」按钮。
  • 安装:新安装一律拉取仓库最新版本。
  • 自动备份:升级前按插件 table_prefix 全量快照,存为 api/data/plugin-backups/{slug}/*.json.gz,每个插件默认保留最近 5 份。
  • 自动回滚:下载校验、解包或迁移失败时,自动恢复备份数据,并尝试重新拉取旧版本代码。
  • 手动恢复:卡片「数据备份」入口可查看备份、立即备份、恢复或删除。
插件业务数据是全站共享的(不是按会员隔离),恢复备份会把这些表回到备份时点,影响所有会员的该插件数据,请在低峰期操作并先手动备份当前数据。

10安装与启停

  • 浏览:业务站拉取仓库索引,把未下载的插件展示为「可安装」。
  • 安装:下载 zip → SHA-256 校验 → 解包到临时目录 → 原子替换插件目录 → 复制 web/ 到 /app/{slug}/ → 按顺序执行 migrations → 写入会员安装记录。安装时会自动选择仓库中的最新版本。
  • 更新:会员中心检测到更高版本后,一键升级;升级前自动备份插件全部数据,失败自动回滚数据并尝试恢复旧版代码。
  • 启用 / 停用:只改变该会员的启用状态,不删文件、不动数据。
  • 卸载:仅删除会员安装记录,插件文件与数据表保留,方便再次安装。

安装状态表为 user_plugins(user_id + slug 唯一),全局包信息记录在 plugin_packages。

11仓库接口

业务站通过签名请求访问仓库,签名串为 account\ntimestamp\n{path?query},使用 HMAC-SHA256。

接口方法用途
/api/hub/indexGET插件索引:slug、名称、版本、描述。
/api/hub/package?slug=GET下载 zip,响应头 X-Package-Sha256 供校验。
/api/hub/reportPOST回报 download / install / update / uninstall 事件与站点。
X-Hub-Account: xiangshua-site
X-Hub-Timestamp: 1770000000
X-Hub-Signature: hash_hmac('sha256', account + "\n" + ts + "\n" + path, secret)

账号与密钥在「账号与密钥」页创建,可随时轮换;时间戳偏移过大会被拒绝。

账号类型与后台登录

仓库账号分为四类:管理员(全部权限)、发布者(上传与上下架插件包)、审计只读(仅查看数据)、接入账号(仅用于签名调用,不能登录后台)。三类可登录账号各自使用独立 Cookie 会话(phub_s_admin / phub_s_publisher / phub_s_viewer),同一浏览器可同时登录多个账号,打开多个窗口也互不影响;退出登录只清理当前角色的会话。

登录页强制图形验证码(4 位、5 分钟有效、一次性使用、最多尝试 6 次),由 /admin/captcha 签发,服务端校验通过后立即作废并写入 phub_sessions 记录。

12插件开发规范

规范分两部分:命名边界(必须遵守)与交付清单(提审前逐项确认)。违反命名边界会被仓库或业务站直接拒绝加载。

命名与边界

  • slug:小写字母开头,a-z0-9_,2–32 位,与目录名、包名一致。
  • 命名空间:OOO\Plugins\{Studly},如 my_app → OOO\Plugins\MyApp。
  • 表前缀:只使用自己的前缀,禁止读写 users、sessions 等公共表。
  • 包内路径:不得出现 ..、绝对路径与盘符,仓库会直接拒绝。
  • 接口前缀:路由必须以 {slug}/ 开头,避免与其它插件或内核路由冲突。
  • 输出:前端渲染用户内容必须转义,富文本另行白名单过滤。
  • 权限:接口默认要求登录;管理员能力不要暴露在普通入口。

交付清单

  • manifest.json 字段完整,slug / version 与包名一致。
  • migrations 全部幂等,重复执行不报错、不丢数据。
  • 所有 SQL 走预处理绑定,分页参数转整数后再拼接。
  • 每个接口都校验 user_id 归属,不能越权读取他人数据。
  • 前端页有加载态、空态、错误态;窄屏一行两个图标。
  • 不使用 Composer,不依赖业务站未提供的扩展。
  • 包内无 node_modules、.git、测试脚本与密钥文件。
  • 上传后从仓库下载一次,本地解压核对目录层级与文件完整。

slug ^[a-z][a-z0-9_]{1,31}$ prefix ^[a-z0-9_]{2,20}$ zip 无目录穿越 SHA-256 校验

13SQL 防注入

仓库所有 SQL 必须经统一入口 hub_query() / hub_prepare() / hub_exec(),底层调用与业务站一致的 SqlGuard::assertSafe():先抹掉字符串字面量,再拒绝多语句(; 后跟可执行内容)与 --、/* */、# 注释截断;动态表名 / 列名必须通过白名单校验。参数仍由 PDO 预处理绑定,并关闭模拟预处理。

插件侧同样适用:Model 内不要拼接用户输入;需要动态排序字段时,用数组白名单映射,而不是直接拼 ORDER BY 参数。

// 正确:字段白名单 + 预处理
$allowed = array('created_at', 'title');
$order = in_array($sort, $allowed, true) ? $sort : 'created_at';

$stmt = $this->prepare(
    'SELECT * FROM ' . self::TABLE . ' WHERE user_id = ? ORDER BY ' . $order . ' DESC LIMIT ' . (int) $limit,
    array((int) $userId)
);

14提示词模板

下面五段是通用模板,可直接交给 AI。先替换花括号内容,再按需删减;一次只让 AI 做一件事,产出更稳定。更新包规则见 09A 更新包制作与发布。

内置插件各自附带专属提示词:网址 docs/plugins/wangzhi/PROMPT.md、记事本 docs/plugins/biji/PROMPT.md、网盘 docs/plugins/wangpan/PROMPT.md、财税 docs/plugins/caishui/PROMPT.md。它们会随插件包放进 zip 的 docs/,二次开发时优先用专属提示词,通用模板只用于从零起新插件。

A. 生成新插件骨架

你是享耍插件开发者。请按以下规范生成一个完整插件,只输出文件内容,不要解释。

插件信息:
- slug:{slug,小写字母开头,仅 a-z0-9_}
- 名称:{name}
- 版本:1.0.0
- 表前缀:{p_slug_}
- 一句话介绍:{description}
- 核心功能:{列出 3-5 条}
- 需要的数据字段:{列出字段与类型}

必须产出:
1. manifest.json:包含 slug、name、version、description、icon、table_prefix、nav、permissions、requires。
2. migrations/001.sql:CREATE TABLE IF NOT EXISTS {prefix}{table},utf8mb4,含 user_id 与 created_at。
3. src/Model.php:继承 OOO\Models\BaseModel,常量为自己的表名,SQL 全部预处理绑定。
4. src/Service.php:业务逻辑,校验入参与数据归属。
5. src/Controller.php:继承 OOO\Http\Controllers\BaseController,方法签名 (Request $request) 或 (Request $request, $id),统一用 $this->ok([...]) 返回。
6. routes.php:返回 [METHOD, 'slug/路径', 'OOO\Plugins\{Studly}\Controller@方法', ['auth']],路径不带前导斜杠。
7. web/index.html:独立前端页,引用 ../js/config.js 与 ../js/api.js,用 window.OOOApi.request('slug/路径') 调用;窄屏一行两个图标的面包屑;深色主题。

约束:
- 不读写公共表;不引入 Composer 依赖;不使用框架外全局函数。
- 列表分页先把页码与每页条数转整数再拼接 LIMIT。
- 所有用户输入输出转义;错误用 HttpException 并给出中文提示。
- 文件放在插件根目录,不要多包一层文件夹。

B. 改造现有插件页面

你是享耍插件前端开发者。只修改我指定的文件,不要重构无关代码。

插件:{slug}
目标页面:{web/index.html 或指定文件}
要改的内容:{逐条描述,例如「卡片一行 4 个」「标题最多 16 字」「移动端一行 2 个」}
保持不变:{列出不能动的结构、类名、接口}

要求:
- 沿用现有 CSS 变量与组件类名,不引入新框架。
- 交互必须可用键盘操作,按钮有 aria-label 或可见文字。
- 不破坏 ?view= 深链与刷新后的状态。
- 完成后说明改了哪些文件、每个文件的改动点与自测步骤。

C. 审查与修复

你是享耍插件代码审查员。请审查我提供的插件文件,按严重程度排序输出问题,不要直接大改。

检查项:
1. SQL 是否全部预处理绑定;分页是否整数拼接;动态字段是否白名单。
2. 是否读写公共表;表名是否使用自己的前缀。
3. 每个接口是否校验 user_id 归属;是否存在越权。
4. 输出是否转义;是否泄露密钥、路径或堆栈。
5. migrations 是否幂等;重复执行是否报错。
6. 前端加载/空/错误态是否齐全;窄屏是否一行两个图标。

输出格式:
- P0/P1/P2 分级列表,每条给出文件、行号、风险与最小修复方案。
- 最后列出需要我确认的问题,不要自行假设业务规则。

D. 制作并发布更新包

你是享耍插件仓库的发布者。我要为 {slug} 发布一个更新包。

当前版本:{x.y.z}
目标版本:{x.y.w}
改动内容:{新增 / 修复 / 结构变更}
是否改表:{是,涉及哪些表与字段 / 否}

请按以下规范产出:
1. 版本号:修订用 x.y.(z+1),向后兼容的新功能用 x.(y+1).0,不兼容用 (x+1).0.0;
   同 slug + 同 version 重传只用于修复包文件。
2. manifest.json:slug、name、table_prefix 不变,version 递增,intro 或 docs/README.md 写明更新说明。
3. migrations:只新增文件(下一个序号.sql),不得修改历史迁移;语句必须幂等
   (CREATE TABLE IF NOT EXISTS / 可重复执行的 ALTER),按文件名顺序执行。
4. 打包:完整包(src/、web/、migrations/、docs/),排除 node_modules、.git、测试脚本、密钥;
   zip 内不套外层目录,命名 {slug}-{version}.zip。
5. 上传位置:后台「插件包」→ 展开 {slug} 分组 → 「上传更新包」,表单会带 slug,
   服务端校验 manifest.slug 一致;上传后确认该版本置顶并标记「最新」,旧版本仍在。
6. 验收:业务站会员中心出现「有更新 v{version}」→ 一键更新成功;升级前自动备份
   {table_prefix} 下全部表到 api/data/plugin-backups/{slug}/*.json.gz,失败可回滚。

输出:文件清单、manifest 与新增迁移内容、上传与验证步骤。不要改动 slug 与 table_prefix。

E. 站点侧升级与备份改造

你是享耍业务站后端 + 前端开发者。目标:会员中心支持「自动安装最新版 + 有更新提示 + 一键更新 + 数据备份恢复」。

后端(OOO\Plugin\PluginManager):
1. catalog() 增加 latest_version 与 has_update(本地 manifest 版本 vs 仓库索引,version_compare 比较);
   内置插件与未安装项不提示更新。
2. install():本机缺包按仓库最新版本下载;本机版本落后时走升级流程。
3. update():校验已安装、非内置、仓库确有更高版本,再执行 runUpgrade()。
4. runUpgrade():先 backup() 全量快照插件 table_prefix 下的表 → 下载校验 SHA-256 → 解包 → migrations;
   任一步失败 restoreBackup() 并尽力回拉旧版本,最后重新抛出原错误。
5. 备份:文件放 api/data/plugin-backups/{slug}/*.json.gz(JSON:tables → schema + rows),
   元数据入 plugin_backups(slug/trigger_type/version_from/version_to/table_count/row_count/size/file),
   保留最近 5 份自动剪枝;restore() 关闭外键检查后 DROP + 原建表语句 + 逐行回填。
6. 路由:POST plugins/{slug}/update、GET|POST plugins/{slug}/backups、POST plugins/{slug}/backups/{id}/restore、
   DELETE plugins/{slug}/backups/{id},全部带 auth 中间件。

前端(会员中心 app.js / styles.css):
- 版本号后显示「有更新 vX」徽标;动作区顺序:更新到最新版 → 打开 → 启停 → 数据备份 → 删除。
- 备份弹窗展示类型/时间/版本/表数/行数/大小,支持立即备份、恢复、删除;
  恢复前二次确认并提示「插件数据全站共享,会覆盖所有会员」。

输出:改动文件清单、接口返回示例、验证步骤(含更新失败回滚的验证方法)。