@brandon_9527/tcode 1.0.6 → 1.0.8

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 (39) hide show
  1. package/dist/python-src/.env +3 -1
  2. package/dist/python-src/entry.py +14 -0
  3. package/dist/python-src/main.py +2 -1
  4. package/dist/python-src/skill_agent.py +144 -0
  5. package/dist/python-src/src/agents/token_tracker.py +44 -0
  6. package/dist/python-src/src/claw/__init__.py +0 -0
  7. package/dist/python-src/src/claw/bus/__init__.py +3 -0
  8. package/dist/python-src/src/claw/bus/events.py +10 -0
  9. package/dist/python-src/src/claw/bus/queue.py +43 -0
  10. package/dist/python-src/src/claw/channels/__init__.py +3 -0
  11. package/dist/python-src/src/claw/channels/base.py +30 -0
  12. package/dist/python-src/src/claw/channels/feishu.py +89 -0
  13. package/dist/python-src/src/claw/channels/manager.py +47 -0
  14. package/dist/python-src/src/claw/config/schema.py +46 -0
  15. package/dist/python-src/src/core/deepagents.py +5 -5
  16. package/dist/python-src/src/managers/manager_agent.py +5 -5
  17. package/dist/python-src/src/managers/manager_instruction.py +3 -3
  18. package/dist/python-src/src/managers/manager_skill.py +121 -0
  19. package/dist/python-src/src/managers/sandbox.py +2 -2
  20. package/dist/python-src/src/middlewares/dynamic_content.py +3 -3
  21. package/dist/python-src/src/middlewares/hitl.py +3 -3
  22. package/dist/python-src/src/middlewares/memory.py +2 -2
  23. package/dist/python-src/src/middlewares/skill.py +27 -0
  24. package/dist/python-src/src/middlewares/subagents.py +4 -4
  25. package/dist/python-src/src/middlewares/summary.py +37 -37
  26. package/dist/python-src/src/stream/formatter.py +19 -19
  27. package/dist/python-src/src/stream/handler.py +4 -4
  28. package/dist/python-src/src/stream/handler_with_tracker.py +9 -9
  29. package/dist/python-src/src/trackers/__init__.py +0 -0
  30. package/dist/python-src/src/trackers/token/__init__.py +0 -0
  31. package/dist/python-src/src/trackers/token/cli.py +45 -0
  32. package/dist/python-src/src/trackers/token/pricing.py +39 -0
  33. package/dist/python-src/src/trackers/token/report.py +114 -0
  34. package/dist/python-src/src/trackers/token/tracker.py +65 -0
  35. package/dist/python-src/src/tui/chatui.py +10 -9
  36. package/dist/python-src/src/tui/components/tlist.py +7 -7
  37. package/dist/python-src/src/tui/components/tscroll_panel.py +8 -8
  38. package/dist/python-src/src/tui/utils/trender.py +22 -21
  39. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
 
2
2
  OPENAI_API_KEY=sk-4c9334245f5f4e7aa4009650ef0438b0
3
3
  OPENAI_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
4
- DEFAULT_MODEL=qwen-plus
4
+ DEFAULT_MODEL=qwen-plus
5
+
6
+ # MAX_TOKENS_BEFORE_SUMMARY=1000000
@@ -29,6 +29,7 @@ warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
29
29
 
30
30
  import typer
31
31
  import asyncio
32
+ import os
32
33
 
33
34
 
