Compare commits
280 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84bc03c9ba | |||
| 1077dd9b97 | |||
| b6def0f25b | |||
| ecf553754c | |||
| 14a00774d9 | |||
| 08feae8bef | |||
| e2ad3bf62b | |||
| 69be1e5293 | |||
| 0c2cb9e283 | |||
| dd091e6e2c | |||
| 8755928405 | |||
| 3318c04490 | |||
| 53eb0014ba | |||
| 85061dfa9f | |||
| 1165ebf684 | |||
| cb7a446d3e | |||
| 5f8ef9ddb0 | |||
| 00a00a1f69 | |||
| bdeab23b83 | |||
| 614837e199 | |||
| d3239ff292 | |||
| 4781bbc156 | |||
| a29158d589 | |||
| e45c9300bf | |||
| bfb6356f7d | |||
| e4657c7e55 | |||
| 12268110f7 | |||
| 927e16d861 | |||
| b782561d08 | |||
| b5a88d9746 | |||
| cd81c4b2bb | |||
| 03c46b37c3 | |||
| 29d9a87d4f | |||
| eb8d760dd9 | |||
| 31fb0e02cf | |||
| 031c6e086a | |||
| fafc1fe7fd | |||
| 2a3579c73a | |||
| f36e8b9292 | |||
| 6bc5d128d2 | |||
| 5ed655b6c3 | |||
| f3a55dce29 | |||
| 8c72d0c950 | |||
| bef72b2768 | |||
| 3139395b58 | |||
| 7b09abeaa2 | |||
| 42baa3e468 | |||
| 8d6e237128 | |||
| 52c7082c70 | |||
| 1ac9ad0277 | |||
| 702871a98c | |||
| 553af7ca7a | |||
| c3c487a8fb | |||
| f543a91cc4 | |||
| cd2cbdbe78 | |||
| 145bf4eb01 | |||
| 9e4074474b | |||
| ebf06b376b | |||
| 996c83bbc8 | |||
| 72fc7cc618 | |||
| 20116b161f | |||
| 7005c2a834 | |||
| ec5b1a6e3f | |||
| 506a551438 | |||
| b258004dad | |||
| b084f6d23c | |||
| ab4dab6b38 | |||
| 49127d8675 | |||
| b4ae4a870c | |||
| c86a3fd15b | |||
| b38dd5961f | |||
| 0e00ed52cd | |||
| 85936fff05 | |||
| ca88716d68 | |||
| 32296cb422 | |||
| 0f48792133 | |||
| c2308a6ead | |||
| 39e4fffe91 | |||
| 97b648322a | |||
| 1b3e64703e | |||
| 0d6e4ead01 | |||
| f0acaec535 | |||
| aba72c6cf8 | |||
| 0a0c619aca | |||
| 22b6220fcc | |||
| a700bb3edb | |||
| 253ee870e7 | |||
| 66c530b1ed | |||
| e4d6267c96 | |||
| ddfa60607a | |||
| 2d777ef394 | |||
| bb7b82173d | |||
| b5781680a8 | |||
| 90c776065c | |||
| bea8eb7c1d | |||
| 87ff84535d | |||
| 9135332467 | |||
| 50929940cc | |||
| 30ef609eba | |||
| 40717d8fc7 | |||
| e94b3740b4 | |||
| f9eb3d1440 | |||
| 8e0ace1389 | |||
| f9a385f333 | |||
| 054379ac98 | |||
| 7a47dc6a98 | |||
| 201cc5fb7a | |||
| c27bf04a71 | |||
| af5396c127 | |||
| 79d310ba29 | |||
| c0f922b5f8 | |||
| 80b762053b | |||
| 212e163006 | |||
| f1fd4cc316 | |||
| b12c1b371c | |||
| 3654639bb3 | |||
| 6340b57fda | |||
| a2d69a0c36 | |||
| 0cfd2d3d2a | |||
| 21e03d86b5 | |||
| e24e6ca4a7 | |||
| a0af049fa7 | |||
| c7d8140a69 | |||
| 8a53dff856 | |||
| bb98734d9c | |||
| a402509d6e | |||
| e3ce4032c6 | |||
| 12f74eb44e | |||
| 05d29ce62d | |||
| 3ea973968e | |||
| 8e5094fcd9 | |||
| 41b7b5ffa3 | |||
| a13808b462 | |||
| b97c8e822e | |||
| e989526886 | |||
| 304a9eaea2 | |||
| ac20ac212e | |||
| 0271d83756 | |||
| e17dcaf4a8 | |||
| c9b7252832 | |||
| fa0024907b | |||
| 4420cca340 | |||
| 04e308e582 | |||
| 8260ff1e0e | |||
| 4fc59729ee | |||
| ec375b1402 | |||
| 164c6627a0 | |||
| c0ed894cff | |||
| 44d41d9851 | |||
| 73a90c80fd | |||
| 1297531b04 | |||
| de5fab370c | |||
| 75e5a56b86 | |||
| 62c1d72470 | |||
| 235ca0971e | |||
| 9aa5229af8 | |||
| 862824202f | |||
| b273bb7aa2 | |||
| 5a970e612e | |||
| 394d3640d1 | |||
| b7af5b00dc | |||
| d7cbf3cc96 | |||
| 65e8a3a5a8 | |||
| ee3dd3b23c | |||
| 6cb1a2475d | |||
| 6e4e0e2937 | |||
| 9c653e9b16 | |||
| ae6eaf8c3e | |||
| 722b18326f | |||
| b1b8d3afc6 | |||
| 71e536eceb | |||
| 2c91689692 | |||
| 06eb129a28 | |||
| 2f6a52151a | |||
| ea6c49f046 | |||
| 6e6d00568a | |||
| 0293abc11f | |||
| 7f0b216775 | |||
| 27960a3762 | |||
| 945b0a609b | |||
| b4a2b2bdaf | |||
| 07530a888c | |||
| ea4daa1298 | |||
| 58521df36a | |||
| 5d9f91ad92 | |||
| a2fc5c55ad | |||
| 27db709815 | |||
| e328dd541d | |||
| 7e3c018a55 | |||
| 6cd453284b | |||
| cd1ff6758d | |||
| f7c35f37e5 | |||
| 2b8dba1784 | |||
| 0203d81942 | |||
| 92b093199a | |||
| 14f46cf318 | |||
| 7906f3c36d | |||
| e93fb6a75f | |||
| 66ca0b3bec | |||
| 4e7ef3d853 | |||
| 98721371a4 | |||
| 6f89a9a236 | |||
| 742aee7148 | |||
| 8f620fa714 | |||
| dff0308ccb | |||
| 4fcb48395e | |||
| a40e340a41 | |||
| 18ea4f82fd | |||
| 08c5b22d4a | |||
| cc98cf02fa | |||
| 0187f26c5b | |||
| d67adc1df8 | |||
| e0e03e95b1 | |||
| 637b32515a | |||
| 2b344dfa60 | |||
| 12a1247c1f | |||
| 16828a58f7 | |||
| 3e313dc8a3 | |||
| 29f6c0095f | |||
| 9465803d95 | |||
| e80831ad4f | |||
| f8787bd330 | |||
| 2e9fcaf9ba | |||
| 66acbc902c | |||
| 615a1e9bf4 | |||
| 504513747e | |||
| 81c38226e5 | |||
| 82ac5fc744 | |||
| f31775b0c7 | |||
| fa2e385849 | |||
| 34ae9de100 | |||
| ea19ffb5fa | |||
| 1af680c6cb | |||
| 451918fbec | |||
| faac6d9772 | |||
| 4a9bc710f1 | |||
| 8264e71f9e | |||
| c924b2bb77 | |||
| 0420c42fe2 | |||
| fc5b50c936 | |||
| f4e8bb51a1 | |||
| 58d824f5e5 | |||
| 0a79a99df9 | |||
| 4bb4858e84 | |||
| def9b0b5eb | |||
| da239bdf09 | |||
| 3bb5d21425 | |||
| a2374c3cd6 | |||
| 1798c07d42 | |||
| 25f8223253 | |||
| f1220a7905 | |||
| 8627188ee7 | |||
| 8502775042 | |||
| 9b4a25799f | |||
| 9ed4fd48d7 | |||
| d51df2fb0a | |||
| e133f9a3a4 | |||
| daa5ab7199 | |||
| 02c651a3d0 | |||
| db0149b025 | |||
| a0b684dba7 | |||
| fe4172d07e | |||
| 41470bafcb | |||
| cb51b94f2c | |||
| 44e962e270 | |||
| 2f07f3733e | |||
| 70492d9eba | |||
| 2e5afe3bfd | |||
| c4de6716f1 | |||
| 229805a070 | |||
| b0f5d6a51c | |||
| ab0718938e | |||
| 6d99d762da | |||
| 03690ee354 | |||
| befeedfd37 | |||
| f818dd0307 | |||
| bb70634a96 | |||
| 79457230c1 | |||
| 8d2f55771c | |||
| d376161ab0 |
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: portal-release
|
||||
description: Package, deploy, verify, and roll back the Memind Portal runtime on 103 production. Use for any "打包/发布/上线/回滚/发布验证" request targeting 103.
|
||||
---
|
||||
|
||||
# Portal 生产发布 / 验证 / 回滚
|
||||
|
||||
103(`john@58.38.22.103`)是唯一生产 Portal 主机。这个 skill 是打包、发布、验证、回滚的**唯一入口**——不要现场发明新流程,不要直接 SSH 到 103 手改代码或手写文件到 `MindSpace/`。
|
||||
|
||||
## 0. 发布前置检查(必须)
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
**worktree 必须干净。** 如果这个仓库有并发运行的其他 agent/session(本仓库经常有),工作区里可能混着别人未提交的改动:
|
||||
|
||||
- 先辨认清楚哪些文件是"这次要发布的改动",哪些是别人正在写的无关改动
|
||||
- **不要**未经确认就自己 stash 别人的未提交内容——这是需要用户明确同意的动作,先问,拿到同意后再 `git stash push -u -m "<说明>" -- <具体文件...>`
|
||||
- 发布完成后必须 `git stash pop` 把对方的改动还回去,不要留着不还
|
||||
|
||||
确认当前分支和要发布的 commit 就是打算上线的那个(`git log --oneline -3`)。
|
||||
|
||||
## 1. 发布前测试(必须,对应 docs/发包必看.md #1.5)
|
||||
|
||||
```bash
|
||||
node --test db.test.mjs capabilities.test.mjs llm-providers.test.mjs wechat-mp.test.mjs
|
||||
```
|
||||
|
||||
全绿之后才能进入打包发布,不要跳过。
|
||||
|
||||
## 2. 打包 + 发布(唯一入口)
|
||||
|
||||
先 dry-run 确认构建产物正常(不会碰生产):
|
||||
|
||||
```bash
|
||||
bash scripts/release-portal-runtime-prod.sh --skip-tests --dry-run
|
||||
```
|
||||
|
||||
确认无误后正式发布(`--skip-tests` 是因为第 1 步已经手动跑过;这里不再重复跑脚本内置的窄范围测试):
|
||||
|
||||
```bash
|
||||
bash scripts/release-portal-runtime-prod.sh --skip-tests --yes
|
||||
```
|
||||
|
||||
这个脚本会自动完成:
|
||||
|
||||
1. 本机构建 `.runtime/portal`
|
||||
2. 上传到 103
|
||||
3. **103 备份当前 `/Users/john/Project/Memind` 全目录**(`full_backup`)
|
||||
4. **103 单独备份持久目录**:`.env`、`MindSpace/`、`data/`、`users/`、`.tailscale/`、`public/plaza-covers/`、`logs/`(`persist_backup`)
|
||||
5. 原子切换 live 目录(旧源码树移入 `archived_source`,不是删除)
|
||||
6. 更新 LaunchAgent,健康检查 `http://127.0.0.1:8081/api/status` 200
|
||||
7. 脚本内置 `trap rollback ERR`:如果切换过程中途失败,会自动尝试把旧目录移回来并重启旧服务——但**这只覆盖脚本执行期间的失败**,脚本成功退出之后发现问题需要走第 4 节手动回滚
|
||||
|
||||
**记录脚本输出里的这几行,回滚和报告都要用**:
|
||||
|
||||
```
|
||||
release_id=...
|
||||
archived_source=/Users/john/Project/archives/Memind-source-before-<release_id>
|
||||
full_backup=/Users/john/Project/backups/memind/memind-full-<release_id>-before.tar.gz
|
||||
persist_backup=/Users/john/Project/backups/memind/memind-persisted-<release_id>-before.tar.gz
|
||||
```
|
||||
|
||||
## 3. 发布后验证(必须,不能只看 /api/status)
|
||||
|
||||
### 3.1 健康检查 + auth/login(本机执行)
|
||||
|
||||
```bash
|
||||
curl -k -i https://m.tkmind.cn/auth/login \
|
||||
-H 'content-type: application/json' \
|
||||
--data '{"username":"john","password":"wrong-password"}' | head -20
|
||||
```
|
||||
|
||||
必须是 **401** + `用户名或密码错误`。如果是 `503` + `未配置用户数据库或访问密码`,说明用户系统 bootstrap 失败——不是简单健康检查能发现的问题,必须当场处理,不能算发布成功。
|
||||
|
||||
### 3.2 Portal + 四个 goosed 实例(SSH 103)
|
||||
|
||||
```bash
|
||||
ssh john@58.38.22.103 '
|
||||
printf "portal="; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081/api/status
|
||||
for p in 18006 18007 18008 18009; do
|
||||
printf "goosed_${p}="
|
||||
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1:${p}/status
|
||||
done
|
||||
pid=$(pgrep -f "node .*server.mjs" | head -1)
|
||||
echo "portal_pid=${pid}"
|
||||
ps eww -p "$pid" | tr " " "\n" | awk -F= "/^(DATABASE_URL|H5_PORT|TKMIND_API_TARGETS|GOOSED_MCP_NODE_PATH|GOOSED_MCP_SERVER_PATH)=/ {print \$1\"=SET\"}"
|
||||
'
|
||||
```
|
||||
|
||||
必须全部是 `200`,五个 env var 全部 `SET`。
|
||||
|
||||
### 3.3 日志关键词检查(SSH 103)
|
||||
|
||||
```bash
|
||||
ssh john@58.38.22.103 '
|
||||
tail -200 ~/Library/Logs/memind-portal.log |
|
||||
egrep "User auth bootstrap failed|ER_PARSE_ERROR|Cannot find package|Failed to add extension|No such file|Unknown extension|Provider not set|not configured" || echo "no error keywords found"
|
||||
'
|
||||
```
|
||||
|
||||
新启动之后不能再出现这些关键词。
|
||||
|
||||
### 3.4 真实业务路径验证(必须,测试账号 `john`)
|
||||
|
||||
只看健康接口不算数,必须走一次真实的"生成页面 → 发布 → 能访问"链路。**禁止直接 SSH 到 103 手写文件到 `MindSpace/` 目录充当验证**——那是绕过应用本身的路径,不代表真实用户会走的流程,而且是对生产文件系统的越权写入。只能通过应用自己的 API。
|
||||
|
||||
密码通过环境变量传入,**不要把明文密码写进这个文件或提交到仓库**(沿用 `scripts/fruit-theme-john-e2e.mjs` 里的既有约定:`JOHN_PASSWORD` / `H5_ACCESS_PASSWORD`,向用户当场要,或从 `.env` 之外的安全渠道取):
|
||||
|
||||
```bash
|
||||
CJ=/tmp/portal-release-verify-cookies.txt
|
||||
|
||||
# 1. 登录(JOHN_PASSWORD 从环境变量取,不要硬编码)
|
||||
curl -k -s -c "$CJ" -X POST https://m.tkmind.cn/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"username\":\"john\",\"password\":\"${JOHN_PASSWORD}\"}"
|
||||
# 必须 authenticated:true
|
||||
|
||||
# 2. 通过真实 API 创建一个页面(验证 mindspace pages 管线本身没坏)
|
||||
BODY=$(node -e "console.log(JSON.stringify({title:'发布验证页',summary:'release verify',content:'<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>发布验证页</title><meta name=\"mindspace-cover\" content=\'{\"tag\":\"报告\"}\'></head><body>ok</body></html>',content_format:'html',page_type:'html',category_code:'draft'}))")
|
||||
curl -k -s -X POST https://m.tkmind.cn/api/mindspace/v1/pages \
|
||||
-H "Content-Type: application/json" -b "$CJ" -c "$CJ" -d "$BODY"
|
||||
# 必须 201,hasThumbnail:true
|
||||
|
||||
# 3. 只读验证公网静态页服务没坏:用一个已知存在的已发布页面,不要新写文件
|
||||
curl -k -s -o /dev/null -w '%{http_code}\n' \
|
||||
"https://m.tkmind.cn/MindSpace/<已知已发布用户ID>/public/<已知文件名>.html"
|
||||
# 必须 200
|
||||
```
|
||||
|
||||
第 2 步创建的测试页面记录会留在生产库里(`draft`/`private`,风险很低)。**不要自己调用 DELETE 清理**——删除生产数据记录需要用户明确同意,验证完之后把 page id 报给用户,让用户决定是否清理。
|
||||
|
||||
## 4. 手动回滚(脚本成功退出之后才发现问题时用)
|
||||
|
||||
**优先方式:重新发布上一个已知good的 commit**,而不是手动倒腾备份目录——这样能完整复用第 2 节的持久化保留逻辑,不会把回滚期间用户新写的 `MindSpace`/`data`/`users` 数据搞丢:
|
||||
|
||||
```bash
|
||||
git log --oneline -5 # 找到上一个已验证通过的 commit
|
||||
git checkout <上一个好的 commit 或分支>
|
||||
bash scripts/release-portal-runtime-prod.sh --skip-tests --yes
|
||||
# 然后重新走第 3 节完整验证
|
||||
```
|
||||
|
||||
**紧急方式**(本地仓库回不去、必须直接用远端备份时):
|
||||
|
||||
```bash
|
||||
ssh john@58.38.22.103 '
|
||||
launchctl bootout gui/$(id -u)/cn.tkmind.memind-portal 2>/dev/null || true
|
||||
lsof -tiTCP:8081 -sTCP:LISTEN | xargs kill 2>/dev/null || true
|
||||
mv /Users/john/Project/Memind /Users/john/Project/Memind-bad-release-$(date +%s)
|
||||
mv <archived_source 路径> /Users/john/Project/Memind
|
||||
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/cn.tkmind.memind-portal.plist
|
||||
launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal
|
||||
'
|
||||
```
|
||||
|
||||
⚠️ `archived_source` 里的 `MindSpace`/`data`/`users` 是**发布那一刻**的快照。如果坏版本上线后已经有用户写入了新数据,直接整体换回去会丢失这段时间的新数据——先确认这一点,必要时把坏版本目录里当前的 `MindSpace`/`data`/`users` 复制出来,回滚后再合并回去,而不是整体覆盖。回滚后必须重新走第 3 节验证。
|
||||
|
||||
## 5. 发布完成后的最小回报格式(必须,不能只说"已发布成功")
|
||||
|
||||
- release id / git head / git branch
|
||||
- full backup 路径
|
||||
- persisted backup 路径
|
||||
- Portal 健康结果(200/非200)
|
||||
- `/auth/login` 结果(401 + 正确消息,还是别的)
|
||||
- 18006-18009 健康结果
|
||||
- 日志关键词检查结果
|
||||
- 第 3.4 节真实业务路径验证结果(含遗留的测试 page id,交给用户决定是否清理)
|
||||
- 是否发现并处理了问题;如果发布后发现回滚,说明触发原因
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: 标准化分支、测试、发布闸门(始终应用)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 标准化开发测试发布闸门
|
||||
|
||||
开始任何开发、修复、发布前,先读:
|
||||
|
||||
- `ENGINEERING_WORKFLOW_RULES.md`
|
||||
- `PRODUCTION_RELEASE_RULES.md`
|
||||
- `AGENTS.md`
|
||||
|
||||
必须遵守:
|
||||
|
||||
- 新建分支必须基于最新 `origin/main`,推荐执行 `bash scripts/new-branch.sh feature/xxx`
|
||||
- 分支代码必须先合并进 `main`,确认 `main` 正常后,才允许发布
|
||||
- 发布只能从完整 `main` 打整包,禁止从功能分支、单个 commit、单个修复或局部差异单包发布
|
||||
- 发布前必须确认 CI 已通过,且没有未合并的关键变更
|
||||
- 发布前必须重新检查当前分支是否落后 `origin/main`
|
||||
- 发布来源必须是干净、可追溯的 Git commit
|
||||
- 禁止从脏工作区、detached HEAD、落后主线的分支发布
|
||||
- 禁止直接 `rsync` 到 `103/105` 或 SSH 到生产手改源码
|
||||
|
||||
发布前必须执行:
|
||||
|
||||
```bash
|
||||
bash scripts/check-release-ready.sh
|
||||
```
|
||||
|
||||
如需临时绕过,只能在用户明确批准后使用项目文档中记录的显式环境变量。
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: MindSpace 公开页 edit_file 落盘与聊天 Finish 同步回归守卫(勿盲覆盖)
|
||||
globs: mindspace-public-finish-sync.mjs,chat-finish-sync.mjs,conversation-display.mjs,src/hooks/useTKMindChat.ts,src/utils/message.ts,server.mjs,chat-skills.mjs
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# MindSpace 发布 & 聊天 Finish — 回归守卫
|
||||
|
||||
已验证行为(2026-06-29)。删改下列逻辑前必须先读 `docs/regression-guards/mindspace-publish-and-chat-finish.md` 并跑 `npm run verify:mindspace-publish-guards`。
|
||||
|
||||
## 公开 HTML
|
||||
|
||||
- `edit_file` 的 `old_str`/`new_str` **必须**经 `mindspace-public-finish-sync.mjs` 落盘并**覆盖**已有 `public/*.html`
|
||||
- 不得回退为仅处理 `write_file` 全量 `content`
|
||||
|
||||
## 聊天 Finish
|
||||
|
||||
- **禁止** `syncSessionMessages` / `UpdateConversation` 用服务端 snapshot **盲覆盖**本地已展示消息
|
||||
- Finish 同步 **必须**使用 `mergeConversationSnapshot`(见 `chat-finish-sync.mjs`),并在服务端条数落后时重试
|
||||
- **禁止**无 hint 时命中 session snapshot 缓存(需 `hint_mc` 与 `hint_ua` 同时匹配)
|
||||
- 用户可见文案 **必须**优先 `metadata.displayText`;否则用 `deriveUserFacingText` 剥离 routing / skill / `[用户身份]` 等 agent-only 前缀
|
||||
|
||||
## 改完必跑
|
||||
|
||||
```bash
|
||||
npm run verify:mindspace-publish-guards
|
||||
```
|
||||
@@ -1,28 +1,119 @@
|
||||
# TKMind H5 环境变量(复制为 .env 后按需修改)
|
||||
#
|
||||
# 本地开发(默认只做本地,不自动发布生产):
|
||||
# 1. pnpm dev # 热更新 (http://127.0.0.1:5173)
|
||||
# 2. pnpm run test:local # 生产构建本地跑 (http://127.0.0.1:8081)
|
||||
# 3. pnpm run test:fruit-john-e2e # john 水果主题:预览图 + 可点击链接 + 我的空间
|
||||
# 4. node scripts/mindspace-rebuild-thumbnails.mjs # 批量重建预览图(可选)
|
||||
#
|
||||
# 发布生产:仅当明确要求「发布生产」时再执行:
|
||||
# pnpm run deploy:105 或 pnpm run deploy:prod 或 ./rsync_to_server.sh
|
||||
# 105 配置见 deploy/h5-105.env.example
|
||||
# 本地开发(127.0.0.1 + 端口,见 docs/local-dev.md)
|
||||
# pnpm dev
|
||||
# MindSpace: http://127.0.0.1:5173/?preview=mindspace
|
||||
#
|
||||
# 后端代理(server.mjs / vite dev proxy 共用)
|
||||
TKMIND_API_TARGET=https://127.0.0.1:18006
|
||||
TKMIND_SERVER__SECRET_KEY=local-dev-secret
|
||||
H5_PORT=8081
|
||||
# Agent 生成链接用(本地开发建议 8081;生产再改为 https://goo.tkmind.cn)
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:8081
|
||||
# MindSpace 与 Plaza 跨子域共享登录(如 g2.tkmind.cn + plaza.tkmind.cn)
|
||||
# H5_COOKIE_DOMAIN=.tkmind.cn
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
|
||||
# 生产 H5 public base 当前临时切到 https://mm.tkmind.cn。
|
||||
# 后续公网 H5 不再依赖 105 转发链路;m.tkmind.cn 仅作为 legacy/rollback 记录处理。
|
||||
# H5_PUBLIC_BASE_URL=https://mm.tkmind.cn
|
||||
# VITE_MINDSPACE_BASE=https://mm.tkmind.cn
|
||||
|
||||
# Memind 2.0 runtime control plane(生产启用 Redis;本地开发可留空回退到健康轮询)
|
||||
# MEMIND_RUNTIME_REDIS_URL=redis://127.0.0.1:6379/0
|
||||
# MEMIND_RUNTIME_REDIS_NAMESPACE=memind:runtime
|
||||
|
||||
# Code-tool agent run 灰度开关(默认关闭)。
|
||||
# 后端必须先开启 MEMIND_AGENT_CODE_RUNS_ENABLED,H5 构建开关才会生效。
|
||||
# 开启后,页面编辑子聊天会传 tool_mode=code;普通聊天仍需同时开启 AUTODETECT 才会按文本自动识别代码任务。
|
||||
# MEMIND_AGENT_CODE_RUNS_ENABLED=0
|
||||
# VITE_AGENT_CODE_RUNS_ENABLED=0
|
||||
# VITE_AGENT_CODE_RUNS_AUTODETECT=0
|
||||
|
||||
# Tool Gateway Queue v0(/agent/runs 后台任务队列,默认保守限流)。
|
||||
# MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1
|
||||
# MEMIND_AGENT_RUN_TIMEOUT_MS=900000
|
||||
# P6.1 外部 Tool Worker 接管入口。默认 Portal 内部 auto-dispatch 保持开启;
|
||||
# 只有外部 worker 验证就绪后,才把 Portal 设为 0 并独立运行 scripts/agent-run-worker.mjs。
|
||||
# MEMIND_AGENT_RUN_AUTODISPATCH=1
|
||||
# MEMIND_AGENT_RUN_WORKER_POLL_MS=1000
|
||||
# MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_MS=30000
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=90000
|
||||
# MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=0
|
||||
|
||||
# Runtime SLO 日报(默认每日 23:55 写 reports/runtime-slo)。
|
||||
# MEMIND_RUNTIME_REPORT_DIR=/Users/john/Project/Memind/reports/runtime-slo
|
||||
# MEMIND_RUNTIME_SLO_HOUR=23
|
||||
# MEMIND_RUNTIME_SLO_MINUTE=55
|
||||
|
||||
# H5 Session Broker(docs/h5-session-architecture-20260706.md Patch 1+)
|
||||
# 本地开发:Patch 2 接入后可设 1 验证 broker 路径;默认 0 保持 userAuth 直调。
|
||||
# 103 生产:仅在灰度窗口于服务器 /Users/john/Project/Memind/.env 设置,勿把生产值写入 Git。
|
||||
# MEMIND_SESSION_BROKER_ENABLED=0
|
||||
|
||||
# --- 本地全量联调示例(复制到 .env,勿提交 Git)---
|
||||
# MEMIND_SESSION_BROKER_ENABLED=1
|
||||
# MEMIND_ROUTER_NORMALIZED_DECISION=1
|
||||
# MEMIND_SSE_EVENT_TAXONOMY=1
|
||||
# MEMIND_RUN_STREAM_REPLAY=1
|
||||
# MEMIND_SESSION_STREAM_REPLAY=1
|
||||
# MEMIND_SESSION_BROKER_METRICS=1
|
||||
# MEMIND_H5_HTML_FINISH_GUARD=1
|
||||
# 启动后 Portal 日志应出现:[Portal] H5 session flags: ...
|
||||
# 103 生产 goosed 多实例示例(仅服务器 .env,见 docs/103-runtime-topology.md):
|
||||
# TKMIND_API_TARGET=https://127.0.0.1:18006
|
||||
# TKMIND_API_TARGET_1=https://127.0.0.1:18007
|
||||
|
||||
# H5 Router normalized decision(docs/h5-session-architecture-20260706.md Patch 3)
|
||||
# 默认 0/off:gateway 仍读 legacy route
|
||||
# shadow:只打 [router-shadow] 日志,行为不变
|
||||
# 1/on:gateway 优先读 classification.decision.route / session_hint
|
||||
# MEMIND_ROUTER_NORMALIZED_DECISION=0
|
||||
|
||||
# H5 Session stream replay(docs/h5-session-architecture-20260706.md Patch 4c)
|
||||
# 默认 0:session SSE 仍纯透传 goosed;设 1 时 Portal 持久化 session 事件并支持 Last-Event-ID 重连补发。
|
||||
# MEMIND_SESSION_STREAM_REPLAY=0
|
||||
|
||||
# H5 Session Broker metrics(docs/h5-session-architecture-20260706.md §13.4)
|
||||
# 默认 0:不打 structured log;设 1 时输出 [session_broker.*] 行
|
||||
# MEMIND_SESSION_BROKER_METRICS=0
|
||||
|
||||
# H5 SSE event taxonomy(docs/h5-session-architecture-20260706.md Patch 4)
|
||||
# 默认 0:不改变 SSE wire format;设 1 时在 session/run SSE payload 附加 taxonomy 字段(shadow)。
|
||||
# MEMIND_SSE_EVENT_TAXONOMY=0
|
||||
|
||||
# H5 goosed proxy boundary(docs/h5-session-architecture-20260706.md Patch 5)
|
||||
# CI/发版前跑:npm run verify:goosed-proxy-boundary
|
||||
# 新增 goosed 调用须走 tkmind-proxy;MindSpace 例外见 goosed-proxy-boundary.mjs allowlist
|
||||
|
||||
# H5 public HTML 假交付 guard(docs/pending-fixes/h5-public-html-fake-delivery-guard-20260706.md)
|
||||
# 默认 0:仅 warn;设 1 时 Finish 后检测缺失 public/*.html / assets/* 并自动 session repair(每 session 1 次)
|
||||
# MEMIND_H5_HTML_FINISH_GUARD=0
|
||||
|
||||
# H5 run SSE replay(Patch 4b,仅本地/测试分支;默认关,未合并 main / 未上 103)
|
||||
# 设 1 时 GET /agent/runs/:id/events 使用 h5_agent_run_events + Last-Event-ID 重连回放
|
||||
# MEMIND_RUN_STREAM_REPLAY=0
|
||||
|
||||
# ===== memind_adm(独立后台服务)=====
|
||||
# 平台超管 /admin-api/* 与广场运营 /api/ops/v1/* 已从公网服务拆出,独立进程运行。
|
||||
# 登录仍在主域完成;后台凭 H5_COOKIE_DOMAIN=.tkmind.cn 共享的会话 Cookie 鉴权(生产务必设置)。
|
||||
ADMIN_PORT=8082
|
||||
ADMIN_HOST=127.0.0.1
|
||||
ADMIN_PUBLIC_HOST=gadm.tkmind.cn
|
||||
|
||||
# 本进程挂载哪些控制台(可拆分架构):admin=平台超管, ops=广场运营。
|
||||
# 默认两者合一;将来拆成两个进程时,A 实例设 admin、B 实例设 ops,路由代码无需改动。
|
||||
# ADMIN_CONSOLES=admin,ops
|
||||
|
||||
# 每控制台独立网络护栏(逗号分隔;留空=放开。支持精确 IP 与 CIDR)。
|
||||
# 超管面建议收得最紧(仅内网/VPN),广场运营可放宽给审核团队网段。
|
||||
# ADMIN_API_ALLOWED_HOSTS=gadm.tkmind.cn
|
||||
# ADMIN_API_IP_ALLOWLIST=10.0.0.0/8,127.0.0.1
|
||||
# OPS_API_ALLOWED_HOSTS=gadm.tkmind.cn
|
||||
# OPS_API_IP_ALLOWLIST=
|
||||
|
||||
# 多用户系统(MySQL 配置后启用;未配置时回退到 H5_ACCESS_PASSWORD)
|
||||
DATABASE_URL=mysql://boot:password@localhost:3306/tkmind
|
||||
# 或分别设置 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE
|
||||
H5_USERS_ROOT=/root/tkmind_go/users
|
||||
# 新用户赠送余额(人民币分,500 = ¥5.00;勿与美元 cents 混淆)
|
||||
H5_SIGNUP_BALANCE_CENTS=500
|
||||
H5_ADMIN_USERNAME=admin
|
||||
H5_ADMIN_PASSWORD=change-me-admin
|
||||
@@ -31,28 +122,30 @@ H5_ADMIN_PASSWORD=change-me-admin
|
||||
# LLM Key 数据库加密(可选;默认派生自 TKMIND_SERVER__SECRET_KEY)
|
||||
# H5_SETTINGS_ENCRYPTION_KEY=change-me-encryption-key
|
||||
# 启动时自动导入 Relay(若不存在同名配置)
|
||||
# H5_RELAY_BOOTSTRAP_NAME=Relay Buyer Ollama
|
||||
# H5_RELAY_BOOTSTRAP_NAME=Local Goose Relay DeepSeek
|
||||
# H5_RELAY_BOOTSTRAP_URL=http://127.0.0.1:18300/relay/buyer/v1/chat/completions
|
||||
# H5_RELAY_BOOTSTRAP_API_KEY=your-bearer-token
|
||||
# H5_RELAY_BOOTSTRAP_MODEL=qwen2.5:3b
|
||||
# H5_RELAY_BOOTSTRAP_MODELS=qwen2.5:3b,llama3.2:1b
|
||||
# H5_RELAY_BOOTSTRAP_PROVIDER=ollama
|
||||
# H5_RELAY_BOOTSTRAP_MODEL=deepseek-chat
|
||||
# H5_RELAY_BOOTSTRAP_MODELS=deepseek-chat,deepseek-reasoner
|
||||
# H5_RELAY_BOOTSTRAP_PROVIDER=deepseek
|
||||
# 一键对齐本地 goosed + Goose Desktop relay:node scripts/fix-local-goose-relay.mjs
|
||||
|
||||
# 本地 Ollama fallback:DeepSeek 额度不足,或 andu Relay 500(agent 请求体含 tools 超过 ~8.5KB)时自动切换
|
||||
# Local Ollama fallback — creditsExhausted or relay 500 (nginx body limit with tool payloads).
|
||||
# H5_LOCAL_LLM_FALLBACK=0
|
||||
# H5_LOCAL_LLM_URL=http://127.0.0.1:11434/v1/chat/completions
|
||||
# H5_LOCAL_LLM_MODEL=qwen2.5-coder:7b
|
||||
# H5_LOCAL_LLM_MODEL=qwen2.5:3b
|
||||
# H5_LOCAL_LLM_API_KEY=ollama
|
||||
# H5_LOCAL_LLM_NAME=Local Ollama 7B
|
||||
H5_ACCESS_PASSWORD=change-me
|
||||
|
||||
# 计费(Phase 2)
|
||||
# H5_USE_BACKEND_COST=1 # 优先用后端 accumulatedCost(USD)计费
|
||||
# H5_USD_CNY_RATE=7.2
|
||||
# H5_BILL_INPUT_CENTS_PER_1K=2
|
||||
# H5_BILL_OUTPUT_CENTS_PER_1K=6
|
||||
# 计费(Phase 2,金额单位均为人民币分)
|
||||
# 默认按 Token 单价扣费(人民币结算);勿开启美元成本换算除非明确需要
|
||||
# H5_USE_BACKEND_COST=0
|
||||
# H5_BILL_INPUT_CENTS_PER_1K=2 # 输入 Token:2 分/1k(¥0.02/1k)
|
||||
# H5_BILL_OUTPUT_CENTS_PER_1K=6 # 输出 Token:6 分/1k(¥0.06/1k)
|
||||
# H5_MIN_BILL_CENTS=1
|
||||
# 仅调试:H5_USE_BACKEND_COST=1 + H5_USD_CNY_RATE=7.2(上游 USD × 汇率 → 人民币分)
|
||||
|
||||
# 用户自助充值(微信支付)
|
||||
# H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000
|
||||
@@ -81,6 +174,10 @@ H5_ACCESS_PASSWORD=change-me
|
||||
# H5_WECHAT_PLATFORM_CERT="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
#
|
||||
# H5_WECHAT_NOTIFY_URL=https://goo.tkmind.cn/webhooks/wechat-pay/notify
|
||||
# 微信内 JSAPI 支付(服务号网页):
|
||||
# - 服务号「设置 → 功能设置 → JS接口安全域名」:mm.tkmind.cn
|
||||
# - 商户平台「产品中心 → 开发配置 → 支付授权目录」:https://mm.tkmind.cn/
|
||||
# - 用户须先用同一服务号微信 OAuth 登录(获取 openid)后再充值
|
||||
# 本地调试验签可临时开启(生产务必关闭):
|
||||
# H5_WECHAT_SKIP_NOTIFY_VERIFY=0
|
||||
|
||||
@@ -118,24 +215,20 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
|
||||
# 历史列表每次展示/加载条数(默认 25,与 desktop 一致)
|
||||
# VITE_SESSION_PAGE_SIZE=25
|
||||
|
||||
# Plaza 发现广场(本机为主,105 已停用)
|
||||
# PLAZA_AUTO_APPROVE=true
|
||||
# 文档:docs/plaza-local.md
|
||||
# 首次:sudo pnpm setup:plaza-dns # 仅纯本地 HTTPS 时需要;走 Tunnel 请用 setup:plaza-dns:remove
|
||||
# 日常:pnpm dev:plaza | 浏览器:pnpm open:plaza | 诊断:pnpm check:plaza
|
||||
# 公网 plaza.tkmind.cn 经 Cloudflare Tunnel → 本机 :3001(需 dev:plaza 常驻)
|
||||
# PLAZA_LOCAL_HOST=plaza.tkmind.cn
|
||||
# Plaza 发现广场(独立域名 plaza.tkmind.cn;Portal 前端链接见 VITE_PLAZA_BASE)
|
||||
# PLAZA_PUBLIC_BASE=https://plaza.tkmind.cn
|
||||
# VITE_PLAZA_BASE=https://plaza.tkmind.cn
|
||||
# CLOUDFLARE_TUNNEL_CONFIG=/Users/john/Project/ollama/cloudflare/config.yml
|
||||
# CLOUDFLARE_TUNNEL_NAME=ollama-tkmind
|
||||
# 本地 dev 脚本会自动注入 VITE_PLAZA_BASE=http://127.0.0.1:3001;勿把该值留进生产构建用的 .env
|
||||
# VITE_PLAZA_BASE=http://127.0.0.1:3001
|
||||
# 文档:docs/plaza-local.md | 本地:http://127.0.0.1:3001/plaza | 生产:https://plaza.tkmind.cn/plaza
|
||||
|
||||
# MindSpace(默认启用;子功能需按需开启)
|
||||
# MINDSPACE_ENABLED=false
|
||||
# MINDSPACE_UPLOAD_ENABLED=false
|
||||
# MINDSPACE_PUBLISH_ENABLED=true
|
||||
# MINDSPACE_PRIVATE_ENABLED=false
|
||||
# MINDSPACE_MAX_FILE_BYTES=5242880
|
||||
# Agent 后台任务(创建/执行空间内 Agent 任务,默认关闭)
|
||||
# MINDSPACE_AGENT_JOBS_ENABLED=true
|
||||
# MINDSPACE_INTERNAL_AGENT_SECRET=local-dev-secret
|
||||
# MINDSPACE_FREE_PUBLIC_PAGE_LIMIT=10
|
||||
# MINDSPACE_STORAGE_ROOT=/path/to/mindspace-storage
|
||||
|
||||
@@ -2,10 +2,12 @@ node_modules/
|
||||
ops/node_modules/
|
||||
dist/
|
||||
ops/dist/
|
||||
.runtime/
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
scripts/g2-aliyun-cert.env
|
||||
|
||||
*.log
|
||||
*.pid
|
||||
@@ -18,3 +20,6 @@ users/
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
.aider*
|
||||
.env.experience-test
|
||||
*.bak
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
Portal runtime artifact
|
||||
|
||||
This directory is meant to run on production without the source tree.
|
||||
Required persisted items to inherit on the host:
|
||||
.env
|
||||
MindSpace/
|
||||
data/
|
||||
users/
|
||||
.tailscale/
|
||||
public/plaza-covers/
|
||||
logs/
|
||||
|
||||
Bundled alongside server.mjs (required for sandbox-fs MCP):
|
||||
mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)
|
||||
|
||||
Post-deploy validation (scripts/check-mindspace-public-links.mjs):
|
||||
Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.
|
||||
|
||||
Runtime install mode: bundle-node-modules
|
||||
Bundle target: node24
|
||||
|
||||
Platform agent skills (synced into user workspaces):
|
||||
skills/
|
||||
Key runtime differences must stay in .env, not in the artifact:
|
||||
DATABASE_URL / MYSQL_*
|
||||
H5_PUBLIC_BASE_URL
|
||||
TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1
|
||||
H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT
|
||||
|
||||
Deployment and operations transport:
|
||||
H5 public domain: mm.tkmind.cn
|
||||
Current public path: mm.tkmind.cn -> local nginx -> Portal :8081
|
||||
Legacy rollback-only path: m.tkmind.cn -> 105 nginx -> reverse SSH tunnel -> Portal :8081
|
||||
Future H5 traffic must not depend on 105 forwarding unless explicitly rolling back.
|
||||
|
||||
Streaming runtime operations:
|
||||
node scripts/check-stream-runtime.mjs
|
||||
node scripts/runtime-worker-drain.mjs status
|
||||
node scripts/runtime-worker-drain.mjs reconcile
|
||||
node scripts/runtime-worker-drain.mjs reconcile --apply
|
||||
node scripts/runtime-worker-metrics.mjs status
|
||||
node scripts/runtime-worker-metrics.mjs sample
|
||||
bash scripts/install-runtime-metrics-agent.sh
|
||||
node scripts/runtime-worker-heartbeat.mjs once
|
||||
node scripts/runtime-worker-heartbeat.mjs serve
|
||||
bash scripts/install-runtime-heartbeat-agent.sh
|
||||
node scripts/runtime-slo-report.mjs
|
||||
node scripts/runtime-slo-report.mjs --write-report
|
||||
node scripts/runtime-slo-report.mjs --write-report --prune --retention-days 30
|
||||
bash scripts/install-runtime-slo-report-agent.sh
|
||||
bash scripts/install-runtime-slo-soak-agent.sh
|
||||
node scripts/runtime-worker-drain.mjs drain goosed-3
|
||||
node scripts/runtime-worker-drain.mjs undrain goosed-3
|
||||
node scripts/check-tool-runtime.mjs
|
||||
node scripts/check-agent-code-run-entry.mjs
|
||||
curl -sk https://mm.tkmind.cn/api/runtime/status # includes toolRuntime.queue
|
||||
node scripts/agent-run-worker.mjs --status
|
||||
node scripts/agent-run-worker.mjs --once
|
||||
bash scripts/install-agent-run-worker-agent.sh # installs disabled by default
|
||||
node scripts/check-agent-run-worker.mjs # read-only LaunchAgent/queue check
|
||||
node scripts/agent-run-guard.mjs # dry-run auto-pause guard check
|
||||
node scripts/agent-run-guard.mjs --apply # stop worker and disable code-run gate when thresholds trip
|
||||
bash scripts/install-agent-run-guard-agent.sh # installs guard LaunchAgent
|
||||
@@ -0,0 +1,151 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const SKIP_REFERENCE = /^(?:https?:|mailto:|javascript:|data:|tel:|#|\/\/)/i;
|
||||
const DOWNLOAD_EXTENSIONS = /\.(?:docx|doc|pdf|xlsx|xls|pptx|ppt|zip|rar|7z|csv)$/i;
|
||||
|
||||
export function isFilesystemPublicReference(raw) {
|
||||
const value = String(raw ?? '').trim();
|
||||
if (!value) return false;
|
||||
if (SKIP_REFERENCE.test(value)) return false;
|
||||
// /api/... and /MindSpace/... are server routes, not on-disk siblings of public HTML.
|
||||
if (value.startsWith('/')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @deprecated use isFilesystemPublicReference */
|
||||
export function isRelativePublicReference(raw) {
|
||||
return isFilesystemPublicReference(raw);
|
||||
}
|
||||
|
||||
export function isDownloadLikeReference(raw, source = '') {
|
||||
if (!isFilesystemPublicReference(raw)) return false;
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (DOWNLOAD_EXTENSIONS.test(ref)) return true;
|
||||
return /\bdownload\b/i.test(String(source ?? ''));
|
||||
}
|
||||
|
||||
export function normalizeReferencePath(raw) {
|
||||
const trimmed = String(raw ?? '')
|
||||
.trim()
|
||||
.split('#')[0]
|
||||
.split('?')[0]
|
||||
.trim();
|
||||
if (!trimmed) return '';
|
||||
try {
|
||||
return decodeURIComponent(trimmed);
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveReferencePath(htmlDir, raw) {
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (!ref) return null;
|
||||
return path.resolve(htmlDir, ref);
|
||||
}
|
||||
|
||||
function parseMindspaceCoverPaths(contentAttr) {
|
||||
const paths = [];
|
||||
if (!contentAttr) return paths;
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(contentAttr);
|
||||
} catch {
|
||||
for (const key of ['cover', 'image']) {
|
||||
const match = contentAttr.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
|
||||
if (match?.[1]) paths.push(match[1]);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
for (const key of ['cover', 'image']) {
|
||||
if (parsed?.[key]) paths.push(String(parsed[key]));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function collectPublicHtmlReferences(html, htmlDir, { downloadsOnly = false } = {}) {
|
||||
const refs = [];
|
||||
const seen = new Set();
|
||||
|
||||
const add = (raw, source) => {
|
||||
if (downloadsOnly && !isDownloadLikeReference(raw, source)) return;
|
||||
if (!downloadsOnly && !isFilesystemPublicReference(raw)) return;
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (!ref) return;
|
||||
const key = `${source}\0${ref}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
refs.push({
|
||||
ref,
|
||||
source,
|
||||
resolvedPath: resolveReferencePath(htmlDir, ref),
|
||||
});
|
||||
};
|
||||
|
||||
for (const match of String(html ?? '').matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/gi)) {
|
||||
add(match[1], match[0]);
|
||||
}
|
||||
|
||||
if (!downloadsOnly) {
|
||||
for (const match of String(html ?? '').matchAll(
|
||||
/<meta\b[^>]*\bname\s*=\s*["']mindspace-cover["'][^>]*>/gi,
|
||||
)) {
|
||||
const tag = match[0];
|
||||
const contentMatch =
|
||||
tag.match(/\bcontent\s*=\s*"([^"]*)"/i) ?? tag.match(/\bcontent\s*=\s*'([^']*)'/i);
|
||||
for (const coverPath of parseMindspaceCoverPaths(contentMatch?.[1])) {
|
||||
add(coverPath, 'mindspace-cover');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
export function findMissingPublicHtmlReferences(htmlPath, html = null, options = {}) {
|
||||
const resolvedHtmlPath = path.resolve(htmlPath);
|
||||
const content = html ?? fs.readFileSync(resolvedHtmlPath, 'utf8');
|
||||
const htmlDir = path.dirname(resolvedHtmlPath);
|
||||
const missing = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const item of collectPublicHtmlReferences(content, htmlDir, options)) {
|
||||
const key = item.ref;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
if (!item.resolvedPath || !fs.existsSync(item.resolvedPath)) {
|
||||
missing.push({ htmlPath: resolvedHtmlPath, ...item });
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
export function listPublicHtmlFiles(publishRoot, { userId = null } = {}) {
|
||||
const root = path.resolve(publishRoot);
|
||||
if (!fs.existsSync(root)) return [];
|
||||
|
||||
const userDirs = userId
|
||||
? [path.join(root, userId)]
|
||||
: fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path.join(root, d.name));
|
||||
|
||||
const files = [];
|
||||
for (const userDir of userDirs) {
|
||||
const publicDir = path.join(userDir, 'public');
|
||||
if (!fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) continue;
|
||||
for (const name of fs.readdirSync(publicDir)) {
|
||||
if (!name.toLowerCase().endsWith('.html')) continue;
|
||||
files.push(path.join(publicDir, name));
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
export function scanPublicHtmlLinks(publishRoot, options = {}) {
|
||||
const issues = [];
|
||||
for (const htmlPath of listPublicHtmlFiles(publishRoot, options)) {
|
||||
issues.push(...findMissingPublicHtmlReferences(htmlPath, null, options));
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "tkmind-h5-portal-runtime",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"debug": "^4.4.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"jsonrepair": "^3.14.0",
|
||||
"mysql2": "^3.22.5",
|
||||
"playwright": "^1.61.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^4.7.1",
|
||||
"sharp": "^0.35.2",
|
||||
"undici": "^6.26.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
nXjhx0ErC6MhQ0SZ
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,50 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>微信分享预览 · 连云港三天两夜攻略 🌊</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; padding: 24px; background: #ececec; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, sans-serif; color: #111; }
|
||||
.wrap { max-width: 420px; margin: 0 auto; }
|
||||
h1 { margin: 0 0 8px; font-size: 18px; }
|
||||
.hint, .note { margin: 0 0 16px; color: #666; font-size: 13px; line-height: 1.6; }
|
||||
.card { display: grid; grid-template-columns: 1fr 72px; gap: 12px; padding: 12px; border-radius: 8px; background: #fff; box-shadow: 0 1px 0 rgba(0,0,0,.06); }
|
||||
.card-main { min-width: 0; }
|
||||
.card-title { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-desc { margin: 6px 0 0; color: #888; font-size: 13px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; color: #999; font-size: 12px; }
|
||||
.card-foot img { width: 16px; height: 16px; border-radius: 999px; object-fit: cover; }
|
||||
.card-thumb { width: 72px; height: 72px; border-radius: 4px; object-fit: cover; background: #f3f3f3; }
|
||||
.card-thumb-empty { display: grid; place-items: center; color: #bbb; font-size: 12px; }
|
||||
.meta { margin-top: 18px; padding: 14px; border-radius: 10px; background: rgba(255,255,255,.72); font-size: 12px; line-height: 1.7; word-break: break-all; }
|
||||
code { background: rgba(0,0,0,.06); padding: 1px 4px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>微信链接卡片预览</h1>
|
||||
<p class="hint">本地模拟的是「粘贴链接后看到的卡片」,不是 JS-SDK 内部分享弹层。源链接:<code>https://m.tkmind.cn/MindSpace/john/public/lianyungang-guide.html</code></p>
|
||||
<p class="note">修复后:标题 + 描述 + 缩略图 + 底部「TKMind 智趣」小图标应同时出现。</p>
|
||||
<article class="card" aria-label="微信分享卡片预览">
|
||||
<div class="card-main">
|
||||
<h2 class="card-title">连云港三天两夜攻略 🌊</h2>
|
||||
<p class="card-desc">三天两夜·沙滩酒店·必吃美食</p>
|
||||
<div class="card-foot">
|
||||
<img src="http://127.0.0.1:8081/brand/tkmind-icon.png" alt="" onerror="this.style.display='none'">
|
||||
<span>TKMind 智趣</span>
|
||||
</div>
|
||||
</div>
|
||||
<img class="card-thumb" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80" alt="">
|
||||
</article>
|
||||
<div class="meta">
|
||||
<div><strong>og:title</strong> 连云港三天两夜攻略 🌊</div>
|
||||
<div><strong>og:description</strong> 三天两夜·沙滩酒店·必吃美食</div>
|
||||
<div><strong>og:site_name</strong> TKMind 智趣</div>
|
||||
<div><strong>og:image</strong> https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80</div>
|
||||
<div><strong>icon</strong> /brand/tkmind-icon.png</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>微信分享预览工具</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; padding: 24px; background: #f3f3f3; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, sans-serif; color: #111; }
|
||||
.panel { max-width: 560px; margin: 0 auto; padding: 20px; border-radius: 16px; background: #fff; box-shadow: 0 8px 30px rgba(0,0,0,.06); }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; }
|
||||
p { margin: 0 0 16px; color: #666; line-height: 1.6; font-size: 14px; }
|
||||
label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: 600; }
|
||||
input { width: 100%; padding: 12px 14px; border: 1px solid #ddd; border-radius: 10px; font: inherit; }
|
||||
button { margin-top: 14px; width: 100%; padding: 12px 14px; border: 0; border-radius: 10px; background: #2f6f57; color: #fff; font: inherit; font-weight: 700; cursor: pointer; }
|
||||
code { background: rgba(0,0,0,.06); padding: 1px 4px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="panel">
|
||||
<h1>微信分享预览</h1>
|
||||
<p>输入已发布页面路径,本地模拟微信「粘贴链接后」看到的卡片。开发环境走 Portal API:<code>/dev/wechat-share-preview</code></p>
|
||||
<form id="preview-form">
|
||||
<label for="url">页面 URL 或路径</label>
|
||||
<input id="url" name="url" placeholder="/MindSpace/john/public/demo.html" required>
|
||||
<button type="submit">生成预览</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('preview-form').addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const value = document.getElementById('url').value.trim();
|
||||
if (!value) return;
|
||||
const target = value.startsWith('http')
|
||||
? `/dev/wechat-share-preview?url=${encodeURIComponent(value)}`
|
||||
: `/dev/wechat-share-preview?url=${encodeURIComponent(value.startsWith('/') ? value : `/${value}`)}`;
|
||||
window.location.href = target;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="John 的专属欢迎页面 - TKMind 为您呈现">
|
||||
<meta name="mindspace-cover" content='{"tag":"报告","emoji":"👋","accent":"#f093fb","accent2":"#24243e","subtitle":"Hello John","cover":"https://images.unsplash.com/photo-1579547945413-497e1b99dac0?w=1200&q=80"}'>
|
||||
<title>Hello, John!</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 动态粒子背景 */
|
||||
.bg-particles {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-particles span {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
animation: float 20s infinite;
|
||||
}
|
||||
|
||||
.bg-particles span:nth-child(1) { top: 10%; left: 10%; animation-duration: 18s; animation-delay: 0s; }
|
||||
.bg-particles span:nth-child(2) { top: 30%; left: 80%; width: 10px; height: 10px; animation-duration: 22s; animation-delay: 2s; }
|
||||
.bg-particles span:nth-child(3) { top: 70%; left: 20%; animation-duration: 16s; animation-delay: 4s; }
|
||||
.bg-particles span:nth-child(4) { top: 85%; left: 85%; width: 8px; height: 8px; animation-duration: 24s; animation-delay: 1s; }
|
||||
.bg-particles span:nth-child(5) { top: 50%; left: 50%; width: 4px; height: 4px; animation-duration: 20s; animation-delay: 3s; }
|
||||
.bg-particles span:nth-child(6) { top: 15%; left: 60%; animation-duration: 19s; animation-delay: 5s; }
|
||||
.bg-particles span:nth-child(7) { top: 60%; left: 5%; width: 7px; height: 7px; animation-duration: 21s; animation-delay: 0.5s; }
|
||||
.bg-particles span:nth-child(8) { top: 40%; left: 70%; width: 5px; height: 5px; animation-duration: 17s; animation-delay: 2.5s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 60px 80px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
animation: fadeIn 1.2s ease-out;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #f093fb, #f5576c, #4facfe);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 16px;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
animation: wave 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 36px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.03); }
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(15deg); }
|
||||
75% { transform: rotate(-15deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 40px 32px;
|
||||
margin: 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
.emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-particles">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="emoji">👋</span>
|
||||
<h1>Hello!</h1>
|
||||
<p>Welcome, John</p>
|
||||
<div class="footer">powered by TKMind</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 613 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 613 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 166 KiB |