@brandon_9527/tcode 1.0.3 → 1.0.7

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 (45) hide show
  1. package/dist/python-src/entry.py +59 -17
  2. package/dist/python-src/main.py +71 -12
  3. package/dist/python-src/pyproject.toml +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 +1 -1
  16. package/dist/python-src/src/managers/manager_agent.py +15 -15
  17. package/dist/python-src/src/managers/manager_context.py +1 -1
  18. package/dist/python-src/src/managers/manager_instruction.py +16 -16
  19. package/dist/python-src/src/managers/manager_skill.py +121 -0
  20. package/dist/python-src/src/managers/sandbox.py +2 -2
  21. package/dist/python-src/src/middlewares/dynamic_content.py +15 -12
  22. package/dist/python-src/src/middlewares/hitl.py +3 -3
  23. package/dist/python-src/src/middlewares/inject_content.py +0 -0
  24. package/dist/python-src/src/middlewares/memory.py +2 -2
  25. package/dist/python-src/src/middlewares/skill.py +27 -0
  26. package/dist/python-src/src/middlewares/subagents.py +4 -4
  27. package/dist/python-src/src/middlewares/summary.py +33 -33
  28. package/dist/python-src/src/prompts/prompts.py +1 -1
  29. package/dist/python-src/src/stream/formatter.py +16 -16
  30. package/dist/python-src/src/stream/handler.py +44 -45
  31. package/dist/python-src/src/stream/handler_with_tracker.py +7 -7
  32. package/dist/python-src/src/tools/tools.py +2 -2
  33. package/dist/python-src/src/trackers/__init__.py +0 -0
  34. package/dist/python-src/src/trackers/token/__init__.py +0 -0
  35. package/dist/python-src/src/trackers/token/cli.py +45 -0
  36. package/dist/python-src/src/trackers/token/pricing.py +39 -0
  37. package/dist/python-src/src/trackers/token/report.py +114 -0
  38. package/dist/python-src/src/trackers/token/tracker.py +65 -0
  39. package/dist/python-src/src/tui/chatui.py +38 -41
  40. package/dist/python-src/src/tui/components/tlist.py +7 -7
  41. package/dist/python-src/src/tui/components/tscroll_panel.py +12 -12
  42. package/dist/python-src/src/tui/demo.py +22 -0
  43. package/dist/python-src/src/tui/utils/trender.py +22 -21
  44. package/dist/python-src/uv.lock +1974 -1939
  45. package/package.json +1 -1
@@ -6,37 +6,79 @@
6
6
  # main()
7
7
 
8
8
 
9
- # ===================== 强制修复 Git Bash 报错:NoConsoleScreenBufferError =====================
10
- import os
11
- import sys
9
+ # # ===================== 强制修复 Git Bash 报错:NoConsoleScreenBufferError =====================
10
+ # import os
11
+ # import sys
12
12
 
13
- # 🔥 核心:强制让 prompt_toolkit 使用兼容模式,彻底关闭 Windows 控制台检测
14
- os.environ["PROMPT_TOOLKIT_NO_WIN32"] = "1"
15
- os.environ["PROMPT_TOOLKIT_BASIC_OUTPUT"] = "1"
16
- os.environ["PROMPT_TOOLKIT_FORCE_VT100"] = "1"
13
+ # # 🔥 核心:强制让 prompt_toolkit 使用兼容模式,彻底关闭 Windows 控制台检测
14
+ # os.environ["PROMPT_TOOLKIT_NO_WIN32"] = "1"
15
+ # os.environ["PROMPT_TOOLKIT_BASIC_OUTPUT"] = "1"
16
+ # os.environ["PROMPT_TOOLKIT_FORCE_VT100"] = "1"
17
17
 
18
- # 🔥 强制设置终端类型,让程序认为是 Linux 终端
19
- if sys.platform == "win32":
20
- os.environ["TERM"] = "xterm-256color"
18
+ # # 🔥 强制设置终端类型,让程序认为是 Linux 终端
19
+ # if sys.platform == "win32":
20
+ # os.environ["TERM"] = "xterm-256color"
21
21
 
22
- # ===================== 下面才是你原来的代码 =====================
22
+ # # ===================== 下面才是你原来的代码 =====================
23
23
 
24
24
 