34
35
  from main import (
@@ -50,7 +51,13 @@ def root(
50
51
  prompt: str = typer.Option(None, "-p", "--prompt", help="用户输入请求内容"),
51
52
  verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
52
53
  mode: str = typer.Option("team", "-m", "--mode", help="运行模式,single或team"),
54
+ # ====================== 新增:接收 max_tokens 参数 ======================
55
+ max_tokens: int = typer.Option(None, "--max-in-tokens", help="覆盖环境变量 MAX_TOKENS_BEFORE_SUMMARY"),
53
56
  ):
57
+ if max_tokens is not None:
58
+ os.environ["MAX_TOKENS_BEFORE_SUMMARY"] = str(max_tokens)
59
+ # print(f"✅ 已强制覆盖 MAX_TOKENS_BEFORE_SUMMARY = {max_tokens}")
60
+
54
61
  if ctx.invoked_subcommand is None:
55
62
  if not prompt:
56
63
  if mode == "single":
@@ -67,7 +74,14 @@ def root(
67
74
  @interact_app.command("interact")
68
75
  def interact_mode(
69
76
  verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
77
+ # ====================== 交互模式也支持覆盖 ======================
78
+ max_tokens: int = typer.Option(None, "--max-in-tokens", help="覆盖环境变量 MAX_TOKENS_BEFORE_SUMMARY"),
70
79
  ):
80
+ if max_tokens is not None:
81
+ print(f"交互模式:当前 MAX_TOKENS_BEFORE_SUMMARY = {os.getenv('MAX_TOKENS_BEFORE_SUMMARY')}")
82
+ os.environ["MAX_TOKENS_BEFORE_SUMMARY"] = str(max_tokens)
83
+ print(f"✅ 交互模式:已覆盖 MAX_TOKENS_BEFORE_SUMMARY = {max_tokens}")
84
+
71
85
  # 执行交互模式
72
86
  asyncio.run(teminal_chat(verbose))
73
87
 
@@ -328,7 +328,8 @@ async def team_main():
328
328
 
329
329
  def _build_team(agents_conf: Dict[str, Any], domain: str, llm: ChatOpenAI, toolkits: Dict[str, List[BaseTool]], workspace: str, saver=None, store=None, recursion_limit=1000):
330
330
  """ """
331
- team_conf = agents_conf[domain]
331
+
332
+ team_conf = agents_conf.setdefault(domain, {"members":{}})
332
333
 
333
334
  # for mname, mconf in team_conf['members'].items():
334
335
  # print(f"member_name: {mname}, member: {mconf}")
@@ -0,0 +1,144 @@
1
+ import uuid
2
+ from langchain.tools import tool
3
+ from langchain.agents import create_agent
4
+
5
+ from langgraph.checkpoint.memory import InMemorySaver
6
+
7
+
8
+ from langchain.messages import AIMessage, AIMessageChunk, HumanMessage, AnyMessage, ToolMessage
9
+ from langchain_openai import ChatOpenAI
10
+ from dotenv import find_dotenv, load_dotenv
11
+ import os
12
+
13
+ from pathlib import Path
14
+ import sys
15
+
16
+
17
+ from src.stream.handler import ainput, anormal_handler
18
+ from src.middlewares.skill import SkillMiddleware
19
+
20
+ from src.tools.tools import (
21
+ SkillAgentContext,
22
+ shell,
23
+ bash,
24
+ read_file,
25
+ write_file,
26
+ glob,
27
+ grep,
28
+ edit,
29
+ list_dir,
30
+ )
31
+
32
+ from src.tools.web import (
33
+ web_search,
34
+ web_fetch
35
+ )
36
+
37
+ toolkits = {
38
+ "filetools": [ bash, read_file, write_file, glob, grep, edit, list_dir],
39
+ "web": [web_search, web_fetch]
40
+ }
41
+
42
+
43
+ _ = load_dotenv(find_dotenv())
44
+
45
+ def get_default_model(streaming: bool = False) -> ChatOpenAI:
46
+ """
47
+ 修复:兼容所有 OpenAI 格式厂商
48
+ - 流式:开启 stream_options 获取 usage
49
+ - 非流式:不传 stream_options,避免报错
50
+ """
51
+ # 基础参数
52
+ model_kwargs = {}
53
+ #model_kwargs['extra_body'] = {"chat_template_kwargs":{'enable_thinking': False}}
54
+ extra_body = {"chat_template_kwargs":{'enable_thinking': False}}
55
+ # 只有流式模式才加 stream_options(非流式不加,避免400错误)
56
+ # 非流式模式默认输出 usage 信息
57
+ if streaming:
58
+ model_kwargs["stream_options"] = {"include_usage": True}
59
+
60
+
61
+
62
+ return ChatOpenAI(
63
+ model_name=os.getenv("DEFAULT_MODEL"),
64
+ base_url=os.getenv("OPENAI_API_BASE"),
65
+ api_key=os.getenv("OPENAI_API_KEY"),
66
+ streaming=streaming,
67
+ model_kwargs=model_kwargs, # 动态传入
68
+ extra_body=extra_body
69
+ )
70
+
71
+
72
+ def _render_message_chunk(token: AIMessageChunk) -> None:
73
+ if token.text:
74
+ print(token.text, end="|")
75
+ if token.tool_call_chunks:
76
+ print(token.tool_call_chunks)
77
+
78
+ def _render_completed_message(message: AnyMessage) -> None:
79
+ if isinstance(message, AIMessage) and message.tool_calls:
80
+ print(f"Tool calls: {message.tool_calls}")
81
+ if isinstance(message, ToolMessage):
82
+ print(f"Tool response: {message.content_blocks}")
83
+
84
+
85
+ async def amain():
86
+ model = get_default_model(streaming=True)
87
+
88
+ BASE_AGENT_PROMPT = """
89
+ 你是一个专业的助手,你的任务是回答用户的问题。
90
+ 你可以使用工具来获取信息,也可以直接回答用户的问题。
91
+ """
92
+
93
+ agent = create_agent(
94
+ model=model,
95
+ system_prompt=BASE_AGENT_PROMPT,
96
+ tools=[bash, read_file, write_file, glob, grep, edit, list_dir, web_search, web_fetch],
97
+ middleware=[SkillMiddleware(workspace=Path.cwd())],
98
+ checkpoint=InMemorySaver(),
99
+ verbose=True,
100
+ )
101
+
102
+ context = SkillAgentContext(
103
+ working_directory=Path.cwd(),
104
+ # skill_loader=None,
105
+ sandbox=None
106
+ )
107
+
108
+
109
+ while True:
110
+
111
+ user_input = ainput("User> ")
112
+ if user_input == "exit":
113
+ break
114
+
115
+ steam = agent.astream(
116
+ {
117
+ "messages": [HumanMessage(content=user_input) ]
118
+ },
119
+ config = {"configurable": {"thread_id": 1}},
120
+ stream_mode=["updates", "custom"],
121
+ context=context
122
+ )
123
+
124
+ async for chunk in steam:
125
+ if chunk["type"] == "messages":
126
+ token, metadate = chunk["data"]
127
+ if isinstance(token, AIMessageChunk):
128
+ #print(token)
129
+ _render_message_chunk(token)
130
+ elif chunk["type"] == "updates":
131
+ for source, update in chunk["data"].items():
132
+ if source in ("model", "tools"):
133
+ _render_completed_message(update["messages"][-1])
134
+
135
+
136
+ if __name__ == "__main__":
137
+ import asyncio
138
+ asyncio.run(amain())
139
+
140
+
141
+
142
+
143
+
144
+
@@ -0,0 +1,44 @@
1
+ _H='content'
2
+ _G=' \n 你是一个专业的助手,你的任务是回答用户的问题。\n 你可以使用工具来获取信息,也可以直接回答用户的问题。\n '
3
+ _F='---------------------------'
4
+ _E='total_tokens'
5
+ _D='output_tokens'
6
+ _C='input_tokens'
7
+ _B=False
8
+ _A='messages'
9
+ from langchain_openai import ChatOpenAI
10
+ from dotenv import find_dotenv,load_dotenv
11
+ import os
12
+ from langchain.messages import AIMessage,AIMessageChunk,AnyMessage,ToolMessage
13
+ from langchain.agents.middleware import AgentMiddleware,ModelRequest,ModelResponse
14
+ from langchain_core.messages import AIMessage
15
+ from typing import Callable,Dict
16
+ _=load_dotenv(find_dotenv())
17
+ def get_default_model(streaming=_B):
18
+ A=streaming;B={};C={'chat_template_kwargs':{'enable_thinking':_B}}
19
+ if A:B['stream_options']={'include_usage':True}
20
+ return ChatOpenAI(model_name=os.getenv('DEFAULT_MODEL'),base_url=os.getenv('OPENAI_API_BASE'),api_key=os.getenv('OPENAI_API_KEY'),streaming=A,model_kwargs=B,extra_body=C)
21
+ class TokenUsageMiddleware(AgentMiddleware):
22
+ def wrap_model_call(H,request,handler):C=request;print(f"[REQUEST]: {C}");A=handler(C);print(f"[RESPONSE]: {A}");D=A.result[-1];B=D.usage_metadata or{};E=B.get(_C,0);F=B.get(_D,0);G=B.get(_E,0);print(f"--- 模型调用 Token 统计 ---");print(f"输入 Tokens: {E}");print(f"输出 Tokens: {F}");print(f"总计 Tokens: {G}");print(_F);return A
23
+ async def awrap_model_call(G,request,handler):B=await handler(request);C=B.result[-1];A=C.usage_metadata or{};D=A.get(_C,0);E=A.get(_D,0);F=A.get(_E,0);print(f"\n--- 模型调用 Token 统计 ---");print(f"输入 Tokens: {D}");print(f"输出 Tokens: {E}");print(f"总计 Tokens: {F}");print(_F);return B
24
+ from langchain.agents import create_agent
25
+ from langchain.chat_models import init_chat_model
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):
28
+ A=token
29
+ if A.text:print(A.text,end='|')
30
+ if A.tool_call_chunks:print(A.tool_call_chunks)
31
+ def cw(message):
32
+ A=message
33
+ if isinstance(A,AIMessage)and A.tool_calls:print(f"Tool calls: {A.tool_calls}")
34
+ if isinstance(A,ToolMessage):print(f"Tool response: {A.content_blocks}")
35
+ async def amain():
36
+ E='data';D='type';C='updates';F=get_default_model(streaming=True);G=[];H=_G;I=create_agent(model=F,tools=G,system_prompt=H,middleware=[TokenUsageMiddleware()]);J={'role':'user',_H:'你好'}
37
+ async for A in I.astream({_A:[J]},stream_mode=[_A,C],version='v2'):
38
+ if A[D]==_A:
39
+ B,M=A[E]
40
+ if isinstance(B,AIMessageChunk):cx(B)
41
+ elif A[D]==C:
42
+ for(K,L)in A[E].items():
43
+ if K in('model','tools'):cw(L[_A][-1])
44
+ if __name__=='__main__':import asyncio;asyncio.run(amain())
File without changes
@@ -0,0 +1,3 @@
1
+ from src.claw.bus.events import InboundMessage,OutboundMessage
2
+ from src.claw.bus.queue import MessageBus
3
+ __all__=['MessageBus','InboundMessage','OutboundMessage']
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass,field
2
+ from datetime import datetime
3
+ from typing import Any
4
+ @dataclass
5
+ class InboundMessage:
6
+ channel:str;sender_id:str;chat_id:str;content:str;timestamp:datetime=field(default_factory=datetime.now);media:list[str]=field(default_factory=list);metadata:dict[str,Any]=field(default_factory=dict)
7
+ @property
8
+ def session_key(self):return f"{self.channel}:{self.chat_id}"
9
+ @dataclass
10
+ class OutboundMessage:channel:str;chat_id:str;content:str;reply_to:str|None=None;media:list[str]=field(default_factory=list);metadata:dict[str,Any]=field(default_factory=dict)
@@ -0,0 +1,43 @@
1
+ _A=False
2
+ import asyncio
3
+ from typing import Callable,Awaitable
4
+ from logging import getLogger
5
+ from src.nano.bus.events import InboundMessage,OutboundMessage
6
+ logger=getLogger(__name__)
7
+ class MessageBus:
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
9
+ async def publish_inbound(A,msg):await A.inbound.put(msg)
10
+ async def consume_inbound(A):return await A.inbound.get()
11
+ async def publish_outbound(A,msg):await A.outbound.put(msg)
12
+ async def consume_outbound(A):return await A.outbound.get()
13
+ def subscribe_inbound(A,channel,callback):
14
+ B=channel
15
+ if B not in A._inbound_subscribers:A._inbound_subscribers[B]=[]
16
+ A._inbound_subscribers[B].append(callback)
17
+ def subscribe_outbound(A,channel,callback):
18
+ B=channel
19
+ if B not in A._outbound_subscribers:A._outbound_subscribers[B]=[]
20
+ A._outbound_subscribers[B].append(callback)
21
+ async def dispatch_inbound(A):
22
+ A._in_running=True
23
+ while A._in_running:
24
+ try:
25
+ B=await asyncio.wait_for(A.inbound.get(),timeout=1.);C=A._inbound_subscribers.get(B.channel,[])
26
+ for D in C:
27
+ try:await D(B)
28
+ except Exception as E:logger.error(f"Error dispatching to {B.channel}: {E}")
29
+ except asyncio.TimeoutError:continue
30
+ async def dispatch_outbound(A):
31
+ A._out_running=True
32
+ while A._out_running:
33
+ try:
34
+ B=await asyncio.wait_for(A.outbound.get(),timeout=1.);C=A._outbound_subscribers.get(B.channel,[])
35
+ for D in C:
36
+ try:await D(B)
37
+ except Exception as E:logger.error(f"Error dispatching to {B.channel}: {E}")
38
+ except asyncio.TimeoutError:continue
39
+ def stop(A):A._in_running=_A;A._out_running=_A
40
+ @property
41
+ def inbound_size(self):return self.inbound.qsize()
42
+ @property
43
+ def outbound_size(self):return self.outbound.qsize()
@@ -0,0 +1,3 @@
1
+ from src.nano.channels.base import BaseChannel
2
+ from src.nano.channels.manager import ChannelManager
3
+ __all__=['BaseChannel','ChannelManager']
@@ -0,0 +1,30 @@
1
+ from abc import ABC,abstractmethod
2
+ from typing import Any
3
+ from logging import getLogger
4
+ from src.nano.bus.events import InboundMessage,OutboundMessage
5
+ from src.nano.bus.queue import MessageBus
6
+ logger=getLogger(__name__)
7
+ class BaseChannel(ABC):
8
+ name:str='base'
9
+ def __init__(A,config,bus):A.config=config;A.bus=bus;A._running=False
10
+ @abstractmethod
11
+ async def start(self):0
12
+ @abstractmethod
13
+ async def stop(self):0
14
+ @abstractmethod
15
+ async def send(self,msg):0
16
+ def is_allowed(E,sender_id):
17
+ C=True;A=getattr(E.config,'allow_from',[])
18
+ if not A:return C
19
+ B=str(sender_id)
20
+ if B in A:return C
21
+ if'|'in B:
22
+ for D in B.split('|'):
23
+ if D and D in A:return C
24
+ return False
25
+ async def _handle_message(A,sender_id,chat_id,content,media=None,metadata=None):
26
+ B=sender_id
27
+ if not A.is_allowed(B):logger.warning(f"Access denied for sender {B} on channel {A.name}.Add them to allowFrom list in config to grant access.");return
28
+ C=InboundMessage(channel=A.name,sender_id=str(B),chat_id=str(chat_id),content=content,media=media or[],metadata=metadata or{});await A.bus.publish_inbound(C)
29
+ @property
30
+ def is_running(self):return self._running
@@ -0,0 +1,89 @@
1
+ _E='THUMBSUP'
2
+ _D=False
3
+ _C=True
4
+ _B='tag'
5
+ _A=None
6
+ import asyncio,json,re,threading
7
+ from collections import OrderedDict
8
+ from typing import Any
9
+ from logging import getLogger
10
+ 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
15
+ try:import lark_oapi as lark;from lark_oapi.api.im.v1 import CreateMessageRequest,CreateMessageRequestBody,CreateMessageReactionRequest,CreateMessageReactionRequestBody,Emoji,P2ImMessageReceiveV1;FEISHU_AVAILABLE=_C
16
+ except ImportError:FEISHU_AVAILABLE=_D;lark=_A;Emoji=_A
17
+ MSG_TYPE_MAP={'image':'[image]','audio':'[audio]','file':'[file]','sticker':'[sticker]'}
18
+ class FeishuChannel(BaseChannel):
19
+ name='feishu'
20
+ 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
+ async def start(A):
22
+ if not FEISHU_AVAILABLE:logger.error('Feishu SDK not installed. Run: pip install lark-oapi');return
23
+ 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)
25
+ def C():
26
+ try:A._ws_client.start()
27
+ except Exception as B:logger.error(f"Feishu WebSocket error: {B}")
28
+ A._ws_thread=threading.Thread(target=C,daemon=_C);A._ws_thread.start();logger.info('Feishu bot started with WebSocket long connection');logger.info('No public IP required - using WebSocket to reveive events')
29
+ while A._running:await asyncio.sleep(1)
30
+ async def stop(A):
31
+ A._running=_D
32
+ if A._ws_client:
33
+ try:A._ws_client.stop()
34
+ except Exception as B:logger.warning(f"Error stoppping WebSocket client: {B}")
35
+ logger.info('Feishu bot stopped')
36
+ def dg(D,message_id,emoji_type):
37
+ C=emoji_type;B=message_id
38
+ try:
39
+ 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)
40
+ if not A.success():logger.warning(f"Failed to add reaction: code={A.code}, msg={A.msg}")
41
+ else:logger.debug(f"Added {C} reaction to message {B}")
42
+ except Exception as F:logger.warning(f"Error adding reaction: {F}")
43
+ async def _add_reaction(A,message_id,emoji_type=_E):
44
+ 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)
46
+ _TABLE_RE=re.compile('((?:^[ \\t]*\\|.+\\|[ \\t]*\\n)(?:^[ \\t]*\\|[-:\\s|]+\\|[ \\t]*\\n)(?:^[ \\t]*\\|.+\\|[ \\t]*\\n?)+)',re.MULTILINE)
47
+ @staticmethod
48
+ def di(table_text):
49
+ A=[A.strip()for A in table_text.strip().split('\n')if A.strip()]
50
+ if len(A)<3:return
51
+ 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):
53
+ E='markdown';D='content';A=content;B,F=[],0
54
+ for C in G._TABLE_RE.finditer(A):
55
+ H=A[F:C.start()].strip()
56
+ 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()
58
+ I=A[F:].strip()
59
+ if I:B.append({_B:E,D:I})
60
+ return B or[{_B:E,D:A}]
61
+ async def send(C,msg):
62
+ A=msg
63
+ if not C._client:logger.warning('Feishu client not initialized');return
64
+ 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)
68
+ 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
+ else:logger.debug(f"Feishu message sent to {A.chat_id}")
70
+ except Exception as I:logger.error(f"Error sending Feishu message: {I}")
71
+ def dj(A,data):
72
+ if A._loop and A._loop.is_running():asyncio.run_coroutine_threadsafe(A._on_message(data),A._loop)
73
+ async def _on_message(A,data):
74
+ J='text'
75
+ try:
76
+ G=data.event;B=G.message;F=G.sender;C=B.message_id
77
+ if C in A._processed_message_ids:return
78
+ A._processed_message_ids[C]=_A
79
+ while len(A._processed_message_ids)>1000:A._processed_message_ids.popitem(last=_D)
80
+ K=F.sender_type
81
+ if K=='bot':return
82
+ H=F.sender_id.open_id if F.sender_id else'unknown';L=B.chat_id;I=B.chat_type;D=B.message_type;await A._add_reaction(C,_E)
83
+ if D==J:
84
+ try:E=json.loads(B.content).get(J,'')
85
+ except json.JSONDecodeError:E=B.content or''
86
+ else:E=MSG_TYPE_MAP.get(D,f"[{D}]")
87
+ if not E:return
88
+ 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}")
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ from typing import Any,TYPE_CHECKING
4
+ from logging import getLogger
5
+ from src.claw.bus.events import OutboundMessage
6
+ from src.claw.bus.queue import MessageBus
7
+ from src.claw.channels.base import BaseChannel
8
+ from src.claw.config.schema import Config
9
+ logger=getLogger(__name__)
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):
13
+ if A.config.channels.feishu.enabled:
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
+ except ImportError as C:logger.warning(f"Feishu channel not available: {C}")
16
+ async def _start_channel(B,name,channel):
17
+ try:await channel.start()
18
+ except Exception as A:logger.error(f"Failed to start channel {name}: {A}")
19
+ async def start_all(A):
20
+ if not A.channels:logger.warning('No channels enabled');return
21
+ A._dispatch_task=asyncio.create_task(A._dispatch_outbound());B=[]
22
+ for(C,D)in A.channels.items():logger.info(f"Starting {C} channel ...");B.append(asyncio.create_task(A._start_channel(C,D)))
23
+ await asyncio.gather(*B,return_exceptions=True)
24
+ async def stop_all(A):
25
+ logger.info('Stopping all channels ...')
26
+ if A._dispatch_task:
27
+ A._dispatch_task.cancel()
28
+ try:await A._dispatch_task
29
+ except asyncio.CancelledError:pass
30
+ for(B,C)in A.channels.items():
31
+ try:await C.stop();logger.info(f"Stopped {B} channel")
32
+ except Exception as D:logger.error(f"Error stopping {B}: {D}")
33
+ async def _dispatch_outbound(B):
34
+ logger.info('Outbound dispatcher started')
35
+ while True:
36
+ try:
37
+ A=await asyncio.wait_for(B.bus.consume_outbound(),timeout=1.);C=B.channels.get(A.channel)
38
+ if C:
39
+ try:await C.send(A)
40
+ except Exception as D:logger.error(f"Error sending to {A.channel}: {D}")
41
+ else:logger.warning(f"Unknown channel: {A.channel}")
42
+ except asyncio.TimeoutError:continue
43
+ except asyncio.CancelledError:break
44
+ def get_channel(A,name):return A.channels.get(name)
45
+ def get_status(A):return{A:{'enabled':True,'running':B.is_running}for(A,B)in A.channels.items()}
46
+ @property
47
+ def enabled_channels(self):return list(self.channels.keys())
@@ -0,0 +1,46 @@
1
+ _C=True
2
+ _B=False
3
+ _A=None
4
+ from pathlib import Path
5
+ from pydantic import BaseModel,Field
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)
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
+ class AgentDefaults(BaseModel):workspace:str='~/.nanobot/workspace';model:str='qwen-plus';max_tokens:int=8192;temperature:float=.7;max_tool_iterations:int=200
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
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
+ class GatewayConfig(BaseModel):host:str='0.0.0.0';port:int=18790
20
+ class WebSearchConfig(BaseModel):api_key:str='';max_results:int=5
21
+ class WebToolsConfig(BaseModel):search:WebSearchConfig=Field(default_factory=WebSearchConfig)
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
24
+ class Config(BaseSettings):
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
+ @property
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
46
+ class Config:env_prefix='NANOBOT_';env_nested_delimiter='__'
@@ -22,9 +22,9 @@ import os
22
22
  _=load_dotenv(find_dotenv())
