Compare commits
392 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5d109d585 | |||
| 331a863288 | |||
| 486e25f86b | |||
| f4bc716789 | |||
| 3cafadba5a | |||
| 122e478894 | |||
| 7b0f269180 | |||
| c7e4a063bf | |||
| fe5d32b451 | |||
| 30fcbaf613 | |||
| e0d4868908 | |||
| e7d5c09e56 | |||
| 475328830a | |||
| 5674d53a64 | |||
| 6250b5989c | |||
| ddb3a330f4 | |||
| 8ac159a5ed | |||
| f6dcf5b14a | |||
| f2d0c99f6c | |||
| 77f1ea8350 | |||
| 4e6abacce1 | |||
| 1f7b288a21 | |||
| 320945a485 | |||
| 6e460336f7 | |||
| c1012da120 | |||
| 933466c8ab | |||
| b3337c9643 | |||
| 5f1c4dcaca | |||
| 07f1ad0a6e | |||
| 3695fecbb6 | |||
| 72f1266078 | |||
| dd0014db83 | |||
| e2d850c2e9 | |||
| 3eee44ef5b | |||
| 87b03f7848 | |||
| a07cf07861 | |||
| 72fd88219c | |||
| 192548b0b9 | |||
| 27b11894f2 | |||
| ae9090948a | |||
| 6ef3ea89c6 | |||
| a6620fb719 | |||
| b33c943b69 | |||
| 7f7aced751 | |||
| a867592367 | |||
| e90a2da6cb | |||
| 804f13bc04 | |||
| 249b0697cb | |||
| cdeb24ba92 | |||
| fb055c6721 | |||
| da422a82e6 | |||
| 7005b501af | |||
| 502334777e | |||
| 16fd99e8ba | |||
| 2f7bc3b6e4 | |||
| ca4805533b | |||
| cef3750ac6 | |||
| 67d529c81e | |||
| f9561cd91d | |||
| 73b308af0a | |||
| f437a37660 | |||
| 5704cb11b0 | |||
| b86ce98d6f | |||
| 294e331a03 | |||
| ee3e405fc7 | |||
| 048f25f580 | |||
| 8928827291 | |||
| 4e36b77237 | |||
| dc7b440bfb | |||
| fdc2712084 | |||
| 680d28ac4b | |||
| f4d9897072 | |||
| 7dd6b060f7 | |||
| a3f669d4e7 | |||
| 03a79322e4 | |||
| 97d0e8d970 | |||
| 57a60d41d7 | |||
| 3163445a9d | |||
| c842119929 | |||
| 04ef1c9105 | |||
| fe6d631ba1 | |||
| b17b2f18cb | |||
| 0fd34a5644 | |||
| 9e5a59b7d9 | |||
| 32fb2cdeaf | |||
| e3063ea806 | |||
| 7f8d692d16 | |||
| 880cd59ca5 | |||
| adc13a167e | |||
| aede1e6fcb | |||
| eea14c1855 | |||
| 8d629e7a4e | |||
| 6b0c633a75 | |||
| 8d01553469 | |||
| 1c8e0db256 | |||
| ea25058db8 | |||
| 31742f914e | |||
| fd364dbbeb | |||
| ea4c6b707c | |||
| 5da4dac16a | |||
| 81967eaf8c | |||
| 1e4f432754 | |||
| ac520d8ae5 | |||
| b4171ca268 | |||
| ec8d0464a3 | |||
| 7edafefc56 | |||
| 0d6258d00a | |||
| cf04e73f51 | |||
| 5485cc563b | |||
| 0edd531ed9 | |||
| 84bc03c9ba | |||
| 3befb0e257 | |||
| 0ae7a6a677 | |||
| 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,110 @@
|
||||
---
|
||||
name: memind-page-data-survey-verify
|
||||
description: >-
|
||||
Verify Page Data survey update chains (e.g. john4 children-hobby q4_diet_meals)
|
||||
without manual chat input. Use when the user asks to 验证问卷链路, 饮食偏好测试,
|
||||
page-data-collect 回归, 儿童爱好问卷验证, or after agent-run / MCP / page-data changes.
|
||||
---
|
||||
|
||||
# Page Data 问卷链路自动验证
|
||||
|
||||
用户说「**帮我验证问卷 / 饮食偏好链路**」「**不用我手动测**」时,**必须**走本 skill,不要只在浏览器里手点。
|
||||
|
||||
## 两条路径
|
||||
|
||||
| 路径 | 何时用 | 命令 | 耗时 |
|
||||
|------|--------|------|------|
|
||||
| **A. 产物验证(默认)** | Agent 已跑完,或只改 Page Data / HTML / 策略 | 见下方 | ~5s |
|
||||
| **B. 全链路 E2E** | 要测 Agent 聊天 + 工具 + 交付 | 见下方 | ~3–10min |
|
||||
|
||||
### A. 产物验证(推荐日常)
|
||||
|
||||
前提:Portal 已运行(`pnpm dev`),默认 `http://127.0.0.1:8081`。
|
||||
|
||||
```bash
|
||||
npm run verify:children-hobby-diet-survey
|
||||
```
|
||||
|
||||
等价:
|
||||
|
||||
```bash
|
||||
node scripts/verify-children-hobby-diet-survey.mjs
|
||||
```
|
||||
|
||||
可选:
|
||||
|
||||
```bash
|
||||
node scripts/verify-children-hobby-diet-survey.mjs --no-insert # 不写测试提交
|
||||
node scripts/verify-children-hobby-diet-survey.mjs --no-runs # 不查 orphan running
|
||||
```
|
||||
|
||||
**自动检查项:**
|
||||
|
||||
1. SQLite 表 `children_hobby_survey` 含 `q4_diet_meals`
|
||||
2. dataset 注册 insert 列含 `q4_diet_meals`
|
||||
3. 问卷 / 后台 HTML 含饮食字段与 Page Data 客户端
|
||||
4. 公开页 policy 允许 insert `q4_diet_meals`
|
||||
5. MindSpace URL HTTP 200
|
||||
6. 公开 insert API 实测提交(默认开启)
|
||||
7. 无 orphan `running` agent run(避免前端一直 loading)
|
||||
|
||||
### B. 全链路 E2E(测 Agent)
|
||||
|
||||
```bash
|
||||
npm run test:scenario:john4-diet
|
||||
```
|
||||
|
||||
场景文件:[`scenarios/john4-children-hobby-diet-update.json`](../../../scenarios/john4-children-hobby-diet-update.json)
|
||||
|
||||
- 账户:`john4` / `888888`(可用 `JOHN4_PASSWORD` 覆盖)
|
||||
- 话术:**「问卷页面再增加一个饮食偏好,比如每日几顿这种」**
|
||||
- 技能:`page-data-collect`
|
||||
- 超时:10 分钟
|
||||
- 结束后自动跑与路径 A 相同的产物验证
|
||||
|
||||
## Agent 工作流
|
||||
|
||||
1. **确认 Portal**:`curl -sf http://127.0.0.1:8081/auth/status`
|
||||
2. **默认跑路径 A**;用户明确要测 Agent 再跑路径 B
|
||||
3. **失败时**:读失败项 → 修代码/环境 → 重跑同一条命令
|
||||
4. **汇报**:用下方模板
|
||||
|
||||
## 已知链路风险(非权限问题)
|
||||
|
||||
验证通过仍可能出现过这些**过程性**问题,写入排查结论时一并说明:
|
||||
|
||||
| 现象 | 根因 | 是否权限 |
|
||||
|------|------|----------|
|
||||
| 前端一直 loading | agent run 孤儿化(Portal 重启) | ❌ |
|
||||
| 工具 `-32603: Transport closed` | sandbox-fs MCP 子进程断开 | ❌ |
|
||||
| insert 403 columns_not_allowed | policy 未更新(bind 失败) | ⚠️ 策略,非用户账号 |
|
||||
| Agent 用 shell 写 HTML | MCP 不稳定时的绕路 | ❌ |
|
||||
|
||||
## 结果模板
|
||||
|
||||
```markdown
|
||||
## 问卷饮食偏好链路验证
|
||||
|
||||
- 模式:产物验证 / E2E
|
||||
- Portal:http://127.0.0.1:8081
|
||||
- 用户:john4
|
||||
- 结果:通过 / 失败
|
||||
|
||||
### 检查项
|
||||
1. SQLite q4_diet_meals — 通过/失败
|
||||
2. HTML 问卷+后台 — 通过/失败
|
||||
3. Page Data policy — 通过/失败
|
||||
4. 公开 insert API — 通过/失败
|
||||
5. Agent run 无 orphan — 通过/失败
|
||||
|
||||
### 失败项与下一步
|
||||
```
|
||||
|
||||
## 与其它 skill 分工
|
||||
|
||||
| 诉求 | 用哪个 |
|
||||
|------|--------|
|
||||
| 验证已有问卷增量(本案例) | **本 skill** |
|
||||
| 通用聊天+做页面场景 | `memind-scenario-test` |
|
||||
| 改代码后单测/守卫 | `memind-test` |
|
||||
| Page Data 单元测试 | `npm run verify:page-data` |
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: memind-scenario-test
|
||||
description: >-
|
||||
Run Memind end-to-end scenario simulations (login, multi-turn chat, page
|
||||
generation, public URL checks). Use when the user asks for 模拟场景测试, 场景测试,
|
||||
E2E 流程验证, 模拟用户操作, or describes a step-by-step chat/page test flow.
|
||||
---
|
||||
|
||||
# Memind 场景模拟测试
|
||||
|
||||
用户说「**帮我模拟场景测试**」或描述多步聊天/做页面流程时,**必须**走本 skill,不要手动拼 curl 或浏览器点选。
|
||||
|
||||
## 唯一入口
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --scenario <id>
|
||||
```
|
||||
|
||||
列出场景:
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --list
|
||||
```
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 本地 Portal 已运行(默认 `http://127.0.0.1:8081`,可用 `pnpm dev`)
|
||||
2. goosed / agent 链路可用(场景会真实发消息并等待 agent 回复)
|
||||
3. 账户默认 `john` / `888888`;密码可用 `JOHN_PASSWORD` 覆盖
|
||||
|
||||
启动前检查:
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:8081/auth/status
|
||||
```
|
||||
|
||||
## 默认场景:苏州攻略页面
|
||||
|
||||
对应用户定义的流程,场景文件 [`scenarios/suzhou-page.json`](../../../scenarios/suzhou-page.json):
|
||||
|
||||
1. 登录 `john` / `888888`
|
||||
2. 发送 `hi`,等待 assistant 回复
|
||||
3. 发送 `帮我看看苏州攻略,1日攻略,做一个页面`
|
||||
4. 验证:收到回复、含苏州关键词、有公网页面链接、HTTP 200、页面内容命中关键词
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --scenario suzhou-page
|
||||
```
|
||||
|
||||
## Agent 工作流
|
||||
|
||||
1. **确认场景**:用户未指定时默认 `suzhou-page`;说了别的流程则查 `--list` 或新建场景文件
|
||||
2. **确认服务**:Portal `/auth/status` 必须 200
|
||||
3. **执行脚本**:完整跑完,不要中途改用浏览器手点
|
||||
4. **失败处理**:读失败项 → 修代码或环境 → 重跑同一场景
|
||||
5. **汇报**:用下方模板
|
||||
|
||||
## 扩展新场景
|
||||
|
||||
复制 [`scenarios/_template.json`](../../../scenarios/_template.json) 为 `scenarios/<id>.json`。
|
||||
|
||||
步骤类型(当前支持):
|
||||
|
||||
| action | 说明 |
|
||||
|--------|------|
|
||||
| `login` | HTTP 登录,取 cookie + userId |
|
||||
| `chat` | 发消息(同 session 多轮),等 run 终态 + assistant 新回复 |
|
||||
|
||||
`chat.expect` 可选断言:
|
||||
|
||||
- `assistantMinChars` / `timeoutMs`
|
||||
- `replyKeywords`:回复文本关键词
|
||||
- `page.keywords` / `requirePublicLink` / `requireHttp200` / `requireMindspaceCover`
|
||||
|
||||
新增更复杂步骤(截图、微信、多账户)时:先扩展 `scripts/scenario-test-lib.mjs`,再加场景 JSON,**不要**在 skill 里写死一次性命令。
|
||||
|
||||
## 结果模板
|
||||
|
||||
```markdown
|
||||
## 场景测试结果
|
||||
|
||||
- 场景:suzhou-page(苏州攻略一日游页面)
|
||||
- Portal:http://127.0.0.1:8081
|
||||
- 账户:john
|
||||
- 结果:通过 / 失败
|
||||
|
||||
### 步骤
|
||||
1. 登录 — 通过
|
||||
2. hi → 回复 — 通过(N 字)
|
||||
3. 苏州页面 — 通过 / 失败
|
||||
|
||||
### 页面(如有)
|
||||
- 链接:
|
||||
- HTTP:
|
||||
- 关键词:
|
||||
|
||||
### 失败项与下一步
|
||||
```
|
||||
|
||||
## 与 memind-test 的分工
|
||||
|
||||
| 诉求 | 用哪个 |
|
||||
|------|--------|
|
||||
| 模拟真实用户聊天 + 做页面 | **本 skill**(`run-scenario-test.mjs`) |
|
||||
| 改代码后跑单测/回归守卫 | `memind-test` skill(`run-memind-tests.mjs`) |
|
||||
| 103 发包 | `portal-release` skill |
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: memind-test
|
||||
description: >-
|
||||
Run Memind scoped or release tests automatically after code changes. Use when
|
||||
the user asks to test, verify, run tests, check regressions, or before
|
||||
merging/releasing; also after editing protected MindSpace/chat/agent paths.
|
||||
---
|
||||
|
||||
# Memind 自动测试
|
||||
|
||||
不要手动拼凑 `node --test` 命令。统一入口:
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs --mode <mode>
|
||||
```
|
||||
|
||||
## 选模式
|
||||
|
||||
| 用户意图 | 模式 |
|
||||
|----------|------|
|
||||
| 改完代码、日常验证 | `changed`(默认) |
|
||||
| 快速冒烟 | `quick` |
|
||||
| 只跑回归守卫 | `guards` |
|
||||
| 发包前 | `release` |
|
||||
| 全量单测 | `full` |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs
|
||||
node scripts/run-memind-tests.mjs --mode quick
|
||||
node scripts/run-memind-tests.mjs --mode release
|
||||
```
|
||||
|
||||
对比基准分支时用 `--base origin/main`。
|
||||
|
||||
## Agent 工作流
|
||||
|
||||
1. **判断模式**:用户说「测试/验证/跑一下」→ `changed`;说「发包前」→ `release`;说「全量」→ `full`。
|
||||
2. **执行**:在仓库根目录跑脚本,不要跳过。
|
||||
3. **失败时**:读报错 → 修代码 → 用同一模式重跑,直到通过。
|
||||
4. **汇报**:用下方模板,不要只说「通过了」。
|
||||
|
||||
## 受保护路径(改后必跑 guards)
|
||||
|
||||
改这些文件时,即使 `changed` 未命中也要加跑 `guards`:
|
||||
|
||||
- `mindspace-public-finish-sync.mjs`、`chat-finish-sync.mjs`
|
||||
- `conversation-display.mjs`、`src/utils/message.ts`、`src/hooks/useTKMindChat.ts`
|
||||
- `mindspace-pages.mjs`、`mindspace-page-sync-service.mjs`、`server.mjs`
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs --mode guards
|
||||
```
|
||||
|
||||
详见 [docs/regression-guards/README.md](../../../docs/regression-guards/README.md)。
|
||||
|
||||
## 结果模板
|
||||
|
||||
```markdown
|
||||
## 测试结果
|
||||
|
||||
- 模式:`changed` / `quick` / `guards` / `release` / `full`
|
||||
- 变更文件:N 个(列出关键路径)
|
||||
- 执行:X 个 test + Y 个 verify
|
||||
- 结果:通过 / 失败
|
||||
|
||||
### 失败项(如有)
|
||||
- 命令:
|
||||
- 错误摘要:
|
||||
- 下一步:
|
||||
```
|
||||
|
||||
## 与发布 skill 的关系
|
||||
|
||||
- 日常开发:本 skill + `changed` / `guards`
|
||||
- 103 生产发布:用 `portal-release` skill,其发包前测试与本 skill 的 `release` 模式一致
|
||||
- 不要混用「手动挑几个 test 文件」替代上述入口
|
||||
@@ -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 $(seq 18006 18014); 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`(Portal + 9 个 goosed target),五个 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`–`18014` 健康结果(共 9 个 target)
|
||||
- 日志关键词检查结果
|
||||
- 第 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
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: memind-scenario-test
|
||||
description: >-
|
||||
Run Memind end-to-end scenario simulations (login, multi-turn chat, page
|
||||
generation, public URL checks). Use when the user asks for 模拟场景测试, 场景测试,
|
||||
E2E 流程验证, 模拟用户操作, or describes a step-by-step chat/page test flow.
|
||||
---
|
||||
|
||||
# Memind 场景模拟测试
|
||||
|
||||
用户说「**帮我模拟场景测试**」或描述多步聊天/做页面流程时,**必须**走本 skill,不要手动拼 curl 或浏览器点选。
|
||||
|
||||
## 唯一入口
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --scenario <id>
|
||||
```
|
||||
|
||||
列出场景:
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --list
|
||||
```
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 本地 Portal 已运行(默认 `http://127.0.0.1:8081`,可用 `pnpm dev`)
|
||||
2. goosed / agent 链路可用(场景会真实发消息并等待 agent 回复)
|
||||
3. 账户默认 `john` / `888888`;密码可用 `JOHN_PASSWORD` 覆盖
|
||||
|
||||
启动前检查:
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:8081/auth/status
|
||||
```
|
||||
|
||||
## 默认场景:苏州攻略页面
|
||||
|
||||
对应用户定义的流程,场景文件 [`scenarios/suzhou-page.json`](../../../scenarios/suzhou-page.json):
|
||||
|
||||
1. 登录 `john` / `888888`
|
||||
2. 发送 `hi`,等待 assistant 回复
|
||||
3. 发送 `帮我看看苏州攻略,1日攻略,做一个页面`
|
||||
4. 验证:收到回复、含苏州关键词、有公网页面链接、HTTP 200、页面内容命中关键词
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
node scripts/run-scenario-test.mjs --scenario suzhou-page
|
||||
```
|
||||
|
||||
## Agent 工作流
|
||||
|
||||
1. **确认场景**:用户未指定时默认 `suzhou-page`;说了别的流程则查 `--list` 或新建场景文件
|
||||
2. **确认服务**:Portal `/auth/status` 必须 200
|
||||
3. **执行脚本**:完整跑完,不要中途改用浏览器手点
|
||||
4. **失败处理**:读失败项 → 修代码或环境 → 重跑同一场景
|
||||
5. **汇报**:用下方模板
|
||||
|
||||
## 扩展新场景
|
||||
|
||||
复制 [`scenarios/_template.json`](../../../scenarios/_template.json) 为 `scenarios/<id>.json`。
|
||||
|
||||
步骤类型(当前支持):
|
||||
|
||||
| action | 说明 |
|
||||
|--------|------|
|
||||
| `login` | HTTP 登录,取 cookie + userId |
|
||||
| `chat` | 发消息(同 session 多轮),等 run 终态 + assistant 新回复 |
|
||||
|
||||
`chat.expect` 可选断言:
|
||||
|
||||
- `assistantMinChars` / `timeoutMs`
|
||||
- `replyKeywords`:回复文本关键词
|
||||
- `page.keywords` / `requirePublicLink` / `requireHttp200` / `requireMindspaceCover`
|
||||
|
||||
新增更复杂步骤(截图、微信、多账户)时:先扩展 `scripts/scenario-test-lib.mjs`,再加场景 JSON,**不要**在 skill 里写死一次性命令。
|
||||
|
||||
## 结果模板
|
||||
|
||||
```markdown
|
||||
## 场景测试结果
|
||||
|
||||
- 场景:suzhou-page(苏州攻略一日游页面)
|
||||
- Portal:http://127.0.0.1:8081
|
||||
- 账户:john
|
||||
- 结果:通过 / 失败
|
||||
|
||||
### 步骤
|
||||
1. 登录 — 通过
|
||||
2. hi → 回复 — 通过(N 字)
|
||||
3. 苏州页面 — 通过 / 失败
|
||||
|
||||
### 页面(如有)
|
||||
- 链接:
|
||||
- HTTP:
|
||||
- 关键词:
|
||||
|
||||
### 失败项与下一步
|
||||
```
|
||||
|
||||
## 与 memind-test 的分工
|
||||
|
||||
| 诉求 | 用哪个 |
|
||||
|------|--------|
|
||||
| 模拟真实用户聊天 + 做页面 | **本 skill**(`run-scenario-test.mjs`) |
|
||||
| 改代码后跑单测/回归守卫 | `memind-test` skill(`run-memind-tests.mjs`) |
|
||||
| 103 发包 | `portal-release` skill |
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: memind-test
|
||||
description: >-
|
||||
Run Memind scoped or release tests automatically after code changes. Use when
|
||||
the user asks to test, verify, run tests, check regressions, or before
|
||||
merging/releasing; also after editing protected MindSpace/chat/agent paths.
|
||||
---
|
||||
|
||||
# Memind 自动测试
|
||||
|
||||
不要手动拼凑 `node --test` 命令。统一入口:
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs --mode <mode>
|
||||
```
|
||||
|
||||
## 选模式
|
||||
|
||||
| 用户意图 | 模式 |
|
||||
|----------|------|
|
||||
| 改完代码、日常验证 | `changed`(默认) |
|
||||
| 快速冒烟 | `quick` |
|
||||
| 只跑回归守卫 | `guards` |
|
||||
| 发包前 | `release` |
|
||||
| 全量单测 | `full` |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs
|
||||
node scripts/run-memind-tests.mjs --mode quick
|
||||
node scripts/run-memind-tests.mjs --mode release
|
||||
```
|
||||
|
||||
对比基准分支时用 `--base origin/main`。
|
||||
|
||||
## Agent 工作流
|
||||
|
||||
1. **判断模式**:用户说「测试/验证/跑一下」→ `changed`;说「发包前」→ `release`;说「全量」→ `full`。
|
||||
2. **执行**:在仓库根目录跑脚本,不要跳过。
|
||||
3. **失败时**:读报错 → 修代码 → 用同一模式重跑,直到通过。
|
||||
4. **汇报**:用下方模板,不要只说「通过了」。
|
||||
|
||||
## 受保护路径(改后必跑 guards)
|
||||
|
||||
改这些文件时,即使 `changed` 未命中也要加跑 `guards`:
|
||||
|
||||
- `mindspace-public-finish-sync.mjs`、`chat-finish-sync.mjs`
|
||||
- `conversation-display.mjs`、`src/utils/message.ts`、`src/hooks/useTKMindChat.ts`
|
||||
- `mindspace-pages.mjs`、`mindspace-page-sync-service.mjs`、`server.mjs`
|
||||
|
||||
```bash
|
||||
node scripts/run-memind-tests.mjs --mode guards
|
||||
```
|
||||
|
||||
详见 [docs/regression-guards/README.md](../../../docs/regression-guards/README.md)。
|
||||
|
||||
## 结果模板
|
||||
|
||||
```markdown
|
||||
## 测试结果
|
||||
|
||||
- 模式:`changed` / `quick` / `guards` / `release` / `full`
|
||||
- 变更文件:N 个(列出关键路径)
|
||||
- 执行:X 个 test + Y 个 verify
|
||||
- 结果:通过 / 失败
|
||||
|
||||
### 失败项(如有)
|
||||
- 命令:
|
||||
- 错误摘要:
|
||||
- 下一步:
|
||||
```
|
||||
|
||||
## 与发布 skill 的关系
|
||||
|
||||
- 日常开发:本 skill + `changed` / `guards`
|
||||
- **模拟用户聊天/做页面**:用 `memind-scenario-test` skill(`run-scenario-test.mjs`)
|
||||
- 103 生产发布:用 `portal-release` skill,其发包前测试与本 skill 的 `release` 模式一致
|
||||
- 不要混用「手动挑几个 test 文件」替代上述入口
|
||||
@@ -1,28 +1,129 @@
|
||||
# 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
|
||||
# Vite UI 预览用 5173;Agent 交付的 MindSpace 公开页 + Page Data API 走 Portal(8081)。
|
||||
# pnpm dev / dev-core 在未显式覆盖时会把 H5_PUBLIC_BASE_URL 设为 8081。
|
||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
|
||||
# Local Umami analytics (disabled by default; points only to the local
|
||||
# memind-analytics service when enabled).
|
||||
# MEMIND_ANALYTICS_ENABLED=true
|
||||
# MEMIND_ANALYTICS_URL=http://127.0.0.1:3100
|
||||
# MEMIND_ANALYTICS_WEBSITE_ID=<local Umami Website ID>
|
||||
# MEMIND_ANALYTICS_ID_SECRET=<local-only pseudonymization secret>
|
||||
# MEMIND_ANALYTICS_DOMAINS=127.0.0.1,localhost
|
||||
|
||||
# 生产 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 +132,37 @@ 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 单价扣费;生产推荐开启成本模式(见 docs/h5-metering-gateway.md)
|
||||
# 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
|
||||
# 成本模式:上游 accumulated_cost(USD) × 汇率 × 毛利
|
||||
# H5_USE_BACKEND_COST=1
|
||||
# H5_USD_CNY_RATE=7.2
|
||||
# H5_MARGIN_MULTIPLIER=1.2
|
||||
# 当 goose Finish 未带 accumulatedCost 时,按 token 估算上游 USD 成本(默认开启,设 0 关闭)
|
||||
# H5_COST_ESTIMATE_FROM_TOKENS=1
|
||||
# H5_COST_ESTIMATE_INPUT_USD_PER_1M=0.27
|
||||
# H5_COST_ESTIMATE_OUTPUT_USD_PER_1M=1.1
|
||||
|
||||
# 用户自助充值(微信支付)
|
||||
# H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000
|
||||
@@ -81,6 +191,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
|
||||
|
||||
@@ -95,6 +209,10 @@ H5_ACCESS_PASSWORD=change-me
|
||||
# H5_WECHAT_OPEN_APP_ID=
|
||||
# H5_WECHAT_OPEN_APP_SECRET=
|
||||
|
||||
# 微信小程序原生登录(wx.login → /auth/wechat-miniapp/login)
|
||||
# H5_WECHAT_MINIAPP_APP_ID=wx...
|
||||
# H5_WECHAT_MINIAPP_APP_SECRET=小程序AppSecret
|
||||
|
||||
# 本地开发:pnpm dev 会自动启动 server.mjs + vite
|
||||
# 若只跑前端:pnpm dev:vite(需另开 pnpm dev:server)
|
||||
# H5_DEV_PORTAL=http://127.0.0.1:8081
|
||||
@@ -118,24 +236,47 @@ 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_STORAGE_ROOT=/path/to/mindspace-storage
|
||||
# MINDSPACE_FREE_PUBLIC_PAGE_LIMIT=10
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Memind runtime profile(本地 vs 生产 必须区分)
|
||||
# 由 scripts/memind-runtime-profile.mjs 在 pnpm dev / server.mjs 启动时应用。
|
||||
#
|
||||
# | profile | 场景 | MindSpace 数据路径 |
|
||||
# |----------------|-------------------|--------------------|
|
||||
# | local | 本机日常开发 | <repo>/data/mindspace |
|
||||
# | split-service | 本机验证 103 拆分 | /Users/john/MindSpace/data/mindspace |
|
||||
# | production | 103 生产服务器 | 103 .env 为准,勿提交 Git |
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 【本地开发 — 复制到 .env 的默认值】
|
||||
MEMIND_RUNTIME_PROFILE=local
|
||||
# MINDSPACE_STORAGE_ROOT 留空即可,自动使用 <repo>/data/mindspace
|
||||
|
||||
# 【本机 split-service 联调 — 需要独立 MindSpace LaunchAgent 已启动】
|
||||
# MEMIND_RUNTIME_PROFILE=split-service
|
||||
# MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:8082
|
||||
# MINDSPACE_REMOTE_AUTH_TOKEN=local-dev-secret
|
||||
# MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace
|
||||
|
||||
# 【103 生产 — 仅在服务器维护,示例勿照抄密钥】
|
||||
# MEMIND_RUNTIME_PROFILE=production
|
||||
# MINDSPACE_SERVER_ADAPTER=remote
|
||||
# MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:8082
|
||||
# MINDSPACE_REMOTE_AUTH_TOKEN=<与 cn.tkmind.mindspace-service 一致>
|
||||
# MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Memind CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: memind-ci-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Test, build, and release guards
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Check out exact commit
|
||||
run: |
|
||||
git init .
|
||||
git remote add origin https://git.tkmind.cn/tkmind/memind.git
|
||||
git fetch --depth=2 origin "${{ gitea.sha }}"
|
||||
git checkout --detach FETCH_HEAD
|
||||
test "$(git rev-parse HEAD)" = "${{ gitea.sha }}"
|
||||
|
||||
- name: Install system test dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends sqlite3
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install locked dependencies
|
||||
run: |
|
||||
npm ci --include=optional
|
||||
SHARP_ARM64_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/@img/sharp-linux-arm64'].version")"
|
||||
LIBVIPS_ARM64_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/@img/sharp-libvips-linux-arm64'].version")"
|
||||
npm install --no-save --package-lock=false \
|
||||
"@img/sharp-linux-arm64@${SHARP_ARM64_VERSION}" \
|
||||
"@img/sharp-libvips-linux-arm64@${LIBVIPS_ARM64_VERSION}"
|
||||
node -e "import('sharp').then((sharp) => sharp.default({ create: { width: 1, height: 1, channels: 4, background: '#000' } }).png().toBuffer())"
|
||||
|
||||
- name: Check patch formatting
|
||||
run: git diff --check HEAD^
|
||||
|
||||
- name: Run full test suite
|
||||
run: npm test
|
||||
|
||||
- name: Build production frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Verify MindSpace publish guards
|
||||
run: npm run verify:mindspace-publish-guards
|
||||
|
||||
- name: Verify MindSpace page sync guards
|
||||
run: npm run verify:mindspace-page-sync-guards
|
||||
|
||||
- name: Verify Page Data delivery
|
||||
run: npm run verify:page-data
|
||||
|
||||
- name: Check published download links
|
||||
run: npm run check:mindspace-public-links
|
||||
@@ -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 |