@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
@@ -1,491 +1,1400 @@
1
- """
2
- JSON-RPC 2.0 message dispatcher for Kernel.
3
- Routes builtin methods (registry.*, event.*, kernel.*) and forwards
4
- cross-module RPC requests with timeout tracking.
5
- """
6
-
7
- import asyncio
8
- import json
9
- import time
10
- import uuid
11
- from dataclasses import dataclass, field
12
- from datetime import datetime, timezone
13
-
14
- from starlette.websockets import WebSocket
15
-
16
- from .registry_store import RegistryStore
17
- from .event_hub import EventHub
18
-
19
-
20
- # ── JSON-RPC 2.0 error codes ──
21
-
22
- PARSE_ERROR = -32700
23
- INVALID_REQUEST = -32600
24
- METHOD_NOT_FOUND = -32601
25
- INVALID_PARAMS = -32602
26
- INTERNAL_ERROR = -32603
27
-
28
- # Kite custom error codes
29
- MODULE_OFFLINE = -32001
30
- RPC_TIMEOUT = -32002
31
- AUTH_FAILED = -32003
32
- PERMISSION_DENIED = -32004
33
- DUPLICATE_EVENT = -32005
34
-
35
- DEFAULT_FORWARD_TIMEOUT = 5.0 # seconds
36
-
37
-
38
- @dataclass
39
- class PendingForward:
40
- """Tracks a forwarded cross-module RPC awaiting response."""
41
- caller_ws: WebSocket
42
- original_id: str
43
- caller_id: str
44
- target_id: str
45
- created_at: float = field(default_factory=time.time)
46
- timeout_handle: asyncio.TimerHandle | None = None
47
-
48
-
49
- def _result_msg(msg_id: str, result: dict) -> str:
50
- return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result})
51
-
52
-
53
- def _error_msg(msg_id: str, code: int, message: str, data: dict = None) -> str:
54
- err = {"code": code, "message": message}
55
- if data:
56
- err["data"] = data
57
- return json.dumps({"jsonrpc": "2.0", "id": msg_id, "error": err})
58
-
59
-
60
- class RpcRouter:
61
- """JSON-RPC 2.0 message dispatcher.
62
-
63
- Handles:
64
- - 13 builtin methods (registry.*, event.*, kernel.*)
65
- - Cross-module RPC forwarding with timeout
66
- - RPC response matching for forwarded calls
67
- """
68
-
69
- def __init__(self, registry: RegistryStore, event_hub: EventHub,
70
- connections: dict[str, WebSocket], kernel_server):
71
- self.registry = registry
72
- self.event_hub = event_hub
73
- self.connections = connections
74
- self.kernel_server = kernel_server # Direct reference to KernelServer
75
-
76
- # RPC 统计计数器
77
- self._rpc_total = 0 # RPC 调用次数
78
- self._rpc_builtin = 0 # 内置方法调用次数
79
- self._rpc_forwarded = 0 # 转发调用次数
80
- self._rpc_errors = 0 # 错误次数
81
-
82
- # Builtin method dispatch table
83
- self.methods: dict[str, callable] = {
84
- "registry.register": self._registry_register,
85
- "registry.deregister": self._registry_deregister,
86
- "registry.lookup": self._registry_lookup,
87
- "registry.get": self._registry_get,
88
- "registry.verify": self._registry_verify,
89
- "event.publish": self._event_publish,
90
- "event.subscribe": self._event_subscribe,
91
- "event.unsubscribe": self._event_unsubscribe,
92
- "kernel.ping": self._kernel_ping,
93
- "kernel.stats": self._kernel_stats,
94
- "kernel.health": self._kernel_health,
95
- "kernel.latencies": self._kernel_latencies,
96
- "kernel.generate_tokens": self._kernel_generate_tokens,
97
- "kernel.register_tokens": self._kernel_register_tokens,
98
- }
99
-
100
- # Pending cross-module forwards: internal_id -> PendingForward
101
- self._pending: dict[str, PendingForward] = {}
102
-
103
- # ── Main dispatch ──
104
-
105
- async def dispatch(self, caller_id: str, ws: WebSocket, msg: dict):
106
- """Route a parsed JSON-RPC message to builtin handler or cross-module forward."""
107
- method = msg.get("method", "")
108
- msg_id = msg.get("id")
109
-
110
- # 统计:总调用次数
111
- self._rpc_total += 1
112
-
113
- # JSON-RPC Notification (no id) — currently not handled from clients
114
- if msg_id is None:
115
- return
116
-
117
- params = msg.get("params") or {}
118
-
119
- # Builtin method
120
- handler = self.methods.get(method)
121
- if handler:
122
- # 统计:内置方法调用
123
- self._rpc_builtin += 1
124
- try:
125
- result = await handler(caller_id, params)
126
- except Exception as e:
127
- # 统计:错误次数
128
- self._rpc_errors += 1
129
- print(f"[kernel] RPC handler error ({method}): {e}")
130
- try:
131
- await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
132
- except Exception:
133
- # Can't send error response, connection closed
134
- pass
135
- return
136
-
137
- # Send result (may fail if connection closed during shutdown)
138
- try:
139
- await ws.send_text(_result_msg(msg_id, result))
140
- except Exception as e:
141
- # Connection closed during shutdown — this is normal, exit silently
142
- pass
143
- return
144
-
145
- # Cross-module forward: method prefix is target module_id
146
- dot_idx = method.find(".")
147
- if dot_idx > 0:
148
- target = method[:dot_idx]
149
- if target in self.connections:
150
- # Check if target module is ready (state machine protection)
151
- if not self.registry.is_ready(target):
152
- # 统计:错误次数
153
- self._rpc_errors += 1
154
- await ws.send_text(_error_msg(
155
- msg_id, MODULE_OFFLINE,
156
- f"Module not ready: {target} (status: {self.registry.modules.get(target, {}).get('status', 'unknown')})"))
157
- return
158
- # 统计:转发调用
159
- self._rpc_forwarded += 1
160
- await self._forward(caller_id, ws, msg_id, target, method, params)
161
- return
162
- # Target not connected — check if registered but offline
163
- if target in self.registry.modules:
164
- # 统计:错误次数
165
- self._rpc_errors += 1
166
- await ws.send_text(_error_msg(
167
- msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
168
- return
169
-
170
- # Method not found
171
- # 统计:错误次数
172
- self._rpc_errors += 1
173
- await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
174
-
175
- async def handle_response(self, module_id: str, msg: dict):
176
- """Handle an RPC response from a module (matches pending forwards)."""
177
- msg_id = msg.get("id")
178
- if not msg_id:
179
- return
180
-
181
- pending = self._pending.pop(msg_id, None)
182
- if not pending:
183
- return # orphan response, ignore
184
-
185
- # Cancel timeout
186
- if pending.timeout_handle:
187
- pending.timeout_handle.cancel()
188
-
189
- # Forward response to original caller with original ID
190
- response = {"jsonrpc": "2.0", "id": pending.original_id}
191
- if "result" in msg:
192
- response["result"] = msg["result"]
193
- elif "error" in msg:
194
- response["error"] = msg["error"]
195
- else:
196
- response["result"] = None
197
-
198
- try:
199
- await pending.caller_ws.send_text(json.dumps(response))
200
- except Exception:
201
- pass # caller disconnected
202
-
203
- # ── Cross-module forwarding ──
204
-
205
- async def _forward(self, caller_id: str, caller_ws: WebSocket,
206
- original_id: str, target: str, method: str, params: dict):
207
- """Forward RPC request to target module with timeout tracking."""
208
- internal_id = f"fwd-{uuid.uuid4().hex[:12]}"
209
-
210
- # Extract timeout from params (optional _timeout field)
211
- timeout = DEFAULT_FORWARD_TIMEOUT
212
- if isinstance(params, dict) and "_timeout" in params:
213
- try:
214
- timeout = float(params.pop("_timeout"))
215
- except (ValueError, TypeError):
216
- pass
217
-
218
- # Strip target prefix from method for the forwarded request
219
- actual_method = method[len(target) + 1:] # e.g. "watchdog.get_status" -> "get_status"
220
-
221
- # Inject caller_id into params for permission checking
222
- if not isinstance(params, dict):
223
- params = {}
224
- params["_caller_id"] = caller_id
225
-
226
- # Record pending forward
227
- loop = asyncio.get_event_loop()
228
- pending = PendingForward(
229
- caller_ws=caller_ws,
230
- original_id=original_id,
231
- caller_id=caller_id,
232
- target_id=target,
233
- )
234
- pending.timeout_handle = loop.call_later(
235
- timeout, lambda iid=internal_id: asyncio.ensure_future(self._handle_timeout(iid))
236
- )
237
- self._pending[internal_id] = pending
238
-
239
- # Send to target
240
- fwd_msg = json.dumps({
241
- "jsonrpc": "2.0",
242
- "id": internal_id,
243
- "method": actual_method,
244
- "params": params or {},
245
- })
246
- target_ws = self.connections.get(target)
247
- if target_ws:
248
- try:
249
- await target_ws.send_text(fwd_msg)
250
- except Exception:
251
- # Target send failed — clean up and error to caller
252
- self._pending.pop(internal_id, None)
253
- if pending.timeout_handle:
254
- pending.timeout_handle.cancel()
255
- await caller_ws.send_text(_error_msg(
256
- original_id, MODULE_OFFLINE, f"Failed to reach module: {target}"))
257
- else:
258
- # Target disconnected between check and send
259
- self._pending.pop(internal_id, None)
260
- if pending.timeout_handle:
261
- pending.timeout_handle.cancel()
262
- await caller_ws.send_text(_error_msg(
263
- original_id, MODULE_OFFLINE, f"Module offline: {target}"))
264
-
265
- async def _handle_timeout(self, internal_id: str):
266
- """Called when a forwarded RPC times out."""
267
- pending = self._pending.pop(internal_id, None)
268
- if not pending:
269
- return
270
- try:
271
- await pending.caller_ws.send_text(_error_msg(
272
- pending.original_id, RPC_TIMEOUT,
273
- f"RPC timeout waiting for {pending.target_id}"))
274
- except Exception:
275
- pass
276
-
277
- # ── Builtin handlers: registry.* ──
278
-
279
- async def _registry_register(self, caller_id: str, params: dict) -> dict:
280
- mid = params.get("module_id")
281
- if not mid:
282
- raise ValueError("module_id required")
283
- # Permission: only Launcher or the module itself
284
- if caller_id != "launcher" and caller_id != mid:
285
- raise PermissionError(f"Module '{caller_id}' cannot register as '{mid}'")
286
-
287
- print(f"[kernel] registry.register called by {caller_id} for module {mid}")
288
- print(f"[kernel] rpc_methods: {params.get('rpc_methods')}")
289
-
290
- result = self.registry.register_module(params)
291
- self.event_hub.publish_internal("module.registered", {"module_id": mid}, source=self.kernel_server.module_id)
292
-
293
- # Only publish registry.updated if content actually changed
294
- if result.get("changed", True):
295
- from datetime import datetime, timezone
296
- self.event_hub.publish_internal("registry.updated", {
297
- "module_id": mid,
298
- "timestamp": datetime.now(timezone.utc).isoformat(),
299
- "action": "register",
300
- }, source=self.kernel_server.module_id)
301
- print(f"[kernel] registry.updated published for {mid}")
302
- else:
303
- print(f"[kernel] {mid} re-registered with no changes, skipping registry.updated")
304
-
305
- # When Launcher registers, Kernel publishes its own module.ready
306
- if mid == "launcher" and self.kernel_server:
307
- self.kernel_server.publish_ready()
308
- print(f"[kernel] launcher registered → kernel module.ready published")
309
-
310
- return result
311
-
312
- async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
313
- mid = params.get("module_id")
314
- if not mid:
315
- raise ValueError("module_id required")
316
- if caller_id != "launcher" and caller_id != mid:
317
- raise PermissionError(f"Module '{caller_id}' cannot deregister '{mid}'")
318
-
319
- self.registry.deregister_module(mid)
320
- self.event_hub.publish_internal("module.unregistered", {"module_id": mid}, source=self.kernel_server.module_id)
321
-
322
- # Publish registry.updated event for cache invalidation
323
- from datetime import datetime, timezone
324
- self.event_hub.publish_internal("registry.updated", {
325
- "module_id": mid,
326
- "timestamp": datetime.now(timezone.utc).isoformat(),
327
- "action": "deregister",
328
- }, source=self.kernel_server.module_id)
329
-
330
- return {}
331
-
332
- async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
333
- field = params.get("field")
334
- module = params.get("module")
335
- value = params.get("value")
336
-
337
- print(f"[kernel] registry.lookup called by {caller_id}: field={field}, module={module}, value={value}")
338
-
339
- results = self.registry.lookup(
340
- field=field,
341
- module=module,
342
- value=value,
343
- )
344
-
345
- print(f"[kernel] registry.lookup results: {len(results)} matches")
346
- for r in results:
347
- print(f"[kernel] - {r['module']}.{r['field']} = {r['value']}")
348
-
349
- return {
350
- "results": results,
351
- "last_update_time": self.registry.last_update_time
352
- }
353
-
354
- async def _registry_get(self, caller_id: str, params: dict) -> dict:
355
- path = params.get("path", "")
356
- if not path:
357
- raise ValueError("path required")
358
- val, found = self.registry.get_by_path(path)
359
- if not found:
360
- raise KeyError(f"Path not found: {path}")
361
- return {"value": val}
362
-
363
- async def _registry_verify(self, caller_id: str, params: dict) -> dict:
364
- token = params.get("token", "")
365
- module_id = self.registry.verify_token(token)
366
- if not module_id:
367
- raise PermissionError("Invalid token")
368
- return {"module_id": module_id}
369
-
370
- # ── Builtin handlers: event.* ──
371
-
372
- async def _event_publish(self, caller_id: str, params: dict) -> dict:
373
- event_id = params.get("event_id", "")
374
- event_type = params.get("event", "")
375
- data = params.get("data")
376
- echo = params.get("echo", False)
377
-
378
- # When a module publishes module.ready, update its status in registry
379
- if event_type == "module.ready":
380
- mid = (data or {}).get("module_id", caller_id)
381
- print(f"[kernel] DEBUG: 收到 module.ready 事件,module_id={mid}, caller_id={caller_id}")
382
- self.registry.set_ready(mid)
383
- print(f"[kernel] DEBUG: 已调用 set_ready({mid})")
384
-
385
- return self.event_hub.publish_event(caller_id, event_id, event_type, data, echo)
386
-
387
- async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
388
- events = params.get("events", [])
389
- if not isinstance(events, list) or not events:
390
- raise ValueError("events must be a non-empty list")
391
- self.event_hub.handle_subscribe(caller_id, events)
392
- return {}
393
-
394
- async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
395
- events = params.get("events", [])
396
- if not isinstance(events, list) or not events:
397
- raise ValueError("events must be a non-empty list")
398
- return self.event_hub.handle_unsubscribe(caller_id, events)
399
-
400
- # ── Builtin handlers: kernel.* ──
401
-
402
- async def _kernel_ping(self, caller_id: str, params: dict) -> dict:
403
- return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
404
-
405
- async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
406
- event_stats = self.event_hub.get_stats()
407
- return {
408
- **event_stats,
409
- "rpc": {
410
- "total": self._rpc_total,
411
- "builtin": self._rpc_builtin,
412
- "forwarded": self._rpc_forwarded,
413
- "errors": self._rpc_errors
414
- }
415
- }
416
-
417
- async def _kernel_health(self, caller_id: str, params: dict) -> dict:
418
- eh_health = self.event_hub.get_health()
419
- return {
420
- "status": "healthy",
421
- "module_count": len(self.registry.modules),
422
- "online_count": sum(
423
- 1 for m in self.registry.modules.values()
424
- if m.get("status") in ("registered", "ready")
425
- ),
426
- "event_stats": eh_health.get("details", {}),
427
- }
428
-
429
- async def _kernel_latencies(self, caller_id: str, params: dict) -> dict:
430
- """Get ping/pong latencies for all connected modules.
431
-
432
- Returns:
433
- {
434
- "latencies": {
435
- "module1": {
436
- "outbound": 12.34, # ms, Kernel → module
437
- "inbound": 23.45, # ms, module → Kernel
438
- "status": "ok" | "timeout" | "never"
439
- },
440
- ...
441
- }
442
- }
443
- """
444
- result = {}
445
- for module_id in self.kernel_server.connections.keys():
446
- latency_data = self.kernel_server._pong_latencies.get(module_id, {})
447
- status = self.kernel_server._pong_status.get(module_id, "never")
448
-
449
- result[module_id] = {
450
- "outbound": latency_data.get("outbound"),
451
- "inbound": latency_data.get("inbound"),
452
- "status": status,
453
- }
454
-
455
- return {"latencies": result}
456
-
457
- async def _kernel_generate_tokens(self, caller_id: str, params: dict) -> dict:
458
- """Generate tokens for a list of module names.
459
-
460
- Args:
461
- params: {"modules": ["mod1", "mod2", ...]}
462
-
463
- Returns:
464
- {"tokens": {"mod1": "token1", "mod2": "token2", ...}}
465
- """
466
- # Only Launcher may request token generation
467
- if caller_id != "launcher":
468
- raise PermissionError("Only Launcher may generate tokens")
469
-
470
- modules = params.get("modules", [])
471
- if not isinstance(modules, list):
472
- raise ValueError("modules must be a list")
473
-
474
- import secrets
475
- tokens = {}
476
- for module_name in modules:
477
- tokens[module_name] = secrets.token_hex(32)
478
-
479
- # Register tokens in registry
480
- self.registry.register_tokens(tokens)
481
-
482
- return {"tokens": tokens}
483
-
484
- async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
485
- # Only Launcher may register tokens
486
- if caller_id != "launcher":
487
- raise PermissionError("Only Launcher may register tokens")
488
- self.registry.register_tokens(params)
489
- return {}
490
-
491
-
1
+ """
2
+ JSON-RPC 2.0 message dispatcher for Kernel.
3
+ Routes builtin methods (registry.*, event.*, kernel.*) and forwards
4
+ cross-module RPC requests with timeout tracking.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import time
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+
14
+ from starlette.websockets import WebSocket
15
+
16
+ from .registry_store import RegistryStore
17
+ from .event_hub import EventHub
18
+
19
+
20
+ # ── JSON-RPC 2.0 error codes ──
21
+
22
+ PARSE_ERROR = -32700
23
+ INVALID_REQUEST = -32600
24
+ METHOD_NOT_FOUND = -32601
25
+ INVALID_PARAMS = -32602
26
+ INTERNAL_ERROR = -32603
27
+
28
+ # Kite custom error codes
29
+ MODULE_OFFLINE = -32001
30
+ RPC_TIMEOUT = -32002
31
+ AUTH_FAILED = -32003
32
+ PERMISSION_DENIED = -32004
33
+ DUPLICATE_EVENT = -32005
34
+ MODULE_DEGRADED = -32006
35
+ TRACE_DEPTH_EXCEEDED = -32007
36
+
37
+ DEFAULT_FORWARD_TIMEOUT = 5.0 # seconds
38
+
39
+ # ── Trace 链路追踪配置 ──
40
+ TRACE_CACHE_TTL = 300 # 5 分钟
41
+ TRACE_CACHE_MAX = 1000 # 最多保留 1000 条 trace
42
+ TRACE_MAX_DEPTH = 20 # 最大调用深度
43
+
44
+
45
+ @dataclass
46
+ class TraceSpan:
47
+ """一次 RPC 调用的追踪记录。"""
48
+ rpc_id: str
49
+ trace_id: str
50
+ caller: str
51
+ target: str
52
+ method: str
53
+ duration: float # 毫秒
54
+ status: str # success | error | timeout | offline
55
+ error: str | None = None
56
+ parent_rpc_id: str | None = None
57
+ depth: int = 1
58
+ timestamp: str = ""
59
+
60
+
61
+ @dataclass
62
+ class PendingForward:
63
+ """Tracks a forwarded cross-module RPC awaiting response."""
64
+ caller_ws: WebSocket
65
+ original_id: str
66
+ caller_id: str
67
+ target_id: str
68
+ method: str # 完整方法名(如 "watchdog.get_status")
69
+ params: dict # 调用参数
70
+ trace_id: str = "" # 链路追踪 ID
71
+ parent_rpc_id: str | None = None # 上游 rpc_id
72
+ depth: int = 1 # 调用深度
73
+ created_at: float = field(default_factory=time.time)
74
+ timeout_handle: asyncio.TimerHandle | None = None
75
+
76
+
77
+ def _result_msg(msg_id: str, result: dict) -> str:
78
+ return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result})
79
+
80
+
81
+ def _error_msg(msg_id: str, code: int, message: str, data: dict = None) -> str:
82
+ err = {"code": code, "message": message}
83
+ if data:
84
+ err["data"] = data
85
+ return json.dumps({"jsonrpc": "2.0", "id": msg_id, "error": err})
86
+
87
+
88
+ class RpcRouter:
89
+ """JSON-RPC 2.0 message dispatcher.
90
+
91
+ Handles:
92
+ - 13 builtin methods (registry.*, event.*, kernel.*)
93
+ - Cross-module RPC forwarding with timeout
94
+ - RPC response matching for forwarded calls
95
+ """
96
+
97
+ def __init__(self, registry: RegistryStore, event_hub: EventHub,
98
+ connections: dict[str, WebSocket], kernel_server):
99
+ self.registry = registry
100
+ self.event_hub = event_hub
101
+ self.connections = connections
102
+ self.kernel_server = kernel_server # Direct reference to KernelServer
103
+
104
+ # RPC 统计计数器
105
+ self._rpc_total = 0 # RPC 调用次数
106
+ self._rpc_builtin = 0 # 内置方法调用次数
107
+ self._rpc_forwarded = 0 # 转发调用次数
108
+ self._rpc_errors = 0 # 错误次数
109
+
110
+ # ── 链路追踪内存缓存 ──
111
+ # trace_id -> {"spans": [TraceSpan], "created_at": float, "origin": str}
112
+ self._trace_cache: dict[str, dict] = {}
113
+
114
+ # Builtin method dispatch table
115
+ self.methods: dict[str, callable] = {
116
+ "registry.register": self._registry_register,
117
+ "registry.deregister": self._registry_deregister,
118
+ "registry.lookup": self._registry_lookup,
119
+ "registry.get": self._registry_get,
120
+ "registry.verify": self._registry_verify,
121
+ "event.publish": self._event_publish,
122
+ "event.subscribe": self._event_subscribe,
123
+ "event.unsubscribe": self._event_unsubscribe,
124
+ "kernel.ping": self._kernel_ping,
125
+ "kernel.stats": self._kernel_stats,
126
+ "kernel.health": self._kernel_health,
127
+ "kernel.latencies": self._kernel_latencies,
128
+ "kernel.generate_tokens": self._kernel_generate_tokens,
129
+ "kernel.register_tokens": self._kernel_register_tokens,
130
+ "kernel.get_module_events": self._kernel_get_module_events,
131
+ "kernel.get_registry_stats": self._kernel_get_registry_stats,
132
+ "kernel.report_degraded": self._kernel_report_degraded,
133
+ "kernel.report_recovered": self._kernel_report_recovered,
134
+ "kernel.set_ordering_groups": self._kernel_set_ordering_groups,
135
+ "kernel.connection_status": self._kernel_connection_status,
136
+ "kernel.add_connection": self._kernel_add_connection,
137
+ "kernel.flush_init": self._kernel_flush_init,
138
+ "kernel.instance_pressure": self._kernel_instance_pressure,
139
+ "kernel.update_module_config": self._kernel_update_module_config,
140
+ "kernel.set_scaling": self._kernel_set_scaling,
141
+ "trace.query": self._trace_query,
142
+ "trace.list": self._trace_list,
143
+ }
144
+
145
+ # Pending cross-module forwards: internal_id -> PendingForward
146
+ self._pending: dict[str, PendingForward] = {}
147
+
148
+ # ── Main dispatch ──
149
+
150
+ async def dispatch(self, caller_id: str, ws: WebSocket, msg: dict):
151
+ """Route a parsed JSON-RPC message to builtin handler or cross-module forward."""
152
+ method = msg.get("method", "")
153
+ msg_id = msg.get("id")
154
+
155
+ # 统计:总调用次数
156
+ self._rpc_total += 1
157
+
158
+ # JSON-RPC Notification (no id) — currently not handled from clients
159
+ if msg_id is None:
160
+ return
161
+
162
+ params = msg.get("params") or {}
163
+
164
+ # Builtin method
165
+ handler = self.methods.get(method)
166
+ if handler:
167
+ # 统计:内置方法调用
168
+ self._rpc_builtin += 1
169
+ try:
170
+ result = await handler(caller_id, params)
171
+ except Exception as e:
172
+ # 统计:错误次数
173
+ self._rpc_errors += 1
174
+ print(f"[kernel] RPC handler error ({method}): {e}")
175
+ try:
176
+ await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
177
+ except Exception:
178
+ # Can't send error response, connection closed
179
+ pass
180
+ return
181
+
182
+ # Send result (may fail if connection closed during shutdown)
183
+ try:
184
+ await ws.send_text(_result_msg(msg_id, result))
185
+ except Exception as e:
186
+ # Connection closed during shutdown — this is normal, exit silently
187
+ pass
188
+ return
189
+
190
+ # Cross-module forward: method prefix is target module_id
191
+ dot_idx = method.find(".")
192
+ if dot_idx > 0:
193
+ target = method[:dot_idx]
194
+
195
+ # Resolve target to an available instance_key
196
+ # For multi-instance modules, pick one instance randomly
197
+ target_instance = self._resolve_rpc_target(target)
198
+
199
+ if target_instance:
200
+ # Check if target module is ready (state machine protection)
201
+ if not self.registry.is_ready(target):
202
+ # 统计:错误次数
203
+ self._rpc_errors += 1
204
+ state = self.registry.get_state(target)
205
+ await ws.send_text(_error_msg(
206
+ msg_id, MODULE_OFFLINE,
207
+ f"Module not ready: {target} (status: {state})",
208
+ data={"module_status": state}))
209
+ return
210
+ # 统计:转发调用
211
+ self._rpc_forwarded += 1
212
+ await self._forward(caller_id, ws, msg_id, target_instance, method, params,
213
+ target_module=target)
214
+ return
215
+ # Target not connected — check if registered but offline
216
+ if target in self.registry.modules:
217
+ # 统计:错误次数
218
+ self._rpc_errors += 1
219
+ await ws.send_text(_error_msg(
220
+ msg_id, MODULE_OFFLINE, f"Module offline: {target}",
221
+ data={"module_status": "offline"}))
222
+ return
223
+
224
+ # Method not found
225
+ # 统计:错误次数
226
+ self._rpc_errors += 1
227
+ await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
228
+
229
+ async def handle_response(self, module_id: str, msg: dict):
230
+ """Handle an RPC response from a module (matches pending forwards)."""
231
+ msg_id = msg.get("id")
232
+ if not msg_id:
233
+ return
234
+
235
+ pending = self._pending.pop(msg_id, None)
236
+ if not pending:
237
+ return # orphan response, ignore
238
+
239
+ # Cancel timeout
240
+ if pending.timeout_handle:
241
+ pending.timeout_handle.cancel()
242
+
243
+ # 计算耗时并发出 RPC 调用完成事件
244
+ duration = (time.time() - pending.created_at) * 1000
245
+ if "result" in msg:
246
+ self._publish_rpc_event(msg_id, pending, duration, "success", result=msg["result"])
247
+ elif "error" in msg:
248
+ error_msg = msg["error"].get("message", str(msg["error"])) if isinstance(msg["error"], dict) else str(msg["error"])
249
+ self._publish_rpc_event(msg_id, pending, duration, "error", error=error_msg)
250
+ else:
251
+ self._publish_rpc_event(msg_id, pending, duration, "success", result=None)
252
+
253
+ # Forward response to original caller with original ID
254
+ response = {"jsonrpc": "2.0", "id": pending.original_id}
255
+ if "result" in msg:
256
+ response["result"] = msg["result"]
257
+ elif "error" in msg:
258
+ response["error"] = msg["error"]
259
+ else:
260
+ response["result"] = None
261
+
262
+ try:
263
+ await pending.caller_ws.send_text(json.dumps(response))
264
+ except Exception:
265
+ # caller 原始连接失败 尝试同模块其他 slot
266
+ caller_slots = self.connections.get(pending.caller_id, {})
267
+ sent = False
268
+ for ws in caller_slots.values():
269
+ if ws is not pending.caller_ws:
270
+ try:
271
+ await ws.send_text(json.dumps(response))
272
+ sent = True
273
+ break
274
+ except Exception:
275
+ continue
276
+ if not sent:
277
+ pass # caller 完全断开
278
+
279
+ def _resolve_rpc_target(self, target_module: str) -> str | None:
280
+ """Resolve a module_id to an available instance_key for RPC forwarding.
281
+
282
+ Strategy: random selection across all connected instances.
283
+ Returns None if no instance is connected.
284
+ """
285
+ import random
286
+ # Get all active instances from EventHub
287
+ inst_keys = self.event_hub.get_instance_keys(target_module)
288
+ if not inst_keys:
289
+ return None
290
+ # Filter to instances that actually have connections
291
+ connected = [k for k in inst_keys if k in self.connections and self.connections[k]]
292
+ if not connected:
293
+ return None
294
+ if len(connected) == 1:
295
+ return connected[0]
296
+ return random.choice(connected)
297
+
298
+ # ── Cross-module forwarding ──
299
+
300
+ async def _forward(self, caller_id: str, caller_ws: WebSocket,
301
+ original_id: str, target: str, method: str, params: dict,
302
+ target_module: str = ""):
303
+ """Forward RPC request to target module with timeout tracking.
304
+
305
+ Args:
306
+ target: instance_key to route to (e.g. 'assistant' or 'assistant#2')
307
+ target_module: base module_id for registry lookups. If empty, derived from target.
308
+ """
309
+ if not target_module:
310
+ from .event_hub import parse_instance_key
311
+ target_module = parse_instance_key(target)[0]
312
+
313
+ # 检查目标模块状态(用 base module_id
314
+ target_state = self.registry.get_state(target_module)
315
+ if target_state == "degraded":
316
+ # 降级状态:检查调用的方法是否在 affected 列表中
317
+ degraded_info = self.registry.get_degraded_info(target_module)
318
+ if degraded_info:
319
+ actual_method = method[len(target_module) + 1:]
320
+ affected = degraded_info.get("affected", [])
321
+ if actual_method in affected:
322
+ error_response = {
323
+ "jsonrpc": "2.0",
324
+ "id": original_id,
325
+ "error": {
326
+ "code": MODULE_DEGRADED,
327
+ "message": f"Module '{target}' is degraded, method '{actual_method}' is affected",
328
+ "data": {
329
+ "module_status": "degraded",
330
+ "level": degraded_info.get("level"),
331
+ "reason": degraded_info.get("reason"),
332
+ "retry_after_ms": degraded_info.get("estimated_recovery_ms"),
333
+ }
334
+ }
335
+ }
336
+ try:
337
+ await caller_ws.send_text(json.dumps(error_response))
338
+ except Exception:
339
+ pass
340
+ return
341
+ elif target_state != "ready":
342
+ # 目标模块不在 ready 状态,立即返回错误
343
+ error_response = {
344
+ "jsonrpc": "2.0",
345
+ "id": original_id,
346
+ "error": {
347
+ "code": MODULE_OFFLINE,
348
+ "message": f"Module '{target}' is not ready (state: {target_state})",
349
+ "data": {
350
+ "module_status": target_state,
351
+ }
352
+ }
353
+ }
354
+ try:
355
+ await caller_ws.send_text(json.dumps(error_response))
356
+ except Exception:
357
+ pass
358
+ return
359
+
360
+ internal_id = f"fwd-{uuid.uuid4().hex[:12]}"
361
+
362
+ # Extract timeout from params (optional _timeout field)
363
+ timeout = DEFAULT_FORWARD_TIMEOUT
364
+ if isinstance(params, dict) and "_timeout" in params:
365
+ try:
366
+ timeout = float(params.pop("_timeout"))
367
+ except (ValueError, TypeError):
368
+ pass
369
+
370
+ # Strip target prefix from method for the forwarded request
371
+ actual_method = method[len(target_module) + 1:] # e.g. "watchdog.get_status" -> "get_status"
372
+
373
+ # ── 链路追踪:生成或继承 _trace ──
374
+ if not isinstance(params, dict):
375
+ params = {}
376
+ upstream_trace = params.pop("_trace", None)
377
+
378
+ if upstream_trace and isinstance(upstream_trace, dict):
379
+ # 继承上游 trace
380
+ trace_id = upstream_trace.get("trace_id", str(uuid.uuid4()))
381
+ parent_rpc_id = upstream_trace.get("parent_rpc_id")
382
+ origin = upstream_trace.get("origin", caller_id)
383
+ depth = upstream_trace.get("depth", 0) + 1
384
+ else:
385
+ # 新链路
386
+ trace_id = str(uuid.uuid4())
387
+ parent_rpc_id = None
388
+ origin = caller_id
389
+ depth = 1
390
+
391
+ # 深度保护
392
+ if depth > TRACE_MAX_DEPTH:
393
+ await caller_ws.send_text(_error_msg(
394
+ original_id, TRACE_DEPTH_EXCEEDED,
395
+ f"Trace depth exceeded maximum ({TRACE_MAX_DEPTH}), possible recursive call chain"))
396
+ return
397
+
398
+ # 保存原始参数(用于事件发布,不包含 _caller_id 和 _trace)
399
+ original_params = dict(params)
400
+
401
+ # Inject caller_id into params for permission checking
402
+ params["_caller_id"] = caller_id
403
+
404
+ # 注入 _trace 到转发的 params(模块可读取也可忽略)
405
+ params["_trace"] = {
406
+ "trace_id": trace_id,
407
+ "parent_rpc_id": internal_id, # 当前跳的 rpc_id 作为下游的 parent
408
+ "origin": origin,
409
+ "depth": depth,
410
+ }
411
+
412
+ # Record pending forward
413
+ loop = asyncio.get_event_loop()
414
+ pending = PendingForward(
415
+ caller_ws=caller_ws,
416
+ original_id=original_id,
417
+ caller_id=caller_id,
418
+ target_id=target,
419
+ method=method,
420
+ params=original_params,
421
+ trace_id=trace_id,
422
+ parent_rpc_id=parent_rpc_id,
423
+ depth=depth,
424
+ )
425
+ pending.timeout_handle = loop.call_later(
426
+ timeout, lambda iid=internal_id: asyncio.ensure_future(self._handle_timeout(iid))
427
+ )
428
+ self._pending[internal_id] = pending
429
+
430
+ # Send to target(从多 slot 中取任意一个可用连接)
431
+ fwd_msg = json.dumps({
432
+ "jsonrpc": "2.0",
433
+ "id": internal_id,
434
+ "method": actual_method,
435
+ "params": params or {},
436
+ })
437
+ target_slots = self.connections.get(target, {})
438
+ target_ws = next(iter(target_slots.values()), None) if target_slots else None
439
+ if target_ws:
440
+ try:
441
+ await target_ws.send_text(fwd_msg)
442
+ except Exception as e:
443
+ # Target send failed — clean up and error to caller
444
+ self._pending.pop(internal_id, None)
445
+ if pending.timeout_handle:
446
+ pending.timeout_handle.cancel()
447
+
448
+ # 发出 RPC 调用失败事件
449
+ duration = (time.time() - pending.created_at) * 1000
450
+ self._publish_rpc_event(internal_id, pending, duration, "error", error=str(e))
451
+
452
+ await caller_ws.send_text(_error_msg(
453
+ original_id, MODULE_OFFLINE, f"Failed to reach module: {target}"))
454
+ else:
455
+ # Target disconnected between check and send
456
+ self._pending.pop(internal_id, None)
457
+ if pending.timeout_handle:
458
+ pending.timeout_handle.cancel()
459
+
460
+ # 发出 RPC 调用失败事件
461
+ duration = (time.time() - pending.created_at) * 1000
462
+ self._publish_rpc_event(internal_id, pending, duration, "offline", error=f"Module offline: {target}")
463
+
464
+ await caller_ws.send_text(_error_msg(
465
+ original_id, MODULE_OFFLINE, f"Module offline: {target}"))
466
+
467
+ async def _handle_timeout(self, internal_id: str):
468
+ """Called when a forwarded RPC times out."""
469
+ pending = self._pending.pop(internal_id, None)
470
+ if not pending:
471
+ return
472
+
473
+ # 发出 RPC 调用超时事件
474
+ duration = (time.time() - pending.created_at) * 1000
475
+ self._publish_rpc_event(internal_id, pending, duration, "timeout", error=f"RPC timeout waiting for {pending.target_id}")
476
+
477
+ try:
478
+ await pending.caller_ws.send_text(_error_msg(
479
+ pending.original_id, RPC_TIMEOUT,
480
+ f"RPC timeout waiting for {pending.target_id}"))
481
+ except Exception:
482
+ pass
483
+
484
+ # ── Builtin handlers: registry.* ──
485
+
486
+ async def _registry_register(self, caller_id: str, params: dict) -> dict:
487
+ mid = params.get("module_id")
488
+ if not mid:
489
+ raise ValueError("module_id required")
490
+ # Permission: only Launcher or the module itself (including any instance of the module)
491
+ from .event_hub import parse_instance_key
492
+ caller_base = parse_instance_key(caller_id)[0]
493
+ if caller_id != "launcher" and caller_base != mid:
494
+ raise PermissionError(f"Module '{caller_id}' cannot register as '{mid}'")
495
+
496
+ # 幂等:同一 module_id 已注册(多实例的 #2~#N 会走到这里),直接返回
497
+ existing = self.registry.modules.get(mid)
498
+ if existing and existing.get("status") in ("registered", "ready"):
499
+ return {"module_id": mid, "changed": False}
500
+
501
+ print(f"[kernel] registry.register called by {caller_id} for module {mid}")
502
+
503
+ # Validate RPC method names: "." is reserved for cross-module routing
504
+ rpc_tool = params.get("tools", {}).get("rpc", {})
505
+ if isinstance(rpc_tool, dict):
506
+ bad_methods = []
507
+ for _cat, methods in rpc_tool.items():
508
+ if isinstance(methods, dict):
509
+ for key, spec in methods.items():
510
+ if "." in key:
511
+ bad_methods.append(key)
512
+ elif isinstance(spec, dict) and "." in spec.get("method", ""):
513
+ bad_methods.append(spec["method"])
514
+ if bad_methods:
515
+ raise ValueError(
516
+ f"RPC method names must not contain '.': {bad_methods}. "
517
+ f"Use snake_case instead (e.g. 'llm_sessions_list' not 'llm_sessions.list'). "
518
+ f"The '.' separator is reserved for cross-module routing.")
519
+
520
+ result = self.registry.register_module(params)
521
+ self.event_hub.publish_internal("module.registered", {"module_id": mid}, source=self.kernel_server.module_id)
522
+
523
+ # Only publish registry.updated if content actually changed
524
+ if result.get("changed", True):
525
+ from datetime import datetime, timezone
526
+ self.event_hub.publish_internal("registry.updated", {
527
+ "module_id": mid,
528
+ "timestamp": datetime.now(timezone.utc).isoformat(),
529
+ "action": "register",
530
+ }, source=self.kernel_server.module_id)
531
+ print(f"[kernel] registry.updated published for {mid}")
532
+ else:
533
+ print(f"[kernel] {mid} re-registered with no changes, skipping registry.updated")
534
+
535
+ # When Launcher registers, Kernel publishes its own module.ready
536
+ if mid == "launcher" and self.kernel_server:
537
+ self.kernel_server.publish_ready()
538
+ print(f"[kernel] launcher registered → kernel module.ready published")
539
+
540
+ return result
541
+
542
+ async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
543
+ mid = params.get("module_id")
544
+ if not mid:
545
+ raise ValueError("module_id required")
546
+ if caller_id != "launcher" and caller_id != mid:
547
+ raise PermissionError(f"Module '{caller_id}' cannot deregister '{mid}'")
548
+
549
+ self.registry.deregister_module(mid)
550
+ self.event_hub.publish_internal("module.unregistered", {"module_id": mid}, source=self.kernel_server.module_id)
551
+
552
+ # Publish registry.updated event for cache invalidation
553
+ from datetime import datetime, timezone
554
+ self.event_hub.publish_internal("registry.updated", {
555
+ "module_id": mid,
556
+ "timestamp": datetime.now(timezone.utc).isoformat(),
557
+ "action": "deregister",
558
+ }, source=self.kernel_server.module_id)
559
+
560
+ return {}
561
+
562
+ async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
563
+ field = params.get("field")
564
+ module = params.get("module")
565
+ value = params.get("value")
566
+
567
+ print(f"[kernel] registry.lookup called by {caller_id}: field={field}, module={module}, value={value}")
568
+
569
+ results = self.registry.lookup(
570
+ field=field,
571
+ module=module,
572
+ value=value,
573
+ )
574
+
575
+ print(f"[kernel] registry.lookup results: {len(results)} matches")
576
+ for r in results:
577
+ print(f"[kernel] - {r['module']}.{r['field']} = {r['value']}")
578
+
579
+ return {
580
+ "results": results,
581
+ "last_update_time": self.registry.last_update_time
582
+ }
583
+
584
+ async def _registry_get(self, caller_id: str, params: dict) -> dict:
585
+ path = params.get("path", "")
586
+ if not path:
587
+ raise ValueError("path required")
588
+ val, found = self.registry.get_by_path(path)
589
+ if not found:
590
+ raise KeyError(f"Path not found: {path}")
591
+ return {"value": val}
592
+
593
+ async def _registry_verify(self, caller_id: str, params: dict) -> dict:
594
+ token = params.get("token", "")
595
+ module_id = self.registry.verify_token(token)
596
+ if not module_id:
597
+ raise PermissionError("Invalid token")
598
+ return {"module_id": module_id}
599
+
600
+ # ── Builtin handlers: event.* ──
601
+
602
+ async def _event_publish(self, caller_id: str, params: dict) -> dict:
603
+ event_id = params.get("event_id", "")
604
+ event_type = params.get("event", "")
605
+ data = params.get("data")
606
+ echo = params.get("echo", False)
607
+ droppable = params.get("droppable", False)
608
+
609
+ # 提取并校验 priority
610
+ priority = params.get("priority", "normal")
611
+ if priority not in ("normal", "high", "critical"):
612
+ raise ValueError(f"Invalid priority: {priority}, must be 'normal', 'high', or 'critical'")
613
+
614
+ # 提取节流参数
615
+ throttle_key = params.get("throttle_key", "")
616
+ throttle_ms = params.get("throttle_ms", 0)
617
+ if not isinstance(throttle_ms, (int, float)):
618
+ throttle_ms = 0
619
+
620
+ # When a module publishes module.ready, update its status in registry
621
+ # and sync queue_elastic capability to EventHub
622
+ if event_type == "module.ready":
623
+ # caller_id is instance_key (e.g. 'assistant' or 'assistant#2')
624
+ mid = (data or {}).get("module_id", caller_id)
625
+ self.registry.set_ready(mid, data)
626
+ # Sync elastic capability to EventHub (per instance_key for queue management)
627
+ is_elastic = (data or {}).get("queue_elastic", False)
628
+ self.event_hub.set_module_elastic(caller_id, bool(is_elastic))
629
+
630
+ # 多连接/多实例:从配置文件读取并统一应用
631
+ asyncio.ensure_future(self._apply_module_config(mid, caller_id))
632
+
633
+ # When a module reports queue pressure, store it per instance
634
+ elif event_type == "module.queue_pressure":
635
+ self.event_hub.report_instance_pressure(caller_id, data or {})
636
+
637
+ return self.event_hub.publish_event(caller_id, event_id, event_type, data, echo,
638
+ droppable=droppable, priority=priority,
639
+ throttle_key=throttle_key,
640
+ throttle_ms=int(throttle_ms))
641
+
642
+ async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
643
+ events = params.get("events", [])
644
+ if not isinstance(events, list) or not events:
645
+ raise ValueError("events must be a non-empty list")
646
+ self.event_hub.handle_subscribe(caller_id, events)
647
+ # Update init time for this instance (caller_id may be instance_key like 'assistant#2')
648
+ self.registry.update_init_time(caller_id)
649
+ return {}
650
+
651
+ async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
652
+ events = params.get("events", [])
653
+ if not isinstance(events, list) or not events:
654
+ raise ValueError("events must be a non-empty list")
655
+ return self.event_hub.handle_unsubscribe(caller_id, events)
656
+
657
+ # ── Builtin handlers: kernel.* ──
658
+
659
+ async def _kernel_ping(self, caller_id: str, params: dict) -> dict:
660
+ return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
661
+
662
+ async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
663
+ event_stats = self.event_hub.get_stats()
664
+ return {
665
+ **event_stats,
666
+ "rpc": {
667
+ "total": self._rpc_total,
668
+ "builtin": self._rpc_builtin,
669
+ "forwarded": self._rpc_forwarded,
670
+ "errors": self._rpc_errors
671
+ }
672
+ }
673
+
674
+ async def _kernel_health(self, caller_id: str, params: dict) -> dict:
675
+ eh_health = self.event_hub.get_health()
676
+ return {
677
+ "status": "healthy",
678
+ "module_count": len(self.registry.modules),
679
+ "online_count": sum(
680
+ 1 for m in self.registry.modules.values()
681
+ if m.get("status") in ("registered", "ready", "degraded")
682
+ ),
683
+ "degraded_count": sum(
684
+ 1 for m in self.registry.modules.values()
685
+ if m.get("status") == "degraded"
686
+ ),
687
+ "event_stats": eh_health.get("details", {}),
688
+ }
689
+
690
+ async def _kernel_latencies(self, caller_id: str, params: dict) -> dict:
691
+ """Get ping/pong latencies for all connected modules.
692
+
693
+ Returns:
694
+ {
695
+ "latencies": {
696
+ "module1": {
697
+ "outbound": 12.34, # ms, Kernel → module
698
+ "inbound": 23.45, # ms, module → Kernel
699
+ "status": "ok" | "timeout" | "never"
700
+ },
701
+ ...
702
+ }
703
+ }
704
+ """
705
+ import time
706
+
707
+ # 更新最后调用时间,触发 ping 间隔调整
708
+ current_time = time.time()
709
+ old_time = self.kernel_server._last_latencies_call
710
+ self.kernel_server._last_latencies_call = current_time
711
+
712
+ # 如果是首次调用或距离上次调用超过 30 秒,触发间隔变化
713
+ if old_time == 0 or (current_time - old_time) > 30:
714
+ # 通知 ping 循环间隔可能需要调整
715
+ self.kernel_server._ping_interval_event.set()
716
+
717
+ result = {}
718
+ for module_id in [mid for mid, slots in self.kernel_server.connections.items() if slots]:
719
+ latency_data = self.kernel_server._pong_latencies.get(module_id, {})
720
+ status = self.kernel_server._pong_status.get(module_id, "never")
721
+
722
+ result[module_id] = {
723
+ "outbound": latency_data.get("outbound"),
724
+ "inbound": latency_data.get("inbound"),
725
+ "status": status,
726
+ "last_update": latency_data.get("last_update"),
727
+ }
728
+
729
+ # 添加 kernel 自己的数据
730
+ if "kernel" in self.kernel_server._pong_latencies:
731
+ kernel_data = self.kernel_server._pong_latencies["kernel"]
732
+ result["kernel"] = {
733
+ "outbound": kernel_data.get("outbound"),
734
+ "inbound": kernel_data.get("inbound"),
735
+ "status": self.kernel_server._pong_status.get("kernel", "never"),
736
+ "last_update": kernel_data.get("last_update"),
737
+ }
738
+
739
+ return {"latencies": result}
740
+
741
+ async def _kernel_generate_tokens(self, caller_id: str, params: dict) -> dict:
742
+ """Generate tokens for a list of module names.
743
+
744
+ Args:
745
+ params: {"modules": ["mod1", "mod2", ...]}
746
+
747
+ Returns:
748
+ {"tokens": {"mod1": "token1", "mod2": "token2", ...}}
749
+ """
750
+ # Only Launcher may request token generation
751
+ if caller_id != "launcher":
752
+ raise PermissionError("Only Launcher may generate tokens")
753
+
754
+ modules = params.get("modules", [])
755
+ if not isinstance(modules, list):
756
+ raise ValueError("modules must be a list")
757
+
758
+ import secrets
759
+ tokens = {}
760
+ for module_name in modules:
761
+ tokens[module_name] = secrets.token_hex(32)
762
+
763
+ # Register tokens in registry
764
+ self.registry.register_tokens(tokens)
765
+
766
+ return {"tokens": tokens}
767
+
768
+ async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
769
+ """注册 token 映射(运行时动态注册,如启动新模块、浏览器连接)。"""
770
+ if caller_id != "launcher":
771
+ raise PermissionError("Only Launcher may register tokens")
772
+ remote = bool(params.pop("__remote__", False))
773
+ self.registry.register_tokens(params, remote=remote)
774
+ return {}
775
+
776
+ async def _kernel_get_module_events(self, caller_id: str, params: dict) -> dict:
777
+ """获取模块的事件订阅和发布信息(含详细信息和动态统计)"""
778
+ module_name = params.get("module_name")
779
+ if not module_name:
780
+ raise ValueError("module_name is required")
781
+
782
+ # 从订阅表查询实际订阅(subscriptions: module_id -> set of (pattern_str, pattern_tuple))
783
+ active_subs = []
784
+ module_patterns = self.event_hub.subscriptions.get(module_name, set())
785
+ for pattern_str, pattern_tuple in module_patterns:
786
+ active_subs.append(pattern_str)
787
+
788
+ # 从注册数据读取声明(events_subscribe: list, events_publish: dict)
789
+ module_data = self.registry.modules.get(module_name, {})
790
+ declared_subs = module_data.get("events_subscribe", [])
791
+ pub_map = self._flatten_event_publish(module_data.get("events_publish", {}))
792
+ declared_pubs = sorted(pub_map.keys())
793
+
794
+ # 构建每个事件的详细信息
795
+ event_details = {}
796
+
797
+ # 订阅事件的详情:找出谁发布了这个事件
798
+ all_events = set(active_subs) | set(declared_subs)
799
+ for event in all_events:
800
+ info = self.event_hub.get_event_info(event)
801
+ # 找出哪些模块声明发布此事件
802
+ publishers = self._find_publishers(event)
803
+ event_details[event] = {
804
+ "description": None,
805
+ "publishers": publishers,
806
+ "stats": info["stats"],
807
+ }
808
+
809
+ # 发布事件的详情:找出谁订阅了这个事件
810
+ for event in declared_pubs:
811
+ info = self.event_hub.get_event_info(event)
812
+ event_details[event] = {
813
+ "description": pub_map.get(event),
814
+ "subscribers": info["subscribers"],
815
+ "stats": info["stats"],
816
+ }
817
+
818
+ return {
819
+ "module_name": module_name,
820
+ "subscriptions": {
821
+ "active": sorted(active_subs),
822
+ "declared": sorted(declared_subs)
823
+ },
824
+ "publications": {
825
+ "declared": declared_pubs
826
+ },
827
+ "event_details": event_details,
828
+ }
829
+
830
+ def _find_publishers(self, event_type: str) -> list[str]:
831
+ """查找声明发布某事件的所有模块"""
832
+ result = []
833
+ for mid, data in self.registry.modules.items():
834
+ if data.get("status") not in ("registered", "ready", "degraded"):
835
+ continue
836
+ pub_map = self._flatten_event_publish(data.get("events_publish", {}))
837
+ if event_type in pub_map:
838
+ result.append(mid)
839
+ return sorted(result)
840
+
841
+ @staticmethod
842
+ def _flatten_event_publish(d: dict, prefix: str = "") -> dict[str, str | None]:
843
+ """将嵌套 events_publish dict 展平为 {事件名: description}。
844
+ 含 description 键的 dict 视为叶子节点(事件定义)。"""
845
+ result = {}
846
+ for key, value in d.items():
847
+ path = f"{prefix}.{key}" if prefix else key
848
+ if isinstance(value, dict) and "description" not in value:
849
+ result.update(RpcRouter._flatten_event_publish(value, path))
850
+ else:
851
+ desc = value.get("description") if isinstance(value, dict) else None
852
+ result[path] = desc
853
+ return result
854
+
855
+ async def _kernel_get_registry_stats(self, caller_id: str, params: dict) -> dict:
856
+ """获取注册中心统计数据(用于前端统计面板)
857
+
858
+ Returns:
859
+ {
860
+ "total_records": int, # 总注册记录数
861
+ "by_category": {
862
+ "rpc": int, # tools.rpc.* 数量
863
+ "hook": int, # tools.hook.* 数量
864
+ "api": int # tools.api.* 数量
865
+ }
866
+ }
867
+ """
868
+ total_records = 0
869
+ by_category = {
870
+ "rpc": 0,
871
+ "hook": 0,
872
+ "api": 0
873
+ }
874
+
875
+ # 遍历所有已注册模块
876
+ for module_id, module_data in self.registry.modules.items():
877
+ if module_data.get("status") not in ("registered", "ready", "degraded"):
878
+ continue
879
+
880
+ # 分别查询 RPC、Hook、API(需要查询到叶子节点)
881
+ rpc_results = self.registry.lookup(field="tools.rpc.*", module=module_id)
882
+ hook_results = self.registry.lookup(field="tools.hook.*", module=module_id)
883
+ api_results = self.registry.lookup(field="tools.api.*", module=module_id)
884
+
885
+ by_category["rpc"] += len(rpc_results)
886
+ by_category["hook"] += len(hook_results)
887
+ by_category["api"] += len(api_results)
888
+ total_records += len(rpc_results) + len(hook_results) + len(api_results)
889
+
890
+ return {
891
+ "total_records": total_records,
892
+ "by_category": by_category
893
+ }
894
+
895
+ # ── Builtin handlers: kernel.report_degraded / kernel.report_recovered ──
896
+
897
+ async def _kernel_report_degraded(self, caller_id: str, params: dict) -> dict:
898
+ """模块报告自身降级。Kernel 验证后更新 Registry 并广播 module.degraded 系统事件。
899
+ Watchdog 可代发(通过 module_id 参数指定目标模块)。
900
+
901
+ Args:
902
+ params: {
903
+ module_id: str (可选,默认为 caller_id;Watchdog 代发时指定目标模块),
904
+ level: "full" | "partial" | "slow",
905
+ reason: str,
906
+ affected: list[str] (partial 时必填),
907
+ available: list[str] (可选),
908
+ estimated_recovery_ms: int (可选),
909
+ detail: str (可选),
910
+ }
911
+ """
912
+ # 确定目标模块:默认是调用者自身,Watchdog 可代发
913
+ target_module = params.get("module_id", caller_id)
914
+ if target_module != caller_id and caller_id != "watchdog":
915
+ raise PermissionError(f"Module '{caller_id}' cannot report degradation for '{target_module}'")
916
+
917
+ level = params.get("level")
918
+ if level not in ("full", "partial", "slow"):
919
+ raise ValueError(f"Invalid degradation level: {level}, must be 'full', 'partial', or 'slow'")
920
+
921
+ reason = params.get("reason")
922
+ if not reason:
923
+ raise ValueError("reason is required")
924
+
925
+ if level == "partial" and not params.get("affected"):
926
+ raise ValueError("affected list is required when level is 'partial'")
927
+
928
+ # 更新 Registry
929
+ self.registry.set_degraded(target_module, params)
930
+
931
+ # 广播 module.degraded 系统事件
932
+ event_data = {
933
+ "module_id": target_module,
934
+ "level": level,
935
+ "reason": reason,
936
+ "affected": params.get("affected", []),
937
+ "available": params.get("available", []),
938
+ "detail": params.get("detail"),
939
+ }
940
+ if params.get("estimated_recovery_ms") is not None:
941
+ event_data["estimated_recovery_ms"] = params["estimated_recovery_ms"]
942
+
943
+ self.event_hub.publish_internal(
944
+ "module.degraded", event_data,
945
+ source=caller_id,
946
+ priority="high"
947
+ )
948
+
949
+ print(f"[kernel] module.degraded: {target_module} level={level} reason={reason} (reported by {caller_id})")
950
+ return {}
951
+
952
+ async def _kernel_report_recovered(self, caller_id: str, params: dict) -> dict:
953
+ """模块报告自身从降级恢复。Kernel 验证后更新 Registry 并广播 module.recovered 系统事件。
954
+ Watchdog 可代发(通过 module_id 参数指定目标模块)。
955
+
956
+ Args:
957
+ params: {
958
+ module_id: str (可选,默认为 caller_id),
959
+ previous_level: str (可选),
960
+ duration_ms: int (可选),
961
+ }
962
+ """
963
+ target_module = params.get("module_id", caller_id)
964
+ if target_module != caller_id and caller_id != "watchdog":
965
+ raise PermissionError(f"Module '{caller_id}' cannot report recovery for '{target_module}'")
966
+
967
+ # 获取降级信息用于事件数据
968
+ degraded_info = self.registry.get_degraded_info(target_module)
969
+ previous_level = params.get("previous_level")
970
+ duration_ms = params.get("duration_ms")
971
+
972
+ if degraded_info and not previous_level:
973
+ previous_level = degraded_info.get("level")
974
+ if degraded_info and not duration_ms:
975
+ degraded_at = degraded_info.get("degraded_at")
976
+ if degraded_at:
977
+ import time as _time
978
+ duration_ms = int((_time.time() - degraded_at) * 1000)
979
+
980
+ # 更新 Registry
981
+ self.registry.clear_degraded(target_module)
982
+
983
+ # 广播 module.recovered 系统事件
984
+ event_data = {"module_id": target_module}
985
+ if previous_level:
986
+ event_data["previous_level"] = previous_level
987
+ if duration_ms is not None:
988
+ event_data["duration_ms"] = duration_ms
989
+
990
+ self.event_hub.publish_internal(
991
+ "module.recovered", event_data,
992
+ source=caller_id,
993
+ )
994
+
995
+ print(f"[kernel] module.recovered: {target_module} (reported by {caller_id})")
996
+ return {}
997
+
998
+ # ── Builtin handlers: kernel.set_ordering_groups ──
999
+
1000
+ async def _kernel_set_ordering_groups(self, caller_id: str, params: dict) -> dict:
1001
+ """模块声明保序集合,同组事件 key 保证通过同一 slot 发送。
1002
+
1003
+ Args:
1004
+ params: {
1005
+ "groups": [
1006
+ {"name": "session", "keys": ["session.started", "session.ended"]}
1007
+ ]
1008
+ }
1009
+ """
1010
+ groups = params.get("groups", [])
1011
+ if not isinstance(groups, list):
1012
+ raise ValueError("groups must be a list")
1013
+ self.event_hub.set_ordering_groups(caller_id, groups)
1014
+ return {}
1015
+
1016
+ async def _kernel_connection_status(self, caller_id: str, params: dict) -> dict:
1017
+ """返回所有模块的连接槽位状态。"""
1018
+ modules = {}
1019
+ for module_id, slots in self.kernel_server.connections.items():
1020
+ if not slots:
1021
+ continue
1022
+ modules[module_id] = {
1023
+ "elastic": self.kernel_server._module_conn_elastic.get(module_id, False),
1024
+ "slots": {str(slot): {"connected": True} for slot in slots}
1025
+ }
1026
+ return {"modules": modules}
1027
+
1028
+ async def _kernel_add_connection(self, caller_id: str, params: dict) -> dict:
1029
+ """手动为指定模块增加一个连接 slot。
1030
+
1031
+ Params: module_id (str)
1032
+ Returns: {slot: int} — 新增的 slot 编号
1033
+ """
1034
+ module_id = params.get("module_id", "")
1035
+ if not module_id:
1036
+ raise ValueError("module_id required")
1037
+
1038
+ current_slots = self.kernel_server.connections.get(module_id, {})
1039
+ if not current_slots:
1040
+ raise RuntimeError(f"模块 {module_id} 未连接")
1041
+
1042
+ max_conns = self.kernel_server._get_max_connections(module_id)
1043
+
1044
+ # 找到下一个空闲 slot
1045
+ next_slot = None
1046
+ for s in range(1, max_conns):
1047
+ if s not in current_slots:
1048
+ next_slot = s
1049
+ break
1050
+
1051
+ if next_slot is None:
1052
+ raise RuntimeError(f"模块 {module_id} 连接已满 ({len(current_slots)}/{max_conns})")
1053
+
1054
+ self.kernel_server._send_connection_offer(module_id, [next_slot])
1055
+ return {"slot": next_slot}
1056
+
1057
+ async def _kernel_instance_pressure(self, caller_id: str, params: dict) -> dict:
1058
+ """Query instance pressure reports.
1059
+
1060
+ Params:
1061
+ module_id (optional): filter by module name. If omitted, return all.
1062
+
1063
+ Returns: {instances: {instance_key: {level, depth, capacity, updated_at}, ...}}
1064
+ """
1065
+ module_id = params.get("module_id", "")
1066
+ if module_id:
1067
+ result = self.event_hub.get_module_pressure(module_id)
1068
+ else:
1069
+ result = dict(self.event_hub._instance_pressure)
1070
+ return {"instances": result}
1071
+
1072
+ async def _kernel_update_module_config(self, caller_id: str, params: dict) -> dict:
1073
+ """热更新模块的多连接/多实例配置。
1074
+
1075
+ 从配置文件(通过 launcher.get_module_config)读取最新配置并应用。
1076
+ 前端在调用此方法前应先调用 launcher.update_module_config 写入 module.md。
1077
+
1078
+ Params:
1079
+ module_id (str): 目标模块 ID
1080
+ """
1081
+ module_id = params.get("module_id", "")
1082
+ if not module_id:
1083
+ raise RuntimeError("module_id is required")
1084
+
1085
+ if not self.registry.modules.get(module_id):
1086
+ raise RuntimeError(f"模块 {module_id} 未注册")
1087
+
1088
+ if self.registry.is_remote_module(module_id):
1089
+ raise RuntimeError(f"模块 {module_id} 是远程模块,不支持热更新配置")
1090
+
1091
+ result = await self._apply_module_config(module_id, module_id)
1092
+ return result
1093
+
1094
+ async def _kernel_set_scaling(self, caller_id: str, params: dict) -> dict:
1095
+ """全局冻结/解冻扩缩容控制面。仅 Launcher 可调用。
1096
+
1097
+ 冻结时:_scaler_tick 跳过所有扩缩容操作,多连接 recovery offer 不发送。
1098
+ 解冻时:立即触发一次 scaler tick 以补齐目标状态。
1099
+
1100
+ Params:
1101
+ enabled (bool): True=解冻(正常运行),False=冻结
1102
+ """
1103
+ if caller_id != "launcher":
1104
+ raise PermissionError("Only launcher can control scaling")
1105
+
1106
+ enabled = bool(params.get("enabled", True))
1107
+ server = self.kernel_server
1108
+ server._scaling_frozen = not enabled
1109
+
1110
+ if enabled:
1111
+ # 解冻后立即触发一次检查,补齐实例/连接数
1112
+ asyncio.ensure_future(server._scaler_tick())
1113
+ print(f"[kernel] 扩缩容已解冻,立即触发一次检查")
1114
+ else:
1115
+ print(f"[kernel] 扩缩容已冻结")
1116
+
1117
+ return {"enabled": enabled, "frozen": not enabled}
1118
+
1119
+ async def _apply_module_config(self, module_id: str, instance_key: str) -> dict:
1120
+ """统一的配置应用入口:从配置文件读取多连接/多实例配置并更新 Kernel 状态。
1121
+
1122
+ 扩缩容执行由 _scaler_tick 统一负责,本方法只更新配置并唤醒 scaler。
1123
+
1124
+ Args:
1125
+ module_id: 模块名(如 'acp_channel'),用于读取配置文件和管理实例
1126
+ instance_key: 实例键(如 'acp_channel' 或 'acp_channel#2'),用于管理连接
1127
+ """
1128
+ server = self.kernel_server
1129
+
1130
+ # 远程模块(通过 Relay 连入)无配置文件,Launcher 不管理其生命周期。
1131
+ # 多连接配置由模块自身在 registry.register 时声明,从注册表读取后应用。
1132
+ # 多实例扩缩容不适用(Launcher 不管理远程模块生命周期)。
1133
+ if self.registry.is_remote_module(module_id):
1134
+ mod = self.registry.modules.get(module_id, {})
1135
+ max_conns = max(1, min(10, int(mod.get("max_connections", 1))))
1136
+ conn_elastic = bool(mod.get("connection_elastic", False))
1137
+ # 取消旧收缩任务(如从弹性改为非弹性)
1138
+ if not conn_elastic and instance_key in server._shrink_tasks:
1139
+ server._shrink_tasks[instance_key].cancel()
1140
+ del server._shrink_tasks[instance_key]
1141
+ server._module_conn_elastic[instance_key] = conn_elastic
1142
+ # max_connections 已在 registry 中,_get_max_connections() 可直接读取
1143
+ if max_conns > 1:
1144
+ print(f"[kernel] remote module {module_id}: max_connections={max_conns}, connection_elastic={conn_elastic}")
1145
+ server._initiate_multi_connection(instance_key, max_conns)
1146
+ return {
1147
+ "module_id": module_id,
1148
+ "max_connections": max_conns,
1149
+ "connection_elastic": conn_elastic,
1150
+ }
1151
+
1152
+ # 从 Launcher 读取配置文件(唯一权威来源)
1153
+ try:
1154
+ config = await server._call_launcher_rpc("get_module_config", {"module_name": module_id})
1155
+ except Exception as e:
1156
+ print(f"[kernel] _apply_module_config: 获取 {module_id} 配置失败: {e}")
1157
+ return {"module_id": module_id, "error": str(e)}
1158
+
1159
+ max_conns = max(1, min(10, int(config.get("max_connections", 1))))
1160
+ conn_elastic = bool(config.get("connection_elastic", False))
1161
+ max_inst = max(1, min(10, int(config.get("max_instances", 1))))
1162
+ inst_elastic = bool(config.get("instance_elastic", False))
1163
+
1164
+ # ── 更新多连接配置 ──
1165
+ # 如果从弹性改为非弹性,取消旧的收缩任务
1166
+ if not conn_elastic and instance_key in server._shrink_tasks:
1167
+ server._shrink_tasks[instance_key].cancel()
1168
+ del server._shrink_tasks[instance_key]
1169
+ server._module_conn_elastic[instance_key] = conn_elastic
1170
+ # 更新注册中心的 max_connections(供 _get_max_connections 读取)
1171
+ mod = self.registry.modules.get(module_id)
1172
+ if mod is not None:
1173
+ mod["max_connections"] = max_conns
1174
+
1175
+ # ── 更新多实例配置 ──
1176
+ server._module_instance_config[module_id] = {
1177
+ "max_instances": max_inst,
1178
+ "instance_elastic": inst_elastic,
1179
+ }
1180
+
1181
+ print(f"[kernel] apply_module_config({module_id}): max_connections={max_conns}, "
1182
+ f"connection_elastic={conn_elastic}, max_instances={max_inst}, "
1183
+ f"instance_elastic={inst_elastic}")
1184
+
1185
+ # ── 触发多连接建立 ──
1186
+ if max_conns > 1:
1187
+ server._initiate_multi_connection(instance_key, max_conns)
1188
+
1189
+ # 触发一次 scaler tick 以执行扩缩容(仅在未冻结时生效)
1190
+ if not server._scaling_frozen:
1191
+ asyncio.ensure_future(server._scaler_tick())
1192
+
1193
+ return {
1194
+ "module_id": module_id,
1195
+ "max_connections": max_conns,
1196
+ "connection_elastic": conn_elastic,
1197
+ "max_instances": max_inst,
1198
+ "instance_elastic": inst_elastic,
1199
+ }
1200
+
1201
+ async def _trim_excess_instances(self, module_id: str, max_inst: int):
1202
+ """将超出 max_inst 的多余实例标记为 draining(等队列空再关闭)。"""
1203
+ server = self.kernel_server
1204
+ from .event_hub import parse_instance_key
1205
+ inst_keys = self.event_hub.get_instance_keys(module_id)
1206
+ for k in inst_keys:
1207
+ _, num = parse_instance_key(k)
1208
+ if num > max_inst and k not in self.event_hub._draining_instances:
1209
+ self.event_hub.set_draining(k)
1210
+ print(f"[kernel] Draining excess instance: {module_id}#{num} (max={max_inst})")
1211
+
1212
+ async def _kernel_flush_init(self, caller_id: str, params: dict) -> dict:
1213
+ """打开 require_init 门控,对所有暂存的模块发送 system.require_init。
1214
+ 由 Launcher 在恢复模式下注册好 event waiters 后调用。"""
1215
+ server = self.kernel_server
1216
+ server._init_gate_open = True
1217
+ pending = server._init_pending[:]
1218
+ server._init_pending.clear()
1219
+
1220
+ sent = 0
1221
+ for inst_key, ws in pending:
1222
+ # 跳过已断开的连接(stale ws)
1223
+ if inst_key not in server.connections or not server.connections[inst_key]:
1224
+ continue
1225
+ if server.registry.needs_init(inst_key):
1226
+ from .event_hub import parse_instance_key
1227
+ base_mid, inst_num = parse_instance_key(inst_key)
1228
+ try:
1229
+ await ws.send_text(json.dumps({
1230
+ "jsonrpc": "2.0",
1231
+ "method": "event",
1232
+ "params": {
1233
+ "event": "system.require_init",
1234
+ "source": server.module_id,
1235
+ "data": {"module_id": base_mid, "instance_num": inst_num},
1236
+ },
1237
+ }))
1238
+ sent += 1
1239
+ print(f"[kernel] Sent deferred system.require_init to {inst_key}")
1240
+ except Exception as e:
1241
+ print(f"[kernel] Failed to send deferred system.require_init to {inst_key}: {e}")
1242
+
1243
+ print(f"[kernel] Init gate opened, sent {sent} deferred require_init")
1244
+ return {"flushed": sent}
1245
+
1246
+ # ── RPC Event Publishing ──
1247
+
1248
+ def _publish_rpc_event(self, rpc_id: str, pending: PendingForward, duration: float,
1249
+ status: str, result=None, error: str = None):
1250
+ """发出 RPC 调用完成事件(按需工作:只有订阅者时才发出)
1251
+
1252
+ Args:
1253
+ rpc_id: RPC 调用 ID
1254
+ pending: PendingForward 对象
1255
+ duration: 耗时(毫秒)
1256
+ status: 状态 ("success" | "error" | "timeout" | "offline")
1257
+ result: 成功时的返回值(可选)
1258
+ error: 失败时的错误信息(可选)
1259
+ """
1260
+ # 检查是否有订阅者(优化:没人订阅就不发出事件)
1261
+ has_subscribers = False
1262
+ for patterns in self.event_hub.subscriptions.values():
1263
+ for pattern_str, _ in patterns:
1264
+ if pattern_str == "rpc.call.completed" or pattern_str.startswith("rpc.") or pattern_str == "*" or pattern_str == ">":
1265
+ has_subscribers = True
1266
+ break
1267
+ if has_subscribers:
1268
+ break
1269
+
1270
+ if not has_subscribers:
1271
+ return
1272
+
1273
+ # 构造事件数据
1274
+ event_data = {
1275
+ "rpc_id": rpc_id,
1276
+ "trace_id": pending.trace_id,
1277
+ "caller": pending.caller_id,
1278
+ "target": pending.target_id,
1279
+ "method": pending.method,
1280
+ "params": pending.params,
1281
+ "duration": round(duration, 2),
1282
+ "status": status,
1283
+ "parent_rpc_id": pending.parent_rpc_id,
1284
+ "depth": pending.depth,
1285
+ }
1286
+
1287
+ if result is not None:
1288
+ event_data["result"] = result
1289
+ if error:
1290
+ event_data["error"] = error
1291
+
1292
+ # 记录到 trace 缓存
1293
+ self._add_to_trace_cache(pending.trace_id, TraceSpan(
1294
+ rpc_id=rpc_id,
1295
+ trace_id=pending.trace_id,
1296
+ caller=pending.caller_id,
1297
+ target=pending.target_id,
1298
+ method=pending.method,
1299
+ duration=round(duration, 2),
1300
+ status=status,
1301
+ error=error,
1302
+ parent_rpc_id=pending.parent_rpc_id,
1303
+ depth=pending.depth,
1304
+ timestamp=datetime.now(timezone.utc).isoformat(),
1305
+ ))
1306
+
1307
+ # 发出事件
1308
+ self.event_hub.publish_internal("rpc.call.completed", event_data, source="kernel")
1309
+
1310
+ # ── Trace 缓存管理 ──
1311
+
1312
+ def _add_to_trace_cache(self, trace_id: str, span: TraceSpan):
1313
+ """添加 span 到 trace 缓存,自动清理过期条目"""
1314
+ now = time.time()
1315
+
1316
+ # 清理过期条目
1317
+ expired = [tid for tid, data in self._trace_cache.items()
1318
+ if now - data["created_at"] > TRACE_CACHE_TTL]
1319
+ for tid in expired:
1320
+ self._trace_cache.pop(tid, None)
1321
+
1322
+ # 容量限制:FIFO 淘汰
1323
+ if len(self._trace_cache) >= TRACE_CACHE_MAX:
1324
+ oldest = min(self._trace_cache.items(), key=lambda x: x[1]["created_at"])
1325
+ self._trace_cache.pop(oldest[0], None)
1326
+
1327
+ # 添加 span
1328
+ if trace_id not in self._trace_cache:
1329
+ self._trace_cache[trace_id] = {
1330
+ "spans": [],
1331
+ "created_at": now,
1332
+ "origin": span.caller,
1333
+ }
1334
+ self._trace_cache[trace_id]["spans"].append(span)
1335
+
1336
+ async def _trace_query(self, caller_id: str, params: dict) -> dict:
1337
+ """查询指定 trace_id 的完整调用链"""
1338
+ trace_id = params.get("trace_id")
1339
+ if not trace_id:
1340
+ raise ValueError("trace_id is required")
1341
+
1342
+ trace_data = self._trace_cache.get(trace_id)
1343
+ if not trace_data:
1344
+ return {"trace_id": trace_id, "found": False}
1345
+
1346
+ spans = trace_data["spans"]
1347
+ total_duration = sum(s.duration for s in spans)
1348
+
1349
+ return {
1350
+ "trace_id": trace_id,
1351
+ "found": True,
1352
+ "origin": trace_data["origin"],
1353
+ "span_count": len(spans),
1354
+ "total_duration_ms": round(total_duration, 2),
1355
+ "spans": [
1356
+ {
1357
+ "rpc_id": s.rpc_id,
1358
+ "caller": s.caller,
1359
+ "target": s.target,
1360
+ "method": s.method,
1361
+ "duration": s.duration,
1362
+ "status": s.status,
1363
+ "error": s.error,
1364
+ "parent_rpc_id": s.parent_rpc_id,
1365
+ "depth": s.depth,
1366
+ "timestamp": s.timestamp,
1367
+ }
1368
+ for s in spans
1369
+ ],
1370
+ }
1371
+
1372
+ async def _trace_list(self, caller_id: str, params: dict) -> dict:
1373
+ """列出最近的 trace 摘要"""
1374
+ limit = params.get("limit", 20)
1375
+
1376
+ traces = []
1377
+ for trace_id, data in sorted(
1378
+ self._trace_cache.items(),
1379
+ key=lambda x: x[1]["created_at"],
1380
+ reverse=True
1381
+ )[:limit]:
1382
+ spans = data["spans"]
1383
+ total_duration = sum(s.duration for s in spans)
1384
+ has_error = any(s.status in ("error", "timeout", "offline") for s in spans)
1385
+ started_at = spans[0].timestamp if spans else ""
1386
+
1387
+ traces.append({
1388
+ "trace_id": trace_id,
1389
+ "origin": data["origin"],
1390
+ "span_count": len(spans),
1391
+ "total_duration_ms": round(total_duration, 2),
1392
+ "has_error": has_error,
1393
+ "started_at": started_at,
1394
+ })
1395
+
1396
+ return {
1397
+ "traces": traces,
1398
+ "total": len(self._trace_cache),
1399
+ }
1400
+