23
23
  def get_default_model(streaming=False):D='OPENAI_API_KEY';C='OPENAI_API_BASE';B='DEFAULT_MODEL';A=streaming;return ChatOpenAI(model_name=os.getenv(B),base_url=os.getenv(C),api_key=os.getenv(D),streaming=A,model_kwargs={'stream_options':{'include_usage':True}})if A else ChatOpenAI(model_name=os.getenv(B),base_url=os.getenv(C),api_key=os.getenv(D),streaming=A)
24
24
  def create_deep_agent(model=_A,tools=_A,*,system_prompt=_A,middleware=(),subagents=_A,response_format=_A,context_schema=_A,checkpointer=_A,store=_A,interrupt_on=_A,debug=False,name=_A,cache=_A,recursion_limit=1000):
25
- G=subagents;F=middleware;E=system_prompt;D=tools;B=interrupt_on;A=model
25
+ J='messages';I='tokens';H=subagents;G=middleware;F=system_prompt;E=tools;B=interrupt_on;A=model
26
26
  if A is _A:A=get_default_model(streaming=True)
27
- C=[TodoListMiddleware(),SubAgentMiddleware(default_model=A,default_tools=D,subagents=G if G is not _A else[],default_middleware=[TodoListMiddleware(),SummarizationMiddleware(model=A,max_tokens_before_summary=170000,messages_to_keep=6),PatchToolCallsMiddleware()],default_interrupt_on=B,general_purpose_agent=True),SummarizationMiddleware(model=A,max_tokens_before_summary=170000,messages_to_keep=6),PatchToolCallsMiddleware()]
28
- if F:C.extend(F)
29
- if B is not _A:C.append(HumanInTheLoopMiddleware(interrupt_on=B))
30
- return create_agent(model=A,system_prompt=E+'\n\n'+BASE_AGENT_PROMPT if E else BASE_AGENT_PROMPT,tools=D,middleware=C,response_format=response_format,context_schema=context_schema,checkpointer=checkpointer,store=store,debug=debug,name=name,cache=cache).with_config({'recursion_limit':recursion_limit})
27
+ C=int(os.getenv('MAX_TOKENS_BEFORE_SUMMARY',170000));print(f"当前 MAX_TOKENS_BEFORE_SUMMARY = {C}");D=[TodoListMiddleware(),SubAgentMiddleware(default_model=A,default_tools=E,subagents=H if H is not _A else[],default_middleware=[TodoListMiddleware(),SummarizationMiddleware(model=A,trigger=(I,C),keep=(J,6)),PatchToolCallsMiddleware()],default_interrupt_on=B,general_purpose_agent=True),SummarizationMiddleware(model=A,trigger=(I,C),keep=(J,6)),PatchToolCallsMiddleware()]
28
+ if G:D.extend(G)
29
+ if B is not _A:D.append(HumanInTheLoopMiddleware(interrupt_on=B))
30
+ return create_agent(model=A,system_prompt=F+'\n\n'+BASE_AGENT_PROMPT if F else BASE_AGENT_PROMPT,tools=E,middleware=D,response_format=response_format,context_schema=context_schema,checkpointer=checkpointer,store=store,debug=debug,name=name,cache=cache).with_config({'recursion_limit':recursion_limit})
@@ -15,8 +15,8 @@ from pathlib import Path
15
15
  import yaml,json,sys,re,os