25
25
  import warnings
26
26
  warnings.filterwarnings("ignore", category=DeprecationWarning)
27
27
  warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
28
28
 
29
+
30
+ import typer
31
+ import asyncio
32
+
33
+
29
34
  from main import (
30
35
  agent_ui,
31
36
  asingle_agent,
32
37
  team_main,
38
+ run_once,
39
+ teminal_chat
33
40
  )
34
41
 
42
+ app = typer.Typer()
43
+ interact_app = typer.Typer()
44
+ app.add_typer(interact_app)
45
+
46
+
47
+ @app.callback(invoke_without_command=True, no_args_is_help=False)
48
+ def root(
49
+ ctx: typer.Context,
50
+ prompt: str = typer.Option(None, "-p", "--prompt", help="用户输入请求内容"),
51
+ verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
52
+ mode: str = typer.Option("team", "-m", "--mode", help="运行模式,single或team"),
53
+ ):
54
+ if ctx.invoked_subcommand is None:
55
+ if not prompt:
56
+ if mode == "single":
57
+ asyncio.run(asingle_agent())
58
+ elif mode == "team":
59
+ asyncio.run(team_main())
60
+
61
+ # 同步环境执行异步任务
62
+ asyncio.run(run_once(prompt, verbose))
63
+ # typer.echo(result)
64
+ # raise typer.Exit()
65
+
66
+
67
+ @interact_app.command("interact")
68
+ def interact_mode(
69
+ verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
70
+ ):
71
+ # 执行交互模式
72
+ asyncio.run(teminal_chat(verbose))
73
+
74
+
75
+
35
76
  if __name__ == "__main__":
36
- import asyncio
77
+ app()
78
+ # import asyncio
37
79
 
38
- try:
39
- asyncio.run(asingle_agent())
40
- # asyncio.run(team_main())
41
- except Exception as e:
42
- pass
80
+ # try:
81
+ # # asyncio.run(asingle_agent())
82
+ # asyncio.run(team_main())
83
+ # except Exception as e:
84
+ # pass
@@ -328,14 +328,15 @@ 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}")
335
336
 
336
337
  agents = []
337
338
  for member_name, member in team_conf["members"].items():
338
- print(f"member_name: {member_name}, member: {member}")
339
+ # print(f"member_name: {member_name}, member: {member}")
339
340
 
340
341
  tools = []
341
342
  for tool_name in member["tools"]:
@@ -404,8 +405,7 @@ async def team_main():
404
405
  await ui.run_async()
405
406
 
406
407
 
407
- async def teminal_chat():
408
- """ """
408
+ def build_team():
409
409
  from dataclasses import dataclass, field
410
410
  from pathlib import Path
411
411
 
@@ -414,8 +414,6 @@ async def teminal_chat():
414
414
  from src.utils.prompt import apply_template, apply_prompt
415
415
  from src.managers.sandbox import Container
416
416
  from src.prompts.prompts import leader
417
- from src.stream.handler import ainput, anormal_handler
418
-
419
417
  from src.tools.tools import (
420
418
  SkillAgentContext,
421
419
  shell,
@@ -431,13 +429,15 @@ async def teminal_chat():
431
429
  from src.tools.web import (
432
430
  web_search,
433
431
  web_fetch
434
- )
435
-
432
+ )
433
+ from src.middlewares.dynamic_content import DynamicContentMiddleware
434
+
436
435
  from langgraph.checkpoint.memory import MemorySaver
437
- from langchain_core.messages import HumanMessage
438
436
  from langchain_core.tools import BaseTool
439
437
 
440
438
 
439
+
440
+
441
441
  def _prepare(workspace, run_mode, session_id):
442
442
  saver = MemorySaver()
443
443
 
@@ -493,14 +493,14 @@ async def teminal_chat():
493
493
 
494
494
  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):
495
495
  """ """
496
- team_conf = agents_conf[domain]
496
+ team_conf = agents_conf.setdefault(domain, {"members":{}})
497
497
 
498
498
  # for mname, mconf in team_conf['members'].items():
499
499
  # print(f"member_name: {mname}, member: {mconf}")
500
500
 
501
501
  agents = []
502
502
  for member_name, member in team_conf["members"].items():
