@agentunion/kite 1.5.0 → 1.6.1
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.
- package/.claude/skills/kite/checklists/feature-checklist.md +496 -0
- package/.claude/skills/kite/references/event-patterns.md +180 -0
- package/.claude/skills/kite/references/health-check.md +202 -0
- package/.claude/skills/kite/references/http-service.md +199 -0
- package/.claude/skills/kite/references/module-md-spec.md +172 -0
- package/.claude/skills/kite/references/multi-connection.md +147 -0
- package/.claude/skills/kite/references/rpc-patterns.md +199 -0
- package/.claude/skills/kite/references/shutdown-sequence.md +146 -0
- package/.claude/skills/kite/references/stdin-protocol.md +147 -0
- package/.claude/skills/kite/references/test-center-integration.md +178 -0
- package/.claude/skills/kite/references/ws-lifecycle.md +301 -0
- package/.claude/skills/kite/skill.md +272 -0
- package/.claude/skills/kite/templates/go/README.md +20 -0
- package/.claude/skills/kite/templates/node/entry.js +134 -0
- package/.claude/skills/kite/templates/node/module.md +16 -0
- package/.claude/skills/kite/templates/node/server.js +351 -0
- package/.claude/skills/kite/templates/node/server_http.js +90 -0
- package/.claude/skills/kite/templates/python/entry.py +425 -0
- package/.claude/skills/kite/templates/python/module.md +26 -0
- package/.claude/skills/kite/templates/python/server.py +447 -0
- package/.claude/skills/kite/templates/python/server_http.py +433 -0
- package/cli.js +38 -4
- package/core/env_checker.py +96 -0
- 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
- package/docs/ACP/345/215/217/350/256/256/345/205/274/345/256/271/346/226/271/346/241/210.md +138 -0
- 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
- package/docs/CLI/345/274/200/345/217/221/350/256/241/345/210/222.md +595 -0
- 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
- 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
- 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
- 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
- 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
- package/docs/Evol/346/250/241/345/235/227/350/256/276/350/256/241/346/226/271/346/241/210.md +1154 -0
- 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
- 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
- package/docs/HTTP-RPC/350/277/201/347/247/273/345/210/260WebSocket/350/256/241/345/210/222.md +318 -0
- package/docs/INDEX.md +388 -0
- package/docs/KITE_DOCS_GUIDE.md +33 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/README.md +46 -0
- package/docs/Kite/347/263/273/347/273/237/345/220/257/345/212/250/346/265/201/347/250/213.md +567 -0
- package/docs/Launcher/345/220/257/345/212/250/345/231/250/346/226/207/346/241/243.md +745 -0
- 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
- 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
- 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
- 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
- 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
- package/docs/Watchdog/350/265/204/346/272/220/347/233/221/346/216/247/347/255/226/347/225/245.md +92 -0
- 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
- 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
- package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/346/226/271/346/241/210.md +169 -0
- 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
- 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
- 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
- package/docs/audit-api-guide.md +68 -0
- package/docs/audit-module-design.md +315 -0
- package/docs/audit-module-implementation-summary.md +149 -0
- package/docs/llm-context-design.md +52 -0
- package/docs/llm-test-enhancement-plan.md +970 -0
- package/docs/logs-api-guide.md +42 -0
- 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
- 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
- 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
- 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
- package/docs//344/272/213/344/273/266/345/244/204/347/220/206/346/234/272/345/210/266.md +388 -0
- package/docs//344/272/213/344/273/266/345/244/204/347/220/206/350/247/204/350/214/203.md +113 -0
- 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
- 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
- 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
- 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
- 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
- package/docs//344/274/230/351/233/205/351/200/200/345/207/272/350/247/204/350/214/203.md +362 -0
- package/docs//344/276/235/350/265/226/347/256/241/347/220/206/350/257/264/346/230/216.md +141 -0
- 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
- package/docs//345/210/240/351/231/244kernel-client-example/345/256/214/346/210/220.md +309 -0
- package/docs//345/210/240/351/231/244ws-management/345/256/214/346/210/220.md +418 -0
- package/docs//345/220/257/345/212/250/344/274/230/345/214/226/346/226/271/346/241/210.md +522 -0
- 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
- 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
- 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
- package/docs//345/256/236/347/216/260/350/247/204/345/210/222.md +195 -0
- 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
- 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
- package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210.md +908 -0
- package/docs//346/226/207/346/241/243/346/233/264/346/226/260/346/270/205/345/215/225.md +83 -0
- 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
- 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
- package/docs//346/236/266/346/236/204/345/200/237/351/211/264/346/214/207/345/215/227.md +977 -0
- 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
- 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
- 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
- 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
- package/docs//346/250/241/345/235/227/345/274/200/345/217/221/346/214/207/345/215/227.md +1824 -0
- package/docs//346/250/241/345/235/227/347/203/255/346/233/264/346/226/260.md +89 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241.md +451 -0
- package/docs//351/207/215/346/236/204/346/234/272/345/210/266/346/270/205/345/215/225.md +192 -0
- package/docs//351/223/276/350/267/257/350/277/275/350/270/252/346/226/271/346/241/210.md +242 -0
- 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
- package/extensions/agents/assistant/entry.py +113 -14
- package/extensions/agents/assistant/module.md +27 -22
- package/extensions/agents/assistant/server.py +291 -105
- package/extensions/channels/acp_channel/entry.py +114 -16
- package/extensions/channels/acp_channel/module.md +4 -0
- package/extensions/channels/acp_channel/server.py +396 -105
- package/extensions/channels/phone_channel/__init__.py +1 -0
- package/extensions/channels/phone_channel/entry.py +503 -0
- package/extensions/channels/phone_channel/module.md +31 -0
- package/extensions/channels/phone_channel/server.py +686 -0
- package/extensions/event_hub_bench/entry.py +55 -12
- package/extensions/event_hub_bench/module.md +27 -27
- package/extensions/services/audit/README.md +134 -0
- package/extensions/services/audit/collector.py +73 -0
- package/extensions/services/audit/entry.py +444 -0
- package/extensions/services/audit/module.md +66 -0
- package/extensions/services/audit/query_audit.py +111 -0
- package/extensions/services/audit/routes/__init__.py +1 -0
- package/extensions/services/audit/routes/routes_audit.py +113 -0
- package/extensions/services/audit/schemas/__init__.py +5 -0
- package/extensions/services/audit/schemas/audit_event.py +92 -0
- package/extensions/services/audit/server.py +542 -0
- package/extensions/services/audit/storage.py +95 -0
- package/extensions/services/auth/entry.py +1054 -0
- package/extensions/services/auth/module.md +31 -0
- package/extensions/services/auth/token_store.py +185 -0
- package/extensions/services/auth/verifiers/evol_account.py +101 -0
- package/extensions/services/auth/verifiers/kite_token.py +38 -0
- package/extensions/services/auth/verifiers/pairing_code.py +71 -0
- package/extensions/services/backup/entry.py +494 -197
- package/extensions/services/backup/module.md +4 -2
- package/extensions/services/dataclaw/api/__init__.py +0 -0
- package/extensions/services/dataclaw/api/admin.py +367 -0
- package/extensions/services/dataclaw/api/copyright.py +175 -0
- package/extensions/services/dataclaw/api/credits.py +177 -0
- package/extensions/services/dataclaw/api/data.py +179 -0
- package/extensions/services/dataclaw/api/demands.py +269 -0
- package/extensions/services/dataclaw/api/feeds.py +262 -0
- package/extensions/services/dataclaw/api/identity.py +505 -0
- package/extensions/services/dataclaw/api/notifications.py +104 -0
- package/extensions/services/dataclaw/api/reviews.py +138 -0
- package/extensions/services/dataclaw/api/search.py +153 -0
- package/extensions/services/dataclaw/api/subscriptions.py +157 -0
- package/extensions/services/dataclaw/config.json5 +96 -0
- package/extensions/services/dataclaw/core/__init__.py +0 -0
- package/extensions/services/dataclaw/core/auth.py +95 -0
- package/extensions/services/dataclaw/core/config.py +50 -0
- package/extensions/services/dataclaw/core/database.py +70 -0
- package/extensions/services/dataclaw/entry.py +416 -0
- 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
- package/extensions/services/dataclaw/migrate.py +283 -0
- package/extensions/services/dataclaw/models/__init__.py +0 -0
- package/extensions/services/dataclaw/module.md +49 -0
- package/extensions/services/dataclaw/requirements.txt +18 -0
- package/extensions/services/dataclaw/server.py +759 -0
- package/extensions/services/dataclaw/services/__init__.py +0 -0
- package/extensions/services/dataclaw/services/agent_service.py +132 -0
- package/extensions/services/dataclaw/services/credit_service.py +235 -0
- package/extensions/services/dataclaw/services/email_service.py +140 -0
- package/extensions/services/dataclaw/services/feed_service.py +259 -0
- package/extensions/services/dataclaw/services/notification_service.py +209 -0
- package/extensions/services/dataclaw/services/oauth_service.py +275 -0
- package/extensions/services/dataclaw/services/pricing.py +102 -0
- package/extensions/services/dataclaw/services/quality.py +79 -0
- package/extensions/services/dataclaw/services/reputation.py +142 -0
- package/extensions/services/dataclaw/services/sms_service.py +174 -0
- package/extensions/services/dataclaw/static/css/common.css +853 -0
- package/extensions/services/dataclaw/static/css/themes/blue.css +42 -0
- package/extensions/services/dataclaw/static/css/themes/dark.css +42 -0
- package/extensions/services/dataclaw/static/css/themes/light.css +35 -0
- package/extensions/services/dataclaw/static/js/api.js +103 -0
- package/extensions/services/dataclaw/static/js/common.js +321 -0
- package/extensions/services/dataclaw/static/js/i18n.js +95 -0
- package/extensions/services/dataclaw/static/js/pages/admin.js +152 -0
- package/extensions/services/dataclaw/static/js/pages/dashboard.js +82 -0
- package/extensions/services/dataclaw/static/js/pages/feed-detail.js +180 -0
- package/extensions/services/dataclaw/static/js/pages/feed-manage.js +158 -0
- package/extensions/services/dataclaw/static/js/theme.js +46 -0
- package/extensions/services/dataclaw/static/locales/en-US.json +464 -0
- package/extensions/services/dataclaw/static/locales/ja-JP.json +464 -0
- package/extensions/services/dataclaw/static/locales/zh-CN.json +464 -0
- package/extensions/services/dataclaw/templates/admin/index.html +90 -0
- package/extensions/services/dataclaw/templates/base.html +136 -0
- package/extensions/services/dataclaw/templates/credits/balance.html +106 -0
- package/extensions/services/dataclaw/templates/credits/deposit.html +164 -0
- package/extensions/services/dataclaw/templates/credits/history.html +90 -0
- package/extensions/services/dataclaw/templates/dashboard.html +52 -0
- package/extensions/services/dataclaw/templates/demands/create.html +78 -0
- package/extensions/services/dataclaw/templates/demands/detail.html +136 -0
- package/extensions/services/dataclaw/templates/demands/list.html +94 -0
- package/extensions/services/dataclaw/templates/feeds/create.html +95 -0
- package/extensions/services/dataclaw/templates/feeds/detail.html +110 -0
- package/extensions/services/dataclaw/templates/feeds/list.html +110 -0
- package/extensions/services/dataclaw/templates/feeds/manage.html +88 -0
- package/extensions/services/dataclaw/templates/index.html +185 -0
- package/extensions/services/dataclaw/templates/login.html +246 -0
- package/extensions/services/dataclaw/templates/register.html +164 -0
- package/extensions/services/dataclaw/templates/settings/notifications.html +96 -0
- package/extensions/services/dataclaw/templates/settings/profile.html +167 -0
- package/extensions/services/dataclaw/templates/subscriptions/list.html +64 -0
- package/extensions/services/dataclaw/tests/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/conftest.py +68 -0
- package/extensions/services/dataclaw/tests/integration/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/integration/test_workflows.py +239 -0
- package/extensions/services/dataclaw/tests/unit/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/unit/test_admin.py +70 -0
- package/extensions/services/dataclaw/tests/unit/test_copyright.py +63 -0
- package/extensions/services/dataclaw/tests/unit/test_credits.py +80 -0
- package/extensions/services/dataclaw/tests/unit/test_data.py +98 -0
- package/extensions/services/dataclaw/tests/unit/test_demands.py +106 -0
- package/extensions/services/dataclaw/tests/unit/test_feeds.py +98 -0
- package/extensions/services/dataclaw/tests/unit/test_identity.py +88 -0
- package/extensions/services/dataclaw/tests/unit/test_notifications.py +36 -0
- package/extensions/services/dataclaw/tests/unit/test_reviews.py +68 -0
- package/extensions/services/dataclaw/tests/unit/test_search.py +64 -0
- package/extensions/services/dataclaw/tests/unit/test_subscriptions.py +65 -0
- package/extensions/services/dataclaw/tests/unit/test_system.py +106 -0
- package/extensions/services/dataclaw/utils/__init__.py +0 -0
- package/extensions/services/dataclaw/utils/crypto.py +38 -0
- package/extensions/services/dataclaw/utils/id_generator.py +52 -0
- package/extensions/services/dataclaw/ws/__init__.py +0 -0
- package/extensions/services/dataclaw/ws/handler.py +163 -0
- 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
- 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
- package/extensions/services/evol/__init__.py +1 -0
- package/extensions/services/evol/async_http.py +551 -0
- package/extensions/services/evol/auth_manager.py +602 -443
- package/extensions/services/evol/config.json5 +16 -0
- package/extensions/services/evol/entry.py +568 -406
- package/extensions/services/evol/evol_api.py +969 -173
- package/extensions/services/evol/mfa_totp.py +77 -0
- package/extensions/services/evol/module.md +150 -32
- package/extensions/services/evol/nonce_pool.py +113 -0
- package/extensions/services/evol/oauth_manager.py +223 -0
- package/extensions/services/evol/pairing.py +3 -2
- package/extensions/services/evol/pairing_codes.jsonl +1 -0
- package/extensions/services/evol/relay.py +1031 -682
- package/extensions/services/evol/relay_config.json5 +85 -67
- package/extensions/services/evol/routes/routes_llm.py +231 -0
- package/extensions/services/evol/routes/routes_rpc.py +90 -89
- package/extensions/services/evol/routes/routes_test.py +11 -4
- package/extensions/services/evol/server.py +2426 -875
- package/extensions/services/evol/static/assets/CommissionView-Cs_ys6Gm.js +1 -0
- package/extensions/services/evol/static/assets/CommissionView-DACet_Oo.css +1 -0
- package/extensions/services/evol/static/assets/IframePage-DbO11U9G.js +1 -0
- package/extensions/services/evol/static/assets/IframePage-c572lT8i.css +1 -0
- package/extensions/services/evol/static/assets/TeamDetailView-DULrGD7k.css +1 -0
- package/extensions/services/evol/static/assets/TeamDetailView-gy_MBEqG.js +139 -0
- package/extensions/services/evol/static/assets/element-plus-Bd7pZkkM.js +63 -0
- package/extensions/services/evol/static/assets/index-CmMONKzG.css +1 -0
- package/extensions/services/evol/static/assets/index-D44bBe__.js +2 -0
- package/extensions/services/evol/static/assets/vue-vendor-DtF-__I4.js +29 -0
- package/extensions/services/evol/static/index.html +16 -781
- package/extensions/services/evol/static/logo.png +0 -0
- package/extensions/services/evol/stats_manager.py +243 -240
- package/extensions/services/evol/web/README.md +89 -0
- package/extensions/services/evol/web/build.bat +44 -0
- package/extensions/services/evol/web/index.html +13 -0
- package/extensions/services/evol/web/package-lock.json +1718 -0
- package/extensions/services/evol/web/package.json +26 -0
- package/extensions/services/evol/web/public/logo.png +0 -0
- package/extensions/services/evol/web/src/App.vue +7 -0
- package/extensions/services/evol/web/src/components/layout/AppHeader.vue +202 -0
- package/extensions/services/evol/web/src/components/layout/AppLayout.vue +61 -0
- package/extensions/services/evol/web/src/components/layout/AppSidebar.vue +115 -0
- package/extensions/services/evol/web/src/components/login/LoginPage.vue +271 -0
- package/extensions/services/evol/web/src/components/team/AddMemberModal.vue +181 -0
- package/extensions/services/evol/web/src/components/team/GroupTreeNode.vue +156 -0
- package/extensions/services/evol/web/src/components/team/TeamAlertConfig.vue +221 -0
- package/extensions/services/evol/web/src/components/team/TeamBillModal.vue +165 -0
- package/extensions/services/evol/web/src/components/team/TeamMembersAndGroups.vue +499 -0
- package/extensions/services/evol/web/src/components/team/TeamStatsPanel.vue +907 -0
- package/extensions/services/evol/web/src/components/team/TreeNode.vue +331 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsExportProgress.vue +44 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsHeader.vue +89 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsMemberDetail.vue +415 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsSummary.vue +42 -0
- package/extensions/services/evol/web/src/components/team/stats/helpers.ts +195 -0
- package/extensions/services/evol/web/src/components/team/stats/stats.css +741 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsApi.ts +114 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsCharts.ts +242 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsExport.ts +232 -0
- package/extensions/services/evol/web/src/composables/useFormatters.ts +42 -0
- package/extensions/services/evol/web/src/composables/useTheme.ts +52 -0
- package/extensions/services/evol/web/src/env.d.ts +7 -0
- package/extensions/services/evol/web/src/i18n/en.ts +361 -0
- package/extensions/services/evol/web/src/i18n/index.ts +36 -0
- package/extensions/services/evol/web/src/i18n/zh.ts +379 -0
- package/extensions/services/evol/web/src/main.ts +21 -0
- package/extensions/services/evol/web/src/router/index.ts +81 -0
- package/extensions/services/evol/web/src/services/kernel-client.ts +406 -0
- package/extensions/services/evol/web/src/stores/auth.ts +189 -0
- package/extensions/services/evol/web/src/stores/connection.ts +134 -0
- package/extensions/services/evol/web/src/stores/pages.ts +79 -0
- package/extensions/services/evol/web/src/styles/base.css +213 -0
- package/extensions/services/evol/web/src/styles/variables.css +138 -0
- package/extensions/services/evol/web/src/types/rpc.ts +35 -0
- package/extensions/services/evol/web/src/types/token.ts +87 -0
- package/extensions/services/evol/web/src/views/AccountView.vue +1532 -0
- package/extensions/services/evol/web/src/views/AiServiceView.vue +219 -0
- package/extensions/services/evol/web/src/views/CommissionView.vue +1220 -0
- package/extensions/services/evol/web/src/views/CreditsView.vue +131 -0
- package/extensions/services/evol/web/src/views/EndpointView.vue +163 -0
- package/extensions/services/evol/web/src/views/IframePage.vue +120 -0
- package/extensions/services/evol/web/src/views/TeamDetailView.vue +473 -0
- package/extensions/services/evol/web/src/views/TeamView.vue +332 -0
- package/extensions/services/evol/web/tsconfig.json +31 -0
- package/extensions/services/evol/web/tsconfig.node.json +10 -0
- package/extensions/services/evol/web/vite.config.ts +49 -0
- package/extensions/services/evolmem/__init__.py +0 -0
- package/extensions/services/evolmem/entry.py +387 -0
- package/extensions/services/evolmem/hooks/__init__.py +0 -0
- package/extensions/services/evolmem/hooks/assistant_stop.py +228 -0
- package/extensions/services/evolmem/hooks/common.py +76 -0
- package/extensions/services/evolmem/hooks/pre_tool_use.py +56 -0
- package/extensions/services/evolmem/hooks/session_end.py +133 -0
- package/extensions/services/evolmem/hooks/session_start.py +229 -0
- package/extensions/services/evolmem/hooks/user_prompt.py +122 -0
- package/extensions/services/evolmem/module.md +48 -0
- package/extensions/services/evolmem/prompts/00-server-info.md +28 -0
- package/extensions/services/evolmem/prompts/01-behavior.md +46 -0
- package/extensions/services/evolmem/prompts/02-summary-format.md +112 -0
- package/extensions/services/evolmem/prompts/03-file-query.md +92 -0
- package/extensions/services/evolmem/prompts/04-topic-stats.md +11 -0
- package/extensions/services/evolmem/prompts/05-recent-topics.md +84 -0
- package/extensions/services/evolmem/scripts/__init__.py +0 -0
- package/extensions/services/evolmem/scripts/extract_keywords.py +40 -0
- package/extensions/services/evolmem/scripts/search_topics.py +91 -0
- package/extensions/services/evolmem/server.py +641 -0
- package/extensions/services/gateway/entry.py +964 -0
- package/extensions/services/gateway/module.md +29 -0
- package/extensions/services/gateway/nonce_pool.py +65 -0
- package/extensions/services/gateway/relay.py +133 -0
- package/extensions/services/gateway/ws_server.py +285 -0
- package/extensions/services/kite_console/auth_manager.py +603 -0
- package/extensions/services/kite_console/config.json5 +19 -0
- package/extensions/services/kite_console/config_loader.py +117 -0
- package/extensions/services/kite_console/entry.py +528 -0
- package/extensions/services/kite_console/evol_api.py +179 -0
- package/extensions/services/kite_console/evol_config.json5 +29 -0
- package/extensions/services/kite_console/mfa_totp.py +77 -0
- package/extensions/services/kite_console/migrate_tokens.py +122 -0
- package/extensions/services/kite_console/module.md +37 -0
- package/extensions/services/kite_console/nonce_pool.py +113 -0
- package/extensions/services/kite_console/oauth_manager.py +223 -0
- package/extensions/services/kite_console/pairing.py +280 -0
- package/extensions/services/kite_console/pairing_codes.jsonl +2 -0
- package/extensions/services/kite_console/relay.py +1350 -0
- package/extensions/services/kite_console/relay_config.json5 +96 -0
- package/extensions/services/kite_console/routes/__init__.py +1 -0
- package/extensions/services/kite_console/routes/routes_llm.py +231 -0
- package/extensions/services/kite_console/routes/routes_proxy.py +115 -0
- package/extensions/services/kite_console/routes/routes_rpc.py +89 -0
- package/extensions/services/kite_console/routes/routes_test.py +68 -0
- package/extensions/services/kite_console/server.py +1742 -0
- package/extensions/services/{evol → kite_console}/static/css/style.css +656 -2
- package/extensions/services/kite_console/static/index.html +1524 -0
- package/extensions/services/{evol → kite_console}/static/js/dialog.js +11 -4
- package/extensions/services/kite_console/static/js/evol-app.js +7740 -0
- package/extensions/services/{evol/static/js/evol-app.js → kite_console/static/js/evol-app.js.backup} +2777 -1949
- package/extensions/services/kite_console/static/js/kernel-client.js +560 -0
- package/extensions/services/{evol/static/js/kernel-client.js → kite_console/static/js/kernel-client.js.backup} +41 -3
- package/extensions/services/{evol → kite_console}/static/js/registry-tests.js +7 -0
- package/extensions/services/kite_console/static/js/tests/ARCHITECTURE.md +67 -0
- package/extensions/services/kite_console/static/js/tests/README.md +140 -0
- package/extensions/services/kite_console/static/js/tests/index.js +161 -0
- package/extensions/services/kite_console/static/js/tests/integration/auth.js +120 -0
- package/extensions/services/kite_console/static/js/tests/integration/channel-interaction.js +188 -0
- package/extensions/services/kite_console/static/js/tests/integration/elastic-connection.js +115 -0
- package/extensions/services/kite_console/static/js/tests/integration/full-workflow.js +43 -0
- package/extensions/services/kite_console/static/js/tests/integration/multi-instance.js +304 -0
- package/extensions/services/kite_console/static/js/tests/integration/nested-rpc.js +266 -0
- package/extensions/services/kite_console/static/js/tests/integration/pingpong.js +25 -0
- package/extensions/services/kite_console/static/js/tests/integration/redis.js +227 -0
- package/extensions/services/kite_console/static/js/tests/integration/registry-core.js +52 -0
- package/extensions/services/kite_console/static/js/tests/integration/remote-deploy.js +85 -0
- package/extensions/services/kite_console/static/js/tests/integration/require-init.js +96 -0
- package/extensions/services/kite_console/static/js/tests/integration/scaling-control.js +193 -0
- package/extensions/services/kite_console/static/js/tests/integration/trace.js +109 -0
- package/extensions/services/kite_console/static/js/tests/modules/acp_channel.js +339 -0
- package/extensions/services/kite_console/static/js/tests/modules/auth.js +96 -0
- package/extensions/services/kite_console/static/js/tests/modules/backup.js +49 -0
- package/extensions/services/kite_console/static/js/tests/modules/gateway.js +41 -0
- package/extensions/services/kite_console/static/js/tests/modules/kernel.js +90 -0
- package/extensions/services/kite_console/static/js/tests/modules/launcher.js +75 -0
- package/extensions/services/kite_console/static/js/tests/modules/multi_instance.js +129 -0
- package/extensions/services/kite_console/static/js/tests/modules/phone_channel.js +364 -0
- package/extensions/services/kite_console/static/js/tests/modules/redis.js +178 -0
- package/extensions/services/kite_console/static/js/tests/modules/watchdog.js +60 -0
- package/extensions/services/kite_console/static/js/tests/modules/web.js +70 -0
- package/extensions/services/kite_console/static/js/tests/test-runner.js +123 -0
- package/extensions/services/kite_console/static/js/virtual-list.js +200 -0
- package/extensions/services/kite_console/static/test_kernel_client_token.html +352 -0
- package/extensions/services/kite_console/stats_manager.py +247 -0
- package/extensions/services/logs/README.md +215 -0
- package/extensions/services/logs/api_logger.py +37 -0
- package/extensions/services/logs/baseline.py +121 -0
- package/extensions/services/logs/cleaner.py +76 -0
- package/extensions/services/logs/entry.py +449 -0
- package/extensions/services/logs/formatter.py +129 -0
- package/extensions/services/logs/module.md +38 -0
- package/extensions/services/logs/quick_diagnostic.py +128 -0
- package/extensions/services/logs/routes/__init__.py +1 -0
- package/extensions/services/logs/routes/routes_logs.py +218 -0
- package/extensions/services/logs/routes/routes_logs.py.backup +173 -0
- package/extensions/services/logs/scanner.py +100 -0
- package/extensions/services/logs/searcher.py +263 -0
- package/extensions/services/logs/server.py +553 -0
- package/extensions/services/logs.zip +0 -0
- package/extensions/services/model_service/config.json5 +30 -0
- package/extensions/services/model_service/entry.py +620 -171
- package/extensions/services/model_service/module.md +11 -2
- package/extensions/services/proxy/__init__.py +0 -0
- package/extensions/services/proxy/aid_manager.py +419 -0
- package/extensions/services/proxy/auth_bridge.py +182 -0
- package/extensions/services/proxy/config_store.py +79 -0
- package/extensions/services/proxy/entry.py +528 -0
- package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +2 -2
- package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +18 -28
- package/extensions/services/proxy/evol/presenter/configPresenter.py +80 -1127
- package/extensions/services/proxy/evol/presenter/userPresenter.py +71 -477
- package/extensions/services/proxy/evol/server/claude_proxy_async.py +11 -7
- package/extensions/services/proxy/module.md +151 -0
- package/extensions/services/proxy/server.py +952 -271
- package/extensions/services/redis/ALIGNMENT_CHECKLIST.md +121 -0
- package/extensions/services/redis/ALIGNMENT_STATUS.md +548 -0
- package/extensions/services/redis/config.json5 +8 -0
- package/extensions/services/redis/entry.py +1509 -0
- package/extensions/services/redis/entry.py.backup +405 -0
- package/extensions/services/redis/module.md +48 -0
- package/extensions/services/redis/redis_builtin.py +332 -0
- package/extensions/services/redis/redis_external.py +164 -0
- package/extensions/services/testUi/entry.py +446 -0
- package/extensions/services/testUi/module.md +18 -0
- package/extensions/services/testUi/ui/cards.html +131 -0
- package/extensions/services/testUi/ui/index.html +22 -0
- package/extensions/services/testUi/ui/particles.html +143 -0
- package/extensions/services/watchdog/entry.py +1258 -793
- package/extensions/services/watchdog/module.md +2 -0
- package/extensions/services/watchdog/monitor.py +465 -87
- package/extensions/services/web/auth_manager.py +602 -0
- package/extensions/services/web/config.json5 +11 -0
- package/extensions/services/web/entry.py +598 -478
- package/extensions/services/web/mfa_totp.py +77 -0
- package/extensions/services/web/module.md +16 -13
- package/extensions/services/web/nonce_pool.py +113 -0
- package/extensions/services/web/oauth_manager.py +223 -0
- package/extensions/services/web/pairing.py +3 -2
- package/extensions/services/web/pairing_codes.jsonl +1 -0
- package/extensions/services/web/relay.py +442 -63
- package/extensions/services/web/relay_config.json5 +1 -2
- package/extensions/services/web/routes/routes_rpc.py +6 -6
- package/extensions/services/web/server.py +360 -173
- package/extensions/services/web/static/index.html +1752 -1738
- package/extensions/services/web/static/js/app.js +32 -0
- package/extensions/services/web/static/js/kernel-client.js +48 -9
- package/extensions/services/web/vendor/bluetooth/audio.py +1 -1
- package/extensions/services/web/vendor/config.py +2 -2
- package/extensions/services/web/vendor/storage/identity.py +1 -1
- package/kernel/entry.py +77 -23
- package/kernel/event_hub.py +1122 -74
- package/kernel/module.md +2 -1
- package/kernel/registry_store.py +208 -11
- package/kernel/rpc_router.py +1400 -491
- package/kernel/server.py +1021 -134
- package/kite_cli/__init__.py +9 -1
- package/kite_cli/builders/__init__.py +4 -0
- package/kite_cli/builders/base.py +67 -0
- package/kite_cli/builders/custom.py +31 -0
- package/kite_cli/builders/detector.py +56 -0
- package/kite_cli/builders/go.py +34 -0
- package/kite_cli/builders/gradle.py +41 -0
- package/kite_cli/builders/maven.py +36 -0
- package/kite_cli/builders/npm.py +44 -0
- package/kite_cli/builders/python.py +37 -0
- package/kite_cli/commands/BUILD_GUIDE.md +109 -0
- package/kite_cli/commands/build.py +142 -0
- package/kite_cli/commands/check.py +60 -0
- package/kite_cli/commands/config.py +156 -0
- package/kite_cli/commands/deps.py +58 -0
- package/kite_cli/commands/deps_install.py +7 -7
- package/kite_cli/commands/disable.py +162 -0
- package/kite_cli/commands/enable.py +162 -0
- package/kite_cli/commands/export.py +96 -0
- package/kite_cli/commands/import_cmd.py +110 -0
- package/kite_cli/commands/install.py +50 -23
- package/kite_cli/commands/install_skill.py +107 -0
- package/kite_cli/commands/list.py +128 -31
- package/kite_cli/commands/outdated.py +202 -0
- package/kite_cli/commands/search.py +33 -17
- package/kite_cli/commands/update.py +115 -2
- package/kite_cli/commands/venv_setup.py +6 -6
- package/kite_cli/commands/why.py +48 -0
- package/kite_cli/core/config_manager.py +145 -0
- package/kite_cli/core/downloader.py +32 -2
- package/kite_cli/main.py +153 -7
- package/kite_cli/utils/colors.py +153 -0
- package/kite_cli/utils/dependency_graph.py +209 -0
- package/kite_cli/utils/process.py +55 -0
- package/kite_cli/utils/progress.py +207 -0
- package/kite_cli/utils/table.py +101 -0
- package/launcher/count_lines.py +192 -43
- package/launcher/entry.py +4543 -2802
- package/launcher/logging_setup.py +54 -1
- package/launcher/module.md +32 -6
- package/launcher/module_scanner.py +93 -20
- package/launcher/process_manager.py +355 -76
- package/main.py +6 -0
- package/package.json +4 -1
- package/requirements.txt +41 -38
- package/scripts/auto-fix-deps.py +128 -0
- package/scripts/env-manager.js +25 -2
- package/scripts/final-test.js +78 -0
- package/scripts/setup-python-env.js +700 -191
- package/scripts/test-alluser.js +48 -0
- package/scripts/test-different-version.js +86 -0
- package/scripts/test-direct.js +63 -0
- package/scripts/test-extract-installer.js +28 -0
- package/scripts/test-install-log.js +54 -0
- package/scripts/test-installer.js +39 -0
- package/scripts/test-integration.js +250 -0
- package/scripts/test-real-install.js +210 -0
- package/scripts/test-targetdir.js +49 -0
- package/scripts/test-venv-real.js +47 -0
- package/scripts/test-venv-simple.js +57 -0
- package/scripts/test-wait.js +49 -0
- package/scripts/test-with-log.js +63 -0
- package/extensions/services/evol/config.yaml +0 -149
- package/extensions/services/evol/routes/routes_management_ws.py +0 -127
- package/extensions/services/evol/static/index_evol.html +0 -14
- package/extensions/services/evol/static/js/app.js +0 -6304
- package/extensions/services/evol/static/js/auth.js +0 -326
- package/extensions/services/evol/static/js/evol-app-fixed.js +0 -50
- package/extensions/services/evol/static/js/evol-app.js.bak +0 -1800
- package/extensions/services/evol/static/js/kernel-client-example.js +0 -228
- package/extensions/services/evol/static/js/main.js +0 -141
- package/extensions/services/evol/static/js/stats.js +0 -217
- package/extensions/services/evol/static/js/token-manager.js +0 -175
- package/extensions/services/proxy/CHANGELOG_20260308.md +0 -258
- package/extensions/services/proxy/_fix_prints.py +0 -133
- package/extensions/services/proxy/_fix_prints2.py +0 -87
- package/extensions/services/proxy/console_auth.py +0 -109
- package/extensions/services/proxy/logs/websocket.log +0 -260
- package/extensions/services/proxy/main.py +0 -240
- package/extensions/services/proxy/requirements.txt +0 -13
- package/extensions/services/web/config.yaml +0 -149
- /package/extensions/services/{evol → kite_console}/static/pairing.html +0 -0
- /package/extensions/services/{evol → kite_console}/static/test_registry.html +0 -0
- /package/extensions/services/{evol → kite_console}/static/test_relay.html +0 -0
package/kernel/rpc_router.py
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
self.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
self.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
"
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
|