@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/server.py
CHANGED
|
@@ -7,21 +7,26 @@ Merges Registry + Event Hub into a single process.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import time
|
|
10
11
|
|
|
11
12
|
from fastapi import FastAPI
|
|
12
13
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
13
14
|
|
|
14
15
|
from .registry_store import RegistryStore
|
|
15
|
-
from .event_hub import EventHub
|
|
16
|
+
from .event_hub import EventHub, instance_key, parse_instance_key
|
|
16
17
|
from .rpc_router import RpcRouter
|
|
17
18
|
|
|
18
19
|
try:
|
|
19
20
|
import orjson
|
|
20
21
|
def _loads(raw: str):
|
|
21
22
|
return orjson.loads(raw)
|
|
23
|
+
def _dumps(obj) -> str:
|
|
24
|
+
return orjson.dumps(obj).decode('utf-8')
|
|
22
25
|
except ImportError:
|
|
23
26
|
def _loads(raw: str):
|
|
24
27
|
return json.loads(raw)
|
|
28
|
+
def _dumps(obj) -> str:
|
|
29
|
+
return json.dumps(obj)
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class KernelServer:
|
|
@@ -33,20 +38,33 @@ class KernelServer:
|
|
|
33
38
|
- Event notifications (delivered to subscribers)
|
|
34
39
|
"""
|
|
35
40
|
|
|
36
|
-
def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None):
|
|
41
|
+
def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None, boot_t0: float = 0, is_recover: bool = False):
|
|
37
42
|
if module_id is None:
|
|
38
43
|
raise ValueError("module_id is required")
|
|
39
44
|
self.module_id = module_id
|
|
40
45
|
self.advertise_ip = advertise_ip
|
|
41
46
|
self.port: int = 0 # set by entry.py before uvicorn.run
|
|
47
|
+
self.boot_t0 = boot_t0 # Startup time for ready event
|
|
48
|
+
self.is_recover = is_recover # 恢复模式标记,用于 module.ready reason 字段
|
|
49
|
+
|
|
50
|
+
# TLS configuration
|
|
51
|
+
# 默认值根据环境变量:开发环境允许 WS,生产环境强制 WSS
|
|
52
|
+
env = os.environ.get("KITE_ENV", "development").lower()
|
|
53
|
+
self.require_tls = os.environ.get("KITE_REQUIRE_TLS", "true" if env == "production" else "false").lower() == "true"
|
|
42
54
|
|
|
43
55
|
# Core components
|
|
44
56
|
self.registry = RegistryStore(launcher_token) # Can be None
|
|
45
57
|
self.event_hub = EventHub()
|
|
46
58
|
|
|
47
|
-
# Shared connection table (module_id -> WebSocket)
|
|
59
|
+
# Shared connection table (module_id -> {slot -> WebSocket})
|
|
48
60
|
# RpcRouter and EventHub both reference this
|
|
49
|
-
self.connections: dict[str, WebSocket] = {}
|
|
61
|
+
self.connections: dict[str, dict[int, WebSocket]] = {}
|
|
62
|
+
|
|
63
|
+
# ── 多连接状态 ──
|
|
64
|
+
self._module_conn_elastic: dict[str, bool] = {} # mid → 是否弹性连接
|
|
65
|
+
self._offer_tasks: dict[str, asyncio.Task] = {} # mid → offer task
|
|
66
|
+
self._shrink_tasks: dict[str, asyncio.Task] = {} # mid → shrink task
|
|
67
|
+
self._release_timers: dict[str, asyncio.Task] = {} # mid → release timer
|
|
50
68
|
|
|
51
69
|
# RPC router (pass self reference)
|
|
52
70
|
self.rpc_router = RpcRouter(
|
|
@@ -57,7 +75,6 @@ class KernelServer:
|
|
|
57
75
|
)
|
|
58
76
|
|
|
59
77
|
# Background tasks
|
|
60
|
-
self._ttl_task: asyncio.Task | None = None
|
|
61
78
|
self._dedup_task: asyncio.Task | None = None
|
|
62
79
|
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
63
80
|
self._shutting_down = False
|
|
@@ -67,19 +84,67 @@ class KernelServer:
|
|
|
67
84
|
self._launcher_subscribed = False
|
|
68
85
|
self._ready_published = False
|
|
69
86
|
|
|
87
|
+
# 恢复模式 require_init 门控:关闭时暂缓发送,等 Launcher 就绪后通过 RPC 打开
|
|
88
|
+
self._init_gate_open = True # 正常启动时打开,恢复模式由 entry.py 设为 False
|
|
89
|
+
self._init_pending: list[tuple] = [] # 门控关闭时暂存的 (module_id, ws) 对
|
|
90
|
+
|
|
70
91
|
# Subscribe to events that Kernel needs to handle
|
|
71
92
|
# Kernel 通过订阅机制接收事件(与其他模块一致)
|
|
72
|
-
self.event_hub.handle_subscribe(self.module_id, ["module.shutdown"])
|
|
93
|
+
self.event_hub.handle_subscribe(self.module_id, ["module.shutdown", "system.pong", "system.ready"])
|
|
73
94
|
|
|
74
95
|
# Register internal event callback for Kernel
|
|
75
96
|
# Kernel 自身没有 WebSocket 连接,通过回调机制接收订阅的事件
|
|
76
97
|
self.event_hub.register_internal_callback(self.module_id, self._handle_internal_event)
|
|
77
98
|
|
|
99
|
+
# 正在退出的模块集合(收到 module.shutdown 但尚未全部断开)
|
|
100
|
+
self._modules_exiting: set[str] = set()
|
|
101
|
+
|
|
78
102
|
# Debounce timers for disconnected modules (module_id -> asyncio.Task)
|
|
79
103
|
self._debounce_tasks: dict[str, asyncio.Task] = {}
|
|
80
104
|
# Launcher loss timer (35s after launcher offline)
|
|
81
105
|
self._launcher_loss_task: asyncio.Task | None = None
|
|
82
106
|
|
|
107
|
+
# Ping/Pong tracking
|
|
108
|
+
self._ping_sent_times: dict[str, float] = {} # module_id -> last ping sent time (t1)
|
|
109
|
+
self._pong_latencies: dict[str, dict] = {} # module_id -> {"outbound": ms, "inbound": ms, "last_update": timestamp}
|
|
110
|
+
self._pong_status: dict[str, str] = {} # module_id -> "ok" | "timeout" | "never"
|
|
111
|
+
self._ping_timeout_counts: dict[str, int] = {} # module_id -> consecutive timeout count
|
|
112
|
+
self._ping_task: asyncio.Task | None = None # Global ping broadcast task
|
|
113
|
+
self._first_ping_sent = False # 标记是否已发送第一次 ping
|
|
114
|
+
self._kernel_ping: float | None = None # kernel 自己的 ping 值(所有模块的最小值)
|
|
115
|
+
|
|
116
|
+
# ── Instance scaler (auto-scaling) ──
|
|
117
|
+
self._scaler_task: asyncio.Task | None = None
|
|
118
|
+
# 全局扩缩容冻结开关(Launcher 通过 kernel.set_scaling RPC 控制)
|
|
119
|
+
# 冻结时:_scaler_tick 不执行任何扩缩容操作
|
|
120
|
+
self._scaling_frozen: bool = False
|
|
121
|
+
# module_id → {max_instances, instance_elastic} from module.ready
|
|
122
|
+
self._module_instance_config: dict[str, dict] = {}
|
|
123
|
+
# module_id → timestamp when all instances first went overload
|
|
124
|
+
self._scale_overload_since: dict[str, float] = {}
|
|
125
|
+
# module_id → timestamp when instances first went idle (with count > 1)
|
|
126
|
+
self._scale_idle_since: dict[str, float] = {}
|
|
127
|
+
# module_id → no scaling actions until this time
|
|
128
|
+
self._scale_cooldown_until: dict[str, float] = {}
|
|
129
|
+
# module_id → number of instances pending startup (requested but not yet ready)
|
|
130
|
+
self._scale_pending: dict[str, int] = {}
|
|
131
|
+
# 防止 _scaler_tick 并发执行(多个 apply_module_config 可能同时触发)
|
|
132
|
+
self._scaler_running: bool = False
|
|
133
|
+
# rpc_id → asyncio.Future for Kernel→Launcher RPC responses
|
|
134
|
+
self._rpc_futures: dict[str, asyncio.Future] = {}
|
|
135
|
+
|
|
136
|
+
# 动态 ping 间隔控制
|
|
137
|
+
self._ping_interval = 60.0 # 当前 ping 间隔(秒),默认 60 秒
|
|
138
|
+
self._last_latencies_call = 0.0 # 最后一次调用 kernel.latencies 的时间
|
|
139
|
+
self._ping_interval_event = asyncio.Event() # 用于通知间隔变化
|
|
140
|
+
|
|
141
|
+
# Debug API (仅在 KITE_DEBUG=1 时启用)
|
|
142
|
+
self.debug_app: FastAPI | None = None
|
|
143
|
+
self.debug_port: int = 0
|
|
144
|
+
self._debug_server = None
|
|
145
|
+
if os.environ.get("KITE_DEBUG") == "1":
|
|
146
|
+
self.debug_app = self._create_debug_app()
|
|
147
|
+
|
|
83
148
|
# Build FastAPI app
|
|
84
149
|
self.app = self._create_app()
|
|
85
150
|
|
|
@@ -92,67 +157,190 @@ class KernelServer:
|
|
|
92
157
|
@app.on_event("startup")
|
|
93
158
|
async def _startup():
|
|
94
159
|
server.event_hub.start_internal_senders()
|
|
95
|
-
server._ttl_task = asyncio.create_task(server._ttl_loop())
|
|
96
160
|
server._dedup_task = asyncio.create_task(server._dedup_loop())
|
|
161
|
+
server._ping_task = asyncio.create_task(server._ping_broadcast_loop())
|
|
162
|
+
server._scaler_task = asyncio.create_task(server._instance_scaler_loop())
|
|
97
163
|
|
|
98
164
|
@app.on_event("shutdown")
|
|
99
165
|
async def _shutdown():
|
|
100
|
-
if server._ttl_task:
|
|
101
|
-
server._ttl_task.cancel()
|
|
102
166
|
if server._dedup_task:
|
|
103
167
|
server._dedup_task.cancel()
|
|
168
|
+
if server._ping_task:
|
|
169
|
+
server._ping_task.cancel()
|
|
170
|
+
if server._scaler_task:
|
|
171
|
+
server._scaler_task.cancel()
|
|
104
172
|
|
|
105
173
|
# ── WebSocket endpoint ──
|
|
106
174
|
|
|
107
175
|
@app.websocket("/ws")
|
|
108
176
|
async def ws_endpoint(ws: WebSocket):
|
|
109
|
-
token = ws.query_params.get("token", "")
|
|
110
177
|
mid_hint = ws.query_params.get("id", "")
|
|
111
178
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
179
|
+
# Check TLS requirement
|
|
180
|
+
if server.require_tls:
|
|
181
|
+
# Check if connection is secure (WSS)
|
|
182
|
+
# WebSocket.url.scheme will be "wss" for secure connections
|
|
183
|
+
if ws.url.scheme != "wss":
|
|
184
|
+
await ws.close(code=1008, reason="TLS required")
|
|
185
|
+
print(f"[kernel] Rejected non-TLS connection from {ws.client.host if ws.client else 'unknown'}")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Accept connection first (auth via first message)
|
|
189
|
+
await ws.accept()
|
|
190
|
+
|
|
191
|
+
# 关闭中拒绝新连接
|
|
192
|
+
if server._shutting_down:
|
|
193
|
+
await ws.close(code=1001, reason="Server shutting down")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# 速率限制检查(本地连接豁免,基于 module_id hint,无 hint 时用客户端 IP)
|
|
197
|
+
client_host = ws.client.host if ws.client else "unknown"
|
|
198
|
+
rate_key = mid_hint or client_host
|
|
199
|
+
is_local = client_host in ("127.0.0.1", "::1", "localhost")
|
|
200
|
+
if not is_local and not server.registry.check_auth_rate_limit(rate_key):
|
|
201
|
+
print(f"[kernel] Auth rate limited: key={rate_key}")
|
|
202
|
+
await ws.send_text(_dumps({
|
|
203
|
+
"jsonrpc": "2.0", "id": None,
|
|
204
|
+
"error": {"code": -32003, "message": "Rate limit exceeded"}
|
|
205
|
+
}))
|
|
206
|
+
await ws.close(code=4020, reason="Rate limit exceeded")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# Wait for auth message (5s timeout)
|
|
210
|
+
module_id = None
|
|
211
|
+
try:
|
|
212
|
+
raw = await asyncio.wait_for(ws.receive_text(), timeout=5.0)
|
|
213
|
+
auth_msg = _loads(raw)
|
|
214
|
+
|
|
215
|
+
# Validate auth request format
|
|
216
|
+
if (auth_msg.get("jsonrpc") != "2.0" or
|
|
217
|
+
auth_msg.get("method") != "auth" or
|
|
218
|
+
"params" not in auth_msg or
|
|
219
|
+
"token" not in auth_msg["params"]):
|
|
220
|
+
await ws.send_text(_dumps({
|
|
221
|
+
"jsonrpc": "2.0",
|
|
222
|
+
"id": auth_msg.get("id"),
|
|
223
|
+
"error": {"code": -32600, "message": "Invalid auth request"}
|
|
224
|
+
}))
|
|
225
|
+
await ws.close(code=4001, reason="Invalid auth request")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Verify token
|
|
229
|
+
token = auth_msg["params"]["token"]
|
|
230
|
+
module_id = server.registry.verify_token(token)
|
|
231
|
+
slot = 0 # 默认 slot 0(主连接)
|
|
232
|
+
is_slot_auth = False
|
|
233
|
+
|
|
234
|
+
if module_id is None:
|
|
235
|
+
# 尝试 slot token 认证
|
|
236
|
+
slot_result = server.registry.verify_slot_token(token)
|
|
237
|
+
if slot_result:
|
|
238
|
+
module_id, slot = slot_result
|
|
239
|
+
is_slot_auth = True
|
|
240
|
+
else:
|
|
241
|
+
print(f"[kernel] Auth failed: token={token[:8]}... hint={mid_hint}")
|
|
242
|
+
await ws.send_text(_dumps({
|
|
243
|
+
"jsonrpc": "2.0",
|
|
244
|
+
"id": auth_msg.get("id"),
|
|
245
|
+
"error": {"code": -32003, "message": "Authentication failed"}
|
|
246
|
+
}))
|
|
247
|
+
await ws.close(code=4001, reason="Authentication failed")
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# Use id hint for debug mode
|
|
251
|
+
if module_id == "debug" and mid_hint:
|
|
252
|
+
module_id = mid_hint
|
|
253
|
+
|
|
254
|
+
# inst_key is encoded in the token: instance 1 → "acp_channel", instance 2 → "acp_channel#2"
|
|
255
|
+
# parse_instance_key extracts (base_module_id, instance_num) so module_id stays clean
|
|
256
|
+
module_id, instance_num = parse_instance_key(module_id)
|
|
257
|
+
inst_key = instance_key(module_id, instance_num)
|
|
258
|
+
|
|
259
|
+
# For slot token auth, verify_slot_token returns the full inst_key (e.g. "acp_channel#2")
|
|
260
|
+
# parse_instance_key above already reconstructs inst_key correctly — no override needed
|
|
261
|
+
|
|
262
|
+
# Register connection in both EventHub and shared connections table
|
|
263
|
+
server.event_hub.add_connection(inst_key, ws, slot=slot)
|
|
264
|
+
if inst_key not in server.connections:
|
|
265
|
+
server.connections[inst_key] = {}
|
|
266
|
+
server.connections[inst_key][slot] = ws
|
|
267
|
+
|
|
268
|
+
# Get session info for auth response
|
|
269
|
+
session_id = server.event_hub._session_ids.get(inst_key, "")
|
|
270
|
+
current_seq = server.event_hub._session_seq.get(inst_key, 0)
|
|
271
|
+
|
|
272
|
+
# Send auth success response with session info
|
|
273
|
+
auth_result = {
|
|
274
|
+
"ok": True,
|
|
275
|
+
"session_id": session_id,
|
|
276
|
+
"seq": current_seq
|
|
277
|
+
}
|
|
278
|
+
if is_slot_auth:
|
|
279
|
+
auth_result["slot"] = slot
|
|
280
|
+
await ws.send_text(_dumps({
|
|
281
|
+
"jsonrpc": "2.0",
|
|
282
|
+
"id": auth_msg.get("id"),
|
|
283
|
+
"result": auth_result
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
except asyncio.TimeoutError:
|
|
287
|
+
print(f"[kernel] Auth timeout: hint={mid_hint}")
|
|
118
288
|
try:
|
|
119
|
-
await ws.close(code=
|
|
289
|
+
await ws.close(code=4002, reason="Auth timeout")
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
return
|
|
293
|
+
except Exception as e:
|
|
294
|
+
print(f"[kernel] Auth error: {e}")
|
|
295
|
+
try:
|
|
296
|
+
await ws.close(code=4001, reason="Auth error")
|
|
120
297
|
except Exception:
|
|
121
298
|
pass
|
|
122
299
|
return
|
|
123
300
|
|
|
124
|
-
#
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
301
|
+
# 主连接(slot 0):取消 debounce、更新状态、检查是否需要 require_init
|
|
302
|
+
if not is_slot_auth:
|
|
303
|
+
# Cancel debounce timer if instance is reconnecting within 5s window
|
|
304
|
+
old_debounce = server._debounce_tasks.pop(inst_key, None)
|
|
305
|
+
if old_debounce:
|
|
306
|
+
old_debounce.cancel()
|
|
307
|
+
print(f"[kernel] {inst_key} reconnected within debounce window")
|
|
308
|
+
|
|
309
|
+
# Set connected status in registry (module-level)
|
|
310
|
+
server.registry.set_connected(module_id)
|
|
311
|
+
|
|
312
|
+
# Track Launcher connection
|
|
313
|
+
if module_id == "launcher":
|
|
314
|
+
server._launcher_connected = True
|
|
315
|
+
if server._launcher_loss_task:
|
|
316
|
+
server._launcher_loss_task.cancel()
|
|
317
|
+
server._launcher_loss_task = None
|
|
318
|
+
print(f"[kernel] launcher reconnected, cancelled loss timer")
|
|
319
|
+
|
|
320
|
+
# 检查该实例是否需要注册/订阅(system.require_init)
|
|
321
|
+
if server.registry.needs_init(inst_key):
|
|
322
|
+
if server._init_gate_open or module_id == "launcher":
|
|
323
|
+
try:
|
|
324
|
+
await ws.send_text(_dumps({
|
|
325
|
+
"jsonrpc": "2.0",
|
|
326
|
+
"method": "event",
|
|
327
|
+
"params": {
|
|
328
|
+
"event": "system.require_init",
|
|
329
|
+
"source": server.module_id,
|
|
330
|
+
"data": {"module_id": module_id, "instance_num": instance_num},
|
|
331
|
+
},
|
|
332
|
+
}))
|
|
333
|
+
print(f"[kernel] Sent system.require_init to {inst_key}")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
print(f"[kernel] Failed to send system.require_init to {inst_key}: {e}")
|
|
336
|
+
else:
|
|
337
|
+
# 门控关闭(恢复模式),暂存待 Launcher 就绪后统一发送
|
|
338
|
+
server._init_pending.append((inst_key, ws))
|
|
339
|
+
print(f"[kernel] Deferred system.require_init for {inst_key} (gate closed)")
|
|
340
|
+
|
|
341
|
+
# Initialize ping status for new connection (per instance_key)
|
|
342
|
+
if inst_key not in server._pong_status:
|
|
343
|
+
server._pong_status[inst_key] = "never"
|
|
156
344
|
|
|
157
345
|
try:
|
|
158
346
|
while True:
|
|
@@ -173,6 +361,20 @@ class KernelServer:
|
|
|
173
361
|
if not isinstance(msg, dict):
|
|
174
362
|
continue
|
|
175
363
|
|
|
364
|
+
# JSON-RPC 2.0 协议版本校验
|
|
365
|
+
if msg.get("jsonrpc") != "2.0":
|
|
366
|
+
msg_id = msg.get("id")
|
|
367
|
+
if msg_id is not None:
|
|
368
|
+
try:
|
|
369
|
+
await ws.send_text(json.dumps({
|
|
370
|
+
"jsonrpc": "2.0", "id": msg_id,
|
|
371
|
+
"error": {"code": -32600,
|
|
372
|
+
"message": 'Invalid Request: missing or wrong "jsonrpc" field, must be "2.0"'}
|
|
373
|
+
}))
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
continue
|
|
377
|
+
|
|
176
378
|
# Classify message type:
|
|
177
379
|
has_method = "method" in msg
|
|
178
380
|
has_id = "id" in msg
|
|
@@ -180,11 +382,22 @@ class KernelServer:
|
|
|
180
382
|
has_error = "error" in msg
|
|
181
383
|
|
|
182
384
|
if has_method and has_id:
|
|
183
|
-
# RPC Request → dispatch to handler
|
|
184
|
-
await server.rpc_router.dispatch(
|
|
385
|
+
# RPC Request → dispatch to handler (use inst_key as caller)
|
|
386
|
+
await server.rpc_router.dispatch(inst_key, ws, msg)
|
|
185
387
|
elif has_id and (has_result or has_error):
|
|
186
|
-
#
|
|
187
|
-
|
|
388
|
+
# Check if this is a response to Kernel's own RPC call
|
|
389
|
+
msg_id = msg.get("id", "")
|
|
390
|
+
if isinstance(msg_id, str) and msg_id.startswith("kernel-") and msg_id in server._rpc_futures:
|
|
391
|
+
result = msg.get("result")
|
|
392
|
+
error = msg.get("error")
|
|
393
|
+
if error:
|
|
394
|
+
err_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
395
|
+
server._resolve_rpc_future(msg_id, error=err_msg)
|
|
396
|
+
else:
|
|
397
|
+
server._resolve_rpc_future(msg_id, result=result)
|
|
398
|
+
else:
|
|
399
|
+
# RPC Response → match to pending forward
|
|
400
|
+
await server.rpc_router.handle_response(inst_key, msg)
|
|
188
401
|
# else: notification or unknown — ignore
|
|
189
402
|
|
|
190
403
|
except WebSocketDisconnect:
|
|
@@ -192,21 +405,39 @@ class KernelServer:
|
|
|
192
405
|
except Exception as e:
|
|
193
406
|
err = str(e).lower()
|
|
194
407
|
if "not connected" not in err and "closed" not in err:
|
|
195
|
-
print(f"[kernel] WebSocket error for {
|
|
408
|
+
print(f"[kernel] WebSocket error for {inst_key}: {e}")
|
|
196
409
|
finally:
|
|
197
|
-
#
|
|
198
|
-
server.event_hub.remove_connection(
|
|
199
|
-
server.connections.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
server.
|
|
209
|
-
|
|
410
|
+
# 清理该 slot 的连接(使用 inst_key)
|
|
411
|
+
server.event_hub.remove_connection(inst_key, slot=slot)
|
|
412
|
+
slots_dict = server.connections.get(inst_key, {})
|
|
413
|
+
slots_dict.pop(slot, None)
|
|
414
|
+
if not slots_dict:
|
|
415
|
+
server.connections.pop(inst_key, None)
|
|
416
|
+
|
|
417
|
+
# 检查是否所有 slot 都已断开
|
|
418
|
+
remaining = server.connections.get(inst_key, {})
|
|
419
|
+
if not remaining:
|
|
420
|
+
# 所有 slot 断开 → 清理退出标记、启动 debounce
|
|
421
|
+
server._modules_exiting.discard(inst_key)
|
|
422
|
+
old_task = server._debounce_tasks.pop(inst_key, None)
|
|
423
|
+
if old_task:
|
|
424
|
+
old_task.cancel()
|
|
425
|
+
server._debounce_tasks[inst_key] = asyncio.create_task(
|
|
426
|
+
server._debounce_offline(inst_key)
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
# 部分 slot 断开 → 不触发 debounce
|
|
430
|
+
# 冻结或模块正在退出则跳过 recovery offer
|
|
431
|
+
if server._scaling_frozen or inst_key in server._modules_exiting:
|
|
432
|
+
pass
|
|
433
|
+
# 非弹性模式:发 recovery offer 补充断掉的 slot
|
|
434
|
+
elif not server._module_conn_elastic.get(inst_key, False):
|
|
435
|
+
max_conns = server._get_max_connections(inst_key)
|
|
436
|
+
if max_conns > 1 and len(remaining) < max_conns:
|
|
437
|
+
missing = [s for s in range(1, max_conns)
|
|
438
|
+
if s not in remaining]
|
|
439
|
+
if missing:
|
|
440
|
+
server._send_connection_offer(inst_key, missing)
|
|
210
441
|
|
|
211
442
|
# ── HTTP endpoints (debug only) ──
|
|
212
443
|
|
|
@@ -241,22 +472,295 @@ class KernelServer:
|
|
|
241
472
|
|
|
242
473
|
return app
|
|
243
474
|
|
|
475
|
+
def _create_debug_app(self) -> FastAPI:
|
|
476
|
+
"""创建 Debug API(仅在 KITE_DEBUG=1 时使用)"""
|
|
477
|
+
app = FastAPI(title="Kite Kernel Debug API", docs_url=None, redoc_url=None)
|
|
478
|
+
server = self
|
|
479
|
+
|
|
480
|
+
@app.get("/health")
|
|
481
|
+
async def debug_health():
|
|
482
|
+
"""健康检查 — 返回 Kernel 是否 ready"""
|
|
483
|
+
return {
|
|
484
|
+
"status": "ready" if server._ready_published else "starting",
|
|
485
|
+
"module_count": len(server.registry.modules),
|
|
486
|
+
"online_count": sum(
|
|
487
|
+
1 for m in server.registry.modules.values()
|
|
488
|
+
if m.get("status") in ("registered", "ready")
|
|
489
|
+
),
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@app.get("/status")
|
|
493
|
+
async def debug_status():
|
|
494
|
+
"""详细状态 — 返回所有模块状态和事件统计"""
|
|
495
|
+
import time as _time
|
|
496
|
+
return {
|
|
497
|
+
"kernel": {
|
|
498
|
+
"ready": server._ready_published,
|
|
499
|
+
"port": server.port,
|
|
500
|
+
"advertise_ip": server.advertise_ip,
|
|
501
|
+
"boot_time": server.event_hub._start_time,
|
|
502
|
+
"uptime_seconds": round(_time.time() - server.event_hub._start_time),
|
|
503
|
+
},
|
|
504
|
+
"registry": {
|
|
505
|
+
"modules": {
|
|
506
|
+
mid: {
|
|
507
|
+
"status": data.get("status"),
|
|
508
|
+
"module_type": data.get("module_type"),
|
|
509
|
+
"registered_at": data.get("registered_at"),
|
|
510
|
+
"max_connections": data.get("max_connections"),
|
|
511
|
+
"port": data.get("port"),
|
|
512
|
+
}
|
|
513
|
+
for mid, data in server.registry.modules.items()
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
"event_hub": server.event_hub.get_stats(),
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@app.get("/registry")
|
|
520
|
+
async def debug_registry():
|
|
521
|
+
"""注册中心详情 — 返回所有模块的完整注册信息"""
|
|
522
|
+
return {
|
|
523
|
+
"modules": {
|
|
524
|
+
mid: {
|
|
525
|
+
"status": data.get("status"),
|
|
526
|
+
"module_type": data.get("module_type"),
|
|
527
|
+
"registered_at": data.get("registered_at"),
|
|
528
|
+
"max_connections": data.get("max_connections"),
|
|
529
|
+
"port": data.get("port"),
|
|
530
|
+
"base_url": data.get("base_url"),
|
|
531
|
+
"tools": list(data.get("tools", {}).keys()),
|
|
532
|
+
"events_subscribe": data.get("events_subscribe", []),
|
|
533
|
+
"events_publish": list(data.get("events_publish", {}).keys()),
|
|
534
|
+
"launcher_id": data.get("launcher_id"),
|
|
535
|
+
}
|
|
536
|
+
for mid, data in server.registry.modules.items()
|
|
537
|
+
},
|
|
538
|
+
"module_count": len(server.registry.modules),
|
|
539
|
+
"last_update_time": server.registry.last_update_time,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@app.get("/ws-url")
|
|
543
|
+
async def debug_ws_url():
|
|
544
|
+
"""返回 Kernel WebSocket 连接地址"""
|
|
545
|
+
return {
|
|
546
|
+
"ws_url": f"ws://{server.advertise_ip}:{server.port}/ws",
|
|
547
|
+
"port": server.port,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
@app.post("/shutdown")
|
|
551
|
+
async def debug_shutdown():
|
|
552
|
+
"""触发 Kernel 优雅关闭"""
|
|
553
|
+
if server._shutting_down:
|
|
554
|
+
return {"status": "already_shutting_down"}
|
|
555
|
+
|
|
556
|
+
server._shutting_down = True
|
|
557
|
+
# 发布 module.shutdown 事件通知所有模块
|
|
558
|
+
server.event_hub.publish_internal(
|
|
559
|
+
"module.shutdown",
|
|
560
|
+
{"module_id": "kernel", "reason": "debug_api_shutdown"},
|
|
561
|
+
source="kernel"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# 延迟关闭 uvicorn(给事件传递留时间)
|
|
565
|
+
async def _delayed_shutdown():
|
|
566
|
+
await asyncio.sleep(1)
|
|
567
|
+
if server._uvicorn_server:
|
|
568
|
+
server._uvicorn_server.should_exit = True
|
|
569
|
+
if server._debug_server:
|
|
570
|
+
server._debug_server.should_exit = True
|
|
571
|
+
|
|
572
|
+
asyncio.create_task(_delayed_shutdown())
|
|
573
|
+
return {"status": "shutting_down"}
|
|
574
|
+
|
|
575
|
+
return app
|
|
576
|
+
|
|
244
577
|
# ── Background loops ──
|
|
245
578
|
|
|
246
|
-
async def
|
|
247
|
-
"""
|
|
579
|
+
async def _send_first_ping(self):
|
|
580
|
+
"""发送第一次 ping(在 system.ready 后立即触发)"""
|
|
581
|
+
import time
|
|
582
|
+
if self._first_ping_sent:
|
|
583
|
+
return
|
|
584
|
+
self._first_ping_sent = True
|
|
585
|
+
|
|
586
|
+
t1 = time.time()
|
|
587
|
+
connected_modules = [mid for mid, slots in self.connections.items() if slots]
|
|
588
|
+
print(f"[kernel] _send_first_ping: {len(connected_modules)} modules connected")
|
|
589
|
+
|
|
590
|
+
if connected_modules:
|
|
591
|
+
for module_id in connected_modules:
|
|
592
|
+
self._ping_sent_times[module_id] = t1
|
|
593
|
+
self._pong_status[module_id] = "never"
|
|
594
|
+
|
|
595
|
+
# Broadcast system.ping event
|
|
596
|
+
self.event_hub.publish_internal(
|
|
597
|
+
"system.ping",
|
|
598
|
+
{"ping_time": t1},
|
|
599
|
+
source=self.module_id
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
async def _ping_broadcast_loop(self):
|
|
603
|
+
"""Broadcast system.ping event with dynamic interval (5s when active, 60s when idle)."""
|
|
604
|
+
import time
|
|
605
|
+
import uuid
|
|
606
|
+
|
|
607
|
+
# 等待第一次 ping 发送(由 system.ready 触发)
|
|
608
|
+
while not self._first_ping_sent:
|
|
609
|
+
await asyncio.sleep(0.5)
|
|
610
|
+
|
|
611
|
+
# 第一次 ping 后等待 5 秒检查响应
|
|
612
|
+
await asyncio.sleep(5)
|
|
613
|
+
await self._check_ping_timeouts()
|
|
614
|
+
|
|
615
|
+
# 初始间隔:60 秒(空闲模式)
|
|
616
|
+
remaining_wait = 55.0 # 60 - 5 = 55 秒
|
|
617
|
+
|
|
248
618
|
while True:
|
|
249
|
-
await asyncio.sleep(10)
|
|
250
619
|
try:
|
|
251
|
-
|
|
252
|
-
|
|
620
|
+
# 动态调整间隔
|
|
621
|
+
current_time = time.time()
|
|
622
|
+
time_since_last_call = current_time - self._last_latencies_call
|
|
623
|
+
|
|
624
|
+
# 如果最近 30 秒内有调用 kernel.latencies,使用 5 秒间隔
|
|
625
|
+
if time_since_last_call < 30:
|
|
626
|
+
target_interval = 5.0
|
|
627
|
+
else:
|
|
628
|
+
target_interval = 60.0
|
|
629
|
+
|
|
630
|
+
# 如果间隔发生变化,调整等待时间
|
|
631
|
+
if target_interval != self._ping_interval:
|
|
632
|
+
old_interval = self._ping_interval
|
|
633
|
+
self._ping_interval = target_interval
|
|
634
|
+
print(f"[kernel] Ping interval changed: {old_interval:.0f}s → {target_interval:.0f}s")
|
|
635
|
+
|
|
636
|
+
# 等待剩余时间(可被中断)
|
|
637
|
+
try:
|
|
638
|
+
await asyncio.wait_for(
|
|
639
|
+
self._ping_interval_event.wait(),
|
|
640
|
+
timeout=remaining_wait
|
|
641
|
+
)
|
|
642
|
+
# 如果被唤醒(interval 变化),重新计算等待时间
|
|
643
|
+
self._ping_interval_event.clear()
|
|
644
|
+
remaining_wait = self._ping_interval - 5 # 减去检查超时的 5 秒
|
|
645
|
+
continue
|
|
646
|
+
except asyncio.TimeoutError:
|
|
647
|
+
# 正常超时,发送 ping
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
# 发送 ping
|
|
651
|
+
t1 = time.time()
|
|
652
|
+
connected_modules = [mid for mid, slots in self.connections.items() if slots]
|
|
653
|
+
|
|
654
|
+
if connected_modules:
|
|
655
|
+
for module_id in connected_modules:
|
|
656
|
+
self._ping_sent_times[module_id] = t1
|
|
657
|
+
|
|
658
|
+
# Broadcast system.ping event
|
|
253
659
|
self.event_hub.publish_internal(
|
|
254
|
-
"
|
|
660
|
+
"system.ping",
|
|
661
|
+
{"ping_time": t1},
|
|
662
|
+
source=self.module_id
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Check for timeouts after 5s
|
|
666
|
+
await asyncio.sleep(5)
|
|
667
|
+
await self._check_ping_timeouts()
|
|
668
|
+
|
|
669
|
+
# 计算下一次等待时间
|
|
670
|
+
remaining_wait = self._ping_interval - 5
|
|
671
|
+
|
|
672
|
+
except asyncio.CancelledError:
|
|
673
|
+
break
|
|
255
674
|
except Exception as e:
|
|
256
|
-
print(f"[kernel]
|
|
675
|
+
print(f"[kernel] Ping broadcast loop error: {e}")
|
|
676
|
+
|
|
677
|
+
async def _check_ping_timeouts(self):
|
|
678
|
+
"""检查 ping 超时并计算汇总统计,连续 3 次超时则强断连接"""
|
|
679
|
+
import time
|
|
680
|
+
t_check = time.time()
|
|
681
|
+
modules_to_disconnect = []
|
|
682
|
+
|
|
683
|
+
for module_id in list(self.connections.keys()):
|
|
684
|
+
if module_id in self._ping_sent_times:
|
|
685
|
+
# If no pong received within 5s, mark as timeout
|
|
686
|
+
if module_id not in self._pong_latencies or \
|
|
687
|
+
self._pong_latencies[module_id].get("last_update", 0) < self._ping_sent_times[module_id]:
|
|
688
|
+
if self._pong_status.get(module_id) == "never":
|
|
689
|
+
# Keep as "never" if never received
|
|
690
|
+
pass
|
|
691
|
+
else:
|
|
692
|
+
self._pong_status[module_id] = "timeout"
|
|
693
|
+
# 增加连续超时计数
|
|
694
|
+
self._ping_timeout_counts[module_id] = self._ping_timeout_counts.get(module_id, 0) + 1
|
|
695
|
+
|
|
696
|
+
# 连续 3 次超时 → 强断连接
|
|
697
|
+
if self._ping_timeout_counts[module_id] >= 3:
|
|
698
|
+
modules_to_disconnect.append(module_id)
|
|
699
|
+
else:
|
|
700
|
+
# 收到 pong,重置超时计数
|
|
701
|
+
self._ping_timeout_counts[module_id] = 0
|
|
702
|
+
|
|
703
|
+
# 强断超时连接(关闭所有 slot)
|
|
704
|
+
for module_id in modules_to_disconnect:
|
|
705
|
+
slots = self.connections.get(module_id, {})
|
|
706
|
+
if slots:
|
|
707
|
+
print(f"[kernel] {module_id} 连续 3 次 Ping 超时,强制断开连接 ({len(slots)} slots)")
|
|
708
|
+
for ws in list(slots.values()):
|
|
709
|
+
try:
|
|
710
|
+
await ws.close(code=4012, reason="Ping timeout")
|
|
711
|
+
except Exception:
|
|
712
|
+
pass
|
|
713
|
+
|
|
714
|
+
# 计算汇总统计
|
|
715
|
+
total_modules = len(self.connections)
|
|
716
|
+
ok_pings = []
|
|
717
|
+
timeout_modules = []
|
|
718
|
+
all_inbounds = []
|
|
719
|
+
all_outbounds = []
|
|
720
|
+
|
|
721
|
+
for module_id in self.connections.keys():
|
|
722
|
+
status = self._pong_status.get(module_id, "never")
|
|
723
|
+
if status == "ok":
|
|
724
|
+
latency = self._pong_latencies.get(module_id, {})
|
|
725
|
+
outbound = latency.get("outbound")
|
|
726
|
+
inbound = latency.get("inbound")
|
|
727
|
+
if outbound is not None:
|
|
728
|
+
ok_pings.append((module_id, outbound))
|
|
729
|
+
all_outbounds.append(outbound)
|
|
730
|
+
if inbound is not None:
|
|
731
|
+
all_inbounds.append(inbound)
|
|
732
|
+
elif status == "timeout":
|
|
733
|
+
timeout_modules.append(module_id)
|
|
734
|
+
|
|
735
|
+
# 计算 kernel 自己的 ping 数据并存储
|
|
736
|
+
if ok_pings:
|
|
737
|
+
min_module, min_ping = min(ok_pings, key=lambda x: x[1])
|
|
738
|
+
max_module, max_ping = max(ok_pings, key=lambda x: x[1])
|
|
739
|
+
|
|
740
|
+
# kernel 的延迟数据
|
|
741
|
+
kernel_outbound = min(all_inbounds) if all_inbounds else None
|
|
742
|
+
kernel_inbound = min(all_outbounds) if all_outbounds else None
|
|
743
|
+
|
|
744
|
+
if kernel_outbound is not None and kernel_inbound is not None:
|
|
745
|
+
self._pong_latencies["kernel"] = {
|
|
746
|
+
"outbound": kernel_outbound,
|
|
747
|
+
"inbound": kernel_inbound,
|
|
748
|
+
"last_update": list(self._ping_sent_times.values())[0] if self._ping_sent_times else t_check
|
|
749
|
+
}
|
|
750
|
+
self._pong_status["kernel"] = "ok"
|
|
751
|
+
|
|
752
|
+
# 打印汇总信息
|
|
753
|
+
timeout_info = f", 超时: {timeout_modules}" if timeout_modules else ""
|
|
754
|
+
print(f"[kernel] Ping 汇总: 发送 {total_modules} 个模块, 最小 {min_ping:.2f}ms ({min_module}), 最大 {max_ping:.2f}ms ({max_module}){timeout_info}")
|
|
755
|
+
else:
|
|
756
|
+
self._pong_latencies.pop("kernel", None)
|
|
757
|
+
self._pong_status.pop("kernel", None)
|
|
758
|
+
if total_modules > 0:
|
|
759
|
+
print(f"\033[91m[kernel] 警告: 所有模块 Ping 超时 ({total_modules} 个模块)\033[0m")
|
|
760
|
+
|
|
257
761
|
|
|
258
762
|
async def _dedup_loop(self):
|
|
259
|
-
"""Clean up dedup table every 30s."""
|
|
763
|
+
"""Clean up dedup table and throttle cache every 30s."""
|
|
260
764
|
while True:
|
|
261
765
|
await asyncio.sleep(30)
|
|
262
766
|
try:
|
|
@@ -265,23 +769,168 @@ class KernelServer:
|
|
|
265
769
|
except Exception as e:
|
|
266
770
|
print(f"[kernel] Dedup cleanup error: {e}")
|
|
267
771
|
|
|
772
|
+
# 清理过期 slot token
|
|
773
|
+
try:
|
|
774
|
+
self.registry.cleanup_expired_slot_tokens()
|
|
775
|
+
except Exception as e:
|
|
776
|
+
print(f"[kernel] Slot token cleanup error: {e}")
|
|
777
|
+
|
|
778
|
+
# 清理节流缓存:移除 10s 前的条目,防止内存泄漏
|
|
779
|
+
try:
|
|
780
|
+
import time
|
|
781
|
+
cutoff = time.time() - 10
|
|
782
|
+
stale_keys = [k for k, t in self.event_hub._throttle_last.items() if t < cutoff]
|
|
783
|
+
for k in stale_keys:
|
|
784
|
+
del self.event_hub._throttle_last[k]
|
|
785
|
+
except Exception as e:
|
|
786
|
+
print(f"[kernel] Throttle cache cleanup error: {e}")
|
|
787
|
+
|
|
788
|
+
# ── 多连接:offer / release ──
|
|
789
|
+
|
|
790
|
+
def _get_max_connections(self, module_id: str) -> int:
|
|
791
|
+
"""从 registry 读取模块的 max_connections 配置。module_id 可以是 instance_key。"""
|
|
792
|
+
base_mid = parse_instance_key(module_id)[0]
|
|
793
|
+
mod = self.registry.modules.get(base_mid, {})
|
|
794
|
+
return mod.get("max_connections", 1)
|
|
795
|
+
|
|
796
|
+
def _send_connection_offer(self, module_id: str, slots: list[int]):
|
|
797
|
+
"""生成 slot token 并通过事件通知模块建立附加连接。"""
|
|
798
|
+
if self._shutting_down or module_id in self._modules_exiting:
|
|
799
|
+
return
|
|
800
|
+
tokens = self.registry.generate_slot_tokens(module_id, slots)
|
|
801
|
+
offer_data = {
|
|
802
|
+
"module_id": module_id,
|
|
803
|
+
"slots": {
|
|
804
|
+
str(s): {"token": t, "expires_in": 30}
|
|
805
|
+
for s, t in tokens.items()
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
self.event_hub.publish_internal(
|
|
809
|
+
"system.connection.offer", offer_data,
|
|
810
|
+
source=self.module_id
|
|
811
|
+
)
|
|
812
|
+
print(f"[kernel] Sent connection offer to {module_id}: slots={slots}")
|
|
813
|
+
|
|
814
|
+
def _send_connection_release(self, module_id: str, slots: list[int], reason: str = "idle_shrink"):
|
|
815
|
+
"""请求模块释放指定 slot,超时强制关闭。"""
|
|
816
|
+
self.event_hub.publish_internal(
|
|
817
|
+
"system.connection.release",
|
|
818
|
+
{"module_id": module_id, "slots": slots, "reason": reason, "grace_period": 2},
|
|
819
|
+
source=self.module_id
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# 启动 2 秒可取消定时器,超时强制关闭
|
|
823
|
+
async def _force_close():
|
|
824
|
+
try:
|
|
825
|
+
await asyncio.sleep(2)
|
|
826
|
+
for s in slots:
|
|
827
|
+
ws = self.connections.get(module_id, {}).get(s)
|
|
828
|
+
if ws:
|
|
829
|
+
try:
|
|
830
|
+
await ws.close(code=4014, reason=f"Release timeout: {reason}")
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
except asyncio.CancelledError:
|
|
834
|
+
pass
|
|
835
|
+
|
|
836
|
+
timer_key = f"{module_id}:{','.join(map(str, slots))}"
|
|
837
|
+
old = self._release_timers.pop(timer_key, None)
|
|
838
|
+
if old:
|
|
839
|
+
old.cancel()
|
|
840
|
+
self._release_timers[timer_key] = asyncio.create_task(_force_close())
|
|
841
|
+
print(f"[kernel] Sent connection release to {module_id}: slots={slots} reason={reason}")
|
|
842
|
+
|
|
843
|
+
def _initiate_multi_connection(self, module_id: str, max_conns: int):
|
|
844
|
+
"""启动多连接建立流程。"""
|
|
845
|
+
if max_conns <= 1:
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
if self._scaling_frozen:
|
|
849
|
+
return
|
|
850
|
+
|
|
851
|
+
if module_id in self._modules_exiting:
|
|
852
|
+
return
|
|
853
|
+
|
|
854
|
+
current_slots = len(self.connections.get(module_id, {}))
|
|
855
|
+
if current_slots >= max_conns:
|
|
856
|
+
return
|
|
857
|
+
|
|
858
|
+
is_elastic = self._module_conn_elastic.get(module_id, False)
|
|
859
|
+
missing_slots = [s for s in range(1, max_conns) if s not in self.connections.get(module_id, {})]
|
|
860
|
+
if not missing_slots:
|
|
861
|
+
return
|
|
862
|
+
|
|
863
|
+
if not is_elastic:
|
|
864
|
+
# 非弹性:立即发 offer 建满
|
|
865
|
+
self._send_connection_offer(module_id, missing_slots)
|
|
866
|
+
else:
|
|
867
|
+
# 弹性:先建 1 个附加连接,后续根据压力动态调整
|
|
868
|
+
self._send_connection_offer(module_id, missing_slots[:1])
|
|
869
|
+
# 启动弹性收缩监控
|
|
870
|
+
if module_id not in self._shrink_tasks:
|
|
871
|
+
self._shrink_tasks[module_id] = asyncio.create_task(
|
|
872
|
+
self._elastic_shrink_loop(module_id, max_conns)
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
async def _elastic_shrink_loop(self, module_id: str, max_conns: int):
|
|
876
|
+
"""弹性收缩循环:idle 30s → release 1 个最空闲 slot,最少保留 2 个。"""
|
|
877
|
+
try:
|
|
878
|
+
while True:
|
|
879
|
+
await asyncio.sleep(30)
|
|
880
|
+
slots = self.connections.get(module_id, {})
|
|
881
|
+
if len(slots) <= 2:
|
|
882
|
+
continue # 最少保留 2 个
|
|
883
|
+
|
|
884
|
+
# 检查队列压力
|
|
885
|
+
level = self.event_hub._queue_levels.get(module_id, "idle")
|
|
886
|
+
if level != "idle":
|
|
887
|
+
continue # 非空闲不收缩
|
|
888
|
+
|
|
889
|
+
# 找最空闲的 slot(避开保序绑定的 slot 和 slot 0)
|
|
890
|
+
bound_slots = set()
|
|
891
|
+
for group in self.event_hub._ordering_groups.get(module_id, []):
|
|
892
|
+
if group.bound_slot is not None:
|
|
893
|
+
bound_slots.add(group.bound_slot)
|
|
894
|
+
|
|
895
|
+
candidates = [s for s in slots if s != 0 and s not in bound_slots]
|
|
896
|
+
if not candidates:
|
|
897
|
+
continue
|
|
898
|
+
|
|
899
|
+
# Release 最后一个(编号最大的)
|
|
900
|
+
to_release = max(candidates)
|
|
901
|
+
self._send_connection_release(module_id, [to_release])
|
|
902
|
+
except asyncio.CancelledError:
|
|
903
|
+
pass
|
|
904
|
+
finally:
|
|
905
|
+
self._shrink_tasks.pop(module_id, None)
|
|
906
|
+
|
|
268
907
|
# ── Debounce & Launcher loss ──
|
|
269
908
|
|
|
270
909
|
async def _debounce_offline(self, module_id: str):
|
|
271
|
-
"""Wait 5s after WS disconnect. If
|
|
910
|
+
"""Wait 5s after WS disconnect. If instance doesn't reconnect, mark offline.
|
|
911
|
+
module_id here is actually instance_key (e.g. 'assistant' or 'assistant#2').
|
|
912
|
+
"""
|
|
272
913
|
try:
|
|
273
914
|
await asyncio.sleep(5)
|
|
274
915
|
except asyncio.CancelledError:
|
|
275
|
-
return #
|
|
916
|
+
return # Instance reconnected within 5s — cancelled by ws_endpoint
|
|
276
917
|
|
|
277
|
-
# 5s elapsed,
|
|
918
|
+
# 5s elapsed, instance did not reconnect
|
|
278
919
|
self._debounce_tasks.pop(module_id, None)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
920
|
+
base_mid = parse_instance_key(module_id)[0]
|
|
921
|
+
|
|
922
|
+
# Check if other instances of this module are still connected
|
|
923
|
+
if self.event_hub.has_other_instances(base_mid, module_id):
|
|
924
|
+
# Other instances alive — don't deregister module, just log
|
|
925
|
+
print(f"[kernel] {module_id} offline (other instances still active)")
|
|
926
|
+
else:
|
|
927
|
+
# Last instance → deregister module and publish offline event
|
|
928
|
+
self.registry.deregister_module(base_mid)
|
|
929
|
+
self.event_hub.publish_internal("module.offline", {"module_id": base_mid}, source=self.module_id)
|
|
930
|
+
print(f"[kernel] {module_id} offline → deregistered (5s debounce expired)")
|
|
282
931
|
|
|
283
932
|
# If launcher went offline, start 35s launcher loss timer
|
|
284
|
-
if
|
|
933
|
+
if base_mid == "launcher":
|
|
285
934
|
self._launcher_connected = False
|
|
286
935
|
if not self._launcher_loss_task:
|
|
287
936
|
self._launcher_loss_task = asyncio.create_task(
|
|
@@ -309,6 +958,251 @@ class KernelServer:
|
|
|
309
958
|
# Shutdown Kernel itself
|
|
310
959
|
await self.shutdown()
|
|
311
960
|
|
|
961
|
+
# ── Instance auto-scaler ──
|
|
962
|
+
|
|
963
|
+
# Thresholds (aligned with design doc 4.6)
|
|
964
|
+
_SCALE_CHECK_INTERVAL = 5 # seconds between scaler checks
|
|
965
|
+
_SCALE_UP_HOLD = 30 # all instances overload for this long → scale up
|
|
966
|
+
_SCALE_DOWN_HOLD = 30 # instances idle for this long → scale down
|
|
967
|
+
_SCALE_UP_COOLDOWN = 30 # seconds after scale-up before another scale-up
|
|
968
|
+
_SCALE_DOWN_COOLDOWN = 15 # seconds after scale-down before another scale-down
|
|
969
|
+
|
|
970
|
+
async def _instance_scaler_loop(self):
|
|
971
|
+
"""Periodically check instance pressure and auto-scale elastic modules."""
|
|
972
|
+
try:
|
|
973
|
+
while True:
|
|
974
|
+
await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
|
|
975
|
+
if self._shutting_down:
|
|
976
|
+
return
|
|
977
|
+
try:
|
|
978
|
+
await self._scaler_tick()
|
|
979
|
+
except Exception as e:
|
|
980
|
+
print(f"[kernel] Scaler error: {e}")
|
|
981
|
+
except asyncio.CancelledError:
|
|
982
|
+
pass
|
|
983
|
+
|
|
984
|
+
async def _scaler_tick(self):
|
|
985
|
+
"""One iteration of the scaler loop."""
|
|
986
|
+
if self._scaling_frozen or self._scaler_running:
|
|
987
|
+
return
|
|
988
|
+
self._scaler_running = True
|
|
989
|
+
try:
|
|
990
|
+
await self._scaler_tick_inner()
|
|
991
|
+
finally:
|
|
992
|
+
self._scaler_running = False
|
|
993
|
+
|
|
994
|
+
async def _scaler_tick_inner(self):
|
|
995
|
+
"""Actual scaler logic, guarded by _scaler_running flag."""
|
|
996
|
+
now = time.time()
|
|
997
|
+
|
|
998
|
+
for module_id, config in list(self._module_instance_config.items()):
|
|
999
|
+
max_inst = config.get("max_instances", 1)
|
|
1000
|
+
|
|
1001
|
+
# 远程模块(通过 Relay 连入)不受 Launcher 生命周期管理,跳过实例扩缩容
|
|
1002
|
+
# 多连接扩缩(_multi_conn_tick)不受影响
|
|
1003
|
+
if self.registry.is_remote_module(module_id):
|
|
1004
|
+
continue
|
|
1005
|
+
|
|
1006
|
+
inst_elastic = config.get("instance_elastic", False)
|
|
1007
|
+
|
|
1008
|
+
# Get all active instances and their pressure
|
|
1009
|
+
inst_keys = self.event_hub.get_instance_keys(module_id)
|
|
1010
|
+
# Exclude draining instances from active count
|
|
1011
|
+
active_keys = [k for k in inst_keys if not self.event_hub.is_draining(k)]
|
|
1012
|
+
current_count = len(active_keys) + self._scale_pending.get(module_id, 0)
|
|
1013
|
+
|
|
1014
|
+
# ── Fixed scale: 一次性扩/缩到目标数量 ──
|
|
1015
|
+
if not inst_elastic:
|
|
1016
|
+
if current_count < max_inst:
|
|
1017
|
+
while current_count < max_inst:
|
|
1018
|
+
await self._scale_up(module_id, current_count)
|
|
1019
|
+
current_count += 1
|
|
1020
|
+
# Fixed scale 一次性扩完,不需要 cooldown
|
|
1021
|
+
self._scale_cooldown_until.pop(module_id, None)
|
|
1022
|
+
elif current_count > max_inst:
|
|
1023
|
+
# 缩容:一次性 drain 所有超出的实例
|
|
1024
|
+
excess = current_count - max_inst
|
|
1025
|
+
for _ in range(excess):
|
|
1026
|
+
await self._scale_down(module_id, active_keys)
|
|
1027
|
+
# 更新 active_keys(排除刚标记为 draining 的)
|
|
1028
|
+
active_keys = [k for k in inst_keys if not self.event_hub.is_draining(k)]
|
|
1029
|
+
continue
|
|
1030
|
+
|
|
1031
|
+
# Cooldown check(仅弹性模式需要)
|
|
1032
|
+
if now < self._scale_cooldown_until.get(module_id, 0):
|
|
1033
|
+
continue
|
|
1034
|
+
|
|
1035
|
+
# ── Elastic scale ──
|
|
1036
|
+
pressures = {}
|
|
1037
|
+
for k in active_keys:
|
|
1038
|
+
p = self.event_hub.get_instance_pressure(k)
|
|
1039
|
+
if p:
|
|
1040
|
+
pressures[k] = p
|
|
1041
|
+
|
|
1042
|
+
# ── Scale UP check ──
|
|
1043
|
+
if current_count < max_inst:
|
|
1044
|
+
all_overload = (
|
|
1045
|
+
len(pressures) > 0 and
|
|
1046
|
+
all(p.get("level") in ("overload", "critical") for p in pressures.values())
|
|
1047
|
+
)
|
|
1048
|
+
if all_overload:
|
|
1049
|
+
since = self._scale_overload_since.get(module_id)
|
|
1050
|
+
if since is None:
|
|
1051
|
+
self._scale_overload_since[module_id] = now
|
|
1052
|
+
elif now - since >= self._SCALE_UP_HOLD:
|
|
1053
|
+
await self._scale_up(module_id, current_count)
|
|
1054
|
+
self._scale_overload_since.pop(module_id, None)
|
|
1055
|
+
else:
|
|
1056
|
+
self._scale_overload_since.pop(module_id, None)
|
|
1057
|
+
|
|
1058
|
+
# ── Scale DOWN check ──
|
|
1059
|
+
if len(active_keys) > 1:
|
|
1060
|
+
all_idle = (
|
|
1061
|
+
len(pressures) > 0 and
|
|
1062
|
+
all(p.get("level") == "idle" for p in pressures.values())
|
|
1063
|
+
)
|
|
1064
|
+
if all_idle:
|
|
1065
|
+
since = self._scale_idle_since.get(module_id)
|
|
1066
|
+
if since is None:
|
|
1067
|
+
self._scale_idle_since[module_id] = now
|
|
1068
|
+
elif now - since >= self._SCALE_DOWN_HOLD:
|
|
1069
|
+
await self._scale_down(module_id, active_keys)
|
|
1070
|
+
self._scale_idle_since.pop(module_id, None)
|
|
1071
|
+
else:
|
|
1072
|
+
self._scale_idle_since.pop(module_id, None)
|
|
1073
|
+
|
|
1074
|
+
async def _scale_up(self, module_id: str, current_count: int):
|
|
1075
|
+
"""Request Launcher to start a new instance."""
|
|
1076
|
+
new_num = current_count + 1
|
|
1077
|
+
print(f"[kernel] Auto-scaling UP: {module_id} → instance #{new_num}")
|
|
1078
|
+
|
|
1079
|
+
self._scale_pending[module_id] = self._scale_pending.get(module_id, 0) + 1
|
|
1080
|
+
self._scale_cooldown_until[module_id] = time.time() + self._SCALE_UP_COOLDOWN
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
result = await self._call_launcher_rpc("start_module_instance", {
|
|
1084
|
+
"name": module_id,
|
|
1085
|
+
"instance_num": new_num,
|
|
1086
|
+
})
|
|
1087
|
+
if result:
|
|
1088
|
+
self.event_hub.publish_internal("system.instance.started", {
|
|
1089
|
+
"module_id": module_id,
|
|
1090
|
+
"instance_num": new_num,
|
|
1091
|
+
"reason": "auto_scale",
|
|
1092
|
+
}, source=self.module_id)
|
|
1093
|
+
print(f"[kernel] Scale-up requested: {module_id}#{new_num}")
|
|
1094
|
+
except Exception as e:
|
|
1095
|
+
print(f"[kernel] Scale-up failed for {module_id}: {e}")
|
|
1096
|
+
finally:
|
|
1097
|
+
pending = self._scale_pending.get(module_id, 1)
|
|
1098
|
+
self._scale_pending[module_id] = max(0, pending - 1)
|
|
1099
|
+
|
|
1100
|
+
async def _scale_down(self, module_id: str, inst_keys: list[str]):
|
|
1101
|
+
"""Mark one instance as draining; stop it when its queue empties."""
|
|
1102
|
+
# 候选:排除 primary(#1)和已在 draining 的实例
|
|
1103
|
+
candidates = []
|
|
1104
|
+
for k in inst_keys:
|
|
1105
|
+
_, num = parse_instance_key(k)
|
|
1106
|
+
if num <= 1:
|
|
1107
|
+
continue # Never scale down the primary instance
|
|
1108
|
+
if self.event_hub.is_draining(k):
|
|
1109
|
+
continue # Already draining
|
|
1110
|
+
pressure = self.event_hub.get_instance_pressure(k) or {}
|
|
1111
|
+
depth = pressure.get("depth", 0)
|
|
1112
|
+
candidates.append((k, depth))
|
|
1113
|
+
|
|
1114
|
+
if not candidates:
|
|
1115
|
+
return
|
|
1116
|
+
|
|
1117
|
+
# 优先选队列最浅的实例(最接近空闲)
|
|
1118
|
+
candidates.sort(key=lambda x: x[1])
|
|
1119
|
+
target_key, _ = candidates[0]
|
|
1120
|
+
_, target_num = parse_instance_key(target_key)
|
|
1121
|
+
|
|
1122
|
+
print(f"[kernel] Scale-down: draining {target_key}")
|
|
1123
|
+
self.event_hub.set_draining(target_key)
|
|
1124
|
+
self._scale_cooldown_until[module_id] = time.time() + self._SCALE_DOWN_COOLDOWN
|
|
1125
|
+
|
|
1126
|
+
# 异步等待队列清空后关闭
|
|
1127
|
+
asyncio.ensure_future(self._drain_and_stop(module_id, target_key, target_num))
|
|
1128
|
+
|
|
1129
|
+
async def _drain_and_stop(self, module_id: str, inst_key: str, instance_num: int):
|
|
1130
|
+
"""Wait for instance queue to empty, then stop it via Launcher."""
|
|
1131
|
+
try:
|
|
1132
|
+
# 首先等待至少收到一次压力报告,避免刚启动未上报就被误判为空
|
|
1133
|
+
for _ in range(6): # 最多等 6 * SCALE_CHECK_INTERVAL
|
|
1134
|
+
await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
|
|
1135
|
+
if self._scaling_frozen:
|
|
1136
|
+
self.event_hub.clear_draining(inst_key)
|
|
1137
|
+
return
|
|
1138
|
+
if self.event_hub.get_instance_pressure(inst_key) is not None:
|
|
1139
|
+
break
|
|
1140
|
+
|
|
1141
|
+
while True:
|
|
1142
|
+
await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
|
|
1143
|
+
if self._scaling_frozen:
|
|
1144
|
+
# 系统冻结(如关机),直接放弃 drain,由关闭流程处理
|
|
1145
|
+
self.event_hub.clear_draining(inst_key)
|
|
1146
|
+
return
|
|
1147
|
+
pressure = self.event_hub.get_instance_pressure(inst_key) or {}
|
|
1148
|
+
depth = pressure.get("depth", 0)
|
|
1149
|
+
if depth == 0:
|
|
1150
|
+
break
|
|
1151
|
+
# 队列已空,通知 Launcher 停止该实例
|
|
1152
|
+
print(f"[kernel] Scale-down: stopping {inst_key} (queue empty)")
|
|
1153
|
+
await self._call_launcher_rpc("stop_module_instance", {
|
|
1154
|
+
"name": module_id,
|
|
1155
|
+
"instance_num": instance_num,
|
|
1156
|
+
})
|
|
1157
|
+
self.event_hub.publish_internal("system.instance.stopped", {
|
|
1158
|
+
"module_id": module_id,
|
|
1159
|
+
"instance_num": instance_num,
|
|
1160
|
+
"reason": "auto_scale",
|
|
1161
|
+
}, source=self.module_id)
|
|
1162
|
+
print(f"[kernel] Scale-down completed: {inst_key}")
|
|
1163
|
+
except Exception as e:
|
|
1164
|
+
print(f"[kernel] Scale-down failed for {inst_key}: {e}")
|
|
1165
|
+
finally:
|
|
1166
|
+
self.event_hub.clear_draining(inst_key)
|
|
1167
|
+
|
|
1168
|
+
async def _call_launcher_rpc(self, method: str, params: dict, timeout: float = 10) -> dict | None:
|
|
1169
|
+
"""Call a Launcher RPC method and wait for response."""
|
|
1170
|
+
import uuid
|
|
1171
|
+
launcher_slots = self.connections.get("launcher", {})
|
|
1172
|
+
if not launcher_slots:
|
|
1173
|
+
raise RuntimeError("Launcher not connected")
|
|
1174
|
+
ws = next(iter(launcher_slots.values()))
|
|
1175
|
+
|
|
1176
|
+
rpc_id = f"kernel-{uuid.uuid4().hex[:8]}"
|
|
1177
|
+
await ws.send_text(json.dumps({
|
|
1178
|
+
"jsonrpc": "2.0",
|
|
1179
|
+
"id": rpc_id,
|
|
1180
|
+
"method": method,
|
|
1181
|
+
"params": params,
|
|
1182
|
+
}))
|
|
1183
|
+
|
|
1184
|
+
# Wait for response via rpc_router pending mechanism
|
|
1185
|
+
# Use a simpler approach: the response will come back through handle_response
|
|
1186
|
+
# which routes back to Kernel. We use an asyncio.Event to wait.
|
|
1187
|
+
future = asyncio.get_event_loop().create_future()
|
|
1188
|
+
self._rpc_futures[rpc_id] = future
|
|
1189
|
+
|
|
1190
|
+
try:
|
|
1191
|
+
result = await asyncio.wait_for(future, timeout=timeout)
|
|
1192
|
+
return result
|
|
1193
|
+
except asyncio.TimeoutError:
|
|
1194
|
+
self._rpc_futures.pop(rpc_id, None)
|
|
1195
|
+
raise RuntimeError(f"Launcher RPC timeout: {method}")
|
|
1196
|
+
|
|
1197
|
+
def _resolve_rpc_future(self, rpc_id: str, result: dict = None, error: str = None):
|
|
1198
|
+
"""Resolve a pending Kernel→Launcher RPC future."""
|
|
1199
|
+
future = self._rpc_futures.pop(rpc_id, None)
|
|
1200
|
+
if future and not future.done():
|
|
1201
|
+
if error:
|
|
1202
|
+
future.set_exception(RuntimeError(error))
|
|
1203
|
+
else:
|
|
1204
|
+
future.set_result(result)
|
|
1205
|
+
|
|
312
1206
|
# ── Self-registration ──
|
|
313
1207
|
|
|
314
1208
|
def self_register(self):
|
|
@@ -316,8 +1210,8 @@ class KernelServer:
|
|
|
316
1210
|
self.registry.register_module({
|
|
317
1211
|
"module_id": self.module_id,
|
|
318
1212
|
"module_type": "infrastructure",
|
|
319
|
-
"
|
|
320
|
-
"
|
|
1213
|
+
"base_url": f"http://{self.advertise_ip}:{self.port}",
|
|
1214
|
+
"health_path": "/health",
|
|
321
1215
|
"metadata": {
|
|
322
1216
|
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
323
1217
|
},
|
|
@@ -325,11 +1219,18 @@ class KernelServer:
|
|
|
325
1219
|
|
|
326
1220
|
def publish_ready(self):
|
|
327
1221
|
"""Publish module.ready event for Kernel (internal, no WS needed)."""
|
|
1222
|
+
import time
|
|
1223
|
+
startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
1224
|
+
reason = "recovery" if self.is_recover else "startup"
|
|
328
1225
|
self.event_hub.publish_internal("module.ready", {
|
|
329
1226
|
"module_id": self.module_id,
|
|
330
1227
|
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
331
1228
|
"graceful_shutdown": True,
|
|
1229
|
+
"queue_elastic": True,
|
|
1230
|
+
"startup_time": startup_time,
|
|
1231
|
+
"reason": reason,
|
|
332
1232
|
}, source=self.module_id)
|
|
1233
|
+
self._ready_published = True
|
|
333
1234
|
|
|
334
1235
|
async def _handle_internal_event(self, event_type: str, data: dict):
|
|
335
1236
|
"""内部事件处理器(通过 EventHub 回调机制调用)
|
|
@@ -347,10 +1248,21 @@ class KernelServer:
|
|
|
347
1248
|
# 处理 Kernel 订阅的事件
|
|
348
1249
|
if event_type == "module.shutdown":
|
|
349
1250
|
await self.handle_shutdown_event(data)
|
|
1251
|
+
elif event_type == "system.pong":
|
|
1252
|
+
await self._handle_pong_event(data)
|
|
1253
|
+
elif event_type == "system.ready":
|
|
1254
|
+
# 系统启动完成,立即发送第一次 ping
|
|
1255
|
+
print(f"[kernel] Received system.ready event, triggering first ping")
|
|
1256
|
+
# 检查 recovered 标记:崩溃恢复时重新启动 Ping/Pong
|
|
1257
|
+
if data.get("recovered"):
|
|
1258
|
+
self._first_ping_sent = False
|
|
1259
|
+
print(f"[kernel] system.ready (recovered=true), resetting ping state")
|
|
1260
|
+
await self._send_first_ping()
|
|
350
1261
|
# 忽略系统广播事件(Kernel 没有订阅但会收到广播)
|
|
351
1262
|
elif event_type in ("module.ready", "module.registered", "module.started",
|
|
352
1263
|
"module.stopped", "module.crashed", "module.exiting",
|
|
353
|
-
"module.offline", "
|
|
1264
|
+
"module.offline", "module.degraded", "module.recovered",
|
|
1265
|
+
"registry.updated"):
|
|
354
1266
|
# 这些是系统事件,会广播给所有模块,Kernel 收到是正常的
|
|
355
1267
|
pass
|
|
356
1268
|
else:
|
|
@@ -363,6 +1275,44 @@ class KernelServer:
|
|
|
363
1275
|
traceback.print_exc()
|
|
364
1276
|
raise # 重新抛出,让 EventHub 的包装器记录
|
|
365
1277
|
|
|
1278
|
+
async def _handle_pong_event(self, data: dict):
|
|
1279
|
+
"""处理 system.pong 事件,计算往返延迟
|
|
1280
|
+
|
|
1281
|
+
Args:
|
|
1282
|
+
data: {
|
|
1283
|
+
"module_id": str,
|
|
1284
|
+
"ping_time": float, # t1 - Kernel 发送 ping 的时间
|
|
1285
|
+
"pong_time": float, # t2 - 模块收到 ping 并发送 pong 的时间
|
|
1286
|
+
}
|
|
1287
|
+
"""
|
|
1288
|
+
import time
|
|
1289
|
+
module_id = data.get("module_id")
|
|
1290
|
+
t1 = data.get("ping_time") # Kernel 发送 ping 的时间
|
|
1291
|
+
t2 = data.get("pong_time") # 模块收到 ping 并发送 pong 的时间
|
|
1292
|
+
t3 = time.time() # Kernel 收到 pong 的时间
|
|
1293
|
+
|
|
1294
|
+
if not module_id or t1 is None or t2 is None:
|
|
1295
|
+
return
|
|
1296
|
+
|
|
1297
|
+
# 验证 ping_time 是否匹配
|
|
1298
|
+
if module_id not in self._ping_sent_times:
|
|
1299
|
+
return
|
|
1300
|
+
|
|
1301
|
+
expected_t1 = self._ping_sent_times[module_id]
|
|
1302
|
+
if abs(t1 - expected_t1) > 0.1: # 允许 100ms 误差
|
|
1303
|
+
return
|
|
1304
|
+
|
|
1305
|
+
# 计算两个延迟
|
|
1306
|
+
outbound_ms = (t2 - t1) * 1000 # 去程:Kernel → 模块
|
|
1307
|
+
inbound_ms = (t3 - t2) * 1000 # 回程:模块 → Kernel
|
|
1308
|
+
|
|
1309
|
+
self._pong_latencies[module_id] = {
|
|
1310
|
+
"outbound": round(outbound_ms, 2),
|
|
1311
|
+
"inbound": round(inbound_ms, 2),
|
|
1312
|
+
"last_update": t3,
|
|
1313
|
+
}
|
|
1314
|
+
self._pong_status[module_id] = "ok"
|
|
1315
|
+
|
|
366
1316
|
async def handle_shutdown_event(self, event_data):
|
|
367
1317
|
"""处理 module.shutdown 事件(标准优雅退出流程)
|
|
368
1318
|
|
|
@@ -378,10 +1328,12 @@ class KernelServer:
|
|
|
378
1328
|
print(f"[kernel] Warning: Invalid shutdown event data type: {type(event_data)}")
|
|
379
1329
|
return
|
|
380
1330
|
|
|
381
|
-
#
|
|
1331
|
+
# 判断 shutdown 目标
|
|
382
1332
|
target_module = event_data.get("module_id")
|
|
383
1333
|
if target_module != self.module_id:
|
|
384
|
-
|
|
1334
|
+
# 其他模块的 shutdown 通知:标记该模块正在退出,不再发 offer
|
|
1335
|
+
if target_module:
|
|
1336
|
+
self._modules_exiting.add(target_module)
|
|
385
1337
|
return
|
|
386
1338
|
|
|
387
1339
|
# 防御性检查:防止重复处理
|
|
@@ -422,13 +1374,14 @@ class KernelServer:
|
|
|
422
1374
|
# Step 5: 等待一小段时间确保事件发送完成
|
|
423
1375
|
await asyncio.sleep(0.2)
|
|
424
1376
|
|
|
425
|
-
# Step 6: 关闭所有 WebSocket 连接(包括 Launcher)
|
|
1377
|
+
# Step 6: 关闭所有 WebSocket 连接(包括 Launcher,所有 slot)
|
|
426
1378
|
close_tasks = []
|
|
427
|
-
for mid,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1379
|
+
for mid, slots in list(self.connections.items()):
|
|
1380
|
+
for s, ws in list(slots.items()):
|
|
1381
|
+
try:
|
|
1382
|
+
close_tasks.append(ws.close(code=1000, reason="Graceful shutdown complete"))
|
|
1383
|
+
except Exception as e:
|
|
1384
|
+
print(f"[kernel] 关闭 {mid} slot {s} 连接失败: {e}")
|
|
432
1385
|
|
|
433
1386
|
if close_tasks:
|
|
434
1387
|
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
@@ -460,6 +1413,14 @@ class KernelServer:
|
|
|
460
1413
|
self._shutting_down = True
|
|
461
1414
|
print("[kernel] 执行清理工作...")
|
|
462
1415
|
|
|
1416
|
+
# 取消所有队列压力倒计时
|
|
1417
|
+
if self.event_hub._critical_timers:
|
|
1418
|
+
print(f"[kernel] 取消 {len(self.event_hub._critical_timers)} 个压力倒计时")
|
|
1419
|
+
for task in self.event_hub._critical_timers.values():
|
|
1420
|
+
if not task.done():
|
|
1421
|
+
task.cancel()
|
|
1422
|
+
self.event_hub._critical_timers.clear()
|
|
1423
|
+
|
|
463
1424
|
# 取消所有 debounce 任务
|
|
464
1425
|
if self._debounce_tasks:
|
|
465
1426
|
print(f"[kernel] 取消 {len(self._debounce_tasks)} 个 debounce 任务")
|
|
@@ -474,15 +1435,28 @@ class KernelServer:
|
|
|
474
1435
|
self._launcher_loss_task.cancel()
|
|
475
1436
|
self._launcher_loss_task = None
|
|
476
1437
|
|
|
1438
|
+
# 取消多连接相关任务
|
|
1439
|
+
for name, task_dict in [("offer", self._offer_tasks),
|
|
1440
|
+
("shrink", self._shrink_tasks),
|
|
1441
|
+
("release", self._release_timers)]:
|
|
1442
|
+
if task_dict:
|
|
1443
|
+
print(f"[kernel] 取消 {len(task_dict)} 个 {name} 任务")
|
|
1444
|
+
for task in task_dict.values():
|
|
1445
|
+
if not task.done():
|
|
1446
|
+
task.cancel()
|
|
1447
|
+
task_dict.clear()
|
|
1448
|
+
|
|
477
1449
|
# 关闭其他模块的 WebSocket 连接(保留 Launcher 连接)
|
|
478
|
-
|
|
479
|
-
if
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1450
|
+
other_mids = [mid for mid in self.connections if mid != "launcher"]
|
|
1451
|
+
if other_mids:
|
|
1452
|
+
count = sum(len(self.connections.get(mid, {})) for mid in other_mids)
|
|
1453
|
+
print(f"[kernel] 关闭 {count} 个其他模块的 WebSocket 连接")
|
|
1454
|
+
for mid in other_mids:
|
|
1455
|
+
for slot, ws in list(self.connections.get(mid, {}).items()):
|
|
1456
|
+
try:
|
|
1457
|
+
await ws.close(code=1001, reason="Server shutting down")
|
|
1458
|
+
except Exception as e:
|
|
1459
|
+
print(f"[kernel] 关闭连接失败 {mid} slot {slot}: {e}")
|
|
486
1460
|
|
|
487
1461
|
# 清空 RPC 转发队列
|
|
488
1462
|
pending_count = len(self.rpc_router._pending)
|
|
@@ -517,13 +1491,15 @@ class KernelServer:
|
|
|
517
1491
|
self._launcher_loss_task.cancel()
|
|
518
1492
|
self._launcher_loss_task = None
|
|
519
1493
|
|
|
520
|
-
# Close all WebSocket connections
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1494
|
+
# Close all WebSocket connections (multi-slot)
|
|
1495
|
+
total_slots = sum(len(s) for s in self.connections.values())
|
|
1496
|
+
print(f"[kernel] Closing {total_slots} WebSocket connections...")
|
|
1497
|
+
for module_id, slots in list(self.connections.items()):
|
|
1498
|
+
for slot, ws in list(slots.items()):
|
|
1499
|
+
try:
|
|
1500
|
+
await ws.close(code=1001, reason="Server shutting down")
|
|
1501
|
+
except Exception as e:
|
|
1502
|
+
print(f"[kernel] Failed to close connection for {module_id} slot {slot}: {e}")
|
|
527
1503
|
self.connections.clear()
|
|
528
1504
|
|
|
529
1505
|
# Clear pending RPC forwards
|