@bastdewfn/cc-remote 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/1.jpg +0 -0
- package/2.jpg +0 -0
- package/README.md +150 -0
- package/bin/cc-remote.js +183 -0
- package/commands/cc-remote-close.md +5 -0
- package/commands/cc-remote-doctor.md +20 -0
- package/commands/cc-remote-open.md +5 -0
- package/config.example.json +10 -0
- package/dist/cli.js +313 -0
- package/dist/commands.js +122 -0
- package/dist/config-setup.js +366 -0
- package/dist/core.js +453 -0
- package/dist/engine/events.js +78 -0
- package/dist/feishu/cards/base.js +114 -0
- package/dist/feishu/cards/help.js +33 -0
- package/dist/feishu/cards/live.js +65 -0
- package/dist/feishu/cards/session.js +59 -0
- package/dist/feishu/cards/status.js +60 -0
- package/dist/feishu/client.js +174 -0
- package/dist/feishu/replier.js +143 -0
- package/dist/feishu/router.js +61 -0
- package/dist/feishu-bot.js +139 -0
- package/dist/feishu-mode.js +62 -0
- package/dist/index.js +62 -0
- package/dist/main.js +397 -0
- package/dist/port.js +79 -0
- package/dist/preload.js +41 -0
- package/dist/pty.js +23 -0
- package/dist/relay.js +851 -0
- package/dist/renderer.js +318 -0
- package/dist/weixin/api.js +328 -0
- package/dist/weixin/client.js +136 -0
- package/dist/weixin/replier.js +73 -0
- package/dist/weixin/types.js +10 -0
- package/index.html +32 -0
- package/package.json +85 -0
- package/scripts/patch-7za.js +94 -0
package/1.jpg
ADDED
|
Binary file
|
package/2.jpg
ADDED
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# cc-remote
|
|
2
|
+
 [![npm]](https://www.npmjs.com/package/@bastdewfn/cc-remote)
|
|
3
|
+
|
|
4
|
+
远程控制 Claude Code 的桌面终端,支持飞书和微信双频道接入。
|
|
5
|
+
有问题可联系邮箱else-love@qq.com
|
|
6
|
+
|
|
7
|
+
## 系统
|
|
8
|
+
windows
|
|
9
|
+
mac
|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- **飞书频道** — WebSocket 接收消息,卡片式工具审批,回复自动推送
|
|
14
|
+
- **微信频道** — 长轮询接收消息,文字式工具审批,扫码登录
|
|
15
|
+
- **工具审批** — Claude 执行高风险工具前需用户确认,支持始终允许
|
|
16
|
+
- 同一时间暂只支持一个项目会话,期待下个版本
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
npm i @bastdewfn/cc-remote
|
|
22
|
+
|
|
23
|
+
# 全局安装
|
|
24
|
+
npm install -g @bastdewfn/cc-remote
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
配置频道 命令
|
|
31
|
+
```bash
|
|
32
|
+
cc-remote config
|
|
33
|
+
```
|
|
34
|
+
### 首次启动可能会下载 Electron 如果失败,请切换海外网络
|
|
35
|
+
|
|
36
|
+
#### 选择飞书或微信,按提示录入凭据
|
|
37
|
+
#### 飞书:App ID / App Secret / Relay Port / claudePath
|
|
38
|
+
#### 微信:扫码登录 / Relay Port / claudePath /会出现二维码
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
| 字段 | 说明 |
|
|
42
|
+
|------|------|
|
|
43
|
+
| `claudePath` | Claude 可执行文件路径,留空则用 PATH 中的 `claude`(macOS)或 `claude.exe`(Windows) |
|
|
44
|
+
| `Relay Port ` | 本机服务端口默认19200 |
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#### *飞书需申请机器人,配置事件回调,较麻烦,但交互体验好* 配置 https://open.feishu.cn/document/develop-an-echo-bot/faq
|
|
48
|
+
### 微信在入门快,但体验一般。 在我的微信里 ,设置->插件->微信ClawBot->详情->扫码
|
|
49
|
+
|
|
50
|
+
## 启动
|
|
51
|
+
在项目目录下 运行命令
|
|
52
|
+
```base
|
|
53
|
+
cc-remote
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
配置保存在 `~/.cc-remote/config.json`。微信凭据保存在 `~/.cc-remote/weixin/accounts/`。
|
|
57
|
+
|
|
58
|
+
## 命令
|
|
59
|
+
|
|
60
|
+
| 命令 | 说明 |
|
|
61
|
+
|------|------|
|
|
62
|
+
| `cc-remote config` | 频道配置向导 |
|
|
63
|
+
| `cc-remote` | 在当前目录启动 |
|
|
64
|
+
| `cc-remote --project <path>` | 指定目录启动 |
|
|
65
|
+
| `cc-remote cli` | 纯终端启动(备用) |
|
|
66
|
+
|
|
67
|
+
Claude ,开启远程接管,会话内命令(需先进入 remote 模式):
|
|
68
|
+
|
|
69
|
+
| 命令 | 说明 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| `/cc-remote-open` | 进入远程模式,消息通过频道收发 |
|
|
72
|
+
| `/cc-remote-close` | 退出远程模式,恢复本地交互 |
|
|
73
|
+
| `/cc-remote-doctor` | 运行诊断,检查连接状态 |
|
|
74
|
+
|
|
75
|
+
飞书/微信快捷命令(手机键盘无法直接输入方向键等控制键时使用):
|
|
76
|
+
|
|
77
|
+
| 命令 | 说明 |
|
|
78
|
+
|------|------|
|
|
79
|
+
| `/up` `/上` | 上箭头 ↑ |
|
|
80
|
+
| `/down` `/下` | 下箭头 ↓ |
|
|
81
|
+
| `/left` `/左` | 左箭头 ← |
|
|
82
|
+
| `/right` `/右` | 右箭头 → |
|
|
83
|
+
| `/enter` `/回车` | 回车 |
|
|
84
|
+
| `/esc` `/取消` | Esc |
|
|
85
|
+
| `/切换` | Shift+Tab(切换焦点) |
|
|
86
|
+
| `/space` `/空格` | 空格 |
|
|
87
|
+
| `/snapshot` `/快照` | 截取终端内容(分隔符之后)发送到频道 |
|
|
88
|
+
| `/快照15` | 截取最后 15 行发送到频道 |
|
|
89
|
+
|
|
90
|
+
## 配置
|
|
91
|
+
|
|
92
|
+
`~/.cc-remote/config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"appId": "飞书应用 App ID",
|
|
97
|
+
"appSecret": "飞书应用 App Secret",
|
|
98
|
+
"openId": "机器人 open_id(可选)",
|
|
99
|
+
"relayPort": 19200,
|
|
100
|
+
"approvalTimeoutS": 1800,
|
|
101
|
+
"claudePath": "",
|
|
102
|
+
"enableFeishu": true,
|
|
103
|
+
"enableWeixin": false
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## 工具审批
|
|
109
|
+
|
|
110
|
+
remote 模式下,Claude 调用工具时会触发审批:
|
|
111
|
+
|
|
112
|
+
**飞书** — 卡片按钮:
|
|
113
|
+
- ✅ 允许 — 本次放行
|
|
114
|
+
- ❌ 拒绝 — 阻止执行
|
|
115
|
+
- 🟢 始终允许 — 记住规则,永久放行
|
|
116
|
+
|
|
117
|
+
**微信** — 文字回复:
|
|
118
|
+
- `go` — 允许
|
|
119
|
+
- `no` — 拒绝
|
|
120
|
+
- `ok` — 始终允许
|
|
121
|
+
|
|
122
|
+
只读工具(Read / Glob / Grep / WebSearch)自动放行,无需审批。
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+

|
|
127
|
+
|
|
128
|
+
## 项目结构
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
├── bin/cc-remote.js # CJS 启动器 (config 向导 + Electron spawn)
|
|
132
|
+
├── src/
|
|
133
|
+
│ ├── main.ts # Electron 主进程: 窗口、PTY、Relay、Hook
|
|
134
|
+
│ ├── cli.ts # 无头 CLI 模式
|
|
135
|
+
│ ├── renderer.ts # xterm.js 终端 UI
|
|
136
|
+
│ ├── config-setup.ts # 频道配置向导
|
|
137
|
+
│ ├── feishu/ # 飞书: WebSocket 客户端、消息发送、卡片
|
|
138
|
+
│ ├── weixin/ # 微信: 长轮询客户端、API、QR 登录
|
|
139
|
+
│ └── hooks/ # PreToolUse / Stop Hook 脚本
|
|
140
|
+
├── commands/ # Claude 命令定义
|
|
141
|
+
└── dist/ # 编译输出
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 依赖
|
|
145
|
+
|
|
146
|
+
- Electron — 桌面窗口
|
|
147
|
+
- xterm.js + node-pty — 终端模拟
|
|
148
|
+
- @larksuiteoapi/node-sdk — 飞书 API
|
|
149
|
+
- @tencent-weixin/openclaw-weixin — 微信协议
|
|
150
|
+
- qrcode-terminal — 终端二维码
|
package/bin/cc-remote.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cc-remote launcher — handles subcommands, then spawns Electron
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const sub = args[0];
|
|
11
|
+
|
|
12
|
+
// ---- Helpers ----
|
|
13
|
+
function getRelayPort() {
|
|
14
|
+
let port = 19200;
|
|
15
|
+
try {
|
|
16
|
+
const cfgPath = path.join(os.homedir(), '.cc-remote', 'config.json');
|
|
17
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
18
|
+
if (cfg.relayPort > 0) port = cfg.relayPort;
|
|
19
|
+
} catch {}
|
|
20
|
+
return port;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function injectProject(stdinData) {
|
|
24
|
+
try {
|
|
25
|
+
const obj = JSON.parse(stdinData || '{}');
|
|
26
|
+
obj.project = process.cwd();
|
|
27
|
+
return JSON.stringify(obj);
|
|
28
|
+
} catch {
|
|
29
|
+
return JSON.stringify({ project: process.cwd() });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---- pre-tool-use: forward hook stdin to relay ----
|
|
34
|
+
if (sub === 'pre-tool-use') {
|
|
35
|
+
let stdinData = '';
|
|
36
|
+
process.stdin.setEncoding('utf-8');
|
|
37
|
+
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
38
|
+
process.stdin.on('end', () => {
|
|
39
|
+
const body = injectProject(stdinData);
|
|
40
|
+
const req = http.request({
|
|
41
|
+
hostname: '127.0.0.1',
|
|
42
|
+
port: getRelayPort(),
|
|
43
|
+
path: '/hooks/pre-tool-use',
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Content-Length': Buffer.byteLength(body),
|
|
48
|
+
},
|
|
49
|
+
timeout: 600000,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
req.on('error', () => { process.exit(0); });
|
|
53
|
+
req.on('response', (res) => {
|
|
54
|
+
let respBody = '';
|
|
55
|
+
res.on('data', (chunk) => { respBody += chunk; });
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
if (respBody) process.stdout.write(respBody);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
req.write(body);
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- stop-notify: forward hook stdin to relay ----
|
|
68
|
+
if (sub === 'stop-notify') {
|
|
69
|
+
let stdinData = '';
|
|
70
|
+
process.stdin.setEncoding('utf-8');
|
|
71
|
+
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
72
|
+
process.stdin.on('end', () => {
|
|
73
|
+
const body = injectProject(stdinData);
|
|
74
|
+
const req = http.request({
|
|
75
|
+
hostname: '127.0.0.1',
|
|
76
|
+
port: getRelayPort(),
|
|
77
|
+
path: '/hooks/stop-notify',
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Content-Length': Buffer.byteLength(body),
|
|
82
|
+
},
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('error', () => { process.exit(0); });
|
|
87
|
+
req.on('response', () => { process.exit(0); });
|
|
88
|
+
req.write(body);
|
|
89
|
+
req.end();
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- mode: switch feishu remote/close mode ----
|
|
95
|
+
if (sub === 'mode') {
|
|
96
|
+
const modeIdx = args.indexOf('--remote');
|
|
97
|
+
const isRemote = modeIdx >= 0;
|
|
98
|
+
const isClose = args.indexOf('--close') >= 0;
|
|
99
|
+
if (!isRemote && !isClose) {
|
|
100
|
+
console.error('用法: cc-remote mode --remote | --close');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const body = JSON.stringify({ mode: isRemote ? 'remote' : 'close', project: process.cwd() });
|
|
104
|
+
const req = http.request({
|
|
105
|
+
hostname: '127.0.0.1',
|
|
106
|
+
port: getRelayPort(),
|
|
107
|
+
path: '/feishu-mode',
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
'Content-Length': Buffer.byteLength(body),
|
|
112
|
+
},
|
|
113
|
+
timeout: 5000,
|
|
114
|
+
});
|
|
115
|
+
req.on('error', () => { process.exit(1); });
|
|
116
|
+
req.on('response', (res) => {
|
|
117
|
+
let respBody = '';
|
|
118
|
+
res.on('data', (chunk) => { respBody += chunk; });
|
|
119
|
+
res.on('end', () => {
|
|
120
|
+
if (respBody) process.stdout.write(respBody);
|
|
121
|
+
process.exit(res.statusCode >= 400 ? 1 : 0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
req.write(body);
|
|
125
|
+
req.end();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- cli: headless CLI mode (no Electron) ----
|
|
130
|
+
if (sub === 'cli') {
|
|
131
|
+
const cliPath = path.join(__dirname, '..', 'dist', 'cli.js');
|
|
132
|
+
if (!fs.existsSync(cliPath)) {
|
|
133
|
+
console.error('错误: 未找到 dist/cli.js,请先运行 npm run build');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
// Strip 'cli' from argv so the cli module sees the remaining args
|
|
137
|
+
process.argv.splice(2, 1);
|
|
138
|
+
require(cliPath);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- config: interactive setup wizard ----
|
|
143
|
+
const isConfigCmd = sub === 'config';
|
|
144
|
+
|
|
145
|
+
// Remove subcommand from args passed to Electron
|
|
146
|
+
const electronArgs = isConfigCmd ? args.slice(1) : args;
|
|
147
|
+
|
|
148
|
+
const appDir = path.join(__dirname, '..');
|
|
149
|
+
const mainFile = path.join(appDir, 'dist', 'main.js');
|
|
150
|
+
|
|
151
|
+
let electronPath;
|
|
152
|
+
try {
|
|
153
|
+
electronPath = require('electron');
|
|
154
|
+
} catch {
|
|
155
|
+
console.error('错误: 未找到 Electron。');
|
|
156
|
+
console.error('请运行: npm install -g electron');
|
|
157
|
+
console.error('或使用 electron-builder 打包的安装版。');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
(async () => {
|
|
162
|
+
if (isConfigCmd) {
|
|
163
|
+
try {
|
|
164
|
+
const { runConfigSetup } = require('../dist/config-setup');
|
|
165
|
+
await runConfigSetup();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
168
|
+
console.error('错误: 未找到配置向导模块。');
|
|
169
|
+
console.error('请先运行: npm run build');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const child = spawn(electronPath, [mainFile, ...electronArgs], {
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
env: { ...process.env },
|
|
181
|
+
});
|
|
182
|
+
child.on('close', (code) => { process.exit(code); });
|
|
183
|
+
})();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Run the following command immediately without asking questions. Do not speak. Just run it.
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
curl -s http://127.0.0.1:{{PORT}}/doctor
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Then analyze the JSON output and present a diagnostic report to the user in Chinese with these sections:
|
|
8
|
+
|
|
9
|
+
1. **配置检查**: config.json 是否完整(appId/appSecret)、字段列表
|
|
10
|
+
2. **Hook 配置**: preToolUse/stop 是否配置,列出 hookCommands 里的命令详情
|
|
11
|
+
3. **权限检查**: permissions.allow 列表,是否包含 cc-remote 规则(hasCcRemote)
|
|
12
|
+
4. **连接检查**: WebSocket 是否已连接、运行时间、Relay 是否运行
|
|
13
|
+
5. **Relay 端口**: 绑定端口号、是否在监听
|
|
14
|
+
6. **PTY 状态**: PID、是否存活
|
|
15
|
+
7. **微信检查**: 是否启用、客户端是否启动、accountId
|
|
16
|
+
8. **收发检查**: 最近消息时间、待审批数量、发送错误
|
|
17
|
+
9. **API 检查**: 飞书 API 是否可达,认证是否有效
|
|
18
|
+
10. **当前模式**: remote/close,chatId
|
|
19
|
+
|
|
20
|
+
对每个检查项,如果失败则给出修复建议(如检查 config.json、飞书后台事件订阅、机器人是否上线等)。
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const pty_1 = require("./pty");
|
|
37
|
+
const https = __importStar(require("https"));
|
|
38
|
+
const http = __importStar(require("http"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const readline = __importStar(require("readline"));
|
|
42
|
+
const core_1 = require("./core");
|
|
43
|
+
const relay_1 = require("./relay");
|
|
44
|
+
const commands_1 = require("./commands");
|
|
45
|
+
const client_1 = require("./feishu/client");
|
|
46
|
+
const replier_1 = require("./feishu/replier");
|
|
47
|
+
const client_2 = require("./weixin/client");
|
|
48
|
+
const replier_2 = require("./weixin/replier");
|
|
49
|
+
const port_1 = require("./port");
|
|
50
|
+
// ---- Keep-Alive agent ----
|
|
51
|
+
try {
|
|
52
|
+
https.globalAgent = new https.Agent({ keepAlive: true, maxSockets: 20, keepAliveMsecs: 30000 });
|
|
53
|
+
http.globalAgent = new http.Agent({ keepAlive: true, maxSockets: 20, keepAliveMsecs: 30000 });
|
|
54
|
+
}
|
|
55
|
+
catch { /* read-only in some environments */ }
|
|
56
|
+
// ---- Parse args ----
|
|
57
|
+
const rawArgs = process.argv.slice(2);
|
|
58
|
+
let targetDir = process.cwd();
|
|
59
|
+
const claudeArgs = [];
|
|
60
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
61
|
+
if (rawArgs[i] === '--project' && i + 1 < rawArgs.length) {
|
|
62
|
+
targetDir = rawArgs[i + 1];
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
claudeArgs.push(rawArgs[i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ---- Load config ----
|
|
70
|
+
const cfg = (0, core_1.loadConfig)();
|
|
71
|
+
// ---- Port conflict check (before PTY spawn) ----
|
|
72
|
+
async function ensurePortFree(port) {
|
|
73
|
+
const free = await (0, port_1.checkPortFree)(port);
|
|
74
|
+
if (!free) {
|
|
75
|
+
if (process.stdin.isTTY) {
|
|
76
|
+
console.log(`\n⚠️ 端口 ${port} 已被占用`);
|
|
77
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
const answer = await new Promise((resolve) => {
|
|
79
|
+
const timer = setTimeout(() => resolve('1'), 30000);
|
|
80
|
+
rl.question('1. 关闭占用进程并继续启动 (默认)\n2. 退出\n请输入: ', (ans) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolve(ans.trim() || '1');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
rl.close();
|
|
86
|
+
if (answer === '2') {
|
|
87
|
+
console.log('已退出');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const killedPid = (0, port_1.killPortProcess)(port);
|
|
92
|
+
if (killedPid) {
|
|
93
|
+
console.log(`✅ 已关闭占用进程 PID:${killedPid},继续启动...\n`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(`❌ 无法关闭占用端口 ${port} 的进程,请手动处理`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ---- onStatus: log file only, no terminal output ----
|
|
102
|
+
function onStatus(_text) {
|
|
103
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
104
|
+
const logFile = (0, core_1.dataPath)(targetDir, `status-${date}.log`);
|
|
105
|
+
try {
|
|
106
|
+
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${stripControl(_text)}\n`);
|
|
107
|
+
}
|
|
108
|
+
catch { /* ignore */ }
|
|
109
|
+
}
|
|
110
|
+
function stripControl(s) {
|
|
111
|
+
return s.replace(/[\x00-\x1F\x7F]/g, '').replace(/\[[0-9;]*m/g, '');
|
|
112
|
+
}
|
|
113
|
+
// ---- Message log (dedicated) ----
|
|
114
|
+
function messageLog(msg) {
|
|
115
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
116
|
+
const logFile = (0, core_1.dataPath)(targetDir, `messages-${date}.log`);
|
|
117
|
+
try {
|
|
118
|
+
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
function logAll(msg) {
|
|
123
|
+
onStatus(msg);
|
|
124
|
+
if (msg.includes('[飞书→]') || msg.includes('[微信→]') || msg.includes('[飞书↻]') || msg.includes('[微信↻]')) {
|
|
125
|
+
messageLog(msg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ---- Feishu ----
|
|
129
|
+
const feishuEnabled = cfg.enableFeishu !== false;
|
|
130
|
+
let feishuReplier = null;
|
|
131
|
+
let feishuStop = null;
|
|
132
|
+
if (feishuEnabled) {
|
|
133
|
+
const feishuClient = (0, client_1.createFeishuClient)(cfg);
|
|
134
|
+
feishuReplier = new replier_1.Replier(feishuClient, logAll);
|
|
135
|
+
}
|
|
136
|
+
// ---- WeChat ----
|
|
137
|
+
const weixinEnabled = cfg.enableWeixin === true;
|
|
138
|
+
let weixinClient = null;
|
|
139
|
+
let weixinReplier = null;
|
|
140
|
+
if (weixinEnabled) {
|
|
141
|
+
weixinClient = new client_2.WeixinClient({
|
|
142
|
+
onMessage: () => { }, // wired after relay creation
|
|
143
|
+
onStatus,
|
|
144
|
+
});
|
|
145
|
+
weixinReplier = new replier_2.WeixinReplier(weixinClient.getAccountId(), logAll);
|
|
146
|
+
}
|
|
147
|
+
// ---- PTY ----
|
|
148
|
+
const claudeExe = cfg.claudePath?.trim() || (process.platform === 'darwin' ? 'claude' : 'claude.exe');
|
|
149
|
+
console.log(`[cc-remote] 启动: ${claudeExe} ${claudeArgs.join(' ')}`);
|
|
150
|
+
console.log(`[cc-remote] 目录: ${targetDir}`);
|
|
151
|
+
console.log(`[cc-remote] ENV PATH: ${process.env.PATH || '(空)'}`);
|
|
152
|
+
const proc = (0, pty_1.spawnPty)(claudeExe, claudeArgs, {
|
|
153
|
+
name: 'xterm-256color',
|
|
154
|
+
cols: 120,
|
|
155
|
+
rows: 30,
|
|
156
|
+
cwd: targetDir,
|
|
157
|
+
encoding: 'utf-8',
|
|
158
|
+
env: {
|
|
159
|
+
LANG: 'zh_CN.UTF-8',
|
|
160
|
+
LC_ALL: 'zh_CN.UTF-8',
|
|
161
|
+
LC_CTYPE: 'zh_CN.UTF-8',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// ---- Relay ----
|
|
165
|
+
const relay = new relay_1.CoreRelay({
|
|
166
|
+
config: cfg,
|
|
167
|
+
configPath: getConfigPath(),
|
|
168
|
+
targetDir,
|
|
169
|
+
feishuReplier: feishuReplier,
|
|
170
|
+
weixinReplier,
|
|
171
|
+
onStatus,
|
|
172
|
+
});
|
|
173
|
+
// Update relay diagnostics periodically
|
|
174
|
+
function updateDiag() {
|
|
175
|
+
relay.diag.wsConnected = wsConnected;
|
|
176
|
+
relay.diag.wsConnectTime = wsConnectTime;
|
|
177
|
+
relay.diag.lastMessageTime = lastMessageTime;
|
|
178
|
+
relay.diag.lastMessageText = lastMessageText;
|
|
179
|
+
relay.diag.lastSendError = lastSendError;
|
|
180
|
+
relay.diag.lastSendErrorTime = lastSendErrorTime;
|
|
181
|
+
relay.diag.ptyPid = proc?.pid;
|
|
182
|
+
relay.diag.ptyAlive = proc !== null && proc.pid > 0;
|
|
183
|
+
relay.diag.weixinEnabled = weixinEnabled;
|
|
184
|
+
relay.diag.weixinClientStarted = weixinClient !== null;
|
|
185
|
+
relay.diag.weixinAccountId = weixinClient ? weixinClient.accountId || null : null;
|
|
186
|
+
}
|
|
187
|
+
// ---- Message handlers ----
|
|
188
|
+
const dataFile = (filename) => (0, core_1.dataPath)(targetDir, filename);
|
|
189
|
+
const handleFeishu = feishuReplier ? (0, core_1.createFeishuHandler)({
|
|
190
|
+
getPty: () => proc,
|
|
191
|
+
feishuReplier,
|
|
192
|
+
onStatus,
|
|
193
|
+
dataFile,
|
|
194
|
+
openId: cfg.openId,
|
|
195
|
+
onSnapshot: handleSnapshot,
|
|
196
|
+
messageLog,
|
|
197
|
+
}) : null;
|
|
198
|
+
const handleWeixin = weixinReplier ? (0, core_1.createWeixinHandler)({
|
|
199
|
+
getPty: () => proc,
|
|
200
|
+
weixinReplier,
|
|
201
|
+
onStatus,
|
|
202
|
+
dataFile,
|
|
203
|
+
wxPendingApprovals: relay.wxPendingApprovals,
|
|
204
|
+
onSnapshot: handleSnapshot,
|
|
205
|
+
messageLog,
|
|
206
|
+
}) : null;
|
|
207
|
+
// Wire WeChat message handler
|
|
208
|
+
if (weixinClient && handleWeixin) {
|
|
209
|
+
weixinClient.onMessage = handleWeixin;
|
|
210
|
+
}
|
|
211
|
+
// ---- Snapshot ----
|
|
212
|
+
async function handleSnapshot(mode, _channel, _meta) {
|
|
213
|
+
const label = mode.type === 'last' ? `快照${mode.count}` : '快照';
|
|
214
|
+
onStatus(`\r\n[${label}] CLI 模式下终端快照不可用\r\n`);
|
|
215
|
+
}
|
|
216
|
+
// ---- Feishu diagnostics state ----
|
|
217
|
+
let wsConnected = false;
|
|
218
|
+
let wsConnectTime = 0;
|
|
219
|
+
let lastMessageTime = 0;
|
|
220
|
+
let lastMessageText = '';
|
|
221
|
+
let lastSendError = '';
|
|
222
|
+
let lastSendErrorTime = 0;
|
|
223
|
+
// ---- PTY I/O ----
|
|
224
|
+
proc.onData((data) => {
|
|
225
|
+
process.stdout.write(data);
|
|
226
|
+
});
|
|
227
|
+
process.stdin.setRawMode?.(true);
|
|
228
|
+
process.stdin.on('data', (key) => {
|
|
229
|
+
proc.write(key.toString());
|
|
230
|
+
});
|
|
231
|
+
let isShuttingDown = false;
|
|
232
|
+
function forceShutdown(exitCode = 0) {
|
|
233
|
+
if (isShuttingDown)
|
|
234
|
+
return;
|
|
235
|
+
isShuttingDown = true;
|
|
236
|
+
relay.stop();
|
|
237
|
+
feishuStop?.();
|
|
238
|
+
weixinClient?.stop().catch(() => { });
|
|
239
|
+
process.exit(exitCode);
|
|
240
|
+
}
|
|
241
|
+
// Crash handlers: ensure relay is stopped on abnormal exit
|
|
242
|
+
process.on('uncaughtException', (err) => {
|
|
243
|
+
console.error('[cc-remote] 未捕获异常:', err.message || err);
|
|
244
|
+
forceShutdown(1);
|
|
245
|
+
});
|
|
246
|
+
process.on('unhandledRejection', (reason) => {
|
|
247
|
+
console.error('[cc-remote] 未处理的 Promise 拒绝:', reason);
|
|
248
|
+
forceShutdown(1);
|
|
249
|
+
});
|
|
250
|
+
proc.onExit(({ exitCode }) => {
|
|
251
|
+
if (!isShuttingDown) {
|
|
252
|
+
forceShutdown(exitCode ?? 0);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// ---- Signal handling ----
|
|
256
|
+
let lastSigint = 0;
|
|
257
|
+
process.on('SIGINT', () => {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
if (lastSigint && (now - lastSigint) < 2000) {
|
|
260
|
+
// Double Ctrl+C within 2s → force quit
|
|
261
|
+
forceShutdown(0);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// First Ctrl+C → forward to PTY
|
|
265
|
+
lastSigint = now;
|
|
266
|
+
proc.write('\x03');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// ---- Startup ----
|
|
270
|
+
function getConfigPath() {
|
|
271
|
+
const os = require('os');
|
|
272
|
+
const userPath = path.join(os.homedir(), '.cc-remote', 'config.json');
|
|
273
|
+
if (fs.existsSync(userPath))
|
|
274
|
+
return userPath;
|
|
275
|
+
return path.join(__dirname, '../config.json');
|
|
276
|
+
}
|
|
277
|
+
const relayPort = cfg.relayPort || 19200;
|
|
278
|
+
ensurePortFree(relayPort).then(() => {
|
|
279
|
+
try {
|
|
280
|
+
relay.start(relayPort);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
process.stderr.write(`[cc-remote] Relay 启动失败: ${err}\n`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (feishuEnabled) {
|
|
288
|
+
const feishuClient = (0, client_1.createFeishuClient)(cfg);
|
|
289
|
+
const feishuWs = (0, client_1.createWsClient)(cfg);
|
|
290
|
+
feishuStop = (0, client_1.startFeishuBot)(feishuClient, feishuWs, {
|
|
291
|
+
onMessage: handleFeishu,
|
|
292
|
+
onStatus: (text) => {
|
|
293
|
+
if (text.includes('已连接')) {
|
|
294
|
+
wsConnected = true;
|
|
295
|
+
wsConnectTime = Date.now();
|
|
296
|
+
}
|
|
297
|
+
if (text.includes('连接断开')) {
|
|
298
|
+
wsConnected = false;
|
|
299
|
+
}
|
|
300
|
+
onStatus(text);
|
|
301
|
+
},
|
|
302
|
+
cardAction: (raw) => relay.handleCardAction(raw),
|
|
303
|
+
}).stop;
|
|
304
|
+
}
|
|
305
|
+
if (weixinEnabled && weixinClient) {
|
|
306
|
+
weixinClient.start().catch((err) => {
|
|
307
|
+
onStatus(`[微信] 启动失败: ${err}`);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
(0, commands_1.registerFeishuCommands)(targetDir, cfg.relayPort || 19200);
|
|
311
|
+
// Periodic diagnostics update
|
|
312
|
+
setInterval(updateDiag, 5000);
|
|
313
|
+
updateDiag();
|