@brandon_9527/tcode 1.0.8 → 1.0.10

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.
Files changed (58) hide show
  1. package/dist/python-src/.env +1 -1
  2. package/dist/python-src/README.md +48 -1
  3. package/dist/python-src/_workspace/.autodev/config.json +12 -0
  4. package/dist/python-src/_workspace/.autodev/cron/jobs.json +4 -0
  5. package/dist/python-src/entry.py +21 -1
  6. package/dist/python-src/main.py +763 -42
  7. package/dist/python-src/pyproject.toml +1 -0
  8. package/dist/python-src/run.sh +9 -0
  9. package/dist/python-src/src/agents/token_tracker.py +4 -4
  10. package/dist/python-src/src/claw/bus/queue.py +1 -1
  11. package/dist/python-src/src/claw/channels/__init__.py +2 -2
  12. package/dist/python-src/src/claw/channels/base.py +2 -2
  13. package/dist/python-src/src/claw/channels/feishu.py +57 -16
  14. package/dist/python-src/src/claw/channels/manager.py +2 -2
  15. package/dist/python-src/src/claw/config/__init__.py +3 -0
  16. package/dist/python-src/src/claw/config/loader.py +38 -0
  17. package/dist/python-src/src/claw/config/schema.py +14 -29
  18. package/dist/python-src/src/claw/cron/__init__.py +3 -0
  19. package/dist/python-src/src/claw/cron/service.py +171 -0
  20. package/dist/python-src/src/claw/cron/types_.py +14 -0
  21. package/dist/python-src/src/claw/heartbeat/__init__.py +2 -0
  22. package/dist/python-src/src/claw/heartbeat/service.py +55 -0
  23. package/dist/python-src/src/claw/run.py +82 -0
  24. package/dist/python-src/src/claw/tools/base.py +23 -0
  25. package/dist/python-src/src/claw/tools/channel.py +0 -0
  26. package/dist/python-src/src/claw/tools/cron.py +138 -0
  27. package/dist/python-src/src/claw/utils/__init__.py +2 -0
  28. package/dist/python-src/src/claw/utils/helpers.py +27 -0
  29. package/dist/python-src/src/core/context.py +158 -0
  30. package/dist/python-src/src/managers/manager_agent.py +9 -9
  31. package/dist/python-src/src/managers/manager_command.py +62 -0
  32. package/dist/python-src/src/managers/manager_context.py +1 -1
  33. package/dist/python-src/src/managers/manager_instruction.py +7 -7
  34. package/dist/python-src/src/managers/manager_skill.py +3 -3
  35. package/dist/python-src/src/managers/sandbox.py +3 -3
  36. package/dist/python-src/src/middlewares/dynamic_content.py +2 -2
  37. package/dist/python-src/src/middlewares/hitl.py +3 -3
  38. package/dist/python-src/src/middlewares/memory.py +2 -2
  39. package/dist/python-src/src/middlewares/subagents.py +4 -4
  40. package/dist/python-src/src/middlewares/summary.py +37 -37
  41. package/dist/python-src/src/stream/file_write_parser.py +3 -3
  42. package/dist/python-src/src/stream/formatter.py +19 -19
  43. package/dist/python-src/src/stream/handler.py +4 -4
  44. package/dist/python-src/src/stream/handler_with_tracker.py +10 -10
  45. package/dist/python-src/src/trackers/token/pricing.py +2 -2
  46. package/dist/python-src/src/trackers/token/report.py +4 -4
  47. package/dist/python-src/src/trackers/token/tracker.py +8 -8
  48. package/dist/python-src/src/tui/chatui.py +10 -10
  49. package/dist/python-src/src/tui/clawtui.py +230 -0
  50. package/dist/python-src/src/tui/commands/__init__.py +3 -0
  51. package/dist/python-src/src/tui/commands/base.py +6 -0
  52. package/dist/python-src/src/tui/commands/instruction.py +5 -0
  53. package/dist/python-src/src/tui/components/tlist.py +7 -7
  54. package/dist/python-src/src/tui/components/tscroll_panel.py +73 -44
  55. package/dist/python-src/src/tui/components/tscroll_panel_old.py +58 -0
  56. package/dist/python-src/src/tui/utils/trender.py +21 -21
  57. package/dist/python-src/uv.lock +1969 -1958
  58. package/package.json +1 -1
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "python-minifier>=3.2.0",
34
34
  "ddgs>=9.11.4",
35
35
  "typer>=0.24.1",
36
+ "pyperclip>=1.11.0",
36
37
  ]
37
38
 
38
39
  #[project.scripts]
@@ -0,0 +1,9 @@
1
+ # 获取当前脚本所在目录的绝对路径
2
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3
+
4
+ # 使用该目录下的 .venv/bin/python 运行 cli.py
5
+ # "$SCRIPT_DIR/.venv/bin/python" "$SCRIPT_DIR/src/agents/skill_agent.py"
6
+
7
+ # "$SCRIPT_DIR/.venv/bin/python" "$SCRIPT_DIR/main.py"
8
+
9
+ "$SCRIPT_DIR/.venv/bin/python" "$SCRIPT_DIR/entry.py" "$@"
@@ -24,11 +24,11 @@ class TokenUsageMiddleware(AgentMiddleware):
24
24
  from langchain.agents import create_agent
25
25
  from langchain.chat_models import init_chat_model
26
26
  def main():B=get_default_model(streaming=_B);C=[];D=_G;E=create_agent(model=B,tools=C,system_prompt=D,middleware=[TokenUsageMiddleware()]);A='请介绍一下LangChain v1的新特性';A='你好';E.invoke({_A:[{'role':'user',_H:A}]})
27
- def cx(token):
27
+ def dq(token):
28
28
  A=token
29
29
  if A.text:print(A.text,end='|')
30
30
  if A.tool_call_chunks:print(A.tool_call_chunks)
31
- def cw(message):
31
+ def dr(message):
32
32
  A=message
33
33
  if isinstance(A,AIMessage)and A.tool_calls:print(f"Tool calls: {A.tool_calls}")
34
34
  if isinstance(A,ToolMessage):print(f"Tool response: {A.content_blocks}")
@@ -37,8 +37,8 @@ async def amain():
37
37
  async for A in I.astream({_A:[J]},stream_mode=[_A,C],version='v2'):
38
38
  if A[D]==_A:
39
39
  B,M=A[E]
40
- if isinstance(B,AIMessageChunk):cx(B)
40
+ if isinstance(B,AIMessageChunk):dq(B)
41
41
  elif A[D]==C:
42
42
  for(K,L)in A[E].items():
43
- if K in('model','tools'):cw(L[_A][-1])
43
+ if K in('model','tools'):dr(L[_A][-1])
44
44
  if __name__=='__main__':import asyncio;asyncio.run(amain())
@@ -2,7 +2,7 @@ _A=False
2
2
  import asyncio
3
3
  from typing import Callable,Awaitable
4
4
  from logging import getLogger
5
- from src.nano.bus.events import InboundMessage,OutboundMessage
5
+ from src.claw.bus.events import InboundMessage,OutboundMessage
6
6
  logger=getLogger(__name__)
7
7
  class MessageBus:
8
8
  def __init__(A):A.inbound=asyncio.Queue();A.outbound=asyncio.Queue();A._outbound_subscribers={};A._inbound_subscribers={};A._in_running=_A;A._out_running=_A
@@ -1,3 +1,3 @@
1
- from src.nano.channels.base import BaseChannel
2
- from src.nano.channels.manager import ChannelManager
1
+ from src.claw.channels.base import BaseChannel
2
+ from src.claw.channels.manager import ChannelManager
3
3
  __all__=['BaseChannel','ChannelManager']
@@ -1,8 +1,8 @@
1
1
  from abc import ABC,abstractmethod
2
2
  from typing import Any
3
3
  from logging import getLogger
4
- from src.nano.bus.events import InboundMessage,OutboundMessage
5
- from src.nano.bus.queue import MessageBus
4
+ from src.claw.bus.events import InboundMessage,OutboundMessage
5
+ from src.claw.bus.queue import MessageBus
6
6
  logger=getLogger(__name__)
7
7
  class BaseChannel(ABC):
8
8
  name:str='base'
@@ -1,27 +1,33 @@
1
+ _G='open_id'
2
+ _F='chat_id'
1
3
  _E='THUMBSUP'
2
4
  _D=False
3
5
  _C=True
4
6
  _B='tag'
5
7
  _A=None
6
- import asyncio,json,re,threading
8
+ import asyncio,json,re,os,threading
7
9
  from collections import OrderedDict
8
10
  from typing import Any
9
11
  from logging import getLogger
10
12
  logger=getLogger(__name__)
11
- from src.nano.bus.events import InboundMessage,OutboundMessage
12
- from src.nano.bus.queue import MessageBus
13
- from src.nano.channels.base import BaseChannel
14
- from src.nano.config.schema import FeishuConfig
13
+ from src.claw.bus.events import InboundMessage,OutboundMessage
14
+ from src.claw.bus.queue import MessageBus
15
+ from src.claw.channels.base import BaseChannel
16
+ from src.claw.config.schema import FeishuConfig
15
17
  try:import lark_oapi as lark;from lark_oapi.api.im.v1 import CreateMessageRequest,CreateMessageRequestBody,CreateMessageReactionRequest,CreateMessageReactionRequestBody,Emoji,P2ImMessageReceiveV1;FEISHU_AVAILABLE=_C
16
18
  except ImportError:FEISHU_AVAILABLE=_D;lark=_A;Emoji=_A
17
19
  MSG_TYPE_MAP={'image':'[image]','audio':'[audio]','file':'[file]','sticker':'[sticker]'}
20
+ _IMAGE_EXTS={'.png','.jpg','.jpeg','.gif','.bmp','.webp','.ico','.tiff','.tif'}
21
+ _AUDIO_EXTS={'.opus'}
22
+ _VIDEO_EXTS={'.mp4','.mov','.avi'}
23
+ _FILE_TYPE_MAP={'.opus':'opus','.mp4':'mp4','.pdf':'pdf','.doc':'doc','.docx':'doc','.xls':'xls','.xlsx':'xls','.ppt':'ppt','.pptx':'ppt'}
18
24
  class FeishuChannel(BaseChannel):
19
25
  name='feishu'
20
26
  def __init__(A,config,bus):B=config;super().__init__(B,bus);A.config=B;A._client=_A;A._ws_client=_A;A._ws_thread=_A;A._processed_message_ids=OrderedDict();A._loop=_A
21
27
  async def start(A):
22
28
  if not FEISHU_AVAILABLE:logger.error('Feishu SDK not installed. Run: pip install lark-oapi');return
23
29
  if not A.config.app_id or not A.config.app_secret:logger.error('Feishu app_id and app_secret not configured');return
24
- A._running=_C;A._loop=asyncio.get_running_loop();A._client=lark.Client.builder().app_id(A.config.app_id).app_secret(A.config.app_secret).log_level(lark.LogLevel.INFO).build();B=lark.EventDispatcherHandler.builder(A.config.encrypt_key or'',A.config.verification_token or'').register_p2_im_message_receive_v1(A.dj).build();A._ws_client=lark.ws.Client(A.config.app_id,A.config.app_secret,event_handler=B,log_level=lark.LogLevel.INFO)
30
+ A._running=_C;A._loop=asyncio.get_running_loop();A._client=lark.Client.builder().app_id(A.config.app_id).app_secret(A.config.app_secret).log_level(lark.LogLevel.CRITICAL).build();B=lark.EventDispatcherHandler.builder(A.config.encrypt_key or'',A.config.verification_token or'').register_p2_im_message_receive_v1(A.ek).build();A._ws_client=lark.ws.Client(A.config.app_id,A.config.app_secret,event_handler=B,log_level=lark.LogLevel.CRITICAL)
25
31
  def C():
26
32
  try:A._ws_client.start()
27
33
  except Exception as B:logger.error(f"Feishu WebSocket error: {B}")
@@ -33,7 +39,7 @@ class FeishuChannel(BaseChannel):
33
39
  try:A._ws_client.stop()
34
40
  except Exception as B:logger.warning(f"Error stoppping WebSocket client: {B}")
35
41
  logger.info('Feishu bot stopped')
36
- def dg(D,message_id,emoji_type):
42
+ def eo(D,message_id,emoji_type):
37
43
  C=emoji_type;B=message_id
38
44
  try:
39
45
  E=CreateMessageReactionRequest.builder().message_id(B).request_body(CreateMessageReactionRequestBody.builder().reaction_type(Emoji.builder().emoji_type(C).build()).build()).build();A=D._client.im.v1.message_reaction.create(E)
@@ -42,19 +48,19 @@ class FeishuChannel(BaseChannel):
42
48
  except Exception as F:logger.warning(f"Error adding reaction: {F}")
43
49
  async def _add_reaction(A,message_id,emoji_type=_E):
44
50
  if not A._client or not Emoji:return
45
- B=asyncio.get_running_loop();await B.run_in_executor(_A,A.dg,message_id,emoji_type)
51
+ B=asyncio.get_running_loop();await B.run_in_executor(_A,A.eo,message_id,emoji_type)
46
52
  _TABLE_RE=re.compile('((?:^[ \\t]*\\|.+\\|[ \\t]*\\n)(?:^[ \\t]*\\|[-:\\s|]+\\|[ \\t]*\\n)(?:^[ \\t]*\\|.+\\|[ \\t]*\\n?)+)',re.MULTILINE)
47
53
  @staticmethod
48
- def di(table_text):
54
+ def el(table_text):
49
55
  A=[A.strip()for A in table_text.strip().split('\n')if A.strip()]
50
56
  if len(A)<3:return
51
57
  B=lambda l:[A.strip()for A in l.strip('|').split('|')];C=B(A[0]);D=[B(A)for A in A[2:]];E=[{_B:'column','name':f"c{A}",'display_name':B,'width':'auto'}for(A,B)in enumerate(C)];return{_B:'table','page_size':len(D)+1,'columns':E,'rows':[{f"c{A}":B[A]if A<len(B)else''for A in range(len(C))}for B in D]}
52
- def dh(G,content):
58
+ def en(G,content):
53
59
  E='markdown';D='content';A=content;B,F=[],0
54
60
  for C in G._TABLE_RE.finditer(A):
55
61
  H=A[F:C.start()].strip()
56
62
  if H:B.append({_B:E,D:H})
57
- B.append(G.di(C.group(1))or{_B:E,D:C.group(1)});F=C.end()
63
+ B.append(G.el(C.group(1))or{_B:E,D:C.group(1)});F=C.end()
58
64
  I=A[F:].strip()
59
65
  if I:B.append({_B:E,D:I})
60
66
  return B or[{_B:E,D:A}]
@@ -62,13 +68,32 @@ class FeishuChannel(BaseChannel):
62
68
  A=msg
63
69
  if not C._client:logger.warning('Feishu client not initialized');return
64
70
  try:
65
- if A.chat_id.startswith('oc_'):D='chat_id'
66
- else:D='open_id'
67
- E=C.dh(A.content);F={'config':{'wide_screen_mode':_C},'elements':E};G=json.dumps(F,ensure_ascii=_D);H=CreateMessageRequest.builder().receive_id_type(D).request_body(CreateMessageRequestBody.builder().receive_id(A.chat_id).msg_type('interactive').content(G).build()).build();B=C._client.im.v1.message.create(H)
71
+ if A.chat_id.startswith('oc_'):D=_F
72
+ else:D=_G
73
+ E=C.en(A.content);F={'config':{'wide_screen_mode':_C},'elements':E};G=json.dumps(F,ensure_ascii=_D);H=CreateMessageRequest.builder().receive_id_type(D).request_body(CreateMessageRequestBody.builder().receive_id(A.chat_id).msg_type('interactive').content(G).build()).build();B=C._client.im.v1.message.create(H)
68
74
  if not B.success():logger.error(f"Failed to send Feishu message: code={B.code},msg={B.msg}, log_id={B.get_log_id()}")
69
75
  else:logger.debug(f"Feishu message sent to {A.chat_id}")
70
76
  except Exception as I:logger.error(f"Error sending Feishu message: {I}")
71
- def dj(A,data):
77
+ async def send_message(D,chat_id,msg_type,content):
78
+ B=chat_id
79
+ if B.startswith('oc_'):C=_F
80
+ else:C=_G
81
+ E=CreateMessageRequest.builder().receive_id_type(C).request_body(CreateMessageRequestBody.builder().receive_id(B).msg_type(msg_type).content(content).build()).build();A=D._client.im.v1.message.create(E)
82
+ if A.success():print(f"Feishu message sent to {B} successfully")
83
+ else:print(f"Failed to send Feishu message: code={A.code}, msg={A.msg}, log_id={A.get_log_id()}")
84
+ async def send_image(A,chat_id,image_path):
85
+ B=A.ej(image_path)
86
+ if B is _A:return
87
+ A.send_message(chat_id,'image',json.dumps({'image_key':B}))
88
+ async def send_file(B,chat_id,file_path):
89
+ C=file_path;D=B.em(C)
90
+ if D is _A:return
91
+ E=os.path.splitext(C)[1].lower()
92
+ if E in _AUDIO_EXTS:A='audio'
93
+ elif E in _VIDEO_EXTS:A='video'
94
+ else:A='file'
95
+ B.send_message(chat_id,A,json.dumps({'file_key':D}))
96
+ def ek(A,data):
72
97
  if A._loop and A._loop.is_running():asyncio.run_coroutine_threadsafe(A._on_message(data),A._loop)
73
98
  async def _on_message(A,data):
74
99
  J='text'
@@ -86,4 +111,20 @@ class FeishuChannel(BaseChannel):
86
111
  else:E=MSG_TYPE_MAP.get(D,f"[{D}]")
87
112
  if not E:return
88
113
  M=L if I=='group'else H;await A._handle_message(sender_id=H,chat_id=M,content=E,metadata={'message_id':C,'chat_type':I,'msg_type':D})
89
- except Exception as N:logger.error(f"Error processing Feishu message: {N}")
114
+ except Exception as N:logger.error(f"Error processing Feishu message: {N}")
115
+ def ej(D,file_path):
116
+ B=file_path;from lark_oapi.api.im.v1 import CreateImageRequest as E,CreateImageRequestBody as F
117
+ try:
118
+ with open(B,'rb')as G:
119
+ H=E.builder().request_body(F.builder().image_type('message').image(G).build()).build();A=D._client.im.v1.image.create(H)
120
+ if A.success():C=A.data.image_key;print(f"Uploaded image: {os.path.basename(B)}, {C}");return C
121
+ else:print(f"Failed to upload image: {A.code} {A.msg}");return
122
+ except Exception:print(f"Error uploading image {B}");return
123
+ def em(D,file_path):
124
+ A=file_path;from lark_oapi.api.im.v1 import CreateFileRequest as E,CreateFileRequestBody as F;G=os.path.splitext(A)[1].lower();H=_FILE_TYPE_MAP.get(G,'stream');I=os.path.basename(A)
125
+ try:
126
+ with open(A,'rb')as J:
127
+ K=E.builder().request_body(F.builder().file_type(H).file_name(I).file(J).build()).build();B=D._client.im.v1.file.create(K)
128
+ if B.success():C=B.data.file_key;print(f"Uploaded file: {os.path.basename(A)}, {C}");return C
129
+ else:print(f"Failed to upload file: {B.code} {B.msg}");return
130
+ except Exception:print(f"Error uploading file {A}");return
@@ -8,8 +8,8 @@ from src.claw.channels.base import BaseChannel
8
8
  from src.claw.config.schema import Config
