@agentunion/kite 1.5.0 → 1.6.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 (573) hide show
  1. package/.claude/skills/kite/checklists/feature-checklist.md +496 -0
  2. package/.claude/skills/kite/references/event-patterns.md +180 -0
  3. package/.claude/skills/kite/references/health-check.md +202 -0
  4. package/.claude/skills/kite/references/http-service.md +199 -0
  5. package/.claude/skills/kite/references/module-md-spec.md +172 -0
  6. package/.claude/skills/kite/references/multi-connection.md +147 -0
  7. package/.claude/skills/kite/references/rpc-patterns.md +199 -0
  8. package/.claude/skills/kite/references/shutdown-sequence.md +146 -0
  9. package/.claude/skills/kite/references/stdin-protocol.md +147 -0
  10. package/.claude/skills/kite/references/test-center-integration.md +178 -0
  11. package/.claude/skills/kite/references/ws-lifecycle.md +301 -0
  12. package/.claude/skills/kite/skill.md +272 -0
  13. package/.claude/skills/kite/templates/go/README.md +20 -0
  14. package/.claude/skills/kite/templates/node/entry.js +134 -0
  15. package/.claude/skills/kite/templates/node/module.md +16 -0
  16. package/.claude/skills/kite/templates/node/server.js +351 -0
  17. package/.claude/skills/kite/templates/node/server_http.js +90 -0
  18. package/.claude/skills/kite/templates/python/entry.py +425 -0
  19. package/.claude/skills/kite/templates/python/module.md +26 -0
  20. package/.claude/skills/kite/templates/python/server.py +447 -0
  21. package/.claude/skills/kite/templates/python/server_http.py +433 -0
  22. package/cli.js +38 -4
  23. package/core/env_checker.py +96 -0
  24. package/docs/05-/347/237/255/344/277/241/350/256/244/350/257/201/344/270/216/347/224/250/346/210/267/344/277/241/346/201/257/346/216/245/345/217/243/346/226/207/346/241/243.md +507 -0
  25. package/docs/ACP/345/215/217/350/256/256/345/205/274/345/256/271/346/226/271/346/241/210.md +138 -0
  26. package/docs/CI/344/270/216AI/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/226/271/346/241/210.md +75 -0
  27. package/docs/CLI/345/274/200/345/217/221/350/256/241/345/210/222.md +595 -0
  28. package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237-/346/212/200/346/234/257/350/257/204/344/274/260.md +535 -0
  29. package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237/350/256/276/350/256/241.md +631 -0
  30. package/docs/Evol-App/344/275/277/347/224/250KernelClient/346/224/271/351/200/240/345/256/214/346/210/220.md +342 -0
  31. package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/346/246/202/350/246/201.md +604 -0
  32. package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241.md +1708 -0
  33. package/docs/Evol/346/250/241/345/235/227/350/256/276/350/256/241/346/226/271/346/241/210.md +1154 -0
  34. package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-Evol/346/250/241/345/235/227/345/256/236/346/226/275/346/214/207/345/215/227.md +403 -0
  35. package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-/345/244/226/351/203/250/346/250/241/345/235/227/346/216/245/345/205/245/346/214/207/345/215/227.md +468 -0
  36. package/docs/HTTP-RPC/350/277/201/347/247/273/345/210/260WebSocket/350/256/241/345/210/222.md +318 -0
  37. package/docs/INDEX.md +388 -0
  38. package/docs/KITE_DOCS_GUIDE.md +33 -0
  39. package/docs/Kernel-Client-Kite-Token/346/224/257/346/214/201/345/256/236/346/226/275/345/256/214/346/210/220.md +330 -0
  40. package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266-/346/255/243/347/241/256/345/256/236/347/216/260.md +235 -0
  41. package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266/345/256/236/346/226/275/346/200/273/347/273/223.md +204 -0
  42. package/docs/Kite/345/256/211/350/243/205/351/227/256/351/242/230/350/247/243/345/206/263/346/226/271/346/241/210.md +362 -0
  43. package/docs/Kite/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241-/347/273/210/346/236/201/347/233/256/346/240/207.md +721 -0
  44. package/docs/Kite/346/216/247/345/210/266/345/217/260/347/273/237/344/270/200WebSocket/346/224/271/351/200/240/346/226/271/346/241/210.md +821 -0
  45. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/01-/346/241/206/346/236/266/345/256/232/344/275/215.md +12 -0
  46. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/02-/346/240/270/345/277/203/346/246/202/345/277/265.md +341 -0
  47. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/03-/347/263/273/347/273/237/346/236/266/346/236/204.md +257 -0
  48. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/04-/346/250/241/345/235/227/350/247/204/350/214/203.md +263 -0
  49. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213-/346/226/260/347/211/210.md +267 -0
  50. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213.md +149 -0
  51. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/06-/347/233/256/345/275/225/347/273/223/346/236/204.md +231 -0
  52. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/07-/346/225/260/346/215/256/346/250/241/345/236/213.md +68 -0
  53. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/08-/346/211/251/345/261/225/346/200/247.md +34 -0
  54. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/09-/344/270/216/345/205/267/344/275/223/345/272/224/347/224/250/347/232/204/345/205/263/347/263/273.md +22 -0
  55. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/README.md +46 -0
  56. package/docs/Kite/347/263/273/347/273/237/345/220/257/345/212/250/346/265/201/347/250/213.md +567 -0
  57. package/docs/Launcher/345/220/257/345/212/250/345/231/250/346/226/207/346/241/243.md +745 -0
  58. package/docs/Polyglot/350/277/220/350/241/214/346/227/266/344/270/216Clawdbot/345/205/274/345/256/271/346/200/247/350/256/276/350/256/241.md +321 -0
  59. package/docs/Redis/344/270/216/346/250/241/345/235/227/345/244/232/345/256/236/344/276/213/346/226/271/346/241/210.md +438 -0
  60. package/docs/Relay-Kite-Token/350/256/244/350/257/201/345/256/236/346/226/275/345/256/214/346/210/220.md +178 -0
  61. package/docs/Relay-Token/346/235/203/351/231/220/351/205/215/347/275/256/351/252/214/350/257/201.md +113 -0
  62. package/docs/Watchdog/345/201/245/345/272/267/346/243/200/346/237/245/344/270/216WebSocket-Ping/346/234/272/345/210/266/345/210/206/346/236/220.md +367 -0
  63. package/docs/Watchdog/350/265/204/346/272/220/347/233/221/346/216/247/347/255/226/347/225/245.md +92 -0
  64. package/docs/WebSocket/346/216/245/346/224/266/345/276/252/347/216/257/346/255/273/351/224/201/351/230/262/350/214/203/350/247/204/350/214/203.md +357 -0
  65. package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/344/270/216/351/207/215/350/277/236/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +531 -0
  66. package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/346/226/271/346/241/210.md +169 -0
  67. package/docs/WebSocket/351/207/215/350/277/236/346/234/272/345/210/266/346/265/213/350/257/225/346/212/245/345/221/212.md +169 -0
  68. package/docs/WebSocket/351/207/215/350/277/236/351/200/200/351/201/277/346/234/272/345/210/266/346/226/271/346/241/210.md +394 -0
  69. package/docs/Web/346/250/241/345/235/227/344/270/216Evol/346/250/241/345/235/227/351/207/215/346/236/204/345/210/206/346/236/220.md +521 -0
  70. package/docs/audit-api-guide.md +68 -0
  71. package/docs/audit-module-design.md +315 -0
  72. package/docs/audit-module-implementation-summary.md +149 -0
  73. package/docs/llm-context-design.md +52 -0
  74. package/docs/llm-test-enhancement-plan.md +970 -0
  75. package/docs/logs-api-guide.md +42 -0
  76. package/docs/npm/345/214/205Python/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +302 -0
  77. package/docs/npm/345/217/221/345/270/203/344/270/216CLI/344/275/277/347/224/250/346/214/207/345/215/227.md +245 -0
  78. package/docs/stdio/344/270/216/347/253/257/345/217/243/345/217/221/347/216/260/351/207/215/346/236/204.md +480 -0
  79. package/docs/web/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/350/256/276/350/256/241/346/226/271/346/241/210.md +449 -0
  80. package/docs//344/272/213/344/273/266/345/244/204/347/220/206/346/234/272/345/210/266.md +388 -0
  81. package/docs//344/272/213/344/273/266/345/244/204/347/220/206/350/247/204/350/214/203.md +113 -0
  82. package/docs//344/272/213/344/273/266/350/256/242/351/230/205/351/200/232/351/205/215/347/254/246/350/247/204/350/214/203.md +256 -0
  83. package/docs//344/272/213/344/273/266/351/230/237/345/210/227/345/274/271/346/200/247/347/256/241/347/220/206.md +449 -0
  84. package/docs//344/272/244/344/272/222/345/274/217/347/273/210/347/253/257/346/216/247/345/210/266/346/226/271/346/241/210.md +301 -0
  85. package/docs//344/273/243/347/220/206/345/220/257/345/212/250/345/231/250/344/270/216/345/256/271/345/231/250/345/214/226.md +140 -0
  86. package/docs//344/273/243/347/240/201/347/273/237/350/256/241/345/267/245/345/205/267/344/275/277/347/224/250/350/257/264/346/230/216.md +217 -0
  87. package/docs//344/274/230/351/233/205/351/200/200/345/207/272/350/247/204/350/214/203.md +362 -0
  88. package/docs//344/276/235/350/265/226/347/256/241/347/220/206/350/257/264/346/230/216.md +141 -0
  89. package/docs//344/277/256/345/244/215/346/235/203/351/231/220/351/227/256/351/242/230-evol-RPC/346/235/203/351/231/220.md +268 -0
  90. package/docs//345/210/240/351/231/244kernel-client-example/345/256/214/346/210/220.md +309 -0
  91. package/docs//345/210/240/351/231/244ws-management/345/256/214/346/210/220.md +418 -0
  92. package/docs//345/220/257/345/212/250/344/274/230/345/214/226/346/226/271/346/241/210.md +522 -0
  93. package/docs//345/220/257/345/212/250/344/276/235/350/265/226/344/270/216/346/216/222/345/272/217.md +105 -0
  94. package/docs//345/256/211/350/243/205/350/204/232/346/234/254/345/274/200/345/217/221/346/226/207/346/241/243.md +643 -0
  95. package/docs//345/256/214/346/225/264/345/220/257/345/212/250/346/265/201/347/250/213/350/256/276/350/256/241.md +452 -0
  96. package/docs//345/256/236/347/216/260/350/247/204/345/210/222.md +195 -0
  97. package/docs//345/277/203/350/267/263/346/234/272/345/210/266/351/207/215/346/236/204/346/200/273/347/273/223.md +166 -0
  98. package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210-/345/256/211/345/205/250/345/256/241/346/237/245.md +176 -0
  99. package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210.md +908 -0
  100. package/docs//346/226/207/346/241/243/346/233/264/346/226/260/346/270/205/345/215/225.md +83 -0
  101. package/docs//346/227/245/345/277/227/344/270/216/345/274/202/345/270/270/345/244/204/347/220/206/350/247/204/350/214/203.md +829 -0
  102. package/docs//346/227/245/345/277/227/350/260/203/350/257/225/345/256/236/346/210/230/346/214/207/345/215/227.md +25 -0
  103. package/docs//346/236/266/346/236/204/345/200/237/351/211/264/346/214/207/345/215/227.md +977 -0
  104. package/docs//346/236/266/346/236/204/346/224/271/351/200/240-/345/256/214/346/210/220/346/200/273/347/273/223.md +440 -0
  105. package/docs//346/236/266/346/236/204/347/216/260/347/212/266/344/270/216/347/273/210/346/236/201/347/233/256/346/240/207/345/257/271/346/257/224/345/210/206/346/236/220.md +508 -0
  106. package/docs//346/250/241/345/235/227/345/244/232/350/277/236/346/216/245/346/216/247/345/210/266/347/255/226/347/225/245.md +220 -0
  107. package/docs//346/250/241/345/235/227/345/256/211/350/243/205/346/234/272/345/210/266/350/256/276/350/256/241.md +500 -0
  108. package/docs//346/250/241/345/235/227/345/274/200/345/217/221/346/214/207/345/215/227.md +1824 -0
  109. package/docs//346/250/241/345/235/227/347/203/255/346/233/264/346/226/260.md +89 -0
  110. package/docs//346/250/241/345/235/227/350/277/234/347/250/213/351/203/250/347/275/262/345/274/200/345/217/221/350/247/204/350/214/203.md +460 -0
  111. package/docs//346/250/241/345/235/227/351/200/200/345/207/272/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +303 -0
  112. package/docs//346/250/241/345/235/227/351/205/215/347/275/256/345/212/240/350/275/275/344/270/216/347/203/255/351/207/215/350/275/275/350/247/204/350/214/203.md +369 -0
  113. package/docs//346/265/213/350/257/225/344/270/255/345/277/203/346/267/273/345/212/240/346/250/241/345/235/227/346/265/213/350/257/225/346/214/207/345/215/227.md +147 -0
  114. package/docs//347/211/210/346/234/254/351/224/201/345/256/232/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +331 -0
  115. package/docs//347/216/257/345/242/203/345/217/230/351/207/217/344/270/216/350/277/220/350/241/214/346/227/266/347/233/256/345/275/225/350/256/276/350/256/241.md +499 -0
  116. package/docs//347/216/257/345/242/203/347/256/241/347/220/206/345/256/214/346/225/264/346/226/271/346/241/210.md +334 -0
  117. package/docs//350/231/232/346/213/237/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/345/256/214/346/225/264/350/256/276/350/256/241.md +1496 -0
  118. package/docs//350/231/232/346/213/237/347/216/257/345/242/203/345/267/245/344/275/234/345/216/237/347/220/206.md +163 -0
  119. package/docs//350/256/241/345/210/222/347/256/241/347/220/206/345/231/250/344/275/277/347/224/250/346/214/207/345/215/227.md +196 -0
  120. package/docs//350/256/244/350/257/201/346/250/241/345/235/227/344/270/216Gateway/350/256/276/350/256/241/346/226/271/346/241/210.md +765 -0
  121. package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241-/346/227/247/347/211/210.md +1117 -0
  122. package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241.md +451 -0
  123. package/docs//351/207/215/346/236/204/346/234/272/345/210/266/346/270/205/345/215/225.md +192 -0
  124. package/docs//351/223/276/350/267/257/350/277/275/350/270/252/346/226/271/346/241/210.md +242 -0
  125. package/docs//351/231/215/347/272/247/347/255/226/347/225/245/350/256/276/350/256/241/346/226/271/346/241/210.md +618 -0
  126. package/extensions/agents/assistant/entry.py +113 -14
  127. package/extensions/agents/assistant/module.md +27 -22
  128. package/extensions/agents/assistant/server.py +291 -105
  129. package/extensions/channels/acp_channel/entry.py +114 -16
  130. package/extensions/channels/acp_channel/module.md +4 -0
  131. package/extensions/channels/acp_channel/server.py +396 -105
  132. package/extensions/channels/phone_channel/__init__.py +1 -0
  133. package/extensions/channels/phone_channel/entry.py +503 -0
  134. package/extensions/channels/phone_channel/module.md +31 -0
  135. package/extensions/channels/phone_channel/server.py +686 -0
  136. package/extensions/event_hub_bench/entry.py +55 -12
  137. package/extensions/event_hub_bench/module.md +27 -27
  138. package/extensions/services/audit/README.md +134 -0
  139. package/extensions/services/audit/collector.py +73 -0
  140. package/extensions/services/audit/entry.py +444 -0
  141. package/extensions/services/audit/module.md +66 -0
  142. package/extensions/services/audit/query_audit.py +111 -0
  143. package/extensions/services/audit/routes/__init__.py +1 -0
  144. package/extensions/services/audit/routes/routes_audit.py +113 -0
  145. package/extensions/services/audit/schemas/__init__.py +5 -0
  146. package/extensions/services/audit/schemas/audit_event.py +92 -0
  147. package/extensions/services/audit/server.py +542 -0
  148. package/extensions/services/audit/storage.py +95 -0
  149. package/extensions/services/auth/entry.py +1054 -0
  150. package/extensions/services/auth/module.md +31 -0
  151. package/extensions/services/auth/token_store.py +185 -0
  152. package/extensions/services/auth/verifiers/evol_account.py +101 -0
  153. package/extensions/services/auth/verifiers/kite_token.py +38 -0
  154. package/extensions/services/auth/verifiers/pairing_code.py +71 -0
  155. package/extensions/services/backup/entry.py +494 -197
  156. package/extensions/services/backup/module.md +4 -2
  157. package/extensions/services/dataclaw/api/__init__.py +0 -0
  158. package/extensions/services/dataclaw/api/admin.py +367 -0
  159. package/extensions/services/dataclaw/api/copyright.py +175 -0
  160. package/extensions/services/dataclaw/api/credits.py +177 -0
  161. package/extensions/services/dataclaw/api/data.py +179 -0
  162. package/extensions/services/dataclaw/api/demands.py +269 -0
  163. package/extensions/services/dataclaw/api/feeds.py +262 -0
  164. package/extensions/services/dataclaw/api/identity.py +505 -0
  165. package/extensions/services/dataclaw/api/notifications.py +104 -0
  166. package/extensions/services/dataclaw/api/reviews.py +138 -0
  167. package/extensions/services/dataclaw/api/search.py +153 -0
  168. package/extensions/services/dataclaw/api/subscriptions.py +157 -0
  169. package/extensions/services/dataclaw/config.json5 +96 -0
  170. package/extensions/services/dataclaw/core/__init__.py +0 -0
  171. package/extensions/services/dataclaw/core/auth.py +95 -0
  172. package/extensions/services/dataclaw/core/config.py +50 -0
  173. package/extensions/services/dataclaw/core/database.py +70 -0
  174. package/extensions/services/dataclaw/entry.py +416 -0
  175. package/extensions/services/dataclaw/gofeed/351/241/271/347/233/256/346/211/200/346/234/211/346/235/203/350/275/254/347/247/273/346/265/201/347/250/213/350/257/264/346/230/216.md +309 -0
  176. package/extensions/services/dataclaw/migrate.py +283 -0
  177. package/extensions/services/dataclaw/models/__init__.py +0 -0
  178. package/extensions/services/dataclaw/module.md +49 -0
  179. package/extensions/services/dataclaw/requirements.txt +18 -0
  180. package/extensions/services/dataclaw/server.py +759 -0
  181. package/extensions/services/dataclaw/services/__init__.py +0 -0
  182. package/extensions/services/dataclaw/services/agent_service.py +132 -0
  183. package/extensions/services/dataclaw/services/credit_service.py +235 -0
  184. package/extensions/services/dataclaw/services/email_service.py +140 -0
  185. package/extensions/services/dataclaw/services/feed_service.py +259 -0
  186. package/extensions/services/dataclaw/services/notification_service.py +209 -0
  187. package/extensions/services/dataclaw/services/oauth_service.py +275 -0
  188. package/extensions/services/dataclaw/services/pricing.py +102 -0
  189. package/extensions/services/dataclaw/services/quality.py +79 -0
  190. package/extensions/services/dataclaw/services/reputation.py +142 -0
  191. package/extensions/services/dataclaw/services/sms_service.py +174 -0
  192. package/extensions/services/dataclaw/static/css/common.css +853 -0
  193. package/extensions/services/dataclaw/static/css/themes/blue.css +42 -0
  194. package/extensions/services/dataclaw/static/css/themes/dark.css +42 -0
  195. package/extensions/services/dataclaw/static/css/themes/light.css +35 -0
  196. package/extensions/services/dataclaw/static/js/api.js +103 -0
  197. package/extensions/services/dataclaw/static/js/common.js +321 -0
  198. package/extensions/services/dataclaw/static/js/i18n.js +95 -0
  199. package/extensions/services/dataclaw/static/js/pages/admin.js +152 -0
  200. package/extensions/services/dataclaw/static/js/pages/dashboard.js +82 -0
  201. package/extensions/services/dataclaw/static/js/pages/feed-detail.js +180 -0
  202. package/extensions/services/dataclaw/static/js/pages/feed-manage.js +158 -0
  203. package/extensions/services/dataclaw/static/js/theme.js +46 -0
  204. package/extensions/services/dataclaw/static/locales/en-US.json +464 -0
  205. package/extensions/services/dataclaw/static/locales/ja-JP.json +464 -0
  206. package/extensions/services/dataclaw/static/locales/zh-CN.json +464 -0
  207. package/extensions/services/dataclaw/templates/admin/index.html +90 -0
  208. package/extensions/services/dataclaw/templates/base.html +136 -0
  209. package/extensions/services/dataclaw/templates/credits/balance.html +106 -0
  210. package/extensions/services/dataclaw/templates/credits/deposit.html +164 -0
  211. package/extensions/services/dataclaw/templates/credits/history.html +90 -0
  212. package/extensions/services/dataclaw/templates/dashboard.html +52 -0
  213. package/extensions/services/dataclaw/templates/demands/create.html +78 -0
  214. package/extensions/services/dataclaw/templates/demands/detail.html +136 -0
  215. package/extensions/services/dataclaw/templates/demands/list.html +94 -0
  216. package/extensions/services/dataclaw/templates/feeds/create.html +95 -0
  217. package/extensions/services/dataclaw/templates/feeds/detail.html +110 -0
  218. package/extensions/services/dataclaw/templates/feeds/list.html +110 -0
  219. package/extensions/services/dataclaw/templates/feeds/manage.html +88 -0
  220. package/extensions/services/dataclaw/templates/index.html +185 -0
  221. package/extensions/services/dataclaw/templates/login.html +246 -0
  222. package/extensions/services/dataclaw/templates/register.html +164 -0
  223. package/extensions/services/dataclaw/templates/settings/notifications.html +96 -0
  224. package/extensions/services/dataclaw/templates/settings/profile.html +167 -0
  225. package/extensions/services/dataclaw/templates/subscriptions/list.html +64 -0
  226. package/extensions/services/dataclaw/tests/__init__.py +0 -0
  227. package/extensions/services/dataclaw/tests/conftest.py +68 -0
  228. package/extensions/services/dataclaw/tests/integration/__init__.py +0 -0
  229. package/extensions/services/dataclaw/tests/integration/test_workflows.py +239 -0
  230. package/extensions/services/dataclaw/tests/unit/__init__.py +0 -0
  231. package/extensions/services/dataclaw/tests/unit/test_admin.py +70 -0
  232. package/extensions/services/dataclaw/tests/unit/test_copyright.py +63 -0
  233. package/extensions/services/dataclaw/tests/unit/test_credits.py +80 -0
  234. package/extensions/services/dataclaw/tests/unit/test_data.py +98 -0
  235. package/extensions/services/dataclaw/tests/unit/test_demands.py +106 -0
  236. package/extensions/services/dataclaw/tests/unit/test_feeds.py +98 -0
  237. package/extensions/services/dataclaw/tests/unit/test_identity.py +88 -0
  238. package/extensions/services/dataclaw/tests/unit/test_notifications.py +36 -0
  239. package/extensions/services/dataclaw/tests/unit/test_reviews.py +68 -0
  240. package/extensions/services/dataclaw/tests/unit/test_search.py +64 -0
  241. package/extensions/services/dataclaw/tests/unit/test_subscriptions.py +65 -0
  242. package/extensions/services/dataclaw/tests/unit/test_system.py +106 -0
  243. package/extensions/services/dataclaw/utils/__init__.py +0 -0
  244. package/extensions/services/dataclaw/utils/crypto.py +38 -0
  245. package/extensions/services/dataclaw/utils/id_generator.py +52 -0
  246. package/extensions/services/dataclaw/ws/__init__.py +0 -0
  247. package/extensions/services/dataclaw/ws/handler.py +163 -0
  248. package/extensions/services/dataclaw//345/215/217/350/256/2561-/351/241/271/347/233/256/346/235/241/344/273/266/346/216/210/346/235/203/344/270/216/350/202/241/346/235/203/345/257/271/344/273/267/345/215/217/350/256/256.md +243 -0
  249. package/extensions/services/dataclaw//345/215/217/350/256/2562-/351/241/271/347/233/256/350/264/255/344/271/260/346/235/203/344/270/216/345/244/226/345/214/205/345/247/224/346/211/230/345/274/200/345/217/221/345/215/217/350/256/256.md +434 -0
  250. package/extensions/services/evol/__init__.py +1 -0
  251. package/extensions/services/evol/async_http.py +551 -0
  252. package/extensions/services/evol/auth_manager.py +602 -443
  253. package/extensions/services/evol/config.json5 +16 -0
  254. package/extensions/services/evol/entry.py +568 -406
  255. package/extensions/services/evol/evol_api.py +969 -173
  256. package/extensions/services/evol/mfa_totp.py +77 -0
  257. package/extensions/services/evol/module.md +150 -32
  258. package/extensions/services/evol/nonce_pool.py +113 -0
  259. package/extensions/services/evol/oauth_manager.py +223 -0
  260. package/extensions/services/evol/pairing.py +3 -2
  261. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  262. package/extensions/services/evol/relay.py +1031 -682
  263. package/extensions/services/evol/relay_config.json5 +85 -67
  264. package/extensions/services/evol/routes/routes_llm.py +231 -0
  265. package/extensions/services/evol/routes/routes_rpc.py +90 -89
  266. package/extensions/services/evol/routes/routes_test.py +11 -4
  267. package/extensions/services/evol/server.py +2426 -875
  268. package/extensions/services/evol/static/assets/CommissionView-Cs_ys6Gm.js +1 -0
  269. package/extensions/services/evol/static/assets/CommissionView-DACet_Oo.css +1 -0
  270. package/extensions/services/evol/static/assets/IframePage-DbO11U9G.js +1 -0
  271. package/extensions/services/evol/static/assets/IframePage-c572lT8i.css +1 -0
  272. package/extensions/services/evol/static/assets/TeamDetailView-DULrGD7k.css +1 -0
  273. package/extensions/services/evol/static/assets/TeamDetailView-gy_MBEqG.js +139 -0
  274. package/extensions/services/evol/static/assets/element-plus-Bd7pZkkM.js +63 -0
  275. package/extensions/services/evol/static/assets/index-CmMONKzG.css +1 -0
  276. package/extensions/services/evol/static/assets/index-D44bBe__.js +2 -0
  277. package/extensions/services/evol/static/assets/vue-vendor-DtF-__I4.js +29 -0
  278. package/extensions/services/evol/static/index.html +16 -781
  279. package/extensions/services/evol/static/logo.png +0 -0
  280. package/extensions/services/evol/stats_manager.py +243 -240
  281. package/extensions/services/evol/web/README.md +89 -0
  282. package/extensions/services/evol/web/build.bat +44 -0
  283. package/extensions/services/evol/web/index.html +13 -0
  284. package/extensions/services/evol/web/package-lock.json +1718 -0
  285. package/extensions/services/evol/web/package.json +26 -0
  286. package/extensions/services/evol/web/public/logo.png +0 -0
  287. package/extensions/services/evol/web/src/App.vue +7 -0
  288. package/extensions/services/evol/web/src/components/layout/AppHeader.vue +202 -0
  289. package/extensions/services/evol/web/src/components/layout/AppLayout.vue +61 -0
  290. package/extensions/services/evol/web/src/components/layout/AppSidebar.vue +115 -0
  291. package/extensions/services/evol/web/src/components/login/LoginPage.vue +271 -0
  292. package/extensions/services/evol/web/src/components/team/AddMemberModal.vue +181 -0
  293. package/extensions/services/evol/web/src/components/team/GroupTreeNode.vue +156 -0
  294. package/extensions/services/evol/web/src/components/team/TeamAlertConfig.vue +221 -0
  295. package/extensions/services/evol/web/src/components/team/TeamBillModal.vue +165 -0
  296. package/extensions/services/evol/web/src/components/team/TeamMembersAndGroups.vue +499 -0
  297. package/extensions/services/evol/web/src/components/team/TeamStatsPanel.vue +907 -0
  298. package/extensions/services/evol/web/src/components/team/TreeNode.vue +331 -0
  299. package/extensions/services/evol/web/src/components/team/stats/StatsExportProgress.vue +44 -0
  300. package/extensions/services/evol/web/src/components/team/stats/StatsHeader.vue +89 -0
  301. package/extensions/services/evol/web/src/components/team/stats/StatsMemberDetail.vue +415 -0
  302. package/extensions/services/evol/web/src/components/team/stats/StatsSummary.vue +42 -0
  303. package/extensions/services/evol/web/src/components/team/stats/helpers.ts +195 -0
  304. package/extensions/services/evol/web/src/components/team/stats/stats.css +741 -0
  305. package/extensions/services/evol/web/src/components/team/stats/useStatsApi.ts +114 -0
  306. package/extensions/services/evol/web/src/components/team/stats/useStatsCharts.ts +242 -0
  307. package/extensions/services/evol/web/src/components/team/stats/useStatsExport.ts +232 -0
  308. package/extensions/services/evol/web/src/composables/useFormatters.ts +42 -0
  309. package/extensions/services/evol/web/src/composables/useTheme.ts +52 -0
  310. package/extensions/services/evol/web/src/env.d.ts +7 -0
  311. package/extensions/services/evol/web/src/i18n/en.ts +361 -0
  312. package/extensions/services/evol/web/src/i18n/index.ts +36 -0
  313. package/extensions/services/evol/web/src/i18n/zh.ts +379 -0
  314. package/extensions/services/evol/web/src/main.ts +21 -0
  315. package/extensions/services/evol/web/src/router/index.ts +81 -0
  316. package/extensions/services/evol/web/src/services/kernel-client.ts +406 -0
  317. package/extensions/services/evol/web/src/stores/auth.ts +189 -0
  318. package/extensions/services/evol/web/src/stores/connection.ts +134 -0
  319. package/extensions/services/evol/web/src/stores/pages.ts +79 -0
  320. package/extensions/services/evol/web/src/styles/base.css +213 -0
  321. package/extensions/services/evol/web/src/styles/variables.css +138 -0
  322. package/extensions/services/evol/web/src/types/rpc.ts +35 -0
  323. package/extensions/services/evol/web/src/types/token.ts +87 -0
  324. package/extensions/services/evol/web/src/views/AccountView.vue +1532 -0
  325. package/extensions/services/evol/web/src/views/AiServiceView.vue +219 -0
  326. package/extensions/services/evol/web/src/views/CommissionView.vue +1220 -0
  327. package/extensions/services/evol/web/src/views/CreditsView.vue +131 -0
  328. package/extensions/services/evol/web/src/views/EndpointView.vue +163 -0
  329. package/extensions/services/evol/web/src/views/IframePage.vue +120 -0
  330. package/extensions/services/evol/web/src/views/TeamDetailView.vue +473 -0
  331. package/extensions/services/evol/web/src/views/TeamView.vue +332 -0
  332. package/extensions/services/evol/web/tsconfig.json +31 -0
  333. package/extensions/services/evol/web/tsconfig.node.json +10 -0
  334. package/extensions/services/evol/web/vite.config.ts +49 -0
  335. package/extensions/services/evolmem/__init__.py +0 -0
  336. package/extensions/services/evolmem/entry.py +387 -0
  337. package/extensions/services/evolmem/hooks/__init__.py +0 -0
  338. package/extensions/services/evolmem/hooks/assistant_stop.py +228 -0
  339. package/extensions/services/evolmem/hooks/common.py +76 -0
  340. package/extensions/services/evolmem/hooks/pre_tool_use.py +56 -0
  341. package/extensions/services/evolmem/hooks/session_end.py +133 -0
  342. package/extensions/services/evolmem/hooks/session_start.py +229 -0
  343. package/extensions/services/evolmem/hooks/user_prompt.py +122 -0
  344. package/extensions/services/evolmem/module.md +48 -0
  345. package/extensions/services/evolmem/prompts/00-server-info.md +28 -0
  346. package/extensions/services/evolmem/prompts/01-behavior.md +46 -0
  347. package/extensions/services/evolmem/prompts/02-summary-format.md +112 -0
  348. package/extensions/services/evolmem/prompts/03-file-query.md +92 -0
  349. package/extensions/services/evolmem/prompts/04-topic-stats.md +11 -0
  350. package/extensions/services/evolmem/prompts/05-recent-topics.md +84 -0
  351. package/extensions/services/evolmem/scripts/__init__.py +0 -0
  352. package/extensions/services/evolmem/scripts/extract_keywords.py +40 -0
  353. package/extensions/services/evolmem/scripts/search_topics.py +91 -0
  354. package/extensions/services/evolmem/server.py +641 -0
  355. package/extensions/services/gateway/entry.py +964 -0
  356. package/extensions/services/gateway/module.md +29 -0
  357. package/extensions/services/gateway/nonce_pool.py +65 -0
  358. package/extensions/services/gateway/relay.py +133 -0
  359. package/extensions/services/gateway/ws_server.py +285 -0
  360. package/extensions/services/kite_console/auth_manager.py +603 -0
  361. package/extensions/services/kite_console/config.json5 +19 -0
  362. package/extensions/services/kite_console/config_loader.py +117 -0
  363. package/extensions/services/kite_console/entry.py +528 -0
  364. package/extensions/services/kite_console/evol_api.py +179 -0
  365. package/extensions/services/kite_console/evol_config.json5 +29 -0
  366. package/extensions/services/kite_console/mfa_totp.py +77 -0
  367. package/extensions/services/kite_console/migrate_tokens.py +122 -0
  368. package/extensions/services/kite_console/module.md +37 -0
  369. package/extensions/services/kite_console/nonce_pool.py +113 -0
  370. package/extensions/services/kite_console/oauth_manager.py +223 -0
  371. package/extensions/services/kite_console/pairing.py +280 -0
  372. package/extensions/services/kite_console/pairing_codes.jsonl +2 -0
  373. package/extensions/services/kite_console/relay.py +1350 -0
  374. package/extensions/services/kite_console/relay_config.json5 +96 -0
  375. package/extensions/services/kite_console/routes/__init__.py +1 -0
  376. package/extensions/services/kite_console/routes/routes_llm.py +231 -0
  377. package/extensions/services/kite_console/routes/routes_proxy.py +115 -0
  378. package/extensions/services/kite_console/routes/routes_rpc.py +89 -0
  379. package/extensions/services/kite_console/routes/routes_test.py +68 -0
  380. package/extensions/services/kite_console/server.py +1742 -0
  381. package/extensions/services/{evol → kite_console}/static/css/style.css +656 -2
  382. package/extensions/services/kite_console/static/index.html +1524 -0
  383. package/extensions/services/{evol → kite_console}/static/js/dialog.js +11 -4
  384. package/extensions/services/kite_console/static/js/evol-app.js +7740 -0
  385. package/extensions/services/{evol/static/js/evol-app.js → kite_console/static/js/evol-app.js.backup} +2777 -1949
  386. package/extensions/services/kite_console/static/js/kernel-client.js +560 -0
  387. package/extensions/services/{evol/static/js/kernel-client.js → kite_console/static/js/kernel-client.js.backup} +41 -3
  388. package/extensions/services/{evol → kite_console}/static/js/registry-tests.js +7 -0
  389. package/extensions/services/kite_console/static/js/tests/ARCHITECTURE.md +67 -0
  390. package/extensions/services/kite_console/static/js/tests/README.md +140 -0
  391. package/extensions/services/kite_console/static/js/tests/index.js +161 -0
  392. package/extensions/services/kite_console/static/js/tests/integration/auth.js +120 -0
  393. package/extensions/services/kite_console/static/js/tests/integration/channel-interaction.js +188 -0
  394. package/extensions/services/kite_console/static/js/tests/integration/elastic-connection.js +115 -0
  395. package/extensions/services/kite_console/static/js/tests/integration/full-workflow.js +43 -0
  396. package/extensions/services/kite_console/static/js/tests/integration/multi-instance.js +304 -0
  397. package/extensions/services/kite_console/static/js/tests/integration/nested-rpc.js +266 -0
  398. package/extensions/services/kite_console/static/js/tests/integration/pingpong.js +25 -0
  399. package/extensions/services/kite_console/static/js/tests/integration/redis.js +227 -0
  400. package/extensions/services/kite_console/static/js/tests/integration/registry-core.js +52 -0
  401. package/extensions/services/kite_console/static/js/tests/integration/remote-deploy.js +85 -0
  402. package/extensions/services/kite_console/static/js/tests/integration/require-init.js +96 -0
  403. package/extensions/services/kite_console/static/js/tests/integration/scaling-control.js +193 -0
  404. package/extensions/services/kite_console/static/js/tests/integration/trace.js +109 -0
  405. package/extensions/services/kite_console/static/js/tests/modules/acp_channel.js +339 -0
  406. package/extensions/services/kite_console/static/js/tests/modules/auth.js +96 -0
  407. package/extensions/services/kite_console/static/js/tests/modules/backup.js +49 -0
  408. package/extensions/services/kite_console/static/js/tests/modules/gateway.js +41 -0
  409. package/extensions/services/kite_console/static/js/tests/modules/kernel.js +90 -0
  410. package/extensions/services/kite_console/static/js/tests/modules/launcher.js +75 -0
  411. package/extensions/services/kite_console/static/js/tests/modules/multi_instance.js +129 -0
  412. package/extensions/services/kite_console/static/js/tests/modules/phone_channel.js +364 -0
  413. package/extensions/services/kite_console/static/js/tests/modules/redis.js +178 -0
  414. package/extensions/services/kite_console/static/js/tests/modules/watchdog.js +60 -0
  415. package/extensions/services/kite_console/static/js/tests/modules/web.js +70 -0
  416. package/extensions/services/kite_console/static/js/tests/test-runner.js +123 -0
  417. package/extensions/services/kite_console/static/js/virtual-list.js +200 -0
  418. package/extensions/services/kite_console/static/test_kernel_client_token.html +352 -0
  419. package/extensions/services/kite_console/stats_manager.py +247 -0
  420. package/extensions/services/logs/README.md +215 -0
  421. package/extensions/services/logs/api_logger.py +37 -0
  422. package/extensions/services/logs/baseline.py +121 -0
  423. package/extensions/services/logs/cleaner.py +76 -0
  424. package/extensions/services/logs/entry.py +449 -0
  425. package/extensions/services/logs/formatter.py +129 -0
  426. package/extensions/services/logs/module.md +38 -0
  427. package/extensions/services/logs/quick_diagnostic.py +128 -0
  428. package/extensions/services/logs/routes/__init__.py +1 -0
  429. package/extensions/services/logs/routes/routes_logs.py +218 -0
  430. package/extensions/services/logs/routes/routes_logs.py.backup +173 -0
  431. package/extensions/services/logs/scanner.py +100 -0
  432. package/extensions/services/logs/searcher.py +263 -0
  433. package/extensions/services/logs/server.py +553 -0
  434. package/extensions/services/logs.zip +0 -0
  435. package/extensions/services/model_service/config.json5 +30 -0
  436. package/extensions/services/model_service/entry.py +620 -171
  437. package/extensions/services/model_service/module.md +11 -2
  438. package/extensions/services/proxy/__init__.py +0 -0
  439. package/extensions/services/proxy/aid_manager.py +419 -0
  440. package/extensions/services/proxy/auth_bridge.py +182 -0
  441. package/extensions/services/proxy/config_store.py +79 -0
  442. package/extensions/services/proxy/entry.py +528 -0
  443. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +2 -2
  444. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +18 -28
  445. package/extensions/services/proxy/evol/presenter/configPresenter.py +80 -1127
  446. package/extensions/services/proxy/evol/presenter/userPresenter.py +71 -477
  447. package/extensions/services/proxy/evol/server/claude_proxy_async.py +11 -7
  448. package/extensions/services/proxy/module.md +151 -0
  449. package/extensions/services/proxy/server.py +952 -271
  450. package/extensions/services/redis/ALIGNMENT_CHECKLIST.md +121 -0
  451. package/extensions/services/redis/ALIGNMENT_STATUS.md +548 -0
  452. package/extensions/services/redis/config.json5 +8 -0
  453. package/extensions/services/redis/entry.py +1509 -0
  454. package/extensions/services/redis/entry.py.backup +405 -0
  455. package/extensions/services/redis/module.md +48 -0
  456. package/extensions/services/redis/redis_builtin.py +332 -0
  457. package/extensions/services/redis/redis_external.py +164 -0
  458. package/extensions/services/testUi/entry.py +446 -0
  459. package/extensions/services/testUi/module.md +18 -0
  460. package/extensions/services/testUi/ui/cards.html +131 -0
  461. package/extensions/services/testUi/ui/index.html +22 -0
  462. package/extensions/services/testUi/ui/particles.html +143 -0
  463. package/extensions/services/watchdog/entry.py +1258 -793
  464. package/extensions/services/watchdog/module.md +2 -0
  465. package/extensions/services/watchdog/monitor.py +465 -87
  466. package/extensions/services/web/auth_manager.py +602 -0
  467. package/extensions/services/web/config.json5 +11 -0
  468. package/extensions/services/web/entry.py +598 -478
  469. package/extensions/services/web/mfa_totp.py +77 -0
  470. package/extensions/services/web/module.md +16 -13
  471. package/extensions/services/web/nonce_pool.py +113 -0
  472. package/extensions/services/web/oauth_manager.py +223 -0
  473. package/extensions/services/web/pairing.py +3 -2
  474. package/extensions/services/web/pairing_codes.jsonl +1 -0
  475. package/extensions/services/web/relay.py +442 -63
  476. package/extensions/services/web/relay_config.json5 +1 -2
  477. package/extensions/services/web/routes/routes_rpc.py +6 -6
  478. package/extensions/services/web/server.py +360 -173
  479. package/extensions/services/web/static/index.html +1752 -1738
  480. package/extensions/services/web/static/js/app.js +32 -0
  481. package/extensions/services/web/static/js/kernel-client.js +48 -9
  482. package/extensions/services/web/vendor/bluetooth/audio.py +1 -1
  483. package/extensions/services/web/vendor/config.py +2 -2
  484. package/extensions/services/web/vendor/storage/identity.py +1 -1
  485. package/kernel/entry.py +77 -23
  486. package/kernel/event_hub.py +1122 -74
  487. package/kernel/module.md +2 -1
  488. package/kernel/registry_store.py +208 -11
  489. package/kernel/rpc_router.py +1400 -491
  490. package/kernel/server.py +1021 -134
  491. package/kite_cli/builders/__init__.py +4 -0
  492. package/kite_cli/builders/base.py +67 -0
  493. package/kite_cli/builders/custom.py +31 -0
  494. package/kite_cli/builders/detector.py +56 -0
  495. package/kite_cli/builders/go.py +34 -0
  496. package/kite_cli/builders/gradle.py +41 -0
  497. package/kite_cli/builders/maven.py +36 -0
  498. package/kite_cli/builders/npm.py +44 -0
  499. package/kite_cli/builders/python.py +37 -0
  500. package/kite_cli/commands/BUILD_GUIDE.md +109 -0
  501. package/kite_cli/commands/build.py +142 -0
  502. package/kite_cli/commands/check.py +60 -0
  503. package/kite_cli/commands/config.py +156 -0
  504. package/kite_cli/commands/deps.py +58 -0
  505. package/kite_cli/commands/deps_install.py +7 -7
  506. package/kite_cli/commands/disable.py +162 -0
  507. package/kite_cli/commands/enable.py +162 -0
  508. package/kite_cli/commands/export.py +96 -0
  509. package/kite_cli/commands/import_cmd.py +110 -0
  510. package/kite_cli/commands/install.py +50 -23
  511. package/kite_cli/commands/install_skill.py +107 -0
  512. package/kite_cli/commands/list.py +128 -31
  513. package/kite_cli/commands/outdated.py +202 -0
  514. package/kite_cli/commands/search.py +33 -17
  515. package/kite_cli/commands/update.py +115 -2
  516. package/kite_cli/commands/venv_setup.py +6 -6
  517. package/kite_cli/commands/why.py +48 -0
  518. package/kite_cli/core/config_manager.py +145 -0
  519. package/kite_cli/core/downloader.py +32 -2
  520. package/kite_cli/main.py +151 -5
  521. package/kite_cli/utils/colors.py +153 -0
  522. package/kite_cli/utils/dependency_graph.py +209 -0
  523. package/kite_cli/utils/process.py +55 -0
  524. package/kite_cli/utils/progress.py +207 -0
  525. package/kite_cli/utils/table.py +101 -0
  526. package/launcher/count_lines.py +192 -43
  527. package/launcher/entry.py +4543 -2802
  528. package/launcher/logging_setup.py +54 -1
  529. package/launcher/module.md +32 -6
  530. package/launcher/module_scanner.py +93 -20
  531. package/launcher/process_manager.py +355 -76
  532. package/main.py +6 -0
  533. package/package.json +4 -1
  534. package/requirements.txt +41 -38
  535. package/scripts/auto-fix-deps.py +128 -0
  536. package/scripts/env-manager.js +25 -2
  537. package/scripts/final-test.js +78 -0
  538. package/scripts/setup-python-env.js +700 -191
  539. package/scripts/test-alluser.js +48 -0
  540. package/scripts/test-different-version.js +86 -0
  541. package/scripts/test-direct.js +63 -0
  542. package/scripts/test-extract-installer.js +28 -0
  543. package/scripts/test-install-log.js +54 -0
  544. package/scripts/test-installer.js +39 -0
  545. package/scripts/test-integration.js +250 -0
  546. package/scripts/test-real-install.js +210 -0
  547. package/scripts/test-targetdir.js +49 -0
  548. package/scripts/test-venv-real.js +47 -0
  549. package/scripts/test-venv-simple.js +57 -0
  550. package/scripts/test-wait.js +49 -0
  551. package/scripts/test-with-log.js +63 -0
  552. package/extensions/services/evol/config.yaml +0 -149
  553. package/extensions/services/evol/routes/routes_management_ws.py +0 -127
  554. package/extensions/services/evol/static/index_evol.html +0 -14
  555. package/extensions/services/evol/static/js/app.js +0 -6304
  556. package/extensions/services/evol/static/js/auth.js +0 -326
  557. package/extensions/services/evol/static/js/evol-app-fixed.js +0 -50
  558. package/extensions/services/evol/static/js/evol-app.js.bak +0 -1800
  559. package/extensions/services/evol/static/js/kernel-client-example.js +0 -228
  560. package/extensions/services/evol/static/js/main.js +0 -141
  561. package/extensions/services/evol/static/js/stats.js +0 -217
  562. package/extensions/services/evol/static/js/token-manager.js +0 -175
  563. package/extensions/services/proxy/CHANGELOG_20260308.md +0 -258
  564. package/extensions/services/proxy/_fix_prints.py +0 -133
  565. package/extensions/services/proxy/_fix_prints2.py +0 -87
  566. package/extensions/services/proxy/console_auth.py +0 -109
  567. package/extensions/services/proxy/logs/websocket.log +0 -260
  568. package/extensions/services/proxy/main.py +0 -240
  569. package/extensions/services/proxy/requirements.txt +0 -13
  570. package/extensions/services/web/config.yaml +0 -149
  571. /package/extensions/services/{evol → kite_console}/static/pairing.html +0 -0
  572. /package/extensions/services/{evol → kite_console}/static/test_registry.html +0 -0
  573. /package/extensions/services/{evol → kite_console}/static/test_relay.html +0 -0
