@agentunion/kite 1.4.0 → 1.5.0

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 (235) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/cli.js +44 -5
  3. package/core/dependency_checker.py +250 -0
  4. package/core/env_checker.py +490 -0
  5. package/dependencies_lock.json +128 -0
  6. package/extensions/agents/assistant/server.py +33 -17
  7. package/extensions/channels/acp_channel/server.py +33 -17
  8. package/extensions/services/backup/entry.py +23 -16
  9. package/extensions/services/evol/auth_manager.py +443 -0
  10. package/extensions/services/evol/config.yaml +149 -0
  11. package/extensions/services/evol/config_loader.py +117 -0
  12. package/extensions/services/evol/entry.py +406 -0
  13. package/extensions/services/evol/evol_api.py +173 -0
  14. package/extensions/services/evol/evol_config.json5 +29 -0
  15. package/extensions/services/evol/migrate_tokens.py +122 -0
  16. package/extensions/services/evol/module.md +32 -0
  17. package/extensions/services/evol/pairing.py +250 -0
  18. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  19. package/extensions/services/evol/relay.py +682 -0
  20. package/extensions/services/evol/relay_config.json5 +67 -0
  21. package/extensions/services/evol/routes/__init__.py +1 -0
  22. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  23. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  24. package/extensions/services/evol/routes/routes_test.py +61 -0
  25. package/extensions/services/evol/server.py +875 -0
  26. package/extensions/services/evol/static/css/style.css +1200 -0
  27. package/extensions/services/evol/static/index.html +781 -0
  28. package/extensions/services/evol/static/index_evol.html +14 -0
  29. package/extensions/services/evol/static/js/app.js +6304 -0
  30. package/extensions/services/evol/static/js/auth.js +326 -0
  31. package/extensions/services/evol/static/js/dialog.js +285 -0
  32. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  33. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  34. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  35. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  36. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  37. package/extensions/services/evol/static/js/main.js +141 -0
  38. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  39. package/extensions/services/evol/static/js/stats.js +217 -0
  40. package/extensions/services/evol/static/js/token-manager.js +175 -0
  41. package/extensions/services/evol/static/pairing.html +248 -0
  42. package/extensions/services/evol/static/test_registry.html +262 -0
  43. package/extensions/services/evol/static/test_relay.html +462 -0
  44. package/extensions/services/evol/stats_manager.py +240 -0
  45. package/extensions/services/model_service/entry.py +23 -1
  46. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  47. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  48. package/extensions/services/proxy/_fix_prints.py +133 -0
  49. package/extensions/services/proxy/_fix_prints2.py +87 -0
  50. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  51. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  52. package/extensions/services/proxy/agentcp/README.md +260 -0
  53. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  54. package/extensions/services/proxy/agentcp/agent.py +4 -0
  55. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  56. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  57. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  58. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  59. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  60. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  61. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  62. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  63. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  64. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  65. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  66. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  67. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  68. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  69. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  70. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  71. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  72. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  73. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  74. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  75. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  76. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  77. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  78. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  79. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  80. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  81. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  82. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  83. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  84. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  85. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  86. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  87. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  88. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  89. package/extensions/services/proxy/agentcp/message.py +149 -0
  90. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  91. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  92. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  93. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  94. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  95. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  96. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  97. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  98. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  99. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  100. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  101. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  102. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  103. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  104. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  105. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  106. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  107. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  108. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  109. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  110. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  111. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  112. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  113. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  122. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  123. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  124. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  125. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  126. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  127. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  128. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  129. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  130. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  131. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  132. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  133. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  134. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  135. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  136. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  138. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  139. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  140. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  141. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  142. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  143. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  144. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  145. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  146. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  147. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  148. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  149. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  150. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  151. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  152. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  153. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  154. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  155. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  156. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  157. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  158. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  159. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  160. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  161. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  162. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  163. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  165. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  166. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  168. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  169. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  171. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  172. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  173. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  174. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  175. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  176. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  177. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  178. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  179. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  180. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  181. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  182. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  183. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  184. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  185. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  186. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  187. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  188. package/extensions/services/proxy/console_auth.py +109 -0
  189. package/extensions/services/proxy/evol/__init__.py +1 -0
  190. package/extensions/services/proxy/evol/config.py +37 -0
  191. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  192. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  193. package/extensions/services/proxy/evol/log.py +28 -0
  194. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  195. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  196. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  197. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  198. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  199. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  200. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  201. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  202. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  203. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  204. package/extensions/services/proxy/evol/version.py +24 -0
  205. package/extensions/services/proxy/logs/websocket.log +260 -0
  206. package/extensions/services/proxy/main.py +240 -0
  207. package/extensions/services/proxy/requirements.txt +13 -0
  208. package/extensions/services/proxy/server.py +271 -0
  209. package/extensions/services/watchdog/entry.py +42 -16
  210. package/extensions/services/watchdog/module.md +1 -0
  211. package/extensions/services/watchdog/monitor.py +34 -4
  212. package/extensions/services/web/module.md +1 -1
  213. package/extensions/services/web/server.py +30 -18
  214. package/extensions/services/web/static/js/token-manager.js +10 -10
  215. package/kernel/entry.py +1 -1
  216. package/kernel/module.md +25 -1
  217. package/kernel/registry_store.py +2 -26
  218. package/kernel/rpc_router.py +36 -10
  219. package/kernel/server.py +106 -17
  220. package/kite_cli/commands/deps_install.py +67 -0
  221. package/kite_cli/commands/env_check.py +45 -0
  222. package/kite_cli/commands/prepare.py +49 -0
  223. package/kite_cli/commands/venv_setup.py +56 -0
  224. package/kite_cli/main.py +29 -1
  225. package/launcher/entry.py +306 -21
  226. package/launcher/module.md +9 -0
  227. package/launcher/module_scanner.py +11 -1
  228. package/main.py +4 -1
  229. package/package.json +8 -1
  230. package/python_version.json +4 -0
  231. package/requirements.txt +38 -0
  232. package/scripts/env-manager.js +328 -0
  233. package/scripts/python-env.js +79 -0
  234. package/scripts/scan_dependencies.py +461 -0
  235. package/scripts/setup-python-env.js +191 -0
