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 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
@@ -369,6 +369,9 @@ install_dependencies() {
369
369
  fi
370
370
 
371
371
  print_success "依赖安装完成"
372
+
373
+ # 上报 init_install 事件(后台执行,不阻塞,失败静默)
374
+ python3 scripts/report_install.py &>/dev/null &
372
375
  }
373
376
 
374
377
  # 配置飞书环境
@@ -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": f"【{dir_label}】{config.BOT_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"✅ 已创建专属群「【{dir_label}】{config.BOT_NAME}」并已连接\n"
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.5",
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
@@ -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._mp or not self._is_first_run:
327
+ if not self._is_first_run or not self._mp_token:
325
328
  return
326
- try:
327
- machine_info = get_machine_info()
328
- import datetime
329
- self._mp.people_set(self._machine_id, {
330
- '$name': machine_info['hostname'],
331
- **machine_info,
332
- 'first_seen': datetime.datetime.now().isoformat(),
333
- })
334
- self._mp_track('install', machine_info)
335
- except Exception:
336
- pass
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()