@@ -22,10 +22,39 @@ except ImportError:
22
22
 
23
23
  from starlette.websockets import WebSocket
24
24
 
25
+ from dataclasses import dataclass, field
26
+
25
27
  from .dedup import EventDedup
26
28
  from .router import match_parts
27
29
 
28
30
 
31
+ @dataclass
32
+ class OrderingGroup:
33
+ """保序集合:同组事件 key 绑定同一 slot 发送,保证顺序。"""
34
+ name: str
35
+ keys: set = field(default_factory=set)
36
+ bound_slot: int | None = None
37
+ last_enqueued_seq: int | None = None
38
+
39
+
40
+ # ── Multi-instance helpers ──
41
+
42
+ def instance_key(module_id: str, instance_num: int = 1) -> str:
43
+ """Build internal key: 'module_id' for instance 1, 'module_id#N' for N>1."""
44
+ return module_id if instance_num <= 1 else f"{module_id}#{instance_num}"
45
+
46
+
47
+ def parse_instance_key(key: str) -> tuple[str, int]:
48
+ """Parse internal key back to (module_id, instance_num)."""
49
+ if "#" in key:
50
+ name, num_str = key.rsplit("#", 1)
51
+ try:
52
+ return name, int(num_str)
53
+ except ValueError:
54
+ pass
55
+ return key, 1
56
+
57
+
29
58
  def _dumps(obj) -> str:
30
59
  if orjson is not None:
31
60
  return orjson.dumps(obj).decode()
@@ -39,21 +68,63 @@ def _loads(raw: str):
39
68
 
40
69
 
41
70
  QUEUE_MAXSIZE = 10000
71
+ QUEUE_MAX_BYTES = 10 * 1024 * 1024 # 10 MB
72
+
73
+ # Compression limits
74
+ COMPRESS_MAX_COUNT = 100 # 每次最多压缩 100 条消息
75
+ COMPRESS_MAX_BYTES = 100 * 1024 # 每次最多压缩 100KB
42
76
 
43
77
  # System events that are auto-broadcast to ALL connected modules (no subscription needed)
44
- SYSTEM_EVENTS = {"module.offline", "module.ready", "module.shutdown"}
78
+ # These events can ONLY be published via publish_internal() (Kernel-originated).
79
+ # Modules attempting to publish these via event.publish RPC will be rejected.
80
+ SYSTEM_EVENTS = {"module.offline", "module.ready", "module.shutdown",
81
+ "module.degraded", "module.recovered",
82
+ "system.connection.offer", "system.connection.release",
83
+ "system.instance.started", "system.instance.stopped",
84
+ "system.ping"}
85
+
86
+ # Queue pressure system events (always delivered, never filtered by pressure)
87
+ PRESSURE_EVENTS = {"system.queue_pressure", "system.queue_warning"}
45
88
 
