Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,27 @@
|
||||
|
||||
## 调灰度比例(最常用)
|
||||
|
||||
## 105 联通约束(硬性要求)
|
||||
|
||||
**要求:105 机器与本机通信必须走 Tailscale 隧道,不允许走外网 IP。**
|
||||
|
||||
- 访问、同步、部署 105 一律使用 `ssh105`。
|
||||
- `ssh105` 在 `~/.ssh/config` 中绑定为 `100.101.255.32` 并通过 `tailscale nc` 代理;
|
||||
- 现有脚本默认主机已改为 `root@ssh105`。
|
||||
- 本机对 105 的联通入口是 `127.0.0.1:18080`,不是 `105.tkmind.cn`。
|
||||
|
||||
快速自检(每次操作 105 前):
|
||||
|
||||
```bash
|
||||
tailscale --socket /Users/john/Project/ollama/.tailscale/tailscaled.sock status
|
||||
tailscale --socket /Users/john/Project/ollama/.tailscale/tailscaled.sock ping 100.101.255.32
|
||||
ssh ssh105 'echo tunnel-ok-105'
|
||||
curl -s http://127.0.0.1:18080/api/status # 应返回 ok
|
||||
```
|
||||
|
||||
- 禁止:`HostName 120.26.184.105` 直接作为 105 脚本主机。
|
||||
- 禁止:用 `105.tkmind.cn` 做 API 健康检查或同步链路入口。
|
||||
|
||||
编辑 **Studio** 上的 `~/Project/Memind/scripts/g2-lb.Caddyfile`,改 `lb_policy weighted_round_robin` 后面两个权重(第一个=Studio :8081,第二个=105 :18080):
|
||||
|
||||
| 配置 | Studio | 105 |
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# H5 计量计费网关设计
|
||||
|
||||
## 背景:当前计费的问题
|
||||
|
||||
当前计费内联在 `tkmind-proxy.mjs` 里,靠嗅探上游 SSE 的 `Finish` 帧扣费。100 服务实测发现三类问题:
|
||||
|
||||
1. **定价与成本脱钩,约 20× 超收**:默认 flat `2分/1k input` 对 DeepSeek 中继(goose `custom_tkmind_relay_deepseek`)严重偏贵。对账每条会话 H5 实扣 / goose 自报 `accumulated_cost × 7.2` 稳定落在 ~20×。换 provider 又会反向亏——单一硬编码 token 单价无法跟随模型。
|
||||
2. **计量点分散**:只覆盖走 `tkmind-proxy` 的 H5 会话;`mindspace-agent-runner` 等其他 LLM 出口可能未计费。
|
||||
3. **可靠性弱**:`onFinish` 为 fire-and-forget(`sse-billing.mjs` 的 `.catch(()=>{})`),扣费失败即漏计;`requestId` 全程传 `null`,无幂等键、无流水对账;读 `previous` 在事务外存在并发双扣竞态(`user-auth.mjs:943`)。
|
||||
|
||||
## 已落地的止血改动(本次)
|
||||
|
||||
`billing.mjs` 扩展 `H5_USE_BACKEND_COST` 成本路径,新增 `H5_MARGIN_MULTIPLIER`:
|
||||
|
||||
```
|
||||
最终扣费 = 上游 accumulated_cost(USD) 增量 × H5_USD_CNY_RATE × H5_MARGIN_MULTIPLIER
|
||||
```
|
||||
|
||||
- 自动跟随 provider/模型,换模型不需重调 token 单价。
|
||||
- 仅当上游回传 `accumulatedCost` 时生效;缺失则**回退**原 flat token 路径,不破坏现有计费。
|
||||
- 生产启用:`.env` 设 `H5_USE_BACKEND_COST=1` + `H5_MARGIN_MULTIPLIER=<目标毛利>`。
|
||||
|
||||
> ⚠️ 上线前需确认:一帧真实 SSE 的 `token_state` 是否带 `accumulated_cost`(goose `sessions.db` 有该列,但要确认 SSE Finish 事件也序列化了它)。确认前 multiplier 改动是安全的(无 cost 即回退)。
|
||||
|
||||
## 目标架构:独立计量网关
|
||||
|
||||
把"计量 + 定价 + 扣费 + 对账"从 H5 代理里抽出,成为所有 LLM 出口的统一收口点。
|
||||
|
||||
```
|
||||
H5 session ─┐
|
||||
agent-runner ┤→ [计量网关] → upstream(goose/...) → usage+cost
|
||||
cover-ai 等 ─┘ │
|
||||
├─ 定价引擎(按 provider/模型,cost × margin)
|
||||
├─ 幂等扣费(requestId 去重 + 事务内读写钱包)
|
||||
└─ ledger 流水(每次调用一条,可对账/审计/退款)
|
||||
```
|
||||
|
||||
### 关键设计点
|
||||
|
||||
1. **统一收口**:所有发往 LLM 的调用都经网关,拿 provider 回传的真实 `usage` + `cost`,而非各出口各自实现。
|
||||
2. **定价引擎按 provider 配置**:`{ provider, model } → { 计价基准, marginMultiplier }`。DeepSeek 与未来的 Claude 成本差几十倍,必须差异化,不能一个 flat 单价打天下。优先用成本基准(cost × margin),无 cost 的 provider 才退回 token 单价表。
|
||||
3. **幂等扣费**:每次调用带 `requestId`;ledger 以 `requestId` 唯一约束,重放/重连不重复扣。当前 SSE 重连靠"accumulated 单调 → delta 0"兜底,脆弱,应换显式幂等键。
|
||||
4. **事务内读写**:钱包扣减与计费状态读取在同一事务 + `FOR UPDATE`,消除并发双扣。
|
||||
5. **ledger 流水表**:`(request_id, user_id, session_id, provider, model, input_tokens, output_tokens, upstream_cost_usd, charged_cents, margin, created_at)`。支撑对账(H5 扣费 vs 上游成本)、用户账单明细、纠纷退款。
|
||||
6. **失败可恢复**:扣费写库失败要可重试/补偿,不能像现在 fire-and-forget 直接吞掉。
|
||||
|
||||
### 落地阶段
|
||||
|
||||
- **P0(已做)**:成本模式 + margin,env 切换止血 20× 超收。
|
||||
- **P1**:补 `requestId` 幂等 + 修事务竞态 + ledger 流水表(仍在 proxy 内,先把可靠性和对账补齐)。
|
||||
- **P2**:抽出独立计量模块/服务,纳入 agent-runner 等其他出口,定价引擎按 provider 配置化。
|
||||
@@ -1,5 +1,7 @@
|
||||
# 本地开发
|
||||
|
||||
> 生产安全提醒:`g2.tkmind.cn` 当前使用本机 `8081`。普通开发预览必须走测试端口,不要直接在生产目录运行默认 `pnpm dev`。完整规程见 [生产 / 测试 / 预览隔离规程](./service-isolation-runbook.md)。
|
||||
|
||||
`pnpm dev` 启动后,用 **127.0.0.1 + 端口** 访问:
|
||||
|
||||
| 服务 | 地址 |
|
||||
@@ -17,6 +19,22 @@ PLAZA_APP_DIR=/path/to/plaza pnpm dev
|
||||
pnpm open:local-test # 浏览器打开 H5
|
||||
```
|
||||
|
||||
在生产机器上做预览时,请使用测试目录和测试端口:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/test/Memind
|
||||
H5_PORT=18081 \
|
||||
VITE_PORT=15173 \
|
||||
ADMIN_PORT=18082 \
|
||||
PLAZA_PORT=13001 \
|
||||
OPS_PORT=13002 \
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:15173 \
|
||||
VITE_MINDSPACE_BASE=http://127.0.0.1:15173 \
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
预览地址:`http://127.0.0.1:15173/?preview=mindspace`。
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
@@ -30,3 +48,28 @@ pnpm open:local-test # 浏览器打开 H5
|
||||
| `PLAZA_APP_DIR` | ../tkmind_go/ui/plaza | Plaza 源码路径 |
|
||||
|
||||
Plaza 公网部署见 [plaza-local.md](./plaza-local.md)。
|
||||
|
||||
## 公众号 Agent 调试
|
||||
|
||||
服务号消息转发到专属 Agent 这条链路默认关闭,只有显式设置下面这些变量后才会启用:
|
||||
|
||||
- `H5_WECHAT_MP_ENABLED=1`
|
||||
- `H5_WECHAT_MP_APP_ID`
|
||||
- `H5_WECHAT_MP_APP_SECRET`
|
||||
- `H5_WECHAT_MP_TOKEN`
|
||||
|
||||
服务号后台需要把服务器地址指向:
|
||||
|
||||
```text
|
||||
{H5_PUBLIC_BASE_URL}/webhooks/wechat-mp/messages
|
||||
```
|
||||
|
||||
当前版本只支持“明文模式”回调,不支持 `aes` 安全模式解密。要保证 H5 微信 OAuth 和公众号消息使用同一个服务号 `AppID`,这样消息里的 `openid` 才能直接命中已绑定用户。
|
||||
|
||||
如果要立即停用这版,不改代码也可以先把:
|
||||
|
||||
```bash
|
||||
H5_WECHAT_MP_ENABLED=0
|
||||
```
|
||||
|
||||
然后重启 `server.mjs`。这相当于运行时止血;如果确认整版不要,再按代码回撤处理。
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Memind 小白使用手册
|
||||
|
||||
这份手册给第一次使用 Memind 的人看。先照着主流程走,熟悉以后再看空间分类、Plaza 和记忆功能。
|
||||
|
||||
## 1. 打开 Memind
|
||||
|
||||
常用入口:
|
||||
|
||||
- 主应用:<https://g2.tkmind.cn/>
|
||||
- 发现广场:<https://plaza.tkmind.cn/plaza>
|
||||
|
||||
打开主应用后,如果已经登录,会看到顶部有你的名字、额度、我的空间、新会话等按钮;页面底部有输入框。
|
||||
|
||||
第一次使用只记住三个位置:
|
||||
|
||||
- 输入框:告诉 AI 你想做什么。
|
||||
- 发送按钮:输入内容后才会亮起。
|
||||
- 我的空间:查看、编辑和分享已经生成的页面或文件。
|
||||
|
||||
## 2. 跟 AI 对话
|
||||
|
||||
在底部输入框里直接写需求,例如:
|
||||
|
||||
```text
|
||||
帮我生成一个 hello 页面
|
||||
```
|
||||
|
||||
也可以写得更具体:
|
||||
|
||||
```text
|
||||
帮我做一个 618 活动页,风格清爽一点,要有标题、活动规则和报名按钮。
|
||||
```
|
||||
|
||||
发送后等待 AI 回复。如果它生成了页面,通常会在回复里给出一个可点击的链接。
|
||||
|
||||
小提示:
|
||||
|
||||
- 需求越清楚,结果越接近你想要的。
|
||||
- 如果不满意,直接继续说“把颜色改成绿色”“加一个价格表”“标题再短一点”。
|
||||
- 顶部会显示账户额度;额度很低时,尽量先想清楚再发送。
|
||||
|
||||
## 3. 实操示例:生成一个精美页面
|
||||
|
||||
下面是一条完整实操流程,可以照着做一遍。
|
||||
|
||||
### 第一步:点新会话
|
||||
|
||||
在聊天页顶部点击“新会话”。
|
||||
|
||||
页面可能会短暂显示:
|
||||
|
||||
```text
|
||||
正在连接会话…
|
||||
```
|
||||
|
||||
等输入框恢复成“输入任务,例如:列出当前目录文件”后,再开始输入。
|
||||
|
||||
### 第二步:输入一个清楚的需求
|
||||
|
||||
不要只写“帮我做个页面”。可以把主题、内容和风格都写出来。
|
||||
|
||||
这次实测使用的示例:
|
||||
|
||||
```text
|
||||
请帮我生成一个精美、有趣的一页式网页,主题叫「时间旅行咖啡馆」。
|
||||
|
||||
页面设定:一家开在午夜街角的咖啡馆,菜单不是咖啡口味,而是不同时间片段:1999、2035、童年夏天、未来清晨。
|
||||
|
||||
页面要求:
|
||||
1. 第一屏要惊艳,有大标题、短副标题和一个醒目的行动按钮。
|
||||
2. 往下有 4 个「时间菜单」卡片,每张卡片写一个时间片段、风味描述和适合谁点。
|
||||
3. 再加一个「今晚营业规则」小版块,语气轻松好玩。
|
||||
4. 风格要精美、有层次,适合手机和电脑打开。
|
||||
5. 生成完成后请保存成一个可以点开的 MindSpace 页面,并把链接发给我。
|
||||
```
|
||||
|
||||
### 第三步:发送并等待
|
||||
|
||||
输入后,“发送”按钮会亮起。点击发送后,页面底部会变成“停止”,输入框也会暂时不能输入。
|
||||
|
||||
这表示 AI 正在生成,不要重复点击,也不要急着刷新页面。
|
||||
|
||||
实测中,AI 会先显示几段进度,比如:
|
||||
|
||||
- 开始制作页面。
|
||||
- 工具完成。
|
||||
- 页面已保存。
|
||||
- 返回一个可以点击的公开链接。
|
||||
|
||||
生成精美页面可能需要几十秒到一分钟左右。
|
||||
|
||||
### 第四步:点开生成链接
|
||||
|
||||
生成完成后,AI 会在聊天里发出页面链接。实测生成的页面标题是:
|
||||
|
||||
```text
|
||||
时间旅行咖啡馆 · 午夜街角的时空驿站
|
||||
```
|
||||
|
||||
打开后能看到完整网页:第一屏有星空背景、大标题、当前时间和“翻阅时间菜单”按钮;往下滚动能看到 4 张时间菜单卡片和营业规则。
|
||||
|
||||
如果点击链接后没有跳转,可以复制链接或在新标签页打开。只要公开地址能打开,就说明页面已经生成成功。
|
||||
|
||||
### 第五步:回到我的空间确认
|
||||
|
||||
页面生成后,可以点顶部“我的空间”,看看它是否出现在“最近页面”。
|
||||
|
||||
注意:实测时,聊天里的公开链接可以正常打开,但“我的空间”的最近页面没有立刻出现这个新页面。遇到这种情况不要慌:
|
||||
|
||||
1. 先回聊天页,从 AI 回复里的链接打开。
|
||||
2. 如果需要再次使用,可以从历史会话里找到这次对话。
|
||||
3. 如果要正式管理、编辑或发布,优先检查“最近页面”“页面草稿”和“公开区”。
|
||||
|
||||
## 4. 查看历史对话
|
||||
|
||||
点击左上角的“展开历史对话”,可以看到以前的会话。
|
||||
|
||||
使用方法:
|
||||
|
||||
- 点击会话标题,可以继续查看或接着聊。
|
||||
- 点击“+ 新对话”,开始一个全新的任务。
|
||||
- 不要随手点“删除”,删除后对应历史会话就不好找了。
|
||||
|
||||
注意:打开历史会话时,页面可能会短暂显示“正在连接会话…”,等几秒即可。
|
||||
|
||||
## 5. 使用我的空间
|
||||
|
||||
点击顶部“我的空间”,进入你的内容管理页。
|
||||
|
||||
这里主要看两块:
|
||||
|
||||
- 空间分类:OA 工作区、私人区、公开区、页面草稿、归档区。
|
||||
- 最近页面:最近生成或保存的页面列表。
|
||||
|
||||
新手优先看“最近页面”。每个页面旁边通常有这些按钮:
|
||||
|
||||
- 预览:打开页面看看效果。
|
||||
- 编辑:继续修改页面内容。
|
||||
- 公开链接:获取可以分享给别人的链接。
|
||||
- 保存长图:把页面导出成长图。
|
||||
- 删除:删除这个页面,谨慎点击。
|
||||
|
||||
如果你只是想找刚刚生成的东西,先看“最近页面”的第一屏。
|
||||
|
||||
## 6. 分享一个页面
|
||||
|
||||
当页面做好后,进入“我的空间”,找到对应页面。
|
||||
|
||||
推荐流程:
|
||||
|
||||
1. 先点“预览”,确认内容没有问题。
|
||||
2. 再点“公开链接”,获取分享地址。
|
||||
3. 把链接发给别人,对方就可以直接打开网页。
|
||||
|
||||
公开页面是一个独立网页,例如这类地址:
|
||||
|
||||
```text
|
||||
https://g2.tkmind.cn/MindSpace/.../public/hello.html
|
||||
```
|
||||
|
||||
分享前最好检查:
|
||||
|
||||
- 页面标题是否正确。
|
||||
- 是否包含隐私信息。
|
||||
- 手机和电脑打开是否都能看清楚。
|
||||
|
||||
## 7. 逛 Plaza 找灵感
|
||||
|
||||
Plaza 是公开作品广场,入口是:
|
||||
|
||||
<https://plaza.tkmind.cn/plaza>
|
||||
|
||||
你可以在这里:
|
||||
|
||||
- 按分类浏览作品,比如学习笔记、旅行攻略、数据分析、创意作品。
|
||||
- 看热门或最新作品。
|
||||
- 打开作品详情页,参考结构、标题和展示方式。
|
||||
- 从顶部“我的空间”回到自己的内容管理页。
|
||||
|
||||
Plaza 更像灵感库,不是你的私人文件夹。自己的页面还是回“我的空间”管理。
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### 发送按钮是灰色的
|
||||
|
||||
通常是因为输入框还没有内容。先输入需求,发送按钮就会变成可点状态。
|
||||
|
||||
### 历史会话一直显示正在连接
|
||||
|
||||
先等几秒。如果仍然不动,可以点“新会话”重新开始,或者刷新页面后再打开历史。
|
||||
|
||||
### 找不到刚生成的页面
|
||||
|
||||
按这个顺序找:
|
||||
|
||||
1. 回到聊天,看 AI 回复里有没有页面链接。
|
||||
2. 点“我的空间”。
|
||||
3. 看“最近页面”的前几项。
|
||||
4. 如果是草稿,看“页面草稿”。
|
||||
5. 如果已经公开,看“公开区”。
|
||||
6. 如果空间里暂时找不到,先从历史会话里的链接打开。
|
||||
|
||||
### 不想让别人看到页面
|
||||
|
||||
不要分享公开链接。发布前检查页面内容,敏感资料建议放在“私人区”,并在公开前做脱敏。
|
||||
|
||||
### 页面做得不满意
|
||||
|
||||
回到对应聊天或点页面的“编辑”,直接提出修改要求。比如:
|
||||
|
||||
```text
|
||||
把首页标题改短一点,按钮放到第一屏,整体更像一个正式活动页。
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
# Memind 生产更新发布指南
|
||||
|
||||
> 本文描述如何把本地开发代码安全发布到 **g2.tkmind.cn** 生产环境。
|
||||
> 发布前请先阅读 [生产 / 测试 / 预览隔离规程](./service-isolation-runbook.md),避免误占生产端口或覆盖用户数据。
|
||||
|
||||
## 1. 架构与发布目标
|
||||
|
||||
用户访问 `https://g2.tkmind.cn/` 的流量路径:
|
||||
|
||||
```text
|
||||
Cloudflare(橙云)
|
||||
→ Cloudflare Tunnel
|
||||
→ Studio Caddy(:8090,g2 负载均衡)
|
||||
├─ 权重 19 → Studio Portal(127.0.0.1:8081) ← 主流量
|
||||
└─ 权重 1 → 105 Portal(经隧道 127.0.0.1:18080 → :8080) ← 灰度副机
|
||||
```
|
||||
|
||||
两台 Portal 都是 **无状态前端**,共用 Studio 上的 goosed 与 MindSpace 数据目录。
|
||||
|
||||
| 角色 | 机器 | 代码目录 | 服务 | 重启方式 |
|
||||
|------|------|----------|------|----------|
|
||||
| 生产主 | Studio Mac(`100.99.38.66`) | `/Users/john/Project/Memind` | Portal `:8081`、Plaza `:3001` | `launchctl kickstart` |
|
||||
| 灰度副 | 105(经 Tailscale `ssh105`) | `/root/tkmind_go/ui/h5` | `goose-h5` `:8080` | `systemctl restart goose-h5` |
|
||||
| 负载均衡 | Studio | `scripts/g2-lb.Caddyfile` | Caddy `:8090` | 改权重后 `caddy reload` |
|
||||
|
||||
更详细的流量与灰度比例说明见 [g2 负载均衡](./g2-load-balancing.md)。
|
||||
|
||||
## 2. 目录与环境对照
|
||||
|
||||
| 环境 | 典型目录 | 端口 | 用途 |
|
||||
|------|----------|------|------|
|
||||
| **生产(Studio)** | `/Users/john/Project/Memind` | `8081` / `3001` | 线上 g2 / plaza |
|
||||
| **开发预览** | `/Users/john/Project/test/Memind` 或本机副本 | `18081` / `13001` | 本地联调,禁止占 `8081` |
|
||||
| **105 副机** | `/root/tkmind_go/ui/h5` | `8080` | g2 灰度流量 |
|
||||
|
||||
**重要:** `rsync_to_server.sh` 会把**你执行命令时所在的本地目录**同步到 Studio 生产目录。发布前请确认当前目录里的代码就是你要上线的版本,而不是半成品或未验证的分支。
|
||||
|
||||
## 3. 发布入口(主流程)
|
||||
|
||||
**推荐唯一入口:** 项目根目录的 `rsync_to_server.sh`
|
||||
|
||||
```bash
|
||||
cd /path/to/your/memind-repo # 开发完成、已自测的目录
|
||||
|
||||
# ① 预览(不修改任何远端)
|
||||
./rsync_to_server.sh --dry-run
|
||||
|
||||
# ② 正式发布(Studio + 105 全量)
|
||||
./rsync_to_server.sh
|
||||
```
|
||||
|
||||
脚本会自动完成:
|
||||
|
||||
1. 本地 Pre-flight(关键文件、安全 patch、exclude 规则)
|
||||
2. Studio Pre-flight(SSH、`.env`、MindSpace、data 完整性)
|
||||
3. 交互确认(可用 `--yes` 跳过)
|
||||
4. rsync 代码 → Studio 生产目录
|
||||
5. rsync 后校验(`.env` 未变、MindSpace 未减少、patch 仍在)
|
||||
6. Studio 上 `npm install` + `npm run build`
|
||||
7. 重启 Studio Portal → Plaza,并做健康检查
|
||||
8. 经 Studio 触发 `scripts/sync-to-105.sh`,同步并重启 105
|
||||
|
||||
## 4. 发布前检查清单
|
||||
|
||||
在 `./rsync_to_server.sh` 之前,逐项确认:
|
||||
|
||||
### 4.1 本地构建与测试
|
||||
|
||||
```bash
|
||||
pnpm install # 依赖有变更时
|
||||
pnpm run build # 前端改动必须能编过
|
||||
pnpm test # 建议跑;涉及核心逻辑时必跑
|
||||
```
|
||||
|
||||
| 改动类型 | 是否必须 build | 能否 `--skip-build` |
|
||||
|----------|----------------|---------------------|
|
||||
| 前端(`.tsx` / `.css` / `src/`) | **是** | 否 |
|
||||
| 仅后端(`.mjs`) | 否(但 build 无害) | 可以 |
|
||||
| 仅文档 / 脚本 | 否 | 可以 |
|
||||
|
||||
### 4.2 生产在线(只读)
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8081/api/status # Studio Portal,期望 ok
|
||||
curl -s http://127.0.0.1:18080/api/status # 105 隧道,期望 ok
|
||||
```
|
||||
|
||||
105 联通必须走 Tailscale,不要用公网 IP:
|
||||
|
||||
```bash
|
||||
ssh ssh105 'echo tunnel-ok-105'
|
||||
```
|
||||
|
||||
### 4.3 安全约束(脚本会自动检查)
|
||||
|
||||
- `server.mjs` 必须含 `WORKSPACE_MAINTENANCE_ENABLED` patch(否则 105 重启会在 rclone 挂载上卡死)
|
||||
- `.env`、`MindSpace/`、`data/` 在 exclude 列表中,**不会被 rsync 覆盖**
|
||||
- rsync 使用 `--delete`:本地已删的文件会从 Studio 删掉(exclude 外的路径)
|
||||
|
||||
### 4.4 发布范围确认
|
||||
|
||||
- `rsync_to_server.sh` 同步的是**整个仓库**(除 exclude 外),不是单个文件
|
||||
- 工作区若有未完成的其它改动,会一并上线——发布前建议 commit 或整理干净
|
||||
- 涉及数据库迁移、批量清数据、支付回调测试等,**不要**直接在生产目录试跑,见 [隔离规程](./service-isolation-runbook.md)
|
||||
|
||||
## 5. 分步与保守发布
|
||||
|
||||
`rsync_to_server.sh` 支持的常用参数:
|
||||
|
||||
| 参数 | 作用 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `--dry-run` | 只预览 diff,不改远端 | **每次正式发布前必做** |
|
||||
| `--only-100` | 只更新 Studio,不推 105 | 先让 95% 主流量生效,105 稍后 |
|
||||
| `--only-105` | 只触发 Studio→105 同步 | Studio 已是最新,只补 105 |
|
||||
| `--skip-build` | 跳过远端 `npm run build` | 纯后端改动且确认 dist 无需更新 |
|
||||
| `--no-restart` | 只同步代码,不重启服务 | 分批发布;需自行重启 |
|
||||
| `--yes` / `-y` | 跳过交互确认 | 自动化或你已看过 dry-run |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 只发 Studio(主流量 95%)
|
||||
./rsync_to_server.sh --only-100
|
||||
|
||||
# 纯 server.mjs 改动,跳过 build
|
||||
./rsync_to_server.sh --skip-build
|
||||
|
||||
# 先同步代码,稍后再重启
|
||||
./rsync_to_server.sh --no-restart
|
||||
# 之后在 Studio 上手动 kickstart,或再跑一遍带重启的同步
|
||||
```
|
||||
|
||||
## 6. 105 单独同步
|
||||
|
||||
若 Studio 代码已是最新,只需更新 105:
|
||||
|
||||
```bash
|
||||
# 在 Studio 生产目录执行
|
||||
cd /Users/john/Project/Memind
|
||||
bash scripts/sync-to-105.sh
|
||||
|
||||
# 或从本地只触发 105 链路
|
||||
./rsync_to_server.sh --only-105
|
||||
```
|
||||
|
||||
`sync-to-105.sh` 会:
|
||||
|
||||
1. rsync 代码(`--delete`)与 `dist/` 到 `root@ssh105:/root/tkmind_go/ui/h5`
|
||||
2. 远端 `npm install` + `npm run build`(可用 `SKIP_BUILD=1` 跳过)
|
||||
3. `systemctl restart goose-h5`
|
||||
4. 校验关键文件 md5 与 `:8080/api/status`
|
||||
|
||||
环境变量(可选):
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `H5_DEPLOY_HOST` | `root@ssh105` | 105 SSH 目标 |
|
||||
| `H5_REMOTE_DIR` | `/root/tkmind_go/ui/h5` | 105 代码路径 |
|
||||
| `H5_SYSTEMD_SERVICE` | `goose-h5` | systemd 服务名 |
|
||||
| `SKIP_BUILD` | `0` | `1` 跳过远端 build |
|
||||
| `NO_RESTART` | `0` | `1` 不重启服务 |
|
||||
|
||||
package.json 中的 `pnpm deploy:105` 等价于本地执行 `bash scripts/sync-to-105.sh`(需本机能 SSH 到 105,或已在 Studio 上)。
|
||||
|
||||
## 7. 发布过程与影响窗口
|
||||
|
||||
全量 `./rsync_to_server.sh` 典型耗时 **5~10 分钟**。
|
||||
|
||||
| 阶段 | 用户可见影响 |
|
||||
|------|----------------|
|
||||
| rsync + npm install + build | 无(旧进程仍在跑) |
|
||||
| Studio Portal 重启 | **8081 短暂不可用**,约 10~40 秒;g2 主流量(约 95%)可能短暂 502 |
|
||||
| 105 goose-h5 重启 | **灰度流量(约 5%)** 短暂不可用 |
|
||||
| Plaza 重启 | plaza.tkmind.cn 可能短暂不可用;与 g2 主聊天无关 |
|
||||
|
||||
Caddy 会对不健康上游做 active health check,105 重启期间可能暂时从池中摘除。
|
||||
|
||||
## 8. 发布后验证
|
||||
|
||||
### 8.1 健康检查
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8081/api/status
|
||||
curl -s http://127.0.0.1:18080/api/status
|
||||
curl -s https://g2.tkmind.cn/api/status
|
||||
```
|
||||
|
||||
### 8.2 确认命中哪台上游
|
||||
|
||||
g2 响应头 `X-Memind-Upstream`:
|
||||
|
||||
- `127.0.0.1:8081` → Studio
|
||||
- `127.0.0.1:18080` → 105
|
||||
|
||||
```bash
|
||||
curl -s -D - -o /dev/null https://g2.tkmind.cn/api/status | grep -i x-memind-upstream
|
||||
```
|
||||
|
||||
### 8.3 业务冒烟
|
||||
|
||||
按本次改动选手动验证,例如:
|
||||
|
||||
- 打开 g2 聊天页,测试新功能
|
||||
- 登录 / 微信 OAuth(若动到 auth)
|
||||
- MindSpace 页面读写(若动到 pages)
|
||||
- Plaza 列表(若动到 plaza)
|
||||
|
||||
### 8.4 日志
|
||||
|
||||
```bash
|
||||
# Studio Portal
|
||||
tail -f ~/Library/Logs/memind-portal.log
|
||||
|
||||
# Studio Plaza
|
||||
tail -f ~/Library/Logs/plaza-prod.log
|
||||
|
||||
# 105(经 ssh105)
|
||||
ssh ssh105 'journalctl -u goose-h5 -n 50 --no-pager'
|
||||
```
|
||||
|
||||
## 9. 回滚思路
|
||||
|
||||
项目没有一键回滚脚本,常见做法:
|
||||
|
||||
1. **代码回滚:** 在本地 git 回到上一个 good commit,`./rsync_to_server.sh` 再发一版
|
||||
2. **仅 Studio:** 若 105 有问题,可临时把 g2 权重调到 100% Studio(见 [g2-load-balancing.md](./g2-load-balancing.md))
|
||||
3. **紧急恢复 Portal:** 若 8081 挂了,见 [隔离规程 · 事故恢复](./service-isolation-runbook.md#事故恢复最小步骤)
|
||||
|
||||
发布前建议打 tag 或记录当前 commit,便于回滚:
|
||||
|
||||
```bash
|
||||
git rev-parse HEAD
|
||||
git tag -a release-2026-06-19 -m "before voice UI deploy"
|
||||
```
|
||||
|
||||
## 10. 其它发布路径(非 g2 主站)
|
||||
|
||||
### 10.1 同步到局域网测试机
|
||||
|
||||
仅源码同步到 `192.168.1.9` 上的 test 目录,**不是生产**:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy-to-test-host.sh --dry-run
|
||||
bash scripts/deploy-to-test-host.sh
|
||||
```
|
||||
|
||||
目标:`test-memind` / `test-memindadm` / `test-memindplaza`。同步后需按该机习惯手动重启服务。
|
||||
|
||||
### 10.2 Plaza 105 静态站
|
||||
|
||||
```bash
|
||||
pnpm deploy:plaza-105
|
||||
```
|
||||
|
||||
依赖 goose-h5 已部署(`pnpm deploy:105` 或全量 rsync)。详见 [Plaza 本机部署](./plaza-local.md)。
|
||||
|
||||
### 10.3 已废弃 / 不可用
|
||||
|
||||
| 命令 | 状态 |
|
||||
|------|------|
|
||||
| `pnpm deploy:prod` | 指向 `../../deploy/deploy-h5-prod.sh`,当前仓库旁路不存在,**勿用** |
|
||||
|
||||
## 11. 推荐发布 SOP(标准作业)
|
||||
|
||||
适合大多数功能迭代的固定步骤:
|
||||
|
||||
```text
|
||||
1. 在测试端口完成开发与自测(18081,勿占 8081)
|
||||
2. pnpm run build && pnpm test
|
||||
3. ./rsync_to_server.sh --dry-run ← 看清将要同步什么
|
||||
4. 确认无多余改动、无数据库破坏性操作
|
||||
5. ./rsync_to_server.sh ← 全量 Studio + 105
|
||||
6. curl 健康检查 + 浏览器冒烟
|
||||
7. 观察 5~10 分钟日志,确认无 ERROR 尖峰
|
||||
```
|
||||
|
||||
**保守版(先主后副):**
|
||||
|
||||
```text
|
||||
1~4 同上
|
||||
5. ./rsync_to_server.sh --only-100
|
||||
6. 冒烟通过后
|
||||
7. ./rsync_to_server.sh --only-105
|
||||
```
|
||||
|
||||
## 12. 相关文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [service-isolation-runbook.md](./service-isolation-runbook.md) | 生产 / 测试端口隔离、禁止事项、事故恢复 |
|
||||
| [g2-load-balancing.md](./g2-load-balancing.md) | g2 权重、105 隧道、灰度比例调整 |
|
||||
| [local-dev.md](./local-dev.md) | 本地开发端口与 preview |
|
||||
| [plaza-local.md](./plaza-local.md) | Plaza 部署与 Tunnel |
|
||||
|
||||
---
|
||||
|
||||
**维护说明:** 若部署脚本路径、主机名或 systemd 服务名变更,请同步更新本文与 `rsync_to_server.sh` 头部注释。
|
||||
@@ -0,0 +1,683 @@
|
||||
# 待办 / 日程 / 提醒能力设计文档
|
||||
|
||||
> **状态:** 设计稿
|
||||
> **目标版本:** v0.2.x
|
||||
> **适用范围:** H5 门户、微信服务号 Agent、MindSpace 行程展示页
|
||||
> **生产提醒:** 当前仓库目录可能承载生产服务。开发和验证按 `docs/service-isolation-runbook.md` 先在测试目录完成,不在生产目录直接跑迁移或重启服务。
|
||||
|
||||
## 背景
|
||||
|
||||
用户希望用自然语言完成两类高频动作:
|
||||
|
||||
1. 记录事项:例如“我明天要去开会,帮我记录增加一个提醒”。
|
||||
2. 查看计划:例如“看看我的行程计划”。
|
||||
|
||||
现状里已有几个可利用基础:
|
||||
|
||||
- `capabilities.mjs` 已预留 `todo` 能力,但默认关闭,且当前项目没有本地一等的待办/日程表。
|
||||
- `wechat-mp.mjs` 已能接收微信服务号文本、转给用户专属 Agent、再通过客服消息回复。
|
||||
- MindSpace 已有生成公开/私有 HTML 页面的能力,适合把一周行程做成简洁精美页面。
|
||||
|
||||
缺口在于:日程和提醒还没有结构化存储、没有到点派发 worker、没有 Agent 可调用的本地日程工具,也没有固定的澄清规则。
|
||||
|
||||
## 产品目标
|
||||
|
||||
### 必须支持
|
||||
|
||||
- 用户用自然语言创建待办、日程、提醒。
|
||||
- 缺少必要时间信息时,助手必须追问,不猜测具体钟点。
|
||||
- 用户明确“不提醒”时,只记录到待办或日程列表,不创建提醒推送。
|
||||
- 对会议类表达,如果已有明确开始时间但没有指定提前多久提醒,默认提前 1 小时提醒。
|
||||
- 用户查看行程时,按指定时间范围展示;未指定范围时展示最近 7 天。
|
||||
- 行程查询结果优先以简洁消息回复;当内容较多或用户要求“页面/好看一点”时,生成 MindSpace HTML 页面。
|
||||
|
||||
### 暂不纳入 MVP
|
||||
|
||||
- 复杂重复规则,如“每月第二个周三”。
|
||||
- 多人共享日程、会议邀请、外部日历同步。
|
||||
- 地理围栏提醒。
|
||||
- 跨端原生 push。MVP 先走已绑定微信服务号通知通道,后续再扩展短信、邮件或 App push。
|
||||
|
||||
## 概念模型
|
||||
|
||||
| 概念 | 说明 | 例子 |
|
||||
|------|------|------|
|
||||
| 待办 task | 有或没有截止时间的任务,不一定占用时间段 | “买票”、“周五前交材料” |
|
||||
| 日程 event | 有开始时间,通常可以有结束时间,占用时间段 | “今天下午三点开会” |
|
||||
| 提醒 reminder | 某个时间点触发的一次通知,可挂在 task/event 上 | “会议前 1 小时提醒” |
|
||||
|
||||
设计原则:
|
||||
|
||||
- 待办和日程是“记录”;提醒是“通知计划”。
|
||||
- 一条待办/日程可以没有提醒。
|
||||
- 一条待办/日程可以有多条提醒,但 MVP 默认最多一条。
|
||||
- 所有时间入库使用 epoch 毫秒,额外保存用户时区,展示时按用户时区格式化。
|
||||
|
||||
## 自然语言交互规则
|
||||
|
||||
### 创建提醒或日程
|
||||
|
||||
#### 规则 1:缺少事件发生时间时必须追问
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
我明天要去开会,帮我记录增加一个提醒
|
||||
```
|
||||
|
||||
如果系统当前日期是 2026-06-18,助手能解析“明天”为 2026-06-19,但缺少具体钟点,不能创建到点提醒。回复:
|
||||
|
||||
```text
|
||||
可以。我先记下“明天开会”。你想几点提醒?会议大概几点开始,提前多久提醒你?
|
||||
```
|
||||
|
||||
待用户补充后再创建。
|
||||
|
||||
#### 规则 2:用户明确不提醒时只记录
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
不用提醒,先记一下
|
||||
```
|
||||
|
||||
系统创建一条待办或全天日程,不创建 `h5_schedule_reminders` 记录。回复:
|
||||
|
||||
```text
|
||||
已记录到待办列表:明天开会,未设置提醒。
|
||||
```
|
||||
|
||||
#### 规则 3:会议类有明确开始时间时默认提前 1 小时
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
今天下午三点有个会
|
||||
```
|
||||
|
||||
如果系统当前日期是 2026-06-18,解析为:
|
||||
|
||||
- 日程:2026-06-18 15:00,标题“开会”或“会议”
|
||||
- 提醒:2026-06-18 14:00
|
||||
- 提醒偏移:60 分钟
|
||||
|
||||
回复:
|
||||
|
||||
```text
|
||||
已记录:今天 15:00 会议。我会提前 1 小时,也就是 14:00 提醒你。
|
||||
```
|
||||
|
||||
#### 规则 4:非会议类默认不擅自加提前提醒
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
明天上午十点去取护照
|
||||
```
|
||||
|
||||
创建日程,但如果用户没有说“提醒我”,不自动创建提醒。回复可提示:
|
||||
|
||||
```text
|
||||
已记录:明天 10:00 取护照。需要我提前提醒的话,可以告诉我提前多久。
|
||||
```
|
||||
|
||||
#### 规则 5:用户直接指定提醒时间时按提醒时间创建
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
明天上午九点提醒我带材料
|
||||
```
|
||||
|
||||
创建待办“带材料”,并创建 `remind_at = 明天 09:00` 的提醒。此时不需要追问“提前多久”。
|
||||
|
||||
#### 规则 6:每天固定时间推送当天待办记录
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
每天早上 7 点给我发一天的待办记录
|
||||
```
|
||||
|
||||
系统必须创建一条每日待办摘要订阅:
|
||||
|
||||
- 类型:`todo_day`
|
||||
- 时间:每天 07:00
|
||||
- 通道:微信服务号
|
||||
- 行为:每天到点查询用户当天待办记录,通过服务号主动发送给用户
|
||||
|
||||
回复:
|
||||
|
||||
```text
|
||||
已设置:我会每天早上 7点 通过服务号把当天待办记录发给你。
|
||||
```
|
||||
|
||||
如果用户只说“每天给我发待办记录”,缺少时间,必须追问具体时间。
|
||||
|
||||
### 查看行程
|
||||
|
||||
#### 规则 7:没有时间范围时默认最近 7 天
|
||||
|
||||
用户说:
|
||||
|
||||
```text
|
||||
看看我的行程计划
|
||||
```
|
||||
|
||||
查询范围:
|
||||
|
||||
- 起点:用户时区当天 00:00
|
||||
- 终点:起点 + 7 天
|
||||
|
||||
回复格式优先按日期分组:
|
||||
|
||||
```text
|
||||
未来 7 天你有 3 个安排:
|
||||
|
||||
6 月 18 日 周四
|
||||
14:00 会议提醒
|
||||
15:00 会议
|
||||
|
||||
6 月 19 日 周五
|
||||
全天 开会
|
||||
```
|
||||
|
||||
#### 规则 8:指定时间范围时按范围查询
|
||||
|
||||
用户说“看下明天的安排”、“下周有什么会”、“6 月 20 到 25 日的计划”,按指定范围查询。
|
||||
|
||||
#### 规则 9:需要精美展示时生成页面
|
||||
|
||||
触发条件:
|
||||
|
||||
- 用户明确说“用页面展示”、“好看一点”、“生成一个行程页”。
|
||||
- 查询结果超过 8 条,普通文本不易读。
|
||||
- 用户来自 H5 页面上下文,适合打开 MindSpace 页面。
|
||||
|
||||
页面要求:
|
||||
|
||||
- 第一屏直接是行程表,不做营销式 landing page。
|
||||
- 按日期分组,突出今天、明天、逾期、即将到来。
|
||||
- 对日程、待办、提醒用不同视觉标识。
|
||||
- 移动端优先,宽屏下使用双栏或周视图。
|
||||
|
||||
## 系统架构
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U["用户文本: 微信/H5"] --> I["意图解析层"]
|
||||
I -->|缺必要信息| Q["追问用户"]
|
||||
I -->|可执行| T["Schedule Service"]
|
||||
T --> DB["MySQL: schedule tables"]
|
||||
T --> R["Reminder Worker"]
|
||||
R --> W["微信通知通道"]
|
||||
T --> V["行程查询 API"]
|
||||
V --> A["Agent 文本回复"]
|
||||
V --> P["MindSpace 行程页面"]
|
||||
```
|
||||
|
||||
### 组件职责
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| 意图解析层 | 从自然语言提取 action、title、date/time、reminder offset、query range;判断是否需要追问 |
|
||||
| Schedule Service | 统一创建、查询、修改、取消待办/日程/提醒 |
|
||||
| Reminder Worker | 周期扫描到期提醒,锁定、投递、记录成功/失败 |
|
||||
| 微信通知通道 | 发送提醒消息。MVP 优先复用已绑定服务号 openid,生产上线前确认模板/订阅通知资质 |
|
||||
| MindSpace 展示 | 将查询结果渲染为 HTML 页面,保存到用户 `public/` 或私有页面记录 |
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### `h5_schedule_items`
|
||||
|
||||
记录待办和日程主体。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS h5_schedule_items (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
kind ENUM('task', 'event') NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
status ENUM('active', 'completed', 'cancelled', 'deleted') NOT NULL DEFAULT 'active',
|
||||
start_at BIGINT NULL,
|
||||
end_at BIGINT NULL,
|
||||
due_at BIGINT NULL,
|
||||
all_day TINYINT(1) NOT NULL DEFAULT 0,
|
||||
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
location VARCHAR(255) NULL,
|
||||
source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
|
||||
source_session_id VARCHAR(128) NULL,
|
||||
source_message_id VARCHAR(128) NULL,
|
||||
source_text TEXT NULL,
|
||||
metadata_json JSON NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
deleted_at BIGINT NULL,
|
||||
KEY idx_schedule_user_time (user_id, status, start_at, due_at),
|
||||
KEY idx_schedule_user_updated (user_id, updated_at),
|
||||
CONSTRAINT fk_schedule_item_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `kind = task`:待办,通常使用 `due_at`,也可没有时间。
|
||||
- `kind = event`:日程,通常使用 `start_at/end_at`。
|
||||
- `all_day = 1`:只有日期没有具体钟点,如“明天开会”。
|
||||
- `source_text`:保留用户原文,便于回溯和修正。
|
||||
|
||||
### `h5_schedule_reminders`
|
||||
|
||||
记录提醒计划和投递状态。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS h5_schedule_reminders (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
item_id CHAR(36) NOT NULL,
|
||||
remind_at BIGINT NOT NULL,
|
||||
offset_minutes INT NULL,
|
||||
channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
|
||||
status ENUM('pending', 'locked', 'sent', 'failed', 'cancelled') NOT NULL DEFAULT 'pending',
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
last_error VARCHAR(500) NULL,
|
||||
locked_until BIGINT NULL,
|
||||
sent_at BIGINT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
UNIQUE KEY uq_schedule_item_remind_at (item_id, remind_at, channel),
|
||||
KEY idx_reminder_due (status, remind_at),
|
||||
KEY idx_reminder_user (user_id, status, remind_at),
|
||||
CONSTRAINT fk_schedule_reminder_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_schedule_reminder_item FOREIGN KEY (item_id) REFERENCES h5_schedule_items(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### `h5_schedule_delivery_logs`
|
||||
|
||||
记录每次投递尝试,用于排错和审计。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS h5_schedule_delivery_logs (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
reminder_id CHAR(36) NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
channel ENUM('wechat', 'in_app') NOT NULL,
|
||||
status ENUM('success', 'failed') NOT NULL,
|
||||
provider_message_id VARCHAR(128) NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(500) NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
KEY idx_delivery_reminder (reminder_id, created_at),
|
||||
KEY idx_delivery_user (user_id, created_at),
|
||||
CONSTRAINT fk_schedule_delivery_reminder FOREIGN KEY (reminder_id) REFERENCES h5_schedule_reminders(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_schedule_delivery_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 后端模块设计
|
||||
|
||||
### 新增文件建议
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `schedule-service.mjs` | 领域服务:创建、查询、更新、取消、完成 |
|
||||
| `schedule-intent.mjs` | 轻量规则解析和澄清决策,先覆盖中文常用表达 |
|
||||
| `schedule-reminder-worker.mjs` | 到点提醒扫描、锁定、投递、重试 |
|
||||
| `schedule-render.mjs` | 文本摘要和页面数据结构渲染 |
|
||||
| `schedule-service.test.mjs` | 服务层单测 |
|
||||
| `schedule-intent.test.mjs` | 意图解析和追问规则单测 |
|
||||
| `schedule-reminder-worker.test.mjs` | worker 锁和重试单测 |
|
||||
|
||||
### `schedule-service.mjs`
|
||||
|
||||
建议导出:
|
||||
|
||||
```js
|
||||
export function createScheduleService(pool, deps = {}) {
|
||||
return {
|
||||
createItem,
|
||||
updateItem,
|
||||
completeItem,
|
||||
cancelItem,
|
||||
listItems,
|
||||
createReminder,
|
||||
cancelReminder,
|
||||
listDueReminders,
|
||||
lockReminder,
|
||||
markReminderSent,
|
||||
markReminderFailed,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
关键约束:
|
||||
|
||||
- 所有写入校验 `user_id` 所属。
|
||||
- 删除使用软删除,避免误删历史提醒。
|
||||
- 创建提醒时必须确认 `remind_at` 是具体时间点,不能是全天日期。
|
||||
- `remind_at <= now` 的提醒允许创建,但 worker 应尽快投递,并在回复里提示“时间已到,会立即提醒”。
|
||||
|
||||
### `schedule-intent.mjs`
|
||||
|
||||
MVP 不必追求全量 NLP。先做规则 + LLM 辅助的混合策略:
|
||||
|
||||
1. 规则层识别高置信意图:提醒、开会、行程查询、不提醒、完成/取消。
|
||||
2. 规则层解析常用中文时间:今天、明天、后天、上午/下午/晚上、几点、半点、下周。
|
||||
3. 不确定时让 Agent 追问,而不是静默创建。
|
||||
4. 后续可引入 LLM JSON 解析,但输出必须过 schema 校验。
|
||||
|
||||
解析结果结构:
|
||||
|
||||
```ts
|
||||
type ScheduleIntent =
|
||||
| {
|
||||
action: 'create';
|
||||
kind: 'task' | 'event';
|
||||
title: string;
|
||||
startAt?: number;
|
||||
endAt?: number;
|
||||
dueAt?: number;
|
||||
allDay?: boolean;
|
||||
reminder?: { remindAt?: number; offsetMinutes?: number; explicitNoReminder?: boolean };
|
||||
needsClarification?: Array<'event_time' | 'reminder_time' | 'reminder_offset'>;
|
||||
}
|
||||
| {
|
||||
action: 'query';
|
||||
rangeStart?: number;
|
||||
rangeEnd?: number;
|
||||
preferPage?: boolean;
|
||||
}
|
||||
| {
|
||||
action: 'update' | 'cancel' | 'complete';
|
||||
targetText: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Agent 接入策略
|
||||
|
||||
推荐分两阶段做。
|
||||
|
||||
#### 阶段 A:微信服务号入口先做确定性拦截
|
||||
|
||||
在 `wechat-mp.mjs` 转发给 Agent 前增加可选的 schedule preflight:
|
||||
|
||||
1. 对用户文本调用 `schedule-intent.mjs`。
|
||||
2. 如果能确定执行,直接调用 `schedule-service.mjs` 并通过微信回复结果。
|
||||
3. 如果缺必要信息,直接追问。
|
||||
4. 如果不是日程意图,继续走现有 Agent 回复链路。
|
||||
|
||||
优点:
|
||||
|
||||
- 不依赖 Agent 是否会正确调用工具。
|
||||
- 对“提醒”这种强业务流程更稳定。
|
||||
- 不需要先改 goosed extension。
|
||||
|
||||
#### 阶段 B:给 H5 Agent 增加本地 schedule 工具
|
||||
|
||||
后续把日程能力暴露为平台工具,而不是让 Agent 调 H5 内部 API。建议工具:
|
||||
|
||||
- `schedule_create_item`
|
||||
- `schedule_list_items`
|
||||
- `schedule_update_item`
|
||||
- `schedule_cancel_item`
|
||||
- `schedule_create_reminder`
|
||||
- `schedule_render_plan_page`
|
||||
|
||||
注意:当前 `api_lockdown` 会阻止 Agent 经代理访问 H5 本地接口,所以不要设计成“Agent 直接请求 `/mindspace/...` 或 `/api/schedule/...`”。更稳的是在服务端扩展平台工具,或在代理层专门白名单化受控的 schedule 工具调用。
|
||||
|
||||
## API 设计
|
||||
|
||||
H5 前端和管理调试可用 REST API;Agent 工具不直接走这些 API。
|
||||
|
||||
### 创建事项
|
||||
|
||||
`POST /api/schedule/items`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "event",
|
||||
"title": "会议",
|
||||
"start_at": 1781766000000,
|
||||
"end_at": null,
|
||||
"all_day": false,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"source_text": "今天下午三点有个会",
|
||||
"reminder": {
|
||||
"remind_at": 1781762400000,
|
||||
"offset_minutes": 60,
|
||||
"channel": "wechat"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查询事项
|
||||
|
||||
`GET /api/schedule/items?from=1781712000000&to=1782316800000&include_reminders=1`
|
||||
|
||||
返回按时间升序的 items,每条带提醒摘要。
|
||||
|
||||
### 更新、完成、取消
|
||||
|
||||
- `PATCH /api/schedule/items/:id`
|
||||
- `POST /api/schedule/items/:id/complete`
|
||||
- `POST /api/schedule/items/:id/cancel`
|
||||
- `POST /api/schedule/reminders/:id/cancel`
|
||||
|
||||
所有接口需要登录态和 user ownership 校验。
|
||||
|
||||
## 提醒 worker 设计
|
||||
|
||||
### 扫描策略
|
||||
|
||||
每 30 秒扫描一次:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM h5_schedule_reminders
|
||||
WHERE status = 'pending'
|
||||
AND remind_at <= ?
|
||||
ORDER BY remind_at ASC
|
||||
LIMIT 50
|
||||
```
|
||||
|
||||
锁定时使用事务和状态更新:
|
||||
|
||||
```sql
|
||||
UPDATE h5_schedule_reminders
|
||||
SET status = 'locked',
|
||||
locked_until = ?,
|
||||
attempts = attempts + 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND status = 'pending';
|
||||
```
|
||||
|
||||
投递成功后标记 `sent`;失败则:
|
||||
|
||||
- 可重试错误:状态改回 `pending`,`remind_at = now + backoff`。
|
||||
- 不可重试错误:状态改为 `failed`。
|
||||
- 超过最大次数:状态改为 `failed`。
|
||||
|
||||
### 幂等和并发
|
||||
|
||||
- `locked_until` 防止多进程重复投递。
|
||||
- `delivery_logs` 记录每次尝试。
|
||||
- `uq_schedule_item_remind_at` 防止同一事项重复创建同一时间提醒。
|
||||
|
||||
### 通知通道
|
||||
|
||||
MVP:
|
||||
|
||||
- 已绑定微信服务号用户:通过微信通知。
|
||||
- 未绑定或通知失败:保留站内提醒状态,用户下次打开 H5 时展示。
|
||||
|
||||
生产上线前必须确认:
|
||||
|
||||
- 服务号客服消息是否适用于该次主动提醒。
|
||||
- 是否需要模板消息或订阅通知模板。
|
||||
- 对失败码做明确分流,例如未关注、超出发送窗口、模板不可用。
|
||||
|
||||
## 行程页面设计
|
||||
|
||||
### 页面数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "未来 7 天行程",
|
||||
"range_label": "6 月 18 日 - 6 月 24 日",
|
||||
"days": [
|
||||
{
|
||||
"date": "2026-06-18",
|
||||
"weekday": "周四",
|
||||
"items": [
|
||||
{
|
||||
"time": "15:00",
|
||||
"title": "会议",
|
||||
"kind": "event",
|
||||
"reminder": "14:00 提醒",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 视觉要求
|
||||
|
||||
- 页面第一屏就是行程,不做大段介绍。
|
||||
- 移动端使用纵向时间线;桌面端可使用 7 日网格。
|
||||
- 颜色不使用单一紫蓝渐变;建议用清爽白底、墨色文本、低饱和蓝/绿/橙作为状态色。
|
||||
- 所有文字在 375px 宽度下不溢出。
|
||||
- 对“已过期、今天、明天、已提醒、无提醒”有明确状态。
|
||||
|
||||
## 权限和隐私
|
||||
|
||||
- 行程属于用户私密数据,默认不发布到 Plaza。
|
||||
- 生成 MindSpace 页面时默认保存为私有草稿或用户私有空间;只有用户明确要求分享时才创建公开页。
|
||||
- API 只返回当前登录用户的数据。
|
||||
- 日程原文 `source_text` 可能含隐私,后台日志不要直接打印全文。
|
||||
|
||||
## 配置项
|
||||
|
||||
建议新增环境变量:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `H5_SCHEDULE_ENABLED` | `0` | 是否启用日程 API 和微信 preflight |
|
||||
| `H5_REMINDER_WORKER_ENABLED` | `0` | 是否启动提醒 worker |
|
||||
| `H5_REMINDER_SCAN_INTERVAL_MS` | `30000` | worker 扫描间隔 |
|
||||
| `H5_REMINDER_DEFAULT_MEETING_OFFSET_MINUTES` | `60` | 会议默认提前提醒分钟数 |
|
||||
| `H5_REMINDER_MAX_ATTEMPTS` | `5` | 最大投递次数 |
|
||||
| `H5_DEFAULT_TIMEZONE` | `Asia/Shanghai` | 默认用户时区 |
|
||||
|
||||
部署“每天早上 7 点服务号推送待办记录”时,至少需要:
|
||||
|
||||
```bash
|
||||
H5_WECHAT_MP_ENABLED=1
|
||||
H5_SCHEDULE_ENABLED=1
|
||||
H5_REMINDER_WORKER_ENABLED=1
|
||||
H5_DEFAULT_TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
## 开发步骤
|
||||
|
||||
### P0:设计和测试骨架
|
||||
|
||||
- 新增本文档。
|
||||
- 新增 `schedule-intent.test.mjs`,把关键中文场景先写成测试。
|
||||
- 新增空服务骨架,确保不影响现有启动。
|
||||
|
||||
### P1:本地存储和 API
|
||||
|
||||
- 在 `schema.sql` 加三张表。
|
||||
- 在 `db.mjs` 的 `migrateSchema` 加 `CREATE TABLE IF NOT EXISTS`。
|
||||
- 实现 `schedule-service.mjs`。
|
||||
- 实现 REST API,并补 `src/api/client.ts` 类型。
|
||||
|
||||
### P2:微信入口 preflight
|
||||
|
||||
- 在 `wechat-mp.mjs` 中,当 `H5_SCHEDULE_ENABLED=1` 时启用日程意图解析。
|
||||
- 对可执行请求直接创建并回复。
|
||||
- 对缺信息请求直接追问。
|
||||
- 非日程消息保持现有 Agent 路径。
|
||||
|
||||
### P3:提醒 worker
|
||||
|
||||
- 实现 `schedule-reminder-worker.mjs`。
|
||||
- 在 `server.mjs` 启动时按 env 开关启动。
|
||||
- 先接入微信发送方法,失败后写 delivery log。
|
||||
|
||||
### P4:行程展示
|
||||
|
||||
- 文本摘要:直接在微信/H5 聊天里展示。
|
||||
- 页面摘要:生成 MindSpace HTML 草稿或公开页,用户确认后再公开。
|
||||
|
||||
## 测试计划
|
||||
|
||||
### 单元测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- “我明天要去开会,帮我记录增加一个提醒”解析为缺 `event_time` 和 `reminder_offset`。
|
||||
- “不用提醒,先记一下”不会创建 reminder。
|
||||
- “今天下午三点有个会”创建 event,并默认 offset 60。
|
||||
- “明天上午九点提醒我带材料”创建 task reminder,remind_at 为明天 09:00。
|
||||
- “看看我的行程计划”默认查询最近 7 天。
|
||||
- “看下明天的安排”查询明天 00:00 到后天 00:00。
|
||||
|
||||
### 服务层测试
|
||||
|
||||
- 创建 event + reminder 成功。
|
||||
- 同一 item 同一 remind_at 重复创建被幂等处理。
|
||||
- cancel item 后 reminder 自动不可投递。
|
||||
- listItems 不返回其他用户数据。
|
||||
|
||||
### Worker 测试
|
||||
|
||||
- 到期 reminder 被锁定并发送。
|
||||
- 并发 worker 不重复发送。
|
||||
- 发送失败按 backoff 重试。
|
||||
- 超过最大次数标记 failed。
|
||||
|
||||
### 回归测试
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
涉及微信入口时补 `wechat-mp.test.mjs`:
|
||||
|
||||
- 日程意图命中时不进入 Agent。
|
||||
- 非日程文本仍走 Agent。
|
||||
- 重复微信 `msgId` 仍只处理一次。
|
||||
|
||||
## 验收用例
|
||||
|
||||
| 输入 | 期望 |
|
||||
|------|------|
|
||||
| 我明天要去开会,帮我记录增加一个提醒 | 追问具体会议时间和提前多久提醒 |
|
||||
| 不提醒,先记一下 | 创建待办/全天日程,无 reminder |
|
||||
| 今天下午三点有个会 | 创建 15:00 会议,14:00 提醒 |
|
||||
| 明天上午九点提醒我带材料 | 创建 09:00 提醒 |
|
||||
| 每天早上 7 点给我发一天的待办记录 | 创建每日 07:00 服务号待办摘要订阅 |
|
||||
| 看看我的行程计划 | 展示最近 7 天 |
|
||||
| 看看明天行程 | 只展示明天 |
|
||||
| 生成一个好看的本周行程页面 | 生成 MindSpace 行程页面 |
|
||||
|
||||
## 风险和决策
|
||||
|
||||
| 风险 | 处理 |
|
||||
|------|------|
|
||||
| 微信主动通知规则限制 | 上线前确认模板/订阅通知资质;客服消息仅作可用时通道 |
|
||||
| 自然语言时间歧义 | 缺关键时间必须追问 |
|
||||
| Agent 幻觉创建 | MVP 在微信入口做确定性 preflight,后续再开放工具 |
|
||||
| 生产库迁移风险 | 测试库先迁移,生产按维护窗口执行 |
|
||||
| 用户隐私 | 默认私有,不自动公开行程页面 |
|
||||
|
||||
## 推荐结论
|
||||
|
||||
这项能力可做,建议按 P1 到 P3 先交付一个可靠 MVP:能记录、能追问、能查询、能到点提醒。MindSpace 精美行程页作为 P4 增强,不阻塞核心提醒能力上线。
|
||||
@@ -0,0 +1,222 @@
|
||||
> **生产环境警示:当前目录 `/Users/john/Project/Memind` 为生产目录与生产环境,禁止重启服务,所有操作必须谨慎并优先避免影响在线流量。**
|
||||
|
||||
# 生产 / 测试 / 预览隔离规程
|
||||
|
||||
这份文档的目标只有一个:开发预览不能再影响生产。
|
||||
|
||||
## 端口边界
|
||||
|
||||
| 环境 | 目录 | 用途 | 端口 |
|
||||
|------|------|------|------|
|
||||
| 生产 | `/Users/john/Project/Memind` | `g2.tkmind.cn` 当前在线服务 | `8081` |
|
||||
| 生产 Plaza | `/Users/john/Project/Memind` + Plaza | `plaza.tkmind.cn` 当前在线服务 | `3001` |
|
||||
| 生产 Caddy | `/Users/john/Project/Memind/scripts/g2-lb.Caddyfile` | Cloudflare Tunnel 回源入口 | `8090` |
|
||||
| 测试 Portal | `/Users/john/Project/test/Memind` | 开发预览 API / Portal | `18081` |
|
||||
| 测试 Vite | `/Users/john/Project/test/Memind` | 开发预览前端 | `15173` |
|
||||
| 测试 Admin | `/Users/john/Project/test/Memind` | 开发预览后台 | `18082` |
|
||||
| 测试 Plaza | `/Users/john/Project/test/Memind` | 开发预览 Plaza | `13001` |
|
||||
| 测试 Ops | `/Users/john/Project/test/Memind` | 开发预览 Ops | `13002` |
|
||||
|
||||
硬规则:
|
||||
|
||||
- 不在开发预览中使用 `8081`、`3001`、`8090`。
|
||||
- 不在生产目录里跑会清理端口的开发脚本。
|
||||
- 不执行 `scripts/install-prod-services.sh` 来做开发预览;它会释放生产端口。
|
||||
- 不手动 kill `8081` 上的进程,除非目标就是恢复/重启生产,并且已经确认影响窗口。
|
||||
|
||||
## Harness 角色边界
|
||||
|
||||
Harness 是开发工具层,不是生产服务层。
|
||||
|
||||
正确关系:
|
||||
|
||||
```text
|
||||
Codex / Cursor / Goose 开发过程
|
||||
-> harness 记忆、recall、context pack、审计
|
||||
-> Memind / tkmind_go 开发
|
||||
-> 测试通过后发布到生产
|
||||
```
|
||||
|
||||
禁止关系:
|
||||
|
||||
```text
|
||||
g2.tkmind.cn 生产请求
|
||||
-> 必须依赖 harness 才能运行
|
||||
```
|
||||
|
||||
本机已有全局 harness:
|
||||
|
||||
```text
|
||||
/Users/john/Project/harness
|
||||
```
|
||||
|
||||
它应该嵌入 Codex、Cursor 这类开发工具,让项目开发有记忆:
|
||||
|
||||
```bash
|
||||
/Users/john/Project/harness/bin/codex-harness
|
||||
/Users/john/Project/harness/bin/install_cursor_hooks.sh
|
||||
/Users/john/Project/harness/bin/start_dashboard.sh
|
||||
```
|
||||
|
||||
开发时按项目目录区分记忆上下文:
|
||||
|
||||
| 项目目录 | 记忆含义 |
|
||||
|----------|----------|
|
||||
| `/Users/john/Project/Memind` | 生产项目的开发记忆 |
|
||||
| `/Users/john/Project/test/Memind` | 测试项目的开发记忆 |
|
||||
| `/Users/john/Project/test/test_tkmind_go` | 测试 Goose 的开发记忆 |
|
||||
|
||||
硬规则:
|
||||
|
||||
- 生产启动脚本不能依赖 harness 才能启动。
|
||||
- 生产请求链路不能调用 harness 才能响应。
|
||||
- 测试开发可以使用 harness,但不能写入或覆盖生产运行数据。
|
||||
- 如果要做测试专用长期记忆,优先放在 `/Users/john/Project/test/harness` 或明确的 test 项目命名空间。
|
||||
|
||||
## 当前生产如何确认
|
||||
|
||||
只读检查:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/Memind
|
||||
lsof -nP -iTCP:8081 -sTCP:LISTEN
|
||||
curl -s http://127.0.0.1:8081/api/status
|
||||
cat .h5.pid
|
||||
```
|
||||
|
||||
期望:
|
||||
|
||||
- `8081` 有 `node server.mjs` 监听。
|
||||
- `/api/status` 返回 `ok`。
|
||||
- `.h5.pid` 指向当前生产进程。
|
||||
|
||||
不要用下面这些命令做普通预览:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
pnpm dev:server
|
||||
pnpm start
|
||||
pnpm start:plaza
|
||||
node scripts/dev.mjs
|
||||
node server.mjs
|
||||
scripts/install-prod-services.sh
|
||||
```
|
||||
|
||||
这些命令如果没有端口隔离,可能占用或释放生产端口。
|
||||
|
||||
## 下次开发要预览,怎么做
|
||||
|
||||
优先在测试目录进行:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/test/Memind
|
||||
```
|
||||
|
||||
启动前先确认生产还在:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8081/api/status
|
||||
lsof -nP -iTCP:8081 -sTCP:LISTEN
|
||||
```
|
||||
|
||||
再用测试端口启动预览:
|
||||
|
||||
```bash
|
||||
H5_PORT=18081 \
|
||||
VITE_PORT=15173 \
|
||||
ADMIN_PORT=18082 \
|
||||
PLAZA_PORT=13001 \
|
||||
OPS_PORT=13002 \
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:15173 \
|
||||
VITE_MINDSPACE_BASE=http://127.0.0.1:15173 \
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
访问地址:
|
||||
|
||||
| 页面 | 地址 |
|
||||
|------|------|
|
||||
| H5 预览 | `http://127.0.0.1:15173/?preview=mindspace` |
|
||||
| 测试 Portal | `http://127.0.0.1:18081/api/status` |
|
||||
| 测试 Admin | `http://127.0.0.1:18082/healthz` |
|
||||
| 测试 Plaza | `http://127.0.0.1:13001/plaza` |
|
||||
| 测试 Ops | `http://127.0.0.1:13002/ops/` |
|
||||
|
||||
如果只改前端样式,优先只启动 Vite:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/test/Memind
|
||||
VITE_PORT=15173 \
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:15173 \
|
||||
VITE_MINDSPACE_BASE=http://127.0.0.1:15173 \
|
||||
pnpm dev:vite -- --host 127.0.0.1 --port 15173
|
||||
```
|
||||
|
||||
这种方式不会碰 `8081`,适合做 UI 预览。
|
||||
|
||||
## Goose 测试服务
|
||||
|
||||
生产 Goose 端口当前不要复用。测试 Goose 放在:
|
||||
|
||||
```text
|
||||
/Users/john/Project/test/test_tkmind_go
|
||||
```
|
||||
|
||||
测试 Goose 端口建议固定为:
|
||||
|
||||
| 用途 | 端口 |
|
||||
|------|------|
|
||||
| 测试 goosed A | `18106` |
|
||||
| 测试 goosed B | `18107` |
|
||||
|
||||
测试 H5 如果要连测试 Goose,必须在测试目录 `.env` 中显式设置对应地址,不能指向生产 Goose。
|
||||
|
||||
## 数据库口径
|
||||
|
||||
短期如果沿用同一个 RDS 库,只允许做 UI 和非破坏性流程预览。
|
||||
|
||||
不能在共用库时做这些事:
|
||||
|
||||
- 改表结构。
|
||||
- 跑迁移脚本。
|
||||
- 批量清理数据。
|
||||
- 测试支付回调、发布审核、权限变更等会污染真实用户状态的流程。
|
||||
|
||||
涉及数据结构或真实业务状态的开发,先建独立测试库,再预览。
|
||||
|
||||
## 发布前流程
|
||||
|
||||
完整步骤、分步发布、105 同步与回滚见 **[生产更新发布指南](./release-deploy.md)**。
|
||||
|
||||
最小检查:
|
||||
|
||||
1. 在测试端口(如 `18081`)完成开发与预览,不要在生产目录跑 `pnpm dev`。
|
||||
2. `pnpm run build` + `pnpm test`。
|
||||
3. `curl -s http://127.0.0.1:8081/api/status` 确认生产仍在线。
|
||||
4. `./rsync_to_server.sh --dry-run` 预览 diff,确认后再执行正式发布。
|
||||
|
||||
## 事故恢复最小步骤
|
||||
|
||||
如果发现 `g2.tkmind.cn` 异常,先检查本机生产:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/Memind
|
||||
lsof -nP -iTCP:8081 -sTCP:LISTEN
|
||||
curl -s http://127.0.0.1:8081/api/status
|
||||
```
|
||||
|
||||
如果 `8081` 没有监听,再恢复生产 Portal:
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/Memind
|
||||
nohup /opt/homebrew/opt/node@24/bin/node server.mjs >> h5.log 2>&1 &
|
||||
echo $! > .h5.pid
|
||||
sleep 2
|
||||
curl -s http://127.0.0.1:8081/api/status
|
||||
```
|
||||
|
||||
恢复后再检查 Caddy:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8090/api/status
|
||||
```
|
||||
Reference in New Issue
Block a user