@@ -0,0 +1,149 @@
1
+ user:
2
+ phone_number: "" # 当前 AI 使用的号码
3
+ phone_numbers: [] # AI 号码列表,可在配置页面管理
4
+ owners: [] # 主人列表,每项包含 phone/name 等基础信息
5
+ # 示例:
6
+ # phone_numbers:
7
+ # - "13800138000"
8
+ # - "13800138001"
9
+ # owners:
10
+ # - phone: "13900139000"
11
+ # name: "张三"
12
+ # note: "公司CEO"
13
+ # - phone: "13700137000"
14
+ # name: "李四"
15
+
16
+ server:
17
+ host: "0.0.0.0"
18
+ port: 18766
19
+ ssl: true
20
+
21
+ bluetooth:
22
+ auto_connect: true
23
+ reconnect_interval: 5
24
+ reconnect_max_interval: 60
25
+ device_address: ""
26
+
27
+ audio:
28
+ sample_rate: 16000
29
+ channels: 1
30
+ format: "s16le"
31
+ record_calls: true
32
+
33
+ llm:
34
+ active_provider: "openai"
35
+ providers:
36
+ openai:
37
+ base_url: "https://api.openai.com/v1"
38
+ api_key: ""
39
+ model: "gpt-4o"
40
+ temperature: 0.7
41
+ max_tokens: 1024
42
+ claude:
43
+ base_url: "https://api.anthropic.com/v1"
44
+ api_key: ""
45
+ model: "claude-sonnet-4-20250514"
46
+ temperature: 0.7
47
+ max_tokens: 1024
48
+ gemini:
49
+ base_url: "https://generativelanguage.googleapis.com/v1beta"
50
+ api_key: ""
51
+ model: "gemini-2.0-flash"
52
+ temperature: 0.7
53
+ max_tokens: 1024
54
+
55
+ asr:
56
+ provider: "whisper"
57
+ whisper:
58
+ base_url: "https://api.openai.com/v1"
59
+ api_key: ""
60
+ model: "whisper-1"
61
+ language: "zh"
62
+ xunfei:
63
+ app_id: ""
64
+ api_key: ""
65
+ api_secret: ""
66
+ volcengine:
67
+ app_id: ""
68
+ access_token: ""
69
+ resource_id: "volc.bigasr.sauc.duration"
70
+ ws_url: "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
71
+ tencent:
72
+ app_id: ""
73
+ secret_id: ""
74
+ secret_key: ""
75
+ engine_model_type: "16k_zh_large"
76
+
77
+ tts:
78
+ provider: "edge-tts"
79
+ edge_tts:
80
+ voice: "zh-CN-XiaoxiaoNeural"
81
+ rate: "+0%"
82
+ volume: "+0%"
83
+ volcengine:
84
+ app_id: ""
85
+ access_token: ""
86
+ cluster: "volcano_tts"
87
+ voice_type: "BV001_streaming"
88
+ speed_ratio: 1.0
89
+ volume_ratio: 1.0
90
+ pitch_ratio: 1.0
91
+ encoding: "mp3"
92
+ sample_rate: 24000
93
+ tencent:
94
+ app_id: ""
95
+ secret_id: ""
96
+ secret_key: ""
97
+ voice_type: 101001
98
+ codec: "pcm"
99
+ sample_rate: 16000
100
+ speed: 0
101
+ volume: 0
102
+ azure:
103
+ api_key: ""
104
+ region: "eastasia"
105
+ voice: "zh-CN-XiaoxiaoNeural"
106
+ rate: "1.0"
107
+ volume: "100"
108
+ openai:
109
+ api_key: ""
110
+ base_url: "https://api.openai.com/v1"
111
+ model: "tts-1"
112
+ voice: "alloy"
113
+ speed: 1.0
114
+ aliyun:
115
+ access_key_id: ""
116
+ access_key_secret: ""
117
+ app_key: ""
118
+ voice: "zhixiaobai"
119
+ sample_rate: 16000
120
+ speech_rate: 0
121
+ volume: 50
122
+ local:
123
+ engine: "sherpa-onnx"
124
+ model_path: ""
125
+ voice: ""
126
+ sample_rate: 16000
127
+
128
+ vad:
129
+ mode: 3
130
+ energy_threshold: 300
131
+ silence_threshold_ms: 800
132
+ min_speech_ms: 250
133
+
134
+ call:
135
+ max_duration_seconds: 300
136
+ no_response_timeout: 15
137
+ no_response_max_retries: 3
138
+ incoming_confirm_timeout: 15
139
+ incoming_default_action: "reject"
140
+ opening_message: ""
141
+
142
+ webhook:
143
+ default_url: ""
144
+ retry_count: 3
145
+ retry_delays: [1, 5, 30]
146
+ timeout: 10
147
+
148
+ data:
149
+ base_dir: "./data"
@@ -0,0 +1,117 @@
1
+ """
2
+ 配置加载工具
3
+
4
+ 用于加载模块的业务配置。支持从 module.md 读取业务配置块,
5
+ 并动态加载对应的 JSON5 配置文件。
6
+
7
+ 零共享代码依赖 - 此文件可以独立拷贝到其他模块使用。
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import json5
13
+ import yaml
14
+
15
+
16
+ def load_module_metadata(module_dir: str) -> dict:
17
+ """
18
+ 读取 module.md 的 YAML frontmatter。
19
+
20
+ Args:
21
+ module_dir: 模块目录路径
22
+
23
+ Returns:
24
+ 模块元数据字典
25
+ """
26
+ md_path = os.path.join(module_dir, "module.md")
27
+
28
+ if not os.path.exists(md_path):
29
+ return {}
30
+
31
+ try:
32
+ with open(md_path, "r", encoding="utf-8") as f:
33
+ text = f.read()
34
+
35
+ # 提取 YAML frontmatter (--- ... ---)
36
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
37
+ if m:
38
+ return yaml.safe_load(m.group(1)) or {}
39
+ except Exception as e:
40
+ print(f"[config_loader] Error loading module.md: {e}")
41
+
42
+ return {}
43
+
44
+
45
+ def load_business_configs(module_dir: str) -> dict:
46
+ """
47
+ 加载所有业务配置。
48
+
49
+ 从 module.md 的 businesses 块读取业务列表,
50
+ 然后加载每个业务的配置文件(JSON5 格式)。
51
+
52
+ Args:
53
+ module_dir: 模块目录路径
54
+
55
+ Returns:
56
+ 业务配置字典,格式:
57
+ {
58
+ "business_name": {
59
+ "metadata": {
60
+ "name": "business_name",
61
+ "type": "business_type",
62
+ "description": "...",
63
+ "config_file": "config.json5"
64
+ },
65
+ "config": { ... } # JSON5 配置内容
66
+ }
67
+ }
68
+ """
69
+ metadata = load_module_metadata(module_dir)
70
+ businesses = metadata.get('businesses', [])
71
+
72
+ if not businesses:
73
+ print(f"[config_loader] No businesses configured in module.md")
74
+ return {}
75
+
76
+ configs = {}
77
+ for business in businesses:
78
+ name = business.get('name')
79
+ config_file = business.get('config_file')
80
+
81
+ if not name or not config_file:
82
+ print(f"[config_loader] Warning: Invalid business entry (missing name or config_file): {business}")
83
+ continue
84
+
85
+ config_path = os.path.join(module_dir, config_file)
86
+
87
+ if os.path.exists(config_path):
88
+ try:
89
+ with open(config_path, "r", encoding="utf-8") as f:
90
+ configs[name] = {
91
+ 'metadata': business,
92
+ 'config': json5.load(f)
93
+ }
94
+ print(f"[config_loader] Loaded business config: {name} from {config_file}")
95
+ except json5.JSON5DecodeError as e:
96
+ print(f"[config_loader] Error: Invalid JSON5 syntax in {config_file}: {e}")
97
+ except Exception as e:
98
+ print(f"[config_loader] Error loading {config_file}: {e}")
99
+ else:
100
+ print(f"[config_loader] Warning: Config file not found: {config_path}")
101
+
102
+ return configs
103
+
104
+
105
+ def get_business_config(module_dir: str, business_name: str) -> dict | None:
106
+ """
107
+ 获取指定业务的配置。
108
+
109
+ Args:
110
+ module_dir: 模块目录路径
111
+ business_name: 业务名称
112
+
113
+ Returns:
114
+ 业务配置字典,如果不存在则返回 None
115
+ """
116
+ configs = load_business_configs(module_dir)
117
+ return configs.get(business_name)
@@ -0,0 +1,406 @@
1
+ """
2
+ Evol module entry point.
3
+ Reads boot_info from stdin, starts HTTP server for Evol account management.
4
+ """
5
+
6
+ import builtins
7
+ import json
8
+ import os
9
+ import re
10
+ import socket
11
+ import sys
12
+ import threading
13
+ import time
14
+ import traceback
15
+ from datetime import datetime, timezone
16
+
17
+ import uvicorn
18
+
19
+
20
+ # ── Module configuration ──
21
+
22
+ def _load_module_config() -> dict:
23
+ """Load module configuration from module.md frontmatter."""
24
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
25
+ module_md = os.path.join(_this_dir, "module.md")
26
+
27
+ project_root = os.environ.get("KITE_PROJECT", "")
28
+ if project_root and _this_dir.startswith(project_root):
29
+ rel_path = os.path.relpath(_this_dir, project_root)
30
+ else:
31
+ rel_path = _this_dir
32
+
33
+ result = {
34
+ "name": "",
35
+ "preferred_port": 0,
36
+ "advertise_ip": "0.0.0.0"
37
+ }
38
+
39
+ if not os.path.exists(module_md):
40
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
41
+ print(f" Path: {rel_path}/module.md")
42
+ print(f" Reason: File not found")
43
+ sys.exit(1)
44
+
45
+ try:
46
+ with open(module_md, encoding="utf-8") as f:
47
+ text = f.read()
48
+
49
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
50
+ if not m:
51
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
52
+ print(f" Path: {rel_path}/module.md")
53
+ print(f" Reason: Missing YAML frontmatter")
54
+ sys.exit(1)
55
+
56
+ import yaml
57
+ fm = yaml.safe_load(m.group(1)) or {}
58
+
59
+ if "name" not in fm:
60
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
61
+ print(f" Path: {rel_path}/module.md")
62
+ print(f" Reason: Missing 'name' field")
63
+ sys.exit(1)
64
+
65
+ raw_name = str(fm["name"]).strip()
66
+
67
+ if not raw_name:
68
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
69
+ print(f" Path: {rel_path}/module.md")
70
+ print(f" Reason: Empty module name")
71
+ sys.exit(1)
72
+
73
+ sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
74
+
75
+ if sanitized != raw_name:
76
+ invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
77
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
78
+ print(f" Path: {rel_path}/module.md")
79
+ print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
80
+ sys.exit(1)
81
+
82
+ result["name"] = sanitized
83
+
84
+ if "preferred_port" in fm:
85
+ try:
86
+ result["preferred_port"] = int(fm["preferred_port"])
87
+ except (ValueError, TypeError):
88
+ pass
89
+
90
+ if "advertise_ip" in fm:
91
+ result["advertise_ip"] = str(fm["advertise_ip"])
92
+
93
+ except SystemExit:
94
+ raise
95
+ except Exception as e:
96
+ print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
97
+ sys.exit(1)
98
+
99
+ return result
100
+
101
+ _module_config = _load_module_config()
102
+ MODULE_NAME = _module_config["name"]
103
+
104
+
105
+ # ── Safe stdout/stderr ──
106
+
107
+ class _SafeWriter:
108
+ def __init__(self, stream):
109
+ self._stream = stream
110
+
111
+ def write(self, s):
112
+ try:
113
+ self._stream.write(s)
114
+ except (BrokenPipeError, OSError):
115
+ pass
116
+
117
+ def flush(self):
118
+ try:
119
+ self._stream.flush()
120
+ except (BrokenPipeError, OSError):
121
+ pass
122
+
123
+ def __getattr__(self, name):
124
+ return getattr(self._stream, name)
125
+
126
+ sys.stdout = _SafeWriter(sys.stdout)
127
+ sys.stderr = _SafeWriter(sys.stderr)
128
+
129
+
130
+ # ── Timestamped print + log file writer ──
131
+
132
+ _builtin_print = builtins.print
133
+ _start_ts = time.monotonic()
134
+ _last_ts = time.monotonic()
135
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
136
+ _log_lock = threading.Lock()
137
+ _log_latest_path = None
138
+ _log_daily_path = None
139
+ _log_daily_date = ""
140
+ _log_dir = None
141
+ _crash_log_path = None
142
+
143
+ def _strip_ansi(s: str) -> str:
144
+ return _ANSI_RE.sub("", s)
145
+
146
+ def _resolve_daily_log_path():
147
+ """Resolve daily log path based on current date."""
148
+ global _log_daily_path, _log_daily_date
149
+ if not _log_dir:
150
+ return
151
+ today = datetime.now().strftime("%Y-%m-%d")
152
+ if today == _log_daily_date and _log_daily_path:
153
+ return
154
+ month_dir = os.path.join(_log_dir, today[:7])
155
+ os.makedirs(month_dir, exist_ok=True)
156
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
157
+ _log_daily_date = today
158
+
159
+ def _write_log(plain_line: str):
160
+ """Write a plain-text line to both latest.log and daily log."""
161
+ with _log_lock:
162
+ if _log_latest_path:
163
+ try:
164
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
165
+ f.write(plain_line)
166
+ except Exception:
167
+ pass
168
+ _resolve_daily_log_path()
169
+ if _log_daily_path:
170
+ try:
171
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
172
+ f.write(plain_line)
173
+ except Exception:
174
+ pass
175
+
176
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
177
+ """Write crash record to crashes.jsonl + daily crash archive."""
178
+ record = {
179
+ "timestamp": datetime.now(timezone.utc).isoformat(),
180
+ "module": MODULE_NAME,
181
+ "thread": thread_name or threading.current_thread().name,
182
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
183
+ "exception_message": str(exc_value),
184
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
185
+ "severity": severity,
186
+ "handled": handled,
187
+ "process_id": os.getpid(),
188
+ "platform": sys.platform,
189
+ "runtime_version": f"Python {sys.version.split()[0]}",
190
+ }
191
+
192
+ if exc_tb:
193
+ tb_entries = traceback.extract_tb(exc_tb)
194
+ if tb_entries:
195
+ last = tb_entries[-1]
196
+ record["context"] = {
197
+ "function": last.name,
198
+ "file": os.path.basename(last.filename),
199
+ "line": last.lineno,
200
+ }
201
+
202
+ line = json.dumps(record, ensure_ascii=False) + "\n"
203
+
204
+ if _crash_log_path:
205
+ try:
206
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
207
+ f.write(line)
208
+ except Exception:
209
+ pass
210
+
211
+ if _log_dir:
212
+ try:
213
+ today = datetime.now().strftime("%Y-%m-%d")
214
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
215
+ os.makedirs(archive_dir, exist_ok=True)
216
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
217
+ with open(archive_path, "a", encoding="utf-8") as f:
218
+ f.write(line)
219
+ except Exception:
220
+ pass
221
+
222
+ def _setup_exception_hooks():
223
+ """Set up global exception hooks."""
224
+ _orig_excepthook = sys.excepthook
225
+
226
+ def _excepthook(exc_type, exc_value, exc_tb):
227
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
228
+ _orig_excepthook(exc_type, exc_value, exc_tb)
229
+
230
+ sys.excepthook = _excepthook
231
+
232
+ if hasattr(threading, "excepthook"):
233
+ def _thread_excepthook(args):
234
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
235
+ thread_name=args.thread.name if args.thread else "unknown",
236
+ severity="error", handled=False)
237
+ threading.excepthook = _thread_excepthook
238
+
239
+ def _tprint(*args, **kwargs):
240
+ global _last_ts
241
+ now = time.monotonic()
242
+ elapsed = now - _start_ts
243
+ delta = now - _last_ts
244
+ _last_ts = now
245
+
246
+ if elapsed < 1:
247
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
248
+ elif elapsed < 100:
249
+ elapsed_str = f"{elapsed:.1f}s"
250
+ else:
251
+ elapsed_str = f"{elapsed:.0f}s"
252
+
253
+ if delta < 0.001:
254
+ delta_str = ""
255
+ elif delta < 1:
256
+ delta_str = f"+{delta * 1000:.0f}ms"
257
+ elif delta < 100:
258
+ delta_str = f"+{delta:.1f}s"
259
+ else:
260
+ delta_str = f"+{delta:.0f}s"
261
+
262
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
263
+
264
+ _builtin_print(*args, **kwargs)
265
+
266
+ if _log_latest_path or _log_daily_path:
267
+ sep = kwargs.get("sep", " ")
268
+ end = kwargs.get("end", "\n")
269
+ text = sep.join(str(a) for a in args)
270
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} [{MODULE_NAME}] "
271
+ _write_log(prefix + _strip_ansi(text) + end)
272
+
273
+ builtins.print = _tprint
274
+
275
+
276
+ # ── Path setup ──
277
+
278
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
279
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(_this_dir)))
280
+ if _project_root not in sys.path:
281
+ sys.path.insert(0, _project_root)
282
+
283
+
284
+ # ── Import server ──
285
+
286
+ from extensions.services.evol.server import EvolServer
287
+
288
+
289
+ def _fmt_elapsed(t0: float) -> str:
290
+ d = time.monotonic() - t0
291
+ if d < 1:
292
+ return f"{d * 1000:.0f}ms"
293
+ if d < 10:
294
+ return f"{d:.1f}s"
295
+ return f"{d:.0f}s"
296
+
297
+
298
+ def _bind_port(preferred: int, host: str, max_attempts: int = 10) -> int | None:
299
+ """Try to bind to preferred port, then port+1, port+2, ..."""
300
+ if not preferred:
301
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
302
+ s.bind((host, 0))
303
+ return s.getsockname()[1]
304
+
305
+ for attempt in range(max_attempts):
306
+ port = preferred + attempt
307
+ try:
308
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
309
+ s.bind((host, port))
310
+ if attempt > 0:
311
+ print(f"Bound to port {port} (preferred {preferred} was occupied)")
312
+ return port
313
+ except OSError:
314
+ if attempt < max_attempts - 1:
315
+ continue
316
+ else:
317
+ print(f"ERROR: Failed to bind port after {max_attempts} attempts")
318
+ return None
319
+
320
+ return None
321
+
322
+
323
+ def main():
324
+ # Initialize log file paths
325
+ global _log_dir, _log_latest_path, _crash_log_path
326
+ module_data = os.environ.get("KITE_MODULE_DATA")
327
+ if module_data:
328
+ _log_dir = os.path.join(module_data, "log")
329
+ os.makedirs(_log_dir, exist_ok=True)
330
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
331
+
332
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
333
+ try:
334
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
335
+ pass
336
+ except Exception:
337
+ _log_latest_path = None
338
+
339
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
340
+ try:
341
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
342
+ pass
343
+ except Exception:
344
+ _crash_log_path = None
345
+
346
+ _resolve_daily_log_path()
347
+
348
+ _setup_exception_hooks()
349
+
350
+ _t0 = time.monotonic()
351
+
352
+ # Read boot_info from stdin
353
+ token = ""
354
+ try:
355
+ line = sys.stdin.readline().strip()
356
+ if line:
357
+ boot_info = json.loads(line)
358
+ token = boot_info.get("token", "")
359
+ except Exception:
360
+ pass
361
+
362
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
363
+
364
+ if not token or not kernel_port:
365
+ print("ERROR: Missing token or KITE_KERNEL_PORT")
366
+ sys.exit(1)
367
+
368
+ print(f"Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
369
+
370
+ host = _module_config["advertise_ip"]
371
+ port = _bind_port(_module_config["preferred_port"], host)
372
+
373
+ if port is None:
374
+ print("ERROR: Cannot bind to any port, exiting")
375
+ sys.exit(1)
376
+
377
+ server = EvolServer(
378
+ token=token,
379
+ kernel_port=kernel_port,
380
+ host=host,
381
+ port=port,
382
+ boot_t0=_t0,
383
+ )
384
+
385
+ display_host = "localhost" if host == "0.0.0.0" else host
386
+ url = f"http://{display_host}:{port}"
387
+ print(f"Starting on {host}:{port} ({_fmt_elapsed(_t0)})")
388
+ print(f"\033[32m✓ Evol UI ready: {url}\033[0m")
389
+
390
+ try:
391
+ config = uvicorn.Config(server.app, host=host, port=port, log_level="warning")
392
+ uvi_server = uvicorn.Server(config)
393
+ server._uvicorn_server = uvi_server
394
+ uvi_server.run()
395
+ except Exception as e:
396
+ print(f"ERROR: {e}")
397
+ traceback.print_exc()
398
+ sys.exit(1)
399
+
400
+ # Check if server requested exit with non-zero code
401
+ if server._exit_code != 0:
402
+ sys.exit(server._exit_code)
403
+
404
+
405
+ if __name__ == "__main__":
406
+ main()