46
89
  # Callback execution pool size (max concurrent callback tasks)
47
90
  CALLBACK_POOL_SIZE = 100
48
91
 
92
+ # ── Queue pressure levels & thresholds ──
93
+ # Hysteresis: upgrade uses high thresholds, downgrade uses low thresholds
94
+ PRESSURE_LEVELS = ("idle", "busy", "overload", "critical")
95
+ PRESSURE_UPGRADE = {"idle": 0.30, "busy": 0.60, "overload": 0.85} # -> next level
96
+ PRESSURE_DOWNGRADE = {"busy": 0.20, "overload": 0.45, "critical": 0.70} # -> prev level
97
+ PRESSURE_SUGGESTIONS = {
98
+ "idle": "正常发送",
99
+ "busy": "注意队列压力",
100
+ "overload": "减少事件发送",
101
+ "critical": "立即停止发送"
102
+ }
103
+
104
+ # Critical countdown durations (seconds)
105
+ CRITICAL_TIMEOUT_ELASTIC = 60 # 支持弹性管理的模块
106
+ CRITICAL_TIMEOUT_DEFAULT = 30 # 不支持弹性管理的模块
107
+ CRITICAL_WARNING_INTERVAL = 5 # 警告事件发送间隔
108
+
49
109
 
50
110
  class EventHub:
51
111
 
52
112
  def __init__(self):
53
- # Connection table: module_id -> WebSocket
54
- self.connections: dict[str, WebSocket] = {}
113
+ # Connection table: module_id -> {slot -> WebSocket}
114
+ self.connections: dict[str, dict[int, WebSocket]] = {}
55
115
  # Connection metadata: module_id -> {connected_at}
56
116
  self.connection_info: dict[str, dict] = {}
117
+
118
+ # ── 多连接:双层队列 ──
119
+ self._slot_queues: dict[str, dict[int, asyncio.Queue]] = {} # mid → {slot → queue}
120
+ self._slot_senders: dict[str, dict[int, asyncio.Task]] = {} # mid → {slot → task}
121
+
122
+ # ── 多连接:保序集合 ──
123
+ self._ordering_groups: dict[str, list[OrderingGroup]] = {} # mid → groups
124
+ self._key_to_group: dict[tuple[str, str], OrderingGroup] = {} # (mid, key) → group
125
+
126
+ # ── 多连接:轮询计数 ──
127
+ self._rr_counters: dict[str, int] = {}
57
128
  # Subscription table: module_id -> set of (pattern_str, pattern_tuple)
58
129
  self.subscriptions: dict[str, set[tuple]] = {}
59
130
  # Per-subscriber delivery queue + sender task
@@ -76,49 +147,374 @@ class EventHub:
76
147
  # Queue overflow tracking (per module)
77
148
  self._queue_overflow: dict[str, int] = {} # module_id -> overflow count
78
149
 
150
+ # Queue byte size tracking (per module)
151
+ self._queue_bytes: dict[str, int] = {} # module_id -> current bytes in queue
152
+
153
+ # Event statistics (per event type)
154
+ # event_type -> {count, last_time, last_source}
155
+ self._event_stats: dict[str, dict] = {}
156
+
157
+ # ── Queue pressure management ──
158
+ self._queue_levels: dict[str, str] = {} # module_id -> current pressure level
159
+ self._module_elastic: dict[str, bool] = {} # module_id -> supports queue_elastic
160
+ self._critical_timers: dict[str, asyncio.Task] = {} # module_id -> countdown task
161
+ self._cnt_dropped: int = 0 # total events dropped by pressure filtering
162
+
163
+ # ── Session sequence number management ──
164
+ self._session_ids: dict[str, str] = {} # module_id -> session_id (UUID)
165
+ self._session_seq: dict[str, int] = {} # module_id -> current sequence number
166
+
167
+ # ── Periodic pressure notification ──
168
+ self._pressure_reminder_tasks: dict[str, asyncio.Task] = {} # module_id -> reminder task
169
+
170
+ # ── Source-side throttle ──
171
+ self._throttle_last: dict[str, float] = {} # full_key -> last publish timestamp
172
+ self._cnt_throttled: int = 0 # total events throttled
173
+
174
+ # ── Multi-instance tracking ──
175
+ # module_id → [instance_key, ...] (ordered, primary first)
176
+ self._module_instances: dict[str, list[str]] = {}
177
+ # module_id → round-robin counter for instance selection
178
+ self._instance_rr: dict[str, int] = {}
179
+ # (module_id, event_key) → instance_key — sticky binding for ordering
180
+ self._instance_bindings: dict[tuple[str, str], str] = {}
181
+
182
+ # ── Instance pressure reports (from modules) ──
183
+ # instance_key → {level, depth, capacity, updated_at}
184
+ self._instance_pressure: dict[str, dict] = {}
185
+
186
+ # ── Draining instances ──
187
+ # 正在排干队列的实例:路由层不再向其派发新请求/事件,等队列清空后由 scaler 关闭
188
+ self._draining_instances: set[str] = set()
189
+
79
190
  # ── Connection lifecycle ──
80
191
 
