协作开发总览
Align 开放平台让外部开发者以「插件」形式扩展平台能力,主体源码完全不外露。
协作模式
Align 采用插件隔离协作模式:
- 主体仓库(私有):核心源码,只有 Align 团队访问
- 插件仓库(每功能一个独立私有 repo):协作者只能看到自己负责的插件
- SDK 仓库(公开):定义了插件与 Align 交互的完整接口
Align 主体(私有)
└── pluginLoader → /api/plugins/{name}/*
↑
| 实现 AlignPlugin 接口
|
align-plugin-wecom (私有,企微开发者)
align-plugin-payment (私有,支付开发者)
align-plugin-xxx (未来更多...)
|
└── 依赖 @aimacgao-lab/align-plugin-sdk (公开)
现有插件仓库
| 仓库 | 可见性 | 功能 | 状态 |
| align-plugin-sdk |
公开 |
TypeScript 类型合约 |
✅ 已发布 |
| align-plugin-wecom |
私有 |
企业微信集成 |
🚧 骨架待实现 |
| align-plugin-payment |
私有 |
支付(微信/支付宝) |
🚧 骨架待实现 |
协作者接入流程
收到仓库邀请
点击邀请邮件里的「Accept invitation」,获得对应插件仓库的访问权限。
克隆 + 安装
git clone https://github.com/aimacgao-lab/align-plugin-wecom.git
cd align-plugin-wecom
npm install
实现 TODO
打开 src/index.ts,找到 TODO 注释,逐一实现各路由 handler。
提 PR
git checkout -b feat/your-feature
npm run build # 确认编译通过
git add -A && git commit -m "feat: ..."
git push origin feat/your-feature
在 GitHub 上创建 PR → 等待 Align 团队 review → merge。
快速入门
从零到提交第一个 PR,约 30 分钟。
📋 前置条件
已收到插件仓库邀请、Node.js 18+、npm 或 pnpm
环境搭建
git clone https://github.com/aimacgao-lab/align-plugin-wecom.git
cd align-plugin-wecom
npm install
目录结构
align-plugin-wecom/
├── src/
│ └── index.ts ← 插件入口,export default plugin
├── package.json
├── tsconfig.json
└── README.md ← 功能清单和约束说明
写一个 Handler
import type { AlignPlugin, PluginContext } from '@aimacgao-lab/align-plugin-sdk';
async function getContacts(ctx: PluginContext): Promise<void> {
const { userId } = ctx.iam!;
const binding = await ctx.db.collection('bindings').findOne({ userId });
if (!binding) {
ctx.res.status(400).json({ error: '未绑定企微账号' });
return;
}
// 调企微 API 拿外部联系人...
ctx.res.json({ contacts: [] });
}
const plugin: AlignPlugin = {
name: 'wecom',
version: '1.0.0',
routes: [
{ method: 'get', path: '/contacts', handler: getContacts },
],
};
export default plugin;
本地调试技巧
无需启动完整 Align 服务,直接 mock ctx:
const ctx = {
iam: { userId: 'test-user', orgId: 'org-1', role: 'user', email: '', name: '' },
req: { body: {}, query: {}, params: {}, headers: {} },
res: {
json: (d) => console.log(JSON.stringify(d, null, 2)),
status: () => ctx.res,
send: console.log,
},
db: { collection: () => mockCollection }, // 你的 mock
align: { getUser: async () => null, /* ... */ },
};
await getContacts(ctx);
PR 规范
| 项目 | 要求 |
| 编译 | npm run build 必须通过(0 TypeScript 错误) |
| 提交格式 | feat/fix/chore(scope): 描述 |
| PR 描述 | 说明实现了哪些路由、怎么验证 |
| 环境变量 | 新增的 env key 在 PR 描述里列出,由主库配置 |
| 幂等性 | 回调接口必须幂等(同一事件多次通知只处理一次) |
常见问题
Q: 我能访问 Align 的数据库吗?
不能。ctx.db 只能访问以 plugin_{名称}_ 为前缀的集合,Align 主体数据完全隔离。
Q: 路由挂在哪里?
插件名 wecom,路由 /contacts 实际挂载到 /api/plugins/wecom/contacts。
Q: 我需要处理鉴权吗?
不需要。route.auth !== false 时 Align 自动验证 JWT,ctx.iam 里就是已验证的用户信息。
Plugin SDK 手册
完整的运行时接口参考。SDK 源码:
align-plugin-sdk
AlignPlugin(插件入口)
每个插件的 src/index.ts 必须默认导出一个 AlignPlugin 对象:
const plugin: AlignPlugin = {
name: 'wecom', // 英文小写,决定路由前缀
version: '1.0.0',
routes: [...],
onInstall: async (ctx) => { /* 可选,首次加载时执行 */ },
};
export default plugin;
PluginContext
| 字段 | 类型 | 说明 |
iam? | IamInfo | 当前登录用户(auth:false 时 undefined) |
req | PluginRequest | 请求信息(body/query/params/headers) |
res | PluginResponse | 响应工具(json/status/send) |
db | PluginDB | 插件专属数据库(集合自动加前缀) |
align | AlignServices | Align 平台能力(查用户/发 IM/通知/SSE) |
ctx.iam
| 字段 | 类型 | 说明 |
userId | string | Align 用户 ID |
orgId | string | 所属组织 ID |
role | string | 'user' | 'admin' | 'super_admin' |
email | string | 邮箱 |
name | string | 姓名 |
ctx.db
隔离保证
ctx.db.collection('orders') 实际操作 MongoDB 集合
plugin_{name}_orders,无法访问 Align 主体集合。
| 方法 | 返回 | 说明 |
findOne(filter) | T | null | 查一条 |
find(filter, opts?) | T[] | 查多条,支持 limit/sort |
insertOne(doc) | T & {_id:string} | 插入 |
updateOne(filter, update, opts?) | void | 更新($set),支持 upsert |
deleteOne(filter) | void | 删除 |
count(filter) | number | 计数 |
ctx.align
| 方法 | 说明 |
getUser(userId) | 查 Align 用户信息(id/name/email/orgId/role/avatar) |
getOrg(orgId) | 查组织信息(id/name/memberCount) |
sendIM(userId, content) | 向用户发系统 IM 消息(出现在聊天页) |
notify({userId, title, body, link?}) | 发铃铛通知 |
emit(userId, domain, action, id) | 触发前端 SSE 实时刷新 |
路由定义
{
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
path: '/your-path', // 相对路径
handler: async (ctx) => { ... },
auth: true, // 默认 true;Webhook 等设 false
}
// 最终挂载: /api/plugins/{pluginName}{path}
环境变量
插件通过 process.env 读取变量,变量由 Align 主库负责人配置到生产服务器:
const CORP_ID = process.env.WECOM_CORP_ID;
if (!CORP_ID) throw new Error('WECOM_CORP_ID not configured');
⚠️ 安全
新增的环境变量名必须在 PR 描述里列出,由主库负责人配置。不要在代码里硬编码密钥。
企业微信集成规格
仓库:align-plugin-wecom(私有)— OAuth 绑定 · 外部联系人 · 消息收发
功能范围
| 功能 | 路由 | 状态 |
| 员工 OAuth 绑定企微账号 | GET /oauth/redirect, /oauth/callback | TODO |
| 查看外部联系人 | GET /contacts | TODO |
| 向外部联系人发消息 | POST /messages/send | TODO |
| 接收企微回调(Webhook) | POST /webhook | TODO |
环境变量
| 变量名 | 说明 | 获取位置 |
WECOM_CORP_ID | 企业 ID | 企微管理后台 → 我的企业 |
WECOM_AGENT_ID | 自建应用 AgentId | 应用管理 → 自建应用 |
WECOM_SECRET | 应用 Secret | 同上 |
WECOM_TOKEN | 消息推送 Token | 应用接收消息设置 |
WECOM_AES_KEY | 消息加解密 Key(43位) | 同上 |
OAuth 绑定流程
Align 前端 → GET /api/plugins/wecom/oauth/redirect
← 返回企微 OAuth URL(前端跳转)
↓ 用户授权
企微 → GET /api/plugins/wecom/oauth/callback?code=xxx
→ 存绑定关系 plugin_wecom_bindings
← 重定向回 Align
⚠️ 48h 限制
企微外部联系人消息接口要求客户在
48小时内有过互动才可主动发送。超出限制返回错误码
130822,需给用户友好提示。
Access Token 缓存
企微 Access Token 有效期 7200 秒,每日请求上限 2000 次,必须缓存:
async function getAccessToken(ctx: PluginContext): Promise<string> {
const cache = await ctx.db.collection('token_cache').findOne({ key: 'access_token' });
if (cache && cache.expiresAt > Date.now() + 60_000) return cache.token;
const resp = await fetch(
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${CORP_ID}&corpsecret=${SECRET}`
);
const { access_token, expires_in } = await resp.json();
await ctx.db.collection('token_cache').updateOne(
{ key: 'access_token' },
{ key: 'access_token', token: access_token, expiresAt: Date.now() + expires_in * 1000 },
{ upsert: true }
);
return access_token;
}
Webhook 解密
企微推送使用 AES-CBC-256 加密 + XML 格式:
1. 验签
msg_signature = sha1(sort([token, timestamp, nonce, encrypt_msg]))
2. AES-CBC 解密
key = base64_decode(WECOM_AES_KEY + '=') // 补全 padding
iv = key[0..16]
plaintext = aes_cbc_decrypt(ciphertext, key, iv)
// plaintext = random(16) + msg_len(4) + xml_content + appid
3. 解析 XML → MsgType / Content / FromUserName
4. 存入 DB + 通知员工
开发优先级
- OAuth 绑定(必须先做,其他功能依赖绑定关系)
- 外部联系人列表(验证绑定是否正确)
- 发送消息(核心功能)
- Webhook 接收(完成双向通信闭环)
参考文档
支付集成规格
仓库:align-plugin-payment(私有)— 微信支付 · 支付宝 · 回调幂等 · 退款
功能范围
| 功能 | 路由 | 状态 |
| 创建支付订单 | POST /orders/create | TODO |
| 查询订单状态 | GET /orders/:orderId | TODO |
| 微信支付回调 | POST /notify/wxpay | TODO |
| 支付宝回调 | POST /notify/alipay | TODO |
| 退款(admin only) | POST /orders/:orderId/refund | TODO |
🚨 金额规则
所有金额必须用整数分(fen),禁止浮点。99.00元 →
9900,不允许
99.0。
环境变量
微信支付(APIv3)
| 变量名 | 说明 |
WXPAY_MCH_ID | 商户号 |
WXPAY_APP_ID | AppID |
WXPAY_API_KEY_V3 | APIv3 密钥(32位) |
WXPAY_CERT_SERIAL | 证书序列号 |
WXPAY_PRIVATE_KEY | 商户私钥(PEM 文本) |
支付宝
| 变量名 | 说明 |
ALIPAY_APP_ID | 应用 AppID |
ALIPAY_PRIVATE_KEY | 应用私钥(RSA2 PKCS8) |
ALIPAY_PUBLIC_KEY | 支付宝公钥 |
创建订单
POST /api/plugins/payment/orders/create
{
"subject": "购买高级会员",
"amount": 9900, // 分(9900 = ¥99.00)
"channel": "wxpay", // 或 "alipay"
"notifyUrl": "https://align.xin/api/plugins/payment/notify/wxpay"
}
→ {
"orderId": "PAY_20240101_abc123",
"payParams": { /* 前端唤起支付参数 */ }
}
回调幂等处理
幂等是强制要求
同一订单可能收到多次回调通知,处理前必须检查已处理状态,已处理则直接返回成功。
async function handleWxpayNotify(ctx: PluginContext) {
// 1. 解密验签拿 orderId
const { orderId, trade_state } = decryptNotify(ctx.req.body, ctx.req.headers);
// 2. 幂等检查
const order = await ctx.db.collection('orders').findOne({ orderId });
if (order?.status === 'paid') {
ctx.res.json({ code: 'SUCCESS', message: '幂等' }); // 直接返回
return;
}
// 3. 更新状态
await ctx.db.collection('orders').updateOne(
{ orderId },
{ status: 'paid', paidAt: new Date().toISOString() }
);
// 4. 通知用户
await ctx.align.sendIM(order.userId, `支付成功:${order.subject}`);
ctx.res.json({ code: 'SUCCESS' });
}
退款权限
🔒 退款仅限管理员
骨架代码已内置权限校验,
不能删除。普通用户调用退款接口会收到 403。
if (!ctx.iam || !['admin', 'super_admin'].includes(ctx.iam.role)) {
ctx.res.status(403).json({ error: '无权限' });
return;
}
开发优先级
- 微信支付建单 + 回调(先跑通一个渠道全链路)
- 支付宝建单 + 回调
- 退款接口
- 查询接口(最简单,直接查 DB)
参考文档