remote-claude 0.1.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/bin/cl +20 -0
- package/bin/cla +20 -0
- package/bin/remote-claude +21 -0
- package/client/client.py +251 -0
- package/lark_client/__init__.py +3 -0
- package/lark_client/capture_output.py +91 -0
- package/lark_client/card_builder.py +1114 -0
- package/lark_client/card_service.py +250 -0
- package/lark_client/config.py +22 -0
- package/lark_client/lark_handler.py +841 -0
- package/lark_client/main.py +306 -0
- package/lark_client/output_cleaner.py +222 -0
- package/lark_client/session_bridge.py +195 -0
- package/lark_client/shared_memory_poller.py +364 -0
- package/lark_client/terminal_buffer.py +215 -0
- package/lark_client/terminal_renderer.py +69 -0
- package/package.json +41 -0
- package/pyproject.toml +14 -0
- package/remote_claude.py +518 -0
- package/scripts/check-env.sh +40 -0
- package/scripts/completion.sh +76 -0
- package/scripts/postinstall.sh +76 -0
- package/server/component_parser.py +1113 -0
- package/server/rich_text_renderer.py +301 -0
- package/server/server.py +801 -0
- package/server/shared_state.py +198 -0
- package/stats/__init__.py +38 -0
- package/stats/collector.py +325 -0
- package/stats/machine.py +47 -0
- package/stats/query.py +151 -0
- package/utils/components.py +165 -0
- package/utils/protocol.py +164 -0
- package/utils/session.py +409 -0
- package/uv.lock +703 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
飞书卡片服务 - 支持创建和实时更新卡片
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
import lark_oapi as lark
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger('CardService')
|
|
15
|
+
from lark_oapi.api.im.v1 import (
|
|
16
|
+
CreateMessageRequest, CreateMessageRequestBody,
|
|
17
|
+
)
|
|
18
|
+
from lark_oapi.api.cardkit.v1 import (
|
|
19
|
+
CreateCardRequest, CreateCardRequestBody,
|
|
20
|
+
UpdateCardRequest, UpdateCardRequestBody, Card
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from . import config
|
|
24
|
+
|
|
25
|
+
import sys as _sys
|
|
26
|
+
_sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent.parent))
|
|
27
|
+
try:
|
|
28
|
+
from stats import track as _track_stats
|
|
29
|
+
except Exception:
|
|
30
|
+
def _track_stats(*args, **kwargs): pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CardState:
|
|
35
|
+
"""卡片状态"""
|
|
36
|
+
card_id: str
|
|
37
|
+
message_id: Optional[str] = None
|
|
38
|
+
sequence: int = 0
|
|
39
|
+
last_update: float = field(default_factory=time.time)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CardService:
|
|
43
|
+
"""飞书卡片服务"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self.client: Optional[lark.Client] = None
|
|
47
|
+
self._init_client()
|
|
48
|
+
# chat_id -> CardState
|
|
49
|
+
self._active_cards: Dict[str, CardState] = {}
|
|
50
|
+
# message_id -> CardState(反查,用于按钮点击就地更新)
|
|
51
|
+
self._cards_by_message_id: Dict[str, CardState] = {}
|
|
52
|
+
|
|
53
|
+
def _init_client(self):
|
|
54
|
+
"""初始化飞书客户端"""
|
|
55
|
+
if config.FEISHU_APP_ID and config.FEISHU_APP_SECRET:
|
|
56
|
+
self.client = lark.Client.builder() \
|
|
57
|
+
.app_id(config.FEISHU_APP_ID) \
|
|
58
|
+
.app_secret(config.FEISHU_APP_SECRET) \
|
|
59
|
+
.build()
|
|
60
|
+
|
|
61
|
+
async def create_card(self, card_content: Dict[str, Any]) -> Optional[str]:
|
|
62
|
+
"""创建卡片实体,返回 card_id(失败自动重试 1 次)"""
|
|
63
|
+
if not self.client:
|
|
64
|
+
print("[CardService] 客户端未初始化")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
import asyncio
|
|
68
|
+
|
|
69
|
+
for attempt in range(2):
|
|
70
|
+
try:
|
|
71
|
+
request = CreateCardRequest.builder() \
|
|
72
|
+
.request_body(
|
|
73
|
+
CreateCardRequestBody.builder()
|
|
74
|
+
.type("card_json")
|
|
75
|
+
.data(json.dumps(card_content, ensure_ascii=False))
|
|
76
|
+
.build()
|
|
77
|
+
) \
|
|
78
|
+
.build()
|
|
79
|
+
|
|
80
|
+
response = await asyncio.to_thread(
|
|
81
|
+
self.client.cardkit.v1.card.create, request
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.success():
|
|
85
|
+
card_id = getattr(getattr(response, "data", None), "card_id", None)
|
|
86
|
+
return card_id
|
|
87
|
+
else:
|
|
88
|
+
logger.warning(f"创建卡片失败(attempt={attempt+1}): code={response.code} msg={response.msg}")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"创建卡片异常(attempt={attempt+1}): {e}")
|
|
91
|
+
|
|
92
|
+
if attempt == 0:
|
|
93
|
+
await asyncio.sleep(1)
|
|
94
|
+
|
|
95
|
+
_track_stats('error', 'card_api', detail='create_card')
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
async def send_card(self, chat_id: str, card_id: str) -> Optional[str]:
|
|
99
|
+
"""发送卡片消息,返回 message_id"""
|
|
100
|
+
if not self.client:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
import asyncio
|
|
105
|
+
|
|
106
|
+
card_content = {
|
|
107
|
+
"type": "card",
|
|
108
|
+
"data": {"card_id": card_id}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
request = CreateMessageRequest.builder() \
|
|
112
|
+
.receive_id_type("chat_id") \
|
|
113
|
+
.request_body(
|
|
114
|
+
CreateMessageRequestBody.builder()
|
|
115
|
+
.receive_id(chat_id)
|
|
116
|
+
.msg_type("interactive")
|
|
117
|
+
.content(json.dumps(card_content))
|
|
118
|
+
.build()
|
|
119
|
+
) \
|
|
120
|
+
.build()
|
|
121
|
+
|
|
122
|
+
response = await asyncio.to_thread(
|
|
123
|
+
self.client.im.v1.message.create, request
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if response.success():
|
|
127
|
+
message_id = getattr(getattr(response, "data", None), "message_id", None)
|
|
128
|
+
return message_id
|
|
129
|
+
else:
|
|
130
|
+
logger.warning(f"发送卡片失败: code={response.code} msg={response.msg}")
|
|
131
|
+
return None
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"发送卡片异常: {e}")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
async def create_and_send_card(
|
|
137
|
+
self, chat_id: str, card_content: Dict[str, Any]
|
|
138
|
+
) -> Optional[str]:
|
|
139
|
+
"""创建卡片并发送,内部维护 message_id 反查索引,返回 message_id"""
|
|
140
|
+
card_id = await self.create_card(card_content)
|
|
141
|
+
if not card_id:
|
|
142
|
+
return None
|
|
143
|
+
message_id = await self.send_card(chat_id, card_id)
|
|
144
|
+
if message_id:
|
|
145
|
+
state = CardState(card_id=card_id, message_id=message_id)
|
|
146
|
+
self._cards_by_message_id[message_id] = state
|
|
147
|
+
logger.info(f"已记录卡片: msg={message_id}, card={card_id}")
|
|
148
|
+
return message_id
|
|
149
|
+
|
|
150
|
+
async def update_card_by_message_id(
|
|
151
|
+
self, message_id: str, card_content: Dict[str, Any]
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""按 message_id 就地更新卡片内容(通过 card_id 反查使用 CardKit update)"""
|
|
154
|
+
state = self._cards_by_message_id.get(message_id)
|
|
155
|
+
if not state:
|
|
156
|
+
logger.warning(f"未找到 message_id 对应的卡片状态: {message_id}")
|
|
157
|
+
return False
|
|
158
|
+
state.sequence += 1
|
|
159
|
+
return await self.update_card(state.card_id, state.sequence, card_content)
|
|
160
|
+
|
|
161
|
+
async def update_card(self, card_id: str, sequence: int, card_content: Dict[str, Any]) -> bool:
|
|
162
|
+
"""更新卡片内容(失败自动重试 1 次)"""
|
|
163
|
+
if not self.client:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
import asyncio
|
|
167
|
+
|
|
168
|
+
for attempt in range(2):
|
|
169
|
+
try:
|
|
170
|
+
update_uuid = f"{int(time.time() * 1000)}-{uuid.uuid4()}"
|
|
171
|
+
|
|
172
|
+
request = UpdateCardRequest.builder() \
|
|
173
|
+
.card_id(card_id) \
|
|
174
|
+
.request_body(
|
|
175
|
+
UpdateCardRequestBody.builder()
|
|
176
|
+
.uuid(update_uuid)
|
|
177
|
+
.sequence(sequence)
|
|
178
|
+
.card(
|
|
179
|
+
Card.builder()
|
|
180
|
+
.type("card_json")
|
|
181
|
+
.data(json.dumps(card_content, ensure_ascii=False))
|
|
182
|
+
.build()
|
|
183
|
+
)
|
|
184
|
+
.build()
|
|
185
|
+
) \
|
|
186
|
+
.build()
|
|
187
|
+
|
|
188
|
+
response = await asyncio.to_thread(
|
|
189
|
+
self.client.cardkit.v1.card.update, request
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if response.success():
|
|
193
|
+
return True
|
|
194
|
+
else:
|
|
195
|
+
logger.warning(f"更新卡片失败(attempt={attempt+1}): card_id={card_id} seq={sequence} code={response.code} msg={response.msg}")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"更新卡片异常(attempt={attempt+1}): card_id={card_id} seq={sequence} error={e}")
|
|
198
|
+
|
|
199
|
+
if attempt == 0:
|
|
200
|
+
await asyncio.sleep(1)
|
|
201
|
+
|
|
202
|
+
_track_stats('error', 'card_api', detail='update_card')
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
async def send_text(self, chat_id: str, text: str):
|
|
206
|
+
"""发送纯文本消息(备用)"""
|
|
207
|
+
if not self.client:
|
|
208
|
+
print(f"[Lark] 消息: {text}")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
import asyncio
|
|
213
|
+
|
|
214
|
+
request = CreateMessageRequest.builder() \
|
|
215
|
+
.receive_id_type("chat_id") \
|
|
216
|
+
.request_body(
|
|
217
|
+
CreateMessageRequestBody.builder()
|
|
218
|
+
.receive_id(chat_id)
|
|
219
|
+
.msg_type("text")
|
|
220
|
+
.content(json.dumps({"text": text}))
|
|
221
|
+
.build()
|
|
222
|
+
) \
|
|
223
|
+
.build()
|
|
224
|
+
|
|
225
|
+
response = await asyncio.to_thread(
|
|
226
|
+
self.client.im.v1.message.create, request
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if not response.success():
|
|
230
|
+
logger.warning(f"发送文本失败: code={response.code} msg={response.msg}")
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"发送文本异常: {e}")
|
|
233
|
+
|
|
234
|
+
# 管理活跃卡片的方法
|
|
235
|
+
def get_active_card(self, chat_id: str) -> Optional[CardState]:
|
|
236
|
+
"""获取聊天的活跃卡片"""
|
|
237
|
+
return self._active_cards.get(chat_id)
|
|
238
|
+
|
|
239
|
+
def set_active_card(self, chat_id: str, card_state: CardState):
|
|
240
|
+
"""设置聊天的活跃卡片"""
|
|
241
|
+
self._active_cards[chat_id] = card_state
|
|
242
|
+
|
|
243
|
+
def clear_active_card(self, chat_id: str):
|
|
244
|
+
"""清除聊天的活跃卡片"""
|
|
245
|
+
if chat_id in self._active_cards:
|
|
246
|
+
del self._active_cards[chat_id]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# 全局实例
|
|
250
|
+
card_service = CardService()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lark 客户端配置
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# 加载 .env 文件
|
|
10
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
11
|
+
load_dotenv(BASE_DIR / ".env")
|
|
12
|
+
|
|
13
|
+
# 飞书应用配置
|
|
14
|
+
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "")
|
|
15
|
+
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "")
|
|
16
|
+
|
|
17
|
+
# 用户白名单(逗号分隔的 open_id 列表)
|
|
18
|
+
ALLOWED_USERS = os.getenv("ALLOWED_USERS", "").split(",")
|
|
19
|
+
ENABLE_USER_WHITELIST = os.getenv("ENABLE_USER_WHITELIST", "false").lower() == "true"
|
|
20
|
+
|
|
21
|
+
# 机器人名称(用于群聊命名)
|
|
22
|
+
BOT_NAME = os.getenv("BOT_NAME", "Claude")
|