81
- def add_connection(self, module_id: str, ws: WebSocket):
82
- """Add a new connection. If module_id already connected, close the old one."""
83
- old_ws = self.connections.get(module_id)
84
- if old_ws is not None:
85
- asyncio.ensure_future(self._close_old(module_id, old_ws))
192
+ def add_connection(self, module_id: str, ws: WebSocket, slot: int = 0):
193
+ """Add a new connection.
86
194
 
87
- self.connections[module_id] = ws
88
- self.connection_info[module_id] = {
89
- "connected_at": datetime.now(timezone.utc).isoformat(),
90
- }
91
- self.subscriptions.pop(module_id, None)
92
- # Create delivery queue + sender task
93
- q = asyncio.Queue(maxsize=QUEUE_MAXSIZE)
94
- self._queues[module_id] = q
95
- self._senders[module_id] = asyncio.create_task(
96
- self._sender_loop(module_id, ws, q)
195
+ module_id: For single-instance modules this is the module name.
196
+ For multi-instance, this is instance_key (e.g. 'assistant#2').
197
+ slot 0: 主连接,重建 session 和共享队列(与原逻辑一致)。
198
+ slot > 0: 附加连接,不重建 session,只创建 slot 队列和 sender。
199
+ """
200
+ # Track instance in _module_instances
201
+ base_mid, inst_num = parse_instance_key(module_id)
202
+ if base_mid not in self._module_instances:
203
+ self._module_instances[base_mid] = []
204
+ if module_id not in self._module_instances[base_mid]:
205
+ self._module_instances[base_mid].append(module_id)
206
+
207
+ if slot == 0:
208
+ # 主连接逻辑
209
+ old_slots = self.connections.get(module_id)
210
+ if old_slots and 0 in old_slots:
211
+ asyncio.ensure_future(self._close_old(module_id, old_slots[0]))
212
+
213
+ # 生成新的会话 ID
214
+ import uuid
215
+ session_id = str(uuid.uuid4())
216
+ self._session_ids[module_id] = session_id
217
+ self._session_seq[module_id] = 0
218
+
219
+ # 初始化连接表
220
+ if module_id not in self.connections:
221
+ self.connections[module_id] = {}
222
+ self.connections[module_id][0] = ws
223
+ self.connection_info[module_id] = {
224
+ "connected_at": datetime.now(timezone.utc).isoformat(),
225
+ "session_id": session_id,
226
+ }
227
+ # 仅主实例重连时清除订阅(多实例场景下不影响其他实例的订阅)
228
+ if inst_num <= 1:
229
+ self.subscriptions.pop(base_mid, None)
230
+
231
+ # 创建共享队列 + 分发器(sender_loop)
232
+ q = asyncio.Queue(maxsize=QUEUE_MAXSIZE)
233
+ self._queues[module_id] = q
234
+ old_sender = self._senders.pop(module_id, None)
235
+ if old_sender:
236
+ old_sender.cancel()
237
+ self._senders[module_id] = asyncio.create_task(
238
+ self._sender_loop(module_id, q)
239
+ )
240
+
241
+ # 初始化 slot 队列表
242
+ if module_id not in self._slot_queues:
243
+ self._slot_queues[module_id] = {}
244
+ if module_id not in self._slot_senders:
245
+ self._slot_senders[module_id] = {}
246
+
247
+ # 清理旧的 slot 0 队列和 sender(如果有的话)
248
+ old_slot_sender = self._slot_senders.get(module_id, {}).pop(0, None)
249
+ if old_slot_sender:
250
+ old_slot_sender.cancel()
251
+ self._slot_queues.get(module_id, {}).pop(0, None)
252
+
253
+ print(f"[kernel] {module_id} connected (slot 0), session={session_id[:8]}")
254
+ else:
255
+ # 附加 slot:不重建 session,不清订阅
256
+ if module_id not in self.connections:
257
+ self.connections[module_id] = {}
258
+ self.connections[module_id][slot] = ws
259
+ if module_id not in self._slot_queues:
260
+ self._slot_queues[module_id] = {}
261
+ if module_id not in self._slot_senders:
262
+ self._slot_senders[module_id] = {}
263
+ print(f"[kernel] {module_id} connected (slot {slot})")
264
+
265
+ # 为该 slot 创建独立的发送队列和 sender
266
+ slot_q = asyncio.Queue(maxsize=QUEUE_MAXSIZE)
267
+ self._slot_queues.setdefault(module_id, {})[slot] = slot_q
268
+ self._slot_senders.setdefault(module_id, {})[slot] = asyncio.create_task(
269
+ self._slot_sender(module_id, slot, ws, slot_q)
97
270
  )
98
- print(f"[kernel] {module_id} connected")
99
271
 
100
272
  async def _close_old(self, module_id: str, ws: WebSocket):
101
273
  try:
102
- await ws.close(code=4000, reason="replaced by new connection")
274
+ await ws.close(code=4010, reason="replaced by new connection")
103
275
  except Exception:
104
276
  pass
105
277
  print(f"[kernel] Closed old connection for {module_id}")
106
278
 
107
- def remove_connection(self, module_id: str):
108
- """Clean up on disconnect."""
279
+ def remove_connection(self, module_id: str, slot: int = 0):
280
+ """Clean up on disconnect.
281
+
282
+ 删除指定 slot。只有当所有 slot 都断开时才清理模块级资源。
283
+ """
284
+ # 删除该 slot 的 ws
285
+ slots = self.connections.get(module_id)
286
+ if slots:
287
+ slots.pop(slot, None)
288
+
289
+ # 删除该 slot 的队列和 sender
290
+ slot_senders = self._slot_senders.get(module_id, {})
291
+ slot_task = slot_senders.pop(slot, None)
292
+ if slot_task:
293
+ slot_task.cancel()
294
+ slot_queues = self._slot_queues.get(module_id, {})
295
+ slot_queues.pop(slot, None)
296
+
297
+ # 清理该 slot 上的保序绑定
298
+ for group in self._ordering_groups.get(module_id, []):
299
+ if group.bound_slot == slot:
300
+ group.bound_slot = None
301
+
302
+ # 检查是否所有 slot 都已断开
303
+ remaining_slots = self.connections.get(module_id, {})
304
+ if remaining_slots:
305
+ print(f"[kernel] {module_id} slot {slot} disconnected, {len(remaining_slots)} slot(s) remaining")
306
+ return
307
+
308
+ # 所有 slot 都断开 → 清理模块级资源
109
309
  self.connections.pop(module_id, None)
110
310
  self.connection_info.pop(module_id, None)
111
- self.subscriptions.pop(module_id, None)
311
+
312
+ # Remove this instance from _module_instances
313
+ base_mid = parse_instance_key(module_id)[0]
314
+ inst_list = self._module_instances.get(base_mid, [])
315
+ if module_id in inst_list:
316
+ inst_list.remove(module_id)
317
+ if not inst_list:
318
+ self._module_instances.pop(base_mid, None)
319
+ # 最后一个实例断开 → 清除订阅和所有实例绑定
320
+ self.subscriptions.pop(base_mid, None)
321
+ self._instance_bindings = {
322
+ k: v for k, v in self._instance_bindings.items()
323
+ if k[0] != base_mid
324
+ }
325
+ self._instance_rr.pop(base_mid, None)
326
+ else:
327
+ # 清理指向已断开实例的绑定(让 _pick_instance 重新绑定)
328
+ stale = [k for k, v in self._instance_bindings.items()
329
+ if k[0] == base_mid and v == module_id]
330
+ for k in stale:
331
+ del self._instance_bindings[k]
332
+
112
333
  self._queues.pop(module_id, None)
113
334
  task = self._senders.pop(module_id, None)
114
335
  if task:
115
336
  task.cancel()
116
- print(f"[kernel] {module_id} disconnected")
337
+ self._slot_queues.pop(module_id, None)
338
+ self._slot_senders.pop(module_id, None)
339
+ # Clean up pressure state
340
+ self._queue_levels.pop(module_id, None)
341
+ self._queue_overflow.pop(module_id, None)
342
+ self._queue_bytes.pop(module_id, None)
343
+ timer = self._critical_timers.pop(module_id, None)
344
+ if timer:
345
+ timer.cancel()
346
+ # Clean up pressure reminder task
347
+ reminder_task = self._pressure_reminder_tasks.pop(module_id, None)
348
+ if reminder_task:
349
+ reminder_task.cancel()
350
+ # Clean up session state
351
+ self._session_ids.pop(module_id, None)
352
+ self._session_seq.pop(module_id, None)
353
+ # Clean up ordering groups
354
+ groups = self._ordering_groups.pop(module_id, [])
355
+ for g in groups:
356
+ for key in g.keys:
357
+ self._key_to_group.pop((module_id, key), None)
358
+ self._rr_counters.pop(module_id, None)
359
+ # Clean up instance pressure data
360
+ self._instance_pressure.pop(module_id, None)
361
+ print(f"[kernel] {module_id} disconnected (all slots)")
362
+
363
+ def get_instance_keys(self, module_id: str) -> list[str]:
364
+ """Return active instance_keys for a module_id. Single-instance returns [module_id]."""
365
+ return list(self._module_instances.get(module_id, []))
366
+
367
+ def get_active_instance_keys(self, module_id: str) -> list[str]:
368
+ """Return non-draining instance_keys. Used by scaler to count live targets."""
369
+ return [k for k in self._module_instances.get(module_id, [])
370
+ if k not in self._draining_instances]
371
+
372
+ def set_draining(self, inst_key: str):
373
+ """标记实例为 draining:路由层不再派发新请求/事件,等队列清空后由 scaler 关闭。"""
374
+ self._draining_instances.add(inst_key)
375
+
376
+ def clear_draining(self, inst_key: str):
377
+ """清除 draining 标记(实例已停止)。"""
378
+ self._draining_instances.discard(inst_key)
379
+
380
+ def is_draining(self, inst_key: str) -> bool:
381
+ return inst_key in self._draining_instances
382
+
383
+ def has_other_instances(self, module_id: str, exclude_key: str) -> bool:
384
+ """Check if module has other active instances besides exclude_key."""
385
+ inst_list = self._module_instances.get(module_id, [])
386
+ return any(k != exclude_key for k in inst_list)
387
+
388
+ def _pick_instance(self, module_id: str, event_key: str) -> str | None:
389
+ """Pick one instance_key for event delivery.
390
+
391
+ Strategy:
392
+ - Single instance: fast path, return it
393
+ - Ordering key has sticky binding: return bound instance (migrate if dead)
394
+ - Otherwise: round-robin
395
+ """
396
+ inst_keys = self._module_instances.get(module_id, [])
397
+ if not inst_keys:
398
+ return None
399
+
400
+ # 过滤掉正在 draining 的实例;若全部 draining 则 fallback 到 primary
401
+ active = [k for k in inst_keys if k not in self._draining_instances]
402
+ if not active:
403
+ active = inst_keys[:1] # fallback: primary 兜底
404
+
405
+ if len(active) == 1:
406
+ return active[0]
407
+
408
+ # Check sticky binding for ordering keys
409
+ if event_key:
410
+ binding_key = (module_id, event_key)
411
+ bound = self._instance_bindings.get(binding_key)
412
+ if bound and bound in active:
413
+ return bound
414
+ # Bind to round-robin choice (or rebind if instance died/draining)
415
+ picked = self._instance_round_robin(module_id, active)
416
+ self._instance_bindings[binding_key] = picked
417
+ return picked
418
+
419
+ return self._instance_round_robin(module_id, active)
420
+
421
+ def _instance_round_robin(self, module_id: str, inst_keys: list[str]) -> str:
422
+ """Round-robin across instances."""
423
+ idx = self._instance_rr.get(module_id, 0) % len(inst_keys)
424
+ self._instance_rr[module_id] = idx + 1
425
+ return inst_keys[idx]
426
+
427
+ # ── Instance pressure reports ──
428
+
429
+ def report_instance_pressure(self, instance_key: str, data: dict):
430
+ """Store pressure report from a module instance.
431
+
432
+ Args:
433
+ instance_key: e.g. 'assistant' or 'assistant#2'
434
+ data: {level: str, depth: int, capacity: int}
435
+ """
436
+ self._instance_pressure[instance_key] = {
437
+ "level": data.get("level", "idle"),
438
+ "depth": data.get("depth", 0),
439
+ "capacity": data.get("capacity", 0),
440
+ "updated_at": time.time(),
441
+ }
442
+
443
+ def get_instance_pressure(self, instance_key: str) -> dict | None:
444
+ """Get last reported pressure for an instance."""
445
+ return self._instance_pressure.get(instance_key)
446
+
447
+ def get_module_pressure(self, module_id: str) -> dict:
448
+ """Get pressure summary for all instances of a module.
449
+
450
+ Returns: {instance_key: {level, depth, capacity, updated_at}, ...}
451
+ """
452
+ result = {}
453
+ for inst_key in self._module_instances.get(module_id, []):
454
+ p = self._instance_pressure.get(inst_key)
455
+ if p:
456
+ result[inst_key] = p
457
+ return result
117
458
 
118
459
  # ── Sender loop (per-subscriber) ──
119
460
 
120
- async def _sender_loop(self, mid: str, ws: WebSocket, queue: asyncio.Queue):
121
- """Drain queue and send to subscriber. Exits on send failure."""
461
+ async def _sender_loop(self, mid: str, queue: asyncio.Queue):
462
+ """分发器:从共享队列取消息 添加 seq slot 放入 slot 队列。
463
+
464
+ 弹性模块在 critical 级别时执行压缩后再分发。
465
+ """
466
+ try:
467
+ is_elastic = self._module_elastic.get(mid, False)
468
+ while True:
469
+ # 弹性模块压缩逻辑
470
+ if is_elastic:
471
+ level = self._queue_levels.get(mid, "idle")
472
+ should_compress = level == "critical"
473
+ else:
474
+ should_compress = False
475
+
476
+ if should_compress and queue.qsize() > 1:
477
+ compressed_msg = await self._compress_and_send(mid, queue)
478
+ if compressed_msg:
479
+ # 压缩摘要也需要分发到某个 slot
480
+ slot = self._pick_slot(mid, "system.events_compressed")
481
+ slot_q = self._slot_queues.get(mid, {}).get(slot)
482
+ if slot_q:
483
+ try:
484
+ slot_q.put_nowait(compressed_msg)
485
+ except asyncio.QueueFull:
486
+ pass
487
+ continue
488
+
489
+ raw = await queue.get()
490
+
491
+ # 减少字节计数
492
+ msg_size = len(raw)
493
+ current_bytes = self._queue_bytes.get(mid, 0)
494
+ self._queue_bytes[mid] = max(0, current_bytes - msg_size)
495
+
496
+ # 添加序列号
497
+ raw = self._add_sequence_number(mid, raw)
498
+
499
+ # 提取事件 key 用于保序
500
+ event_key = self._extract_event_key(raw)
501
+
502
+ # 选 slot 并放入 slot 队列
503
+ slot = self._pick_slot(mid, event_key)
504
+ slot_q = self._slot_queues.get(mid, {}).get(slot)
505
+ if slot_q:
506
+ try:
507
+ slot_q.put_nowait(raw)
508
+ except asyncio.QueueFull:
509
+ self._cnt_dropped += 1
510
+ else:
511
+ # 没有可用 slot,消息丢弃
512
+ self._cnt_dropped += 1
513
+ except asyncio.CancelledError:
514
+ pass
515
+
516
+ async def _slot_sender(self, mid: str, slot: int, ws: WebSocket, queue: asyncio.Queue):
517
+ """每个 slot 独立发送协程:从 slot 队列取消息 → 发送到 WebSocket。"""
122
518
  try:
