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