feat: 持久对话 MCP 插件 v1.6.37 · Hub + Panel + MCP 三形态部署
- src/: server.js (MCP 4 工具) + hub.js (HTTP Hub) + cursor-title.js - web/: panel.html (主面板) + panel.remote.html (远端 1637) - scripts/: ensure-hub.sh (stdio→http 桥) + mongo_sync.py - mcp-config/: trae.mcp.json + cursor.mcp.json - docs/: INSTALL / DEPLOY / MCP_SETUP / ARCHITECTURE / QUICKSTART - ZIP 原始安装包备查 关键机制: - 4 MCP 工具:init / select / wait / merge - 垂直绑定:workspace::cursorTitle@hostIp 三件指纹 - 真挂起:关闭 timedReply 后 wait 真正阻塞,agent turn 不结束 - 双 Hub 部署:远端 NAS 24×7 + 本地 launchd 守护备份 - 三形态组合:远端 + 本地 + Trae 插件,可同时跑
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# 运行时数据
|
||||
sessions.json
|
||||
bindings.json
|
||||
plan-bindings.json
|
||||
*.bak.*
|
||||
thread-bindings/
|
||||
archive/
|
||||
replies/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 临时
|
||||
.DS_Store
|
||||
.removed-20260626/
|
||||
BIN
Cursor持久对话-安装包-v1.6.37_20260607.zip
Normal file
BIN
Cursor持久对话-安装包-v1.6.37_20260607.zip
Normal file
Binary file not shown.
2
LICENSE
Normal file
2
LICENSE
Normal file
@@ -0,0 +1,2 @@
|
||||
私有项目 · 仅公司内部使用 © 2026 卡若AI
|
||||
仅授权公司 NAS (192.168.110.101) 内网使用,未经许可不得外传。
|
||||
154
README.md
Normal file
154
README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Cursor/Trae 持久对话 MCP 插件 v1.6.37
|
||||
|
||||
> **垂直绑定 · 真挂起 · 永不断开**
|
||||
> 一个让 Cursor / Trae / VSCode 与 Agent 之间实现"可持续对话"的 MCP 插件。
|
||||
|
||||
[](http://192.168.110.101:3000/fnvtk/persistent-chat-plugin)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 一句话描述
|
||||
|
||||
普通 MCP 调一次就结束对话;本插件用 **4 个工具 + Hub 长连接**,让 agent 与用户的对话**永不中断**,适合长跑学习清单 / 跨天多计划 / 自动续跑任务。
|
||||
|
||||
---
|
||||
|
||||
## 📦 仓库内容
|
||||
|
||||
```
|
||||
persistent-chat-plugin/
|
||||
├── src/ # 核心源码(Node 22)
|
||||
│ ├── server.js (42KB) # MCP Server 端,4 个工具入口
|
||||
│ ├── hub.js (89KB) # Hub 主服务(HTTP + WebSocket)
|
||||
│ └── cursor-title.js (4KB) # Cursor 标题解析器
|
||||
│
|
||||
├── web/ # 持久对话面板(浏览器 UI)
|
||||
│ ├── panel.html (84KB) # 主面板 v1.6.37
|
||||
│ ├── panel.remote.html # 远端 NAS 面板(panel_remote_v1637)
|
||||
│ └── panel.html.local # 本地备份面板
|
||||
│
|
||||
├── scripts/ # 部署与运维脚本
|
||||
│ ├── ensure-hub.sh # 启动器(MCP stdio → http)
|
||||
│ ├── mongo_sync.py # 会话 → MongoDB 同步
|
||||
│ └── (deploy scripts...)
|
||||
│
|
||||
├── mcp-config/ # MCP 配置模板
|
||||
│ ├── trae.mcp.json # Trae CN 配法
|
||||
│ └── cursor.mcp.json # Cursor 配法
|
||||
│
|
||||
├── docs/ # 完整文档
|
||||
│ ├── INSTALL.md # 安装
|
||||
│ ├── DEPLOY.md # 部署
|
||||
│ ├── MCP_SETUP.md # MCP 配置
|
||||
│ ├── ARCHITECTURE.md # 架构
|
||||
│ └── QUICKSTART.md # 5 分钟上手
|
||||
│
|
||||
├── Cursor持久对话-安装包-v1.6.37_20260607.zip # 原始安装包(备查)
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 三种部署形态
|
||||
|
||||
| 形态 | 端口 | 适用 | 关键 |
|
||||
|:---|:---|:---|:---|
|
||||
| **本地备份** | 127.0.0.1:13458 | 单机 | launchd 守护,挂了自动起 |
|
||||
| **远端 NAS** | 192.168.110.101:13458 | 多人 / 跨机 | Node 守护,跨网段访问 |
|
||||
| **Trae 插件** | MCP stdio | 与 IDE 集成 | ensure-hub.sh 起 4 工具 |
|
||||
|
||||
可同时跑,三者共享同一 Hub 协议。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 4 个 MCP 工具
|
||||
|
||||
| # | 工具 | 用途 |
|
||||
|:---:|:---|:---|
|
||||
| 1 | `init_conversation` | 起新 session(支持 `newPlan:true` 换计划) |
|
||||
| 2 | `select_conversation` | 复用旧 session(垂直绑定校验) |
|
||||
| 3 | `wait_for_user_input` | 真挂起,等用户下一条 |
|
||||
| 4 | `merge_conversation` | 合流两个 session 的上下文 |
|
||||
|
||||
**核心机制**:Hub 把 session 存在内存 + 周期 save,token (`ct_xxx`) 垂直绑定到「工作区 + Cursor 标签 + IP」三件指纹。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 5 分钟上手
|
||||
|
||||
```bash
|
||||
# 1. 克隆
|
||||
git clone http://192.168.110.101:3000/fnvtk/persistent-chat-plugin.git
|
||||
cd persistent-chat-plugin
|
||||
|
||||
# 2. 部署到 ~/.persistent-chat-local
|
||||
mkdir -p ~/.persistent-chat-local
|
||||
cp src/* scripts/ensure-hub.sh ~/.persistent-chat-local/
|
||||
cp web/panel.html ~/.persistent-chat-local/panel.html
|
||||
|
||||
# 3. 编辑 mcp.json,填到 Trae/Cursor
|
||||
cat mcp-config/trae.mcp.json
|
||||
# 复制内容到 ~/.trae/mcp.json
|
||||
|
||||
# 4. Trae Cmd+Q 退出重开,让 MCP 重新加载
|
||||
# 5. 在 SOLO Agent 窗口发"开始测试"或"继续"
|
||||
```
|
||||
|
||||
详细见 [QUICKSTART.md](docs/QUICKSTART.md)。
|
||||
|
||||
---
|
||||
|
||||
## 🔒 垂直绑定
|
||||
|
||||
每个 session 用「工作区::Cursor 标签 @ IP」做绑定指纹:
|
||||
|
||||
```
|
||||
/Users/karuo/Documents/个人::持久对话永久续跑-20260626@192.168.3.1
|
||||
```
|
||||
|
||||
换工作区 / 换标签 / 换 IP 都不会复用 session,避免串台。
|
||||
|
||||
---
|
||||
|
||||
## 📊 核心指标
|
||||
|
||||
| 指标 | 值 |
|
||||
|:---|:---|
|
||||
| Hub 进程 | Node 22 (`/usr/local/opt/node@22/bin/node`) |
|
||||
| 端口 | 13458 |
|
||||
| 协议 | HTTP + 短轮询(10s 续约) |
|
||||
| 保活档 | 10s / 30s / 60s / 2min / 4min / 9min / 15min / 30min |
|
||||
| 校验频率 | 每次 wait 触发一次 |
|
||||
| 单 session 容量 | 无限制(内存 + JSON 周期 save) |
|
||||
|
||||
---
|
||||
|
||||
## 🆘 常见问题
|
||||
|
||||
**Q: 提示"Token未获取, 未登录"?**
|
||||
A: Trae 需 `Cmd+Q` 完全退出重开,让 MCP 配置生效。
|
||||
|
||||
**Q: "任务完成" 是不是对话死了?**
|
||||
A: 不是。Hub 上 session 仍 holding,等用户下一条自动续跑。可在 http://192.168.110.101:13458/ 看到。
|
||||
|
||||
**Q: 如何真正关掉对话?**
|
||||
A: 在对话里说"结束持久对话",agent 会主动 merge。
|
||||
|
||||
---
|
||||
|
||||
## 📜 许可证
|
||||
|
||||
私有项目 · 仅公司内部使用 © 2026 卡若AI
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- **Gitea 仓库**:http://192.168.110.101:3000/fnvtk/persistent-chat-plugin
|
||||
- **远端面板**:http://192.168.110.101:13458/
|
||||
- **本地备份面板**:http://127.0.0.1:13458/
|
||||
- **Trae 配置**:`~/.trae/mcp.json`
|
||||
- **Cursor 配置**:`~/Documents/个人/.cursor/mcp.json`
|
||||
340
docs/ARCHITECTURE.md
Normal file
340
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# 架构(ARCHITECTURE.md)
|
||||
|
||||
> 持久对话 MCP 插件的完整技术架构
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体拓扑
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ Trae / Cursor │ IDE(SOLO Agent 窗口)
|
||||
│ (MCP client) │
|
||||
└─────────┬──────────┘
|
||||
│ stdio JSON-RPC
|
||||
│ 4 tools: init / select / wait / merge
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ ensure-hub.sh │ stdio → HTTP 桥接
|
||||
│ (MCP server) │
|
||||
└─────────┬──────────┘
|
||||
│ HTTP (13458)
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ hub.js (主) │ ┌─ 远端: 192.168.110.101:13458
|
||||
│ Node 22 + HTTP │ └─ 本地: 127.0.0.1:13458
|
||||
└─────────┬──────────┘
|
||||
│ 短轮询(10s)
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ sessions.json │ 持久化层
|
||||
│ bindings.json │ - sessions: ct_xxx → 状态
|
||||
│ plan-bindings │ - bindings: workspace::title@ip → ct_
|
||||
│ thread-bindings/ │ - plan: planName → ct_
|
||||
└────────────────────┘ - thread: threadId → ct_
|
||||
|
||||
↓ 周期同步(可选)
|
||||
┌────────────────────┐
|
||||
│ MongoDB │ 远端数据库
|
||||
│ karuo_site │ (port 27017)
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心数据流
|
||||
|
||||
### 2.1 初始化
|
||||
|
||||
```
|
||||
Agent → init_conversation(cursorTitle=标签, newPlan=true, planLabel=xxx)
|
||||
→ ensure-hub.sh → POST /api/init
|
||||
→ hub.js 创建 session(ct_xxx)
|
||||
→ 写入 sessions.json
|
||||
→ 写 binding:workspace::title@ip → ct_xxx
|
||||
→ 返回 { token: ct_xxx, status: holding }
|
||||
```
|
||||
|
||||
### 2.2 选择已有
|
||||
|
||||
```
|
||||
Agent → select_conversation(token=ct_xxx, cursorTitle=标签)
|
||||
→ ensure-hub.sh → POST /api/select
|
||||
→ hub.js 验证垂直绑定(workspace::title@ip 三件指纹)
|
||||
→ 不匹配 → 403 BINDING_REJECTED
|
||||
→ 匹配 → 复用 session
|
||||
```
|
||||
|
||||
### 2.3 真挂起
|
||||
|
||||
```
|
||||
Agent → wait_for_user_input(token, message, timeout)
|
||||
→ ensure-hub.sh → POST /api/wait/register
|
||||
→ hub.js 设置 session.status = "waiting"
|
||||
→ 注册到 pendingWaits[token]
|
||||
→ 长连接(短轮询 10s 续约)
|
||||
→ 收到 reply:
|
||||
a. 用户发消息 → deliverReply → 返回
|
||||
b. timedReply 触发(10s 间隔)→ deliverReply("保活续跑") → 返回
|
||||
c. POLL_TIMEOUT_MS=10s → renewal → 继续轮询
|
||||
→ Agent 端按 status 决定:
|
||||
- "replied" → 返回 text
|
||||
- "renewal" → 继续 poll
|
||||
- "gone" → 重新 register
|
||||
```
|
||||
|
||||
### 2.4 合并
|
||||
|
||||
```
|
||||
Agent → merge_conversation(fromToken, toToken, cursorTitle)
|
||||
→ ensure-hub.sh → POST /api/merge
|
||||
→ hub.js 把 fromToken 的 messages 追加到 toToken
|
||||
→ fromToken 标记 archived
|
||||
→ 增量保存 sessions.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 垂直绑定
|
||||
|
||||
### 3.1 绑定指纹
|
||||
|
||||
```
|
||||
bindingKey = `${workspace}::${cursorTitle}@${hostIp}`
|
||||
```
|
||||
|
||||
示例:
|
||||
```
|
||||
/Users/karuo/Documents/个人::持久对话永久续跑-20260626@192.168.3.1
|
||||
```
|
||||
|
||||
### 3.2 验证时机
|
||||
|
||||
| 触发 | 验证 |
|
||||
|:---|:---|
|
||||
| `select_conversation` | 严校验(必须完全匹配) |
|
||||
| `init_conversation` | 宽校验(可换 title 重新绑定) |
|
||||
| `wait_for_user_input` | 中校验(容许 title 改名) |
|
||||
| `merge_conversation` | 严校验(不匹配拒绝) |
|
||||
|
||||
### 3.3 改标签自动迁移
|
||||
|
||||
```javascript
|
||||
// 用户在 Trae 改了标签 "A" → "B"
|
||||
// wait 时传入新 title "B"
|
||||
// hub.js 检测到旧 binding 有 token,旧 bindingKey 不匹配
|
||||
// 允许迁移:把 bindingKey 改成新值,token 复用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. session 状态机
|
||||
|
||||
```
|
||||
init
|
||||
│
|
||||
▼
|
||||
holding ──────┐
|
||||
│ │ (每 10s 续约 / 接收新消息)
|
||||
│ │
|
||||
▼ │
|
||||
waiting ───────┤
|
||||
│ │
|
||||
│ (用户发消息)│
|
||||
▼ │
|
||||
replied ───────┘
|
||||
│
|
||||
│ (autoStreak 累加)
|
||||
▼
|
||||
running
|
||||
│
|
||||
│ (merge / 结束持久对话)
|
||||
▼
|
||||
merged / archived
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. autoStreak 与 keepAlive
|
||||
|
||||
### 5.1 概念
|
||||
|
||||
- `autoStreak`:连续"用户不发消息"的轮次计数
|
||||
- `keepAlive`:Hub 在 autoStreak 超阈值时主动推"保活续跑"消息
|
||||
|
||||
### 5.2 真挂起模式(推荐)
|
||||
|
||||
```json
|
||||
POST /api/session/config
|
||||
{
|
||||
"sessionAutoContinue": false,
|
||||
"timedReplyEnabled": false,
|
||||
"timedReplyIntervalMs": 0
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 关闭 timedReply → 不会主动推"保活续跑"
|
||||
- 关闭 autoContinue → 不会因为 idle 触发续跑
|
||||
- wait 唯一返回源 = 用户真消息
|
||||
- MCP 工具调用一直阻塞,agent turn 不会结束
|
||||
|
||||
### 5.3 自动模式(不推荐,会顶回 wait)
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionAutoContinue": true,
|
||||
"timedReplyEnabled": true,
|
||||
"timedReplyIntervalMs": 10000
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 每 10s 推"保活续跑"
|
||||
- wait 立即返回
|
||||
- agent turn 结束(UI 显示"任务完成")
|
||||
|
||||
---
|
||||
|
||||
## 6. 短轮询 vs 长连接
|
||||
|
||||
| 方案 | 优点 | 缺点 | 采用 |
|
||||
|:---|:---|:---|:---|
|
||||
| 短轮询(10s) | 简单、跨网段友好、稳 | 续约频繁 | ✅ |
|
||||
| WebSocket | 实时、省资源 | 需配 wss、断线重连复杂 | ❌ |
|
||||
| Server-Sent Events | 单向实时 | 不支持双向 | ❌ |
|
||||
| Long Polling | 接近实时 | 服务器维护成本 | ❌(短轮询已够) |
|
||||
|
||||
短轮询间隔:`POLL_TIMEOUT_MS = 10000`(10s),可在 `hub.js` 改。
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据持久化
|
||||
|
||||
### 7.1 文件布局
|
||||
|
||||
```
|
||||
~/.persistent-chat-local/
|
||||
├── sessions.json # 主 session 数据
|
||||
├── bindings.json # workspace::title@ip → ct_
|
||||
├── plan-bindings.json # planName → ct_
|
||||
├── thread-bindings/ # threadId → ct_ (mkdir)
|
||||
├── archive/ # 归档 session
|
||||
│ └── 2026-06/
|
||||
│ └── ct_xxx.json
|
||||
├── replies/ # 历史用户回复缓存
|
||||
├── logs/
|
||||
│ ├── hub.log # Hub 主日志
|
||||
│ ├── launchd.log # macOS launchd 日志
|
||||
│ └── launchd.err
|
||||
├── templates/ # 面板模板
|
||||
├── server.js # MCP server(41KB)
|
||||
├── hub.js # Hub 主服务(89KB)
|
||||
├── panel.html # 浏览器面板(84KB)
|
||||
├── cursor-title.js # 标签解析器(4KB)
|
||||
├── ensure-hub.sh # 启动器
|
||||
└── mongo_sync.py # → MongoDB 同步
|
||||
```
|
||||
|
||||
### 7.2 sessions.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ct_xxxx": {
|
||||
"token": "ct_xxxx",
|
||||
"title": "永久续跑三段式",
|
||||
"displayTitle": "",
|
||||
"status": "holding",
|
||||
"workspace": "/Users/karuo/Documents/个人",
|
||||
"createdAt": 1782448693854,
|
||||
"lastActiveAt": 1782449356228,
|
||||
"msgCount": 10,
|
||||
"autoStreak": 1,
|
||||
"bindingKey": "/Users/karuo/Documents/个人::标签@192.168.3.1",
|
||||
"hostIp": "192.168.3.1",
|
||||
"hub": "192.168.110.101:13458",
|
||||
"bindingCheckCount": 17,
|
||||
"continueMode": "optimize",
|
||||
"sessionAutoContinue": false,
|
||||
"timedReplyEnabled": false,
|
||||
"timedReplyIntervalMs": 0,
|
||||
"messages": [
|
||||
{ "role": "user", "content": "...", "at": 1782448794716, "auto": false },
|
||||
{ "role": "assistant", "content": "...", "at": 1782449116363, "auto": false }
|
||||
],
|
||||
"lastReview": {
|
||||
"hasReview": false,
|
||||
"completionPct": null,
|
||||
"nextStep": "",
|
||||
"isIdleNext": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 周期保存
|
||||
|
||||
- 每次 register / deliverReply / merge 后 `saveSessions()`
|
||||
- 写盘:原子 rename(先写 .tmp,再 rename)
|
||||
- 加载:启动时一次 loadSessions(),之后只内存操作
|
||||
|
||||
---
|
||||
|
||||
## 8. 与 MongoDB 同步(可选)
|
||||
|
||||
`mongo_sync.py` 把 sessions.json 周期同步到 MongoDB `karuo_site.sessions`:
|
||||
|
||||
```python
|
||||
# 触发
|
||||
python3 mongo_sync.py
|
||||
# 或 cron 每 5 分钟
|
||||
*/5 * * * * cd ~/.persistent-chat-local && python3 mongo_sync.py
|
||||
```
|
||||
|
||||
用途:
|
||||
- 多机同步(局域网内多 Hub)
|
||||
- 长期归档(超过 1000 个 session 不卡)
|
||||
- 跨平台(Mac / NAS / Linux)
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全性
|
||||
|
||||
| 维度 | 措施 |
|
||||
|:---|:---|
|
||||
| 端口 | 13458 默认仅 LAN 监听,不暴露公网 |
|
||||
| 鉴权 | 垂直绑定(三件指纹),无 token → 拒绝 |
|
||||
| 数据 | sessions.json 落在 ~/.persistent-chat-local/,权限 600 |
|
||||
| 跨网段 | 靠 SSH 隧道 / VPN |
|
||||
| 日志 | 不记录用户隐私内容(只记 metadata) |
|
||||
| 卡密激活 | **已删除**(v1.6.37 不再有 license 校验) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 性能
|
||||
|
||||
| 指标 | 值 |
|
||||
|:---|:---|
|
||||
| Hub 内存 | ~50MB(1000 个 session) |
|
||||
| Hub CPU | < 1%(空闲) |
|
||||
| 单 session 占内存 | ~5KB(仅 metadata) |
|
||||
| 单消息占内存 | ~1KB |
|
||||
| 短轮询带宽 | ~1KB/10s/session |
|
||||
| 启动时间 | < 1s(Node 22) |
|
||||
| sessions.json 加载 | 10000 个 session < 500ms |
|
||||
|
||||
---
|
||||
|
||||
## 11. 版本演进
|
||||
|
||||
| 版本 | 日期 | 关键变化 |
|
||||
|:---|:---|:---|
|
||||
| v1.1 | 2025-12 | 初版(仅 hub.js) |
|
||||
| v1.5 | 2026-03 | 加 server.js / MCP 4 工具 |
|
||||
| v1.6.30 | 2026-05 | autoStreak / timedReply |
|
||||
| v1.6.37 | 2026-06-07 | **删除卡密激活** / 加 MongoDB 同步 |
|
||||
| v1.6.68 | 2026-06-20 | 本地增强(VSIX 4.6 兼容) |
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-26 · 卡若AI
|
||||
281
docs/DEPLOY.md
Normal file
281
docs/DEPLOY.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 部署文档(DEPLOY.md)
|
||||
|
||||
> 三种部署形态 + 完整运维手册
|
||||
> 适用版本:v1.6.37(2026-06-07)
|
||||
|
||||
---
|
||||
|
||||
## 形态 A:远端 NAS(推荐 · 公司 13458)
|
||||
|
||||
### 适用场景
|
||||
- 多人 / 跨机 / 跨网段访问
|
||||
- 24×7 在线(NAS 不关机)
|
||||
- 唯一权威 Hub,所有 IDE 共享
|
||||
|
||||
### 前置条件
|
||||
| 项 | 要求 |
|
||||
|:---|:---|
|
||||
| NAS IP | 192.168.110.101(公司)/ 192.168.1.201(公司 LAN) |
|
||||
| Node | v22(路径 `/usr/local/opt/node@22/bin/node`) |
|
||||
| 端口 | 13458(HTTP) |
|
||||
| 用户 | `fnvtk`(SSH) |
|
||||
| 数据目录 | `/volume1/homes/fnvtk/.persistent-chat-local/` |
|
||||
|
||||
### 部署步骤
|
||||
|
||||
```bash
|
||||
# 1. SSH 登录 NAS
|
||||
ssh fnvtk@192.168.110.101
|
||||
|
||||
# 2. 创建部署目录
|
||||
mkdir -p /volume1/homes/fnvtk/.persistent-chat-local
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
|
||||
# 3. 上传 6 个核心文件
|
||||
# (用 scp / git pull / rsync 任一方式)
|
||||
# 必需文件:server.js / hub.js / panel.html / ensure-hub.sh / mongo_sync.py / cursor-title.js
|
||||
|
||||
# 4. 设置权限
|
||||
chmod +x ensure-hub.sh
|
||||
|
||||
# 5. 启动 Hub(前台调试用)
|
||||
/usr/local/opt/node@22/bin/node hub.js &
|
||||
|
||||
# 6. 验证
|
||||
curl http://192.168.110.101:13458/api/health
|
||||
# 期望:{"ok":true,"pid":<X>,"panelVersion":"1.6.37"}
|
||||
```
|
||||
|
||||
### 后台守护(用 nohup)
|
||||
|
||||
```bash
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
nohup /usr/local/opt/node@22/bin/node hub.js > logs/hub.log 2>&1 &
|
||||
disown
|
||||
echo $! > /tmp/pchat-hub.pid
|
||||
```
|
||||
|
||||
### 系统级守护(Synology Task Scheduler)
|
||||
|
||||
1. DSM 控制面板 → 任务计划
|
||||
2. 新增 → 触发的任务 → 用户自定义脚本
|
||||
3. **事件**:开机 / 启动
|
||||
4. **用户**:root
|
||||
5. **脚本**:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
/usr/local/opt/node@22/bin/node hub.js >> /volume1/homes/fnvtk/.persistent-chat-local/logs/hub.log 2>&1 &
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 形态 B:本地备份(127.0.0.1:13458)
|
||||
|
||||
### 适用场景
|
||||
- 单机 fallback(远端挂时自动接上)
|
||||
- 开发调试(启停方便)
|
||||
- 离线工作
|
||||
|
||||
### macOS launchd 守护(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 写 plist
|
||||
cat > ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>com.karuo.persistent-chat-hub</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/opt/node@22/bin/node</string>
|
||||
<string>/Users/karuo/.persistent-chat-local/hub.js</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key><string>/Users/karuo/.persistent-chat-local</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.log</string>
|
||||
<key>StandardErrorPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# 2. 加载
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
# 3. 验证
|
||||
launchctl print gui/$UID/com.karuo.persistent-chat-hub | head -10
|
||||
curl http://127.0.0.1:13458/api/health
|
||||
```
|
||||
|
||||
### 启停命令
|
||||
|
||||
```bash
|
||||
# 启动
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
# 停止
|
||||
launchctl bootout gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
# 状态
|
||||
launchctl print gui/$UID/com.karuo.persistent-chat-hub
|
||||
|
||||
# 日志
|
||||
tail -f /Users/karuo/.persistent-chat-local/logs/launchd.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 形态 C:Trae / Cursor 插件(stdio MCP)
|
||||
|
||||
### 适用场景
|
||||
- 直接在 IDE 里用
|
||||
- 与 Hub 通过 stdio 通信(ensure-hub.sh 桥接)
|
||||
|
||||
### 部署步骤
|
||||
|
||||
```bash
|
||||
# 1. 创建本地目录
|
||||
mkdir -p ~/.persistent-chat-local
|
||||
|
||||
# 2. 拷贝核心文件
|
||||
cp src/server.js src/hub.js src/cursor-title.js ~/.persistent-chat-local/
|
||||
cp web/panel.html ~/.persistent-chat-local/panel.html
|
||||
cp scripts/ensure-hub.sh ~/.persistent-chat-local/
|
||||
chmod +x ~/.persistent-chat-local/ensure-hub.sh
|
||||
|
||||
# 3. 编辑 MCP 配置(见 mcp-config/trae.mcp.json)
|
||||
```
|
||||
|
||||
### ensure-hub.sh 原理
|
||||
|
||||
```
|
||||
MCP stdio ←→ ensure-hub.sh ←→ Hub HTTP (13458)
|
||||
```
|
||||
|
||||
- 收到 MCP 调用 → 转发到 Hub HTTP
|
||||
- Hub 持久挂着,session 不丢
|
||||
- IDE 重启不影响 Hub 状态
|
||||
|
||||
---
|
||||
|
||||
## 三形态组合
|
||||
|
||||
**推荐**:远端 NAS 为主(24×7),本地为备份(断网续命),Trae 插件为入口。
|
||||
|
||||
```
|
||||
Trae ←stdio→ ensure-hub.sh ←HTTP→ [本地 13458] (fallback)
|
||||
↑
|
||||
[远端 192.168.110.101:13458] (主)
|
||||
```
|
||||
|
||||
通过 `PCHAT_HUB_HOST` 环境变量切换。
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
部署完后挨个跑:
|
||||
|
||||
```bash
|
||||
# 1. 健康
|
||||
curl http://192.168.110.101:13458/api/health
|
||||
curl http://127.0.0.1:13458/api/health
|
||||
|
||||
# 2. session list
|
||||
curl http://192.168.110.101:13458/api/state | python3 -m json.tool
|
||||
|
||||
# 3. 面板可访问
|
||||
open http://192.168.110.101:13458/
|
||||
open http://127.0.0.1:13458/
|
||||
|
||||
# 4. Trae 插件已加载
|
||||
ps aux | grep "server.js" | grep -v grep
|
||||
# 期望:看到 /Users/karuo/.persistent-chat-local/server.js
|
||||
|
||||
# 5. 跑 init + select + wait 三步
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 升级流程
|
||||
|
||||
```bash
|
||||
# 1. 备份
|
||||
cp -r ~/.persistent-chat-local ~/.persistent-chat-local.bak.$(date +%Y%m%d)
|
||||
ssh fnvtk@192.168.110.101 "cp -r /volume1/homes/fnvtk/.persistent-chat-local /volume1/homes/fnvtk/.persistent-chat-local.bak.\$(date +%Y%m%d)"
|
||||
|
||||
# 2. 拉新代码
|
||||
cd /Users/karuo/Documents/个人/persistent-chat-plugin
|
||||
git pull
|
||||
|
||||
# 3. 覆盖核心文件(不动 data/)
|
||||
cp src/* ~/.persistent-chat-local/
|
||||
cp web/panel.html ~/.persistent-chat-local/
|
||||
|
||||
# 4. 远端同步
|
||||
scp src/* fnvtk@192.168.110.101:/volume1/homes/fnvtk/.persistent-chat-local/
|
||||
scp web/panel.html fnvtk@192.168.110.101:/volume1/homes/fnvtk/.persistent-chat-local/
|
||||
|
||||
# 5. 重启 Hub(不影响 session 文件)
|
||||
launchctl bootout gui/$UID/com.karuo.persistent-chat-hub
|
||||
launchctl bootstrap gui/$UID/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
ssh fnvtk@192.168.110.101 "pkill -f 'node hub.js' ; cd /volume1/homes/fnvtk/.persistent-chat-local && nohup /usr/local/opt/node@22/bin/node hub.js > logs/hub.log 2>&1 &"
|
||||
|
||||
# 6. Trae Cmd+Q 重开
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据备份
|
||||
|
||||
- `sessions.json`(远端/本地):所有 session 状态
|
||||
- `bindings.json`:工作区→token 绑定
|
||||
- `plan-bindings.json`:计划→token 绑定
|
||||
- `thread-bindings/`:线程→token 绑定
|
||||
- `logs/`:运行日志
|
||||
|
||||
**建议**:每天 cron 备份整个 `.persistent-chat-local` 目录到 NAS / 云盘。
|
||||
|
||||
```bash
|
||||
# cron 任务:每天 02:00 备份
|
||||
0 2 * * * tar czf ~/backups/pchat-$(date +\%Y\%m\%d).tar.gz ~/.persistent-chat-local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 症状 | 原因 | 处理 |
|
||||
|:---|:---|:---|
|
||||
| 面板打不开 | Hub 未启动 | `launchctl print gui/$UID/com.karuo.persistent-chat-hub` |
|
||||
| `Token未获取` | Trae 未重启 | `Cmd+Q` 完全退出重开 |
|
||||
| session 串台 | 多用户同 IP | 加 `hostIp` 区分 / 改 cursorTitle |
|
||||
| Hub 占 CPU | 短轮询频繁 | 改 `POLL_TIMEOUT_MS` 到 30000 |
|
||||
| 数据丢失 | 没备份 | 立刻 cron 跑起来 |
|
||||
|
||||
---
|
||||
|
||||
## 安全注意
|
||||
|
||||
- 13458 端口默认仅监听 LAN IP,**不暴露公网**
|
||||
- 跨网段访问靠 SSH 隧道或 VPN
|
||||
- 永远不要把 `127.0.0.1:13458` 暴露到 0.0.0.0
|
||||
|
||||
---
|
||||
|
||||
## 监控
|
||||
|
||||
```bash
|
||||
# Hub 状态
|
||||
watch -n 5 'curl -s http://192.168.110.101:13458/api/health | python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get(\"panelVersion\"),d.get(\"pid\"),\"sessions=\"+str(len(json.load(open(\"/dev/null\")))))" 2>/dev/null'
|
||||
|
||||
# session 数量
|
||||
curl -s http://192.168.110.101:13458/api/state | python3 -c "import json,sys;print(len(json.load(sys.stdin)['sessions']))"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-26 · 卡若AI
|
||||
189
docs/INSTALL.md
Normal file
189
docs/INSTALL.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# 安装文档(INSTALL.md)
|
||||
|
||||
> 完整安装步骤 · 三种形态
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
| 项 | 要求 | 验证 |
|
||||
|:---|:---|:---|
|
||||
| macOS | 13+ | `sw_vers` |
|
||||
| Node | v22 | `node --version` |
|
||||
| Git | 2.30+ | `git --version` |
|
||||
| 网络 | 能访问 192.168.110.101 | `ping -c1 192.168.110.101` |
|
||||
|
||||
---
|
||||
|
||||
## 安装形态 A:远端 NAS(公司 13458)
|
||||
|
||||
### 步骤 1:准备目录
|
||||
|
||||
```bash
|
||||
ssh fnvtk@192.168.110.101
|
||||
mkdir -p /volume1/homes/fnvtk/.persistent-chat-local
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
```
|
||||
|
||||
### 步骤 2:上传文件
|
||||
|
||||
从本地 git 仓库:
|
||||
|
||||
```bash
|
||||
# 本地
|
||||
scp src/{server.js,hub.js,cursor-title.js} \
|
||||
web/panel.html \
|
||||
scripts/{ensure-hub.sh,mongo_sync.py} \
|
||||
fnvtk@192.168.110.101:/volume1/homes/fnvtk/.persistent-chat-local/
|
||||
```
|
||||
|
||||
### 步骤 3:安装依赖
|
||||
|
||||
无第三方依赖(纯 Node 22 标准库)。
|
||||
|
||||
### 步骤 4:启动
|
||||
|
||||
```bash
|
||||
# 远端
|
||||
ssh fnvtk@192.168.110.101
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
chmod +x ensure-hub.sh
|
||||
nohup /usr/local/opt/node@22/bin/node hub.js > logs/hub.log 2>&1 &
|
||||
```
|
||||
|
||||
### 步骤 5:验证
|
||||
|
||||
```bash
|
||||
curl http://192.168.110.101:13458/api/health
|
||||
# 期望:{"ok":true,"pid":<X>,"panelVersion":"1.6.37"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装形态 B:本地备份(launchd)
|
||||
|
||||
### 步骤 1:建目录
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.persistent-chat-local
|
||||
```
|
||||
|
||||
### 步骤 2:拷文件
|
||||
|
||||
```bash
|
||||
cp src/server.js src/hub.js src/cursor-title.js ~/.persistent-chat-local/
|
||||
cp web/panel.html ~/.persistent-chat-local/panel.html
|
||||
cp scripts/ensure-hub.sh ~/.persistent-chat-local/
|
||||
chmod +x ~/.persistent-chat-local/ensure-hub.sh
|
||||
```
|
||||
|
||||
### 步骤 3:写 launchd plist
|
||||
|
||||
```bash
|
||||
cat > ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>com.karuo.persistent-chat-hub</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/opt/node@22/bin/node</string>
|
||||
<string>/Users/karuo/.persistent-chat-local/hub.js</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key><string>/Users/karuo/.persistent-chat-local</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.log</string>
|
||||
<key>StandardErrorPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
```
|
||||
|
||||
### 步骤 4:加载
|
||||
|
||||
```bash
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
```
|
||||
|
||||
### 步骤 5:验证
|
||||
|
||||
```bash
|
||||
launchctl print gui/$UID/com.karuo.persistent-chat-hub | head -5
|
||||
curl http://127.0.0.1:13458/api/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装形态 C:Trae/Cursor MCP 插件
|
||||
|
||||
### 步骤 1:复制 mcp.json
|
||||
|
||||
```bash
|
||||
# Trae
|
||||
cp mcp-config/trae.mcp.json ~/.trae/mcp.json
|
||||
|
||||
# Cursor
|
||||
cp mcp-config/cursor.mcp.json ~/Documents/个人/.cursor/mcp.json
|
||||
```
|
||||
|
||||
### 步骤 2:完全退出 IDE
|
||||
|
||||
- Trae: `Cmd+Q`
|
||||
- Cursor: `Cmd+Q`
|
||||
|
||||
### 步骤 3:重开 IDE
|
||||
|
||||
### 步骤 4:验证
|
||||
|
||||
在 Trae / Cursor 看到 `persistent-chat` MCP server 绿色点。
|
||||
|
||||
---
|
||||
|
||||
## 升级
|
||||
|
||||
```bash
|
||||
# 1. 拉新代码
|
||||
cd /Users/karuo/Documents/个人/persistent-chat-plugin
|
||||
git pull
|
||||
|
||||
# 2. 备份
|
||||
cp -r ~/.persistent-chat-local ~/.persistent-chat-local.bak.$(date +%Y%m%d)
|
||||
|
||||
# 3. 覆盖核心(不动 data/)
|
||||
cp src/* ~/.persistent-chat-local/
|
||||
cp web/panel.html ~/.persistent-chat-local/
|
||||
|
||||
# 4. 重启 Hub
|
||||
launchctl bootout gui/$UID/com.karuo.persistent-chat-hub
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
# 5. Trae Cmd+Q 重开
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸载
|
||||
|
||||
```bash
|
||||
# 1. 停止 launchd
|
||||
launchctl bootout gui/$UID/com.karuo.persistent-chat-hub
|
||||
|
||||
# 2. 删除文件
|
||||
rm -rf ~/.persistent-chat-local
|
||||
rm ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
|
||||
# 3. 删除 MCP 配置
|
||||
# Trae: 编辑 ~/.trae/mcp.json 删 persistent-chat 段
|
||||
# Cursor: 编辑 ~/Documents/个人/.cursor/mcp.json 删 persistent-chat 段
|
||||
|
||||
# 4. 远端 NAS
|
||||
ssh fnvtk@192.168.110.101
|
||||
pkill -f "node hub.js"
|
||||
# 不删数据可保留 ~/.persistent-chat-local 目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-26 · 卡若AI
|
||||
294
docs/MCP_SETUP.md
Normal file
294
docs/MCP_SETUP.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# MCP 配置(MCP_SETUP.md)
|
||||
|
||||
> Trae / Cursor 接入持久对话 MCP 插件的完整配置
|
||||
|
||||
---
|
||||
|
||||
## 1. Trae CN(macOS)
|
||||
|
||||
### 配置文件位置
|
||||
|
||||
```
|
||||
~/.trae/mcp.json
|
||||
```
|
||||
|
||||
### 完整配置
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"persistent-chat": {
|
||||
"type": "stdio",
|
||||
"command": "/Users/karuo/.persistent-chat-local/ensure-hub.sh",
|
||||
"args": [
|
||||
"--workspace=${workspaceFolder}"
|
||||
],
|
||||
"env": {
|
||||
"PCHAT_HTTP_PORT": "13458",
|
||||
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
|
||||
"PCHAT_HUB_HOST": "192.168.110.101",
|
||||
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost"
|
||||
},
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"wait_for_user_input",
|
||||
"init_conversation",
|
||||
"merge_conversation",
|
||||
"select_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键字段
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| `command` | `~/.persistent-chat-local/ensure-hub.sh` | 启动器,桥接 stdio ↔ HTTP |
|
||||
| `PCHAT_HUB_HOST` | `192.168.110.101` | 远端 NAS IP(主) |
|
||||
| `PCHAT_HTTP_PORT` | `13458` | Hub 端口 |
|
||||
| `PCHAT_NODE` | `/usr/local/opt/node@22/bin/node` | Node 22 路径 |
|
||||
| `NO_PROXY` | `192.168.110.101,127.0.0.1,localhost` | 绕过代理直连 |
|
||||
| `autoApprove` | 4 个工具全开 | 不弹确认框 |
|
||||
|
||||
### 加载步骤
|
||||
|
||||
1. 写入 `~/.trae/mcp.json`
|
||||
2. **Trae 必须 `Cmd+Q` 完全退出**(不是关窗)
|
||||
3. 重新打开 Trae
|
||||
4. 看 SOLO Agent 窗口的工具列表 → 出现 `persistent-chat` 4 个工具
|
||||
|
||||
### 验证
|
||||
|
||||
```bash
|
||||
# 1. 看 Trae 进程里是否有 server.js
|
||||
ps aux | grep "persistent-chat-local/server.js" | grep -v grep
|
||||
# 期望:看到 1 个进程,env 含 PCHAT_HUB_HOST=192.168.110.101
|
||||
|
||||
# 2. 在 Trae 里发:"开始测试"
|
||||
# 期望:4 工具出现 + 拿到 ct_ 开头的 token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Cursor
|
||||
|
||||
### 配置文件位置
|
||||
|
||||
```
|
||||
~/Documents/个人/.cursor/mcp.json
|
||||
```
|
||||
|
||||
### 完整配置(与 Trae 相同)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"persistent-chat": {
|
||||
"type": "stdio",
|
||||
"command": "/Users/karuo/.persistent-chat-local/ensure-hub.sh",
|
||||
"args": ["--workspace=${workspaceFolder}"],
|
||||
"env": {
|
||||
"PCHAT_HTTP_PORT": "13458",
|
||||
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
|
||||
"PCHAT_HUB_HOST": "192.168.110.101",
|
||||
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost"
|
||||
},
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"wait_for_user_input",
|
||||
"init_conversation",
|
||||
"merge_conversation",
|
||||
"select_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 加载步骤
|
||||
|
||||
1. 写入 `~/Documents/个人/.cursor/mcp.json`
|
||||
2. Cursor → 设置 → MCP → Refresh
|
||||
3. 看 `persistent-chat` 是否绿色点
|
||||
4. 面板访问:Ctrl+L → 输入 `/mcp`
|
||||
|
||||
---
|
||||
|
||||
## 3. 多 IDE 共存
|
||||
|
||||
### 场景
|
||||
- 同时开 Trae 和 Cursor,都想用持久对话
|
||||
- 共享同一个 Hub(远端 NAS)
|
||||
|
||||
### 配置要点
|
||||
|
||||
**两边的 mcp.json 保持一致**,都指向同一个 Hub:
|
||||
|
||||
```json
|
||||
"PCHAT_HUB_HOST": "192.168.110.101"
|
||||
```
|
||||
|
||||
**垂直绑定隔离**:
|
||||
- 不同工作区 → 不同 session
|
||||
- 不同 Cursor 标签 → 不同 session
|
||||
- 同工作区 + 同标签 → 复用 session
|
||||
|
||||
**冲突场景**:
|
||||
- Trae 标签 = "学习" + Cursor 标签 = "学习" → 互窜(不推荐)
|
||||
- 建议:Trae 用 "Trae_xxx",Cursor 用 "Cursor_xxx"
|
||||
|
||||
---
|
||||
|
||||
## 4. 双 Hub 配置(远端 + 本地)
|
||||
|
||||
### 场景
|
||||
- 远端 NAS 24×7 在
|
||||
- 本地 launchd 守护备份
|
||||
- 远端挂时自动 fallback
|
||||
|
||||
### 方案:脚本切换
|
||||
|
||||
**ensure-hub.sh 增强版**(可加 fallback):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# /Users/karuo/.persistent-chat-local/ensure-hub.sh
|
||||
HUB_HOST="${PCHAT_HUB_HOST:-127.0.0.1}"
|
||||
# 测试主 Hub
|
||||
if ! curl -sf "http://${HUB_HOST}:13458/api/health" -m 2 > /dev/null; then
|
||||
# 失败,fallback 到本地
|
||||
HUB_HOST="127.0.0.1"
|
||||
fi
|
||||
export PCHAT_HUB_HOST="$HUB_HOST"
|
||||
exec /usr/local/opt/node@22/bin/node \
|
||||
/Users/karuo/.persistent-chat-local/server.js \
|
||||
--workspace="${1#--workspace=}"
|
||||
```
|
||||
|
||||
这样远端挂时自动回退到本地(launchd 守护一直跑)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 工具参数详解
|
||||
|
||||
### 5.1 init_conversation
|
||||
|
||||
```typescript
|
||||
{
|
||||
token?: string; // 已有 token(merge 用)
|
||||
cursorTitle: string; // 必填:Cursor 标签
|
||||
newPlan?: boolean; // true=换计划,归档旧 ct_
|
||||
planLabel?: string; // 计划名(newPlan 时必填)
|
||||
workspace?: string; // 工作区路径
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```json
|
||||
{
|
||||
"Conversation initialized. pchat-token: ct_xxxx"
|
||||
"🆕 新计划线程:ct_xxxx"
|
||||
"[BINDING_AUTO_NEW] 新对话已自动绑定"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 select_conversation
|
||||
|
||||
```typescript
|
||||
{
|
||||
token: string; // 必填:要复用的 ct_
|
||||
cursorTitle: string; // 必填:当前 Cursor 标签
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```json
|
||||
{
|
||||
"[BINDING_REUSE]"
|
||||
"🔄 恢复对话: ct_xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 wait_for_user_input
|
||||
|
||||
```typescript
|
||||
{
|
||||
token: string; // 必填
|
||||
message: string; // 提示用户的话
|
||||
timeout?: number; // 最长等多久(ms),默认 60000
|
||||
renewalOnly?: boolean; // true=只续约不等消息
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```json
|
||||
{
|
||||
"<用户发的消息 或 '保活续跑' 默认文本>"
|
||||
"[pchat-token: ct_xxxx]"
|
||||
"校验#N"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 merge_conversation
|
||||
|
||||
```typescript
|
||||
{
|
||||
fromToken: string; // 源 token
|
||||
toToken: string; // 目标 token
|
||||
cursorTitle: string; // 当前标签
|
||||
}
|
||||
```
|
||||
|
||||
**用途**:把两个 plan 的上下文合并。
|
||||
|
||||
---
|
||||
|
||||
## 6. 完整对话循环示例
|
||||
|
||||
```javascript
|
||||
// 1. 起新计划
|
||||
const init = await mcp.call('init_conversation', {
|
||||
cursorTitle: '持久对话永久续跑-20260626',
|
||||
newPlan: true,
|
||||
planLabel: '永久续跑三段式',
|
||||
});
|
||||
const token = extractToken(init); // ct_xxxx
|
||||
|
||||
// 2. 干活(可任意调用其他工具)
|
||||
// ...
|
||||
|
||||
// 3. 真挂起等用户
|
||||
const reply = await mcp.call('wait_for_user_input', {
|
||||
token,
|
||||
message: '【真挂起】请发任务或"继续"',
|
||||
timeout: 1800000, // 30 分钟
|
||||
});
|
||||
// 收到用户消息 → reply.text
|
||||
|
||||
// 4. 复用旧 session
|
||||
await mcp.call('select_conversation', {
|
||||
token,
|
||||
cursorTitle: '持久对话永久续跑-20260626',
|
||||
});
|
||||
|
||||
// 5. 循环到用户说"结束持久对话"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见配置错误
|
||||
|
||||
| 错误 | 现象 | 修复 |
|
||||
|:---|:---|:---|
|
||||
| `PCHAT_NODE` 路径错 | `node not found` | `which node` 改 |
|
||||
| `PCHAT_HUB_HOST` 用 0.0.0.0 | 端口冲突 | 改成具体 IP |
|
||||
| `NO_PROXY` 缺 | 代理拦截 | 加 `192.168.110.101` |
|
||||
| `autoApprove` 缺 | 每次弹窗 | 加 4 个工具名 |
|
||||
| Trae 没重启 | MCP 不加载 | `Cmd+Q` 退出 |
|
||||
| Cursor 没 refresh | 看不到 | 设置 → MCP → Refresh |
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-26 · 卡若AI
|
||||
161
docs/QUICKSTART.md
Normal file
161
docs/QUICKSTART.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 5 分钟上手(QUICKSTART.md)
|
||||
|
||||
> 假设你的工作环境是 macOS + Trae CN + 公司 NAS(192.168.110.101)
|
||||
> 跟着抄,5 分钟内跑起来。
|
||||
|
||||
---
|
||||
|
||||
## Step 1 · 装到本地(30s)
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.persistent-chat-local
|
||||
|
||||
# 从仓库拷核心文件
|
||||
git clone http://192.168.110.101:3000/fnvtk/persistent-chat-plugin.git /tmp/pchat
|
||||
cp /tmp/pchat/src/* ~/.persistent-chat-local/
|
||||
cp /tmp/pchat/web/panel.html ~/.persistent-chat-local/
|
||||
cp /tmp/pchat/scripts/ensure-hub.sh ~/.persistent-chat-local/
|
||||
chmod +x ~/.persistent-chat-local/ensure-hub.sh
|
||||
```
|
||||
|
||||
## Step 2 · 写 MCP 配置(30s)
|
||||
|
||||
```bash
|
||||
cat > ~/.trae/mcp.json <<'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"persistent-chat": {
|
||||
"type": "stdio",
|
||||
"command": "/Users/karuo/.persistent-chat-local/ensure-hub.sh",
|
||||
"args": ["--workspace=${workspaceFolder}"],
|
||||
"env": {
|
||||
"PCHAT_HTTP_PORT": "13458",
|
||||
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
|
||||
"PCHAT_HUB_HOST": "192.168.110.101",
|
||||
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost"
|
||||
},
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"wait_for_user_input",
|
||||
"init_conversation",
|
||||
"merge_conversation",
|
||||
"select_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
## Step 3 · 配 Hub(远端 NAS,已有则跳过)
|
||||
|
||||
在 NAS 上(ssh fnvtk@192.168.110.101):
|
||||
|
||||
```bash
|
||||
mkdir -p /volume1/homes/fnvtk/.persistent-chat-local
|
||||
cd /volume1/homes/fnvtk/.persistent-chat-local
|
||||
# scp 把 src/* + web/panel.html 拷过来
|
||||
chmod +x ensure-hub.sh
|
||||
|
||||
# 启动
|
||||
nohup /usr/local/opt/node@22/bin/node hub.js > logs/hub.log 2>&1 &
|
||||
disown
|
||||
```
|
||||
|
||||
## Step 4 · 配本地备份 Hub(30s)
|
||||
|
||||
```bash
|
||||
cat > ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>com.karuo.persistent-chat-hub</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/opt/node@22/bin/node</string>
|
||||
<string>/Users/karuo/.persistent-chat-local/hub.js</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key><string>/Users/karuo/.persistent-chat-local</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.log</string>
|
||||
<key>StandardErrorPath</key><string>/Users/karuo/.persistent-chat-local/logs/launchd.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.karuo.persistent-chat-hub.plist
|
||||
```
|
||||
|
||||
## Step 5 · Trae Cmd+Q 重开(10s)
|
||||
|
||||
完全退出 Trae(不是关窗),重开。
|
||||
|
||||
## Step 6 · 验证(30s)
|
||||
|
||||
打开 Trae → SOLO Agent 窗口 → 看到 4 个工具:
|
||||
|
||||
```
|
||||
✓ init_conversation
|
||||
✓ select_conversation
|
||||
✓ wait_for_user_input
|
||||
✓ merge_conversation
|
||||
```
|
||||
|
||||
发:"**开始测试**"
|
||||
|
||||
期望:
|
||||
1. 看到 `init_conversation` 调出
|
||||
2. 拿到 `ct_xxxx` 的 token
|
||||
3. 收到 [卡若复盘] 格式回复
|
||||
4. Agent 继续 wait
|
||||
|
||||
## Step 7 · 跑三段式(持续)
|
||||
|
||||
每次用户发新消息:
|
||||
|
||||
```
|
||||
1. select_conversation(token=ct_xxx) ← 接回旧 session
|
||||
2. 执行用户任务
|
||||
3. 写 [卡若复盘] 🎯📌💡📝▶
|
||||
4. wait_for_user_input(token=ct_xxx, message=提示)
|
||||
5. 循环
|
||||
```
|
||||
|
||||
## Step 8 · 结束(仅在用户说"结束持久对话"时)
|
||||
|
||||
```javascript
|
||||
// 用户说"结束持久对话"后
|
||||
await mcp.call('merge_conversation', {
|
||||
fromToken: 'ct_xxx',
|
||||
toToken: 'ct_归档', // 或留空 = 纯归档
|
||||
cursorTitle: '当前标签',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 完成
|
||||
|
||||
你现在有:
|
||||
- ✅ 远端 NAS Hub 24×7 在
|
||||
- ✅ 本地备份 Hub(挂了自动起)
|
||||
- ✅ Trae 插件 4 工具全可用
|
||||
- ✅ 持久对话循环(不主动结束)
|
||||
|
||||
---
|
||||
|
||||
## 🆘 卡住看这里
|
||||
|
||||
| 现象 | 一行修 |
|
||||
|:---|:---|
|
||||
| Trae 看不到 4 工具 | `Cmd+Q` 重开 |
|
||||
| Hub 启动失败 | `node hub.js` 前台跑看错 |
|
||||
| 提示 Token未获取 | Trae 没重启 |
|
||||
| 任务完成 = 对话死 | 不是,Hub 仍 holding,下一条进来自动续 |
|
||||
| wait 不返回 | 真挂起模式生效,等你发消息 |
|
||||
|
||||
---
|
||||
|
||||
> 详细见 [DEPLOY.md](DEPLOY.md) / [MCP_SETUP.md](MCP_SETUP.md) / [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
37
mcp-config/cursor.mcp.json
Normal file
37
mcp-config/cursor.mcp.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codegraph": {
|
||||
"type": "stdio",
|
||||
"command": "/Users/karuo/Documents/个人/卡若AI/04_卡火(火)/火种_知识模型/CodeGraph_MCP语义代码图谱/脚本/codegraph_mcp_serve.sh",
|
||||
"args": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"codegraph_search",
|
||||
"codegraph_callers",
|
||||
"codegraph_callees",
|
||||
"codegraph_impact",
|
||||
"codegraph_node",
|
||||
"codegraph_context",
|
||||
"codegraph_explore",
|
||||
"codegraph_files",
|
||||
"codegraph_status"
|
||||
]
|
||||
},
|
||||
"persistent-chat": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/karuo/.cursor-loop/bridge.js",
|
||||
"--workspace=/Users/karuo/Documents/个人"
|
||||
],
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"wait_for_user_input",
|
||||
"init_conversation",
|
||||
"merge_conversation",
|
||||
"select_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
mcp-config/trae.mcp.json
Normal file
25
mcp-config/trae.mcp.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"persistent-chat": {
|
||||
"type": "stdio",
|
||||
"command": "/Users/karuo/.persistent-chat-local/ensure-hub.sh",
|
||||
"args": [
|
||||
"--workspace=${workspaceFolder}"
|
||||
],
|
||||
"env": {
|
||||
"PCHAT_HTTP_PORT": "13458",
|
||||
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
|
||||
"PCHAT_HUB_HOST": "192.168.110.101",
|
||||
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost",
|
||||
"no_proxy": "192.168.110.101,127.0.0.1,localhost"
|
||||
},
|
||||
"disabled": false,
|
||||
"autoApprove": [
|
||||
"wait_for_user_input",
|
||||
"init_conversation",
|
||||
"merge_conversation",
|
||||
"select_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
58
scripts/ensure-hub.sh
Executable file
58
scripts/ensure-hub.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# 启动 MCP 前确保 Hub 在 13458 运行(全局唯一)
|
||||
set -eo pipefail
|
||||
INSTALL="$(cd "$(dirname "$0")" && pwd)"
|
||||
PORT="${PCHAT_HTTP_PORT:-13458}"
|
||||
LOG="$INSTALL/logs/hub.log"
|
||||
NODE="${PCHAT_NODE:-$(command -v node 2>/dev/null || true)}"
|
||||
[[ -z "$NODE" ]] && NODE="/usr/local/opt/node@22/bin/node"
|
||||
|
||||
mkdir -p "$INSTALL/logs"
|
||||
|
||||
if [[ ! -x "$NODE" ]]; then
|
||||
echo "[ensure-hub] FATAL: node not found: $NODE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
hub_health() {
|
||||
curl -sf -m 2 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_hub() {
|
||||
if hub_health; then return 0; fi
|
||||
echo "[ensure-hub] starting hub on :${PORT}" >> "$LOG"
|
||||
nohup "$NODE" "$INSTALL/hub.js" >> "$INSTALL/logs/hub.stdout.log" 2>&1 &
|
||||
local i=0
|
||||
while [[ $i -lt 80 ]]; do
|
||||
hub_health && return 0
|
||||
sleep 0.15
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! start_hub; then
|
||||
echo "[ensure-hub] FATAL: Hub not ready on 127.0.0.1:${PORT} (waited ~12s). Run: bash 卡若AI/.../Cursor持久对话/脚本/sync-runtime.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cursor 无工作区时 ${workspaceFolder} 可能为空/none/未替换字面量 → 兜底 HOME
|
||||
ARGS=()
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--workspace=|--workspace=none|--workspace=\$\{workspaceFolder\})
|
||||
ARGS+=("--workspace=$HOME")
|
||||
;;
|
||||
--workspace=*)
|
||||
ARGS+=("$a")
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$a")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#ARGS[@]} -eq 0 ]] || ! printf '%s\n' "${ARGS[@]}" | grep -q '^--workspace='; then
|
||||
ARGS+=("--workspace=${PWD:-$HOME}")
|
||||
fi
|
||||
|
||||
exec "$NODE" "$INSTALL/server.js" "${ARGS[@]}"
|
||||
52
scripts/mongo_sync.py
Executable file
52
scripts/mongo_sync.py
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""持久对话会话 → MongoDB karuo_site.持久对话会话(本地 ~/.persistent-chat-local/sessions.json)"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HOME = os.path.expanduser("~")
|
||||
SESSIONS_FILE = os.path.join(HOME, ".persistent-chat-local", "sessions.json")
|
||||
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://127.0.0.1:27017")
|
||||
DB_NAME = os.environ.get("MONGO_DB", "karuo_site")
|
||||
COLL_NAME = "持久对话会话"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not os.path.isfile(SESSIONS_FILE):
|
||||
print("SKIP: no sessions.json")
|
||||
return 0
|
||||
try:
|
||||
from pymongo import MongoClient
|
||||
except ImportError:
|
||||
print("SKIP: pymongo not installed (pip3 install pymongo)")
|
||||
return 0
|
||||
|
||||
with open(SESSIONS_FILE, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=4000)
|
||||
client.admin.command("ping")
|
||||
col = client[DB_NAME][COLL_NAME]
|
||||
col.create_index("token", unique=True)
|
||||
col.create_index("workspace")
|
||||
col.create_index("lastActiveAt")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
n = 0
|
||||
for token, sess in data.items():
|
||||
doc = dict(sess)
|
||||
doc["token"] = token
|
||||
doc["mongo_sync_at"] = now
|
||||
col.update_one({"token": token}, {"$set": doc}, upsert=True)
|
||||
n += 1
|
||||
print(f"OK: synced {n} sessions → {DB_NAME}.{COLL_NAME}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as e:
|
||||
print(f"FAIL: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
141
src/cursor-title.js
Executable file
141
src/cursor-title.js
Executable file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const VSCDB = path.join(
|
||||
HOME,
|
||||
'Library/Application Support/Cursor/User/globalStorage/state.vscdb'
|
||||
);
|
||||
const LOOP_DIR = path.join(HOME, '.cursor-loop');
|
||||
const ACTIVE_VERTICAL = path.join(LOOP_DIR, 'active-vertical.json');
|
||||
|
||||
function normTitle(title) {
|
||||
const t = String(title || '').trim();
|
||||
return t || '(未命名标签)';
|
||||
}
|
||||
|
||||
function readActiveVertical() {
|
||||
try {
|
||||
const raw = fs.readFileSync(ACTIVE_VERTICAL, 'utf8');
|
||||
const j = JSON.parse(raw);
|
||||
if (j && j.title) return j;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeActiveVertical(payload) {
|
||||
try {
|
||||
fs.mkdirSync(LOOP_DIR, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
ACTIVE_VERTICAL,
|
||||
JSON.stringify({ ...payload, updatedAt: Date.now() }, null, 2)
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function queryVscdb(sql) {
|
||||
if (!fs.existsSync(VSCDB)) return null;
|
||||
try {
|
||||
return execFileSync('sqlite3', [VSCDB, sql], { encoding: 'utf8', timeout: 3000 }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadComposerHeaders() {
|
||||
const raw = queryVscdb(
|
||||
"SELECT value FROM ItemTable WHERE key='composer.composerHeaders' LIMIT 1;"
|
||||
);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pickComposer(headers, opts = {}) {
|
||||
const list = headers?.allComposers || [];
|
||||
const composerId =
|
||||
opts.composerId ||
|
||||
process.env.CURSOR_CONVERSATION_ID ||
|
||||
process.env.CURSOR_CHAT_ID ||
|
||||
process.env.COMPOSER_ID ||
|
||||
readActiveVertical()?.composerId;
|
||||
|
||||
if (composerId) {
|
||||
const hit = list.find((c) => c.composerId === composerId);
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
const wsHint = String(opts.workspace || '').replace(/\/+$/, '');
|
||||
const candidates = list.filter((c) => c.type === 'head' && !c.isArchived);
|
||||
if (wsHint) {
|
||||
const wsMatch = candidates.filter((c) => {
|
||||
const cfg = c.workspaceIdentifier?.configPath;
|
||||
const fp = cfg?.fsPath || cfg?.path || '';
|
||||
return fp && (wsHint.includes(fp) || fp.includes(wsHint));
|
||||
});
|
||||
if (wsMatch.length) {
|
||||
wsMatch.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
||||
return wsMatch[0];
|
||||
}
|
||||
}
|
||||
candidates.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
||||
return candidates[0] || null;
|
||||
}
|
||||
|
||||
/** 解析当前 Cursor Agent 标签名(真源:args > threadLock > active-vertical > vscdb) */
|
||||
function resolveCursorTabTitle(opts = {}) {
|
||||
const fromArgs = opts.cursorTitle || opts.title;
|
||||
if (fromArgs && normTitle(fromArgs) !== '(未命名标签)') {
|
||||
return { title: normTitle(fromArgs), source: 'args', composerId: opts.composerId || null };
|
||||
}
|
||||
|
||||
const active = readActiveVertical();
|
||||
if (active?.title && normTitle(active.title) !== '(未命名标签)') {
|
||||
const age = Date.now() - (active.updatedAt || 0);
|
||||
if (age < 30 * 60 * 1000) {
|
||||
return {
|
||||
title: normTitle(active.title),
|
||||
source: 'active-vertical',
|
||||
composerId: active.composerId || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const envTitle = process.env.CURSOR_CHAT_TITLE || process.env.PCHAT_CURSOR_TITLE;
|
||||
if (envTitle) {
|
||||
return { title: normTitle(envTitle), source: 'env', composerId: null };
|
||||
}
|
||||
|
||||
const headers = loadComposerHeaders();
|
||||
const composer = pickComposer(headers, opts);
|
||||
if (composer?.name) {
|
||||
const payload = {
|
||||
composerId: composer.composerId,
|
||||
title: normTitle(composer.name),
|
||||
workspace: composer.workspaceIdentifier?.configPath?.fsPath || opts.workspace || '',
|
||||
};
|
||||
writeActiveVertical(payload);
|
||||
return {
|
||||
title: payload.title,
|
||||
source: 'vscdb',
|
||||
composerId: composer.composerId,
|
||||
};
|
||||
}
|
||||
|
||||
return { title: '', source: 'missing', composerId: null };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveCursorTabTitle,
|
||||
writeActiveVertical,
|
||||
readActiveVertical,
|
||||
ACTIVE_VERTICAL,
|
||||
normTitle,
|
||||
};
|
||||
2386
src/hub.js
Executable file
2386
src/hub.js
Executable file
File diff suppressed because it is too large
Load Diff
1129
src/server.js
Executable file
1129
src/server.js
Executable file
File diff suppressed because it is too large
Load Diff
1743
web/panel.html
Executable file
1743
web/panel.html
Executable file
File diff suppressed because it is too large
Load Diff
1743
web/panel.html.local
Executable file
1743
web/panel.html.local
Executable file
File diff suppressed because it is too large
Load Diff
1744
web/panel.remote.html
Normal file
1744
web/panel.remote.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user