123
519
  while True:
124
520
  raw = await queue.get()
@@ -126,14 +522,230 @@ class EventHub:
126
522
  if len(raw) > 65536:
127
523
  await asyncio.wait_for(ws.send_text(raw), timeout=5 + len(raw) / 102400)
128
524
  else:
129
- await ws.send_text(raw)
525
+ await asyncio.wait_for(ws.send_text(raw), timeout=10.0)
130
526
  self._cnt_routed += 1
527
+ except asyncio.TimeoutError:
528
+ print(f"[kernel] slot_sender timeout: {mid} slot {slot}, closing")
529
+ try:
530
+ await ws.close(code=4013, reason="Send timeout")
531
+ except Exception:
532
+ pass
533
+ break
131
534
  except Exception:
132
- # Connection closed (normal during shutdown) — exit silently
133
535
  break
134
536
  except asyncio.CancelledError:
135
537
  pass
136
538
 
539
+ def _extract_event_key(self, raw: str) -> str:
540
+ """从 JSON 消息中提取事件 key(event type)用于保序分发。"""
541
+ try:
542
+ msg = _loads(raw)
543
+ return msg.get("params", {}).get("event", "")
544
+ except Exception:
545
+ return ""
546
+
547
+ def _pick_slot(self, mid: str, event_key: str) -> int:
548
+ """选择目标 slot:保序集合绑定 → 轮询。"""
549
+ slot_queues = self._slot_queues.get(mid, {})
550
+ alive_slots = list(slot_queues.keys())
551
+ if not alive_slots:
552
+ return 0 # fallback
553
+
554
+ # 单 slot 快速路径
555
+ if len(alive_slots) == 1:
556
+ return alive_slots[0]
557
+
558
+ # 检查保序集合
559
+ if event_key:
560
+ group = self._key_to_group.get((mid, event_key))
561
+ if group:
562
+ if group.bound_slot is not None and group.bound_slot in slot_queues:
563
+ return group.bound_slot
564
+ # 需要绑定新 slot
565
+ slot = self._round_robin(mid, alive_slots)
566
+ group.bound_slot = slot
567
+ # 记录最后入队的 seq,用于释放绑定
568
+ group.last_enqueued_seq = self._session_seq.get(mid, 0)
569
+ return slot
570
+
571
+ # 不在任何保序集合 → 轮询
572
+ return self._round_robin(mid, alive_slots)
573
+
574
+ def _round_robin(self, mid: str, alive_slots: list[int]) -> int:
575
+ """轮询选择 slot。"""
576
+ idx = self._rr_counters.get(mid, 0)
577
+ slot = alive_slots[idx % len(alive_slots)]
578
+ self._rr_counters[mid] = idx + 1
579
+ return slot
580
+
581
+ def set_ordering_groups(self, module_id: str, groups: list[dict]):
582
+ """设置模块的保序集合(由 kernel.set_ordering_groups RPC 调用)。
583
+
584
+ Args:
585
+ module_id: 模块 ID
586
+ groups: [{"name": "session", "keys": ["session.started", "session.ended"]}]
587
+ """
588
+ # 清理旧映射
589
+ old_groups = self._ordering_groups.pop(module_id, [])
590
+ for g in old_groups:
591
+ for key in g.keys:
592
+ self._key_to_group.pop((module_id, key), None)
593
+
594
+ # 创建新映射
595
+ new_groups = []
596
+ for g_data in groups:
597
+ name = g_data.get("name", "")
598
+ keys = set(g_data.get("keys", []))
599
+ group = OrderingGroup(name=name, keys=keys)
600
+ new_groups.append(group)
601
+ for key in keys:
602
+ self._key_to_group[(module_id, key)] = group
603
+
604
+ self._ordering_groups[module_id] = new_groups
605
+ print(f"[kernel] {module_id} set ordering groups: {[g.name for g in new_groups]}")
606
+
607
+ def get_slot_count(self, module_id: str) -> int:
608
+ """获取模块当前活跃的 slot 数量。"""
609
+ return len(self.connections.get(module_id, {}))
610
+
611
+ def _add_sequence_number(self, module_id: str, raw: str) -> str:
612
+ """为消息添加序列号和会话 ID"""
613
+ # 分配序列号
614
+ seq = self._session_seq.get(module_id, 0) + 1
615
+ self._session_seq[module_id] = seq
616
+
617
+ # 获取会话 ID
618
+ session_id = self._session_ids.get(module_id, "")
619
+
620
+ # 解析消息并添加 seq 和 session_id
621
+ msg = _loads(raw)
622
+ if "params" in msg:
623
+ msg["params"]["seq"] = seq
624
+ msg["params"]["session_id"] = session_id
625
+ # 剥离内部 _priority 字段(不发给订阅者)
626
+ msg["params"].pop("_priority", None)
627
+
628
+ return _dumps(msg)
629
+
630
+ async def _compress_and_send(self, module_id: str, queue: asyncio.Queue) -> str | None:
631
+ """从队列中取出多条可压缩消息,合并成压缩摘要。
632
+
633
+ 压缩规则:
634
+ 1. 最多压缩 100 条消息或 100KB 内容
635
+ 2. 遇到不可压缩消息(系统事件、压力事件)时停止
636
+ 3. 返回压缩摘要消息(JSON-RPC 2.0 格式)
637
+ """
638
+ compressed = []
639
+ total_bytes = 0
640
+ event_types = {}
641
+ sources = {}
642
+ seq_list = []
643
+
644
+ # 尝试从队列中取出消息
645
+ while len(compressed) < COMPRESS_MAX_COUNT and total_bytes < COMPRESS_MAX_BYTES:
646
+ if queue.empty():
647
+ break
648
+
649
+ try:
650
+ raw = queue.get_nowait()
651
+ except asyncio.QueueEmpty:
652
+ break
653
+
654
+ msg_size = len(raw)
655
+
656
+ # 解析消息
657
+ try:
658
+ msg = _loads(raw)
659
+ params = msg.get("params", {})
660
+ event_type = params.get("event", "")
661
+ source = params.get("source", "")
662
+ event_id = params.get("event_id", "")
663
+
664
+ # 检查是否可压缩
665
+ if event_type in SYSTEM_EVENTS or event_type in PRESSURE_EVENTS:
666
+ # 不可压缩 → 放回队列,停止压缩
667
+ # 注意:put_nowait 可能失败(队列满),但这是极端情况
668
+ try:
669
+ queue.put_nowait(raw)
670
+ except asyncio.QueueFull:
671
+ pass
672
+ break
673
+
674
+ # priority=critical 的事件不可压缩(与系统事件同等待遇)
675
+ msg_priority = params.get("_priority", "normal")
676
+ if msg_priority == "critical":
677
+ try:
678
+ queue.put_nowait(raw)
679
+ except asyncio.QueueFull:
680
+ pass
681
+ break
682
+
683
+ # 可压缩 → 加入压缩列表
684
+ compressed.append({
685
+ "event_id": event_id,
686
+ "event": event_type,
687
+ "source": source,
688
+ "timestamp": params.get("timestamp", ""),
689
+ })
690
+
691
+ # 统计
692
+ event_types[event_type] = event_types.get(event_type, 0) + 1
693
+ sources[source] = sources.get(source, 0) + 1
694
+ seq_list.append(params.get("seq"))
695
+
696
+ # 减少字节计数
697
+ current_bytes = self._queue_bytes.get(module_id, 0)
698
+ self._queue_bytes[module_id] = max(0, current_bytes - msg_size)
699
+
700
+ total_bytes += msg_size
701
+
702
+ except Exception as e:
703
+ # 解析失败 → 跳过这条消息
704
+ print(f"[kernel] Failed to parse message for compression: {e}")
705
+ continue
706
+
707
+ # 如果没有压缩任何消息,返回 None
708
+ if not compressed:
709
+ return None
710
+
711
+ # 构建压缩摘要
712
+ import uuid
713
+ seq_range = None
714
+ if seq_list:
715
+ valid_seqs = [s for s in seq_list if s is not None]
716
+ if valid_seqs:
717
+ seq_range = (min(valid_seqs), max(valid_seqs))
718
+
719
+ compress_msg = {
720
+ "jsonrpc": "2.0",
721
+ "method": "event",
722
+ "params": {
723
+ "event_id": str(uuid.uuid4()),
724
+ "event": "system.events_compressed",
725
+ "source": "kernel",
726
+ "timestamp": datetime.now(timezone.utc).isoformat(),
727
+ "session_id": self._session_ids.get(module_id, ""),
728
+ "data": {
729
+ "module_id": module_id,
730
+ "compressed_count": len(compressed),
731
+ "event_types": event_types,
732
+ "sources": sources,
733
+ "seq_range": seq_range,
734
+ "level": self._queue_levels.get(module_id, "idle"),
735
+ "total_bytes": total_bytes,
736
+ },
737
+ },
738
+ }
739
+
740
+ # 添加序列号(压缩摘要本身也需要序列号)
741
+ seq = self._session_seq.get(module_id, 0) + 1
742
+ self._session_seq[module_id] = seq
743
+ compress_msg["params"]["seq"] = seq
744
+
745
+ print(f"[kernel] Compressed {len(compressed)} events for {module_id}, seq_range={seq_range}, bytes={total_bytes}")
746
+
747
+ return _dumps(compress_msg)
748
+
137
749
  async def _callback_sender_loop(self, mid: str, callback: callable, queue: asyncio.Queue):