16
16
  sys.path.append('/Users/brandon/workspace/coder/autodev')
17
17
  class AgentInfo:
18
- def __init__(A,name,content,src_):B=content;A.name=name;A.original_content=B;A.src_=src_;A.meta,A.body=A.bt(B)
19
- def bt(F,content):
18
+ def __init__(A,name,content,src_):B=content;A.name=name;A.original_content=B;A.src_=src_;A.meta,A.body=A.bv(B)
19
+ def bv(F,content):
20
20
  B=content;C='^---\\n(.*?)\\n---\\n(.*)$';A=re.match(C,B,re.S)
21
21
  if A:D=yaml.safe_load(A.group(1));E=A.group(2).strip();return D,E
22
22
  else:return{},B.strip()
@@ -30,7 +30,7 @@ class AgentManager:
30
30
  if A.project_agents_dir and not os.path.exists(A.project_agents_dir):os.makedirs(A.project_agents_dir)
31
31
  if not os.path.exists(A.user_agents_dir):os.makedirs(A.user_agents_dir)
32
32
  A.reload_agents()
33
- def bv(O,dir_path,source):
33
+ def bw(O,dir_path,source):
34
34
  F=source;C=dir_path;A={}
35
35
  if not os.path.exists(C):return A
36
36
  for(G,P,I)in os.walk(C):