9
9
  logger=getLogger(__name__)
10
10
  class ChannelManager:
11
- def __init__(A,config,bus):A.config=config;A.bus=bus;A.channels={};A._dispatch_task=None;A.dk()
12
- def dk(A):
11
+ def __init__(A,config,bus):A.config=config;A.bus=bus;A.channels={};A._dispatch_task=None;A.ep()
12
+ def ep(A):
13
13
  if A.config.channels.feishu.enabled:
14
14
  try:from src.claw.channels.feishu import FeishuChannel as B;A.channels['feishu']=B(A.config.channels.feishu,A.bus);logger.info('Feishu channel enabled')
15
15
  except ImportError as C:logger.warning(f"Feishu channel not available: {C}")
@@ -0,0 +1,3 @@
1
+ from src.claw.config.loader import load_config,get_config_path
2
+ from src.claw.config.schema import Config
3
+ __all__=['Conifg','load_config','get_config_path']
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ import json
4
+ from src.claw.utils.helpers import get_data_path
5
+ from src.claw.config.schema import Config
6
+ def get_config_path():return Path.home()/'.autodev'/'config.json'
7
+ def get_data_dir():return get_data_path()
8
+ def load_config(config_path=None):
9
+ A=config_path or get_config_path()
10
+ if A.exists():
11
+ try:
12
+ with open(A)as C:B=json.load(C)
13
+ B=dz(B);return Config.model_validate(convert_keys(B))
14
+ except(json.JSONDecodeError,ValueError)as D:print(f"Warning: Failed to load config from {A}: {D}");print('Using default configuration.')
15
+ return Config()
16
+ def save_config(config,config_path=None):
17
+ B=config_path or get_config_path();B.parent.mkdir(parents=True,exist_ok=True);A=config.model_dump();A=convert_to_camel(A)
18
+ with open(B,'w')as C:json.dump(A,C,indent=2)
19
+ def dz(data):
20
+ A='restrictToWorkspace';B=data.get('tools',{});C=B.get('exec',{})
21
+ if A in C and A not in B:B[A]=C.pop(A)
22
+ return data
23
+ def convert_keys(data):
24
+ A=data
25
+ if isinstance(A,dict):return{camel_to_snake(A):convert_keys(B)for(A,B)in A.items()}
26
+ if isinstance(A,list):return[convert_keys(A)for A in A]
27
+ return A
28
+ def convert_to_camel(data):
29
+ A=data
30
+ if isinstance(A,dict):return{snake_to_camel(A):convert_to_camel(B)for(A,B)in A.items()}
31
+ if isinstance(A,list):return[convert_to_camel(A)for A in A]
32
+ def camel_to_snake(name):
33
+ A=[]
34
+ for(C,B)in enumerate(name):
35
+ if B.isupper()and C>0:A.append('_')
36
+ A.append(B.lower())
37
+ return''.join(A)
38
+ def snake_to_camel(name):A=name.split('_');return A[0]+''.join(A.title()for A in A[1:])
@@ -1,46 +1,31 @@
1
1
  _C=True
2
- _B=False
3
- _A=None
2
+ _B=None
3
+ _A=False
4
4
  from pathlib import Path
5
5
  from pydantic import BaseModel,Field
6
6
  from pydantic_settings import BaseSettings
7
- class FeishuConfig(BaseModel):enabled:bool=_B;app_id:str='';app_secret:str='';encrypt_key:str='';verification_token:str='';allow_from:list[str]=Field(default_factory=list)
8
- class DingTalkConfig(BaseModel):enabled:bool=_B;client_id:str='';client_secret:str='';allow_from:list[str]=Field(default_factory=list)
9
- class EmailConfig(BaseModel):enabled:bool=_B;consent_granted:bool=_B;imap_host:str='';imap_port:int=993;imap_username:str='';imap_password:str='';imap_mailbox:str='INBOX';imap_use_ssl:bool=_C;smtp_host:str='';smtp_port:int=587;smtp_username:str='';smtp_password:str='';smtp_use_tls:bool=_C;smtp_use_ssl:bool=_B;from_address:str='';auto_reply_enabled:bool=_C;poll_interval_seconds:int=30;mark_seen:bool=_C;max_body_chars:int=12000;subject_prefix:str='Re: ';allow_from:list[str]=Field(default_factory=list)
10
- class MochatMentionConfig(BaseModel):require_in_groups:bool=_B
11
- class MochatGroupRule(BaseModel):require_mention:bool=_B
12
- class MochatConfig(BaseModel):enabled:bool=_B;base_url:str='https://mochat.io';socket_url:str='';socket_path:str='/socket.io';socket_disable_msgpack:bool=_B;socket_reconnect_delay_ms:int=1000;socket_max_reconnect_delay_ms:int=10000;socket_connect_timeout_ms:int=10000;refresh_interval_ms:int=30000;watch_timeout_ms:int=25000;watch_limit:int=100;retry_delay_ms:int=500;max_retry_attempts:int=0;claw_token:str='';agent_user_id:str='';sessions:list[str]=Field(default_factory=list);panels:list[str]=Field(default_factory=list);allow_from:list[str]=Field(default_factory=list);mention:MochatMentionConfig=Field(default_factory=MochatMentionConfig);groups:dict[str,MochatGroupRule]=Field(default_factory=dict);reply_delay_mode:str='non-mention';reply_delay_ms:int=120000
13
- class QQConfig(BaseModel):enabled:bool=_B;app_id:str='';secret:str='';allow_from:list[str]=Field(default_factory=list)
7
+ class FeishuConfig(BaseModel):enabled:bool=_A;app_id:str='';app_secret:str='';encrypt_key:str='';verification_token:str='';allow_from:list[str]=Field(default_factory=list)
8
+ class DingTalkConfig(BaseModel):enabled:bool=_A;client_id:str='';client_secret:str='';allow_from:list[str]=Field(default_factory=list)
9
+ class EmailConfig(BaseModel):enabled:bool=_A;consent_granted:bool=_A;imap_host:str='';imap_port:int=993;imap_username:str='';imap_password:str='';imap_mailbox:str='INBOX';imap_use_ssl:bool=_C;smtp_host:str='';smtp_port:int=587;smtp_username:str='';smtp_password:str='';smtp_use_tls:bool=_C;smtp_use_ssl:bool=_A;from_address:str='';auto_reply_enabled:bool=_C;poll_interval_seconds:int=30;mark_seen:bool=_C;max_body_chars:int=12000;subject_prefix:str='Re: ';allow_from:list[str]=Field(default_factory=list)
10
+ class MochatMentionConfig(BaseModel):require_in_groups:bool=_A
11
+ class MochatGroupRule(BaseModel):require_mention:bool=_A
12
+ class MochatConfig(BaseModel):enabled:bool=_A;base_url:str='https://mochat.io';socket_url:str='';socket_path:str='/socket.io';socket_disable_msgpack:bool=_A;socket_reconnect_delay_ms:int=1000;socket_max_reconnect_delay_ms:int=10000;socket_connect_timeout_ms:int=10000;refresh_interval_ms:int=30000;watch_timeout_ms:int=25000;watch_limit:int=100;retry_delay_ms:int=500;max_retry_attempts:int=0;claw_token:str='';agent_user_id:str='';sessions:list[str]=Field(default_factory=list);panels:list[str]=Field(default_factory=list);allow_from:list[str]=Field(default_factory=list);mention:MochatMentionConfig=Field(default_factory=MochatMentionConfig);groups:dict[str,MochatGroupRule]=Field(default_factory=dict);reply_delay_mode:str='non-mention';reply_delay_ms:int=120000
13
+ class QQConfig(BaseModel):enabled:bool=_A;app_id:str='';secret:str='';allow_from:list[str]=Field(default_factory=list)
14
14
  class ChannelsConfig(BaseModel):feishu:FeishuConfig=Field(default_factory=FeishuConfig);mochat:MochatConfig=Field(default_factory=MochatConfig);dingtalk:DingTalkConfig=Field(default_factory=DingTalkConfig);email:EmailConfig=Field(default_factory=EmailConfig);qq:QQConfig=Field(default_factory=QQConfig)
15
15
  class AgentDefaults(BaseModel):workspace:str='~/.nanobot/workspace';model:str='qwen-plus';max_tokens:int=8192;temperature:float=.7;max_tool_iterations:int=200
16
16
  class AgentsConfig(BaseModel):defaults:AgentDefaults=Field(default_factory=AgentDefaults)
17
- class ProviderConfig(BaseModel):api_key:str='';api_base:str|_A=_A;extra_headers:dict[str,str]|_A=_A
17
+ class ProviderConfig(BaseModel):api_key:str='';api_base:str|_B=_B;extra_headers:dict[str,str]|_B=_B
18
18
  class ProvidersConfig(BaseModel):anthropic:ProviderConfig=Field(default_factory=ProviderConfig);openai:ProviderConfig=Field(default_factory=ProviderConfig);openrouter:ProviderConfig=Field(default_factory=ProviderConfig);deepseek:ProviderConfig=Field(default_factory=ProviderConfig);groq:ProviderConfig=Field(default_factory=ProviderConfig);zhipu:ProviderConfig=Field(default_factory=ProviderConfig);dashscope:ProviderConfig=Field(default_factory=ProviderConfig);vllm:ProviderConfig=Field(default_factory=ProviderConfig);gemini:ProviderConfig=Field(default_factory=ProviderConfig);moonshot:ProviderConfig=Field(default_factory=ProviderConfig);minimax:ProviderConfig=Field(default_factory=ProviderConfig);aihubmix:ProviderConfig=Field(default_factory=ProviderConfig)
19
19
  class GatewayConfig(BaseModel):host:str='0.0.0.0';port:int=18790
20
20
  class WebSearchConfig(BaseModel):api_key:str='';max_results:int=5
21
21
  class WebToolsConfig(BaseModel):search:WebSearchConfig=Field(default_factory=WebSearchConfig)
22
22
  class ExecToolConfig(BaseModel):timeout:int=60
23
- class ToolsConfig(BaseModel):web:WebToolsConfig=Field(default_factory=WebToolsConfig);exec:ExecToolConfig=Field(default_factory=ExecToolConfig);restrict_to_workspace:bool=_B
23
+ class ToolsConfig(BaseModel):web:WebToolsConfig=Field(default_factory=WebToolsConfig);exec:ExecToolConfig=Field(default_factory=ExecToolConfig);restrict_to_workspace:bool=_A
24
24
  class Config(BaseSettings):
25
25
  agents:AgentsConfig=Field(default_factory=AgentsConfig);channels:ChannelsConfig=Field(default_factory=ChannelsConfig);providers:ProvidersConfig=Field(default_factory=ProvidersConfig);gateway:GatewayConfig=Field(default_factory=GatewayConfig);tools:ToolsConfig=Field(default_factory=ToolsConfig)
26
26
  @property
27
27
  def workspace_path(self):return Path(self.agents.defaults.workspace).expanduser()
28
- def df(C,model=_A):
29
- from nanobot.providers.registry import PROVIDERS as D;E=(model or C.agents.defaults.model).lower()
30
- for B in D:
31
- A=getattr(C.providers,B.name,_A)
32
- if A and any(A in E for A in B.keywords)and A.api_key:return A,B.name
33
- for B in D:
34
- A=getattr(C.providers,B.name,_A)
35
- if A and A.api_key:return A,B.name
36
- return _A,_A
37
- def get_provider(A,model=_A):B,C=A.df(model);return B
38
- def get_provider_name(A,model=_A):C,B=A.df(model);return B
39
- def get_api_key(B,model=_A):A=B.get_provider(model);return A.api_key if A else _A
40
- def get_api_base(D,model=_A):
41
- from nanobot.providers.registry import find_by_name as E;B,C=D.df(model)
42
- if B and B.api_base:return B.api_base
43
- if C:
44
- A=E(C)
45
- if A and A.is_gateway and A.default_api_base:return A.default_api_base
28
+ def get_provider(A,model=_B):B,C=A._match_provider(model);return B
29
+ def get_provider_name(A,model=_B):C,B=A._match_provider(model);return B
30
+ def get_api_key(B,model=_B):A=B.get_provider(model);return A.api_key if A else _B
46
31
  class Config:env_prefix='NANOBOT_';env_nested_delimiter='__'
@@ -0,0 +1,3 @@
1
+ from src.claw.cron.service import CronService
2
+ from src.claw.cron.types_ import CronJob,CronSchedule
3
+ __all__=['CronService','CronJob','CronSchedule']
@@ -0,0 +1,171 @@
1
+ _X='deleteAfterRun'
2
+ _W='updatedAtMs'
3
+ _V='createdAtMs'
4
+ _U='lastError'
5
+ _T='lastStatus'
6
+ _S='lastRunAtMs'
7
+ _R='nextRunAtMs'
8
+ _Q='channel'
9
+ _P='deliver'
10
+ _O='message'
11
+ _N='agent_turn'
12
+ _M='everyMs'
13
+ _L='enabled'
14
+ _K='jobs'
15
+ _J='cron'
16
+ _I='every'
17
+ _H='kind'
18
+ _G='at'
19
+ _F='state'
20
+ _E='payload'
21
+ _D='schedule'
22
+ _C=None
23
+ _B=True
24
+ _A=False
25
+ import asyncio,json,time,uuid
26
+ from pathlib import Path
27
+ from typing import Any,Callable,Coroutine,Optional
28
+ from datetime import datetime
29
+ from logging import getLogger
30
+ import logging
31
+ logging.basicConfig(level=logging.CRITICAL+1,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',handlers=[logging.StreamHandler()])
32
+ logger=getLogger(__name__)
33
+ import logging
34
+ logger.setLevel(logging.CRITICAL+1)
35
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
36
+ from apscheduler.executors.asyncio import AsyncIOExecutor
37
+ from apscheduler.triggers.cron import CronTrigger
38
+ import sys,os
39
+ sys.path.append(os.getcwd())
40
+ from src.claw.cron.types_ import CronJob,CronJobState,CronPayload,CronSchedule,CronStore
41
+ def ec():return int(time.time()*1000)
42
+ def ed(schedule,now_ms):
43
+ B=now_ms;A=schedule
44
+ if A.kind==_G:return A.at_ms if A.at_ms and A.at_ms>B else _C
45
+ if A.kind==_I:
46
+ if not A.every_ms or A.every_ms<=0:return
47
+ return B+A.every_ms
48
+ if A.kind==_J and A.expr:
49
+ try:from croniter import croniter as C;D=C(A.expr,time.time());E=D.get_next();return int(E*1000)
50
+ except Exception as F:logger.error(f"解析cron表达式失败: {F}");return
51
+ class CronService:
52
+ def __init__(A,store_path,on_job=_C):A.store_path=store_path;A.on_job=on_job;A._store=_C;B={'default':AsyncIOExecutor()};C={'coalesce':_A,'max_instances':1};A._scheduler=AsyncIOScheduler(executors=B,job_defaults=C);A._running=_A
53
+ def ef(B):
54
+ if B._store:return B._store
55
+ if B.store_path.exists():
56
+ try:
57
+ D=json.loads(B.store_path.read_text());C=[]
58
+ for A in D.get(_K,[]):C.append(CronJob(id=A['id'],name=A['name'],enabled=A.get(_L,_B),schedule=CronSchedule(kind=A[_D][_H],at_ms=A[_D].get('atMs'),every_ms=A[_D].get(_M),expr=A[_D].get('expr'),tz=A[_D].get('tz')),payload=CronPayload(kind=A[_E].get(_H,_N),message=A[_E].get(_O,''),deliver=A[_E].get(_P,_A),channel=A[_E].get(_Q),to=A[_E].get('to')),state=CronJobState(next_run_at_ms=A.get(_F,{}).get(_R),last_run_at_ms=A.get(_F,{}).get(_S),last_status=A.get(_F,{}).get(_T),last_error=A.get(_F,{}).get(_U)),created_at_ms=A.get(_V,0),updated_at_ms=A.get(_W,0),delete_after_run=A.get(_X,_A)))
59
+ B._store=CronStore(jobs=C)
60
+ except Exception as E:logger.warning(f"Failed to load cron store: {E}");B._store=CronStore()
61
+ else:B._store=CronStore()
62
+ return B._store
63
+ def ee(A):
64
+ if not A._store:return
65
+ A.store_path.parent.mkdir(parents=_B,exist_ok=_B);B={'version':A._store.version,_K:[{'id':A.id,'name':A.name,_L:A.enabled,_D:{_H:A.schedule.kind,'atMs':A.schedule.at_ms,_M:A.schedule.every_ms,'expr':A.schedule.expr,'tz':A.schedule.tz},_E:{_H:A.payload.kind,_O:A.payload.message,_P:A.payload.deliver,_Q:A.payload.channel,'to':A.payload.to},_F:{_R:A.state.next_run_at_ms,_S:A.state.last_run_at_ms,_T:A.state.last_status,_U:A.state.last_error},_V:A.created_at_ms,_W:A.updated_at_ms,_X:A.delete_after_run}for A in A._store.jobs]};A.store_path.write_text(json.dumps(B,indent=2))
66
+ async def start(A):
67
+ A._running=_B;A.ef();A.eg();A.ee()
68
+ if not A._scheduler.running:A._scheduler.start();logger.info('APScheduler调度器已启动')
69
+ for B in A._store.jobs:
70
+ if B.enabled:A.eh(B)
71
+ logger.info(f"Cron service started with {len(A._store.jobs if A._store else[])} jobs")
72
+ async def stop(A):
73
+ A._running=_A
74
+ if A._scheduler.running:A._scheduler.shutdown()
75
+ logger.info('Cron service stopped')
76
+ def eg(B):
77
+ if not B._store:return
78
+ C=ec()
79
+ for A in B._store.jobs:
80
+ if A.enabled:A.state.next_run_at_ms=ed(A.schedule,C)
81
+ def ei(A):
82
+ if not A._store:return
83
+ B=[A.state.next_run_at_ms for A in A._store.jobs if A.enabled and A.state.next_run_at_ms];return min(B)if B else _C
84
+ def eh(D,job):
85
+ A=job
86
+ if not A.enabled:return
87
+ try:
88
+ C=_C
89
+ if A.schedule.kind==_I:from apscheduler.triggers.interval import IntervalTrigger as F;C=F(seconds=A.schedule.every_ms/1000)
90
+ elif A.schedule.kind==_J and A.schedule.expr:
91
+ E=A.schedule.expr.strip();B=E.split()
92
+ if len(B)==6:C=CronTrigger(second=B[0],minute=B[1],hour=B[2],day=B[3],month=B[4],day_of_week=B[5])
93
+ elif len(B)==5:C=CronTrigger.from_crontab(E)
94
+ else:raise ValueError(f"无效的Cron表达式格式: {E} (字段数: {len(B)})")
95
+ elif A.schedule.kind==_G and A.schedule.at_ms:from apscheduler.triggers.date import DateTrigger as G;H=datetime.fromtimestamp(A.schedule.at_ms/1000);C=G(run_date=H)
96
+ else:logger.warning(f"不支持的任务类型: {A.schedule.kind}");return
97
+ D._scheduler.add_job(func=D._execute_job,args=[A],trigger=C,id=A.id,name=A.name,replace_existing=_B,misfire_grace_time=30);logger.debug(f"任务 {A.id} 已添加到调度器,触发器类型: {type(C).__name__}")
98
+ except Exception as I:logger.error(f"添加任务到调度器失败: {I}",exc_info=_B);A.enabled=_A;D.ee()
99
+ async def _execute_job(B,job):
100
+ A=job;D=ec();logger.info(f"Cron: executing job '{A.name}' ({A.id})")
101
+ try:
102
+ E=_C
103
+ if B.on_job:E=await B.on_job(A)
104
+ A.state.last_status='ok';A.state.last_error=_C;logger.info(f"Cron: job '{A.name}' completed")
105
+ except Exception as C:A.state.last_status='error';A.state.last_error=str(C);logger.error(f"Cron: job '{A.name}' failed: {C}")
106
+ A.state.last_run_at_ms=D;A.updated_at_ms=ec()
107
+ if A.schedule.kind==_G:
108
+ if A.delete_after_run:
109
+ B._store.jobs=[B for B in B._store.jobs if B.id!=A.id]
110
+ try:B._scheduler.remove_job(A.id)
111
+ except Exception:pass
112
+ else:
113
+ A.enabled=_A;A.state.next_run_at_ms=_C
114
+ try:B._scheduler.pause_job(A.id)
115
+ except Exception:pass
116
+ else:A.state.next_run_at_ms=ed(A.schedule,ec())
117
+ B.ee()
118
+ def list_jobs(B,include_disabled=_A):A=B.ef();C=A.jobs if include_disabled else[A for A in A.jobs if A.enabled];return sorted(C,key=lambda j:j.state.next_run_at_ms or float('inf'))
119
+ def add_job(A,name,schedule,message,deliver=_A,channel=_C,to=_C,delete_after_run=_A):
120
+ D=schedule;E=A.ef();C=ec();B=CronJob(id=str(uuid.uuid4())[:8],name=name,enabled=_B,schedule=D,payload=CronPayload(kind=_N,message=message,deliver=deliver,channel=channel,to=to),state=CronJobState(next_run_at_ms=ed(D,C)),created_at_ms=C,updated_at_ms=C,delete_after_run=delete_after_run);E.jobs.append(B);A.ee()
121
+ if A._running and A._scheduler.running:A.eh(B)
122
+ logger.info(f"Cron: added job '{name}' ({B.id})");return B
123
+ def remove_job(A,job_id):
124
+ B=job_id;C=A.ef();E=len(C.jobs);C.jobs=[A for A in C.jobs if A.id!=B];D=len(C.jobs)<E
125
+ if D:
126
+ A.ee()
127
+ if A._running and A._scheduler.running:
128
+ try:A._scheduler.remove_job(B)
129
+ except Exception as F:logger.warning(f"从调度器移除任务{B}失败: {F}")
130
+ logger.info(f"Cron: removed job {B}")
131
+ return D
132
+ def enable_job(A,job_id,enabled=_B):
133
+ E=enabled;C=job_id;F=A.ef()
134
+ for B in F.jobs:
135
+ if B.id==C:
136
+ try:D=A._scheduler.get_job(C)
137
+ except Exception:D=_C
138
+ B.enabled=E;B.updated_at_ms=ec()
139
+ if E:
140
+ B.state.next_run_at_ms=ed(B.schedule,ec())
141
+ if A._running and A._scheduler.running:
142
+ if D:A._scheduler.resume_job(C)
143
+ A.eh(B)
144
+ else:
145
+ B.state.next_run_at_ms=_C
146
+ if A._running and A._scheduler.running and D:
147
+ try:A._scheduler.pause_job(C)
148
+ except Exception as G:logger.warning(f"暂停任务{C}失败: {G}")
149
+ A.ee();return B
150
+ async def run_job(A,job_id,force=_A):
151
+ C=A.ef()
152
+ for B in C.jobs:
153
+ if B.id==job_id:
154
+ if not force and not B.enabled:return _A
155
+ await A._execute_job(B);A.ee();return _B
156
+ return _A
157
+ def status(A):B=A.ef();return{_L:A._running,_K:len(B.jobs),'next_wake_at_ms':A.ei()}
158
+ async def main():
159
+ F=Path('./agent_cron.json')
160
+ async def G(job):A=job;logger.info(f"Agent 处理任务: {A.name}");logger.info(f"任务消息: {A.payload.message}");logger.info(f"投递渠道: {A.payload.channel} -> {A.payload.to}");return f"任务 {A.id} 处理完成"
161
+ A=CronService(store_path=F,on_job=G);await A.start();print('\n=== 服务启动状态 ===');print(json.dumps(A.status(),indent=2,ensure_ascii=_A));print('\n=== 添加固定间隔任务 ===');C=A.add_job(name='定时心跳任务',schedule=CronSchedule(kind=_I,every_ms=3000),message='Agent 心跳检测:正常运行中',deliver=_B,channel='agent_internal',to='admin');print(f"添加间隔任务成功,ID: {C.id}");print('\n=== 添加 Cron 表达式任务 ===');D=A.add_job(name='Cron 定时任务',schedule=CronSchedule(kind=_J,expr='*/5 * * * * *'),message='每 5 秒执行一次的 Cron 任务',deliver=_A);print(f"添加 Cron 任务成功,ID: {D.id}");print('\n=== 添加一次性任务 ===');E=ec()+5000;H=A.add_job(name='一次性通知任务',schedule=CronSchedule(kind=_G,at_ms=E),message='5秒后执行的一次性任务,执行后自动删除',deliver=_B,channel='slack',to='user_123',delete_after_run=_B);print(f"添加一次性任务成功,ID: {H.id},执行时间: {datetime.fromtimestamp(E/1000)}");print('\n=== 任务列表 ===');I=A.list_jobs()
162
+ for(J,B)in enumerate(I):print(f"{J+1}. ID: {B.id} | 名称: {B.name} | 类型: {B.schedule.kind} | 状态: {'启用'if B.enabled else'禁用'}")
163
+ print('\n=== 服务状态 ===');K=A.status();print(json.dumps(K,indent=2,ensure_ascii=_A));print('\n=== 手动执行任务 ===');await A.run_job(C.id,force=_B);print('\n=== 运行 20 秒(观察任务执行)===');await asyncio.sleep(20);print('\n=== 禁用 Cron 任务 ===');A.enable_job(D.id,enabled=_A);print(f"任务 {D.id} 已禁用");print('\n=== 删除间隔任务 ===')
164
+ if A.remove_job(C.id):print(f"任务 {C.id} 已删除")
165
+ print('\n=== 最终状态 ===');print(json.dumps(A.status(),indent=2,ensure_ascii=_A));print('\n=== 剩余任务 ===')
166
+ for B in A.list_jobs(include_disabled=_B):print(f"ID: {B.id} | 名称: {B.name} | 状态: {'启用'if B.enabled else'禁用'}")
167
+ print('\n=== 停止 Cron 服务 ===');await A.stop();print('Cron 服务已停止')
168
+ if __name__=='__main__':
169
+ try:asyncio.run(main())
170
+ except KeyboardInterrupt:print('\n程序被手动终止')
171
+ except Exception as e:logger.error(f"程序运行出错: {e}",exc_info=_B);raise
@@ -0,0 +1,14 @@
1
+ _B='agent_turn'
2
+ _A=None
3
+ from dataclasses import dataclass,field
4
+ from typing import Literal
5
+ @dataclass
6
+ class CronSchedule:kind:Literal['at','every','cron'];at_ms:int|_A=_A;every_ms:int|_A=_A;expr:str|_A=_A;tz:str|_A=_A
7
+ @dataclass
8
+ class CronPayload:kind:Literal['system_event',_B]=_B;message:str='';deliver:bool=False;channel:str|_A=_A;to:str|_A=_A
9
+ @dataclass
10
+ class CronJobState:next_run_at_ms:int|_A=_A;last_run_at_ms:int|_A=_A;last_status:Literal['ok','error','skipped']|_A=_A;last_error:str|_A=_A
11
+ @dataclass
12
+ class CronJob:id:str;name:str;enabled:bool=True;schedule:CronSchedule=field(default_factory=lambda:CronSchedule(kind='every'));payload:CronPayload=field(default_factory=CronPayload);state:CronJobState=field(default_factory=CronJobState);created_at_ms:int=0;updated_at_ms:int=0;delete_after_run:bool=False
13
+ @dataclass
14
+ class CronStore:version:int=1;jobs:list[CronJob]=field(default_factory=list)
@@ -0,0 +1,2 @@
1
+ from src.claw.heartbeat.service import HeartbeatService
2
+ __all__=['HeartbeatService']
@@ -0,0 +1,55 @@
1
+ _B=False
2
+ _A=True
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Any,Callable,Coroutine
6
+ from logging import getLogger
7
+ logger=getLogger(__name__)
8
+ import logging
9
+ logger.setLevel(logging.CRITICAL+1)
10
+ DEFAULT_HEARTBEAT_INTERVAL_S=1800
11
+ HEARTBEAT_PROMPT='Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK'
12
+ HEARTBEAT_OK_TOKEN='HEARTBEAT_OK'
13
+ def eb(content):
14
+ B=content
15
+ if not B:return _A
16
+ C={'- [ ]','* [ ]','- [x]','* [x]'}
17
+ for A in B.split('\n'):
18
+ A=A.strip()
19
+ if not A or A.startswith('#')or A.startswith('<!--')or A in C:continue
20
+ return _B
21
+ return _A
22
+ class HeartbeatService:
23
+ def __init__(A,workspace,on_heartbeat=None,interval_s=DEFAULT_HEARTBEAT_INTERVAL_S,enabled=_A):A.workspace=workspace;A.on_heartbeat=on_heartbeat;A.interval_s=interval_s;A.enabled=enabled;A._running=_B;A._task=None
24
+ @property
25
+ def heartbeat_file(self):return self.workspace/'HEARTBEAT.md'
26
+ def ea(A):
27
+ if A.heartbeat_file.exists():
28
+ try:return A.heartbeat_file.read_text()
29
+ except Exception:return
30
+ async def start(A):
31
+ logger.info(f"Starting heartbeat service ...")
32
+ if not A.enabled:logger.info('Heartbeat disabled');return
33
+ A._running=_A;A._task=asyncio.create_task(A._run_loop());logger.info(f"Heartbeat started (every {A.interval_s} seconds)")
34
+ def stop(A):
35
+ A._running=_B
36
+ if A._task:A._task.cancel();A._task=None
37
+ async def _run_loop(A):
38
+ while A._running:
39
+ try:
40
+ await asyncio.sleep(A.interval_s)
41
+ if A._running:await A._tick()
42
+ except asyncio.CancelledError:print('Heartbeat cancelled');break
43
+ except Exception as B:print(f"Heartbeat error: {B}");logger.error(f"Heartbeat error: {B}")
44
+ async def _tick(A):
45
+ B=A.ea()
46
+ if eb(B):logger.info('Heartbeat: no tasks (HEARTBEAT.md empty)');return
47
+ logger.info('Heartbeat: checking for tasks ...')
48
+ if A.on_heartbeat:
49
+ try:
50
+ C=await A.on_heartbeat(HEARTBEAT_PROMPT)
51
+ if HEARTBEAT_OK_TOKEN.replace('_','')in C.upper().replace('_',''):logger.info('Heartbeat: OK (no action needed)')
52
+ else:logger.info('Heartbeat: completed task')
53
+ except Exception as D:logger.error(f"Heartbeat execution failed: {D}")
54
+ async def trigger_now(A):
55
+ if A.on_heartbeat:return await A.on_heartbeat(HEARTBEAT_PROMPT)