remote-claude 0.2.5 → 0.2.7
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/.env.example +8 -4
- package/README.md +26 -0
- package/init.sh +3 -0
- package/lark_client/config.py +3 -0
- package/lark_client/lark_handler.py +5 -3
- package/package.json +3 -2
- package/remote_claude.py +41 -0
- package/scripts/report_install.py +86 -0
- package/stats/collector.py +48 -12
package/.env.example
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Remote Claude 飞书客户端配置
|
|
2
2
|
|
|
3
|
-
# Claude CLI 命令(可选,默认 claude)
|
|
4
|
-
# 支持多词命令,如:ccr code、/usr/local/bin/claude
|
|
5
|
-
# CLAUDE_COMMAND=claude
|
|
6
|
-
|
|
7
3
|
# 飞书应用配置(必填)
|
|
8
4
|
# 在飞书开发者后台创建应用获取: https://open.feishu.cn/app
|
|
9
5
|
FEISHU_APP_ID=cli_xxxxx
|
|
@@ -14,3 +10,11 @@ FEISHU_APP_SECRET=xxxxx
|
|
|
14
10
|
ENABLE_USER_WHITELIST=false
|
|
15
11
|
ALLOWED_USERS=ou_xxxxx,ou_yyyyy
|
|
16
12
|
|
|
13
|
+
# 群聊名称前缀(可选,默认「Remote-Claude」)
|
|
14
|
+
# 群名格式:{GROUP_NAME_PREFIX}{目录名}-{HH-MM},如:【Remote-Claude】myapp-14-30
|
|
15
|
+
# GROUP_NAME_PREFIX=【Remote-Claude】
|
|
16
|
+
|
|
17
|
+
# Claude CLI 命令(可选,默认 claude)
|
|
18
|
+
# 支持多词命令,如:ccr code、/usr/local/bin/claude
|
|
19
|
+
# CLAUDE_COMMAND=claude
|
|
20
|
+
|
package/README.md
CHANGED
|
@@ -231,6 +231,32 @@ remote-claude lark status # 查看状态
|
|
|
231
231
|
|
|
232
232
|
飞书中与机器人对话,可用命令:`/menu`、`/attach`、`/detach`、`/list`、`/help` 等。
|
|
233
233
|
|
|
234
|
+
## 高级配置
|
|
235
|
+
|
|
236
|
+
在 `~/.remote-claude/.env` 中可配置以下选项:
|
|
237
|
+
|
|
238
|
+
| 配置项 | 默认值 | 说明 |
|
|
239
|
+
|--------|--------|------|
|
|
240
|
+
| `CLAUDE_COMMAND` | `claude` | 启动 Claude CLI 的命令 |
|
|
241
|
+
| `FEISHU_APP_ID` | — | 飞书应用 ID |
|
|
242
|
+
| `FEISHU_APP_SECRET` | — | 飞书应用密钥 |
|
|
243
|
+
| `ENABLE_USER_WHITELIST` | `false` | 是否启用用户白名单 |
|
|
244
|
+
| `ALLOWED_USERS` | — | 白名单用户 ID,逗号分隔 |
|
|
245
|
+
|
|
246
|
+
### 自定义 Claude CLI 命令
|
|
247
|
+
|
|
248
|
+
若你的 Claude CLI 安装方式不同,启动命令不是 `claude`,可通过 `CLAUDE_COMMAND` 指定:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# ~/.remote-claude/.env
|
|
252
|
+
|
|
253
|
+
# 使用两段式命令(如 ccr code)
|
|
254
|
+
CLAUDE_COMMAND=ccr code
|
|
255
|
+
|
|
256
|
+
# 使用绝对路径
|
|
257
|
+
CLAUDE_COMMAND=/usr/local/bin/claude
|
|
258
|
+
```
|
|
259
|
+
|
|
234
260
|
## 系统要求
|
|
235
261
|
|
|
236
262
|
- **操作系统**: macOS 或 Linux
|
package/init.sh
CHANGED
package/lark_client/config.py
CHANGED
|
@@ -32,3 +32,6 @@ ENABLE_USER_WHITELIST = os.getenv("ENABLE_USER_WHITELIST", "false").lower() == "
|
|
|
32
32
|
|
|
33
33
|
# 机器人名称(用于群聊命名)
|
|
34
34
|
BOT_NAME = os.getenv("BOT_NAME", "Claude")
|
|
35
|
+
|
|
36
|
+
# 群聊名称前缀(格式:{GROUP_NAME_PREFIX}{dir}-{HH-MM})
|
|
37
|
+
GROUP_NAME_PREFIX = os.getenv("GROUP_NAME_PREFIX", "【Remote-Claude】")
|
|
@@ -620,13 +620,15 @@ class LarkHandler:
|
|
|
620
620
|
cwd = self._get_pid_cwd(pid) if pid else None
|
|
621
621
|
dir_label = cwd.rstrip("/").rsplit("/", 1)[-1] if cwd else session_name
|
|
622
622
|
|
|
623
|
-
import lark_oapi as lark
|
|
624
623
|
from . import config
|
|
625
624
|
try:
|
|
626
625
|
import json as _json
|
|
627
626
|
import urllib.request
|
|
627
|
+
import datetime
|
|
628
|
+
_time_str = datetime.datetime.now().strftime("%H-%M")
|
|
629
|
+
group_name = f"{config.GROUP_NAME_PREFIX}{dir_label}-{_time_str}"
|
|
628
630
|
req_body = {
|
|
629
|
-
"name":
|
|
631
|
+
"name": group_name,
|
|
630
632
|
"description": f"Remote Claude 专属群 - 会话 {session_name}",
|
|
631
633
|
"user_id_list": [user_id],
|
|
632
634
|
}
|
|
@@ -665,7 +667,7 @@ class LarkHandler:
|
|
|
665
667
|
|
|
666
668
|
await card_service.send_text(
|
|
667
669
|
chat_id,
|
|
668
|
-
f"✅
|
|
670
|
+
f"✅ 已创建专属群「{group_name}」并已连接\n"
|
|
669
671
|
f"在群内直接发消息即可与 Claude 交互"
|
|
670
672
|
)
|
|
671
673
|
# 刷新会话列表卡片,使"创建群聊"按钮变为"进入群聊"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-claude",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "双端共享 Claude CLI 工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"remote-claude": "bin/remote-claude",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
|
-
"scripts
|
|
15
|
+
"scripts/*.sh",
|
|
16
|
+
"scripts/*.py",
|
|
16
17
|
"init.sh",
|
|
17
18
|
"remote_claude.py",
|
|
18
19
|
"server/*.py",
|
package/remote_claude.py
CHANGED
|
@@ -387,6 +387,43 @@ def cmd_stats(args):
|
|
|
387
387
|
return 0
|
|
388
388
|
|
|
389
389
|
|
|
390
|
+
def cmd_update(args):
|
|
391
|
+
"""更新 remote-claude 到最新版本"""
|
|
392
|
+
import subprocess as _sp
|
|
393
|
+
|
|
394
|
+
git_dir = SCRIPT_DIR / ".git"
|
|
395
|
+
if git_dir.exists():
|
|
396
|
+
# 源码安装:git pull 更新
|
|
397
|
+
print(f"检测到源码安装({SCRIPT_DIR})")
|
|
398
|
+
print("正在更新...")
|
|
399
|
+
result = _sp.run(["git", "pull"], cwd=SCRIPT_DIR)
|
|
400
|
+
if result.returncode != 0:
|
|
401
|
+
print("❌ git pull 失败")
|
|
402
|
+
return 1
|
|
403
|
+
# 同步 Python 依赖
|
|
404
|
+
_sp.run(["uv", "sync"], cwd=SCRIPT_DIR)
|
|
405
|
+
print("✅ 更新完成")
|
|
406
|
+
else:
|
|
407
|
+
# npm 安装:区分本地和全局
|
|
408
|
+
install_dir_str = str(SCRIPT_DIR)
|
|
409
|
+
if "node_modules" in install_dir_str:
|
|
410
|
+
# 本地 npm 安装:找到项目根目录(node_modules 的上两级)
|
|
411
|
+
project_root = SCRIPT_DIR.parent.parent
|
|
412
|
+
print(f"检测到 npm 本地安装({project_root})")
|
|
413
|
+
print("正在更新...")
|
|
414
|
+
result = _sp.run(["npm", "install", "remote-claude@latest"], cwd=project_root)
|
|
415
|
+
else:
|
|
416
|
+
# 全局 npm 安装
|
|
417
|
+
print("检测到 npm 全局安装")
|
|
418
|
+
print("正在更新...")
|
|
419
|
+
result = _sp.run(["npm", "install", "-g", "remote-claude@latest"])
|
|
420
|
+
if result.returncode != 0:
|
|
421
|
+
print("❌ npm 更新失败")
|
|
422
|
+
return 1
|
|
423
|
+
print("✅ 更新完成,请重启终端使新版本生效")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
390
427
|
def cmd_lark(args):
|
|
391
428
|
"""飞书客户端管理(兼容旧命令)"""
|
|
392
429
|
# 如果没有子命令,默认显示状态或启动
|
|
@@ -531,6 +568,10 @@ def main():
|
|
|
531
568
|
)
|
|
532
569
|
stats_parser.set_defaults(func=cmd_stats)
|
|
533
570
|
|
|
571
|
+
# update 命令
|
|
572
|
+
update_parser = subparsers.add_parser("update", help="更新 remote-claude 到最新版本")
|
|
573
|
+
update_parser.set_defaults(func=cmd_update)
|
|
574
|
+
|
|
534
575
|
args = parser.parse_args()
|
|
535
576
|
|
|
536
577
|
if args.command is None:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
init_install 上报脚本
|
|
3
|
+
|
|
4
|
+
每次 init.sh 执行时调用(npm 安装和源码安装均覆盖),发送 init_install 事件到 Mixpanel。
|
|
5
|
+
仅依赖标准库,不需要 mixpanel 包,失败静默。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import platform
|
|
11
|
+
import urllib.request
|
|
12
|
+
import uuid
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_TOKEN = 'c4d804fc1fe4337132e4da90fdb690c9'
|
|
16
|
+
_USER_DIR = Path.home() / '.remote-claude'
|
|
17
|
+
_ID_FILE = _USER_DIR / 'machine-id'
|
|
18
|
+
_REPORTED_VERSION_FILE = _USER_DIR / 'init_install_version'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_machine_id() -> str:
|
|
22
|
+
if _ID_FILE.exists():
|
|
23
|
+
try:
|
|
24
|
+
mid = _ID_FILE.read_text().strip()
|
|
25
|
+
if mid:
|
|
26
|
+
return mid
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
mid = str(uuid.uuid4())
|
|
30
|
+
try:
|
|
31
|
+
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
_ID_FILE.write_text(mid)
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return mid
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_version() -> str:
|
|
39
|
+
try:
|
|
40
|
+
pkg = Path(__file__).parent.parent / 'package.json'
|
|
41
|
+
return json.loads(pkg.read_text()).get('version', 'unknown')
|
|
42
|
+
except Exception:
|
|
43
|
+
return 'unknown'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
machine_id = _get_machine_id()
|
|
48
|
+
version = _get_version()
|
|
49
|
+
|
|
50
|
+
# 每个版本只上报一次
|
|
51
|
+
try:
|
|
52
|
+
if _REPORTED_VERSION_FILE.exists() and _REPORTED_VERSION_FILE.read_text().strip() == version:
|
|
53
|
+
return
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
props = {
|
|
57
|
+
'token': _TOKEN,
|
|
58
|
+
'distinct_id': machine_id,
|
|
59
|
+
'version': version,
|
|
60
|
+
'hostname': platform.node(),
|
|
61
|
+
'os': f'{platform.system()} {platform.release()}',
|
|
62
|
+
'python': platform.python_version(),
|
|
63
|
+
}
|
|
64
|
+
data = base64.b64encode(json.dumps([{
|
|
65
|
+
'event': 'init_install',
|
|
66
|
+
'properties': props,
|
|
67
|
+
}]).encode()).decode()
|
|
68
|
+
req = urllib.request.Request(
|
|
69
|
+
'https://api.mixpanel.com/track',
|
|
70
|
+
data=f'data={data}'.encode(),
|
|
71
|
+
method='POST',
|
|
72
|
+
)
|
|
73
|
+
urllib.request.urlopen(req, timeout=10)
|
|
74
|
+
|
|
75
|
+
# 记录已上报版本
|
|
76
|
+
try:
|
|
77
|
+
_REPORTED_VERSION_FILE.write_text(version)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
try:
|
|
84
|
+
main()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
package/stats/collector.py
CHANGED
|
@@ -7,6 +7,7 @@ StatsCollector:事件收集器
|
|
|
7
7
|
- 所有异常均被捕获,绝不影响主流程
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
import sqlite3
|
|
12
13
|
import threading
|
|
@@ -15,6 +16,8 @@ from collections import deque
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Optional
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
18
21
|
from .machine import get_machine_id, get_machine_info
|
|
19
22
|
|
|
20
23
|
# SQLite 数据库路径(持久化,不受 /tmp 清理影响)
|
|
@@ -321,16 +324,49 @@ class StatsCollector:
|
|
|
321
324
|
|
|
322
325
|
def report_install(self) -> None:
|
|
323
326
|
"""首次运行时上报 install 事件和 user profile"""
|
|
324
|
-
if not self.
|
|
327
|
+
if not self._is_first_run or not self._mp_token:
|
|
325
328
|
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
329
|
+
|
|
330
|
+
machine_info = get_machine_info()
|
|
331
|
+
machine_id = self._machine_id
|
|
332
|
+
mp_token = self._mp_token
|
|
333
|
+
mp = self._mp # 可能为 None(mixpanel 包未安装时)
|
|
334
|
+
|
|
335
|
+
def _send() -> None:
|
|
336
|
+
try:
|
|
337
|
+
import base64
|
|
338
|
+
import json
|
|
339
|
+
import urllib.request
|
|
340
|
+
|
|
341
|
+
props = {**machine_info, 'token': mp_token, 'distinct_id': machine_id}
|
|
342
|
+
data = base64.b64encode(json.dumps([{
|
|
343
|
+
'event': 'install',
|
|
344
|
+
'properties': props,
|
|
345
|
+
}]).encode()).decode()
|
|
346
|
+
req = urllib.request.Request(
|
|
347
|
+
'https://api.mixpanel.com/track',
|
|
348
|
+
data=f'data={data}'.encode(),
|
|
349
|
+
method='POST',
|
|
350
|
+
)
|
|
351
|
+
urllib.request.urlopen(req, timeout=10)
|
|
352
|
+
logger.debug('install event sent via urllib (machine_id=%s)', machine_id)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.warning('install event failed: %s', e)
|
|
355
|
+
|
|
356
|
+
# 同时更新 user profile(仅 SDK 可用时)
|
|
357
|
+
if mp:
|
|
358
|
+
try:
|
|
359
|
+
import datetime
|
|
360
|
+
mp.people_set(machine_id, {
|
|
361
|
+
'$name': machine_info['hostname'],
|
|
362
|
+
**machine_info,
|
|
363
|
+
'first_seen': datetime.datetime.now().isoformat(),
|
|
364
|
+
})
|
|
365
|
+
logger.debug('people_set sent via mixpanel SDK')
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.warning('people_set failed: %s', e)
|
|
368
|
+
else:
|
|
369
|
+
logger.debug('mixpanel SDK not available, skipping people_set')
|
|
370
|
+
|
|
371
|
+
t = threading.Thread(target=_send, daemon=True)
|
|
372
|
+
t.start()
|