asemxin commited on
Commit
c811854
·
1 Parent(s): 7eb8e6d

fix: image_daemon v2 with detailed logging

Browse files
Files changed (1) hide show
  1. image_daemon.py +74 -31
image_daemon.py CHANGED
@@ -1,17 +1,18 @@
1
  #!/usr/bin/env python3
2
  """
3
- 飞书图片预处理守护进程 (image_daemon.py)
4
  后台运行,自动检测飞书聊天中的新图片消息,下载并上传到图床,
5
  然后以文本消息回复图片 URL,让 OpenClaw agent 直接看到可访问的 URL。
6
  """
7
- import os, sys, json, time, requests, hashlib
8
 
9
  FEISHU_BASE = "https://open.feishu.cn/open-apis"
10
- POLL_INTERVAL = int(os.environ.get("IMAGE_POLL_INTERVAL", "3"))
11
  processed_file = "/tmp/image_daemon_processed.json"
12
 
13
  def log(msg):
14
- print(f"[image_daemon] {msg}", flush=True)
 
15
 
16
  def load_processed():
17
  try:
@@ -22,16 +23,20 @@ def load_processed():
22
 
23
  def save_processed(s):
24
  with open(processed_file, "w") as f:
25
- json.dump(list(s)[-500:], f) # keep last 500
26
 
27
  def get_tenant_token():
28
  app_id = os.environ.get("FEISHU_APP_ID")
29
  app_secret = os.environ.get("FEISHU_APP_SECRET")
 
 
 
30
  resp = requests.post(f"{FEISHU_BASE}/auth/v3/tenant_access_token/internal",
31
  json={"app_id": app_id, "app_secret": app_secret}, timeout=10)
32
  data = resp.json()
33
  if data.get("code") != 0:
34
- raise Exception(f"token error: {data}")
 
35
  return data["tenant_access_token"]
36
 
37
  def get_bot_chats(token):
@@ -40,9 +45,14 @@ def get_bot_chats(token):
40
  resp = requests.get(f"{FEISHU_BASE}/im/v1/chats",
41
  headers=headers, params={"page_size": 20}, timeout=10)
42
  data = resp.json()
43
- if data.get("code") != 0:
 
 
 
 
44
  return []
45
- return [c["chat_id"] for c in data.get("data", {}).get("items", [])]
 
46
 
47
  def get_recent_messages(token, chat_id, limit=10):
48
  """获取聊天中最近的消息"""
@@ -56,7 +66,11 @@ def get_recent_messages(token, chat_id, limit=10):
56
  "page_size": limit
57
  }, timeout=10)
58
  data = resp.json()
59
- if data.get("code") != 0:
 
 
 
 
60
  return []
61
  return data.get("data", {}).get("items", [])
62
 
@@ -75,7 +89,6 @@ def extract_image_keys(msg):
75
  if k:
76
  keys.append(k)
77
  elif msg_type == "post":
78
- # rich text / forwarded messages
79
  for lang in ["zh_cn", "en_us"]:
80
  lang_content = content.get(lang, {})
81
  if isinstance(lang_content, dict):
@@ -89,16 +102,16 @@ def extract_image_keys(msg):
89
  return keys
90
 
91
  def download_image(token, image_key):
92
- """从飞书下载图片"""
93
  headers = {"Authorization": f"Bearer {token}"}
94
  resp = requests.get(f"{FEISHU_BASE}/im/v1/images/{image_key}",
95
  headers=headers, timeout=30)
96
  if resp.status_code == 200 and len(resp.content) > 100:
97
  return resp.content
 
 
98
  return None
99
 
100
  def upload_image(data):
101
- """上传到免费图床"""
102
  # catbox.moe
103
  try:
104
  resp = requests.post("https://catbox.moe/user/api.php",
@@ -106,16 +119,18 @@ def upload_image(data):
106
  files={"filedata": ("img.jpg", data, "image/jpeg")}, timeout=30)
107
  if resp.status_code == 200 and resp.text.startswith("http"):
108
  return resp.text.strip()
109
- except:
110
- pass
 
111
  # 0x0.st
112
  try:
113
  resp = requests.post("https://0x0.st",
114
  files={"file": ("img.jpg", data, "image/jpeg")}, timeout=30)
115
  if resp.status_code == 200 and resp.text.startswith("http"):
116
  return resp.text.strip()
117
- except:
118
- pass
 
119
  # tmpfiles
120
  try:
121
  resp = requests.post("https://tmpfiles.org/api/v1/upload",
@@ -124,31 +139,32 @@ def upload_image(data):
124
  url = resp.json().get("data", {}).get("url", "")
125
  if url:
126
  return url.replace("tmpfiles.org/", "tmpfiles.org/dl/")
127
- except:
128
- pass
 
129
  return None
130
 
131
  def reply_text(token, chat_id, msg_id, text):
132
- """在飞书聊天中回复文本消息"""
133
  headers = {
134
  "Authorization": f"Bearer {token}",
135
  "Content-Type": "application/json; charset=utf-8"
136
  }
137
- # 作为回复发送
138
  resp = requests.post(f"{FEISHU_BASE}/im/v1/messages/{msg_id}/reply",
139
  headers=headers,
140
  json={
141
  "content": json.dumps({"text": text}),
142
  "msg_type": "text"
143
  }, timeout=10)
144
- return resp.json()
 
 
 
 
 
145
 
146
  def process_message(token, chat_id, msg):
147
- """处理单条消息"""
148
  msg_id = msg.get("message_id", "")
149
  sender_type = msg.get("sender", {}).get("sender_type", "")
150
-
151
- # 跳过 bot 自己发的消息
152
  if sender_type == "app":
153
  return
154
 
@@ -156,8 +172,9 @@ def process_message(token, chat_id, msg):
156
  if not image_keys:
157
  return
158
 
 
159
  urls = []
160
- for key in image_keys[:3]: # 最多处理 3 张
161
  log(f"📥 下载图片 {key}")
162
  data = download_image(token, key)
163
  if data:
@@ -167,33 +184,56 @@ def process_message(token, chat_id, msg):
167
  urls.append(url)
168
  log(f"✅ {url}")
169
  else:
170
- # 保存到本地
171
  path = f"/tmp/{key}.jpg"
172
  with open(path, "wb") as f:
173
  f.write(data)
174
- log(f"⚠️ 图床失败,本地保存: {path}")
175
 
176
  if urls:
177
  url_text = "\n".join(urls)
178
  reply = f"[系统] 图片已自动处理,可通过以下链接查看:\n{url_text}"
179
  result = reply_text(token, chat_id, msg_id, reply)
180
- log(f"📤 已回复图片链接到聊天 (code={result.get('code', '?')})")
181
 
182
  def main():
183
  log("🚀 启动中...")
 
 
 
 
 
184
  processed = load_processed()
185
  token = None
186
  token_time = 0
 
187
 
188
  while True:
189
  try:
 
190
  # 每 30 分钟刷新一次 token
191
  if not token or time.time() - token_time > 1800:
192
  token = get_tenant_token()
 
 
 
 
193
  token_time = time.time()
194
  log("🔑 Token 已刷新")
195
 
196
  chats = get_bot_chats(token)
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  for chat_id in chats:
198
  messages = get_recent_messages(token, chat_id, limit=5)
199
  for msg in messages:
@@ -202,15 +242,18 @@ def main():
202
  continue
203
  processed.add(msg_id)
204
 
205
- # 检查是否包含图片
206
  msg_type = msg.get("msg_type", "")
207
  if msg_type in ("image", "post"):
208
  process_message(token, chat_id, msg)
 
209
 
210
- save_processed(processed)
 
211
 
212
  except Exception as e:
213
- log(f"❌ 错误: {e}")
 
 
214
 
215
  time.sleep(POLL_INTERVAL)
216
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ 飞书图片预处理守护进程 (image_daemon.py) v2
4
  后台运行,自动检测飞书聊天中的新图片消息,下载并上传到图床,
5
  然后以文本消息回复图片 URL,让 OpenClaw agent 直接看到可访问的 URL。
6
  """
7
+ import os, sys, json, time, requests
8
 
9
  FEISHU_BASE = "https://open.feishu.cn/open-apis"
10
+ POLL_INTERVAL = int(os.environ.get("IMAGE_POLL_INTERVAL", "5"))
11
  processed_file = "/tmp/image_daemon_processed.json"
12
 
13
  def log(msg):
14
+ ts = time.strftime("%H:%M:%S")
15
+ print(f"[image_daemon {ts}] {msg}", flush=True)
16
 
17
  def load_processed():
18
  try:
 
23
 
24
  def save_processed(s):
25
  with open(processed_file, "w") as f:
26
+ json.dump(list(s)[-500:], f)
27
 
28
  def get_tenant_token():
29
  app_id = os.environ.get("FEISHU_APP_ID")
30
  app_secret = os.environ.get("FEISHU_APP_SECRET")
31
+ if not app_id or not app_secret:
32
+ log(f"❌ 环境变量缺失: FEISHU_APP_ID={'set' if app_id else 'MISSING'}, FEISHU_APP_SECRET={'set' if app_secret else 'MISSING'}")
33
+ return None
34
  resp = requests.post(f"{FEISHU_BASE}/auth/v3/tenant_access_token/internal",
35
  json={"app_id": app_id, "app_secret": app_secret}, timeout=10)
36
  data = resp.json()
37
  if data.get("code") != 0:
38
+ log(f" Token 获取失败: {data}")
39
+ return None
40
  return data["tenant_access_token"]
41
 
42
  def get_bot_chats(token):
 
45
  resp = requests.get(f"{FEISHU_BASE}/im/v1/chats",
46
  headers=headers, params={"page_size": 20}, timeout=10)
47
  data = resp.json()
48
+ code = data.get("code", -1)
49
+ if code != 0:
50
+ msg = data.get("msg", "unknown")
51
+ log(f"❌ 获取聊天列表失败 (code={code}): {msg}")
52
+ log(f" 需要权限: im:chat / im:chat:readonly")
53
  return []
54
+ items = data.get("data", {}).get("items", [])
55
+ return [c["chat_id"] for c in items]
56
 
57
  def get_recent_messages(token, chat_id, limit=10):
58
  """获取聊天中最近的消息"""
 
66
  "page_size": limit
67
  }, timeout=10)
68
  data = resp.json()
69
+ code = data.get("code", -1)
70
+ if code != 0:
71
+ msg = data.get("msg", "unknown")
72
+ log(f"❌ 获取消息失败 chat={chat_id} (code={code}): {msg}")
73
+ log(f" 需要权限: im:message / im:message:readonly")
74
  return []
75
  return data.get("data", {}).get("items", [])
76
 
 
89
  if k:
90
  keys.append(k)
91
  elif msg_type == "post":
 
92
  for lang in ["zh_cn", "en_us"]:
93
  lang_content = content.get(lang, {})
94
  if isinstance(lang_content, dict):
 
102
  return keys
103
 
104
  def download_image(token, image_key):
 
105
  headers = {"Authorization": f"Bearer {token}"}
106
  resp = requests.get(f"{FEISHU_BASE}/im/v1/images/{image_key}",
107
  headers=headers, timeout=30)
108
  if resp.status_code == 200 and len(resp.content) > 100:
109
  return resp.content
110
+ log(f"❌ 下载图片失败 {image_key}: HTTP {resp.status_code}, {resp.text[:200]}")
111
+ log(f" 需要权限: im:resource")
112
  return None
113
 
114
  def upload_image(data):
 
115
  # catbox.moe
116
  try:
117
  resp = requests.post("https://catbox.moe/user/api.php",
 
119
  files={"filedata": ("img.jpg", data, "image/jpeg")}, timeout=30)
120
  if resp.status_code == 200 and resp.text.startswith("http"):
121
  return resp.text.strip()
122
+ log(f"⚠️ catbox 失败: HTTP {resp.status_code}")
123
+ except Exception as e:
124
+ log(f"⚠️ catbox 异常: {e}")
125
  # 0x0.st
126
  try:
127
  resp = requests.post("https://0x0.st",
128
  files={"file": ("img.jpg", data, "image/jpeg")}, timeout=30)
129
  if resp.status_code == 200 and resp.text.startswith("http"):
130
  return resp.text.strip()
131
+ log(f"⚠️ 0x0 失败: HTTP {resp.status_code}")
132
+ except Exception as e:
133
+ log(f"⚠️ 0x0 异常: {e}")
134
  # tmpfiles
135
  try:
136
  resp = requests.post("https://tmpfiles.org/api/v1/upload",
 
139
  url = resp.json().get("data", {}).get("url", "")
140
  if url:
141
  return url.replace("tmpfiles.org/", "tmpfiles.org/dl/")
142
+ log(f"⚠️ tmpfiles 失败: HTTP {resp.status_code}")
143
+ except Exception as e:
144
+ log(f"⚠️ tmpfiles 异常: {e}")
145
  return None
146
 
147
  def reply_text(token, chat_id, msg_id, text):
 
148
  headers = {
149
  "Authorization": f"Bearer {token}",
150
  "Content-Type": "application/json; charset=utf-8"
151
  }
 
152
  resp = requests.post(f"{FEISHU_BASE}/im/v1/messages/{msg_id}/reply",
153
  headers=headers,
154
  json={
155
  "content": json.dumps({"text": text}),
156
  "msg_type": "text"
157
  }, timeout=10)
158
+ data = resp.json()
159
+ code = data.get("code", -1)
160
+ if code != 0:
161
+ log(f"❌ 回复消息失败 (code={code}): {data.get('msg', '')}")
162
+ log(f" 需要权限: im:message / im:message:create_as_bot")
163
+ return data
164
 
165
  def process_message(token, chat_id, msg):
 
166
  msg_id = msg.get("message_id", "")
167
  sender_type = msg.get("sender", {}).get("sender_type", "")
 
 
168
  if sender_type == "app":
169
  return
170
 
 
172
  if not image_keys:
173
  return
174
 
175
+ log(f"🖼️ 发现 {len(image_keys)} 张图片 (msg_id={msg_id[:16]}...)")
176
  urls = []
177
+ for key in image_keys[:3]:
178
  log(f"📥 下载图片 {key}")
179
  data = download_image(token, key)
180
  if data:
 
184
  urls.append(url)
185
  log(f"✅ {url}")
186
  else:
 
187
  path = f"/tmp/{key}.jpg"
188
  with open(path, "wb") as f:
189
  f.write(data)
190
+ log(f"⚠️ 图床全部失败,本地保存: {path}")
191
 
192
  if urls:
193
  url_text = "\n".join(urls)
194
  reply = f"[系统] 图片已自动处理,可通过以下链接查看:\n{url_text}"
195
  result = reply_text(token, chat_id, msg_id, reply)
196
+ log(f"📤 已回复 (code={result.get('code', '?')})")
197
 
198
  def main():
199
  log("🚀 启动中...")
200
+
201
+ # 环境诊断
202
+ app_id = os.environ.get("FEISHU_APP_ID", "")
203
+ log(f"📋 飞书 App ID: {app_id[:10]}..." if app_id else "❌ FEISHU_APP_ID 未设置!")
204
+
205
  processed = load_processed()
206
  token = None
207
  token_time = 0
208
+ cycle = 0
209
 
210
  while True:
211
  try:
212
+ cycle += 1
213
  # 每 30 分钟刷新一次 token
214
  if not token or time.time() - token_time > 1800:
215
  token = get_tenant_token()
216
+ if not token:
217
+ log("⏳ Token 获取失败,60 秒后重试...")
218
+ time.sleep(60)
219
+ continue
220
  token_time = time.time()
221
  log("🔑 Token 已刷新")
222
 
223
  chats = get_bot_chats(token)
224
+
225
+ # 首次和每 100 轮打印状态
226
+ if cycle == 1 or cycle % 100 == 0:
227
+ log(f"📊 轮询中... 找到 {len(chats)} 个聊天 (第 {cycle} 轮)")
228
+
229
+ if not chats and cycle == 1:
230
+ log("⚠️ 未找到任何聊天!可能原因:")
231
+ log(" 1. 飞书 App 缺少 im:chat 或 im:chat:readonly 权限")
232
+ log(" 2. 还没有用户跟 bot 发过消息")
233
+ log(" 3. App 未正确发布")
234
+ log(" 请在飞书开发者后台开启权限后重新发布")
235
+
236
+ new_imgs = 0
237
  for chat_id in chats:
238
  messages = get_recent_messages(token, chat_id, limit=5)
239
  for msg in messages:
 
242
  continue
243
  processed.add(msg_id)
244
 
 
245
  msg_type = msg.get("msg_type", "")
246
  if msg_type in ("image", "post"):
247
  process_message(token, chat_id, msg)
248
+ new_imgs += 1
249
 
250
+ if new_imgs > 0:
251
+ save_processed(processed)
252
 
253
  except Exception as e:
254
+ log(f"❌ 错误: {type(e).__name__}: {e}")
255
+ import traceback
256
+ traceback.print_exc()
257
 
258
  time.sleep(POLL_INTERVAL)
259