138
750
  """回调专用的发送循环(类似 _sender_loop,但调用回调而不是发送 WS)
139
751
 
@@ -183,6 +795,13 @@ class EventHub:
183
795
  q = asyncio.Queue(maxsize=QUEUE_MAXSIZE)
184
796
  self._queues[module_id] = q
185
797
 
798
+ # 注册到实例列表(使 subscription-based routing 能找到此模块)
799
+ base_mid = parse_instance_key(module_id)[0]
800
+ if base_mid not in self._module_instances:
801
+ self._module_instances[base_mid] = []
802
+ if module_id not in self._module_instances[base_mid]:
803
+ self._module_instances[base_mid].append(module_id)
804
+
186
805
  print(f"[kernel] Registered internal callback for {module_id}")
187
806
 
188
807
  def start_internal_senders(self):
@@ -204,16 +823,19 @@ class EventHub:
204
823
  task.cancel()
205
824
 
206
825
  def handle_subscribe(self, module_id: str, events: list[str]):
207
- if module_id not in self.subscriptions:
208
- self.subscriptions[module_id] = set()
209
- self.subscriptions[module_id].update(
826
+ """Store subscription patterns. Keyed by base module_id (shared across instances)."""
827
+ base_mid = parse_instance_key(module_id)[0]
828
+ if base_mid not in self.subscriptions:
829
+ self.subscriptions[base_mid] = set()
830
+ self.subscriptions[base_mid].update(
210
831
  (p, tuple(p.split("."))) for p in events
211
832
  )
212
833
  print(f"[kernel] {module_id} subscribed: {events}")
213
834
 
214
835
  def handle_unsubscribe(self, module_id: str, events: list[str]) -> dict:
215
836
  """Unsubscribe from events. Returns dict with unsubscribed events and count."""
216
- subs = self.subscriptions.get(module_id)
837
+ base_mid = parse_instance_key(module_id)[0]
838
+ subs = self.subscriptions.get(base_mid)
217
839
  unsubscribed = []
218
840
  if subs:
219
841
  to_remove = {item for item in subs if item[0] in events}
@@ -222,10 +844,270 @@ class EventHub:
222
844
  print(f"[kernel] {module_id} unsubscribed: {events}")
223
845
  return {"unsubscribed": unsubscribed, "count": len(unsubscribed)}
224
846
 
847
+ # ── Queue pressure management ──
848
+
849
+ def set_module_elastic(self, module_id: str, elastic: bool):
850
+ """记录模块是否支持弹性队列管理(由 set_ready 时调用)"""
851
+ self._module_elastic[module_id] = elastic
852
+
853
+ def _calc_pressure_level(self, module_id: str, queue: asyncio.Queue) -> str:
854
+ """根据队列使用率和字节使用率计算压力等级(取较高者)"""
855
+ # 消息数量使用率
856
+ count_usage = queue.qsize() / QUEUE_MAXSIZE
857
+ # 字节大小使用率
858
+ bytes_usage = self._queue_bytes.get(module_id, 0) / QUEUE_MAX_BYTES
859
+ # 取两者中较高的使用率
860
+ usage = max(count_usage, bytes_usage)
861
+
862
+ current = self._queue_levels.get(module_id, "idle")
863
+ level_idx = PRESSURE_LEVELS.index(current)
864
+
865
+ # 尝试升级
866
+ if current in PRESSURE_UPGRADE and usage > PRESSURE_UPGRADE[current]:
867
+ return PRESSURE_LEVELS[level_idx + 1]
868
+ # 尝试降级
869
+ if current in PRESSURE_DOWNGRADE and usage < PRESSURE_DOWNGRADE[current]:
870
+ return PRESSURE_LEVELS[level_idx - 1]
871
+ return current
872
+
873
+ def _on_pressure_change(self, module_id: str, old_level: str, new_level: str, queue: asyncio.Queue):
874
+ """压力等级变化时的处理逻辑"""
875
+ self._queue_levels[module_id] = new_level
876
+ usage_pct = round(queue.qsize() / QUEUE_MAXSIZE * 100, 1)
877
+
878
+ print(f"[kernel] Queue pressure {module_id}: {old_level} → {new_level} ({usage_pct}%)")
879
+
880
+ # 发送压力通知事件(直接入队,不经过 _route_event)
881
+ self._send_pressure_notification(module_id, new_level, queue, old_level)
882
+
883
+ # 管理定期提醒任务
884
+ if new_level in ("busy", "overload", "critical"):
885
+ # 进入 busy 及以上级别 → 启动定期提醒
886
+ if module_id not in self._pressure_reminder_tasks:
887
+ self._pressure_reminder_tasks[module_id] = asyncio.create_task(
888
+ self._pressure_reminder_loop(module_id)
889
+ )
890
+ else:
891
+ # 回落到 idle → 停止定期提醒
892
+ reminder_task = self._pressure_reminder_tasks.pop(module_id, None)
893
+ if reminder_task:
894
+ reminder_task.cancel()
895
+
896
+ # 管理 critical 倒计时
897
+ if new_level == "critical":
898
+ self._start_critical_countdown(module_id)
899
+ elif old_level == "critical":
900
+ # 从 critical 回落,取消倒计时
901
+ timer = self._critical_timers.pop(module_id, None)
902
+ if timer:
903
+ timer.cancel()
904
+ print(f"[kernel] {module_id} critical countdown cancelled (pressure relieved)")
905
+
906
+ async def _pressure_reminder_loop(self, module_id: str):
907
+ """定期发送压力状态提醒(每 3 秒)"""
908
+ try:
909
+ while True:
910
+ await asyncio.sleep(3)
911
+
912
+ # 检查当前压力等级
913
+ level = self._queue_levels.get(module_id, "idle")
914
+ if level not in ("busy", "overload", "critical"):
915
+ # 压力已降到 idle,停止提醒
916
+ break
917
+
918
+ # 获取队列
919
+ queue = self._queues.get(module_id)
920
+ if not queue:
921
+ break
922
+
923
+ # 发送状态通知(old_level = level 表示无变化)
924
+ self._send_pressure_notification(module_id, level, queue, level)
925
+
926
+ except asyncio.CancelledError:
927
+ pass
928
+ finally:
929
+ self._pressure_reminder_tasks.pop(module_id, None)
930
+
931
+ def _send_pressure_notification(self, module_id: str, level: str, queue: asyncio.Queue, old_level: str = None):
932
+ """向目标模块发送 system.queue_pressure 事件"""
933
+ import uuid
934
+
935
+ queue_bytes = self._queue_bytes.get(module_id, 0)
936
+ count_usage = queue.qsize() / QUEUE_MAXSIZE
937
+ bytes_usage = queue_bytes / QUEUE_MAX_BYTES
938
+
939
+ # 判断是升级还是降级
940
+ direction = None
941
+ if old_level:
942
+ old_idx = PRESSURE_LEVELS.index(old_level) if old_level in PRESSURE_LEVELS else -1
943
+ new_idx = PRESSURE_LEVELS.index(level) if level in PRESSURE_LEVELS else -1
944
+ if new_idx > old_idx:
945
+ direction = "upgrade"
946
+ elif new_idx < old_idx:
947
+ direction = "downgrade"
948
+
949
+ msg = {
950
+ "jsonrpc": "2.0",
951
+ "method": "event",
952
+ "params": {
953
+ "event_id": str(uuid.uuid4()),
954
+ "event": "system.queue_pressure",
955
+ "source": "kernel",
956
+ "timestamp": datetime.now(timezone.utc).isoformat(),
957
+ "data": {
958
+ "module_id": module_id,
959
+ "level": level,
960
+ "old_level": old_level,
961
+ "direction": direction, # "upgrade" | "downgrade" | None
962
+ # 消息数量
963
+ "queue_size": queue.qsize(),
964
+ "queue_max": QUEUE_MAXSIZE,
965
+ "count_usage_percent": round(count_usage * 100, 1),
966
+ # 字节大小
967
+ "queue_bytes": queue_bytes,
968
+ "queue_max_bytes": QUEUE_MAX_BYTES,
969
+ "bytes_usage_percent": round(bytes_usage * 100, 1),
970
+ # 综合使用率(取较高者)
971
+ "usage_percent": round(max(count_usage, bytes_usage) * 100, 1),
972
+ "suggestion": PRESSURE_SUGGESTIONS[level],
973
+ },
974
+ },
975
+ }
976
+ raw = _dumps(msg)
977
+ # 直接入队(压力通知本身不受压力过滤)
978
+ try:
979
+ queue.put_nowait(raw)
980
+ except asyncio.QueueFull:
981
+ pass # 队列满也无法发送,只能放弃
982
+
983
+ def _start_critical_countdown(self, module_id: str):
984
+ """启动 critical 倒计时"""
985
+ # 如果已有倒计时在运行,不重复启动
986
+ if module_id in self._critical_timers:
987
+ return
988
+
989
+ is_elastic = self._module_elastic.get(module_id, False)
990
+ timeout = CRITICAL_TIMEOUT_ELASTIC if is_elastic else CRITICAL_TIMEOUT_DEFAULT
991
+ self._critical_timers[module_id] = asyncio.create_task(
992
+ self._critical_countdown_loop(module_id, timeout)
993
+ )
994
+ print(f"[kernel] {module_id} critical countdown started ({timeout}s, elastic={is_elastic})")
995
+
996
+ async def _critical_countdown_loop(self, module_id: str, timeout: int):
997
+ """倒计时循环:定期发送警告,超时后断开连接
998
+
999
+ 弹性模块:由 _on_pressure_change 在压力回落时取消此 task
1000
+ 非弹性模块:在此循环中自行检查队列使用率 < 85% 时退出
1001
+ """
1002
+ try:
1003
+ is_elastic = self._module_elastic.get(module_id, False)
1004
+ remaining = timeout
1005
+ while remaining > 0:
1006
+ await asyncio.sleep(CRITICAL_WARNING_INTERVAL)
1007
+ remaining -= CRITICAL_WARNING_INTERVAL
1008
+
1009
+ # 检查模块是否还在连接中
1010
+ queue = self._queues.get(module_id)
1011
+ slots = self.connections.get(module_id)
1012
+ if not queue or not slots:
1013
+ return
1014
+
1015
+ # 非弹性模块:检查队列使用率是否降到 85% 以下
1016
+ if not is_elastic:
1017
+ count_usage = queue.qsize() / QUEUE_MAXSIZE
1018
+ bytes_usage = self._queue_bytes.get(module_id, 0) / QUEUE_MAX_BYTES
1019
+ usage = max(count_usage, bytes_usage)
1020
+ if usage < 0.85:
1021
+ self._critical_timers.pop(module_id, None)
1022
+ print(f"[kernel] {module_id} critical countdown cancelled (usage dropped to {round(usage * 100, 1)}%)")
1023
+ return
1024
+
1025
+ # 弹性模块:由 _on_pressure_change 取消此 task,这里只负责发送警告和最终断开
1026
+
1027
+ # 发送警告事件
1028
+ import uuid
1029
+ warning_msg = {
1030
+ "jsonrpc": "2.0",
1031
+ "method": "event",
1032
+ "params": {
1033
+ "event_id": str(uuid.uuid4()),
1034
+ "event": "system.queue_warning",
1035
+ "source": "kernel",
1036
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1037
+ "data": {
1038
+ "module_id": module_id,
1039
+ "remaining_seconds": max(remaining, 0),
1040
+ "reason": "queue_critical_timeout",
1041
+ "message": f"Queue pressure critical, will disconnect in {max(remaining, 0)}s",
1042
+ },
1043
+ },
1044
+ }
1045
+ try:
1046
+ queue.put_nowait(_dumps(warning_msg))
1047
+ except asyncio.QueueFull:
1048
+ pass
1049
+
1050
+ # 倒计时归零,断开所有连接
1051
+ self._critical_timers.pop(module_id, None)
1052
+ slots = self.connections.get(module_id, {})
1053
+ if slots:
1054
+ print(f"[kernel] {module_id} disconnected due to critical queue pressure timeout")
1055
+ for ws in list(slots.values()):
1056
+ try:
1057
+ await ws.close(code=4011, reason="queue_pressure_critical")
1058
+ except Exception:
1059
+ pass
1060
+
1061
+ except asyncio.CancelledError:
1062
+ pass
1063
+
1064
+ def _check_pressure(self, module_id: str, queue: asyncio.Queue):
1065
+ """检查队列压力并处理等级变化(在 _route_event 入队前调用)"""
1066
+ new_level = self._calc_pressure_level(module_id, queue)
1067
+ old_level = self._queue_levels.get(module_id, "idle")
1068
+ if new_level != old_level:
1069
+ self._on_pressure_change(module_id, old_level, new_level, queue)
1070
+
1071
+ def _should_drop(self, module_id: str, event_type: str, droppable: bool,
1072
+ priority: str = "normal") -> bool:
1073
+ """判断是否应丢弃该事件
1074
+
1075
+ 丢弃决策矩阵:
1076
+ 压力等级 → idle busy overload critical
1077
+ ─────────────────────────────────────────────────────────────────
1078
+ 系统/压力事件 KEEP KEEP KEEP KEEP
1079
+ priority=critical KEEP KEEP KEEP KEEP
1080
+ priority=high KEEP KEEP KEEP DROP if droppable
1081
+ priority=normal KEEP KEEP DROP if droppable DROP (always)
1082
+ """
1083
+ level = self._queue_levels.get(module_id, "idle")
1084
+ # idle/busy: 不丢弃
1085
+ if level in ("idle", "busy"):
1086
+ return False
1087
+ # 系统事件和压力事件永不丢弃
1088
+ if event_type in SYSTEM_EVENTS or event_type in PRESSURE_EVENTS:
1089
+ return False
1090
+ # priority=critical: 永不被压力丢弃
1091
+ if priority == "critical":
1092
+ return False
1093
+ # overload: normal 丢弃 droppable,high 不丢弃
1094
+ if level == "overload":
1095
+ if priority == "high":
1096
+ return False
1097
+ return droppable
1098
+ # critical 压力: high 仅当 droppable 时丢弃,normal 全部丢弃
1099
+ if priority == "high":
1100
+ return droppable
1101
+ return True
1102
+
225
1103
  # ── Event publishing (called by RpcRouter) ──
226
1104
 
227
1105
  def publish_event(self, sender_id: str, event_id: str, event_type: str,
228
- data: dict = None, echo: bool = False) -> dict:
1106
+ data: dict = None, echo: bool = False,
1107
+ droppable: bool = False,
1108
+ priority: str = "normal",
1109
+ throttle_key: str = "",
1110
+ throttle_ms: int = 0) -> dict:
229
1111
  """Publish an event from a module. Called by RpcRouter for event.publish RPC.
230
1112
  Returns empty dict on success, raises exception on validation failure."""
231
1113
  self._cnt_received += 1
@@ -234,10 +1116,39 @@ class EventHub:
234
1116
  self._cnt_errors += 1
235
1117
  raise ValueError("Missing required field: event_id or event")
236
1118
 
1119
+ # 安全加固:以下系统事件只能由 Kernel 通过 publish_internal() 发出
1120
+ # - module.offline:Kernel 检测连接断开时发布
1121
+ # - module.degraded / module.recovered:必须走 kernel.report_* RPC 中转
1122
+ # 注意:module.ready 由模块自身通过 event.publish 发布(合法),不在此列
1123
+ # 注意:module.shutdown 由 Launcher 通过 event.publish 发布(合法),不在此列
1124
+ KERNEL_ONLY_EVENTS = {"module.offline",
1125
+ "module.degraded", "module.recovered"}
1126
+ if event_type in KERNEL_ONLY_EVENTS:
1127
+ self._cnt_errors += 1
1128
+ raise ValueError(f"System event '{event_type}' cannot be published by modules directly. "
1129
+ f"Use the corresponding kernel.* RPC method instead.")
1130
+
237
1131
  if self.dedup.is_duplicate(event_id):
238
1132
  self._cnt_dedup += 1
239
1133
  return {} # silently accept duplicates
240
1134
 
1135
+ # 源端节流:同一 key 在间隔内的重复发布被静默丢弃
1136
+ if throttle_key and throttle_ms > 0:
1137
+ now = time.time()
1138
+ full_key = f"{sender_id}:{throttle_key}"
1139
+ last = self._throttle_last.get(full_key, 0)
1140
+ if (now - last) * 1000 < throttle_ms:
1141
+ self._cnt_throttled += 1
1142
+ return {}
1143
+ self._throttle_last[full_key] = now
1144
+
1145
+ # Update event statistics
1146
+ self._event_stats[event_type] = {
1147
+ "count": self._event_stats.get(event_type, {}).get("count", 0) + 1,
1148
+ "last_time": time.time(),
1149
+ "last_source": sender_id,
1150
+ }
1151
+
241
1152
  msg = {
242
1153
  "jsonrpc": "2.0",
243
1154
  "method": "event",
@@ -250,10 +1161,11 @@ class EventHub:
250
1161
  },
251
1162
  }
252
1163
 
253
- self._route_event(sender_id, msg, event_type, echo)
1164
+ self._route_event(sender_id, msg, event_type, echo, droppable, priority)
254
1165
  return {}
255
1166
 
256
- def publish_internal(self, event_type: str, data: dict, source: str):
1167
+ def publish_internal(self, event_type: str, data: dict, source: str,
1168
+ priority: str = "normal"):
257
1169
  """Publish a Kernel-originated event (e.g. module.offline, module.registered).
258
1170
  No dedup check — internal events are unique by nature.
259
1171
 
@@ -261,11 +1173,19 @@ class EventHub:
261
1173
  event_type: 事件类型(如 "module.ready")
262
1174
  data: 事件数据
263
1175
  source: 事件来源模块 ID(必填,通常是调用者的 module_id)
1176
+ priority: 事件优先级 ("normal" | "high" | "critical")
264
1177
  """
265
1178
  import uuid
266
1179
  event_id = str(uuid.uuid4())
267
1180
  self._cnt_received += 1
268
1181
 
1182
+ # Update event statistics
1183
+ self._event_stats[event_type] = {
1184
+ "count": self._event_stats.get(event_type, {}).get("count", 0) + 1,
1185
+ "last_time": time.time(),
1186
+ "last_source": source,
1187
+ }
1188
+
269
1189
  msg = {
270
1190
  "jsonrpc": "2.0",
271
1191
  "method": "event",
@@ -278,86 +1198,145 @@ class EventHub:
278
1198
  },
279
1199
  }
280
1200
 
281
- self._route_event(source, msg, event_type, echo=False)
1201
+ self._route_event(source, msg, event_type, echo=False, droppable=False,
1202
+ priority=priority)
282
1203
 
283
1204
  # ── Routing ──
284
1205
 
285
- def _route_event(self, sender_id: str, msg: dict, event_type: str, echo: bool):
1206
+ def _route_event(self, sender_id: str, msg: dict, event_type: str, echo: bool,
1207
+ droppable: bool = False, priority: str = "normal"):
286
1208
  """Enqueue event to all matching subscribers' delivery queues.
287
1209
  System events (module.offline, module.ready, module.shutdown) are auto-broadcast
288
1210
  to ALL connected modules, regardless of subscription.
289
1211
 
290
1212
  Exception: module.shutdown with a target module_id is sent ONLY to that module.
1213
+
1214
+ Queue pressure management:
1215
+ - Checks pressure level before each enqueue
1216
+ - Drops events based on pressure level, droppable flag, and priority
1217
+ - Sends pressure notifications on level change
291
1218
  """
292
1219
  e_parts = tuple(event_type.split("."))
293
1220
  raw = None # lazy serialization
294
1221
  params = msg.get("params", {})
295
1222
  data = params.get("data", {})
296
1223
 
1224
+ # 注入 _priority 到 params(非 normal 时),用于 _compress_and_send 判断
1225
+ if priority != "normal":
1226
+ msg["params"]["_priority"] = priority
1227
+
297
1228
  # System events → broadcast to all connected modules (unless targeted)
298
1229
  if event_type in SYSTEM_EVENTS:
299
- # Check if this is a targeted shutdown (has module_id in data)
300
- if event_type == "module.shutdown":
1230
+ # Targeted delivery: module.shutdown / connection offer/release send only to target
1231
+ if event_type in ("module.shutdown", "system.connection.offer", "system.connection.release"):
301
1232
  target_module = data.get("module_id", "")
302
1233
 
303
- # Targeted shutdown → send only to target module
304
1234
  if target_module:
1235
+ # target_module may be instance_key or module_id; try instance_key first
305
1236
  queue = self._queues.get(target_module)
306
1237
  if queue:
307
1238
  if raw is None:
308
1239
  raw = _dumps(msg)
309
- try:
310
- queue.put_nowait(raw)
311
- self._cnt_queued += 1
312
- except asyncio.QueueFull:
313
- # Queue 满了,记录溢出
314
- self._queue_overflow[target_module] = self._queue_overflow.get(target_module, 0) + 1
315
- overflow_count = self._queue_overflow[target_module]
316
- # 每 100 次溢出打印一次警告
317
- if overflow_count % 100 == 1:
318
- print(f"[kernel] Warning: Queue full for {target_module}, dropped {overflow_count} events")
1240
+ self._enqueue_with_pressure(target_module, queue, raw, event_type, droppable, priority)
319
1241
  return
320
1242
 
321
- # Broadcast system events (no target or non-shutdown events)
1243
+ # Broadcast system events to all instance queues
322
1244
  for mid, queue in self._queues.items():
323
1245
  if mid == sender_id and not echo:
324
1246
  continue
325
1247
  if raw is None:
326
1248
  raw = _dumps(msg)
327
- try:
328
- queue.put_nowait(raw)
329
- self._cnt_queued += 1
330
- except asyncio.QueueFull:
331
- # Queue 满了,记录溢出
332
- self._queue_overflow[mid] = self._queue_overflow.get(mid, 0) + 1
333
- overflow_count = self._queue_overflow[mid]
334
- if overflow_count % 100 == 1:
335
- print(f"[kernel] Warning: Queue full for {mid}, dropped {overflow_count} events")
1249
+ self._enqueue_with_pressure(mid, queue, raw, event_type, droppable, priority)
336
1250
 
337
1251
  return
338
1252
 
339
1253
  # Normal events → subscription-based routing
1254
+ # subscriptions keyed by base module_id, queues keyed by instance_key
1255
+ sender_module = parse_instance_key(sender_id)[0]
1256
+ event_key = data.get("ordering_key", "") or event_type
340
1257
  for mid, patterns in self.subscriptions.items():
341
- if mid == sender_id and not echo:
1258
+ if mid == sender_module and not echo:
342
1259
  continue
343
1260
  for _pat_str, p_parts in patterns:
344
1261
  if match_parts(p_parts, e_parts):
345
- queue = self._queues.get(mid)
1262
+ picked = self._pick_instance(mid, event_key)
1263
+ if not picked:
1264
+ break # no active instance
1265
+ queue = self._queues.get(picked)
346
1266
  if queue:
347
1267
  if raw is None:
348
1268
  raw = _dumps(msg)
349
- try:
350
- queue.put_nowait(raw)
351
- self._cnt_queued += 1
352
- except asyncio.QueueFull:
353
- # Queue 满了,记录溢出
354
- self._queue_overflow[mid] = self._queue_overflow.get(mid, 0) + 1
355
- overflow_count = self._queue_overflow[mid]
356
- # 每 100 次溢出打印一次警告
357
- if overflow_count % 100 == 1:
358
- print(f"[kernel] Warning: Queue full for {mid}, dropped {overflow_count} events")
1269
+ self._enqueue_with_pressure(picked, queue, raw, event_type, droppable, priority)
359
1270
  break
360
1271
 
1272
+ def _enqueue_with_pressure(self, module_id: str, queue: asyncio.Queue,
1273
+ raw: str, event_type: str, droppable: bool,
1274
+ priority: str = "normal"):
1275
+ """入队并执行压力检测。统一所有入队逻辑到一个方法。
1276
+
1277
+ 弹性模块(queue_elastic: true):完整压力管理(等级检测、分级丢弃、通知)
1278
+ 非弹性模块:先进先发,溢出即丢,队列使用率 >= 85% 时启动倒计时
1279
+ """
1280
+ msg_size = len(raw)
1281
+ current_bytes = self._queue_bytes.get(module_id, 0)
1282
+ is_elastic = self._module_elastic.get(module_id, False)
1283
+
1284
+ if is_elastic:
1285
+ # ── 弹性模块:完整压力管理路径 ──
1286
+ # 检查字节限制
1287
+ if current_bytes + msg_size > QUEUE_MAX_BYTES:
1288
+ if self._should_drop(module_id, event_type, droppable, priority):
1289
+ self._cnt_dropped += 1
1290
+ self._queue_overflow[module_id] = self._queue_overflow.get(module_id, 0) + 1
1291
+ overflow_count = self._queue_overflow[module_id]
1292
+ if overflow_count % 100 == 1:
1293
+ print(f"[kernel] Bytes overflow for {module_id}: {overflow_count} events dropped")
1294
+ return
1295
+
1296
+ # 检查压力等级(等级变化时自动发送通知)
1297
+ self._check_pressure(module_id, queue)
1298
+
1299
+ # 压力过滤:根据等级决定是否丢弃
1300
+ if self._should_drop(module_id, event_type, droppable, priority):
1301
+ self._cnt_dropped += 1
1302
+ self._queue_overflow[module_id] = self._queue_overflow.get(module_id, 0) + 1
1303
+ overflow_count = self._queue_overflow[module_id]
1304
+ if overflow_count % 100 == 1:
1305
+ level = self._queue_levels.get(module_id, "idle")
1306
+ print(f"[kernel] Pressure drop for {module_id} [{level}]: {overflow_count} events dropped")
1307
+ return
1308
+
1309
+ # 正常入队
1310
+ try:
1311
+ queue.put_nowait(raw)
1312
+ self._cnt_queued += 1
1313
+ self._queue_bytes[module_id] = current_bytes + msg_size
1314
+ except asyncio.QueueFull:
1315
+ self._cnt_dropped += 1
1316
+ self._queue_overflow[module_id] = self._queue_overflow.get(module_id, 0) + 1
1317
+ overflow_count = self._queue_overflow[module_id]
1318
+ if overflow_count % 100 == 1:
1319
+ print(f"[kernel] Queue full for {module_id}, dropped {overflow_count} events")
1320
+ else:
1321
+ # ── 非弹性模块:极简路径(先进先发,溢出即丢) ──
1322
+ try:
1323
+ queue.put_nowait(raw)
1324
+ self._cnt_queued += 1
1325
+ self._queue_bytes[module_id] = current_bytes + msg_size
1326
+ except asyncio.QueueFull:
1327
+ self._cnt_dropped += 1
1328
+ self._queue_overflow[module_id] = self._queue_overflow.get(module_id, 0) + 1
1329
+ overflow_count = self._queue_overflow[module_id]
1330
+ if overflow_count % 100 == 1:
1331
+ print(f"[kernel] Queue full for {module_id} (non-elastic), dropped {overflow_count} events")
1332
+
1333
+ # 检查队列使用率,>= 85% 时启动倒计时
1334
+ count_usage = queue.qsize() / QUEUE_MAXSIZE
1335
+ bytes_usage = self._queue_bytes.get(module_id, 0) / QUEUE_MAX_BYTES
1336
+ usage = max(count_usage, bytes_usage)
1337
+ if usage >= 0.85:
1338
+ self._start_critical_countdown(module_id)
1339
+
361
1340
  # ── Stats ──
362
1341
 
363
1342
  def _counters_dict(self) -> dict:
@@ -366,6 +1345,8 @@ class EventHub:
366
1345
  "events_routed": self._cnt_routed,
367
1346
  "events_queued": self._cnt_queued,
368
1347
  "events_deduplicated": self._cnt_dedup,
1348
+ "events_dropped": self._cnt_dropped,
1349
+ "events_throttled": self._cnt_throttled,
369
1350
  "errors": self._cnt_errors,
370
1351
  }
371
1352
 
@@ -377,17 +1358,84 @@ class EventHub:
377
1358
  for mid, patterns in self.subscriptions.items()
378
1359
  },
379
1360
  "counters": self._counters_dict(),
1361
+ "queue_pressure": self._get_pressure_stats(),
1362
+ }
1363
+
1364
+ def _get_pressure_stats(self) -> dict:
1365
+ """返回每个模块的队列压力信息"""
1366
+ result = {}
1367
+ for mid, queue in self._queues.items():
1368
+ qsize = queue.qsize()
1369
+ result[mid] = {
1370
+ "level": self._queue_levels.get(mid, "idle"),
1371
+ "queue_size": qsize,
1372
+ "usage_percent": round(qsize / QUEUE_MAXSIZE * 100, 1),
1373
+ "elastic": self._module_elastic.get(mid, False),
1374
+ "overflow_count": self._queue_overflow.get(mid, 0),
1375
+ }
1376
+ return result
1377
+
1378
+ def get_event_info(self, event_pattern: str) -> dict:
1379
+ """获取事件的详细信息,包括订阅者和统计数据。
1380
+
1381
+ Args:
1382
+ event_pattern: 事件模式(如 "module.started" 或 "module.*")
1383
+
1384
+ Returns:
1385
+ {
1386
+ "subscribers": [module_id], # 订阅此事件的模块列表
1387
+ "stats": {
1388
+ "count": int, # 发布次数
1389
+ "last_time": float, # 最后发布时间戳
1390
+ "last_source": str # 最后发布者
1391
+ }
1392
+ }
1393
+ """
1394
+ from .router import match_parts
1395
+
1396
+ # 查找订阅此事件的模块
1397
+ subscribers = []
1398
+ pattern_tuple = tuple(event_pattern.split("."))
1399
+
1400
+ for module_id, patterns in self.subscriptions.items():
1401
+ for pattern_str, pattern_parts in patterns:
1402
+ # 检查模式是否匹配事件
1403
+ if match_parts(pattern_tuple, pattern_parts):
1404
+ subscribers.append(module_id)
1405
+ break
1406
+
1407
+ # 获取统计信息
1408
+ stats = self._event_stats.get(event_pattern, {
1409
+ "count": 0,
1410
+ "last_time": None,
1411
+ "last_source": None
1412
+ })
1413
+
1414
+ return {
1415
+ "subscribers": sorted(subscribers),
1416
+ "stats": stats
380
1417
  }
381
1418
 
1419
+
382
1420
  def get_health(self) -> dict:
1421
+ # 如果有模块处于 overload 或 critical,整体状态为 degraded
1422
+ pressure_levels = {self._queue_levels.get(mid, "idle") for mid in self._queues}
1423
+ if "critical" in pressure_levels:
1424
+ status = "degraded"
1425
+ elif "overload" in pressure_levels:
1426
+ status = "degraded"
1427
+ else:
1428
+ status = "healthy"
1429
+
383
1430
  return {
384
- "status": "healthy",
1431
+ "status": status,
385
1432
  "details": {
386
- "connections": len(self.connections),
1433
+ "connections": sum(len(s) for s in self.connections.values()),
387
1434
  "subscriptions": sum(
388
1435
  len(p) for p in self.subscriptions.values()
389
1436
  ),
390
1437
  "events_routed": self._cnt_routed,
1438
+ "events_dropped": self._cnt_dropped,
391
1439
  "dedup_size": self.dedup.size,
392
1440
  "uptime_seconds": round(time.time() - self._start_time),
393
1441
  },