503
- print(f"member_name: {member_name}, member: {member}")
503
+ # print(f"member_name: {member_name}, member: {member}")
504
504
 
505
505
  tools = []
506
506
  for tool_name in member["tools"]:
@@ -540,6 +540,9 @@ async def teminal_chat():
540
540
  # prompt_name="leader_",
541
541
  # WORKSPACE=workspace
542
542
  # ),
543
+ middleware=[
544
+ DynamicContentMiddleware(),
545
+ ],
543
546
  system_prompt=apply_prompt(leader, WORKSPACE=workspace),
544
547
  checkpointer=saver
545
548
  ).with_config({"recursion_limit": recursion_limit})
@@ -564,6 +567,15 @@ async def teminal_chat():
564
567
  recursion_limit=1000
565
568
  )
566
569
 
570
+ return agent, context, instruction_manager
571
+
572
+
573
+ async def teminal_chat(verbose=True):
574
+ """ """
575
+ from langchain_core.messages import HumanMessage
576
+ from src.stream.handler import ainput, anormal_handler
577
+
578
+ agent, context, instruction_manager = build_team()
567
579
 
568
580
  try:
569
581
  while True:
@@ -580,6 +592,9 @@ async def teminal_chat():
580
592
 
581
593
  new_query = f"\n [注意]: 执行用户请求必须严格遵循如下准则:\n{executed_instruction}\n\n 用户请求:\n{message}" if executed_instruction else message
582
594
 
595
+
596
+ print(f"【new_query】:\n {new_query}")
597
+
583
598
  stream = agent.astream(
584
599
  {
585
600
  "messages":[HumanMessage(content=new_query)]
@@ -589,7 +604,7 @@ async def teminal_chat():
589
604
  context=context
590
605
  ) # .with_config({"recursion_limit": 1000})
591
606
 
592
- await anormal_handler(stream, detail=True)
607
+ await anormal_handler(stream, detail=verbose)
593
608
 
594
609
  except Exception as e:
595
610
  import traceback
@@ -597,6 +612,50 @@ async def teminal_chat():
597
612
  finally:
598
613
  pass
599
614
 
615
+
616
+ async def run_once(prompt:str = None, verbose: bool=True):
617
+ from langchain_core.messages import HumanMessage
618
+ from src.stream.handler import ainput, anormal_handler
619
+
620
+ agent, context, instruction_manager = build_team()
621
+
622
+ query = prompt
623
+
624
+ try:
625
+ if query == "/comands":
626
+ for instruction in instruction_manager.list_instructions():
627
+ # print(instruction)
628
+ print(f"command: {instruction.name}, description: {instruction.description}")
629
+
630
+ instruction_result = instruction_manager.parse(query)
631
+ executed_instruction, message = instruction_result["executed_instruction"], instruction_result['message']
632
+
633
+ new_query = f"\n [注意]: 执行用户请求必须严格遵循如下准则:\n{executed_instruction}\n\n 用户请求:\n{message}" if executed_instruction else message
634
+
635
+
636
+ if verbose:
637
+ print(f"user> {new_query}")
638
+
639
+ stream = agent.astream(
640
+ {
641
+ "messages":[HumanMessage(content=new_query)]
642
+ },
643
+ config = {"configurable": {"thread_id": 1}},
644
+ stream_mode=["updates", "custom"],
645
+ context=context
646
+ ) # .with_config({"recursion_limit": 1000})
647
+
648
+ await anormal_handler(stream, detail=verbose)
649
+
650
+ except Exception as e:
651
+ import traceback
652
+ traceback.print_exc()
653
+
654
+ finally:
655
+ pass
656
+
657
+
658
+
600
659
 
601
660
 
602
661
  if __name__ == "__main__":
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "langchain-core==1.2.13",
33
33
  "python-minifier>=3.2.0",
34
34
  "ddgs>=9.11.4",
35
+ "typer>=0.24.1",
35
36
  ]
36
37
 
37
38
  #[project.scripts]
@@ -56,7 +57,7 @@ skip = [
56
57
  "resources",
57
58
  "_workspace",
58
59
  ".autodev",
59
- "tools"
60
+ "tools",
60
61
  ]
61
62
 
62
63
  exclude = [
@@ -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.dg).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 dh(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.dh,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 dj(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 di(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.dj(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.di(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 dg(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}")