@@ -47,8 +47,8 @@ class AgentManager:
47
47
  except Exception as N:print(f"加载 {F} 来源 agent {B}/{E} 失败: {str(N)}")
48
48
  return A
49
49
  def reload_agents(A):
50
- A.agents[_B].clear();A.agents[_C].clear();A.agents[_C]=A.bv(A.user_agents_dir,_C)
51
- if A.project_agents_dir:A.agents[_B]=A.bv(A.project_agents_dir,_B)
50
+ A.agents[_B].clear();A.agents[_C].clear();A.agents[_C]=A.bw(A.user_agents_dir,_C)
51
+ if A.project_agents_dir:A.agents[_B]=A.bw(A.project_agents_dir,_B)
52
52
  def bu(A,source,domain,name):
53
53
  B=source
54
54
  if B==_B:
@@ -39,7 +39,7 @@ class InstructionManager:
39
39
  if A.project_commands_dir and not os.path.exists(A.project_commands_dir):os.makedirs(A.project_commands_dir)
40
40
  if not os.path.exists(A.user_dir):os.makedirs(A.user_dir)
41
41
  A.reload_instructions()
42
- def bw(O,dir_path,source):
42
+ def by(O,dir_path,source):
43
43
  F=source;C=dir_path;A={}
44
44
  if not os.path.exists(C):return A
45
45
  for(G,P,I)in os.walk(C):
@@ -54,8 +54,8 @@ class InstructionManager:
54
54
  except Exception as N:print(f"从 {F} 加载命令 {B}/{E} 失败: {str(N)}")
55
55
  return A
56
56
  def reload_instructions(A):
57
- A.instructions[_B].clear();A.instructions[_C].clear();A.instructions[_C]=A.bw(A.user_dir,_C)
58
- if A.project_commands_dir:A.instructions[_B]=A.bw(A.project_commands_dir,_B)
57
+ A.instructions[_B].clear();A.instructions[_C].clear();A.instructions[_C]=A.by(A.user_dir,_C)
58
+ if A.project_commands_dir:A.instructions[_B]=A.by(A.project_commands_dir,_B)
59
59
  def bx(A,source,domain,name):
60
60
  B=source
61
61
  if B==_B: