remote-claude 1.0.2 → 1.0.4
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 -0
- package/README.md +0 -2
- package/bin/cdx +9 -1
- package/bin/cl +9 -1
- package/bin/cla +9 -1
- package/bin/cx +9 -1
- package/bin/remote-claude +9 -18
- package/client/client.py +1 -1
- package/init.sh +30 -97
- package/lark_client/card_builder.py +60 -33
- package/lark_client/config.py +4 -0
- package/lark_client/lark_handler.py +316 -83
- package/lark_client/main.py +26 -6
- package/lark_client/session_bridge.py +4 -10
- package/lark_client/shared_memory_poller.py +193 -107
- package/package.json +1 -1
- package/pyproject.toml +1 -0
- package/remote_claude.py +226 -0
- package/server/parsers/claude_parser.py +43 -0
- package/server/parsers/codex_parser.py +8 -0
- package/server/server.py +31 -19
- package/utils/components.py +1 -0
- package/utils/session.py +81 -30
|
@@ -40,6 +40,7 @@ from utils.session import ensure_user_data_dir, USER_DATA_DIR
|
|
|
40
40
|
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
41
41
|
INITIAL_WINDOW = 30 # 首次 attach 最多显示最近 30 个 blocks
|
|
42
42
|
from .config import MAX_CARD_BLOCKS # 单张卡片最多 N 个 blocks → 超限冻结(可通过 .env 配置)
|
|
43
|
+
CARD_SIZE_LIMIT = 25 * 1024 # 25KB,飞书限制 30KB,留 5KB 余量
|
|
43
44
|
POLL_INTERVAL = 1.0 # 轮询间隔(秒)
|
|
44
45
|
RAPID_INTERVAL = 0.2 # 快速轮询间隔(秒)
|
|
45
46
|
RAPID_DURATION = 2.0 # 快速轮询持续时间(秒)
|
|
@@ -153,6 +154,16 @@ class SharedMemoryPoller:
|
|
|
153
154
|
if exc:
|
|
154
155
|
logger.error(f"轮询 Task 异常: chat_id={chat_id[:8]}..., {exc}", exc_info=exc)
|
|
155
156
|
|
|
157
|
+
def read_snapshot(self, chat_id: str) -> Optional[dict]:
|
|
158
|
+
"""直接读取指定 chat_id 的当前共享内存快照(供 handle_option_select 等即时查询使用)"""
|
|
159
|
+
tracker = self._trackers.get(chat_id)
|
|
160
|
+
if tracker and tracker.reader:
|
|
161
|
+
try:
|
|
162
|
+
return tracker.reader.read()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning(f"read_snapshot 失败: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
156
167
|
def kick(self, chat_id: str) -> None:
|
|
157
168
|
"""触发立即轮询并进入快速轮询模式"""
|
|
158
169
|
self._rapid_until[chat_id] = time.time() + RAPID_DURATION
|
|
@@ -191,8 +202,8 @@ class SharedMemoryPoller:
|
|
|
191
202
|
logger.error(f"_poll_once 异常: {e}", exc_info=True)
|
|
192
203
|
|
|
193
204
|
async def _poll_once(self, tracker: StreamTracker) -> None:
|
|
194
|
-
"""单次轮询:读取共享内存 → diff → 创建/更新卡片"""
|
|
195
|
-
#
|
|
205
|
+
"""单次轮询:读取共享内存 → diff → 创建/更新卡片 → 就绪通知"""
|
|
206
|
+
# 步骤 1:延迟初始化 Reader
|
|
196
207
|
if tracker.reader is None:
|
|
197
208
|
try:
|
|
198
209
|
from shared_state import get_mq_path, SharedStateReader
|
|
@@ -219,84 +230,114 @@ class SharedMemoryPoller:
|
|
|
219
230
|
agent_panel = state.get("agent_panel")
|
|
220
231
|
option_block = state.get("option_block")
|
|
221
232
|
cli_type = state.get("cli_type", "claude")
|
|
233
|
+
# timestamp 存在说明 server 已写入有效快照(即使内容全空,如 Codex 就绪等待输入)
|
|
234
|
+
has_valid_snapshot = state.get("timestamp") is not None
|
|
222
235
|
|
|
223
|
-
#
|
|
224
|
-
|
|
236
|
+
# 步骤 2:仅计算就绪状态,不发送通知
|
|
237
|
+
should_notify = self._update_ready_state(tracker, blocks, status_line, option_block, agent_panel)
|
|
225
238
|
|
|
239
|
+
# 步骤 3:卡片操作(含创建/更新/拆分)
|
|
240
|
+
await self._do_card_update(tracker, blocks, status_line, bottom_bar, agent_panel, option_block, cli_type, has_valid_snapshot=has_valid_snapshot)
|
|
241
|
+
|
|
242
|
+
# 步骤 4:通知在卡片操作之后发送,确保新卡先出现
|
|
243
|
+
if should_notify:
|
|
244
|
+
await self._send_ready_notification(tracker, cli_type)
|
|
245
|
+
|
|
246
|
+
async def _do_card_update(
|
|
247
|
+
self, tracker: StreamTracker, blocks: List[dict],
|
|
248
|
+
status_line: Optional[dict], bottom_bar: Optional[dict],
|
|
249
|
+
agent_panel: Optional[dict], option_block: Optional[dict],
|
|
250
|
+
cli_type: str,
|
|
251
|
+
has_valid_snapshot: bool = False,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""卡片操作主体:获取活跃卡片 → 创建/更新/拆分"""
|
|
226
254
|
# 获取活跃卡片(最后一张且未冻结)
|
|
227
255
|
active = None
|
|
228
256
|
if tracker.cards and not tracker.cards[-1].frozen:
|
|
229
257
|
active = tracker.cards[-1]
|
|
230
258
|
|
|
231
259
|
if not blocks and not status_line and not bottom_bar and not agent_panel and not option_block and active is None:
|
|
232
|
-
|
|
260
|
+
if not has_valid_snapshot:
|
|
261
|
+
return # 真的还没有快照,不创建卡片
|
|
262
|
+
# 有效快照但内容全空(如 Codex 就绪等待输入),继续创建就绪卡片
|
|
233
263
|
|
|
234
264
|
if active is None:
|
|
235
265
|
# 需要创建新卡片
|
|
236
266
|
await self._create_new_card(tracker, blocks, status_line, bottom_bar, agent_panel, option_block, cli_type=cli_type)
|
|
237
|
-
|
|
238
|
-
# 有活跃卡片,检查是否需要更新
|
|
239
|
-
blocks_slice = blocks[active.start_idx:]
|
|
267
|
+
return
|
|
240
268
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
logger.warning(
|
|
244
|
-
f"[blocks regression] len(blocks)={len(blocks)} < start_idx={active.start_idx}, "
|
|
245
|
-
f"resetting start_idx to 0 (session={tracker.session_name})"
|
|
246
|
-
)
|
|
247
|
-
active.start_idx = 0
|
|
248
|
-
blocks_slice = blocks[0:]
|
|
249
|
-
tracker.content_hash = "" # 强制刷新
|
|
269
|
+
# 有活跃卡片,检查是否需要更新
|
|
270
|
+
blocks_slice = blocks[active.start_idx:]
|
|
250
271
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
# blocks 骤降检测(compact/重启导致 blocks 从头累积)
|
|
273
|
+
if len(blocks) < active.start_idx:
|
|
274
|
+
logger.warning(
|
|
275
|
+
f"[blocks regression] len(blocks)={len(blocks)} < start_idx={active.start_idx}, "
|
|
276
|
+
f"resetting start_idx to 0 (session={tracker.session_name})"
|
|
277
|
+
)
|
|
278
|
+
active.start_idx = 0
|
|
279
|
+
blocks_slice = blocks[0:]
|
|
280
|
+
tracker.content_hash = "" # 强制刷新
|
|
255
281
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
282
|
+
# 超限检查
|
|
283
|
+
if len(blocks_slice) > MAX_CARD_BLOCKS:
|
|
284
|
+
await self._freeze_and_split(tracker, blocks, status_line, bottom_bar, agent_panel, option_block, cli_type=cli_type)
|
|
285
|
+
return
|
|
260
286
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
# hash diff
|
|
288
|
+
new_hash = self._compute_hash(blocks_slice, status_line, bottom_bar, agent_panel, option_block)
|
|
289
|
+
if new_hash == tracker.content_hash:
|
|
290
|
+
return # 无变化
|
|
291
|
+
|
|
292
|
+
# 更新卡片
|
|
293
|
+
from .card_builder import build_stream_card
|
|
294
|
+
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
264
295
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
296
|
+
# 大小超限检查(与 blocks 数量超限同一套逻辑)
|
|
297
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
298
|
+
if card_size > CARD_SIZE_LIMIT:
|
|
299
|
+
freeze_count = self._find_freeze_count(blocks_slice, tracker.session_name)
|
|
300
|
+
await self._freeze_and_split(
|
|
301
|
+
tracker, blocks, status_line, bottom_bar, agent_panel, option_block,
|
|
302
|
+
cli_type=cli_type, freeze_count=freeze_count,
|
|
270
303
|
)
|
|
304
|
+
return
|
|
271
305
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return
|
|
279
|
-
elif not success:
|
|
280
|
-
# 降级:创建新卡片替代
|
|
281
|
-
logger.warning(
|
|
282
|
-
f"update_card 失败 card_id={active.card_id} seq={active.sequence},降级为新卡片"
|
|
283
|
-
)
|
|
284
|
-
_track_stats('card', 'fallback', session_name=tracker.session_name,
|
|
285
|
-
chat_id=tracker.chat_id)
|
|
286
|
-
new_card_id = await self._card_service.create_card(card_dict)
|
|
287
|
-
if new_card_id:
|
|
288
|
-
await self._card_service.send_card(tracker.chat_id, new_card_id)
|
|
289
|
-
active.card_id = new_card_id
|
|
290
|
-
active.sequence = 0
|
|
291
|
-
else:
|
|
292
|
-
_track_stats('card', 'update', session_name=tracker.session_name,
|
|
293
|
-
chat_id=tracker.chat_id)
|
|
306
|
+
active.sequence += 1
|
|
307
|
+
success = await self._card_service.update_card(
|
|
308
|
+
card_id=active.card_id,
|
|
309
|
+
sequence=active.sequence,
|
|
310
|
+
card_content=card_dict,
|
|
311
|
+
)
|
|
294
312
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
if getattr(success, 'is_element_limit', False):
|
|
314
|
+
# 元素超限:冻结旧卡 + 推新流式卡
|
|
315
|
+
await self._handle_element_limit(
|
|
316
|
+
tracker, blocks, status_line, bottom_bar, agent_panel, option_block,
|
|
317
|
+
cli_type=cli_type,
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
elif not success:
|
|
321
|
+
# 降级:创建新卡片替代
|
|
322
|
+
logger.warning(
|
|
323
|
+
f"update_card 失败 card_id={active.card_id} seq={active.sequence},降级为新卡片"
|
|
299
324
|
)
|
|
325
|
+
_track_stats('card', 'fallback', session_name=tracker.session_name,
|
|
326
|
+
chat_id=tracker.chat_id)
|
|
327
|
+
new_card_id = await self._card_service.create_card(card_dict)
|
|
328
|
+
if new_card_id:
|
|
329
|
+
await self._card_service.send_card(tracker.chat_id, new_card_id)
|
|
330
|
+
active.card_id = new_card_id
|
|
331
|
+
active.sequence = 0
|
|
332
|
+
else:
|
|
333
|
+
_track_stats('card', 'update', session_name=tracker.session_name,
|
|
334
|
+
chat_id=tracker.chat_id)
|
|
335
|
+
|
|
336
|
+
tracker.content_hash = new_hash
|
|
337
|
+
logger.debug(
|
|
338
|
+
f"[UPDATE] session={tracker.session_name} blocks={len(blocks_slice)} "
|
|
339
|
+
f"seq={active.sequence} hash={new_hash[:8]}"
|
|
340
|
+
)
|
|
300
341
|
|
|
301
342
|
async def _create_new_card(
|
|
302
343
|
self, tracker: StreamTracker, blocks: List[dict],
|
|
@@ -321,11 +362,20 @@ class SharedMemoryPoller:
|
|
|
321
362
|
)
|
|
322
363
|
|
|
323
364
|
blocks_slice = blocks[start_idx:]
|
|
324
|
-
|
|
325
|
-
|
|
365
|
+
# 注意:不在此处提前 return,上层 _do_card_update 已做过滤,
|
|
366
|
+
# 走到这里说明确实需要创建卡片(如 Codex 就绪等待输入的空内容卡片)
|
|
326
367
|
|
|
327
368
|
from .card_builder import build_stream_card
|
|
328
369
|
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
370
|
+
|
|
371
|
+
# 新卡大小检查:超限则从头部裁剪
|
|
372
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
373
|
+
while card_size > CARD_SIZE_LIMIT and len(blocks_slice) > 1:
|
|
374
|
+
blocks_slice = blocks_slice[1:]
|
|
375
|
+
start_idx += 1
|
|
376
|
+
card_dict = build_stream_card(blocks_slice, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
377
|
+
card_size = len(json.dumps(card_dict, ensure_ascii=False).encode('utf-8'))
|
|
378
|
+
|
|
329
379
|
card_id = await self._card_service.create_card(card_dict)
|
|
330
380
|
|
|
331
381
|
if card_id:
|
|
@@ -384,6 +434,24 @@ class SharedMemoryPoller:
|
|
|
384
434
|
f"[ELEMENT_LIMIT_SPLIT] session={tracker.session_name} "
|
|
385
435
|
f"new_start={new_start} blocks={len(new_blocks)} card_id={new_card_id}"
|
|
386
436
|
)
|
|
437
|
+
tracker.last_notify_message_id = None
|
|
438
|
+
|
|
439
|
+
def _find_freeze_count(self, blocks_slice: List[dict], session_name: str) -> int:
|
|
440
|
+
"""二分查找冻结卡片能容纳的最大 blocks 数(保证卡片 JSON 大小 ≤ CARD_SIZE_LIMIT)"""
|
|
441
|
+
from .card_builder import build_stream_card
|
|
442
|
+
lo, hi = 1, len(blocks_slice)
|
|
443
|
+
result = 1
|
|
444
|
+
while lo <= hi:
|
|
445
|
+
mid = (lo + hi) // 2
|
|
446
|
+
card = build_stream_card(blocks_slice[:mid], None, None,
|
|
447
|
+
is_frozen=True, session_name=session_name)
|
|
448
|
+
size = len(json.dumps(card, ensure_ascii=False).encode('utf-8'))
|
|
449
|
+
if size <= CARD_SIZE_LIMIT:
|
|
450
|
+
result = mid
|
|
451
|
+
lo = mid + 1
|
|
452
|
+
else:
|
|
453
|
+
hi = mid - 1
|
|
454
|
+
return result
|
|
387
455
|
|
|
388
456
|
async def _freeze_and_split(
|
|
389
457
|
self, tracker: StreamTracker, blocks: List[dict],
|
|
@@ -391,12 +459,15 @@ class SharedMemoryPoller:
|
|
|
391
459
|
agent_panel: Optional[dict] = None,
|
|
392
460
|
option_block: Optional[dict] = None,
|
|
393
461
|
cli_type: str = "claude",
|
|
462
|
+
freeze_count: Optional[int] = None,
|
|
394
463
|
) -> None:
|
|
395
464
|
"""冻结当前卡片 + 开新卡"""
|
|
396
465
|
active = tracker.cards[-1]
|
|
466
|
+
count = freeze_count if freeze_count is not None else MAX_CARD_BLOCKS
|
|
467
|
+
reason = 'size' if freeze_count is not None else 'count'
|
|
397
468
|
|
|
398
|
-
# 冻结当前卡片(只保留前
|
|
399
|
-
frozen_blocks = blocks[active.start_idx:active.start_idx +
|
|
469
|
+
# 冻结当前卡片(只保留前 count 个 blocks,移除状态区和按钮)
|
|
470
|
+
frozen_blocks = blocks[active.start_idx:active.start_idx + count]
|
|
400
471
|
from .card_builder import build_stream_card
|
|
401
472
|
frozen_card = build_stream_card(frozen_blocks, None, None, is_frozen=True)
|
|
402
473
|
active.sequence += 1
|
|
@@ -406,16 +477,25 @@ class SharedMemoryPoller:
|
|
|
406
477
|
chat_id=tracker.chat_id)
|
|
407
478
|
logger.info(
|
|
408
479
|
f"[FREEZE] session={tracker.session_name} card_id={active.card_id} "
|
|
409
|
-
f"blocks=[{active.start_idx}:{active.start_idx +
|
|
480
|
+
f"blocks=[{active.start_idx}:{active.start_idx + count}] reason={reason}"
|
|
410
481
|
)
|
|
411
482
|
|
|
412
483
|
# 创建新卡片
|
|
413
|
-
new_start = active.start_idx +
|
|
484
|
+
new_start = active.start_idx + count
|
|
414
485
|
new_blocks = blocks[new_start:]
|
|
415
486
|
if not new_blocks:
|
|
416
487
|
return
|
|
417
488
|
|
|
418
489
|
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
490
|
+
|
|
491
|
+
# 新卡大小检查:超限则从头部裁剪
|
|
492
|
+
new_card_size = len(json.dumps(new_card_dict, ensure_ascii=False).encode('utf-8'))
|
|
493
|
+
while new_card_size > CARD_SIZE_LIMIT and len(new_blocks) > 1:
|
|
494
|
+
new_blocks = new_blocks[1:]
|
|
495
|
+
new_start += 1
|
|
496
|
+
new_card_dict = build_stream_card(new_blocks, status_line, bottom_bar, agent_panel=agent_panel, option_block=option_block, session_name=tracker.session_name, cli_type=cli_type)
|
|
497
|
+
new_card_size = len(json.dumps(new_card_dict, ensure_ascii=False).encode('utf-8'))
|
|
498
|
+
|
|
419
499
|
new_card_id = await self._card_service.create_card(new_card_dict)
|
|
420
500
|
if new_card_id:
|
|
421
501
|
await self._card_service.send_card(tracker.chat_id, new_card_id)
|
|
@@ -425,54 +505,59 @@ class SharedMemoryPoller:
|
|
|
425
505
|
f"[NEW after FREEZE] session={tracker.session_name} start_idx={new_start} "
|
|
426
506
|
f"blocks={len(new_blocks)} card_id={new_card_id}"
|
|
427
507
|
)
|
|
508
|
+
tracker.last_notify_message_id = None
|
|
428
509
|
|
|
429
|
-
|
|
510
|
+
def _update_ready_state(
|
|
430
511
|
self, tracker: StreamTracker,
|
|
431
512
|
blocks: list, status_line: Optional[dict], option_block: Optional[dict],
|
|
432
|
-
|
|
433
|
-
) ->
|
|
434
|
-
"""
|
|
435
|
-
current_ready = _is_ready(blocks, status_line, option_block)
|
|
513
|
+
agent_panel: Optional[dict] = None,
|
|
514
|
+
) -> bool:
|
|
515
|
+
"""更新就绪状态,返回是否需要发送就绪通知(不执行发送)"""
|
|
516
|
+
current_ready = _is_ready(blocks, status_line, option_block, agent_panel)
|
|
436
517
|
prev_ready = tracker.prev_is_ready
|
|
437
518
|
tracker.prev_is_ready = current_ready
|
|
519
|
+
return current_ready and not prev_ready and tracker.is_group and _notify_enabled
|
|
438
520
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
except Exception as e:
|
|
465
|
-
logger.warning(f"加急通知失败: {e}")
|
|
466
|
-
else:
|
|
467
|
-
# 首次通知(或无法加急时)→ 发新消息,记录 message_id
|
|
468
|
-
label = "所有人" if uid == "all" else ""
|
|
469
|
-
text = f'<at user_id="{uid}">{label}</at> {cli_name} 已就绪,等待您的输入...(这是第{count}次通知)'
|
|
470
|
-
try:
|
|
521
|
+
async def _send_ready_notification(
|
|
522
|
+
self, tracker: StreamTracker, cli_type: str = "claude"
|
|
523
|
+
) -> None:
|
|
524
|
+
"""发送就绪通知(加急或新消息),应在卡片操作完成后调用"""
|
|
525
|
+
count = _increment_ready_count()
|
|
526
|
+
uid = tracker.notify_user_id or "all"
|
|
527
|
+
cli_name = "Claude" if cli_type == "claude" else "Codex"
|
|
528
|
+
logger.info(f"就绪提醒: chat_id={tracker.chat_id[:8]}..., count={count}, uid={uid}, "
|
|
529
|
+
f"last_msg={'有' if tracker.last_notify_message_id else '无'}")
|
|
530
|
+
|
|
531
|
+
if tracker.last_notify_message_id and uid != "all" and _urgent_enabled:
|
|
532
|
+
# 已有通知消息 + 加急开关开启 → 尝试加急
|
|
533
|
+
try:
|
|
534
|
+
ok = await self._card_service.send_urgent_app(
|
|
535
|
+
tracker.last_notify_message_id, [uid]
|
|
536
|
+
)
|
|
537
|
+
if ok:
|
|
538
|
+
# 加急成功 → 5 秒后自动取消
|
|
539
|
+
asyncio.create_task(self._cancel_urgent_later(
|
|
540
|
+
tracker.last_notify_message_id, [uid], delay=5
|
|
541
|
+
))
|
|
542
|
+
else:
|
|
543
|
+
# 加急失败(权限未开通等)→ 降级发新消息
|
|
544
|
+
label = ""
|
|
545
|
+
text = f'<at user_id="{uid}">{label}</at> {cli_name} 已就绪,等待您的输入...(这是第{count}次通知)'
|
|
471
546
|
msg_id = await self._card_service.send_text(tracker.chat_id, text)
|
|
472
547
|
if msg_id:
|
|
473
548
|
tracker.last_notify_message_id = msg_id
|
|
474
|
-
|
|
475
|
-
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.warning(f"加急通知失败: {e}")
|
|
551
|
+
else:
|
|
552
|
+
# 首次通知(或无法加急时)→ 发新消息,记录 message_id
|
|
553
|
+
label = "所有人" if uid == "all" else ""
|
|
554
|
+
text = f'<at user_id="{uid}">{label}</at> {cli_name} 已就绪,等待您的输入...(这是第{count}次通知)'
|
|
555
|
+
try:
|
|
556
|
+
msg_id = await self._card_service.send_text(tracker.chat_id, text)
|
|
557
|
+
if msg_id:
|
|
558
|
+
tracker.last_notify_message_id = msg_id
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.warning(f"就绪提醒发送失败: {e}")
|
|
476
561
|
|
|
477
562
|
async def _cancel_urgent_later(self, message_id: str, user_ids: list, delay: float = 15) -> None:
|
|
478
563
|
"""延迟取消加急通知"""
|
|
@@ -536,10 +621,11 @@ class SharedMemoryPoller:
|
|
|
536
621
|
|
|
537
622
|
# ── 模块级辅助函数 ────────────────────────────────────────────────────────────
|
|
538
623
|
|
|
539
|
-
def _is_ready(blocks: list, status_line: Optional[dict], option_block: Optional[dict]) -> bool:
|
|
540
|
-
"""数据层就绪判断:无 streaming block、无 status_line(option_block 不影响就绪)"""
|
|
624
|
+
def _is_ready(blocks: list, status_line: Optional[dict], option_block: Optional[dict], agent_panel: Optional[dict] = None) -> bool:
|
|
625
|
+
"""数据层就绪判断:无 streaming block、无 status_line、无后台 agent(option_block 不影响就绪)"""
|
|
541
626
|
has_streaming = any(b.get("is_streaming", False) for b in blocks)
|
|
542
|
-
|
|
627
|
+
has_agents = agent_panel is not None
|
|
628
|
+
return not has_streaming and status_line is None and not has_agents
|
|
543
629
|
|
|
544
630
|
|
|
545
631
|
_READY_COUNT_FILE = USER_DATA_DIR / "ready_notify_count"
|
package/package.json
CHANGED