Install
openclaw skills install website-skeleton一句话说需求,AI 生成完整前后端网站并自动部署到 EdgeOne Pages。支持电商栈(Auth/购物车/支付)、AI 栈(SSE 流式对话)、管理后台。
Security findings on these releases were reviewed by staff and cleared for public use.
openclaw skills install website-skeleton版本: 3.0 · 日期: 2026-05-06 · Phase 4A + 4B 实现完成 一句话描述: 用户说一句话,AI 生成完整前后端网站,自动部署到 EdgeOne Pages。
一次设计,无限复用 = 5 个模块 × 3 个场景 × 1 个部署平台
将"建站"拆解为 Layer 0 基础设施 + Layer 1 能力栈 + Layer 2 可选增强:
| 层级 | 内容 | 性质 |
|---|---|---|
| Layer 0(Core) | SPA 骨架 + Auth + Middleware + EventBus | 必选,不可裁剪 |
| Layer 1(Stack) | 🛒 电商栈 · 🤖 AI 栈 · 📊 管理栈 | 按需组合,互不依赖 |
| Layer 2(Addon) | SEO · Analytics · i18n | 可选增强 |
场景模板优先:用户选"电商"、"AI 助手"或"管理后台"场景,不选模块——模块由模板自动组合。
┌──────────────────────────────────────────────────────────────┐
│ Platform Middleware(middleware.js) │
│ ① CORS 预检(OPTIONS) │
│ ② CSP Header 注入 │
│ ③ 轻量 Bearer 检查(公开路径放行) │
│ ④ 支付回调 IP 白名单 → 直接 return,不进 Edge Middleware │
└──────────────────────────────────────────────────────────────┘
↓(非回调路径)
┌──────────────────────────────────────────────────────────────┐
│ Edge Functions Middleware(V8 + KV) │
│ ⑤ JWT 详细校验(crypto.subtle) │
│ ⑥ KV session 验证 │
│ ⑦ KV 限流计数器(滑动窗口) │
└──────────────────────────────────────────────────────────────┘
| 运行时 | 存储 | 职责 | 说明 |
|---|---|---|---|
| Edge Functions(V8) | KV | Auth 登录/me、Products 公开读、Cart、Orders 读、AI History 读、幂等锁 | 延迟敏感、无密钥 |
| Cloud Functions(Node) | D1 | Auth 注册/bcrypt、Payment 创建/回调、Admin CRUD、Orders 创建/取消、AI SSE 流 | 密钥操作、复杂事务 |
⚠️ 平台约束(EdgeOne Pages):
- KV 仅 Edge Functions 可用,Cloud Functions 无法访问
- Cloud Functions 目录名必须为
cloud-functions/- bcrypt 必须在 Cloud Functions 中执行
website-skeleton/
├── SKILL.md # 本文件,Skill 核心指令
│
├── templates/ # 场景预设模板
│ ├── e-commerce.json # 🛒 电商场景
│ ├── ai-assistant.json # 🤖 AI 助手场景
│ └── saas-admin.json # 📊 SaaS 管理后台场景
│
├── sharing/ # 跨运行时共享(构建时同步)
│ ├── types.ts # User/Product/Cart/Order/AISession 接口
│ ├── constants.ts # OrderStatus/UserRole/APIPaths 枚举
│ ├── validators.ts # 共享输入校验
│ └── kv-keys.ts # KV key 命名(含租户前缀占位)
│
├── client/ # 前端 SPA
│ ├── index.html
│ └── src/
│ ├── app.js # 启动 + History API 路由
│ ├── utils/
│ │ ├── event-bus.js # 全局事件总线(P0)
│ │ ├── router.js # History API 路由 + AuthGuard
│ │ ├── escape-html.js # XSS 防护
│ │ └── storage.js # localStorage 封装
│ ├── services/
│ │ ├── api.js # 统一客户端 + 拦截器
│ │ ├── auth.js # 内存 AuthService
│ │ ├── cart.js # 双模式购物车
│ │ └── ai.js # SSE 流式 AI
│ └── components/ # 组件清单
│
├── middleware.js # Platform Middleware
│
├── db/ # 数据库迁移
│ ├── migrations/
│ │ └── 001_init.sql # 建表脚本
│ └── seed.sql # 测试数据
│
├── docs/
│ └── env-vars.md # 环境变量矩阵
│
├── edge-functions/ # Edge Functions(V8 + KV)
│ ├── _middleware.js # JWT 校验 + KV session + 限流
│ ├── api/
│ │ ├── auth/login.js # JWT 签发(Cookie) + KV session
│ │ ├── auth/me.js # KV session 读取
│ │ ├── auth/refresh.js # RT 轮换(KV version 乐观锁)
│ │ ├── auth/logout.js # 清除 Cookie + KV session
│ │ ├── internal/idempotency.js # Edge 原子幂等锁
│ │ ├── products/list.js # KV 缓存 + Cloud D1 回源
│ │ ├── products/[id].js
│ │ ├── products/categories.js
│ │ ├── cart/*.js # KV 购物车
│ │ ├── orders/list.js # D1 订单读取
│ │ ├── orders/[id].js
│ │ └── ai/history.js # KV 读取 AI 会话历史
│ └── utils/
│ ├── kv-helper.js
│ ├── jwt-helper.js # crypto.subtle HS256
│ ├── rate-limit.js # KV 滑动窗口限流
│ └── response.js
│
├── cloud-functions/ # Cloud Functions(Node.js)
│ ├── api/
│ │ ├── auth/register.js # bcrypt cost=12 + D1
│ │ ├── pay/create-order.js # 微信/支付宝预下单
│ │ ├── pay/wx-notify.js # Edge 幂等锁 → 业务处理
│ │ ├── pay/ali-notify.js
│ │ ├── pay/query.js
│ │ ├── pay/close.js
│ │ ├── admin/products.js # D1 CRUD(含 version 乐观锁)
│ │ ├── admin/orders.js # D1 查询
│ │ ├── admin/users.js # D1 CRUD
│ │ ├── admin/stats.js # D1 聚合统计
│ │ ├── order/create.js # SELECT FOR UPDATE + 事务 + 指数退避
│ │ ├── order/detail.js
│ │ ├── order/cancel.js # 状态机 + version 校验
│ │ └── ai/chat-stream.js # SSE 流式(主力实现)
│ └── utils/
│ ├── db.js # D1 数据库工具({tenant} 强制注入)
│ ├── payment-sdk.js # 微信V3/支付宝 SDK 封装
│ ├── admin-guard.js
│ └── notification-hooks.js # 通知钩子空壳
│
├── references/ # 能力参考文档
│ ├── auth-module.md # ✅ JWT RS256 + HS256 兼容 + KV Session
│ ├── cart-module.md
│ ├── payment-module.md
│ ├── ai-chat-module.md
│ ├── admin-module.md # ✅ RBAC + CRUD + 运营统计 + 审计日志
│ ├── notification-module.md # Layer 2:邮件/微信/钉钉通知
│ ├── order-state-machine.md # ✅ 6状态 + 权限矩阵 + 库存联动 + 审计日志
│ ├── edge-functions.md # ✅ Edge Middleware + KV API + 限流
│ ├── cloud-functions.md # ✅ D1 事务 + bcrypt + 支付 SDK + SSE
│ ├── kv-storage.md
│ ├── middleware.md # ✅ Platform + Edge 双层 + CSP + 支付 bypass
│ └── deployment.md # ✅ 完整部署流程 + Cron + 回滚
│
└── scripts/
├── init-site.js # 交互式初始化(模板优先)
├── sync-sharing.js # 构建时 shared → edge/cloud 同步
└── sample-data.js
| 方法 | 路径 | 运行时 | 说明 |
|---|---|---|---|
| POST | /api/auth/login | Edge(KV) | JWT 签发 + KV session |
| GET | /api/auth/me | Edge(KV) | KV session 读取 |
| POST | /api/auth/refresh | Edge(KV) | RT 轮换(version 乐观锁) |
| POST | /api/auth/logout | Edge(KV) | 清除 Cookie + KV session |
| POST | /api/auth/register | Cloud(D1) | bcrypt cost=12 + D1 |
Access Token:短期 JWT(15min)+ HttpOnly Cookie(Secure + SameSite=Strict)
Refresh Token:7天 TTL,存 KV rt:{userId}:meta(含 version)
算法:Phase 1 用 HS256 + 短期 TTL,Phase 2 迁移 RS256
两个请求并发携带同一 RT,只有第一个能成功写入新 version,第二个收到 409 → 客户端稍等重试。
// edge-functions/api/auth/refresh.js
export async function onRequest(context) {
const { RT } = await getTokens(context.request);
const { KV } = context.env;
const payload = parseJWT(RT);
const userId = payload.sub;
if (!userId) return new Response('Invalid', { status: 401 });
const current = await KV.get(`rt:${userId}:meta`);
const { version: oldVersion, token: oldToken } = JSON.parse(current || '{"version":0,"token":""}');
if (oldToken !== RT) {
return new Response('Token already rotated', { status: 409 });
}
const newVersion = oldVersion + 1;
const newToken = signRT(userId, newVersion);
const ok = await KV.put(
`rt:${userId}:meta`,
JSON.stringify({ version: newVersion, token: newToken }),
{ expirationTtl: 604800 }
);
if (!ok) return new Response('Concurrent rotation', { status: 409 });
return new Response(JSON.stringify({ refreshToken: newToken }), {
headers: { 'Content-Type': 'application/json' }
});
}
双模式同步:
未登录:localStorage(30d TTL 自动清理)
登录时:localStorage → 服务端 KV(syncOnLogin())
已登录:服务端 KV(唯一数据源)
/api/pay/wx-notify ← 微信支付回调(IP 白名单后直接 return,不进 Edge Middleware)
/api/pay/ali-notify ← 支付宝回调(独立路径)
微信支付平台会在回调超时后重试(最长 72h),KV 查→判→写三步非原子。解决方案:Edge Function putIfNotExists 原子幂等锁。
// ===== Edge Function(唯一可访问 KV 的路径)=====
// edge-functions/api/internal/idempotency.js
export async function onRequest(context) {
const { KV } = context.env;
const { out_trade_no, callback_id } = await context.request.json();
const acquired = await KV.putIfNotExists(
`pay:idempotency:${out_trade_no}`,
callback_id,
{ expirationTtl: 86400 } // 24h < 微信重试窗口 72h
);
return new Response(JSON.stringify({ acquired }), { status: 200 });
}
// ===== Cloud Function(微信回调处理)=====
// cloud-functions/api/pay/wx-notify.js
export async function onRequest(request, env) {
const rawBody = await request.text();
if (!await verifyWechatSignature(rawBody, env.WX_MCH_SECRET))
return new Response('FAIL', { status: 401 });
const { out_trade_no, transaction_id, trade_state } = JSON.parse(rawBody);
const { acquired } = await fetch(`${env.EDGE_BASE}/api/internal/idempotency`, {
method: 'POST',
body: JSON.stringify({ out_trade_no, callback_id: transaction_id })
}).then(r => r.json());
if (!acquired) return new Response('SUCCESS'); // 幂等跳过,但返回 SUCCESS 止重试
if (trade_state === 'SUCCESS') await processPayment(out_trade_no, transaction_id, env);
return new Response('SUCCESS');
}
高并发下,UPDATE ... WHERE stock >= ? 可能同时通过检查导致超卖。解决方案:SELECT FOR UPDATE + 乐观锁 + D1 CHECK 约束。
// cloud-functions/api/order/create.js
export async function onRequest(request, env) {
const { userId } = await auth(request, env);
const { productId, quantity } = await request.json();
const pool = await getPool(env.DATABASE_URL);
let attempt = 0;
while (attempt < 3) {
attempt++;
try {
await pool.beginTransaction();
// ① SELECT FOR UPDATE:锁定商品行(持有行锁期间其他事务阻塞)
const [rows] = await pool.query(
'SELECT id, stock, price, version FROM products WHERE id = ? FOR UPDATE',
[productId]
);
if (!rows.length) { await pool.rollback(); return 404; }
const product = rows[0];
// ② 持有行锁期间校验库存(无竞态)
if (product.stock < quantity) {
await pool.rollback();
return { error: '库存不足', available: product.stock };
}
// ③ 乐观锁更新(双重保障)
const [updateResult] = await pool.query(
'UPDATE products SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ?',
[quantity, productId, product.version]
);
if (updateResult.affectedRows === 0) {
await pool.rollback();
return { error: '并发冲突,请重试' };
}
// ④ 创建订单(同一事务内)
const orderNo = generateOrderNo();
await pool.query(
`INSERT INTO orders (order_no, out_trade_no, user_id, product_id, qty, amount, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, 'PENDING', NOW())`,
[orderNo, `WX_${orderNo}`, userId, productId, quantity, product.price * quantity]
);
await pool.commit();
// ⑤ 事务成功后,异步调用微信统一下单(不在事务内)
const payment = await createPayment(orderNo, product.price * quantity, env);
return { orderNo, payment };
} catch (err) {
await pool.rollback();
if (isRetryable(err) && attempt < 3) {
await sleep(100 * Math.pow(2, attempt - 1)); // 指数退避
continue;
}
return { error: '创建失败,请重试' };
}
}
}
function isRetryable(err) {
return err.code === 'ER_LOCK_DEADLOCK' || err.code === 'ER_LOCK_WAIT_TIMEOUT';
}
EdgeOne Pages KV 不支持复合查询,按以下策略分层:
| 场景 | KV 层(Edge) | D1 层(Cloud) |
|---|---|---|
| 单商品读取 | ✅ KV 缓存 | — |
| 商品列表(无筛选) | ✅ 缓存第1页 | — |
| 分类+价格区间筛选 | — | ✅ Cloud D1 |
| 搜索关键词 | — | ✅ Cloud D1 |
| AI 会话历史(单用户) | ✅ KV | — |
| 订单统计(多条件聚合) | — | ✅ Cloud D1 |
Cloud Functions SSE 实现(Edge 无法使用 waitUntil):
前端 → GET /api/ai/history(Edge,KV 读取)→ 拿到历史上下文
→ SSE 连接 /api/ai/chat-stream(Cloud)→ 带历史 context
→ Cloud 流式响应 + 异步写 KV 保存历史
RBAC 权限体系:
role: user → 购物车、下单、查看自己的订单
role: admin → 商品 CRUD、订单管理、用户管理、运营统计
-- db/migrations/001_init.sql
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('user','admin') DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL, -- 服务端唯一价格来源
stock INT UNSIGNED NOT NULL DEFAULT 0,
category_id INT UNSIGNED,
status ENUM('active','inactive') DEFAULT 'active',
version INT UNSIGNED DEFAULT 1, -- 乐观锁版本号
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_stock_positive CHECK (stock >= 0)
);
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(64) UNIQUE NOT NULL,
out_trade_no VARCHAR(128) UNIQUE,
user_id BIGINT UNSIGNED NOT NULL,
total DECIMAL(10,2) NOT NULL,
status ENUM('pending','paid','shipped','cancelled','refunded') DEFAULT 'pending',
paid_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE order_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
qty INT UNSIGNED NOT NULL,
price DECIMAL(10,2) NOT NULL, -- 快照价格
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
CREATE TABLE admin_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
admin_id BIGINT UNSIGNED NOT NULL,
action VARCHAR(64) NOT NULL,
target VARCHAR(128),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_status ON products(status);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created ON orders(created_at);
| 环境变量 | 必填 | 用于 | 运行时 |
|---|---|---|---|
JWT_SECRET | ✅ | JWT 签名(HS256) | Edge + Cloud |
AI_API_KEY | ✅(AI栈) | AI 模型调用 | Cloud |
WX_APPID | ✅(电商栈) | 微信支付 AppID | Cloud |
WX_MCHID | ✅(电商栈) | 微信支付商户号 | Cloud |
WX_API_KEY | ✅(电商栈) | 微信支付 APIv3 密钥 | Cloud |
WX_CERT_PATH | ✅(电商栈) | 微信支付证书路径 | Cloud |
ALI_APP_ID | ✅(电商栈) | 支付宝 AppID | Cloud |
ALI_PRIVATE_KEY | ✅(电商栈) | 支付宝私钥 | Cloud |
DATABASE_URL | ✅(电商+管理) | D1 数据库绑定 | Cloud |
EDGE_BASE | ✅(电商栈) | Edge Function 内部网关地址 | Cloud |
Step 1: 选择建站类型
[1] 🛒 快速电商站(推荐)
[2] 🤖 AI 客服站
[3] 📊 SaaS 管理后台
[4] ⚙️ 自定义模块组合
Step 2: 确认预填 / 模块选择
Step 3: 填写基本信息(站点名、域名)
Step 4: 密钥配置(从 env-vars.md 模板读取,EdgeOne Pages 环境变量注入)
Step 5: 执行 db/migrations/001_init.sql(自动或手动)
Step 6: 生成代码 → edgeone deploy → 返回访问 URL
putIfNotExists 锁SELECT FOR UPDATE + D1 事务 + CHECK 约束以下为 Edge Functions 完整实现,Phase 1 已集成:
JWT 签发(login.js) — Access Token 15min + Refresh Token 7d:
// edge-functions/api/auth/login.js
export async function onRequest(context) {
const { email, password } = await context.request.json();
const pool = await getCloudPool(context.env.DATABASE_URL);
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
if (!rows.length) return new Response('Unauthorized', { status: 401 });
const user = rows[0];
const ok = await bcrypt.compare(password, user.password_hash);
if (!ok) return new Response('Unauthorized', { status: 401 });
const now = Math.floor(Date.now() / 1000);
// Access Token:15min
const accessToken = signJWT({ sub: user.id, role: user.role, type: 'access' }, 900);
// Refresh Token:7d,含 version 用于乐观锁
const rtVersion = 1;
const refreshToken = signRT(user.id, rtVersion);
// KV 存 RT meta(用于轮换校验)
await context.env.KV.put(
`rt:${user.id}:meta`,
JSON.stringify({ version: rtVersion, token: refreshToken }),
{ expirationTtl: 604800 }
);
return new Response(null, {
status: 302,
headers: {
'Location': '/',
'Set-Cookie': [
`at=${accessToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900`,
`rt=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=604800`
].join(', ')
}
});
}
RT 轮换(refresh.js) — 并发安全,version 乐观锁:
// edge-functions/api/auth/refresh.js
export async function onRequest(context) {
const { KV } = context.env;
const cookieHeader = context.request.headers.get('Cookie') || '';
const rtMatch = cookieHeader.match(/rt=([^;]+)/);
if (!rtMatch) return new Response('No RT', { status: 401 });
const oldToken = rtMatch[1];
const payload = parseJWT(oldToken);
const userId = payload.sub;
// KV version 乐观锁:只有 RT 匹配当前 version 才允许写入新 version
const current = await KV.get(`rt:${userId}:meta`);
const { version: oldVersion, token: oldStored } = JSON.parse(current || '{"version":0,"token":""}');
if (oldStored !== oldToken) {
// 另一个 tab 已轮换,当前 RT 失效 → 返回 409 让客户端重新登录
return new Response('Concurrent rotation', { status: 409 });
}
const newVersion = oldVersion + 1;
const newToken = signRT(userId, newVersion);
const ok = await KV.put(
`rt:${userId}:meta`,
JSON.stringify({ version: newVersion, token: newToken }),
{ expirationTtl: 604800 }
);
if (!ok) return new Response('Rotation failed', { status: 409 });
return new Response(JSON.stringify({ refreshToken: newToken }), {
headers: {
'Content-Type': 'application/json',
'Set-Cookie': `rt=${newToken}; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=604800`
}
});
}
客户端轮换触发逻辑(event-bus.js 集成):
// client/src/utils/event-bus.js
EventBus.on('auth:401', async () => {
// Access Token 过期 → 尝试轮换 RT
const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' });
if (res.ok) {
// RT 轮换成功 → 重发原请求
return retryOriginalRequest();
}
// RT 也失败 → 跳转登录
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
});
所有认证 Cookie 必须同时满足以下属性(缺一不可):
| 属性 | 值 | 作用 |
|---|---|---|
HttpOnly | 必须 | 阻止 JS 读取,防止 XSS 窃取 |
Secure | 必须 | 仅 HTTPS 传输 |
SameSite=Strict | 强烈建议 | 防止 CSRF(同站请求才带 Cookie) |
SameSite=Lax | 备选 | 允许导航带 Cookie,但阻止跨站 POST |
Path=/ | AT Cookie | 全路径生效 |
Path=/api/auth/refresh | RT Cookie | 仅刷新接口可读 |
Edge Functions 签发示例:
// 正确
headers.set('Set-Cookie',
`at=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900`
);
// 常见错误:缺少 Secure 或 SameSite
// ❌ `at=${token}; HttpOnly` — 可被 HTTP 拦截
// ❌ `at=${token}; HttpOnly; SameSite=None` — 无 CSRF 保护
注意:SameSite=Strict 会导致从外部链接跳转过来时无法携带 Cookie。如有第三方回调场景,改用 SameSite=Lax + CSRF Token 双保险。
限流策略:
| 用户状态 | 限额 | 窗口 |
|---|---|---|
| 未登录(IP 级别) | 10 次/分钟 | 滑动窗口 |
| 已登录(User ID 级别) | 60 次/分钟 | 滑动窗口 |
Edge Function 实现:
// edge-functions/_middleware.js 或独立限流工具
// edge-functions/utils/rate-limit.js
export async function checkRateLimit(context, key, limit) {
const { KV } = context.env;
const now = Date.now();
const windowMs = 60 * 1000; // 1 分钟滑动窗口
const windowKey = `rl:${key}:${Math.floor(now / windowMs)}`;
const prevKey = `rl:${key}:${Math.floor((now - windowMs) / windowMs)}`;
const current = parseInt(await KV.get(windowKey) || '0');
const prev = parseInt(await KV.get(prevKey) || '0');
// 滑动窗口:当前窗口占比 + 上一窗口剩余权重
const prevWeight = (now % windowMs) / windowMs;
const totalWeight = current + prev * prevWeight;
if (totalWeight >= limit) {
return { allowed: false, remaining: 0, resetMs: windowMs - (now % windowMs) };
}
// 写入当前计数
await KV.put(windowKey, String(current + 1), { expirationTtl: 120 });
return { allowed: true, remaining: limit - Math.ceil(totalWeight) - 1, resetMs: windowMs };
}
// 在 AI Chat Edge Middleware 中调用:
// const userId = payload?.sub || request.headers.get('CF-Connecting-IP');
// const { allowed, resetMs } = await checkRateLimit(context, `ai:${userId}`, 60);
// if (!allowed) return new Response('Rate limited', { status: 429, headers: { 'Retry-After': String(Math.ceil(resetMs/1000)) } });
CSP 在 Platform Middleware 层注入,对所有 HTML 响应生效:
// middleware.js(项目根目录,Platform Middleware)
export function onRequest(context) {
const response = context.next();
// 仅对 HTML 响应注入 CSP
const contentType = response.headers.get('Content-Type') || '';
if (!contentType.includes('text/html')) return response;
const CSP = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Skill 生成代码含内联脚本,放行
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self' https://api.edgeone.dev https://api.weixin.qq.com https://openapi.alipay.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; ');
const newHeaders = new Headers(response.headers);
newHeaders.set('Content-Security-Policy', CSP);
newHeaders.set('X-Content-Type-Options', 'nosniff');
newHeaders.set('X-Frame-Options', 'DENY');
newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
配置说明:
connect-src 中的域名需根据实际 AI API 和支付平台调整'unsafe-inline' 用于 Skill 生成的内联脚本(Phase 1 MVP 可接受)unsafe-inline前端 EventBus 统一处理认证失效事件:
// client/src/utils/event-bus.js
class EventBus {
constructor() {
this.listeners = {};
// 全局监听 fetch 401 响应
this._setupGlobal401Handler();
}
_setupGlobal401Handler() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const res = await originalFetch(...args);
if (res.status === 401) {
this.emit('auth:401', { url: args[0], response: res });
}
return res;
} catch (err) {
throw err;
}
};
}
on(event, handler) {
(this.listeners[event] ||= []).push(handler);
return () => this.listeners[event] = this.listeners[event].filter(h => h !== handler);
}
emit(event, data) {
(this.listeners[event] || []).forEach(h => h(data));
}
}
export const eventBus = new EventBus();
// 应用启动时注册 401 跳转
eventBus.on('auth:401', ({ url }) => {
// 排除登录页自身,避免死循环
if (url.includes('/api/auth/login') || url.includes('/api/auth/register')) return;
// 跳过 refresh 接口(它有自己的 401 处理)
if (url.includes('/api/auth/refresh')) return;
// 记录原页面路径,登录后回跳
const redirect = encodeURIComponent(window.location.pathname + window.location.search);
window.location.href = `/login?redirect=${redirect}`;
});
Notification 作为 Layer 2 Addon,按需接入。支持多通道:邮件、微信模板消息、钉钉 Webhook。
接口设计(空壳 → Phase 2 填充适配器):
// cloud-functions/utils/notification-hooks.js
// 通知事件类型
export const NotificationEvent = {
ORDER_CREATED: 'order.created',
ORDER_PAID: 'order.paid',
ORDER_SHIPPED: 'order.shipped',
ORDER_DELIVERED: 'order.delivered',
USER_REGISTERED: 'user.registered',
PASSWORD_CHANGED: 'password.changed',
};
// 通知渠道
export const NotificationChannel = {
EMAIL: 'email',
WECHAT: 'wechat', // 微信模板消息
DINGTALK: 'dingtalk', // 钉钉 Webhook
SMS: 'sms',
};
// 钩子注册表(Phase 2 填充)
const handlers = {
[NotificationEvent.ORDER_PAID]: [],
[NotificationEvent.USER_REGISTERED]: [],
};
export function registerHandler(event, handler) {
handlers[event] ||= [];
handlers[event].push(handler);
}
export async function emit(event, payload) {
const eventHandlers = handlers[event] || [];
await Promise.allSettled(
eventHandlers.map(h => h(payload).catch(err => console.error(`Notification handler error: ${err}`)))
);
}
// ===== 具体适配器示例(Phase 2 实现)=====
// 邮件适配器
registerHandler(NotificationEvent.ORDER_PAID, async ({ order, user }) => {
// 需配置 SMTP 环境变量
if (!process.env.SMTP_HOST) return; // 无邮件配置则跳过
await sendEmail({
to: user.email,
subject: `订单 ${order.order_no} 支付成功`,
html: `<h2>感谢您的购买!</h2><p>订单号:${order.order_no}</p>`
});
});
// 微信模板消息适配器
registerHandler(NotificationEvent.ORDER_SHIPPED, async ({ order, user }) => {
if (!process.env.WX_TEMPLATE_ID_SHIP) return;
await sendWechatTemplate(user.openid, process.env.WX_TEMPLATE_ID_SHIP, {
keyword1: order.order_no,
keyword2: order.express_company + ' ' + order.express_no,
});
});
// 调用示例(Cloud Functions 中)
import { emit, NotificationEvent } from './utils/notification-hooks.js';
export async function onRequest(request, env) {
// 支付回调成功后触发
await emit(NotificationEvent.ORDER_PAID, { order, user });
return new Response('SUCCESS');
}
env-vars.md 补充字段:
NOTIFICATION_SMTP_HOST # 邮件 SMTP 主机
NOTIFICATION_SMTP_PORT # 邮件 SMTP 端口(默认 587)
NOTIFICATION_SMTP_USER # 邮件发件人
NOTIFICATION_SMTP_PASS # 邮件密码
NOTIFICATION_FROM_EMAIL # 发件人地址
WX_TEMPLATE_ID_ORDER # 微信订单通知模板 ID
WX_TEMPLATE_ID_SHIP # 微信发货通知模板 ID
DINGTALK_WEBHOOK_URL # 钉钉群 Webhook URL
Phase 1 使用 HS256(密钥共享,简单快速);Phase 2 迁移到 RS256(公私钥,安全性更高)。
迁移策略:双轨并行,渐进式切换
Phase 1(当前):HS256
- JWT_SECRET = 对称密钥(Edge + Cloud 共享)
Phase 2 迁移:
- 新增 JWT_PRIVATE_KEY(Cloud 签名用 RSA 私钥)
- 新增 JWT_PUBLIC_KEY(Edge 验证用 RSA 公钥)
- Edge Functions 验证用公钥(无需密钥)
- Cloud Functions 签名用私钥
- HS256 保留 30 天兼容窗口(老 token 仍可验证)
生成密钥对:
# 生成 RSA-256 密钥对
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# 将公钥 public.pem 内容填入 EdgeOne Pages 环境变量 JWT_PUBLIC_KEY
# 将私钥 private.pem 内容填入 Cloud Functions 环境变量 JWT_PRIVATE_KEY(严格保密)
Cloud Functions 签名切换:
// cloud-functions/utils/jwt-helper.js
import { SignJWT, jwtVerify } from 'jose';
const getSignKey = (env) => {
if (env.JWT_PRIVATE_KEY) {
return createPrivateKey(env.JWT_PRIVATE_KEY); // RS256
}
return new TextEncoder().encode(env.JWT_SECRET); // 兼容 HS256
};
export async function signJWT(payload, expiresIn, env) {
const key = getSignKey(env);
return new SignJWT(payload)
.setProtectedHeader({ alg: env.JWT_PRIVATE_KEY ? 'RS256' : 'HS256' })
.setIssuedAt()
.setExpirationTime(`${expiresIn}s`)
.sign(key);
}
Edge Functions 验证(始终用公钥):
// edge-functions/utils/jwt-helper.js
export async function verifyJWT(token, env) {
const publicKey = createPublicKey(env.JWT_PUBLIC_KEY); // RS256 验证
try {
const { payload } = await jwtVerify(token, publicKey);
return payload;
} catch {
// 30 天兼容窗口:尝试 HS256 验证(仅过渡期)
const secret = new TextEncoder().encode(env.JWT_SECRET);
try {
const { payload } = await jwtVerify(token, secret);
return { ...payload, _hs256Fallback: true }; // 标记老 token
} catch {
return null;
}
}
}
状态定义与流转:
┌──────────┐ pay ┌───────┐ ship ┌──────────┐ confirm ┌───────────┐
│ PENDING │ ──────→ │ PAID │ ─────→ │ SHIPPED │ ──────→ │ COMPLETED │
└──────────┘ └───────┘ └──────────┘ └───────────┘
│ │ │
│ cancel (user) │ refund (user/admin) │
↓ ↓ │
┌──────────┐ ┌──────────┐ │
│ CANCELLED│ │ REFUNDED │ │
└──────────┘ └──────────┘ │
│
refund (admin, COMPLETED) │
─────────────────────────────────────────┘
合法流转规则(version 乐观锁保护):
| 当前状态 | 允许目标状态 | 触发方 | 条件 |
|---|---|---|---|
| PENDING | PAID | 支付回调 | 金额核对成功 |
| PENDING | CANCELLED | 用户/系统超时 | 30min 未支付 |
| PAID | SHIPPED | 管理员 | 填写物流信息 |
| PAID | REFUNDED | 用户/管理员 | 退款申请 |
| SHIPPED | COMPLETED | 用户/系统 | 7天无售后自动确认 |
| SHIPPED | REFUNDED | 用户/管理员 | 退货退款 |
| COMPLETED | REFUNDED | 管理员 | 特殊退款审批 |
状态机实现(D1 + version 乐观锁):
// cloud-functions/api/order/cancel.js
export async function onRequest(request, env) {
const { userId, role } = await auth(request, env);
const { orderId, reason } = await request.json();
const pool = await getPool(env.DATABASE_URL);
let attempt = 0;
while (attempt < 3) {
attempt++;
try {
await pool.beginTransaction();
// ① 锁定订单行,获取当前状态和版本
const [rows] = await pool.query(
'SELECT * FROM orders WHERE id = ? FOR UPDATE',
[orderId]
);
if (!rows.length) { await pool.rollback(); return 404; }
const order = rows[0];
// ② 权限校验:用户只能取消自己的 PENDING 订单
if (role !== 'admin' && order.user_id !== userId) {
await pool.rollback(); return 403;
}
// ③ 状态机校验
const allowed = {
'PENDING': ['CANCELLED'],
'PAID': ['CANCELLED', 'REFUNDED'], // 退款需管理员
'SHIPPED': ['COMPLETED', 'REFUNDED'], // 已发货需管理员
};
const target = reason === 'user_cancel' ? 'CANCELLED' : 'REFUNDED';
if (!allowed[order.status]?.includes(target)) {
await pool.rollback();
return { error: `状态 ${order.status} 不允许变更为 ${target}` };
}
if (target === 'CANCELLED' && role !== 'admin' && order.status !== 'PENDING') {
await pool.rollback();
return { error: '仅 PENDING 状态可由用户取消' };
}
// ④ 乐观锁更新(防止并发修改)
const [result] = await pool.query(
'UPDATE orders SET status = ?, version = version + 1 WHERE id = ? AND version = ?',
[target, orderId, order.version]
);
if (result.affectedRows === 0) {
await pool.rollback(); // 版本冲突,重试
continue;
}
// ⑤ 释放库存(仅取消时回补)
if (target === 'CANCELLED') {
await pool.query(
'UPDATE products SET stock = stock + (SELECT qty FROM order_items WHERE order_id = ?), version = version + 1 WHERE id = (SELECT product_id FROM order_items WHERE order_id = ?)',
[orderId, orderId]
);
}
// ⑥ 记录操作日志
await pool.query(
'INSERT INTO admin_logs (admin_id, action, target) VALUES (?, ?, ?)',
[userId, `order_status_change:${order.status}→${target}`, orderId]
);
await pool.commit();
// ⑦ 触发通知钩子
await emit(NotificationEvent.ORDER_CANCELLED, { order, reason });
return { success: true, status: target };
} catch (err) {
await pool.rollback();
if (err.code === 'ER_LOCK_DEADLOCK' && attempt < 3) {
await sleep(100 * Math.pow(2, attempt));
continue;
}
return { error: '操作失败,请重试' };
}
}
}
| ID | 验收项 | 验证方法 |
|---|---|---|
| P2-01 | JWT 15min AT + 7d RT + Cookie 全属性 | 登录后 DevTools 查看 Cookie 属性 |
| P2-02 | 并发刷新 RT,第二个请求返回 409 | 两个 tab 同时触发刷新 |
| P2-03 | EventBus 401 跳转登录并回跳 | Token 过期后触发验证 |
| P2-04 | AI 限流:未登录 11 次请求第 11 个返回 429 | 匿名请求连续发送 |
| P2-05 | CSP Header 存在于 HTML 响应中 | curl -I 查看响应头 |
| P2-06 | 订单状态机:PENDING→CANCELLED 成功 | 调用 cancel API |
| P2-07 | 订单状态机:PAID→CANCELLED 被拒绝(需 admin) | 用户端测试 |
| P2-08 | Notification 钩子注册 + emit 触发 | 单元测试验证 |
| P2-09 | RS256 双轨验证(可选 Phase 2 末期) | HS/RS 混合 token 混跑 |
Demo 站点: https://website-skeleton-demo-8mv8fitk.edgeone.cool(需有效期内的 EdgeOne Pages 访问 Token)
| # | 功能 | 验证方法 | 状态 |
|---|---|---|---|
| V-01 | 首页商品浏览(12 个商品) | API 返回 12 个商品,含名称/价格/库存 | ✅ |
| V-02 | 用户注册(bcrypt cost=12) | 注册成功,返回 userId/email | ✅ |
| V-03 | 用户登录(JWT) | 登录成功,返回用户信息 | ✅ |
| V-04 | 购物车(localStorage 持久化) | Next.js 客户端路由,需浏览器测试 | 🟡 浏览器验证 |
| V-05 | 结账(微信/支付宝选择) | checkout 页面存在,需浏览器测试 | 🟡 浏览器验证 |
| V-06 | 模拟支付成功回调 | confirm API 存在,需有效 session | 🟡 需 session |
| V-07 | 我的订单(状态标签) | orders API 存在,需有效 session | 🟡 需 session |
✅ Phase 1 完成:安全 Critical 全部修复(7/7 P0)
🟡 Phase 2 进行中:P1 安全加固 + P2 能力完善
🔲 Phase 3(可选):RS256 + nonce CSP + SSE 优化
Phase 2 完成后,网站骨架 Skill 具备生产级安全性与完整功能集。
✅ Phase 1 完成:Mock 数据 Demo,架构验证
✅ Phase 2 完成:P0/P1 安全设计 + P2 设计文档完整
✅ Phase 3 完成:P2 实现 + Layer 2 Addon + 多租户铺垫
实现文件: sharing/jwt-helper.js
JWT_PRIVATE_KEY 环境变量)// 签发(永远 RS256)
const token = await signJWT({ sub: user.id, role: 'admin' }, AT_TTL_MS, env);
// 验证(自动双轨)
const payload = await verifyJWT(token, env);
// payload._alg === 'RS256' → 新 token
// payload._alg === 'HS256' → 30天兼容窗口内的旧 token
实现文件:
cloud-functions/utils/order-state-machine.js — 核心状态机 + TRANSITIONS 表 + PERMISSIONS 表cloud-functions/api/order/transition.js — 统一状态变更 APIcloud-functions/cron/order-cron.js — 定时任务(PENDING 超时取消 / SHIPPED 自动完成)db/migrations/002_order_logs.sql — order_status_logs 审计表状态流转(6 状态):
PENDING → PAID → SHIPPED → COMPLETED
↓ ↓ ↓
CANCELLED REFUNDED REFUNDED
权限矩阵:
| 变更 | 用户(本人) | 管理员 |
|---|---|---|
| PENDING→CANCELLED | ✅ | ✅ |
| PAID→SHIPPED | — | ✅ |
| PAID/SHIPPED→REFUNDED | ✅(本人) | ✅ |
| SHIPPED→COMPLETED | ✅ | ✅ |
实现文件:
client/src/utils/seo.js — JSON-LD 生成器 + Meta Tags + Sitemap XML 生成器edge-functions/api/sitemap.xml.js — 动态 Sitemap API(Edge Function,5 分钟缓存)sharing/i18n/zh-CN.js + en-US.js — 中英文案JSON-LD 支持:
WebSite(首页)Product(产品页,含 offers/aggregateRating)BreadcrumbList(面包屑)Organization(组织信息)实现文件:
sharing/i18n/zh-CN.js — 中文文案sharing/i18n/en-US.js — 英文文案sharing/i18n/i18n.js — 翻译函数 t(key) + 语言切换使用方式:
import { t, setLang, getLang } from './i18n.js';
t('nav.home') // → '首页'
t('order.status.PAID') // → '已支付'
setLang('en-US'); // 切换语言
实现文件:
client/src/utils/analytics.js — 埋点 SDKedge-functions/api/analytics/event.js — 事件接收 API(KV 存储)预定义事件: page_view / add_to_cart / checkout_start / purchase / signup / login / search
特点: navigator.sendBeacon 不阻塞导航,支持页面卸载时发送。
实现文件: sharing/kv-keys.js
所有 KV Key 统一加租户前缀:
Phase 3: "default:session:abc123"
Phase 4: "{tenant}:session:abc123"(从 JWT payload.tenant 动态读取)
sharing/
├── jwt-helper.js ✅ RS256 + HS256 双轨
├── kv-keys.js ✅ 多租户前缀
└── i18n/
├── zh-CN.js ✅ 中文
├── en-US.js ✅ 英文
└── i18n.js ✅ 翻译函数
cloud-functions/
├── utils/
│ └── order-state-machine.js ✅ 核心状态机
├── api/order/
│ └── transition.js ✅ 状态变更 API
└── cron/
└── order-cron.js ✅ 定时任务
client/src/utils/
├── seo.js ✅ SEO 工具
└── analytics.js ✅ 埋点 SDK
edge-functions/
├── api/
│ ├── sitemap.xml.js ✅ Sitemap API
│ └── analytics/event.js ✅ 埋点接收
db/migrations/
└── 002_order_logs.sql ✅ 审计日志表
references/
├── admin-module.md ✅ 补充
├── edge-functions.md ✅ 补充
├── cloud-functions.md ✅ 补充
├── middleware.md ✅ 补充
└── deployment.md ✅ 补充
| ID | 验收项 | 验证方法 |
|---|---|---|
| P3-01 | RS256:新 token 用 RS256 私钥签发 | 代码审查 + 手动 JWT 解析 |
| P3-02 | RS256:HS256 旧 token 30 天内仍可验证 | 测试过期 token 验证 |
| P3-03 | 订单状态机:用户取消 PENDING 订单成功 | 调用 transition API |
| P3-04 | 订单状态机:用户无法 PAID→CANCELLED(403) | 调用 transition API |
| P3-05 | 库存联动:取消/退款时 stock 回补 | 查询 products 表 |
| P3-06 | 审计日志:每次状态变更写入 order_status_logs | 查询数据库 |
| P3-07 | Cron:PENDING 超时 30 分钟自动 CANCELLED | 模拟超时订单 |
| P3-08 | SEO JSON-LD:产品页含 schema.org 结构化数据 | 审查页面源码 |
| P3-09 | Sitemap:/api/sitemap.xml 返回有效 XML | curl 访问 |
| P3-10 | i18n:t('order.status.PAID') 正确输出中英文 | 切换语言测试 |
| P3-11 | Analytics:add_to_cart 事件通过 sendBeacon 发送 | Network 面板验证 |
| P3-12 | Multi-tenant:KV key 格式含 "default:" 前缀 | 代码审查 |
Phase 1:Mock 数据 Demo ✅
Phase 2:P0/P1 安全设计 + P2 设计文档 ✅
Phase 3:P2 编码实现 + Layer 2 Addon + 多租户铺垫 ✅
Phase 4(规划中):多租户 SaaS
- KV key 从 JWT payload.tenant 动态读取
- 租户隔离数据库(D1 schema)
- 租户管理后台
- 计费系统(按量/订阅)
Phase 5(规划中):npm 包化
npm install @site-skeleton/auth
npm install @site-skeleton/payment
Skill 版本演进由评审驱动,每 Phase 完成后更新版本号与文档。
以下操作必须在执行前暂停,向用户说明并等待确认:
| # | 操作 | 检查点说明 | 风险 |
|---|---|---|---|
| 1 | edgeone pages deploy | 显示待部署文件清单和变更概要,确认目标项目(测试/生产) | 高 |
| 2 | 激活真实支付网关 | 切换到正式商户号前确认:签名验证已通过、测试环境已跑通全流程 | 高 |
| 3 | 注入生产环境变量 | 确认所有密钥(JWT_SECRET / WX_API_KEY / DATABASE_URL)已正确配置 | 高 |
| 4 | 执行数据库迁移 | 先备份现有数据、确认迁移脚本影响范围 | 高 |
| 5 | 启用 Cron 定时任务 | 确认 PENDING 超时(30min)和自动完成(7天)阈值符合业务需求 | 中 |
| 6 | 生成网站代码前 | 向用户展示选择的场景模板和模块组合,确认后再执行代码生成 | 中 |
| 7 | 绑定自定义域名 | 确认 DNS 解析已配置、SSL 证书已签发、旧域名流量迁移方案 | 中 |
| 8 | 回滚部署 | 确认回滚到哪个版本、是否会影响正在进行的交易 | 高 |
| 9 | 开启多租户模式 | 确认 KV key 前缀迁移策略、租户隔离方案 | 中 |
| 10 | 升级 RS256(从 HS256) | 确认 30 天兼容窗口期内新旧 token 混跑测试通过 | 中 |
规则:LLM 遇到上表操作时,先向用户输出摘要说明,明确询问"是否继续?",得到肯定答复后再执行。用户可随时跳过检查点。
本 Skill 可生成网站代码并自动部署到 EdgeOne Pages。部署前 AI Agent 会:
edgeone pages deploy| 数据类型 | 存储位置 | 保留期限 | 说明 |
|---|---|---|---|
| KV Session | EdgeOne KV | 7 天 TTL | 自动过期 |
| AI 聊天历史 | EdgeOne KV | 30 天 TTL | 可配置 |
| 审计日志 | EdgeOne KV | 90 天 TTL | 可配置 |
| 订单数据 | D1 | 永久 | 业务必需 |
cloud-functions/cron/disable-cron.js 一键禁用