@agentunion/kite 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/kite/checklists/feature-checklist.md +496 -0
- package/.claude/skills/kite/references/event-patterns.md +180 -0
- package/.claude/skills/kite/references/health-check.md +202 -0
- package/.claude/skills/kite/references/http-service.md +199 -0
- package/.claude/skills/kite/references/module-md-spec.md +172 -0
- package/.claude/skills/kite/references/multi-connection.md +147 -0
- package/.claude/skills/kite/references/rpc-patterns.md +199 -0
- package/.claude/skills/kite/references/shutdown-sequence.md +146 -0
- package/.claude/skills/kite/references/stdin-protocol.md +147 -0
- package/.claude/skills/kite/references/test-center-integration.md +178 -0
- package/.claude/skills/kite/references/ws-lifecycle.md +301 -0
- package/.claude/skills/kite/skill.md +272 -0
- package/.claude/skills/kite/templates/go/README.md +20 -0
- package/.claude/skills/kite/templates/node/entry.js +134 -0
- package/.claude/skills/kite/templates/node/module.md +16 -0
- package/.claude/skills/kite/templates/node/server.js +351 -0
- package/.claude/skills/kite/templates/node/server_http.js +90 -0
- package/.claude/skills/kite/templates/python/entry.py +425 -0
- package/.claude/skills/kite/templates/python/module.md +26 -0
- package/.claude/skills/kite/templates/python/server.py +447 -0
- package/.claude/skills/kite/templates/python/server_http.py +433 -0
- package/cli.js +38 -4
- package/core/env_checker.py +96 -0
- package/docs/05-/347/237/255/344/277/241/350/256/244/350/257/201/344/270/216/347/224/250/346/210/267/344/277/241/346/201/257/346/216/245/345/217/243/346/226/207/346/241/243.md +507 -0
- package/docs/ACP/345/215/217/350/256/256/345/205/274/345/256/271/346/226/271/346/241/210.md +138 -0
- package/docs/CI/344/270/216AI/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/226/271/346/241/210.md +75 -0
- package/docs/CLI/345/274/200/345/217/221/350/256/241/345/210/222.md +595 -0
- package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237-/346/212/200/346/234/257/350/257/204/344/274/260.md +535 -0
- package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237/350/256/276/350/256/241.md +631 -0
- package/docs/Evol-App/344/275/277/347/224/250KernelClient/346/224/271/351/200/240/345/256/214/346/210/220.md +342 -0
- package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/346/246/202/350/246/201.md +604 -0
- package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241.md +1708 -0
- package/docs/Evol/346/250/241/345/235/227/350/256/276/350/256/241/346/226/271/346/241/210.md +1154 -0
- package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-Evol/346/250/241/345/235/227/345/256/236/346/226/275/346/214/207/345/215/227.md +403 -0
- package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-/345/244/226/351/203/250/346/250/241/345/235/227/346/216/245/345/205/245/346/214/207/345/215/227.md +468 -0
- package/docs/HTTP-RPC/350/277/201/347/247/273/345/210/260WebSocket/350/256/241/345/210/222.md +318 -0
- package/docs/INDEX.md +388 -0
- package/docs/KITE_DOCS_GUIDE.md +33 -0
- package/docs/Kernel-Client-Kite-Token/346/224/257/346/214/201/345/256/236/346/226/275/345/256/214/346/210/220.md +330 -0
- package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266-/346/255/243/347/241/256/345/256/236/347/216/260.md +235 -0
- package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266/345/256/236/346/226/275/346/200/273/347/273/223.md +204 -0
- package/docs/Kite/345/256/211/350/243/205/351/227/256/351/242/230/350/247/243/345/206/263/346/226/271/346/241/210.md +362 -0
- package/docs/Kite/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241-/347/273/210/346/236/201/347/233/256/346/240/207.md +721 -0
- package/docs/Kite/346/216/247/345/210/266/345/217/260/347/273/237/344/270/200WebSocket/346/224/271/351/200/240/346/226/271/346/241/210.md +821 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/01-/346/241/206/346/236/266/345/256/232/344/275/215.md +12 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/02-/346/240/270/345/277/203/346/246/202/345/277/265.md +341 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/03-/347/263/273/347/273/237/346/236/266/346/236/204.md +257 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/04-/346/250/241/345/235/227/350/247/204/350/214/203.md +263 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213-/346/226/260/347/211/210.md +267 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213.md +149 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/06-/347/233/256/345/275/225/347/273/223/346/236/204.md +231 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/07-/346/225/260/346/215/256/346/250/241/345/236/213.md +68 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/08-/346/211/251/345/261/225/346/200/247.md +34 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/09-/344/270/216/345/205/267/344/275/223/345/272/224/347/224/250/347/232/204/345/205/263/347/263/273.md +22 -0
- package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/README.md +46 -0
- package/docs/Kite/347/263/273/347/273/237/345/220/257/345/212/250/346/265/201/347/250/213.md +567 -0
- package/docs/Launcher/345/220/257/345/212/250/345/231/250/346/226/207/346/241/243.md +745 -0
- package/docs/Polyglot/350/277/220/350/241/214/346/227/266/344/270/216Clawdbot/345/205/274/345/256/271/346/200/247/350/256/276/350/256/241.md +321 -0
- package/docs/Redis/344/270/216/346/250/241/345/235/227/345/244/232/345/256/236/344/276/213/346/226/271/346/241/210.md +438 -0
- package/docs/Relay-Kite-Token/350/256/244/350/257/201/345/256/236/346/226/275/345/256/214/346/210/220.md +178 -0
- package/docs/Relay-Token/346/235/203/351/231/220/351/205/215/347/275/256/351/252/214/350/257/201.md +113 -0
- package/docs/Watchdog/345/201/245/345/272/267/346/243/200/346/237/245/344/270/216WebSocket-Ping/346/234/272/345/210/266/345/210/206/346/236/220.md +367 -0
- package/docs/Watchdog/350/265/204/346/272/220/347/233/221/346/216/247/347/255/226/347/225/245.md +92 -0
- package/docs/WebSocket/346/216/245/346/224/266/345/276/252/347/216/257/346/255/273/351/224/201/351/230/262/350/214/203/350/247/204/350/214/203.md +357 -0
- package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/344/270/216/351/207/215/350/277/236/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +531 -0
- package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/346/226/271/346/241/210.md +169 -0
- package/docs/WebSocket/351/207/215/350/277/236/346/234/272/345/210/266/346/265/213/350/257/225/346/212/245/345/221/212.md +169 -0
- package/docs/WebSocket/351/207/215/350/277/236/351/200/200/351/201/277/346/234/272/345/210/266/346/226/271/346/241/210.md +394 -0
- package/docs/Web/346/250/241/345/235/227/344/270/216Evol/346/250/241/345/235/227/351/207/215/346/236/204/345/210/206/346/236/220.md +521 -0
- package/docs/audit-api-guide.md +68 -0
- package/docs/audit-module-design.md +315 -0
- package/docs/audit-module-implementation-summary.md +149 -0
- package/docs/llm-context-design.md +52 -0
- package/docs/llm-test-enhancement-plan.md +970 -0
- package/docs/logs-api-guide.md +42 -0
- package/docs/npm/345/214/205Python/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +302 -0
- package/docs/npm/345/217/221/345/270/203/344/270/216CLI/344/275/277/347/224/250/346/214/207/345/215/227.md +245 -0
- package/docs/stdio/344/270/216/347/253/257/345/217/243/345/217/221/347/216/260/351/207/215/346/236/204.md +480 -0
- package/docs/web/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/350/256/276/350/256/241/346/226/271/346/241/210.md +449 -0
- package/docs//344/272/213/344/273/266/345/244/204/347/220/206/346/234/272/345/210/266.md +388 -0
- package/docs//344/272/213/344/273/266/345/244/204/347/220/206/350/247/204/350/214/203.md +113 -0
- package/docs//344/272/213/344/273/266/350/256/242/351/230/205/351/200/232/351/205/215/347/254/246/350/247/204/350/214/203.md +256 -0
- package/docs//344/272/213/344/273/266/351/230/237/345/210/227/345/274/271/346/200/247/347/256/241/347/220/206.md +449 -0
- package/docs//344/272/244/344/272/222/345/274/217/347/273/210/347/253/257/346/216/247/345/210/266/346/226/271/346/241/210.md +301 -0
- package/docs//344/273/243/347/220/206/345/220/257/345/212/250/345/231/250/344/270/216/345/256/271/345/231/250/345/214/226.md +140 -0
- package/docs//344/273/243/347/240/201/347/273/237/350/256/241/345/267/245/345/205/267/344/275/277/347/224/250/350/257/264/346/230/216.md +217 -0
- package/docs//344/274/230/351/233/205/351/200/200/345/207/272/350/247/204/350/214/203.md +362 -0
- package/docs//344/276/235/350/265/226/347/256/241/347/220/206/350/257/264/346/230/216.md +141 -0
- package/docs//344/277/256/345/244/215/346/235/203/351/231/220/351/227/256/351/242/230-evol-RPC/346/235/203/351/231/220.md +268 -0
- package/docs//345/210/240/351/231/244kernel-client-example/345/256/214/346/210/220.md +309 -0
- package/docs//345/210/240/351/231/244ws-management/345/256/214/346/210/220.md +418 -0
- package/docs//345/220/257/345/212/250/344/274/230/345/214/226/346/226/271/346/241/210.md +522 -0
- package/docs//345/220/257/345/212/250/344/276/235/350/265/226/344/270/216/346/216/222/345/272/217.md +105 -0
- package/docs//345/256/211/350/243/205/350/204/232/346/234/254/345/274/200/345/217/221/346/226/207/346/241/243.md +643 -0
- package/docs//345/256/214/346/225/264/345/220/257/345/212/250/346/265/201/347/250/213/350/256/276/350/256/241.md +452 -0
- package/docs//345/256/236/347/216/260/350/247/204/345/210/222.md +195 -0
- package/docs//345/277/203/350/267/263/346/234/272/345/210/266/351/207/215/346/236/204/346/200/273/347/273/223.md +166 -0
- package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210-/345/256/211/345/205/250/345/256/241/346/237/245.md +176 -0
- package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210.md +908 -0
- package/docs//346/226/207/346/241/243/346/233/264/346/226/260/346/270/205/345/215/225.md +83 -0
- package/docs//346/227/245/345/277/227/344/270/216/345/274/202/345/270/270/345/244/204/347/220/206/350/247/204/350/214/203.md +829 -0
- package/docs//346/227/245/345/277/227/350/260/203/350/257/225/345/256/236/346/210/230/346/214/207/345/215/227.md +25 -0
- package/docs//346/236/266/346/236/204/345/200/237/351/211/264/346/214/207/345/215/227.md +977 -0
- package/docs//346/236/266/346/236/204/346/224/271/351/200/240-/345/256/214/346/210/220/346/200/273/347/273/223.md +440 -0
- package/docs//346/236/266/346/236/204/347/216/260/347/212/266/344/270/216/347/273/210/346/236/201/347/233/256/346/240/207/345/257/271/346/257/224/345/210/206/346/236/220.md +508 -0
- package/docs//346/250/241/345/235/227/345/244/232/350/277/236/346/216/245/346/216/247/345/210/266/347/255/226/347/225/245.md +220 -0
- package/docs//346/250/241/345/235/227/345/256/211/350/243/205/346/234/272/345/210/266/350/256/276/350/256/241.md +500 -0
- package/docs//346/250/241/345/235/227/345/274/200/345/217/221/346/214/207/345/215/227.md +1824 -0
- package/docs//346/250/241/345/235/227/347/203/255/346/233/264/346/226/260.md +89 -0
- package/docs//346/250/241/345/235/227/350/277/234/347/250/213/351/203/250/347/275/262/345/274/200/345/217/221/350/247/204/350/214/203.md +460 -0
- package/docs//346/250/241/345/235/227/351/200/200/345/207/272/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +303 -0
- package/docs//346/250/241/345/235/227/351/205/215/347/275/256/345/212/240/350/275/275/344/270/216/347/203/255/351/207/215/350/275/275/350/247/204/350/214/203.md +369 -0
- package/docs//346/265/213/350/257/225/344/270/255/345/277/203/346/267/273/345/212/240/346/250/241/345/235/227/346/265/213/350/257/225/346/214/207/345/215/227.md +147 -0
- package/docs//347/211/210/346/234/254/351/224/201/345/256/232/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +331 -0
- package/docs//347/216/257/345/242/203/345/217/230/351/207/217/344/270/216/350/277/220/350/241/214/346/227/266/347/233/256/345/275/225/350/256/276/350/256/241.md +499 -0
- package/docs//347/216/257/345/242/203/347/256/241/347/220/206/345/256/214/346/225/264/346/226/271/346/241/210.md +334 -0
- package/docs//350/231/232/346/213/237/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/345/256/214/346/225/264/350/256/276/350/256/241.md +1496 -0
- package/docs//350/231/232/346/213/237/347/216/257/345/242/203/345/267/245/344/275/234/345/216/237/347/220/206.md +163 -0
- package/docs//350/256/241/345/210/222/347/256/241/347/220/206/345/231/250/344/275/277/347/224/250/346/214/207/345/215/227.md +196 -0
- package/docs//350/256/244/350/257/201/346/250/241/345/235/227/344/270/216Gateway/350/256/276/350/256/241/346/226/271/346/241/210.md +765 -0
- package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241-/346/227/247/347/211/210.md +1117 -0
- package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241.md +451 -0
- package/docs//351/207/215/346/236/204/346/234/272/345/210/266/346/270/205/345/215/225.md +192 -0
- package/docs//351/223/276/350/267/257/350/277/275/350/270/252/346/226/271/346/241/210.md +242 -0
- package/docs//351/231/215/347/272/247/347/255/226/347/225/245/350/256/276/350/256/241/346/226/271/346/241/210.md +618 -0
- package/extensions/agents/assistant/entry.py +113 -14
- package/extensions/agents/assistant/module.md +27 -22
- package/extensions/agents/assistant/server.py +291 -105
- package/extensions/channels/acp_channel/entry.py +114 -16
- package/extensions/channels/acp_channel/module.md +4 -0
- package/extensions/channels/acp_channel/server.py +396 -105
- package/extensions/channels/phone_channel/__init__.py +1 -0
- package/extensions/channels/phone_channel/entry.py +503 -0
- package/extensions/channels/phone_channel/module.md +31 -0
- package/extensions/channels/phone_channel/server.py +686 -0
- package/extensions/event_hub_bench/entry.py +55 -12
- package/extensions/event_hub_bench/module.md +27 -27
- package/extensions/services/audit/README.md +134 -0
- package/extensions/services/audit/collector.py +73 -0
- package/extensions/services/audit/entry.py +444 -0
- package/extensions/services/audit/module.md +66 -0
- package/extensions/services/audit/query_audit.py +111 -0
- package/extensions/services/audit/routes/__init__.py +1 -0
- package/extensions/services/audit/routes/routes_audit.py +113 -0
- package/extensions/services/audit/schemas/__init__.py +5 -0
- package/extensions/services/audit/schemas/audit_event.py +92 -0
- package/extensions/services/audit/server.py +542 -0
- package/extensions/services/audit/storage.py +95 -0
- package/extensions/services/auth/entry.py +1054 -0
- package/extensions/services/auth/module.md +31 -0
- package/extensions/services/auth/token_store.py +185 -0
- package/extensions/services/auth/verifiers/evol_account.py +101 -0
- package/extensions/services/auth/verifiers/kite_token.py +38 -0
- package/extensions/services/auth/verifiers/pairing_code.py +71 -0
- package/extensions/services/backup/entry.py +494 -197
- package/extensions/services/backup/module.md +4 -2
- package/extensions/services/dataclaw/api/__init__.py +0 -0
- package/extensions/services/dataclaw/api/admin.py +367 -0
- package/extensions/services/dataclaw/api/copyright.py +175 -0
- package/extensions/services/dataclaw/api/credits.py +177 -0
- package/extensions/services/dataclaw/api/data.py +179 -0
- package/extensions/services/dataclaw/api/demands.py +269 -0
- package/extensions/services/dataclaw/api/feeds.py +262 -0
- package/extensions/services/dataclaw/api/identity.py +505 -0
- package/extensions/services/dataclaw/api/notifications.py +104 -0
- package/extensions/services/dataclaw/api/reviews.py +138 -0
- package/extensions/services/dataclaw/api/search.py +153 -0
- package/extensions/services/dataclaw/api/subscriptions.py +157 -0
- package/extensions/services/dataclaw/config.json5 +96 -0
- package/extensions/services/dataclaw/core/__init__.py +0 -0
- package/extensions/services/dataclaw/core/auth.py +95 -0
- package/extensions/services/dataclaw/core/config.py +50 -0
- package/extensions/services/dataclaw/core/database.py +70 -0
- package/extensions/services/dataclaw/entry.py +416 -0
- package/extensions/services/dataclaw/gofeed/351/241/271/347/233/256/346/211/200/346/234/211/346/235/203/350/275/254/347/247/273/346/265/201/347/250/213/350/257/264/346/230/216.md +309 -0
- package/extensions/services/dataclaw/migrate.py +283 -0
- package/extensions/services/dataclaw/models/__init__.py +0 -0
- package/extensions/services/dataclaw/module.md +49 -0
- package/extensions/services/dataclaw/requirements.txt +18 -0
- package/extensions/services/dataclaw/server.py +759 -0
- package/extensions/services/dataclaw/services/__init__.py +0 -0
- package/extensions/services/dataclaw/services/agent_service.py +132 -0
- package/extensions/services/dataclaw/services/credit_service.py +235 -0
- package/extensions/services/dataclaw/services/email_service.py +140 -0
- package/extensions/services/dataclaw/services/feed_service.py +259 -0
- package/extensions/services/dataclaw/services/notification_service.py +209 -0
- package/extensions/services/dataclaw/services/oauth_service.py +275 -0
- package/extensions/services/dataclaw/services/pricing.py +102 -0
- package/extensions/services/dataclaw/services/quality.py +79 -0
- package/extensions/services/dataclaw/services/reputation.py +142 -0
- package/extensions/services/dataclaw/services/sms_service.py +174 -0
- package/extensions/services/dataclaw/static/css/common.css +853 -0
- package/extensions/services/dataclaw/static/css/themes/blue.css +42 -0
- package/extensions/services/dataclaw/static/css/themes/dark.css +42 -0
- package/extensions/services/dataclaw/static/css/themes/light.css +35 -0
- package/extensions/services/dataclaw/static/js/api.js +103 -0
- package/extensions/services/dataclaw/static/js/common.js +321 -0
- package/extensions/services/dataclaw/static/js/i18n.js +95 -0
- package/extensions/services/dataclaw/static/js/pages/admin.js +152 -0
- package/extensions/services/dataclaw/static/js/pages/dashboard.js +82 -0
- package/extensions/services/dataclaw/static/js/pages/feed-detail.js +180 -0
- package/extensions/services/dataclaw/static/js/pages/feed-manage.js +158 -0
- package/extensions/services/dataclaw/static/js/theme.js +46 -0
- package/extensions/services/dataclaw/static/locales/en-US.json +464 -0
- package/extensions/services/dataclaw/static/locales/ja-JP.json +464 -0
- package/extensions/services/dataclaw/static/locales/zh-CN.json +464 -0
- package/extensions/services/dataclaw/templates/admin/index.html +90 -0
- package/extensions/services/dataclaw/templates/base.html +136 -0
- package/extensions/services/dataclaw/templates/credits/balance.html +106 -0
- package/extensions/services/dataclaw/templates/credits/deposit.html +164 -0
- package/extensions/services/dataclaw/templates/credits/history.html +90 -0
- package/extensions/services/dataclaw/templates/dashboard.html +52 -0
- package/extensions/services/dataclaw/templates/demands/create.html +78 -0
- package/extensions/services/dataclaw/templates/demands/detail.html +136 -0
- package/extensions/services/dataclaw/templates/demands/list.html +94 -0
- package/extensions/services/dataclaw/templates/feeds/create.html +95 -0
- package/extensions/services/dataclaw/templates/feeds/detail.html +110 -0
- package/extensions/services/dataclaw/templates/feeds/list.html +110 -0
- package/extensions/services/dataclaw/templates/feeds/manage.html +88 -0
- package/extensions/services/dataclaw/templates/index.html +185 -0
- package/extensions/services/dataclaw/templates/login.html +246 -0
- package/extensions/services/dataclaw/templates/register.html +164 -0
- package/extensions/services/dataclaw/templates/settings/notifications.html +96 -0
- package/extensions/services/dataclaw/templates/settings/profile.html +167 -0
- package/extensions/services/dataclaw/templates/subscriptions/list.html +64 -0
- package/extensions/services/dataclaw/tests/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/conftest.py +68 -0
- package/extensions/services/dataclaw/tests/integration/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/integration/test_workflows.py +239 -0
- package/extensions/services/dataclaw/tests/unit/__init__.py +0 -0
- package/extensions/services/dataclaw/tests/unit/test_admin.py +70 -0
- package/extensions/services/dataclaw/tests/unit/test_copyright.py +63 -0
- package/extensions/services/dataclaw/tests/unit/test_credits.py +80 -0
- package/extensions/services/dataclaw/tests/unit/test_data.py +98 -0
- package/extensions/services/dataclaw/tests/unit/test_demands.py +106 -0
- package/extensions/services/dataclaw/tests/unit/test_feeds.py +98 -0
- package/extensions/services/dataclaw/tests/unit/test_identity.py +88 -0
- package/extensions/services/dataclaw/tests/unit/test_notifications.py +36 -0
- package/extensions/services/dataclaw/tests/unit/test_reviews.py +68 -0
- package/extensions/services/dataclaw/tests/unit/test_search.py +64 -0
- package/extensions/services/dataclaw/tests/unit/test_subscriptions.py +65 -0
- package/extensions/services/dataclaw/tests/unit/test_system.py +106 -0
- package/extensions/services/dataclaw/utils/__init__.py +0 -0
- package/extensions/services/dataclaw/utils/crypto.py +38 -0
- package/extensions/services/dataclaw/utils/id_generator.py +52 -0
- package/extensions/services/dataclaw/ws/__init__.py +0 -0
- package/extensions/services/dataclaw/ws/handler.py +163 -0
- package/extensions/services/dataclaw//345/215/217/350/256/2561-/351/241/271/347/233/256/346/235/241/344/273/266/346/216/210/346/235/203/344/270/216/350/202/241/346/235/203/345/257/271/344/273/267/345/215/217/350/256/256.md +243 -0
- package/extensions/services/dataclaw//345/215/217/350/256/2562-/351/241/271/347/233/256/350/264/255/344/271/260/346/235/203/344/270/216/345/244/226/345/214/205/345/247/224/346/211/230/345/274/200/345/217/221/345/215/217/350/256/256.md +434 -0
- package/extensions/services/evol/__init__.py +1 -0
- package/extensions/services/evol/async_http.py +551 -0
- package/extensions/services/evol/auth_manager.py +602 -443
- package/extensions/services/evol/config.json5 +16 -0
- package/extensions/services/evol/entry.py +568 -406
- package/extensions/services/evol/evol_api.py +969 -173
- package/extensions/services/evol/mfa_totp.py +77 -0
- package/extensions/services/evol/module.md +150 -32
- package/extensions/services/evol/nonce_pool.py +113 -0
- package/extensions/services/evol/oauth_manager.py +223 -0
- package/extensions/services/evol/pairing.py +3 -2
- package/extensions/services/evol/pairing_codes.jsonl +1 -0
- package/extensions/services/evol/relay.py +1031 -682
- package/extensions/services/evol/relay_config.json5 +85 -67
- package/extensions/services/evol/routes/routes_llm.py +231 -0
- package/extensions/services/evol/routes/routes_rpc.py +90 -89
- package/extensions/services/evol/routes/routes_test.py +11 -4
- package/extensions/services/evol/server.py +2426 -875
- package/extensions/services/evol/static/assets/CommissionView-Cs_ys6Gm.js +1 -0
- package/extensions/services/evol/static/assets/CommissionView-DACet_Oo.css +1 -0
- package/extensions/services/evol/static/assets/IframePage-DbO11U9G.js +1 -0
- package/extensions/services/evol/static/assets/IframePage-c572lT8i.css +1 -0
- package/extensions/services/evol/static/assets/TeamDetailView-DULrGD7k.css +1 -0
- package/extensions/services/evol/static/assets/TeamDetailView-gy_MBEqG.js +139 -0
- package/extensions/services/evol/static/assets/element-plus-Bd7pZkkM.js +63 -0
- package/extensions/services/evol/static/assets/index-CmMONKzG.css +1 -0
- package/extensions/services/evol/static/assets/index-D44bBe__.js +2 -0
- package/extensions/services/evol/static/assets/vue-vendor-DtF-__I4.js +29 -0
- package/extensions/services/evol/static/index.html +16 -781
- package/extensions/services/evol/static/logo.png +0 -0
- package/extensions/services/evol/stats_manager.py +243 -240
- package/extensions/services/evol/web/README.md +89 -0
- package/extensions/services/evol/web/build.bat +44 -0
- package/extensions/services/evol/web/index.html +13 -0
- package/extensions/services/evol/web/package-lock.json +1718 -0
- package/extensions/services/evol/web/package.json +26 -0
- package/extensions/services/evol/web/public/logo.png +0 -0
- package/extensions/services/evol/web/src/App.vue +7 -0
- package/extensions/services/evol/web/src/components/layout/AppHeader.vue +202 -0
- package/extensions/services/evol/web/src/components/layout/AppLayout.vue +61 -0
- package/extensions/services/evol/web/src/components/layout/AppSidebar.vue +115 -0
- package/extensions/services/evol/web/src/components/login/LoginPage.vue +271 -0
- package/extensions/services/evol/web/src/components/team/AddMemberModal.vue +181 -0
- package/extensions/services/evol/web/src/components/team/GroupTreeNode.vue +156 -0
- package/extensions/services/evol/web/src/components/team/TeamAlertConfig.vue +221 -0
- package/extensions/services/evol/web/src/components/team/TeamBillModal.vue +165 -0
- package/extensions/services/evol/web/src/components/team/TeamMembersAndGroups.vue +499 -0
- package/extensions/services/evol/web/src/components/team/TeamStatsPanel.vue +907 -0
- package/extensions/services/evol/web/src/components/team/TreeNode.vue +331 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsExportProgress.vue +44 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsHeader.vue +89 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsMemberDetail.vue +415 -0
- package/extensions/services/evol/web/src/components/team/stats/StatsSummary.vue +42 -0
- package/extensions/services/evol/web/src/components/team/stats/helpers.ts +195 -0
- package/extensions/services/evol/web/src/components/team/stats/stats.css +741 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsApi.ts +114 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsCharts.ts +242 -0
- package/extensions/services/evol/web/src/components/team/stats/useStatsExport.ts +232 -0
- package/extensions/services/evol/web/src/composables/useFormatters.ts +42 -0
- package/extensions/services/evol/web/src/composables/useTheme.ts +52 -0
- package/extensions/services/evol/web/src/env.d.ts +7 -0
- package/extensions/services/evol/web/src/i18n/en.ts +361 -0
- package/extensions/services/evol/web/src/i18n/index.ts +36 -0
- package/extensions/services/evol/web/src/i18n/zh.ts +379 -0
- package/extensions/services/evol/web/src/main.ts +21 -0
- package/extensions/services/evol/web/src/router/index.ts +81 -0
- package/extensions/services/evol/web/src/services/kernel-client.ts +406 -0
- package/extensions/services/evol/web/src/stores/auth.ts +189 -0
- package/extensions/services/evol/web/src/stores/connection.ts +134 -0
- package/extensions/services/evol/web/src/stores/pages.ts +79 -0
- package/extensions/services/evol/web/src/styles/base.css +213 -0
- package/extensions/services/evol/web/src/styles/variables.css +138 -0
- package/extensions/services/evol/web/src/types/rpc.ts +35 -0
- package/extensions/services/evol/web/src/types/token.ts +87 -0
- package/extensions/services/evol/web/src/views/AccountView.vue +1532 -0
- package/extensions/services/evol/web/src/views/AiServiceView.vue +219 -0
- package/extensions/services/evol/web/src/views/CommissionView.vue +1220 -0
- package/extensions/services/evol/web/src/views/CreditsView.vue +131 -0
- package/extensions/services/evol/web/src/views/EndpointView.vue +163 -0
- package/extensions/services/evol/web/src/views/IframePage.vue +120 -0
- package/extensions/services/evol/web/src/views/TeamDetailView.vue +473 -0
- package/extensions/services/evol/web/src/views/TeamView.vue +332 -0
- package/extensions/services/evol/web/tsconfig.json +31 -0
- package/extensions/services/evol/web/tsconfig.node.json +10 -0
- package/extensions/services/evol/web/vite.config.ts +49 -0
- package/extensions/services/evolmem/__init__.py +0 -0
- package/extensions/services/evolmem/entry.py +387 -0
- package/extensions/services/evolmem/hooks/__init__.py +0 -0
- package/extensions/services/evolmem/hooks/assistant_stop.py +228 -0
- package/extensions/services/evolmem/hooks/common.py +76 -0
- package/extensions/services/evolmem/hooks/pre_tool_use.py +56 -0
- package/extensions/services/evolmem/hooks/session_end.py +133 -0
- package/extensions/services/evolmem/hooks/session_start.py +229 -0
- package/extensions/services/evolmem/hooks/user_prompt.py +122 -0
- package/extensions/services/evolmem/module.md +48 -0
- package/extensions/services/evolmem/prompts/00-server-info.md +28 -0
- package/extensions/services/evolmem/prompts/01-behavior.md +46 -0
- package/extensions/services/evolmem/prompts/02-summary-format.md +112 -0
- package/extensions/services/evolmem/prompts/03-file-query.md +92 -0
- package/extensions/services/evolmem/prompts/04-topic-stats.md +11 -0
- package/extensions/services/evolmem/prompts/05-recent-topics.md +84 -0
- package/extensions/services/evolmem/scripts/__init__.py +0 -0
- package/extensions/services/evolmem/scripts/extract_keywords.py +40 -0
- package/extensions/services/evolmem/scripts/search_topics.py +91 -0
- package/extensions/services/evolmem/server.py +641 -0
- package/extensions/services/gateway/entry.py +964 -0
- package/extensions/services/gateway/module.md +29 -0
- package/extensions/services/gateway/nonce_pool.py +65 -0
- package/extensions/services/gateway/relay.py +133 -0
- package/extensions/services/gateway/ws_server.py +285 -0
- package/extensions/services/kite_console/auth_manager.py +603 -0
- package/extensions/services/kite_console/config.json5 +19 -0
- package/extensions/services/kite_console/config_loader.py +117 -0
- package/extensions/services/kite_console/entry.py +528 -0
- package/extensions/services/kite_console/evol_api.py +179 -0
- package/extensions/services/kite_console/evol_config.json5 +29 -0
- package/extensions/services/kite_console/mfa_totp.py +77 -0
- package/extensions/services/kite_console/migrate_tokens.py +122 -0
- package/extensions/services/kite_console/module.md +37 -0
- package/extensions/services/kite_console/nonce_pool.py +113 -0
- package/extensions/services/kite_console/oauth_manager.py +223 -0
- package/extensions/services/kite_console/pairing.py +280 -0
- package/extensions/services/kite_console/pairing_codes.jsonl +2 -0
- package/extensions/services/kite_console/relay.py +1350 -0
- package/extensions/services/kite_console/relay_config.json5 +96 -0
- package/extensions/services/kite_console/routes/__init__.py +1 -0
- package/extensions/services/kite_console/routes/routes_llm.py +231 -0
- package/extensions/services/kite_console/routes/routes_proxy.py +115 -0
- package/extensions/services/kite_console/routes/routes_rpc.py +89 -0
- package/extensions/services/kite_console/routes/routes_test.py +68 -0
- package/extensions/services/kite_console/server.py +1742 -0
- package/extensions/services/{evol → kite_console}/static/css/style.css +656 -2
- package/extensions/services/kite_console/static/index.html +1524 -0
- package/extensions/services/{evol → kite_console}/static/js/dialog.js +11 -4
- package/extensions/services/kite_console/static/js/evol-app.js +7740 -0
- package/extensions/services/{evol/static/js/evol-app.js → kite_console/static/js/evol-app.js.backup} +2777 -1949
- package/extensions/services/kite_console/static/js/kernel-client.js +560 -0
- package/extensions/services/{evol/static/js/kernel-client.js → kite_console/static/js/kernel-client.js.backup} +41 -3
- package/extensions/services/{evol → kite_console}/static/js/registry-tests.js +7 -0
- package/extensions/services/kite_console/static/js/tests/ARCHITECTURE.md +67 -0
- package/extensions/services/kite_console/static/js/tests/README.md +140 -0
- package/extensions/services/kite_console/static/js/tests/index.js +161 -0
- package/extensions/services/kite_console/static/js/tests/integration/auth.js +120 -0
- package/extensions/services/kite_console/static/js/tests/integration/channel-interaction.js +188 -0
- package/extensions/services/kite_console/static/js/tests/integration/elastic-connection.js +115 -0
- package/extensions/services/kite_console/static/js/tests/integration/full-workflow.js +43 -0
- package/extensions/services/kite_console/static/js/tests/integration/multi-instance.js +304 -0
- package/extensions/services/kite_console/static/js/tests/integration/nested-rpc.js +266 -0
- package/extensions/services/kite_console/static/js/tests/integration/pingpong.js +25 -0
- package/extensions/services/kite_console/static/js/tests/integration/redis.js +227 -0
- package/extensions/services/kite_console/static/js/tests/integration/registry-core.js +52 -0
- package/extensions/services/kite_console/static/js/tests/integration/remote-deploy.js +85 -0
- package/extensions/services/kite_console/static/js/tests/integration/require-init.js +96 -0
- package/extensions/services/kite_console/static/js/tests/integration/scaling-control.js +193 -0
- package/extensions/services/kite_console/static/js/tests/integration/trace.js +109 -0
- package/extensions/services/kite_console/static/js/tests/modules/acp_channel.js +339 -0
- package/extensions/services/kite_console/static/js/tests/modules/auth.js +96 -0
- package/extensions/services/kite_console/static/js/tests/modules/backup.js +49 -0
- package/extensions/services/kite_console/static/js/tests/modules/gateway.js +41 -0
- package/extensions/services/kite_console/static/js/tests/modules/kernel.js +90 -0
- package/extensions/services/kite_console/static/js/tests/modules/launcher.js +75 -0
- package/extensions/services/kite_console/static/js/tests/modules/multi_instance.js +129 -0
- package/extensions/services/kite_console/static/js/tests/modules/phone_channel.js +364 -0
- package/extensions/services/kite_console/static/js/tests/modules/redis.js +178 -0
- package/extensions/services/kite_console/static/js/tests/modules/watchdog.js +60 -0
- package/extensions/services/kite_console/static/js/tests/modules/web.js +70 -0
- package/extensions/services/kite_console/static/js/tests/test-runner.js +123 -0
- package/extensions/services/kite_console/static/js/virtual-list.js +200 -0
- package/extensions/services/kite_console/static/test_kernel_client_token.html +352 -0
- package/extensions/services/kite_console/stats_manager.py +247 -0
- package/extensions/services/logs/README.md +215 -0
- package/extensions/services/logs/api_logger.py +37 -0
- package/extensions/services/logs/baseline.py +121 -0
- package/extensions/services/logs/cleaner.py +76 -0
- package/extensions/services/logs/entry.py +449 -0
- package/extensions/services/logs/formatter.py +129 -0
- package/extensions/services/logs/module.md +38 -0
- package/extensions/services/logs/quick_diagnostic.py +128 -0
- package/extensions/services/logs/routes/__init__.py +1 -0
- package/extensions/services/logs/routes/routes_logs.py +218 -0
- package/extensions/services/logs/routes/routes_logs.py.backup +173 -0
- package/extensions/services/logs/scanner.py +100 -0
- package/extensions/services/logs/searcher.py +263 -0
- package/extensions/services/logs/server.py +553 -0
- package/extensions/services/logs.zip +0 -0
- package/extensions/services/model_service/config.json5 +30 -0
- package/extensions/services/model_service/entry.py +620 -171
- package/extensions/services/model_service/module.md +11 -2
- package/extensions/services/proxy/__init__.py +0 -0
- package/extensions/services/proxy/aid_manager.py +419 -0
- package/extensions/services/proxy/auth_bridge.py +182 -0
- package/extensions/services/proxy/config_store.py +79 -0
- package/extensions/services/proxy/entry.py +528 -0
- package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +2 -2
- package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +18 -28
- package/extensions/services/proxy/evol/presenter/configPresenter.py +80 -1127
- package/extensions/services/proxy/evol/presenter/userPresenter.py +71 -477
- package/extensions/services/proxy/evol/server/claude_proxy_async.py +11 -7
- package/extensions/services/proxy/module.md +151 -0
- package/extensions/services/proxy/server.py +952 -271
- package/extensions/services/redis/ALIGNMENT_CHECKLIST.md +121 -0
- package/extensions/services/redis/ALIGNMENT_STATUS.md +548 -0
- package/extensions/services/redis/config.json5 +8 -0
- package/extensions/services/redis/entry.py +1509 -0
- package/extensions/services/redis/entry.py.backup +405 -0
- package/extensions/services/redis/module.md +48 -0
- package/extensions/services/redis/redis_builtin.py +332 -0
- package/extensions/services/redis/redis_external.py +164 -0
- package/extensions/services/testUi/entry.py +446 -0
- package/extensions/services/testUi/module.md +18 -0
- package/extensions/services/testUi/ui/cards.html +131 -0
- package/extensions/services/testUi/ui/index.html +22 -0
- package/extensions/services/testUi/ui/particles.html +143 -0
- package/extensions/services/watchdog/entry.py +1258 -793
- package/extensions/services/watchdog/module.md +2 -0
- package/extensions/services/watchdog/monitor.py +465 -87
- package/extensions/services/web/auth_manager.py +602 -0
- package/extensions/services/web/config.json5 +11 -0
- package/extensions/services/web/entry.py +598 -478
- package/extensions/services/web/mfa_totp.py +77 -0
- package/extensions/services/web/module.md +16 -13
- package/extensions/services/web/nonce_pool.py +113 -0
- package/extensions/services/web/oauth_manager.py +223 -0
- package/extensions/services/web/pairing.py +3 -2
- package/extensions/services/web/pairing_codes.jsonl +1 -0
- package/extensions/services/web/relay.py +442 -63
- package/extensions/services/web/relay_config.json5 +1 -2
- package/extensions/services/web/routes/routes_rpc.py +6 -6
- package/extensions/services/web/server.py +360 -173
- package/extensions/services/web/static/index.html +1752 -1738
- package/extensions/services/web/static/js/app.js +32 -0
- package/extensions/services/web/static/js/kernel-client.js +48 -9
- package/extensions/services/web/vendor/bluetooth/audio.py +1 -1
- package/extensions/services/web/vendor/config.py +2 -2
- package/extensions/services/web/vendor/storage/identity.py +1 -1
- package/kernel/entry.py +77 -23
- package/kernel/event_hub.py +1122 -74
- package/kernel/module.md +2 -1
- package/kernel/registry_store.py +208 -11
- package/kernel/rpc_router.py +1400 -491
- package/kernel/server.py +1021 -134
- package/kite_cli/__init__.py +9 -1
- package/kite_cli/builders/__init__.py +4 -0
- package/kite_cli/builders/base.py +67 -0
- package/kite_cli/builders/custom.py +31 -0
- package/kite_cli/builders/detector.py +56 -0
- package/kite_cli/builders/go.py +34 -0
- package/kite_cli/builders/gradle.py +41 -0
- package/kite_cli/builders/maven.py +36 -0
- package/kite_cli/builders/npm.py +44 -0
- package/kite_cli/builders/python.py +37 -0
- package/kite_cli/commands/BUILD_GUIDE.md +109 -0
- package/kite_cli/commands/build.py +142 -0
- package/kite_cli/commands/check.py +60 -0
- package/kite_cli/commands/config.py +156 -0
- package/kite_cli/commands/deps.py +58 -0
- package/kite_cli/commands/deps_install.py +7 -7
- package/kite_cli/commands/disable.py +162 -0
- package/kite_cli/commands/enable.py +162 -0
- package/kite_cli/commands/export.py +96 -0
- package/kite_cli/commands/import_cmd.py +110 -0
- package/kite_cli/commands/install.py +50 -23
- package/kite_cli/commands/install_skill.py +107 -0
- package/kite_cli/commands/list.py +128 -31
- package/kite_cli/commands/outdated.py +202 -0
- package/kite_cli/commands/search.py +33 -17
- package/kite_cli/commands/update.py +115 -2
- package/kite_cli/commands/venv_setup.py +6 -6
- package/kite_cli/commands/why.py +48 -0
- package/kite_cli/core/config_manager.py +145 -0
- package/kite_cli/core/downloader.py +32 -2
- package/kite_cli/main.py +153 -7
- package/kite_cli/utils/colors.py +153 -0
- package/kite_cli/utils/dependency_graph.py +209 -0
- package/kite_cli/utils/process.py +55 -0
- package/kite_cli/utils/progress.py +207 -0
- package/kite_cli/utils/table.py +101 -0
- package/launcher/count_lines.py +192 -43
- package/launcher/entry.py +4543 -2802
- package/launcher/logging_setup.py +54 -1
- package/launcher/module.md +32 -6
- package/launcher/module_scanner.py +93 -20
- package/launcher/process_manager.py +355 -76
- package/main.py +6 -0
- package/package.json +4 -1
- package/requirements.txt +41 -38
- package/scripts/auto-fix-deps.py +128 -0
- package/scripts/env-manager.js +25 -2
- package/scripts/final-test.js +78 -0
- package/scripts/setup-python-env.js +700 -191
- package/scripts/test-alluser.js +48 -0
- package/scripts/test-different-version.js +86 -0
- package/scripts/test-direct.js +63 -0
- package/scripts/test-extract-installer.js +28 -0
- package/scripts/test-install-log.js +54 -0
- package/scripts/test-installer.js +39 -0
- package/scripts/test-integration.js +250 -0
- package/scripts/test-real-install.js +210 -0
- package/scripts/test-targetdir.js +49 -0
- package/scripts/test-venv-real.js +47 -0
- package/scripts/test-venv-simple.js +57 -0
- package/scripts/test-wait.js +49 -0
- package/scripts/test-with-log.js +63 -0
- package/extensions/services/evol/config.yaml +0 -149
- package/extensions/services/evol/routes/routes_management_ws.py +0 -127
- package/extensions/services/evol/static/index_evol.html +0 -14
- package/extensions/services/evol/static/js/app.js +0 -6304
- package/extensions/services/evol/static/js/auth.js +0 -326
- package/extensions/services/evol/static/js/evol-app-fixed.js +0 -50
- package/extensions/services/evol/static/js/evol-app.js.bak +0 -1800
- package/extensions/services/evol/static/js/kernel-client-example.js +0 -228
- package/extensions/services/evol/static/js/main.js +0 -141
- package/extensions/services/evol/static/js/stats.js +0 -217
- package/extensions/services/evol/static/js/token-manager.js +0 -175
- package/extensions/services/proxy/CHANGELOG_20260308.md +0 -258
- package/extensions/services/proxy/_fix_prints.py +0 -133
- package/extensions/services/proxy/_fix_prints2.py +0 -87
- package/extensions/services/proxy/console_auth.py +0 -109
- package/extensions/services/proxy/logs/websocket.log +0 -260
- package/extensions/services/proxy/main.py +0 -240
- package/extensions/services/proxy/requirements.txt +0 -13
- package/extensions/services/web/config.yaml +0 -149
- /package/extensions/services/{evol → kite_console}/static/pairing.html +0 -0
- /package/extensions/services/{evol → kite_console}/static/test_registry.html +0 -0
- /package/extensions/services/{evol → kite_console}/static/test_relay.html +0 -0
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kernel WebSocket 中转服务
|
|
3
|
+
|
|
4
|
+
功能:
|
|
5
|
+
- 接受前端 WebSocket 连接
|
|
6
|
+
- 处理配对流程(通过 WebSocket)
|
|
7
|
+
- 为每个前端创建到 Kernel 的连接
|
|
8
|
+
- 双向转发 JSON-RPC 消息
|
|
9
|
+
- 管理前端模块的生命周期(注册、重连、优雅退出)
|
|
10
|
+
- RPC 权限控制(白名单)
|
|
11
|
+
|
|
12
|
+
零共享代码依赖 - 此文件可以独立拷贝到其他模块使用。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import secrets
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from collections import deque
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
import websockets
|
|
26
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
27
|
+
|
|
28
|
+
from extensions.services.kite_console.nonce_pool import NoncePool
|
|
29
|
+
from extensions.services.kite_console.mfa_totp import TOTPVerifier
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SessionInfo:
|
|
33
|
+
"""前端会话信息"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
session_token: str,
|
|
38
|
+
module_id: str,
|
|
39
|
+
kernel_ws,
|
|
40
|
+
client_ws: WebSocket,
|
|
41
|
+
frontend_token: str,
|
|
42
|
+
role: str,
|
|
43
|
+
kernel_token: str,
|
|
44
|
+
):
|
|
45
|
+
self.session_token = session_token
|
|
46
|
+
self.module_id = module_id
|
|
47
|
+
self.kernel_ws = kernel_ws
|
|
48
|
+
self.client_ws: Optional[WebSocket] = client_ws
|
|
49
|
+
self.frontend_token = frontend_token
|
|
50
|
+
self.role = role
|
|
51
|
+
self.kernel_token = kernel_token
|
|
52
|
+
self.created_at = time.time()
|
|
53
|
+
self.last_active = time.time()
|
|
54
|
+
self.status = "active" # active | waiting_reconnect
|
|
55
|
+
self.relay_task: Optional[asyncio.Task] = None # 当前 relay 任务,用于重连时取消旧 relay
|
|
56
|
+
self._msg_timestamps: deque[float] = deque() # Layer 3: 消息速率跟踪
|
|
57
|
+
self._reconnecting = False # 重连期间标志,防止 cancel 触发 offline
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class KernelRelay:
|
|
61
|
+
"""Kernel WebSocket 中转服务"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
kernel_host: str,
|
|
66
|
+
kernel_port: int,
|
|
67
|
+
kernel_token: str,
|
|
68
|
+
base_module_id: str,
|
|
69
|
+
reconnect_timeout: int,
|
|
70
|
+
permissions: dict,
|
|
71
|
+
pairing_manager,
|
|
72
|
+
evol_server, # 新增:web server 实例,用于发送事件
|
|
73
|
+
auth_manager=None, # 新增:AuthManager 实例,用于 Kite Token 认证
|
|
74
|
+
oauth_manager=None, # 新增:OAuthManager 实例,用于 OAuth 认证
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
初始化中转服务。
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
kernel_host: Kernel 主机地址
|
|
81
|
+
kernel_port: Kernel 端口
|
|
82
|
+
kernel_token: Kernel 认证 token
|
|
83
|
+
base_module_id: 基础模块 ID(实际 module_id 会加上 session 后缀)
|
|
84
|
+
reconnect_timeout: 重连超时时间(秒)
|
|
85
|
+
permissions: 权限配置(角色 → RPC 白名单)
|
|
86
|
+
pairing_manager: 配对管理器实例
|
|
87
|
+
evol_server: web server 实例
|
|
88
|
+
auth_manager: AuthManager 实例(可选,用于 Kite Token 认证)
|
|
89
|
+
oauth_manager: OAuthManager 实例(可选,用于 OAuth auth_ticket 认证)
|
|
90
|
+
"""
|
|
91
|
+
self.kernel_host = kernel_host
|
|
92
|
+
self.kernel_port = kernel_port
|
|
93
|
+
self.kernel_token = kernel_token
|
|
94
|
+
self.base_module_id = base_module_id
|
|
95
|
+
self.reconnect_timeout = reconnect_timeout
|
|
96
|
+
self.permissions = permissions
|
|
97
|
+
self.pairing_manager = pairing_manager
|
|
98
|
+
self.evol_server = evol_server
|
|
99
|
+
self.auth_manager = auth_manager
|
|
100
|
+
self.oauth_manager = oauth_manager
|
|
101
|
+
|
|
102
|
+
# Nonce 池(challenge-response 防重放)
|
|
103
|
+
self.nonce_pool = NoncePool(max_size=10000, ttl=600)
|
|
104
|
+
|
|
105
|
+
# TLS configuration
|
|
106
|
+
# 默认值根据环境变量:开发环境允许 WS,生产环境强制 WSS
|
|
107
|
+
env = os.environ.get("KITE_ENV", "development").lower()
|
|
108
|
+
self.require_tls = os.environ.get("KITE_REQUIRE_TLS", "true" if env == "production" else "false").lower() == "true"
|
|
109
|
+
|
|
110
|
+
# session_token → SessionInfo
|
|
111
|
+
self.sessions: dict[str, SessionInfo] = {}
|
|
112
|
+
|
|
113
|
+
# 待重连的 session(超时清理)
|
|
114
|
+
self.reconnect_timers: dict[str, asyncio.Task] = {}
|
|
115
|
+
|
|
116
|
+
# 速率限制:(ip, device_id) → [timestamp, timestamp, ...]
|
|
117
|
+
self._rate_limits: dict[tuple[str, str], list[float]] = {}
|
|
118
|
+
self._rate_limit_max = 20 # 10 秒内最多 20 次
|
|
119
|
+
self._rate_limit_window = 10.0 # 秒
|
|
120
|
+
|
|
121
|
+
# Layer 1: Session 级重连节流
|
|
122
|
+
self._reconnect_timestamps: dict[str, list[float]] = {}
|
|
123
|
+
self._reconnect_limit_max = 5 # 最多 N 次
|
|
124
|
+
self._reconnect_limit_window = 30.0 # 每 M 秒
|
|
125
|
+
|
|
126
|
+
# Layer 2: 全局并发 Session 上限
|
|
127
|
+
self._max_sessions = 50
|
|
128
|
+
|
|
129
|
+
# Layer 3: 单连接消息速率限制
|
|
130
|
+
self._msg_rate_limit_max = 200 # 最多 N 条
|
|
131
|
+
self._msg_rate_limit_window = 10.0 # 每 M 秒
|
|
132
|
+
|
|
133
|
+
# Layer 4: 消息大小限制
|
|
134
|
+
self._max_message_size = 1 * 1024 * 1024 # 1MB
|
|
135
|
+
|
|
136
|
+
# MFA/TOTP 验证器
|
|
137
|
+
self._totp = TOTPVerifier()
|
|
138
|
+
# MFA 密钥存储:token → totp_secret(由外部配置注入)
|
|
139
|
+
self._mfa_secrets: dict[str, str] = {}
|
|
140
|
+
|
|
141
|
+
# Kernel 离线状态(全局,影响所有客户端)
|
|
142
|
+
self._kernel_offline = False
|
|
143
|
+
self._kernel_offline_lock = asyncio.Lock()
|
|
144
|
+
self.kernel_reconnect_info = {"state": "connected", "attempt": 0, "max_attempts": 10, "next_retry_ms": 0}
|
|
145
|
+
|
|
146
|
+
async def handle_client(self, client_ws: WebSocket):
|
|
147
|
+
"""
|
|
148
|
+
处理客户端连接。
|
|
149
|
+
|
|
150
|
+
流程:
|
|
151
|
+
1. 接受 WebSocket 连接
|
|
152
|
+
2. 接收认证/配对/请求配对码消息
|
|
153
|
+
3. 验证身份
|
|
154
|
+
4. 创建或恢复 Kernel 连接
|
|
155
|
+
5. 双向转发消息
|
|
156
|
+
"""
|
|
157
|
+
# Check TLS requirement before accepting
|
|
158
|
+
if self.require_tls:
|
|
159
|
+
# Check if connection is secure (WSS)
|
|
160
|
+
if client_ws.url.scheme != "wss":
|
|
161
|
+
await client_ws.close(code=1008, reason="TLS required")
|
|
162
|
+
print(f"[relay] Rejected non-TLS connection from {client_ws.client.host if client_ws.client else 'unknown'}")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
await client_ws.accept()
|
|
166
|
+
|
|
167
|
+
client_ip = client_ws.client.host if client_ws.client else "unknown"
|
|
168
|
+
|
|
169
|
+
# Kernel 离线检查:发送状态信息后关闭
|
|
170
|
+
if self._kernel_offline:
|
|
171
|
+
try:
|
|
172
|
+
info = self.kernel_reconnect_info
|
|
173
|
+
# 根据服务端重连状态计算建议客户端延迟
|
|
174
|
+
if info["state"] == "fatal" or info["state"] == "exiting":
|
|
175
|
+
suggest_ms = 0 # 服务端即将退出,不建议重连
|
|
176
|
+
elif info["next_retry_ms"] > 0:
|
|
177
|
+
suggest_ms = info["next_retry_ms"] + 1000 # 服务端重连后再等 1s
|
|
178
|
+
else:
|
|
179
|
+
suggest_ms = 2000
|
|
180
|
+
await client_ws.send_json({
|
|
181
|
+
"type": "error",
|
|
182
|
+
"message": "Kernel is currently unavailable",
|
|
183
|
+
"code": 4050,
|
|
184
|
+
"retry_after_ms": suggest_ms,
|
|
185
|
+
"kernel_status": info,
|
|
186
|
+
})
|
|
187
|
+
await client_ws.close(code=4050, reason="Kernel unavailable")
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# 速率限制检查
|
|
194
|
+
client_ip = client_ws.client.host if client_ws.client else "unknown"
|
|
195
|
+
if not self._check_rate_limit(client_ip, "unknown"):
|
|
196
|
+
await client_ws.send_json({
|
|
197
|
+
"type": "error",
|
|
198
|
+
"message": "Rate limit exceeded"
|
|
199
|
+
})
|
|
200
|
+
await client_ws.close(code=4020, reason="Rate limit exceeded")
|
|
201
|
+
await self._audit_log("auth.rate_limited", {"ip": client_ip})
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# 接收认证/配对/请求配对码消息
|
|
205
|
+
raw = await client_ws.receive_text()
|
|
206
|
+
msg = json.loads(raw)
|
|
207
|
+
msg_type = msg.get("type")
|
|
208
|
+
|
|
209
|
+
if msg_type == "request_code":
|
|
210
|
+
# 请求配对码
|
|
211
|
+
await self._handle_request_code(client_ws, msg)
|
|
212
|
+
elif msg_type == "challenge":
|
|
213
|
+
# Challenge-response 握手(第一步:客户端请求 challenge)
|
|
214
|
+
await self._handle_challenge(client_ws, msg)
|
|
215
|
+
elif msg_type == "pair":
|
|
216
|
+
# 配对流程
|
|
217
|
+
await self._handle_pair(client_ws, msg)
|
|
218
|
+
elif msg_type == "auth":
|
|
219
|
+
# 认证流程(已有 token)
|
|
220
|
+
await self._handle_auth(client_ws, msg)
|
|
221
|
+
else:
|
|
222
|
+
await client_ws.send_json({
|
|
223
|
+
"type": "error",
|
|
224
|
+
"message": "Invalid message type, expected 'challenge', 'request_code', 'pair' or 'auth'"
|
|
225
|
+
})
|
|
226
|
+
await client_ws.close(code=4000, reason="Invalid message type")
|
|
227
|
+
|
|
228
|
+
except WebSocketDisconnect:
|
|
229
|
+
pass
|
|
230
|
+
except asyncio.CancelledError:
|
|
231
|
+
pass # relay 被 cancel(重连或关闭),正常退出
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"[relay] Client connection error: {e}")
|
|
234
|
+
try:
|
|
235
|
+
await client_ws.close(code=1011, reason="Internal error")
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
async def _handle_challenge(self, client_ws: WebSocket, msg: dict):
|
|
240
|
+
"""处理 challenge 请求(握手第一步)"""
|
|
241
|
+
client_ip = client_ws.client.host if client_ws.client else "unknown"
|
|
242
|
+
nonce = self.nonce_pool.generate(metadata={"client_ip": client_ip})
|
|
243
|
+
|
|
244
|
+
await client_ws.send_json({
|
|
245
|
+
"type": "challenge",
|
|
246
|
+
"nonce": nonce,
|
|
247
|
+
"protocol_version": "1.0",
|
|
248
|
+
"auth_methods": ["pairing_code", "kite_token", "oauth"],
|
|
249
|
+
"expires_in": self.nonce_pool.ttl,
|
|
250
|
+
"server_time": time.time(), # 客户端可据此校准时钟偏移
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
# 等待客户端的 connect 消息(带 nonce)
|
|
254
|
+
try:
|
|
255
|
+
raw = await asyncio.wait_for(client_ws.receive_text(), timeout=30)
|
|
256
|
+
connect_msg = json.loads(raw)
|
|
257
|
+
|
|
258
|
+
if connect_msg.get("type") != "connect":
|
|
259
|
+
await client_ws.send_json({
|
|
260
|
+
"type": "error",
|
|
261
|
+
"message": "Expected 'connect' message after challenge"
|
|
262
|
+
})
|
|
263
|
+
await client_ws.close(code=4000, reason="Protocol error")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# 验证 nonce
|
|
267
|
+
returned_nonce = connect_msg.get("nonce")
|
|
268
|
+
if not returned_nonce:
|
|
269
|
+
await client_ws.send_json({
|
|
270
|
+
"type": "error",
|
|
271
|
+
"message": "Missing nonce in connect message"
|
|
272
|
+
})
|
|
273
|
+
await client_ws.close(code=4000, reason="Missing nonce")
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
nonce_meta = self.nonce_pool.verify(returned_nonce)
|
|
277
|
+
if nonce_meta is None:
|
|
278
|
+
await client_ws.send_json({
|
|
279
|
+
"type": "error",
|
|
280
|
+
"message": "Invalid or expired nonce"
|
|
281
|
+
})
|
|
282
|
+
await client_ws.close(code=4001, reason="Invalid nonce")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Nonce 验证通过,根据 auth_method 分发
|
|
286
|
+
auth_method = connect_msg.get("auth_method", "")
|
|
287
|
+
|
|
288
|
+
if auth_method == "pairing_code":
|
|
289
|
+
code = connect_msg.get("code")
|
|
290
|
+
if not code:
|
|
291
|
+
await client_ws.send_json({"type": "error", "message": "Missing pairing code"})
|
|
292
|
+
await client_ws.close(code=4000, reason="Missing code")
|
|
293
|
+
return
|
|
294
|
+
await self._handle_pair(client_ws, {"code": code})
|
|
295
|
+
|
|
296
|
+
elif auth_method == "kite_token":
|
|
297
|
+
token = connect_msg.get("token")
|
|
298
|
+
session_token = connect_msg.get("session_token")
|
|
299
|
+
if not token or not session_token:
|
|
300
|
+
await client_ws.send_json({"type": "error", "message": "Missing token or session_token"})
|
|
301
|
+
await client_ws.close(code=4000, reason="Missing credentials")
|
|
302
|
+
return
|
|
303
|
+
await self._handle_auth(client_ws, {"token": token, "session_token": session_token})
|
|
304
|
+
|
|
305
|
+
elif auth_method == "oauth":
|
|
306
|
+
auth_ticket = connect_msg.get("auth_ticket")
|
|
307
|
+
if not auth_ticket:
|
|
308
|
+
await client_ws.send_json({"type": "error", "message": "Missing auth_ticket"})
|
|
309
|
+
await client_ws.close(code=4000, reason="Missing auth_ticket")
|
|
310
|
+
return
|
|
311
|
+
await self._handle_auth(client_ws, {
|
|
312
|
+
"method": "oauth",
|
|
313
|
+
"auth_ticket": auth_ticket,
|
|
314
|
+
"session_token": connect_msg.get("session_token", "")
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
else:
|
|
318
|
+
await client_ws.send_json({
|
|
319
|
+
"type": "error",
|
|
320
|
+
"message": f"Unsupported auth_method: {auth_method}"
|
|
321
|
+
})
|
|
322
|
+
await client_ws.close(code=4000, reason="Unsupported auth method")
|
|
323
|
+
|
|
324
|
+
except asyncio.TimeoutError:
|
|
325
|
+
await client_ws.send_json({"type": "error", "message": "Connect timeout"})
|
|
326
|
+
await client_ws.close(code=4000, reason="Timeout")
|
|
327
|
+
|
|
328
|
+
async def _handle_request_code(self, client_ws: WebSocket, msg: dict):
|
|
329
|
+
"""处理请求配对码"""
|
|
330
|
+
# 生成配对码
|
|
331
|
+
code = self.pairing_manager.generate_pairing_code(role="admin")
|
|
332
|
+
|
|
333
|
+
# 发送事件给 Launcher(通过 Kernel)
|
|
334
|
+
if self.evol_server and self.evol_server._ws:
|
|
335
|
+
try:
|
|
336
|
+
await self.evol_server._publish_event(
|
|
337
|
+
self.evol_server._ws,
|
|
338
|
+
"pairing.status",
|
|
339
|
+
{
|
|
340
|
+
"step": "code_generated",
|
|
341
|
+
"success": True,
|
|
342
|
+
"code": code,
|
|
343
|
+
"expires_in": 300,
|
|
344
|
+
"module_id": "evol"
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"[relay] Failed to publish pairing event: {e}")
|
|
349
|
+
|
|
350
|
+
# 返回配对码给前端
|
|
351
|
+
await client_ws.send_json({
|
|
352
|
+
"type": "code_generated",
|
|
353
|
+
"code": code
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
# 关闭连接(前端会重新连接进行配对)
|
|
357
|
+
await client_ws.close()
|
|
358
|
+
|
|
359
|
+
async def _handle_pair(self, client_ws: WebSocket, msg: dict):
|
|
360
|
+
"""处理配对请求"""
|
|
361
|
+
code = msg.get("code")
|
|
362
|
+
if not code:
|
|
363
|
+
await client_ws.send_json({
|
|
364
|
+
"type": "error",
|
|
365
|
+
"message": "Missing pairing code"
|
|
366
|
+
})
|
|
367
|
+
await client_ws.close(code=4000, reason="Missing pairing code")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# 验证配对码
|
|
371
|
+
result = self.pairing_manager.pair(code)
|
|
372
|
+
if not result:
|
|
373
|
+
# 发送配对失败事件
|
|
374
|
+
if self.evol_server and self.evol_server._ws:
|
|
375
|
+
try:
|
|
376
|
+
await self.evol_server._publish_event(
|
|
377
|
+
self.evol_server._ws,
|
|
378
|
+
"pairing.status",
|
|
379
|
+
{
|
|
380
|
+
"step": "completed",
|
|
381
|
+
"success": False,
|
|
382
|
+
"reason": "Invalid pairing code"
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
print(f"[relay] Failed to publish pairing failed event: {e}")
|
|
387
|
+
|
|
388
|
+
await client_ws.send_json({
|
|
389
|
+
"type": "error",
|
|
390
|
+
"message": "Invalid pairing code"
|
|
391
|
+
})
|
|
392
|
+
await client_ws.close(code=4001, reason="Invalid pairing code")
|
|
393
|
+
await self._audit_log("auth.pair_failed", {"reason": "invalid_code"})
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# 生成 session_token
|
|
397
|
+
session_token = "sess_" + secrets.token_urlsafe(6)[:6]
|
|
398
|
+
|
|
399
|
+
# 创建新连接
|
|
400
|
+
await self._create_new_connection(
|
|
401
|
+
client_ws,
|
|
402
|
+
session_token,
|
|
403
|
+
result["token"],
|
|
404
|
+
result["role"],
|
|
405
|
+
is_pairing=True
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
async def _handle_auth(self, client_ws: WebSocket, msg: dict):
|
|
409
|
+
"""处理认证请求(已有 token)"""
|
|
410
|
+
frontend_token = msg.get("token")
|
|
411
|
+
session_token = msg.get("session_token")
|
|
412
|
+
auth_method = msg.get("method", "") # 可选:oauth
|
|
413
|
+
|
|
414
|
+
# OAuth auth_ticket 认证
|
|
415
|
+
if auth_method == "oauth":
|
|
416
|
+
auth_ticket = msg.get("auth_ticket")
|
|
417
|
+
if not auth_ticket:
|
|
418
|
+
await client_ws.send_json({
|
|
419
|
+
"type": "error",
|
|
420
|
+
"message": "Missing auth_ticket for OAuth auth"
|
|
421
|
+
})
|
|
422
|
+
await client_ws.close(code=4000, reason="Missing auth_ticket")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
if not self.oauth_manager:
|
|
426
|
+
await client_ws.send_json({
|
|
427
|
+
"type": "error",
|
|
428
|
+
"message": "OAuth not configured"
|
|
429
|
+
})
|
|
430
|
+
await client_ws.close(code=4000, reason="OAuth not configured")
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
ticket_info = self.oauth_manager.verify_auth_ticket(auth_ticket)
|
|
434
|
+
if not ticket_info:
|
|
435
|
+
await client_ws.send_json({
|
|
436
|
+
"type": "error",
|
|
437
|
+
"message": "Invalid or expired auth_ticket"
|
|
438
|
+
})
|
|
439
|
+
await client_ws.close(code=4001, reason="Invalid auth_ticket")
|
|
440
|
+
await self._audit_log("auth.failed", {"reason": "invalid_auth_ticket", "auth_type": "oauth"})
|
|
441
|
+
return
|
|
442
|
+
print(f"[Relay] Authenticated via OAuth ({ticket_info['provider']}), user={ticket_info['user_info'].get('name', 'unknown')}")
|
|
443
|
+
role = "admin" # OAuth 用户默认 admin
|
|
444
|
+
await self._audit_log("auth.login", {
|
|
445
|
+
"auth_type": "oauth",
|
|
446
|
+
"provider": ticket_info["provider"],
|
|
447
|
+
"user": ticket_info["user_info"].get("name", "unknown"),
|
|
448
|
+
"role": role,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
# 生成 session_token
|
|
452
|
+
if not session_token:
|
|
453
|
+
session_token = "sess_" + secrets.token_urlsafe(6)[:6]
|
|
454
|
+
|
|
455
|
+
await self._create_new_connection(
|
|
456
|
+
client_ws,
|
|
457
|
+
session_token,
|
|
458
|
+
f"oauth:{ticket_info['provider']}:{ticket_info['user_info'].get('id', '')}",
|
|
459
|
+
role
|
|
460
|
+
)
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if not frontend_token or not session_token:
|
|
464
|
+
await client_ws.send_json({
|
|
465
|
+
"type": "error",
|
|
466
|
+
"message": "Missing token or session_token"
|
|
467
|
+
})
|
|
468
|
+
await client_ws.close(code=4000, reason="Missing credentials")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# 1. 尝试配对码认证
|
|
472
|
+
token_info = self.pairing_manager.verify_token(frontend_token)
|
|
473
|
+
if token_info:
|
|
474
|
+
role = token_info["role"]
|
|
475
|
+
auth_type = "pairing"
|
|
476
|
+
print(f"[Relay] Authenticated via pairing code, role={role}")
|
|
477
|
+
|
|
478
|
+
# 2. 尝试 Kite Token 认证
|
|
479
|
+
elif self.auth_manager and self.auth_manager.verify_kite_token(frontend_token):
|
|
480
|
+
role = "admin" # Kite Token 用户默认为 admin
|
|
481
|
+
auth_type = "kite_token"
|
|
482
|
+
print(f"[Relay] Authenticated via Kite Token, role={role}")
|
|
483
|
+
# 构造 token_info 格式以保持兼容
|
|
484
|
+
token_info = {"role": role}
|
|
485
|
+
|
|
486
|
+
# 3. 认证失败
|
|
487
|
+
else:
|
|
488
|
+
await client_ws.send_json({
|
|
489
|
+
"type": "error",
|
|
490
|
+
"message": "Invalid or expired token"
|
|
491
|
+
})
|
|
492
|
+
await client_ws.close(code=4001, reason="Invalid token")
|
|
493
|
+
await self._audit_log("auth.failed", {"reason": "invalid_token", "session_token": session_token})
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# 审计:认证成功
|
|
497
|
+
await self._audit_log("auth.login", {"auth_type": auth_type, "role": role, "session_token": session_token})
|
|
498
|
+
|
|
499
|
+
# MFA 检查(可选:如果该 token 配置了 MFA 密钥)
|
|
500
|
+
mfa_secret = self._mfa_secrets.get(frontend_token)
|
|
501
|
+
if mfa_secret:
|
|
502
|
+
mfa_code = msg.get("mfa_code", "")
|
|
503
|
+
if not mfa_code or not self._totp.verify(mfa_secret, mfa_code):
|
|
504
|
+
await client_ws.send_json({
|
|
505
|
+
"type": "error",
|
|
506
|
+
"message": "MFA verification failed"
|
|
507
|
+
})
|
|
508
|
+
await client_ws.close(code=4004, reason="MFA failed")
|
|
509
|
+
await self._audit_log("auth.mfa_failed", {"session_token": session_token})
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# 检查是否是重连
|
|
513
|
+
if session_token in self.sessions:
|
|
514
|
+
await self._handle_reconnect(client_ws, session_token, token_info)
|
|
515
|
+
else:
|
|
516
|
+
await self._create_new_connection(
|
|
517
|
+
client_ws,
|
|
518
|
+
session_token,
|
|
519
|
+
frontend_token,
|
|
520
|
+
role
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
async def _create_new_connection(
|
|
524
|
+
self,
|
|
525
|
+
client_ws: WebSocket,
|
|
526
|
+
session_token: str,
|
|
527
|
+
frontend_token: str,
|
|
528
|
+
role: str,
|
|
529
|
+
is_pairing: bool = False
|
|
530
|
+
):
|
|
531
|
+
"""创建新的 Kernel 连接"""
|
|
532
|
+
# Layer 2: 全局并发 Session 上限
|
|
533
|
+
if len(self.sessions) >= self._max_sessions:
|
|
534
|
+
await client_ws.send_json({
|
|
535
|
+
"type": "error",
|
|
536
|
+
"message": "Too many sessions",
|
|
537
|
+
"max_sessions": self._max_sessions
|
|
538
|
+
})
|
|
539
|
+
await client_ws.close(code=4029, reason="Too many sessions")
|
|
540
|
+
await self._audit_log("auth.session_cap", {
|
|
541
|
+
"current": len(self.sessions),
|
|
542
|
+
"max": self._max_sessions
|
|
543
|
+
})
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
# 生成 module_id
|
|
547
|
+
suffix = session_token.replace("sess_", "")
|
|
548
|
+
module_id = f"{self.base_module_id}-{suffix}"
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
# 向 Launcher 申请 kernel_token
|
|
552
|
+
kernel_token = await self._request_kernel_token(module_id)
|
|
553
|
+
|
|
554
|
+
# 连接 Kernel
|
|
555
|
+
kernel_ws = await self._connect_kernel(module_id, kernel_token)
|
|
556
|
+
|
|
557
|
+
# 创建 session
|
|
558
|
+
session = SessionInfo(
|
|
559
|
+
session_token=session_token,
|
|
560
|
+
module_id=module_id,
|
|
561
|
+
kernel_ws=kernel_ws,
|
|
562
|
+
client_ws=client_ws,
|
|
563
|
+
frontend_token=frontend_token,
|
|
564
|
+
role=role,
|
|
565
|
+
kernel_token=kernel_token,
|
|
566
|
+
)
|
|
567
|
+
self.sessions[session_token] = session
|
|
568
|
+
|
|
569
|
+
# 返回认证成功
|
|
570
|
+
await client_ws.send_json({
|
|
571
|
+
"type": "paired" if is_pairing else "authenticated",
|
|
572
|
+
"token": frontend_token,
|
|
573
|
+
"session_token": session_token,
|
|
574
|
+
"module_id": module_id,
|
|
575
|
+
"role": role
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
print(f"\033[32m[relay] ← 远程模块连入: {module_id} (role: {role}, ip: {client_ws.client.host if client_ws.client else 'unknown'})\033[0m")
|
|
579
|
+
|
|
580
|
+
# 如果是配对,发送配对成功事件给 Launcher
|
|
581
|
+
if is_pairing and self.evol_server and self.evol_server._ws:
|
|
582
|
+
try:
|
|
583
|
+
await self.evol_server._publish_event(
|
|
584
|
+
self.evol_server._ws,
|
|
585
|
+
"pairing.status",
|
|
586
|
+
{
|
|
587
|
+
"step": "completed",
|
|
588
|
+
"success": True,
|
|
589
|
+
"module_id": module_id,
|
|
590
|
+
"role": role
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
except Exception as e:
|
|
594
|
+
print(f"[relay] Failed to publish pairing success event: {e}")
|
|
595
|
+
|
|
596
|
+
# 开始双向转发
|
|
597
|
+
session.relay_task = asyncio.current_task()
|
|
598
|
+
await self._relay_messages(session)
|
|
599
|
+
|
|
600
|
+
except asyncio.CancelledError:
|
|
601
|
+
# 被 _handle_reconnect cancel,正常退出(不逃逸到 ASGI 层)
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
except Exception as e:
|
|
605
|
+
error_msg = str(e)
|
|
606
|
+
is_fatal = self._is_fatal_error(error_msg)
|
|
607
|
+
error_code = 1011 # Internal Error (default)
|
|
608
|
+
|
|
609
|
+
if is_fatal:
|
|
610
|
+
# 永久性错误(权限、配置错误)
|
|
611
|
+
error_code = 1008 # Policy Violation
|
|
612
|
+
print(f"\033[31m[relay] 致命错误: {error_msg}\033[0m")
|
|
613
|
+
if "relay_modules whitelist" in error_msg:
|
|
614
|
+
print(f"\033[31m[relay] 请在 launcher/module.md 的 relay.modules 中添加本模块名\033[0m")
|
|
615
|
+
else:
|
|
616
|
+
print(f"[relay] Failed to create connection: {error_msg}")
|
|
617
|
+
|
|
618
|
+
await client_ws.send_json({
|
|
619
|
+
"type": "error",
|
|
620
|
+
"message": f"Failed to connect to Kernel: {error_msg}",
|
|
621
|
+
"fatal": is_fatal, # 告知前端是否应该重试
|
|
622
|
+
"code": error_code
|
|
623
|
+
})
|
|
624
|
+
await client_ws.close(code=error_code, reason="Kernel connection failed")
|
|
625
|
+
|
|
626
|
+
def _is_fatal_error(self, error_msg: str) -> bool:
|
|
627
|
+
"""判断是否为永久性错误(不应重试)"""
|
|
628
|
+
fatal_keywords = [
|
|
629
|
+
"Permission denied",
|
|
630
|
+
"not in relay_modules whitelist",
|
|
631
|
+
"module_id must start with",
|
|
632
|
+
"Invalid module_id",
|
|
633
|
+
"Token limit reached",
|
|
634
|
+
]
|
|
635
|
+
return any(keyword in error_msg for keyword in fatal_keywords)
|
|
636
|
+
|
|
637
|
+
async def _handle_reconnect(
|
|
638
|
+
self,
|
|
639
|
+
client_ws: WebSocket,
|
|
640
|
+
session_token: str,
|
|
641
|
+
token_info: dict
|
|
642
|
+
):
|
|
643
|
+
"""处理重连"""
|
|
644
|
+
session = self.sessions[session_token]
|
|
645
|
+
|
|
646
|
+
# Layer 1: Session 级重连节流
|
|
647
|
+
allowed, cooldown = self._check_reconnect_throttle(session_token)
|
|
648
|
+
if not allowed:
|
|
649
|
+
await client_ws.send_json({
|
|
650
|
+
"type": "error",
|
|
651
|
+
"message": "Reconnect rate limit exceeded",
|
|
652
|
+
"cooldown_seconds": cooldown
|
|
653
|
+
})
|
|
654
|
+
await client_ws.close(code=4020, reason="Reconnect rate limit exceeded")
|
|
655
|
+
await self._audit_log("auth.reconnect_throttled", {
|
|
656
|
+
"session_token": session_token,
|
|
657
|
+
"cooldown_seconds": cooldown
|
|
658
|
+
})
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
# 取消超时计时器
|
|
662
|
+
if session_token in self.reconnect_timers:
|
|
663
|
+
self.reconnect_timers[session_token].cancel()
|
|
664
|
+
del self.reconnect_timers[session_token]
|
|
665
|
+
|
|
666
|
+
# 标记 session 正在重连,防止旧 relay cancel 触发 offline
|
|
667
|
+
session._reconnecting = True
|
|
668
|
+
|
|
669
|
+
# 先更新 client_ws,使旧 relay finally 的 "session.client_ws is my_client_ws"
|
|
670
|
+
# 检查返回 False,从而跳过 _on_client_disconnect(避免竞态)
|
|
671
|
+
session.client_ws = client_ws
|
|
672
|
+
session.status = "active"
|
|
673
|
+
session.last_active = time.time()
|
|
674
|
+
|
|
675
|
+
# 取消旧的 relay 任务并等待它退出
|
|
676
|
+
if session.relay_task and not session.relay_task.done():
|
|
677
|
+
session.relay_task.cancel()
|
|
678
|
+
try:
|
|
679
|
+
await session.relay_task
|
|
680
|
+
except asyncio.CancelledError:
|
|
681
|
+
pass
|
|
682
|
+
|
|
683
|
+
# 复用现有 kernel_ws(前端重连不需要重建 kernel 连接)
|
|
684
|
+
# 仅当 kernel_ws 已断开时才重新创建
|
|
685
|
+
kernel_ws = session.kernel_ws
|
|
686
|
+
kernel_dead = not kernel_ws or kernel_ws.close_code is not None
|
|
687
|
+
if kernel_dead:
|
|
688
|
+
try:
|
|
689
|
+
if kernel_ws:
|
|
690
|
+
await kernel_ws.close()
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
693
|
+
kernel_token = await self._request_kernel_token(session.module_id)
|
|
694
|
+
kernel_ws = await self._connect_kernel(session.module_id, kernel_token)
|
|
695
|
+
session.kernel_ws = kernel_ws
|
|
696
|
+
print(f"[relay] Kernel connection rebuilt for {session.module_id}")
|
|
697
|
+
|
|
698
|
+
session._reconnecting = False
|
|
699
|
+
|
|
700
|
+
# 返回重连成功
|
|
701
|
+
await client_ws.send_json({
|
|
702
|
+
"type": "reconnected",
|
|
703
|
+
"module_id": session.module_id,
|
|
704
|
+
"role": session.role
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
print(f"\033[32m[relay] ← 远程模块重连: {session.module_id} (role: {session.role}, ip: {client_ws.client.host if client_ws.client else 'unknown'})\033[0m")
|
|
708
|
+
|
|
709
|
+
# 继续双向转发
|
|
710
|
+
session.relay_task = asyncio.current_task()
|
|
711
|
+
try:
|
|
712
|
+
await self._relay_messages(session)
|
|
713
|
+
except asyncio.CancelledError:
|
|
714
|
+
# 被下一次 _handle_reconnect cancel,正常退出
|
|
715
|
+
pass
|
|
716
|
+
|
|
717
|
+
async def _request_kernel_token(self, module_id: str) -> str:
|
|
718
|
+
"""向 Launcher 申请 kernel_token"""
|
|
719
|
+
if not self.evol_server or not self.evol_server._ws:
|
|
720
|
+
raise RuntimeError("evol_server not connected to Kernel")
|
|
721
|
+
|
|
722
|
+
print(f"[relay] Requesting kernel_token for {module_id} from Launcher")
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
resp = await self.evol_server._rpc_call(
|
|
726
|
+
self.evol_server._ws,
|
|
727
|
+
"launcher.request_client_token",
|
|
728
|
+
{"module_id": module_id}
|
|
729
|
+
)
|
|
730
|
+
# _rpc_call 返回完整 JSON-RPC 响应,需要解包 result 层
|
|
731
|
+
inner = resp.get("result", resp)
|
|
732
|
+
token = inner.get("token")
|
|
733
|
+
if not token:
|
|
734
|
+
raise RuntimeError(f"Launcher returned no token: {resp}")
|
|
735
|
+
|
|
736
|
+
print(f"[relay] Received kernel_token for {module_id}")
|
|
737
|
+
return token
|
|
738
|
+
|
|
739
|
+
except Exception as e:
|
|
740
|
+
print(f"[relay] Failed to request kernel_token: {e}")
|
|
741
|
+
raise
|
|
742
|
+
|
|
743
|
+
async def _connect_kernel(self, module_id: str, kernel_token: str):
|
|
744
|
+
"""连接到 Kernel"""
|
|
745
|
+
url = f"ws://{self.kernel_host}:{self.kernel_port}/ws?id={module_id}"
|
|
746
|
+
kernel_ws = await websockets.connect(
|
|
747
|
+
url,
|
|
748
|
+
open_timeout=5,
|
|
749
|
+
ping_interval=None,
|
|
750
|
+
close_timeout=10
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
# 1. 先发送认证请求(Kernel 要求第一条消息必须是 auth)
|
|
754
|
+
auth_id = str(uuid.uuid4())
|
|
755
|
+
await self._send_to_kernel(kernel_ws, {
|
|
756
|
+
"jsonrpc": "2.0",
|
|
757
|
+
"id": auth_id,
|
|
758
|
+
"method": "auth",
|
|
759
|
+
"params": {
|
|
760
|
+
"token": kernel_token
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
# 等待认证响应
|
|
765
|
+
auth_response = await asyncio.wait_for(kernel_ws.recv(), timeout=5.0)
|
|
766
|
+
auth_msg = json.loads(auth_response)
|
|
767
|
+
if "error" in auth_msg:
|
|
768
|
+
raise RuntimeError(f"Kernel auth failed: {auth_msg['error']}")
|
|
769
|
+
|
|
770
|
+
# 2. 注册模块(不预订阅事件,由前端自己决定)
|
|
771
|
+
await self._send_to_kernel(kernel_ws, {
|
|
772
|
+
"jsonrpc": "2.0",
|
|
773
|
+
"id": str(uuid.uuid4()),
|
|
774
|
+
"method": "registry.register",
|
|
775
|
+
"params": {
|
|
776
|
+
"module_id": module_id,
|
|
777
|
+
"module_type": "web_client",
|
|
778
|
+
"relay_source": self.evol_server.module_name,
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
# 发送 module.ready
|
|
783
|
+
await self._send_to_kernel(kernel_ws, {
|
|
784
|
+
"jsonrpc": "2.0",
|
|
785
|
+
"id": str(uuid.uuid4()),
|
|
786
|
+
"method": "event.publish",
|
|
787
|
+
"params": {
|
|
788
|
+
"event_id": str(uuid.uuid4()),
|
|
789
|
+
"event": "module.ready",
|
|
790
|
+
"data": {
|
|
791
|
+
"module_id": module_id,
|
|
792
|
+
"graceful_shutdown": True,
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
return kernel_ws
|
|
798
|
+
|
|
799
|
+
async def _relay_messages(self, session: SessionInfo):
|
|
800
|
+
"""双向转发消息"""
|
|
801
|
+
# 记住本次 relay 持有的 client_ws,用于 finally 中判断是否已被新连接替换
|
|
802
|
+
my_client_ws = session.client_ws
|
|
803
|
+
client_to_kernel = None
|
|
804
|
+
kernel_to_client = None
|
|
805
|
+
try:
|
|
806
|
+
# 创建两个任务:client → kernel, kernel → client
|
|
807
|
+
client_to_kernel = asyncio.create_task(
|
|
808
|
+
self._forward_client_to_kernel(session)
|
|
809
|
+
)
|
|
810
|
+
kernel_to_client = asyncio.create_task(
|
|
811
|
+
self._forward_kernel_to_client(session)
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# 等待任一任务完成(断开)
|
|
815
|
+
done, pending = await asyncio.wait(
|
|
816
|
+
[client_to_kernel, kernel_to_client],
|
|
817
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
# 先取消未完成的任务
|
|
821
|
+
for task in pending:
|
|
822
|
+
task.cancel()
|
|
823
|
+
try:
|
|
824
|
+
await task
|
|
825
|
+
except asyncio.CancelledError:
|
|
826
|
+
pass
|
|
827
|
+
|
|
828
|
+
# 用标志位判断是否真的是 Kernel 断开(而非 client send 失败)
|
|
829
|
+
kernel_disconnected = getattr(session, '_kernel_disconnected', False)
|
|
830
|
+
graceful_shutdown = getattr(session, '_graceful_shutdown', False)
|
|
831
|
+
|
|
832
|
+
# 仅当 Kernel 真正断开时才触发全局 offline(排除主动断开和重连期间)
|
|
833
|
+
if kernel_disconnected and not graceful_shutdown and not getattr(session, '_reconnecting', False):
|
|
834
|
+
print(f"[relay] Kernel connection lost for {session.module_id}")
|
|
835
|
+
asyncio.create_task(self.set_kernel_offline(True))
|
|
836
|
+
|
|
837
|
+
# 检查已完成任务的异常
|
|
838
|
+
for task in done:
|
|
839
|
+
try:
|
|
840
|
+
task.result()
|
|
841
|
+
except Exception:
|
|
842
|
+
pass # 忽略异常,因为断开连接是正常的
|
|
843
|
+
|
|
844
|
+
except asyncio.CancelledError:
|
|
845
|
+
# 被 _handle_reconnect cancel,必须清理子任务防止竞态
|
|
846
|
+
await self._cancel_relay_tasks([client_to_kernel, kernel_to_client])
|
|
847
|
+
|
|
848
|
+
except Exception as e:
|
|
849
|
+
print(f"[relay] Relay error: {e}")
|
|
850
|
+
|
|
851
|
+
finally:
|
|
852
|
+
# 兜底:确保子任务不会泄漏(正常路径已在 try/except 中清理,这里处理遗漏)
|
|
853
|
+
await self._cancel_relay_tasks([client_to_kernel, kernel_to_client])
|
|
854
|
+
|
|
855
|
+
# 只有当 session.client_ws 仍是本次 relay 持有的 ws 时才触发断开流程
|
|
856
|
+
# 如果已被 _handle_reconnect 替换为新连接,跳过(避免清掉新连接)
|
|
857
|
+
if session.client_ws is my_client_ws:
|
|
858
|
+
await self._on_client_disconnect(session)
|
|
859
|
+
|
|
860
|
+
async def _cancel_relay_tasks(self, tasks, timeout=3):
|
|
861
|
+
"""取消转发子任务,带超时保护防止卡死"""
|
|
862
|
+
pending = []
|
|
863
|
+
for task in tasks:
|
|
864
|
+
if task and not task.done():
|
|
865
|
+
task.cancel()
|
|
866
|
+
pending.append(task)
|
|
867
|
+
if not pending:
|
|
868
|
+
return
|
|
869
|
+
# 批量等待所有子任务退出,最多等 timeout 秒
|
|
870
|
+
done, stuck = await asyncio.wait(pending, timeout=timeout)
|
|
871
|
+
if stuck:
|
|
872
|
+
print(f"[relay] Warning: {len(stuck)} relay subtask(s) did not exit within {timeout}s, abandoning")
|
|
873
|
+
# 收集已完成任务的异常,防止 "Task exception was never retrieved"
|
|
874
|
+
# 注意:Python 3.9+ CancelledError 继承自 BaseException,不是 Exception
|
|
875
|
+
for task in done:
|
|
876
|
+
try:
|
|
877
|
+
task.result()
|
|
878
|
+
except BaseException:
|
|
879
|
+
pass
|
|
880
|
+
|
|
881
|
+
async def _forward_client_to_kernel(self, session: SessionInfo):
|
|
882
|
+
"""转发客户端消息到 Kernel(带权限检查)"""
|
|
883
|
+
try:
|
|
884
|
+
while True:
|
|
885
|
+
raw = await session.client_ws.receive_text()
|
|
886
|
+
|
|
887
|
+
# Kernel 已离线,不再转发
|
|
888
|
+
if self._kernel_offline:
|
|
889
|
+
break
|
|
890
|
+
|
|
891
|
+
# Layer 4: 消息大小限制
|
|
892
|
+
if len(raw) > self._max_message_size:
|
|
893
|
+
await session.client_ws.send_json({
|
|
894
|
+
"type": "error",
|
|
895
|
+
"message": "Message too large",
|
|
896
|
+
"max_bytes": self._max_message_size,
|
|
897
|
+
"actual_bytes": len(raw)
|
|
898
|
+
})
|
|
899
|
+
await session.client_ws.close(code=4008, reason="Message too large")
|
|
900
|
+
await self._audit_log("relay.msg_too_large", {
|
|
901
|
+
"session_token": session.session_token,
|
|
902
|
+
"module_id": session.module_id,
|
|
903
|
+
"size": len(raw)
|
|
904
|
+
})
|
|
905
|
+
return
|
|
906
|
+
|
|
907
|
+
# Layer 3: 单连接消息速率限制(超限时丢弃消息 + 警告,不断连)
|
|
908
|
+
if not self._check_msg_rate_limit(session):
|
|
909
|
+
# 每次超限只发一次警告(避免警告本身也占带宽)
|
|
910
|
+
if not getattr(session, '_rate_warn_sent', False):
|
|
911
|
+
try:
|
|
912
|
+
await session.client_ws.send_json({
|
|
913
|
+
"type": "warning",
|
|
914
|
+
"message": "Message rate limit exceeded, messages are being dropped"
|
|
915
|
+
})
|
|
916
|
+
except Exception:
|
|
917
|
+
pass
|
|
918
|
+
session._rate_warn_sent = True
|
|
919
|
+
print(f"[relay] Message rate limit hit for {session.module_id}, dropping messages")
|
|
920
|
+
continue
|
|
921
|
+
else:
|
|
922
|
+
# 速率恢复,重置警告标志
|
|
923
|
+
if getattr(session, '_rate_warn_sent', False):
|
|
924
|
+
session._rate_warn_sent = False
|
|
925
|
+
|
|
926
|
+
msg = json.loads(raw)
|
|
927
|
+
|
|
928
|
+
# 处理心跳 ping
|
|
929
|
+
if msg.get("type") == "ping":
|
|
930
|
+
await session.client_ws.send_json({"type": "pong"})
|
|
931
|
+
continue
|
|
932
|
+
|
|
933
|
+
# 检查权限
|
|
934
|
+
if not self._check_permission(session.role, msg):
|
|
935
|
+
method = msg.get("method", "")
|
|
936
|
+
print(f"\033[91m[relay] ✗ 权限被拒绝: {method} (角色: {session.role})\033[0m")
|
|
937
|
+
if "id" in msg:
|
|
938
|
+
await session.client_ws.send_json({
|
|
939
|
+
"jsonrpc": "2.0",
|
|
940
|
+
"id": msg["id"],
|
|
941
|
+
"error": {
|
|
942
|
+
"code": -32000,
|
|
943
|
+
"message": f"Permission denied: {method} (role: {session.role})"
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
continue
|
|
947
|
+
|
|
948
|
+
# 检查是否是 web.* RPC 调用
|
|
949
|
+
method = msg.get("method", "")
|
|
950
|
+
if method.startswith("web.") and "id" in msg:
|
|
951
|
+
print(f"[relay] Intercepted web RPC: {method}")
|
|
952
|
+
await self._handle_web_rpc(session, msg)
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
# 转发到 Kernel
|
|
956
|
+
await self._send_to_kernel(session.kernel_ws, msg)
|
|
957
|
+
except (WebSocketDisconnect, AttributeError, asyncio.CancelledError,
|
|
958
|
+
websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError):
|
|
959
|
+
pass # 客户端断开、Kernel 连接关闭或 client_ws 已被置 None,正常退出
|
|
960
|
+
|
|
961
|
+
async def _forward_kernel_to_client(self, session: SessionInfo):
|
|
962
|
+
"""转发 Kernel 消息到客户端
|
|
963
|
+
|
|
964
|
+
通过 session._kernel_disconnected 标志区分断开原因:
|
|
965
|
+
- True: kernel_ws 迭代结束或异常(Kernel 真正断开)
|
|
966
|
+
- False: client_ws.send_text 失败(客户端断开,非 Kernel 问题)
|
|
967
|
+
"""
|
|
968
|
+
session._kernel_disconnected = False
|
|
969
|
+
try:
|
|
970
|
+
async for raw in session.kernel_ws:
|
|
971
|
+
if session.client_ws is None:
|
|
972
|
+
break
|
|
973
|
+
try:
|
|
974
|
+
await session.client_ws.send_text(raw)
|
|
975
|
+
except Exception:
|
|
976
|
+
return # client send 失败,_kernel_disconnected 保持 False
|
|
977
|
+
# kernel_ws 迭代正常结束 → Kernel 断开
|
|
978
|
+
session._kernel_disconnected = True
|
|
979
|
+
except Exception:
|
|
980
|
+
# kernel_ws 异常(连接关闭等)→ Kernel 断开
|
|
981
|
+
session._kernel_disconnected = True
|
|
982
|
+
|
|
983
|
+
def _check_permission(self, role: str, msg: dict) -> bool:
|
|
984
|
+
"""检查 RPC 权限"""
|
|
985
|
+
method = msg.get("method")
|
|
986
|
+
if not method:
|
|
987
|
+
return True # 非 RPC 请求,放行
|
|
988
|
+
|
|
989
|
+
# 获取角色的权限列表
|
|
990
|
+
allowed = self.permissions.get(role, [])
|
|
991
|
+
|
|
992
|
+
# 检查是否匹配
|
|
993
|
+
for pattern in allowed:
|
|
994
|
+
if pattern.endswith(".*"):
|
|
995
|
+
# 通配符匹配
|
|
996
|
+
prefix = pattern[:-2]
|
|
997
|
+
if method.startswith(prefix + "."):
|
|
998
|
+
return True
|
|
999
|
+
elif pattern == method:
|
|
1000
|
+
# 精确匹配
|
|
1001
|
+
return True
|
|
1002
|
+
|
|
1003
|
+
return False
|
|
1004
|
+
|
|
1005
|
+
async def _on_client_disconnect(self, session: SessionInfo):
|
|
1006
|
+
"""客户端断开,启动重连等待"""
|
|
1007
|
+
session.status = "waiting_reconnect"
|
|
1008
|
+
session.client_ws = None
|
|
1009
|
+
|
|
1010
|
+
print(f"[relay] Client disconnected: {session.module_id}, waiting {self.reconnect_timeout}s for reconnect")
|
|
1011
|
+
|
|
1012
|
+
# 启动超时计时器
|
|
1013
|
+
timer = asyncio.create_task(self._reconnect_timeout(session))
|
|
1014
|
+
self.reconnect_timers[session.session_token] = timer
|
|
1015
|
+
|
|
1016
|
+
async def _reconnect_timeout(self, session: SessionInfo):
|
|
1017
|
+
"""重连超时,执行优雅退出"""
|
|
1018
|
+
await asyncio.sleep(self.reconnect_timeout)
|
|
1019
|
+
|
|
1020
|
+
# 超时后仍未重连,执行优雅退出
|
|
1021
|
+
if session.status == "waiting_reconnect":
|
|
1022
|
+
await self._graceful_shutdown(session)
|
|
1023
|
+
|
|
1024
|
+
async def _graceful_shutdown(self, session: SessionInfo):
|
|
1025
|
+
"""代表前端执行优雅退出"""
|
|
1026
|
+
print(f"[relay] Graceful shutdown: {session.module_id}")
|
|
1027
|
+
await self._audit_log("auth.logout", {"module_id": session.module_id, "reason": "reconnect_timeout"})
|
|
1028
|
+
|
|
1029
|
+
# 标记这是主动断开,防止触发全局 Kernel 离线
|
|
1030
|
+
session._graceful_shutdown = True
|
|
1031
|
+
|
|
1032
|
+
try:
|
|
1033
|
+
# 发送 module.exiting 事件(带 token_revoked 标记)
|
|
1034
|
+
await self._send_to_kernel(session.kernel_ws, {
|
|
1035
|
+
"jsonrpc": "2.0",
|
|
1036
|
+
"id": str(uuid.uuid4()),
|
|
1037
|
+
"method": "event.publish",
|
|
1038
|
+
"params": {
|
|
1039
|
+
"event_id": str(uuid.uuid4()),
|
|
1040
|
+
"event": "module.exiting",
|
|
1041
|
+
"data": {
|
|
1042
|
+
"module_id": session.module_id,
|
|
1043
|
+
"action": "none",
|
|
1044
|
+
"token_revoked": True
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
# 发送 module.shutdown.ready 事件
|
|
1050
|
+
await self._send_to_kernel(session.kernel_ws, {
|
|
1051
|
+
"jsonrpc": "2.0",
|
|
1052
|
+
"id": str(uuid.uuid4()),
|
|
1053
|
+
"method": "event.publish",
|
|
1054
|
+
"params": {
|
|
1055
|
+
"event_id": str(uuid.uuid4()),
|
|
1056
|
+
"event": "module.shutdown.ready",
|
|
1057
|
+
"data": {
|
|
1058
|
+
"module_id": session.module_id
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
# 断开 Kernel 连接
|
|
1064
|
+
await session.kernel_ws.close()
|
|
1065
|
+
|
|
1066
|
+
except Exception as e:
|
|
1067
|
+
print(f"[relay] Graceful shutdown error: {e}")
|
|
1068
|
+
|
|
1069
|
+
finally:
|
|
1070
|
+
# 删除 session
|
|
1071
|
+
if session.session_token in self.sessions:
|
|
1072
|
+
del self.sessions[session.session_token]
|
|
1073
|
+
if session.session_token in self.reconnect_timers:
|
|
1074
|
+
del self.reconnect_timers[session.session_token]
|
|
1075
|
+
if session.session_token in self._reconnect_timestamps:
|
|
1076
|
+
del self._reconnect_timestamps[session.session_token]
|
|
1077
|
+
|
|
1078
|
+
async def set_kernel_offline(self, offline: bool):
|
|
1079
|
+
"""设置 kernel 离线状态(由 server.py 调用)"""
|
|
1080
|
+
async with self._kernel_offline_lock:
|
|
1081
|
+
if self._kernel_offline == offline:
|
|
1082
|
+
return
|
|
1083
|
+
self._kernel_offline = offline
|
|
1084
|
+
|
|
1085
|
+
if offline:
|
|
1086
|
+
print(f"[relay] Kernel offline detected, closing all {len(self.sessions)} client connections")
|
|
1087
|
+
await self._close_all_clients_with_reason(
|
|
1088
|
+
code=4050,
|
|
1089
|
+
reason="Kernel unavailable",
|
|
1090
|
+
retry_after_ms=2000
|
|
1091
|
+
)
|
|
1092
|
+
else:
|
|
1093
|
+
print(f"[relay] Kernel back online, accepting new connections")
|
|
1094
|
+
|
|
1095
|
+
async def _close_all_clients_with_reason(self, code: int, reason: str, retry_after_ms: int):
|
|
1096
|
+
"""关闭所有客户端连接并发送错误信息"""
|
|
1097
|
+
info = self.kernel_reconnect_info
|
|
1098
|
+
for session in list(self.sessions.values()):
|
|
1099
|
+
try:
|
|
1100
|
+
if session.client_ws:
|
|
1101
|
+
await session.client_ws.send_json({
|
|
1102
|
+
"type": "error",
|
|
1103
|
+
"message": reason,
|
|
1104
|
+
"code": code,
|
|
1105
|
+
"retry_after_ms": retry_after_ms,
|
|
1106
|
+
"kernel_status": info,
|
|
1107
|
+
})
|
|
1108
|
+
await session.client_ws.close(code=code, reason=reason)
|
|
1109
|
+
except Exception as e:
|
|
1110
|
+
print(f"[relay] Failed to close client {session.module_id}: {e}")
|
|
1111
|
+
|
|
1112
|
+
try:
|
|
1113
|
+
if session.kernel_ws:
|
|
1114
|
+
await session.kernel_ws.close()
|
|
1115
|
+
except Exception:
|
|
1116
|
+
pass
|
|
1117
|
+
|
|
1118
|
+
# 清理所有 session
|
|
1119
|
+
self.sessions.clear()
|
|
1120
|
+
self.reconnect_timers.clear()
|
|
1121
|
+
self._reconnect_timestamps.clear()
|
|
1122
|
+
|
|
1123
|
+
async def _send_to_kernel(self, kernel_ws, msg: dict):
|
|
1124
|
+
"""发送消息到 Kernel"""
|
|
1125
|
+
try:
|
|
1126
|
+
await kernel_ws.send(json.dumps(msg))
|
|
1127
|
+
except (websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError):
|
|
1128
|
+
raise # 向上抛出,由调用方处理
|
|
1129
|
+
|
|
1130
|
+
async def _handle_web_rpc(self, session: SessionInfo, msg: dict):
|
|
1131
|
+
"""处理 web.* RPC 调用"""
|
|
1132
|
+
rpc_id = msg.get("id")
|
|
1133
|
+
method = msg.get("method", "")
|
|
1134
|
+
params = msg.get("params", {})
|
|
1135
|
+
|
|
1136
|
+
print(f"[relay] Handling web RPC: {method} (id={rpc_id})")
|
|
1137
|
+
|
|
1138
|
+
# 去掉 web. 前缀
|
|
1139
|
+
if method.startswith("web."):
|
|
1140
|
+
method = method[4:]
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
# 调用 evol_server 的 RPC 处理器
|
|
1144
|
+
if method == "list_tokens":
|
|
1145
|
+
print(f"[relay] Calling list_tokens")
|
|
1146
|
+
result = await self.evol_server._rpc_list_tokens()
|
|
1147
|
+
elif method == "revoke_token":
|
|
1148
|
+
print(f"[relay] Calling revoke_token with params: {params}")
|
|
1149
|
+
result = await self.evol_server._rpc_revoke_token(params)
|
|
1150
|
+
elif method == "disconnect_client":
|
|
1151
|
+
result = await self._rpc_disconnect_client(session, params)
|
|
1152
|
+
else:
|
|
1153
|
+
raise ValueError(f"Unknown method: web.{method}")
|
|
1154
|
+
|
|
1155
|
+
print(f"[relay] RPC success: {method}, result keys: {list(result.keys()) if isinstance(result, dict) else type(result)}")
|
|
1156
|
+
|
|
1157
|
+
# 返回结果
|
|
1158
|
+
if session.client_ws:
|
|
1159
|
+
await session.client_ws.send_json({
|
|
1160
|
+
"jsonrpc": "2.0",
|
|
1161
|
+
"id": rpc_id,
|
|
1162
|
+
"result": result
|
|
1163
|
+
})
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
# 红色高亮显示 RPC 错误
|
|
1166
|
+
print(f"\033[91m[relay] ✗ RPC 错误: {method}, 错误: {e}\033[0m")
|
|
1167
|
+
# 返回错误(仅当连接存在时)
|
|
1168
|
+
if session.client_ws:
|
|
1169
|
+
await session.client_ws.send_json({
|
|
1170
|
+
"jsonrpc": "2.0",
|
|
1171
|
+
"id": rpc_id,
|
|
1172
|
+
"error": {
|
|
1173
|
+
"code": -32603,
|
|
1174
|
+
"message": str(e)
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
async def _rpc_disconnect_client(self, caller_session: SessionInfo, params: dict) -> dict:
|
|
1179
|
+
"""断开指定远程客户端的连接并吊销其 token"""
|
|
1180
|
+
target_module_id = params.get("module_id")
|
|
1181
|
+
if not target_module_id:
|
|
1182
|
+
raise ValueError("module_id required")
|
|
1183
|
+
|
|
1184
|
+
# 不允许断开自己
|
|
1185
|
+
if target_module_id == caller_session.module_id:
|
|
1186
|
+
raise ValueError("Cannot disconnect self")
|
|
1187
|
+
|
|
1188
|
+
# 查找目标 session
|
|
1189
|
+
target_session = None
|
|
1190
|
+
for s in self.sessions.values():
|
|
1191
|
+
if s.module_id == target_module_id:
|
|
1192
|
+
target_session = s
|
|
1193
|
+
break
|
|
1194
|
+
|
|
1195
|
+
if not target_session:
|
|
1196
|
+
raise ValueError(f"Session not found: {target_module_id}")
|
|
1197
|
+
|
|
1198
|
+
print(f"[relay] Disconnecting client {target_module_id} (requested by {caller_session.module_id})")
|
|
1199
|
+
|
|
1200
|
+
# 吊销 token,使其需要重新配对
|
|
1201
|
+
revoked = self.pairing_manager.revoke_token(target_session.frontend_token)
|
|
1202
|
+
if revoked:
|
|
1203
|
+
print(f"[relay] Token revoked for {target_module_id}")
|
|
1204
|
+
else:
|
|
1205
|
+
print(f"[relay] Token revoke skipped for {target_module_id} (not a pairing token or already revoked)")
|
|
1206
|
+
|
|
1207
|
+
# 先通知被断开的客户端(用 4001 关闭码,前端会停止重连)
|
|
1208
|
+
try:
|
|
1209
|
+
if target_session.client_ws:
|
|
1210
|
+
await target_session.client_ws.close(code=4001, reason="Token revoked")
|
|
1211
|
+
except Exception:
|
|
1212
|
+
pass
|
|
1213
|
+
|
|
1214
|
+
# 执行优雅退出(清理 Kernel 连接和 session)
|
|
1215
|
+
await self._graceful_shutdown(target_session)
|
|
1216
|
+
|
|
1217
|
+
return {"disconnected": target_module_id}
|
|
1218
|
+
|
|
1219
|
+
async def close_all_sessions(self):
|
|
1220
|
+
"""优雅关闭所有会话(用于 shutdown)"""
|
|
1221
|
+
print(f"[relay] Closing {len(self.sessions)} active sessions...")
|
|
1222
|
+
|
|
1223
|
+
# 取消所有重连定时器
|
|
1224
|
+
for timer in self.reconnect_timers.values():
|
|
1225
|
+
timer.cancel()
|
|
1226
|
+
self.reconnect_timers.clear()
|
|
1227
|
+
|
|
1228
|
+
# 关闭所有会话(不等待对端确认,shutdown 场景直接断开)
|
|
1229
|
+
for session in list(self.sessions.values()):
|
|
1230
|
+
try:
|
|
1231
|
+
if session.kernel_ws:
|
|
1232
|
+
await session.kernel_ws.close()
|
|
1233
|
+
except Exception:
|
|
1234
|
+
pass
|
|
1235
|
+
try:
|
|
1236
|
+
if session.client_ws:
|
|
1237
|
+
# 客户端 close 不 await,避免等浏览器回 close frame 卡住
|
|
1238
|
+
asyncio.create_task(session.client_ws.close(code=1001, reason="Server shutting down"))
|
|
1239
|
+
except Exception:
|
|
1240
|
+
pass
|
|
1241
|
+
|
|
1242
|
+
self.sessions.clear()
|
|
1243
|
+
self._reconnect_timestamps.clear()
|
|
1244
|
+
print(f"[relay] All sessions closed")
|
|
1245
|
+
|
|
1246
|
+
# ── 审计日志 ──
|
|
1247
|
+
|
|
1248
|
+
async def _audit_log(self, event: str, data: dict):
|
|
1249
|
+
"""
|
|
1250
|
+
发布 auth.* 审计事件到 Kernel。
|
|
1251
|
+
|
|
1252
|
+
Args:
|
|
1253
|
+
event: 事件名(如 auth.login, auth.logout, auth.failed)
|
|
1254
|
+
data: 事件数据
|
|
1255
|
+
"""
|
|
1256
|
+
if not self.evol_server or not self.evol_server._ws:
|
|
1257
|
+
return
|
|
1258
|
+
try:
|
|
1259
|
+
await self.evol_server._publish_event(
|
|
1260
|
+
self.evol_server._ws,
|
|
1261
|
+
event,
|
|
1262
|
+
{
|
|
1263
|
+
**data,
|
|
1264
|
+
"timestamp": time.time(),
|
|
1265
|
+
"source": "relay",
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1268
|
+
except Exception as e:
|
|
1269
|
+
print(f"[relay] Audit log failed: {e}")
|
|
1270
|
+
|
|
1271
|
+
# ── 速率限制 ──
|
|
1272
|
+
|
|
1273
|
+
def _check_rate_limit(self, ip: str, device_id: str) -> bool:
|
|
1274
|
+
"""
|
|
1275
|
+
检查 (IP, device_id) 是否超出速率限制。
|
|
1276
|
+
|
|
1277
|
+
Returns:
|
|
1278
|
+
True 表示允许,False 表示被限制
|
|
1279
|
+
"""
|
|
1280
|
+
key = (ip, device_id)
|
|
1281
|
+
now = time.time()
|
|
1282
|
+
cutoff = now - self._rate_limit_window
|
|
1283
|
+
|
|
1284
|
+
# 获取或创建时间戳列表
|
|
1285
|
+
timestamps = self._rate_limits.get(key, [])
|
|
1286
|
+
|
|
1287
|
+
# 清理过期的时间戳
|
|
1288
|
+
timestamps = [t for t in timestamps if t > cutoff]
|
|
1289
|
+
|
|
1290
|
+
# 检查是否超限
|
|
1291
|
+
if len(timestamps) >= self._rate_limit_max:
|
|
1292
|
+
self._rate_limits[key] = timestamps
|
|
1293
|
+
return False
|
|
1294
|
+
|
|
1295
|
+
# 记录本次请求
|
|
1296
|
+
timestamps.append(now)
|
|
1297
|
+
self._rate_limits[key] = timestamps
|
|
1298
|
+
|
|
1299
|
+
# 定期清理长期不活跃的 key(避免内存泄漏)
|
|
1300
|
+
if len(self._rate_limits) > 1000:
|
|
1301
|
+
self._cleanup_rate_limits(now)
|
|
1302
|
+
|
|
1303
|
+
return True
|
|
1304
|
+
|
|
1305
|
+
def _cleanup_rate_limits(self, now: float):
|
|
1306
|
+
"""清理不活跃的速率限制条目"""
|
|
1307
|
+
cutoff = now - self._rate_limit_window * 10 # 10 个窗口期未活跃则清理
|
|
1308
|
+
expired = [
|
|
1309
|
+
k for k, ts_list in self._rate_limits.items()
|
|
1310
|
+
if not ts_list or ts_list[-1] < cutoff
|
|
1311
|
+
]
|
|
1312
|
+
for k in expired:
|
|
1313
|
+
del self._rate_limits[k]
|
|
1314
|
+
|
|
1315
|
+
@staticmethod
|
|
1316
|
+
def _sliding_window_check(ts: deque, window: float, max_count: int) -> bool:
|
|
1317
|
+
"""滑动窗口速率检查(公共逻辑)。返回 True=允许, False=超限"""
|
|
1318
|
+
now = time.time()
|
|
1319
|
+
cutoff = now - window
|
|
1320
|
+
while ts and ts[0] < cutoff:
|
|
1321
|
+
ts.popleft()
|
|
1322
|
+
if len(ts) >= max_count:
|
|
1323
|
+
return False
|
|
1324
|
+
ts.append(now)
|
|
1325
|
+
return True
|
|
1326
|
+
|
|
1327
|
+
def _check_reconnect_throttle(self, session_token: str) -> tuple[bool, float]:
|
|
1328
|
+
"""Layer 1: Session 级重连节流。返回 (allowed, cooldown_seconds)"""
|
|
1329
|
+
if session_token not in self._reconnect_timestamps:
|
|
1330
|
+
self._reconnect_timestamps[session_token] = deque()
|
|
1331
|
+
|
|
1332
|
+
ts = self._reconnect_timestamps[session_token]
|
|
1333
|
+
if not self._sliding_window_check(ts, self._reconnect_limit_window, self._reconnect_limit_max):
|
|
1334
|
+
cooldown = ts[0] + self._reconnect_limit_window - time.time()
|
|
1335
|
+
return False, max(cooldown, 1.0)
|
|
1336
|
+
|
|
1337
|
+
# 防内存泄漏:超过 1000 个 session 时清理不活跃的
|
|
1338
|
+
if len(self._reconnect_timestamps) > 1000:
|
|
1339
|
+
stale_cutoff = time.time() - self._reconnect_limit_window * 10
|
|
1340
|
+
stale = [k for k, v in self._reconnect_timestamps.items() if not v or v[-1] < stale_cutoff]
|
|
1341
|
+
for k in stale:
|
|
1342
|
+
del self._reconnect_timestamps[k]
|
|
1343
|
+
|
|
1344
|
+
return True, 0
|
|
1345
|
+
|
|
1346
|
+
def _check_msg_rate_limit(self, session: 'SessionInfo') -> bool:
|
|
1347
|
+
"""Layer 3: 单连接消息速率限制"""
|
|
1348
|
+
return self._sliding_window_check(
|
|
1349
|
+
session._msg_timestamps, self._msg_rate_limit_window, self._msg_rate_limit_max
|
|
1350
|
+
)
|