@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,1742 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evol HTTP Server
|
|
3
|
+
Evol account management with full Kite module management UI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
import random
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import websockets
|
|
17
|
+
from fastapi import FastAPI, WebSocket, Request
|
|
18
|
+
from fastapi.staticfiles import StaticFiles
|
|
19
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
20
|
+
|
|
21
|
+
from extensions.services.kite_console.evol_api import EvolAPI
|
|
22
|
+
from extensions.services.kite_console.auth_manager import AuthManager
|
|
23
|
+
from extensions.services.kite_console.stats_manager import StatsManager
|
|
24
|
+
from extensions.services.kite_console.routes.routes_rpc import router as rpc_router, set_evol_server
|
|
25
|
+
from extensions.services.kite_console.routes.routes_test import router as test_router
|
|
26
|
+
from extensions.services.kite_console.routes.routes_llm import router as llm_router
|
|
27
|
+
from extensions.services.kite_console.routes.routes_proxy import router as proxy_router, set_proxy_server
|
|
28
|
+
from extensions.services.kite_console.config_loader import load_business_configs
|
|
29
|
+
from extensions.services.kite_console.pairing import PairingManager
|
|
30
|
+
from extensions.services.kite_console.relay import KernelRelay
|
|
31
|
+
from extensions.services.kite_console.oauth_manager import OAuthManager
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fmt_elapsed(t0: float) -> str:
|
|
35
|
+
"""Format elapsed time since t0."""
|
|
36
|
+
d = time.monotonic() - t0
|
|
37
|
+
if d < 1:
|
|
38
|
+
return f"{d * 1000:.0f}ms"
|
|
39
|
+
if d < 10:
|
|
40
|
+
return f"{d:.1f}s"
|
|
41
|
+
return f"{d:.0f}s"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# System broadcast events
|
|
47
|
+
SYSTEM_BROADCAST_EVENTS = {
|
|
48
|
+
"module.ready", "module.registered", "module.started", "module.stopped",
|
|
49
|
+
"module.crashed", "module.exiting", "module.offline",
|
|
50
|
+
"module.shutdown.ack", "module.shutdown.ready",
|
|
51
|
+
"system.ready", "registry.updated",
|
|
52
|
+
"system.instance.started", "system.instance.stopped",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EvolServer:
|
|
57
|
+
def __init__(self, module_name: str, token: str, kernel_port: int, host: str, port: int, boot_t0: float, max_connections: int = 1,
|
|
58
|
+
gateway_url: str = "", kite_token: str = ""):
|
|
59
|
+
self.module_name = module_name
|
|
60
|
+
self.token = token
|
|
61
|
+
self.kernel_port = kernel_port
|
|
62
|
+
self.host = host
|
|
63
|
+
self.port = port
|
|
64
|
+
self.boot_t0 = boot_t0
|
|
65
|
+
self.max_connections = max_connections
|
|
66
|
+
self.gateway_url = gateway_url
|
|
67
|
+
self.kite_token = kite_token
|
|
68
|
+
self._ws_task: asyncio.Task | None = None
|
|
69
|
+
self._test_task: asyncio.Task | None = None
|
|
70
|
+
self._ws: object | None = None
|
|
71
|
+
self._shutting_down = False
|
|
72
|
+
self._exit_code = 0
|
|
73
|
+
self._auth_failed = False
|
|
74
|
+
self._uvicorn_server = None
|
|
75
|
+
self._start_time = time.time()
|
|
76
|
+
self._pending_rpc = {}
|
|
77
|
+
self._extra_ws: dict = {} # slot → WebSocket
|
|
78
|
+
self._extra_ws_tasks: dict = {} # slot → recv loop Task
|
|
79
|
+
self._has_registered = False
|
|
80
|
+
# Kernel 重连状态(供 relay 报告给前端)
|
|
81
|
+
self._kernel_reconnect_info = {
|
|
82
|
+
"state": "connected", # connected / reconnecting / fatal / exiting
|
|
83
|
+
"attempt": 0,
|
|
84
|
+
"max_attempts": 10,
|
|
85
|
+
"next_retry_ms": 0,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# 用户信息缓存(10秒有效期)
|
|
89
|
+
self._user_info_cache = {} # {evol_token: {"data": ..., "timestamp": ...}}
|
|
90
|
+
self._cache_ttl = 30 # 缓存有效期(秒)
|
|
91
|
+
|
|
92
|
+
# Business data directory
|
|
93
|
+
data_dir = os.environ.get("KITE_DATA", os.path.expanduser("~/.kite/data"))
|
|
94
|
+
console_data_dir = os.path.join(data_dir, "kite_console")
|
|
95
|
+
os.makedirs(console_data_dir, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
self.evol_api = EvolAPI()
|
|
98
|
+
self.auth_manager = AuthManager(console_data_dir)
|
|
99
|
+
self.stats_manager = StatsManager(console_data_dir, self.evol_api, self.auth_manager)
|
|
100
|
+
|
|
101
|
+
# OAuth manager(从 config.json5 加载配置)
|
|
102
|
+
self.oauth_manager = self._init_oauth_manager()
|
|
103
|
+
|
|
104
|
+
self.app = self._create_app()
|
|
105
|
+
|
|
106
|
+
def _init_oauth_manager(self) -> OAuthManager:
|
|
107
|
+
"""从 config.json5 加载 OAuth 配置并初始化 OAuthManager"""
|
|
108
|
+
import json5
|
|
109
|
+
config_path = Path(__file__).parent / "config.json5"
|
|
110
|
+
oauth_cfg = {}
|
|
111
|
+
if config_path.exists():
|
|
112
|
+
try:
|
|
113
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
114
|
+
cfg = json5.load(f)
|
|
115
|
+
oauth_cfg = cfg.get("oauth", {})
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Failed to load OAuth config: {e}")
|
|
118
|
+
|
|
119
|
+
return OAuthManager(
|
|
120
|
+
state_mode=oauth_cfg.get("state_mode", "memory"),
|
|
121
|
+
state_ttl=oauth_cfg.get("state_ttl", 60),
|
|
122
|
+
jwt_secret=oauth_cfg.get("jwt_secret", ""),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _create_app(self) -> FastAPI:
|
|
126
|
+
app = FastAPI(title="Kite Console", docs_url="/docs", redoc_url=None)
|
|
127
|
+
server = self
|
|
128
|
+
|
|
129
|
+
@app.on_event("startup")
|
|
130
|
+
async def _startup():
|
|
131
|
+
# Token already set in entry.py, no need to read from stdin
|
|
132
|
+
if not server.token:
|
|
133
|
+
print(f"\033[31mERROR: Missing token, cannot connect to Kernel\033[0m")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Start Kernel WS connection (module.ready depends on this)
|
|
137
|
+
if server.kernel_port:
|
|
138
|
+
server._ws_task = asyncio.create_task(server._ws_loop())
|
|
139
|
+
server._test_task = asyncio.create_task(server._test_event_loop())
|
|
140
|
+
|
|
141
|
+
# Business initialization in background (does not block module.ready)
|
|
142
|
+
asyncio.create_task(_deferred_init())
|
|
143
|
+
|
|
144
|
+
async def _deferred_init():
|
|
145
|
+
"""Business initialization that runs after WS connection starts."""
|
|
146
|
+
await server.stats_manager.start()
|
|
147
|
+
|
|
148
|
+
# Load business configurations
|
|
149
|
+
module_dir = Path(__file__).parent
|
|
150
|
+
try:
|
|
151
|
+
business_configs = load_business_configs(str(module_dir))
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to load business configs: {e}")
|
|
154
|
+
business_configs = {}
|
|
155
|
+
|
|
156
|
+
# Get relay service config
|
|
157
|
+
relay_business = business_configs.get('relay_service')
|
|
158
|
+
if relay_business:
|
|
159
|
+
try:
|
|
160
|
+
relay_config = relay_business['config']
|
|
161
|
+
|
|
162
|
+
# Initialize pairing manager
|
|
163
|
+
auth_config = relay_config['auth']
|
|
164
|
+
pairing_file = module_dir / auth_config['pairing_code_file']
|
|
165
|
+
pairing_manager = PairingManager(
|
|
166
|
+
pairing_file=str(pairing_file),
|
|
167
|
+
code_length=auth_config['pairing_code_length'],
|
|
168
|
+
token_expiry=auth_config['token_expiry']
|
|
169
|
+
)
|
|
170
|
+
app.state.pairing_manager = pairing_manager
|
|
171
|
+
logger.info("Pairing manager initialized")
|
|
172
|
+
|
|
173
|
+
# Initialize relay service
|
|
174
|
+
relay_service = KernelRelay(
|
|
175
|
+
kernel_host="127.0.0.1",
|
|
176
|
+
kernel_port=server.kernel_port,
|
|
177
|
+
kernel_token=server.token,
|
|
178
|
+
base_module_id=relay_config['relay']['base_module_id'],
|
|
179
|
+
reconnect_timeout=relay_config['relay']['reconnect_timeout'],
|
|
180
|
+
permissions=relay_config['permissions'],
|
|
181
|
+
pairing_manager=pairing_manager,
|
|
182
|
+
evol_server=server,
|
|
183
|
+
auth_manager=server.auth_manager,
|
|
184
|
+
oauth_manager=server.oauth_manager,
|
|
185
|
+
)
|
|
186
|
+
app.state.relay_service = relay_service
|
|
187
|
+
server.relay = relay_service # server.py 中用于通知 relay kernel 状态变化
|
|
188
|
+
logger.info("Relay service initialized")
|
|
189
|
+
except KeyError as e:
|
|
190
|
+
logger.error(f"Missing required config field for relay_service: {e}")
|
|
191
|
+
logger.warning("Relay service disabled due to config error")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Failed to initialize relay_service: {e}", exc_info=True)
|
|
194
|
+
logger.warning("Relay service disabled due to initialization error")
|
|
195
|
+
else:
|
|
196
|
+
logger.info("Relay service not configured (no 'relay_service' in businesses)")
|
|
197
|
+
|
|
198
|
+
@app.on_event("shutdown")
|
|
199
|
+
async def _shutdown():
|
|
200
|
+
await server.stats_manager.stop()
|
|
201
|
+
if server._ws_task:
|
|
202
|
+
server._ws_task.cancel()
|
|
203
|
+
if server._test_task:
|
|
204
|
+
server._test_task.cancel()
|
|
205
|
+
if server._ws:
|
|
206
|
+
await server._ws.close()
|
|
207
|
+
print(f"Shutdown complete")
|
|
208
|
+
|
|
209
|
+
# Health and status endpoints
|
|
210
|
+
@app.get("/health")
|
|
211
|
+
async def health():
|
|
212
|
+
return {
|
|
213
|
+
"status": "healthy",
|
|
214
|
+
"details": {
|
|
215
|
+
"kernel_connected": server._ws is not None,
|
|
216
|
+
"uptime_seconds": round(time.time() - server._start_time),
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@app.get("/status")
|
|
221
|
+
async def status():
|
|
222
|
+
return {
|
|
223
|
+
"module": server.module_name,
|
|
224
|
+
"status": "running",
|
|
225
|
+
"kernel_connected": server._ws is not None,
|
|
226
|
+
"uptime_seconds": round(time.time() - server._start_time),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@app.get("/api/system_info")
|
|
230
|
+
async def system_info():
|
|
231
|
+
"""获取系统版本和环境信息"""
|
|
232
|
+
# 从环境变量读取版本号和环境类型
|
|
233
|
+
version = os.environ.get("KITE_VERSION", "unknown")
|
|
234
|
+
env = os.environ.get("KITE_ENV", "development")
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"success": True,
|
|
238
|
+
"data": {
|
|
239
|
+
"version": version,
|
|
240
|
+
"environment": env
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Evol API routes
|
|
245
|
+
@app.post("/api/send_sms")
|
|
246
|
+
async def send_sms(request: Request):
|
|
247
|
+
data = await request.json()
|
|
248
|
+
phone = data.get("phone", "")
|
|
249
|
+
result = await server.evol_api.send_sms(phone)
|
|
250
|
+
return JSONResponse(result)
|
|
251
|
+
|
|
252
|
+
@app.post("/api/verify_sms")
|
|
253
|
+
async def verify_sms(request: Request):
|
|
254
|
+
data = await request.json()
|
|
255
|
+
phone = data.get("phone", "")
|
|
256
|
+
code = data.get("code", "")
|
|
257
|
+
device_info = data.get("deviceInfo", {})
|
|
258
|
+
|
|
259
|
+
result = await server.evol_api.verify_sms(phone, code)
|
|
260
|
+
if not result.get("success"):
|
|
261
|
+
return JSONResponse(result)
|
|
262
|
+
|
|
263
|
+
evol_data = result["data"]
|
|
264
|
+
evol_token = evol_data.get("token", "")
|
|
265
|
+
server.auth_manager.save_evol_token(phone, evol_token, evol_data)
|
|
266
|
+
|
|
267
|
+
# 生成 Kite Token
|
|
268
|
+
kite_token = server.auth_manager.generate_kite_token(device_info)
|
|
269
|
+
|
|
270
|
+
# 绑定 Kite Token 到手机号
|
|
271
|
+
server.auth_manager.bind_kite_token_to_phone(kite_token, phone)
|
|
272
|
+
|
|
273
|
+
return JSONResponse({
|
|
274
|
+
"success": True,
|
|
275
|
+
"kiteToken": kite_token,
|
|
276
|
+
"data": {
|
|
277
|
+
"userInfo": evol_data.get("userInfo", {}),
|
|
278
|
+
"apiKey": evol_data.get("apiKey", ""),
|
|
279
|
+
"credits": evol_data.get("credits", 0)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
# Mount module management routes
|
|
284
|
+
app.include_router(rpc_router, prefix="/api")
|
|
285
|
+
app.include_router(test_router, prefix="/api")
|
|
286
|
+
app.include_router(llm_router, prefix="/api")
|
|
287
|
+
app.include_router(proxy_router, prefix="/api")
|
|
288
|
+
|
|
289
|
+
# ── OAuth routes ──
|
|
290
|
+
|
|
291
|
+
@app.get("/auth/oauth/{provider}/authorize")
|
|
292
|
+
async def oauth_authorize(provider: str, redirect_uri: str = ""):
|
|
293
|
+
"""发起 OAuth 授权,返回重定向 URL"""
|
|
294
|
+
import json5
|
|
295
|
+
config_path = Path(__file__).parent / "config.json5"
|
|
296
|
+
oauth_cfg = {}
|
|
297
|
+
if config_path.exists():
|
|
298
|
+
try:
|
|
299
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
300
|
+
cfg = json5.load(f)
|
|
301
|
+
oauth_cfg = cfg.get("oauth", {})
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
providers = oauth_cfg.get("providers", {})
|
|
306
|
+
provider_cfg = providers.get(provider)
|
|
307
|
+
if not provider_cfg or not provider_cfg.get("client_id"):
|
|
308
|
+
return JSONResponse(
|
|
309
|
+
{"error": f"OAuth provider '{provider}' not configured"},
|
|
310
|
+
status_code=400
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# 生成 state(CSRF 防护)
|
|
314
|
+
state = server.oauth_manager.generate_state(provider, redirect_uri)
|
|
315
|
+
|
|
316
|
+
# 构建授权 URL
|
|
317
|
+
client_id = provider_cfg["client_id"]
|
|
318
|
+
cb_uri = provider_cfg.get("redirect_uri", "")
|
|
319
|
+
|
|
320
|
+
if provider == "github":
|
|
321
|
+
auth_url = (
|
|
322
|
+
f"https://github.com/login/oauth/authorize"
|
|
323
|
+
f"?client_id={client_id}&state={state}"
|
|
324
|
+
f"&redirect_uri={cb_uri}&scope=read:user,user:email"
|
|
325
|
+
)
|
|
326
|
+
elif provider == "google":
|
|
327
|
+
auth_url = (
|
|
328
|
+
f"https://accounts.google.com/o/oauth2/v2/auth"
|
|
329
|
+
f"?client_id={client_id}&state={state}"
|
|
330
|
+
f"&redirect_uri={cb_uri}&scope=openid+email+profile"
|
|
331
|
+
f"&response_type=code&access_type=offline"
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
return JSONResponse(
|
|
335
|
+
{"error": f"Unsupported OAuth provider: {provider}"},
|
|
336
|
+
status_code=400
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return JSONResponse({"authorize_url": auth_url, "state": state})
|
|
340
|
+
|
|
341
|
+
@app.get("/auth/oauth/callback")
|
|
342
|
+
async def oauth_callback(request: Request):
|
|
343
|
+
"""OAuth 回调,用 code 换 token,生成 auth_ticket"""
|
|
344
|
+
import httpx
|
|
345
|
+
|
|
346
|
+
code = request.query_params.get("code")
|
|
347
|
+
state = request.query_params.get("state")
|
|
348
|
+
|
|
349
|
+
if not code or not state:
|
|
350
|
+
return JSONResponse({"error": "Missing code or state"}, status_code=400)
|
|
351
|
+
|
|
352
|
+
# 验证 state
|
|
353
|
+
state_info = server.oauth_manager.verify_state(state)
|
|
354
|
+
if not state_info:
|
|
355
|
+
return JSONResponse({"error": "Invalid or expired state"}, status_code=400)
|
|
356
|
+
|
|
357
|
+
provider = state_info["provider"]
|
|
358
|
+
|
|
359
|
+
# 加载 provider 配置
|
|
360
|
+
import json5
|
|
361
|
+
config_path = Path(__file__).parent / "config.json5"
|
|
362
|
+
oauth_cfg = {}
|
|
363
|
+
if config_path.exists():
|
|
364
|
+
try:
|
|
365
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
366
|
+
cfg = json5.load(f)
|
|
367
|
+
oauth_cfg = cfg.get("oauth", {})
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
provider_cfg = oauth_cfg.get("providers", {}).get(provider, {})
|
|
372
|
+
client_id = provider_cfg.get("client_id", "")
|
|
373
|
+
client_secret = provider_cfg.get("client_secret", "")
|
|
374
|
+
cb_uri = provider_cfg.get("redirect_uri", "")
|
|
375
|
+
|
|
376
|
+
if not client_id or not client_secret:
|
|
377
|
+
return JSONResponse({"error": "OAuth provider not properly configured"}, status_code=500)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
381
|
+
# 用 code 换 access_token
|
|
382
|
+
if provider == "github":
|
|
383
|
+
token_resp = await client.post(
|
|
384
|
+
"https://github.com/login/oauth/access_token",
|
|
385
|
+
json={"client_id": client_id, "client_secret": client_secret, "code": code, "redirect_uri": cb_uri},
|
|
386
|
+
headers={"Accept": "application/json"}
|
|
387
|
+
)
|
|
388
|
+
token_data = token_resp.json()
|
|
389
|
+
access_token = token_data.get("access_token")
|
|
390
|
+
if not access_token:
|
|
391
|
+
return JSONResponse({"error": "Failed to get access token", "detail": token_data}, status_code=400)
|
|
392
|
+
|
|
393
|
+
# 获取用户信息
|
|
394
|
+
user_resp = await client.get(
|
|
395
|
+
"https://api.github.com/user",
|
|
396
|
+
headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
|
|
397
|
+
)
|
|
398
|
+
user_data = user_resp.json()
|
|
399
|
+
user_info = {
|
|
400
|
+
"id": str(user_data.get("id", "")),
|
|
401
|
+
"name": user_data.get("name") or user_data.get("login", ""),
|
|
402
|
+
"email": user_data.get("email", ""),
|
|
403
|
+
"provider": "github",
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
elif provider == "google":
|
|
407
|
+
token_resp = await client.post(
|
|
408
|
+
"https://oauth2.googleapis.com/token",
|
|
409
|
+
data={"client_id": client_id, "client_secret": client_secret, "code": code, "redirect_uri": cb_uri, "grant_type": "authorization_code"}
|
|
410
|
+
)
|
|
411
|
+
token_data = token_resp.json()
|
|
412
|
+
access_token = token_data.get("access_token")
|
|
413
|
+
if not access_token:
|
|
414
|
+
return JSONResponse({"error": "Failed to get access token", "detail": token_data}, status_code=400)
|
|
415
|
+
|
|
416
|
+
user_resp = await client.get(
|
|
417
|
+
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
418
|
+
headers={"Authorization": f"Bearer {access_token}"}
|
|
419
|
+
)
|
|
420
|
+
user_data = user_resp.json()
|
|
421
|
+
user_info = {
|
|
422
|
+
"id": user_data.get("id", ""),
|
|
423
|
+
"name": user_data.get("name", ""),
|
|
424
|
+
"email": user_data.get("email", ""),
|
|
425
|
+
"provider": "google",
|
|
426
|
+
}
|
|
427
|
+
else:
|
|
428
|
+
return JSONResponse({"error": f"Unsupported provider: {provider}"}, status_code=400)
|
|
429
|
+
|
|
430
|
+
except httpx.HTTPError as e:
|
|
431
|
+
return JSONResponse({"error": f"OAuth exchange failed: {e}"}, status_code=502)
|
|
432
|
+
|
|
433
|
+
# 生成一次性 auth_ticket
|
|
434
|
+
ticket = server.oauth_manager.generate_auth_ticket(provider, user_info)
|
|
435
|
+
|
|
436
|
+
# 返回 ticket(前端用此 ticket 通过 WebSocket 认证)
|
|
437
|
+
redirect_uri = state_info.get("redirect_uri", "")
|
|
438
|
+
if redirect_uri:
|
|
439
|
+
# 重定向回前端,附带 ticket
|
|
440
|
+
from urllib.parse import urlencode
|
|
441
|
+
sep = "&" if "?" in redirect_uri else "?"
|
|
442
|
+
return JSONResponse(
|
|
443
|
+
{"redirect": f"{redirect_uri}{sep}{urlencode({'auth_ticket': ticket})}"},
|
|
444
|
+
status_code=200
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return JSONResponse({"auth_ticket": ticket})
|
|
448
|
+
|
|
449
|
+
# Relay WebSocket endpoint
|
|
450
|
+
@app.websocket("/ws/relay")
|
|
451
|
+
async def relay_endpoint(ws: WebSocket):
|
|
452
|
+
relay_service = getattr(app.state, 'relay_service', None)
|
|
453
|
+
if relay_service:
|
|
454
|
+
await relay_service.handle_client(ws)
|
|
455
|
+
else:
|
|
456
|
+
await ws.close(code=1011, reason="Relay service not initialized")
|
|
457
|
+
|
|
458
|
+
# Set evol server reference for RPC forwarding
|
|
459
|
+
set_evol_server(server)
|
|
460
|
+
set_proxy_server(server)
|
|
461
|
+
|
|
462
|
+
# Serve frontend static files
|
|
463
|
+
static_dir = Path(__file__).parent / "static"
|
|
464
|
+
if static_dir.exists():
|
|
465
|
+
@app.get("/")
|
|
466
|
+
async def serve_index():
|
|
467
|
+
index_path = static_dir / "index.html"
|
|
468
|
+
if index_path.exists():
|
|
469
|
+
return FileResponse(index_path)
|
|
470
|
+
return {"message": "Kite Evol Module"}
|
|
471
|
+
|
|
472
|
+
@app.get("/pairing.html")
|
|
473
|
+
async def serve_pairing():
|
|
474
|
+
pairing_path = static_dir / "pairing.html"
|
|
475
|
+
if pairing_path.exists():
|
|
476
|
+
return FileResponse(pairing_path)
|
|
477
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
478
|
+
|
|
479
|
+
@app.get("/test_registry.html")
|
|
480
|
+
async def serve_test_registry():
|
|
481
|
+
test_path = static_dir / "test_registry.html"
|
|
482
|
+
if test_path.exists():
|
|
483
|
+
return FileResponse(test_path)
|
|
484
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
485
|
+
|
|
486
|
+
@app.get("/test_relay.html")
|
|
487
|
+
async def serve_test_relay():
|
|
488
|
+
test_path = static_dir / "test_relay.html"
|
|
489
|
+
if test_path.exists():
|
|
490
|
+
return FileResponse(test_path)
|
|
491
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
492
|
+
|
|
493
|
+
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
494
|
+
|
|
495
|
+
@app.get("/js/{file_path:path}")
|
|
496
|
+
async def serve_js(file_path: str):
|
|
497
|
+
file = static_dir / "js" / file_path
|
|
498
|
+
if file.exists() and file.is_file():
|
|
499
|
+
return FileResponse(file)
|
|
500
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
501
|
+
|
|
502
|
+
@app.get("/css/{file_path:path}")
|
|
503
|
+
async def serve_css(file_path: str):
|
|
504
|
+
file = static_dir / "css" / file_path
|
|
505
|
+
if file.exists() and file.is_file():
|
|
506
|
+
return FileResponse(file)
|
|
507
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
508
|
+
|
|
509
|
+
return app
|
|
510
|
+
|
|
511
|
+
# ── system.require_init 处理 ──
|
|
512
|
+
|
|
513
|
+
async def _do_init(self, ws):
|
|
514
|
+
"""收到 system.require_init 后执行:订阅 + 注册 + module.ready"""
|
|
515
|
+
reason = "startup" if not self._has_registered else "recovery"
|
|
516
|
+
print(f"Received system.require_init (reason={reason})")
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
# Subscribe to events
|
|
520
|
+
await self._rpc_call(ws, "event.subscribe", {
|
|
521
|
+
"events": [
|
|
522
|
+
"module.started",
|
|
523
|
+
"module.stopped",
|
|
524
|
+
"module.crashed",
|
|
525
|
+
"module.ready",
|
|
526
|
+
"module.exiting",
|
|
527
|
+
"module.shutdown",
|
|
528
|
+
"module.shutdown.ack",
|
|
529
|
+
"module.shutdown.ready",
|
|
530
|
+
],
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
# Register to Kernel
|
|
534
|
+
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
535
|
+
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
536
|
+
await self._rpc_call(ws, "registry.register", {
|
|
537
|
+
"module_id": self.module_name,
|
|
538
|
+
"module_type": "service",
|
|
539
|
+
"base_url": f"http://127.0.0.1:{self.port}",
|
|
540
|
+
"health_path": "/health",
|
|
541
|
+
"display": {
|
|
542
|
+
"urls": {
|
|
543
|
+
"Kite 控制台": f"http://127.0.0.1:{self.port}"
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
"tools": {
|
|
547
|
+
"rpc": {
|
|
548
|
+
"module": {
|
|
549
|
+
"health": {"method": "health", "description": "健康检查"},
|
|
550
|
+
"status": {"method": "status", "description": "状态查询"}
|
|
551
|
+
},
|
|
552
|
+
"console": {
|
|
553
|
+
"list_tokens": {"method": "list_tokens", "description": "列出所有令牌"},
|
|
554
|
+
"list_kite_tokens": {"method": "list_kite_tokens", "description": "列出 Kite 令牌"},
|
|
555
|
+
"list_evol_tokens": {"method": "list_evol_tokens", "description": "列出 Evol 令牌"},
|
|
556
|
+
"revoke_token": {"method": "revoke_token", "description": "撤销 Kite 令牌"},
|
|
557
|
+
"revoke_evol_token": {"method": "revoke_evol_token", "description": "撤销 Evol 令牌"},
|
|
558
|
+
"llm_sessions_list": {"method": "llm_sessions_list", "description": "列出 LLM 会话"},
|
|
559
|
+
"llm_sessions_get": {"method": "llm_sessions_get", "description": "获取 LLM 会话详情"},
|
|
560
|
+
"llm_sessions_save": {"method": "llm_sessions_save", "description": "保存 LLM 会话"},
|
|
561
|
+
"llm_sessions_delete": {"method": "llm_sessions_delete", "description": "删除 LLM 会话"},
|
|
562
|
+
"llm_sessions_sync": {"method": "llm_sessions_sync", "description": "同步 LLM 会话"},
|
|
563
|
+
"get_system_context": {"method": "get_system_context", "description": "获取系统上下文(CLAUDE.md + module.md + 注册中心)"},
|
|
564
|
+
"chat": {"method": "chat", "description": "LLM 聊天(转发到大模型 API)"},
|
|
565
|
+
"file_read": {
|
|
566
|
+
"method": "file_read",
|
|
567
|
+
"description": "读取项目文件内容(路径限制在项目目录内)",
|
|
568
|
+
"parameters": {
|
|
569
|
+
"type": "object",
|
|
570
|
+
"properties": {
|
|
571
|
+
"path": {"type": "string", "description": "文件路径(相对项目根目录)"},
|
|
572
|
+
"lines": {"type": "number", "description": "最大读取行数(默认 200)"}
|
|
573
|
+
},
|
|
574
|
+
"required": ["path"]
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
"llm_config_get": {"method": "llm_config_get", "description": "获取 LLM 测试配置"},
|
|
578
|
+
"llm_config_save": {"method": "llm_config_save", "description": "保存 LLM 测试配置"}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
"events_publish": {
|
|
583
|
+
"kite_console": {
|
|
584
|
+
"test": {"description": "Test event from kite_console module"},
|
|
585
|
+
"started": {"description": "Kite Console UI started with access URL"},
|
|
586
|
+
"chat.chunk": {"description": "LLM chat response chunk (or final result)"},
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"events_subscribe": [
|
|
590
|
+
"module.started",
|
|
591
|
+
"module.stopped",
|
|
592
|
+
"module.crashed",
|
|
593
|
+
"module.ready",
|
|
594
|
+
"module.exiting",
|
|
595
|
+
"module.shutdown",
|
|
596
|
+
"module.shutdown.ack",
|
|
597
|
+
"module.shutdown.ready",
|
|
598
|
+
],
|
|
599
|
+
})
|
|
600
|
+
print(f"Registered to Kernel{elapsed_str}")
|
|
601
|
+
|
|
602
|
+
# Send module.ready
|
|
603
|
+
if not self._shutting_down:
|
|
604
|
+
startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
605
|
+
await self._publish_event(ws, "module.ready", {
|
|
606
|
+
"module_id": self.module_name,
|
|
607
|
+
"graceful_shutdown": True,
|
|
608
|
+
"startup_time": startup_time,
|
|
609
|
+
"reason": reason,
|
|
610
|
+
})
|
|
611
|
+
elapsed_str = _fmt_elapsed(self.boot_t0)
|
|
612
|
+
print(f"module.ready published ({elapsed_str})")
|
|
613
|
+
|
|
614
|
+
# Publish kite_console.started event
|
|
615
|
+
display_host = "localhost" if self.host == "0.0.0.0" else self.host
|
|
616
|
+
access_url = f"http://{display_host}:{self.port}"
|
|
617
|
+
await self._publish_event(ws, "kite_console.started", {
|
|
618
|
+
"module_id": self.module_name,
|
|
619
|
+
"url": access_url,
|
|
620
|
+
"host": self.host,
|
|
621
|
+
"port": self.port,
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
self._has_registered = True
|
|
625
|
+
|
|
626
|
+
# 通知 relay kernel 已上线(模块完成注册,可以接受客户端连接)
|
|
627
|
+
relay = getattr(self, 'relay', None)
|
|
628
|
+
if relay:
|
|
629
|
+
asyncio.create_task(relay.set_kernel_offline(False))
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
print(f"_do_init failed: {e}")
|
|
633
|
+
|
|
634
|
+
# ── Kernel WebSocket client ──
|
|
635
|
+
|
|
636
|
+
async def _ws_loop(self):
|
|
637
|
+
retry_delay = 0.5
|
|
638
|
+
max_delay = 10.0
|
|
639
|
+
attempt = 0
|
|
640
|
+
cooldown_attempts = 0
|
|
641
|
+
conn_refused_count = 0
|
|
642
|
+
|
|
643
|
+
while not self._shutting_down:
|
|
644
|
+
try:
|
|
645
|
+
await self._ws_connect()
|
|
646
|
+
retry_delay = 0.5
|
|
647
|
+
attempt = 0
|
|
648
|
+
cooldown_attempts = 0
|
|
649
|
+
conn_refused_count = 0
|
|
650
|
+
self._kernel_reconnect_info = {"state": "connected", "attempt": 0, "max_attempts": 10, "next_retry_ms": 0}
|
|
651
|
+
except asyncio.CancelledError:
|
|
652
|
+
print(f"WS loop cancelled")
|
|
653
|
+
return
|
|
654
|
+
except Exception as e:
|
|
655
|
+
if self._shutting_down:
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
code = self._get_close_code(e)
|
|
659
|
+
|
|
660
|
+
# never: 永不重连
|
|
661
|
+
if code in (4001, 4003, 4004, 1008, 4010):
|
|
662
|
+
print(f"\033[31m致命错误 (code {code}),退出\033[0m")
|
|
663
|
+
self._kernel_reconnect_info = {"state": "fatal", "attempt": 0, "max_attempts": 0, "next_retry_ms": 0}
|
|
664
|
+
self._exit_code = 1
|
|
665
|
+
self._auth_failed = True
|
|
666
|
+
self._shutting_down = True
|
|
667
|
+
if self._uvicorn_server:
|
|
668
|
+
self._uvicorn_server.should_exit = True
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
# cooldown: 速率限制
|
|
672
|
+
if code == 4020:
|
|
673
|
+
cooldown_attempts += 1
|
|
674
|
+
if cooldown_attempts >= 5:
|
|
675
|
+
print(f"\033[31m速率限制重试 5 次,退出\033[0m")
|
|
676
|
+
self._kernel_reconnect_info = {"state": "fatal", "attempt": cooldown_attempts, "max_attempts": 5, "next_retry_ms": 0}
|
|
677
|
+
self._exit_code = 1
|
|
678
|
+
self._shutting_down = True
|
|
679
|
+
if self._uvicorn_server:
|
|
680
|
+
self._uvicorn_server.should_exit = True
|
|
681
|
+
return
|
|
682
|
+
self._kernel_reconnect_info = {"state": "cooldown", "attempt": cooldown_attempts, "max_attempts": 5, "next_retry_ms": 10000}
|
|
683
|
+
self._update_relay_reconnect_info()
|
|
684
|
+
print(f"\033[33m速率限制,10.0s 后重试 ({cooldown_attempts}/5)\033[0m")
|
|
685
|
+
await asyncio.sleep(10.0)
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
# 连接被拒绝/重置 — Kernel 可能正在关闭
|
|
689
|
+
# Windows 的 [WinError 1225] 是 OSError 不是 ConnectionRefusedError
|
|
690
|
+
_is_conn_refused = isinstance(e, (ConnectionRefusedError, ConnectionResetError))
|
|
691
|
+
if not _is_conn_refused and isinstance(e, OSError) and getattr(e, 'winerror', None) == 1225:
|
|
692
|
+
_is_conn_refused = True
|
|
693
|
+
if _is_conn_refused:
|
|
694
|
+
conn_refused_count += 1
|
|
695
|
+
if conn_refused_count >= 3:
|
|
696
|
+
print(f"Kernel 持续不可达 ({conn_refused_count} 次),正常退出")
|
|
697
|
+
self._kernel_reconnect_info = {"state": "exiting", "attempt": conn_refused_count, "max_attempts": 3, "next_retry_ms": 0}
|
|
698
|
+
self._shutting_down = True
|
|
699
|
+
if self._uvicorn_server:
|
|
700
|
+
self._uvicorn_server.should_exit = True
|
|
701
|
+
return
|
|
702
|
+
jitter = retry_delay * 0.2 * random.random()
|
|
703
|
+
sleep_time = retry_delay + jitter
|
|
704
|
+
self._kernel_reconnect_info = {"state": "reconnecting", "attempt": conn_refused_count, "max_attempts": 3, "next_retry_ms": int(sleep_time * 1000)}
|
|
705
|
+
self._update_relay_reconnect_info()
|
|
706
|
+
print(f"Kernel 连接失败: {type(e).__name__}, {sleep_time:.1f}s 后重试 ({conn_refused_count}/3)")
|
|
707
|
+
else:
|
|
708
|
+
conn_refused_count = 0
|
|
709
|
+
|
|
710
|
+
# standard: 指数退避 + jitter
|
|
711
|
+
attempt += 1
|
|
712
|
+
jitter = retry_delay * 0.2 * random.random()
|
|
713
|
+
sleep_time = retry_delay + jitter
|
|
714
|
+
print(f"\033[31mKernel connection error: {e}, retrying in {sleep_time:.1f}s (attempt {attempt})\033[0m")
|
|
715
|
+
if attempt >= 10:
|
|
716
|
+
print(f"连续 {attempt} 次连接失败,正常退出")
|
|
717
|
+
self._kernel_reconnect_info = {"state": "exiting", "attempt": attempt, "max_attempts": 10, "next_retry_ms": 0}
|
|
718
|
+
self._shutting_down = True
|
|
719
|
+
if self._uvicorn_server:
|
|
720
|
+
self._uvicorn_server.should_exit = True
|
|
721
|
+
return
|
|
722
|
+
self._kernel_reconnect_info = {"state": "reconnecting", "attempt": attempt, "max_attempts": 10, "next_retry_ms": int(sleep_time * 1000)}
|
|
723
|
+
self._update_relay_reconnect_info()
|
|
724
|
+
|
|
725
|
+
self._ws = None
|
|
726
|
+
if self._shutting_down:
|
|
727
|
+
return
|
|
728
|
+
await asyncio.sleep(sleep_time if 'sleep_time' in locals() else retry_delay)
|
|
729
|
+
retry_delay = min(retry_delay * 2, max_delay)
|
|
730
|
+
|
|
731
|
+
def _update_relay_reconnect_info(self):
|
|
732
|
+
"""将当前 Kernel 重连状态同步到 relay"""
|
|
733
|
+
relay = getattr(self, 'relay', None)
|
|
734
|
+
if relay:
|
|
735
|
+
relay.kernel_reconnect_info = self._kernel_reconnect_info.copy()
|
|
736
|
+
|
|
737
|
+
def _get_close_code(self, e: Exception) -> int:
|
|
738
|
+
"""从 websockets 异常中提取关闭码"""
|
|
739
|
+
if hasattr(e, 'rcvd') and e.rcvd is not None:
|
|
740
|
+
return getattr(e.rcvd, 'code', 0)
|
|
741
|
+
return 0
|
|
742
|
+
|
|
743
|
+
async def _ws_receiver(self, ws):
|
|
744
|
+
"""WebSocket 接收循环(后台任务)"""
|
|
745
|
+
try:
|
|
746
|
+
async for raw in ws:
|
|
747
|
+
try:
|
|
748
|
+
msg = json.loads(raw)
|
|
749
|
+
except (json.JSONDecodeError, TypeError):
|
|
750
|
+
continue
|
|
751
|
+
|
|
752
|
+
try:
|
|
753
|
+
has_method = "method" in msg
|
|
754
|
+
has_id = "id" in msg
|
|
755
|
+
has_result_or_error = "result" in msg or "error" in msg
|
|
756
|
+
|
|
757
|
+
if has_method and not has_id:
|
|
758
|
+
# 检测 system.require_init 事件
|
|
759
|
+
params = msg.get("params", {})
|
|
760
|
+
event_type = params.get("event", "")
|
|
761
|
+
if event_type == "system.require_init":
|
|
762
|
+
asyncio.create_task(self._do_init(ws))
|
|
763
|
+
continue
|
|
764
|
+
# 事件通知也需要异步处理,避免阻塞接收循环
|
|
765
|
+
asyncio.create_task(self._handle_event_notification(msg))
|
|
766
|
+
elif has_method and has_id:
|
|
767
|
+
asyncio.create_task(self._handle_rpc_request(ws, msg))
|
|
768
|
+
elif has_id and has_result_or_error:
|
|
769
|
+
self._handle_rpc_response(msg)
|
|
770
|
+
except Exception as e:
|
|
771
|
+
print(f"消息处理异常(已忽略): {e}")
|
|
772
|
+
except Exception as e:
|
|
773
|
+
print(f"Receive loop exited with exception: {e}")
|
|
774
|
+
finally:
|
|
775
|
+
print(f"Receive loop ended")
|
|
776
|
+
|
|
777
|
+
async def _ws_connect(self):
|
|
778
|
+
url = f"ws://127.0.0.1:{self.kernel_port}/ws?id={self.module_name}"
|
|
779
|
+
print(f"WS connecting to Kernel")
|
|
780
|
+
try:
|
|
781
|
+
async with websockets.connect(url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
|
|
782
|
+
# Send auth message first
|
|
783
|
+
auth_req = {
|
|
784
|
+
"jsonrpc": "2.0",
|
|
785
|
+
"id": "auth",
|
|
786
|
+
"method": "auth",
|
|
787
|
+
"params": {"token": self.token}
|
|
788
|
+
}
|
|
789
|
+
await ws.send(json.dumps(auth_req))
|
|
790
|
+
|
|
791
|
+
# Wait for auth response
|
|
792
|
+
auth_resp_raw = await asyncio.wait_for(ws.recv(), timeout=5)
|
|
793
|
+
auth_resp = json.loads(auth_resp_raw)
|
|
794
|
+
if "error" in auth_resp:
|
|
795
|
+
raise Exception(f"Auth failed: {auth_resp['error']}")
|
|
796
|
+
|
|
797
|
+
self._ws = ws
|
|
798
|
+
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
799
|
+
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
800
|
+
print(f"Connected to Kernel{elapsed_str}")
|
|
801
|
+
|
|
802
|
+
# 启动接收循环(后台任务,等待 system.require_init 触发 _do_init)
|
|
803
|
+
receiver_task = asyncio.create_task(self._ws_receiver(ws))
|
|
804
|
+
print(f"Receiver task started")
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
# 等待接收循环结束(连接断开)
|
|
808
|
+
await receiver_task
|
|
809
|
+
except Exception as e:
|
|
810
|
+
# 取消接收任务
|
|
811
|
+
receiver_task.cancel()
|
|
812
|
+
try:
|
|
813
|
+
await receiver_task
|
|
814
|
+
except asyncio.CancelledError:
|
|
815
|
+
pass
|
|
816
|
+
raise
|
|
817
|
+
except Exception as e:
|
|
818
|
+
print(f"\033[31mWebSocket connection error: {e}\033[0m")
|
|
819
|
+
raise
|
|
820
|
+
finally:
|
|
821
|
+
print(f"WebSocket connection closed")
|
|
822
|
+
self._ws = None
|
|
823
|
+
# 通知 relay kernel 离线(先同步重连状态)
|
|
824
|
+
relay = getattr(self, 'relay', None)
|
|
825
|
+
if relay:
|
|
826
|
+
relay.kernel_reconnect_info = self._kernel_reconnect_info.copy()
|
|
827
|
+
asyncio.create_task(relay.set_kernel_offline(True))
|
|
828
|
+
# 清理所有未完成的 RPC future
|
|
829
|
+
for fut in self._pending_rpc.values():
|
|
830
|
+
if not fut.done():
|
|
831
|
+
fut.set_exception(ConnectionError("WebSocket disconnected"))
|
|
832
|
+
self._pending_rpc.clear()
|
|
833
|
+
|
|
834
|
+
async def _rpc_call(self, ws, method: str, params: dict = None,
|
|
835
|
+
wait_response: bool = True, timeout: float = 3.0) -> dict:
|
|
836
|
+
"""JSON-RPC 2.0 request。默认等待响应。"""
|
|
837
|
+
rpc_id = str(uuid.uuid4())
|
|
838
|
+
msg = {"jsonrpc": "2.0", "id": rpc_id, "method": method}
|
|
839
|
+
if params:
|
|
840
|
+
msg["params"] = params
|
|
841
|
+
if not wait_response:
|
|
842
|
+
await ws.send(json.dumps(msg))
|
|
843
|
+
return
|
|
844
|
+
future = asyncio.get_event_loop().create_future()
|
|
845
|
+
self._pending_rpc[rpc_id] = future
|
|
846
|
+
await ws.send(json.dumps(msg))
|
|
847
|
+
try:
|
|
848
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
849
|
+
except asyncio.TimeoutError:
|
|
850
|
+
self._pending_rpc.pop(rpc_id, None)
|
|
851
|
+
return {"error": {"code": -32000, "message": f"RPC timeout: {method} ({timeout}s)"}}
|
|
852
|
+
|
|
853
|
+
def _handle_rpc_response(self, msg: dict):
|
|
854
|
+
"""处理 RPC 响应(id + result/error)"""
|
|
855
|
+
rpc_id = msg.get("id")
|
|
856
|
+
future = self._pending_rpc.pop(rpc_id, None)
|
|
857
|
+
if future and not future.done():
|
|
858
|
+
future.set_result(msg)
|
|
859
|
+
|
|
860
|
+
async def _handle_ping_event(self, data: dict):
|
|
861
|
+
"""Handle system.ping event and reply with system.pong."""
|
|
862
|
+
import time
|
|
863
|
+
t1 = data.get("ping_time")
|
|
864
|
+
t2 = time.time()
|
|
865
|
+
|
|
866
|
+
if self._ws:
|
|
867
|
+
await self._publish_event(self._ws, "system.pong", {
|
|
868
|
+
"module_id": self.module_name,
|
|
869
|
+
"ping_time": t1,
|
|
870
|
+
"pong_time": t2,
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
async def _handle_event_notification(self, msg: dict):
|
|
874
|
+
params = msg.get("params", {})
|
|
875
|
+
event_type = params.get("event", "")
|
|
876
|
+
data = params.get("data", {})
|
|
877
|
+
|
|
878
|
+
# Handle system.ping event
|
|
879
|
+
if event_type == "system.ping":
|
|
880
|
+
await self._handle_ping_event(data)
|
|
881
|
+
return
|
|
882
|
+
|
|
883
|
+
# 弹性连接 offer/release
|
|
884
|
+
if event_type == "system.connection.offer":
|
|
885
|
+
asyncio.create_task(self._handle_connection_offer(data))
|
|
886
|
+
return
|
|
887
|
+
if event_type == "system.connection.release":
|
|
888
|
+
asyncio.create_task(self._handle_connection_release(data))
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
print(f"Event received: {event_type}, data: {data}")
|
|
892
|
+
|
|
893
|
+
if event_type == "module.shutdown":
|
|
894
|
+
target = data.get("module_id", "")
|
|
895
|
+
reason = data.get("reason", "")
|
|
896
|
+
if target == self.module_name or not target or reason == "launcher_lost":
|
|
897
|
+
await self._handle_shutdown()
|
|
898
|
+
return
|
|
899
|
+
|
|
900
|
+
# Module status events are now handled by Kernel event system
|
|
901
|
+
# Clients subscribe via /ws/relay
|
|
902
|
+
if event_type in (
|
|
903
|
+
"module.started", "module.stopped", "module.crashed",
|
|
904
|
+
"module.ready", "module.exiting",
|
|
905
|
+
"module.shutdown.ack", "module.shutdown.ready",
|
|
906
|
+
):
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
if event_type in SYSTEM_BROADCAST_EVENTS:
|
|
910
|
+
return
|
|
911
|
+
|
|
912
|
+
if os.environ.get("KITE_ENV") == "development":
|
|
913
|
+
print(f"Debug: Unhandled event: {event_type}")
|
|
914
|
+
|
|
915
|
+
async def _handle_rpc_request(self, ws, msg: dict):
|
|
916
|
+
rpc_id = msg.get("id", "")
|
|
917
|
+
method = msg.get("method", "")
|
|
918
|
+
params = msg.get("params", {})
|
|
919
|
+
|
|
920
|
+
if method.startswith("console."):
|
|
921
|
+
method = method[8:]
|
|
922
|
+
|
|
923
|
+
handlers = {
|
|
924
|
+
"health": lambda p: self._rpc_health(),
|
|
925
|
+
"status": lambda p: self._rpc_status(),
|
|
926
|
+
"list_tokens": lambda p: self._rpc_list_tokens(),
|
|
927
|
+
"list_kite_tokens": lambda p: self._rpc_list_kite_tokens(),
|
|
928
|
+
"list_evol_tokens": lambda p: self._rpc_list_evol_tokens(),
|
|
929
|
+
"revoke_token": lambda p: self._rpc_revoke_token(p),
|
|
930
|
+
"revoke_evol_token": lambda p: self._rpc_revoke_evol_token(p),
|
|
931
|
+
"subscribe_events": lambda p: self._rpc_subscribe_events(p),
|
|
932
|
+
"get_user_info": lambda p: self._rpc_get_user_info(p),
|
|
933
|
+
"get_credits_stats": lambda p: self._rpc_get_credits_stats(p),
|
|
934
|
+
"logout": lambda p: self._rpc_logout(p),
|
|
935
|
+
"llm_sessions_list": lambda p: self._rpc_llm_sessions_list(p),
|
|
936
|
+
"llm_sessions_get": lambda p: self._rpc_llm_sessions_get(p),
|
|
937
|
+
"llm_sessions_save": lambda p: self._rpc_llm_sessions_save(p),
|
|
938
|
+
"llm_sessions_delete": lambda p: self._rpc_llm_sessions_delete(p),
|
|
939
|
+
"llm_sessions_sync": lambda p: self._rpc_llm_sessions_sync(p),
|
|
940
|
+
"get_system_context": lambda p: self._rpc_get_system_context(p),
|
|
941
|
+
"chat": lambda p: self._rpc_chat(p),
|
|
942
|
+
"file_read": lambda p: self._rpc_file_read(p),
|
|
943
|
+
"llm_config_get": lambda p: self._rpc_llm_config_get(p),
|
|
944
|
+
"llm_config_save": lambda p: self._rpc_llm_config_save(p),
|
|
945
|
+
}
|
|
946
|
+
handler = handlers.get(method)
|
|
947
|
+
if handler:
|
|
948
|
+
try:
|
|
949
|
+
result = await handler(params)
|
|
950
|
+
await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
|
|
951
|
+
except Exception as e:
|
|
952
|
+
await ws.send(json.dumps({
|
|
953
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
954
|
+
"error": {"code": -32603, "message": str(e)},
|
|
955
|
+
}))
|
|
956
|
+
else:
|
|
957
|
+
await ws.send(json.dumps({
|
|
958
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
959
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
960
|
+
}))
|
|
961
|
+
|
|
962
|
+
async def _rpc_health(self) -> dict:
|
|
963
|
+
return {
|
|
964
|
+
"status": "healthy",
|
|
965
|
+
"details": {
|
|
966
|
+
"uptime_seconds": round(time.time() - self._start_time),
|
|
967
|
+
},
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async def _rpc_status(self) -> dict:
|
|
971
|
+
return {
|
|
972
|
+
"module": self.module_name,
|
|
973
|
+
"status": "running",
|
|
974
|
+
"uptime_seconds": round(time.time() - self._start_time),
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async def _rpc_list_tokens(self) -> dict:
|
|
978
|
+
"""列出所有 Kite Token(从 AuthManager 读取)"""
|
|
979
|
+
latest_tokens = self.auth_manager._get_latest_tokens()
|
|
980
|
+
now = time.time()
|
|
981
|
+
|
|
982
|
+
tokens = []
|
|
983
|
+
for token, info in latest_tokens.items():
|
|
984
|
+
# 只返回有效且未过期的 token
|
|
985
|
+
if info.get("isValid", True) and now <= info.get("expiresAt", 0):
|
|
986
|
+
tokens.append({
|
|
987
|
+
"token": token,
|
|
988
|
+
"deviceId": info.get("deviceId", "unknown"),
|
|
989
|
+
"deviceName": info.get("deviceName", "Unknown Device"),
|
|
990
|
+
"phone": info.get("phone"), # 添加绑定的手机号
|
|
991
|
+
"createdAt": info.get("createdAt_human", ""),
|
|
992
|
+
"lastUsedAt": info.get("lastUsedAt_human", ""),
|
|
993
|
+
"expiresAt": info.get("expiresAt_human", ""),
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
return {"tokens": tokens}
|
|
997
|
+
|
|
998
|
+
async def _rpc_list_kite_tokens(self) -> dict:
|
|
999
|
+
"""列出所有 Kite Token(前端配对令牌)"""
|
|
1000
|
+
return await self._rpc_list_tokens()
|
|
1001
|
+
|
|
1002
|
+
async def _rpc_list_evol_tokens(self) -> dict:
|
|
1003
|
+
"""列出所有 Evol Token(Evol 云端令牌)"""
|
|
1004
|
+
evol_records = self.auth_manager.list_all_evol_tokens()
|
|
1005
|
+
|
|
1006
|
+
if not evol_records:
|
|
1007
|
+
return {"tokens": []}
|
|
1008
|
+
|
|
1009
|
+
tokens = []
|
|
1010
|
+
for evol_record in evol_records:
|
|
1011
|
+
# 提取用户信息
|
|
1012
|
+
user_info = evol_record.get("userInfo", {})
|
|
1013
|
+
account_info = evol_record.get("accountInfo", {})
|
|
1014
|
+
|
|
1015
|
+
tokens.append({
|
|
1016
|
+
"token": evol_record.get("token", ""),
|
|
1017
|
+
"phone": evol_record.get("phone", ""),
|
|
1018
|
+
"nickName": user_info.get("nickName", ""),
|
|
1019
|
+
"userName": user_info.get("userName", ""),
|
|
1020
|
+
"credits": account_info.get("credits", 0),
|
|
1021
|
+
"creditsLimit": account_info.get("creditsLimit", 0),
|
|
1022
|
+
"vipType": account_info.get("vipType", 0),
|
|
1023
|
+
"vipTypeName": account_info.get("vipTypeName", "Unknown"),
|
|
1024
|
+
"vipExpireTime": account_info.get("vipExpireTime", ""),
|
|
1025
|
+
"vipRemainingDays": account_info.get("vipRemainingDays", 0),
|
|
1026
|
+
"obtainedAt": evol_record.get("obtainedAt_human", ""),
|
|
1027
|
+
"lastUsedAt": evol_record.get("lastUsedAt_human", ""),
|
|
1028
|
+
"expiresAt": evol_record.get("expiresAt_human", ""),
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
return {"tokens": tokens}
|
|
1032
|
+
|
|
1033
|
+
async def _rpc_revoke_token(self, params: dict) -> dict:
|
|
1034
|
+
"""吊销 Kite Token(使用 AuthManager)"""
|
|
1035
|
+
token = params.get("token")
|
|
1036
|
+
if not token:
|
|
1037
|
+
raise ValueError("Missing token parameter")
|
|
1038
|
+
|
|
1039
|
+
# 验证 token 是否存在
|
|
1040
|
+
latest_tokens = self.auth_manager._get_latest_tokens()
|
|
1041
|
+
if token not in latest_tokens:
|
|
1042
|
+
raise ValueError("Token not found")
|
|
1043
|
+
|
|
1044
|
+
# 吊销 token
|
|
1045
|
+
self.auth_manager.revoke_kite_token(token)
|
|
1046
|
+
|
|
1047
|
+
return {"success": True, "message": "Token revoked successfully"}
|
|
1048
|
+
|
|
1049
|
+
async def _rpc_revoke_evol_token(self, params: dict) -> dict:
|
|
1050
|
+
"""吊销 Evol Token(使用 AuthManager)"""
|
|
1051
|
+
token = params.get("token")
|
|
1052
|
+
if not token:
|
|
1053
|
+
raise ValueError("Missing token parameter")
|
|
1054
|
+
|
|
1055
|
+
# 吊销 token
|
|
1056
|
+
self.auth_manager.revoke_evol_token_by_token(token)
|
|
1057
|
+
|
|
1058
|
+
return {"success": True, "message": "Evol Token revoked successfully"}
|
|
1059
|
+
|
|
1060
|
+
async def _rpc_get_user_info(self, params: dict) -> dict:
|
|
1061
|
+
"""获取用户信息(RPC 版本)"""
|
|
1062
|
+
kite_token = params.get("kiteToken", "")
|
|
1063
|
+
|
|
1064
|
+
if not self.auth_manager.verify_kite_token(kite_token):
|
|
1065
|
+
return {
|
|
1066
|
+
"success": False,
|
|
1067
|
+
"msg": "Kite Token 无效或已过期",
|
|
1068
|
+
"code": "INVALID_TOKEN"
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
# 根据 Kite Token 获取绑定的手机号
|
|
1072
|
+
phone = self.auth_manager.get_phone_by_kite_token(kite_token)
|
|
1073
|
+
if not phone:
|
|
1074
|
+
return {
|
|
1075
|
+
"success": False,
|
|
1076
|
+
"msg": "未登录 Evol,请先登录",
|
|
1077
|
+
"code": "NOT_LOGGED_IN"
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
# 根据手机号获取 Evol Token
|
|
1081
|
+
evol_record = self.auth_manager.get_evol_token(phone)
|
|
1082
|
+
if not evol_record:
|
|
1083
|
+
return {
|
|
1084
|
+
"success": False,
|
|
1085
|
+
"msg": "Evol Token 已过期,请重新登录",
|
|
1086
|
+
"code": "EVOL_TOKEN_EXPIRED"
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
# 更新 Evol Token 使用时间
|
|
1090
|
+
self.auth_manager.update_evol_token_usage(phone)
|
|
1091
|
+
|
|
1092
|
+
evol_token = evol_record["token"]
|
|
1093
|
+
|
|
1094
|
+
# 检查缓存
|
|
1095
|
+
now = time.time()
|
|
1096
|
+
if evol_token in self._user_info_cache:
|
|
1097
|
+
cached = self._user_info_cache[evol_token]
|
|
1098
|
+
if now - cached["timestamp"] < self._cache_ttl:
|
|
1099
|
+
logger.info(f"Using cached user info (age: {now - cached['timestamp']:.1f}s)")
|
|
1100
|
+
return cached["data"]
|
|
1101
|
+
|
|
1102
|
+
# 缓存未命中或已过期,从云端获取
|
|
1103
|
+
result = await self.evol_api.get_user_info(evol_token)
|
|
1104
|
+
if not result.get("success"):
|
|
1105
|
+
return result
|
|
1106
|
+
|
|
1107
|
+
# 手动触发账户信息采集
|
|
1108
|
+
await self.stats_manager.collect_manual()
|
|
1109
|
+
|
|
1110
|
+
# 统一返回格式
|
|
1111
|
+
user_data = result["data"]
|
|
1112
|
+
response_data = {
|
|
1113
|
+
"success": True,
|
|
1114
|
+
"data": user_data
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
# 更新缓存
|
|
1118
|
+
self._user_info_cache[evol_token] = {
|
|
1119
|
+
"data": response_data,
|
|
1120
|
+
"timestamp": now
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return response_data
|
|
1124
|
+
|
|
1125
|
+
async def _rpc_get_credits_stats(self, params: dict) -> dict:
|
|
1126
|
+
"""获取积分统计(RPC 版本)"""
|
|
1127
|
+
kite_token = params.get("kiteToken", "")
|
|
1128
|
+
period = params.get("period", "day")
|
|
1129
|
+
date = params.get("date")
|
|
1130
|
+
|
|
1131
|
+
if not self.auth_manager.verify_kite_token(kite_token):
|
|
1132
|
+
return {
|
|
1133
|
+
"success": False,
|
|
1134
|
+
"msg": "Kite Token 无效或已过期",
|
|
1135
|
+
"code": "INVALID_TOKEN"
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
result = self.stats_manager.get_stats(period, date)
|
|
1139
|
+
return result
|
|
1140
|
+
|
|
1141
|
+
async def _rpc_logout(self, params: dict) -> dict:
|
|
1142
|
+
"""退出登录(RPC 版本)"""
|
|
1143
|
+
kite_token = params.get("kiteToken", "")
|
|
1144
|
+
|
|
1145
|
+
if not self.auth_manager.verify_kite_token(kite_token):
|
|
1146
|
+
return {"success": False, "msg": "Kite Token 无效"}
|
|
1147
|
+
|
|
1148
|
+
# 获取绑定的手机号
|
|
1149
|
+
phone = self.auth_manager.get_phone_by_kite_token(kite_token)
|
|
1150
|
+
|
|
1151
|
+
# 吊销 Evol Token(如果已绑定手机号)
|
|
1152
|
+
if phone:
|
|
1153
|
+
self.auth_manager.revoke_evol_token(phone)
|
|
1154
|
+
|
|
1155
|
+
# 吊销 Kite Token(解除绑定)
|
|
1156
|
+
self.auth_manager.revoke_kite_token(kite_token)
|
|
1157
|
+
|
|
1158
|
+
return {"success": True, "msg": "已退出登录"}
|
|
1159
|
+
|
|
1160
|
+
async def _rpc_subscribe_events(self, params: dict) -> dict:
|
|
1161
|
+
"""动态订阅事件(通过 Kernel)"""
|
|
1162
|
+
events = params.get("events", [])
|
|
1163
|
+
if not events:
|
|
1164
|
+
raise ValueError("Missing events parameter")
|
|
1165
|
+
|
|
1166
|
+
if not self._ws:
|
|
1167
|
+
raise RuntimeError("Not connected to Kernel")
|
|
1168
|
+
|
|
1169
|
+
# 调用 Kernel 的 event.subscribe
|
|
1170
|
+
await self._rpc_call(self._ws, "event.subscribe", {"events": events}, wait_response=False)
|
|
1171
|
+
|
|
1172
|
+
print(f"Subscribed to events: {events}")
|
|
1173
|
+
return {"success": True, "events": events}
|
|
1174
|
+
|
|
1175
|
+
# ---- LLM Sessions RPC ----
|
|
1176
|
+
|
|
1177
|
+
def _get_phone_from_params(self, params: dict) -> str:
|
|
1178
|
+
"""从 params 中获取手机号,优先用 kiteToken 解析"""
|
|
1179
|
+
kite_token = params.get("kiteToken", "")
|
|
1180
|
+
if kite_token:
|
|
1181
|
+
phone = self.auth_manager.get_phone_by_kite_token(kite_token)
|
|
1182
|
+
if phone:
|
|
1183
|
+
return phone
|
|
1184
|
+
phone = params.get("phone", "")
|
|
1185
|
+
if not phone:
|
|
1186
|
+
raise ValueError("Missing phone or kiteToken")
|
|
1187
|
+
return phone
|
|
1188
|
+
|
|
1189
|
+
def _sessions_dir(self, phone: str) -> Path:
|
|
1190
|
+
"""获取用户会话存储目录"""
|
|
1191
|
+
data_dir = os.environ.get("KITE_DATA", os.path.expanduser("~/.kite/data"))
|
|
1192
|
+
d = Path(data_dir) / "llm_sessions" / phone
|
|
1193
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
1194
|
+
return d
|
|
1195
|
+
|
|
1196
|
+
def _read_sessions_index(self, phone: str) -> list:
|
|
1197
|
+
"""读取会话索引"""
|
|
1198
|
+
f = self._sessions_dir(phone) / "sessions.json"
|
|
1199
|
+
if f.exists():
|
|
1200
|
+
try:
|
|
1201
|
+
return json.loads(f.read_text(encoding="utf-8"))
|
|
1202
|
+
except Exception:
|
|
1203
|
+
return []
|
|
1204
|
+
return []
|
|
1205
|
+
|
|
1206
|
+
def _write_sessions_index(self, phone: str, sessions: list):
|
|
1207
|
+
"""写入会话索引"""
|
|
1208
|
+
f = self._sessions_dir(phone) / "sessions.json"
|
|
1209
|
+
f.write_text(json.dumps(sessions, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1210
|
+
|
|
1211
|
+
async def _rpc_llm_sessions_list(self, params: dict) -> dict:
|
|
1212
|
+
"""列出用户的所有会话(仅元数据)"""
|
|
1213
|
+
phone = self._get_phone_from_params(params)
|
|
1214
|
+
sessions = self._read_sessions_index(phone)
|
|
1215
|
+
return {"success": True, "data": sessions}
|
|
1216
|
+
|
|
1217
|
+
async def _rpc_llm_sessions_get(self, params: dict) -> dict:
|
|
1218
|
+
"""获取单个会话的完整消息"""
|
|
1219
|
+
phone = self._get_phone_from_params(params)
|
|
1220
|
+
session_id = params.get("session_id", "")
|
|
1221
|
+
if not session_id:
|
|
1222
|
+
raise ValueError("Missing session_id")
|
|
1223
|
+
|
|
1224
|
+
f = self._sessions_dir(phone) / f"{session_id}.json"
|
|
1225
|
+
if not f.exists():
|
|
1226
|
+
return {"success": False, "error": "Session not found"}
|
|
1227
|
+
|
|
1228
|
+
try:
|
|
1229
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
1230
|
+
return {"success": True, "data": data}
|
|
1231
|
+
except Exception as e:
|
|
1232
|
+
return {"success": False, "error": str(e)}
|
|
1233
|
+
|
|
1234
|
+
async def _rpc_llm_sessions_save(self, params: dict) -> dict:
|
|
1235
|
+
"""保存会话(创建或更新)"""
|
|
1236
|
+
phone = self._get_phone_from_params(params)
|
|
1237
|
+
session = params.get("session")
|
|
1238
|
+
if not session or not session.get("id"):
|
|
1239
|
+
raise ValueError("Missing session or session.id")
|
|
1240
|
+
|
|
1241
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
1242
|
+
session["updated_at"] = now
|
|
1243
|
+
if not session.get("created_at"):
|
|
1244
|
+
session["created_at"] = now
|
|
1245
|
+
|
|
1246
|
+
# 写入会话文件
|
|
1247
|
+
d = self._sessions_dir(phone)
|
|
1248
|
+
session_file = d / f"{session['id']}.json"
|
|
1249
|
+
session_file.write_text(json.dumps(session, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1250
|
+
|
|
1251
|
+
# 更新索引
|
|
1252
|
+
sessions = self._read_sessions_index(phone)
|
|
1253
|
+
meta = {
|
|
1254
|
+
"id": session["id"],
|
|
1255
|
+
"name": session.get("name", ""),
|
|
1256
|
+
"created_at": session.get("created_at", now),
|
|
1257
|
+
"updated_at": session["updated_at"],
|
|
1258
|
+
"message_count": len(session.get("messages", []))
|
|
1259
|
+
}
|
|
1260
|
+
idx = next((i for i, s in enumerate(sessions) if s["id"] == session["id"]), None)
|
|
1261
|
+
if idx is not None:
|
|
1262
|
+
sessions[idx] = meta
|
|
1263
|
+
else:
|
|
1264
|
+
sessions.insert(0, meta)
|
|
1265
|
+
self._write_sessions_index(phone, sessions)
|
|
1266
|
+
|
|
1267
|
+
return {"success": True, "data": meta}
|
|
1268
|
+
|
|
1269
|
+
async def _rpc_llm_sessions_delete(self, params: dict) -> dict:
|
|
1270
|
+
"""删除会话"""
|
|
1271
|
+
phone = self._get_phone_from_params(params)
|
|
1272
|
+
session_id = params.get("session_id", "")
|
|
1273
|
+
if not session_id:
|
|
1274
|
+
raise ValueError("Missing session_id")
|
|
1275
|
+
|
|
1276
|
+
d = self._sessions_dir(phone)
|
|
1277
|
+
session_file = d / f"{session_id}.json"
|
|
1278
|
+
if session_file.exists():
|
|
1279
|
+
session_file.unlink()
|
|
1280
|
+
|
|
1281
|
+
sessions = self._read_sessions_index(phone)
|
|
1282
|
+
sessions = [s for s in sessions if s["id"] != session_id]
|
|
1283
|
+
self._write_sessions_index(phone, sessions)
|
|
1284
|
+
|
|
1285
|
+
return {"success": True}
|
|
1286
|
+
|
|
1287
|
+
async def _rpc_llm_sessions_sync(self, params: dict) -> dict:
|
|
1288
|
+
"""同步会话 — 比较前后端时间戳"""
|
|
1289
|
+
phone = self._get_phone_from_params(params)
|
|
1290
|
+
local_sessions = params.get("sessions", [])
|
|
1291
|
+
|
|
1292
|
+
server_sessions = self._read_sessions_index(phone)
|
|
1293
|
+
server_map = {s["id"]: s for s in server_sessions}
|
|
1294
|
+
local_map = {s["id"]: s for s in local_sessions}
|
|
1295
|
+
|
|
1296
|
+
to_upload = []
|
|
1297
|
+
to_download = []
|
|
1298
|
+
|
|
1299
|
+
for loc in local_sessions:
|
|
1300
|
+
srv = server_map.get(loc["id"])
|
|
1301
|
+
if not srv or loc.get("updated_at", "") > srv.get("updated_at", ""):
|
|
1302
|
+
to_upload.append(loc["id"])
|
|
1303
|
+
|
|
1304
|
+
for srv in server_sessions:
|
|
1305
|
+
loc = local_map.get(srv["id"])
|
|
1306
|
+
if not loc or srv.get("updated_at", "") > loc.get("updated_at", ""):
|
|
1307
|
+
to_download.append(srv["id"])
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
"success": True,
|
|
1311
|
+
"data": {
|
|
1312
|
+
"to_upload": to_upload,
|
|
1313
|
+
"to_download": to_download,
|
|
1314
|
+
"server_sessions": server_sessions
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async def _rpc_get_system_context(self, params: dict) -> dict:
|
|
1319
|
+
"""获取系统上下文(CLAUDE.md + module.md + 注册中心)"""
|
|
1320
|
+
context = {
|
|
1321
|
+
"claude_md": {"content": "", "bytes": 0},
|
|
1322
|
+
"modules": [],
|
|
1323
|
+
"registry": {"content": "", "bytes": 0, "tools_count": 0},
|
|
1324
|
+
"total_bytes": 0
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
# 1. 读取 CLAUDE.md
|
|
1328
|
+
root_dir = Path(os.environ.get("KITE_ROOT_DIR", "."))
|
|
1329
|
+
claude_md_path = root_dir / "CLAUDE.md"
|
|
1330
|
+
if claude_md_path.exists():
|
|
1331
|
+
try:
|
|
1332
|
+
content = claude_md_path.read_text(encoding="utf-8")
|
|
1333
|
+
context["claude_md"]["content"] = content
|
|
1334
|
+
context["claude_md"]["bytes"] = len(content.encode("utf-8"))
|
|
1335
|
+
except Exception as e:
|
|
1336
|
+
logger.warning(f"Failed to read CLAUDE.md: {e}")
|
|
1337
|
+
|
|
1338
|
+
# 2. 获取注册中心信息(registry.lookup 无参数 = 列出所有在线模块)
|
|
1339
|
+
try:
|
|
1340
|
+
lookup_resp = await self._rpc_call(self._ws, "registry.lookup", {})
|
|
1341
|
+
lookup_result = lookup_resp.get("result", {})
|
|
1342
|
+
module_ids = [r["module"] for r in lookup_result.get("results", [])]
|
|
1343
|
+
|
|
1344
|
+
modules_data = []
|
|
1345
|
+
tools_data = []
|
|
1346
|
+
for mid in module_ids:
|
|
1347
|
+
try:
|
|
1348
|
+
get_resp = await self._rpc_call(self._ws, "registry.get", {"path": mid})
|
|
1349
|
+
get_result = get_resp.get("result", {})
|
|
1350
|
+
mod_data = get_result.get("value", {})
|
|
1351
|
+
modules_data.append({
|
|
1352
|
+
"module_id": mid,
|
|
1353
|
+
"module_type": mod_data.get("module_type", "unknown"),
|
|
1354
|
+
"status": mod_data.get("status", "unknown"),
|
|
1355
|
+
})
|
|
1356
|
+
mod_tools = mod_data.get("tools", {})
|
|
1357
|
+
if mod_tools:
|
|
1358
|
+
tools_data.append({"module_id": mid, "tools": mod_tools})
|
|
1359
|
+
except Exception:
|
|
1360
|
+
modules_data.append({"module_id": mid, "status": "unknown"})
|
|
1361
|
+
|
|
1362
|
+
registry_content = json.dumps({"modules": modules_data, "tools": tools_data}, ensure_ascii=False, indent=2)
|
|
1363
|
+
context["registry"]["content"] = registry_content
|
|
1364
|
+
context["registry"]["bytes"] = len(registry_content.encode("utf-8"))
|
|
1365
|
+
context["registry"]["tools_count"] = len(tools_data)
|
|
1366
|
+
|
|
1367
|
+
# 3. 读取每个模块的 module.md(搜索多个可能的路径)
|
|
1368
|
+
for mid in module_ids:
|
|
1369
|
+
candidates = [
|
|
1370
|
+
root_dir / mid / "module.md",
|
|
1371
|
+
root_dir / "extensions" / "services" / mid / "module.md",
|
|
1372
|
+
root_dir / "extensions" / "agents" / mid / "module.md",
|
|
1373
|
+
root_dir / "extensions" / "channels" / mid / "module.md",
|
|
1374
|
+
root_dir / "extensions" / mid / "module.md",
|
|
1375
|
+
]
|
|
1376
|
+
for md_path in candidates:
|
|
1377
|
+
if md_path.exists():
|
|
1378
|
+
try:
|
|
1379
|
+
content = md_path.read_text(encoding="utf-8")
|
|
1380
|
+
context["modules"].append({
|
|
1381
|
+
"module_id": mid,
|
|
1382
|
+
"content": content,
|
|
1383
|
+
"bytes": len(content.encode("utf-8"))
|
|
1384
|
+
})
|
|
1385
|
+
except Exception as e:
|
|
1386
|
+
logger.warning(f"Failed to read {mid}/module.md: {e}")
|
|
1387
|
+
break
|
|
1388
|
+
except Exception as e:
|
|
1389
|
+
logger.warning(f"Failed to get registry info: {e}")
|
|
1390
|
+
|
|
1391
|
+
# 计算总字节数
|
|
1392
|
+
context["total_bytes"] = (
|
|
1393
|
+
context["claude_md"]["bytes"] +
|
|
1394
|
+
context["registry"]["bytes"] +
|
|
1395
|
+
sum(m["bytes"] for m in context["modules"])
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
return {"success": True, "data": context}
|
|
1399
|
+
|
|
1400
|
+
async def _rpc_llm_config_get(self, params: dict) -> dict:
|
|
1401
|
+
"""获取 LLM 测试配置"""
|
|
1402
|
+
phone = self._get_phone_from_params(params)
|
|
1403
|
+
config_path = self._sessions_dir(phone) / "config.json"
|
|
1404
|
+
|
|
1405
|
+
if config_path.exists():
|
|
1406
|
+
try:
|
|
1407
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1408
|
+
return {"success": True, "data": data}
|
|
1409
|
+
except Exception as e:
|
|
1410
|
+
logger.warning(f"Failed to read LLM config: {e}")
|
|
1411
|
+
|
|
1412
|
+
return {"success": True, "data": {}}
|
|
1413
|
+
|
|
1414
|
+
async def _rpc_llm_config_save(self, params: dict) -> dict:
|
|
1415
|
+
"""保存 LLM 测试配置"""
|
|
1416
|
+
phone = self._get_phone_from_params(params)
|
|
1417
|
+
config = params.get("config", {})
|
|
1418
|
+
|
|
1419
|
+
sessions_dir = self._sessions_dir(phone)
|
|
1420
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
1421
|
+
|
|
1422
|
+
config_path = sessions_dir / "config.json"
|
|
1423
|
+
config_path.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1424
|
+
|
|
1425
|
+
return {"success": True}
|
|
1426
|
+
|
|
1427
|
+
async def _rpc_chat(self, params: dict) -> dict:
|
|
1428
|
+
"""LLM 聊天 — 异步事件机制,RPC 立即返回 request_id"""
|
|
1429
|
+
request_id = str(uuid.uuid4())[:8]
|
|
1430
|
+
|
|
1431
|
+
messages = params.get("messages", [])
|
|
1432
|
+
model = params.get("model", "gpt-3.5-turbo")
|
|
1433
|
+
base_url = params.get("base_url", "https://api.openai.com/v1")
|
|
1434
|
+
api_key = params.get("api_key", "")
|
|
1435
|
+
temperature = params.get("temperature", 0.7)
|
|
1436
|
+
max_tokens = params.get("max_tokens", 1024)
|
|
1437
|
+
tools = params.get("tools")
|
|
1438
|
+
|
|
1439
|
+
logger.info(f"[LLM Chat] 收到请求 {request_id}: model={model}, messages={len(messages)}, tools={len(tools) if tools else 0}")
|
|
1440
|
+
|
|
1441
|
+
if not api_key:
|
|
1442
|
+
return {"success": False, "error": "API key is required"}
|
|
1443
|
+
|
|
1444
|
+
# 启动后台任务处理 LLM 调用
|
|
1445
|
+
asyncio.create_task(self._chat_worker(
|
|
1446
|
+
request_id, messages, model, base_url, api_key,
|
|
1447
|
+
temperature, max_tokens, tools
|
|
1448
|
+
))
|
|
1449
|
+
|
|
1450
|
+
return {"success": True, "data": {"request_id": request_id}}
|
|
1451
|
+
|
|
1452
|
+
async def _chat_worker(self, request_id: str, messages: list, model: str,
|
|
1453
|
+
base_url: str, api_key: str, temperature: float,
|
|
1454
|
+
max_tokens: int, tools: list | None):
|
|
1455
|
+
"""后台执行 LLM 调用,通过事件推送结果"""
|
|
1456
|
+
import httpx
|
|
1457
|
+
|
|
1458
|
+
# 发送 status=processing 事件
|
|
1459
|
+
await self._publish_chat_event(request_id, "processing", {"message": "正在调用模型..."})
|
|
1460
|
+
|
|
1461
|
+
# 启动心跳:每 5 秒发一次 processing 事件
|
|
1462
|
+
heartbeat_running = True
|
|
1463
|
+
|
|
1464
|
+
async def heartbeat():
|
|
1465
|
+
elapsed = 0
|
|
1466
|
+
while heartbeat_running:
|
|
1467
|
+
await asyncio.sleep(5)
|
|
1468
|
+
elapsed += 5
|
|
1469
|
+
if heartbeat_running:
|
|
1470
|
+
await self._publish_chat_event(request_id, "processing", {
|
|
1471
|
+
"message": f"等待模型响应... ({elapsed}s)"
|
|
1472
|
+
})
|
|
1473
|
+
|
|
1474
|
+
heartbeat_task = asyncio.create_task(heartbeat())
|
|
1475
|
+
|
|
1476
|
+
payload = {
|
|
1477
|
+
"model": model,
|
|
1478
|
+
"messages": messages,
|
|
1479
|
+
"temperature": temperature,
|
|
1480
|
+
"max_tokens": max_tokens
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if tools:
|
|
1484
|
+
payload["tools"] = tools
|
|
1485
|
+
|
|
1486
|
+
# 保存上下文到文件
|
|
1487
|
+
debug_dir = Path("data/llm_debug")
|
|
1488
|
+
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
1489
|
+
debug_file = debug_dir / f"{request_id}.json"
|
|
1490
|
+
try:
|
|
1491
|
+
debug_file.write_text(json.dumps({
|
|
1492
|
+
"request_id": request_id,
|
|
1493
|
+
"timestamp": time.time(),
|
|
1494
|
+
"model": model,
|
|
1495
|
+
"base_url": base_url,
|
|
1496
|
+
"payload": payload
|
|
1497
|
+
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1498
|
+
logger.info(f"[LLM Chat] {request_id} 上下文已保存: {debug_file}")
|
|
1499
|
+
except Exception as e:
|
|
1500
|
+
logger.warning(f"[LLM Chat] {request_id} 保存上下文失败: {e}")
|
|
1501
|
+
|
|
1502
|
+
try:
|
|
1503
|
+
url_base = base_url.rstrip('/')
|
|
1504
|
+
if not url_base.endswith('/v1'):
|
|
1505
|
+
url_base += '/v1'
|
|
1506
|
+
api_url = f"{url_base}/chat/completions"
|
|
1507
|
+
logger.info(f"[LLM Chat] {request_id} 调用 API: {api_url}")
|
|
1508
|
+
|
|
1509
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
1510
|
+
response = await client.post(
|
|
1511
|
+
api_url,
|
|
1512
|
+
json=payload,
|
|
1513
|
+
headers={"Authorization": f"Bearer {api_key}"}
|
|
1514
|
+
)
|
|
1515
|
+
response.raise_for_status()
|
|
1516
|
+
result = response.json()
|
|
1517
|
+
|
|
1518
|
+
has_tool_calls = bool(result["choices"][0]["message"].get("tool_calls"))
|
|
1519
|
+
logger.info(f"[LLM Chat] {request_id} 调用成功: has_tool_calls={has_tool_calls}, usage={result.get('usage')}")
|
|
1520
|
+
|
|
1521
|
+
# 发送 status=done 事件
|
|
1522
|
+
heartbeat_running = False
|
|
1523
|
+
heartbeat_task.cancel()
|
|
1524
|
+
await self._publish_chat_event(request_id, "done", {
|
|
1525
|
+
"content": result["choices"][0]["message"].get("content", ""),
|
|
1526
|
+
"tool_calls": result["choices"][0]["message"].get("tool_calls"),
|
|
1527
|
+
"usage": result.get("usage")
|
|
1528
|
+
})
|
|
1529
|
+
|
|
1530
|
+
except Exception as e:
|
|
1531
|
+
heartbeat_running = False
|
|
1532
|
+
heartbeat_task.cancel()
|
|
1533
|
+
logger.error(f"[LLM Chat] {request_id} 调用失败: {e}")
|
|
1534
|
+
await self._publish_chat_event(request_id, "error", {"error": str(e)})
|
|
1535
|
+
|
|
1536
|
+
async def _publish_chat_event(self, request_id: str, status: str, data: dict):
|
|
1537
|
+
"""发布 chat 事件"""
|
|
1538
|
+
if not self._ws:
|
|
1539
|
+
return
|
|
1540
|
+
await self._publish_event(self._ws, "kite_console.chat.chunk", {
|
|
1541
|
+
"request_id": request_id,
|
|
1542
|
+
"status": status,
|
|
1543
|
+
**data
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
async def _rpc_file_read(self, params: dict) -> dict:
|
|
1547
|
+
"""读取项目文件(路径限制在项目目录内)"""
|
|
1548
|
+
path = params.get("path", "")
|
|
1549
|
+
max_lines = params.get("lines", 200)
|
|
1550
|
+
if not path:
|
|
1551
|
+
return {"success": False, "error": "缺少 path 参数"}
|
|
1552
|
+
|
|
1553
|
+
root_dir = Path(os.environ.get("KITE_ROOT_DIR", ".")).resolve()
|
|
1554
|
+
target = (root_dir / path).resolve()
|
|
1555
|
+
if not str(target).startswith(str(root_dir)):
|
|
1556
|
+
return {"success": False, "error": "路径超出项目目录"}
|
|
1557
|
+
if not target.exists():
|
|
1558
|
+
return {"success": False, "error": f"文件不存在: {path}"}
|
|
1559
|
+
|
|
1560
|
+
try:
|
|
1561
|
+
all_lines = target.read_text(encoding="utf-8").splitlines()
|
|
1562
|
+
if max_lines and len(all_lines) > max_lines:
|
|
1563
|
+
content = "\n".join(all_lines[:max_lines]) + f"\n\n... (截断,共 {len(all_lines)} 行)"
|
|
1564
|
+
else:
|
|
1565
|
+
content = "\n".join(all_lines)
|
|
1566
|
+
return {"success": True, "data": {"content": content, "total_lines": len(all_lines)}}
|
|
1567
|
+
except Exception as e:
|
|
1568
|
+
return {"success": False, "error": f"读取失败: {e}"}
|
|
1569
|
+
|
|
1570
|
+
async def _handle_connection_offer(self, data):
|
|
1571
|
+
"""处理 Kernel 下发的 slot token,建立附加连接。"""
|
|
1572
|
+
slots = data.get("slots", {})
|
|
1573
|
+
for slot_str, info in slots.items():
|
|
1574
|
+
slot = int(slot_str)
|
|
1575
|
+
token = info.get("token", "")
|
|
1576
|
+
if not token or slot in self._extra_ws:
|
|
1577
|
+
continue
|
|
1578
|
+
asyncio.create_task(self._connect_slot(slot, token))
|
|
1579
|
+
|
|
1580
|
+
async def _connect_slot(self, slot, token):
|
|
1581
|
+
"""建立单个 slot 附加连接。"""
|
|
1582
|
+
ws_url = f"ws://127.0.0.1:{self.kernel_port}/ws"
|
|
1583
|
+
try:
|
|
1584
|
+
ws = await websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=5)
|
|
1585
|
+
auth_req = {"jsonrpc": "2.0", "id": f"auth-slot-{slot}", "method": "auth", "params": {"token": token}}
|
|
1586
|
+
await ws.send(json.dumps(auth_req))
|
|
1587
|
+
resp = json.loads(await asyncio.wait_for(ws.recv(), timeout=5))
|
|
1588
|
+
if "error" in resp:
|
|
1589
|
+
await ws.close()
|
|
1590
|
+
return
|
|
1591
|
+
self._extra_ws[slot] = ws
|
|
1592
|
+
self._extra_ws_tasks[slot] = asyncio.create_task(self._slot_recv_loop(slot, ws))
|
|
1593
|
+
print(f"[kite_console] Slot {slot} connected")
|
|
1594
|
+
except Exception as e:
|
|
1595
|
+
print(f"[kite_console] Slot {slot} connect failed: {e}")
|
|
1596
|
+
|
|
1597
|
+
async def _slot_recv_loop(self, slot, ws):
|
|
1598
|
+
"""附加连接的接收循环:与主连接平等处理所有消息。"""
|
|
1599
|
+
try:
|
|
1600
|
+
async for raw in ws:
|
|
1601
|
+
try:
|
|
1602
|
+
msg = json.loads(raw)
|
|
1603
|
+
except (json.JSONDecodeError, TypeError):
|
|
1604
|
+
continue
|
|
1605
|
+
|
|
1606
|
+
try:
|
|
1607
|
+
has_method = "method" in msg
|
|
1608
|
+
has_id = "id" in msg
|
|
1609
|
+
has_result_or_error = "result" in msg or "error" in msg
|
|
1610
|
+
|
|
1611
|
+
if has_method and not has_id:
|
|
1612
|
+
asyncio.create_task(self._handle_event_notification(msg))
|
|
1613
|
+
elif has_method and has_id:
|
|
1614
|
+
asyncio.create_task(self._handle_rpc_request(ws, msg))
|
|
1615
|
+
elif has_id and has_result_or_error:
|
|
1616
|
+
self._handle_rpc_response(msg)
|
|
1617
|
+
except Exception as e:
|
|
1618
|
+
print(f"[kite_console] Slot {slot} 消息处理异常: {e}")
|
|
1619
|
+
except Exception as e:
|
|
1620
|
+
print(f"[kite_console] Slot {slot} 接收循环异常: {e}")
|
|
1621
|
+
finally:
|
|
1622
|
+
self._extra_ws.pop(slot, None)
|
|
1623
|
+
self._extra_ws_tasks.pop(slot, None)
|
|
1624
|
+
|
|
1625
|
+
async def _handle_connection_release(self, data):
|
|
1626
|
+
"""Kernel 请求释放 slot,优雅关闭。"""
|
|
1627
|
+
for slot in data.get("slots", []):
|
|
1628
|
+
ws = self._extra_ws.pop(slot, None)
|
|
1629
|
+
task = self._extra_ws_tasks.pop(slot, None)
|
|
1630
|
+
if ws:
|
|
1631
|
+
try:
|
|
1632
|
+
await ws.close(code=1000, reason="release")
|
|
1633
|
+
except Exception:
|
|
1634
|
+
pass
|
|
1635
|
+
if task:
|
|
1636
|
+
task.cancel()
|
|
1637
|
+
|
|
1638
|
+
async def _handle_shutdown(self):
|
|
1639
|
+
print(f"Received module.shutdown")
|
|
1640
|
+
self._shutting_down = True
|
|
1641
|
+
|
|
1642
|
+
# 使用短超时发送 shutdown 事件,避免阻塞
|
|
1643
|
+
try:
|
|
1644
|
+
if self._ws:
|
|
1645
|
+
await self._publish_event(self._ws, "module.shutdown.ack", {
|
|
1646
|
+
"module_id": self.module_name,
|
|
1647
|
+
})
|
|
1648
|
+
except Exception as e:
|
|
1649
|
+
print(f"\033[31mFailed to send shutdown.ack: {e}\033[0m")
|
|
1650
|
+
|
|
1651
|
+
try:
|
|
1652
|
+
if self._ws:
|
|
1653
|
+
await self._publish_event(self._ws, "module.exiting", {
|
|
1654
|
+
"module_id": self.module_name,
|
|
1655
|
+
"type": "passive",
|
|
1656
|
+
"reason": "shutdown_requested",
|
|
1657
|
+
"restart": "auto",
|
|
1658
|
+
"action": "none",
|
|
1659
|
+
"timeout": 3.0,
|
|
1660
|
+
"restart_delay": 0.0,
|
|
1661
|
+
})
|
|
1662
|
+
except Exception as e:
|
|
1663
|
+
print(f"\033[31mFailed to send module.exiting: {e}\033[0m")
|
|
1664
|
+
|
|
1665
|
+
# Send shutdown.ready(必须在关闭连接之前发送)
|
|
1666
|
+
try:
|
|
1667
|
+
if self._ws:
|
|
1668
|
+
await self._publish_event(self._ws, "module.shutdown.ready", {
|
|
1669
|
+
"module_id": self.module_name,
|
|
1670
|
+
})
|
|
1671
|
+
except Exception as e:
|
|
1672
|
+
print(f"\033[31mFailed to send shutdown.ready: {e}\033[0m")
|
|
1673
|
+
|
|
1674
|
+
# 等待 Kernel 处理 shutdown.ready
|
|
1675
|
+
await asyncio.sleep(0.01)
|
|
1676
|
+
|
|
1677
|
+
if self._test_task:
|
|
1678
|
+
self._test_task.cancel()
|
|
1679
|
+
|
|
1680
|
+
# Close WebSocket connections
|
|
1681
|
+
if hasattr(self.app.state, 'relay_service'):
|
|
1682
|
+
try:
|
|
1683
|
+
await asyncio.wait_for(
|
|
1684
|
+
self.app.state.relay_service.close_all_sessions(),
|
|
1685
|
+
timeout=1.0
|
|
1686
|
+
)
|
|
1687
|
+
except asyncio.TimeoutError:
|
|
1688
|
+
print(f"Relay service close timeout")
|
|
1689
|
+
except Exception as e:
|
|
1690
|
+
print(f"\033[31mFailed to close relay sessions: {e}\033[0m")
|
|
1691
|
+
|
|
1692
|
+
# 关闭所有附加连接
|
|
1693
|
+
for _s, _w in list(self._extra_ws.items()):
|
|
1694
|
+
try:
|
|
1695
|
+
await _w.close(code=1000, reason="shutdown")
|
|
1696
|
+
except Exception:
|
|
1697
|
+
pass
|
|
1698
|
+
for _t in self._extra_ws_tasks.values():
|
|
1699
|
+
_t.cancel()
|
|
1700
|
+
self._extra_ws.clear()
|
|
1701
|
+
self._extra_ws_tasks.clear()
|
|
1702
|
+
|
|
1703
|
+
# 关闭 Kernel WebSocket 连接
|
|
1704
|
+
# 先清理未完成的 RPC future
|
|
1705
|
+
for fut in self._pending_rpc.values():
|
|
1706
|
+
if not fut.done():
|
|
1707
|
+
fut.cancel()
|
|
1708
|
+
self._pending_rpc.clear()
|
|
1709
|
+
|
|
1710
|
+
if self._ws:
|
|
1711
|
+
try:
|
|
1712
|
+
await self._ws.close(code=1000, reason="Graceful shutdown")
|
|
1713
|
+
print(f"Kernel WebSocket closed")
|
|
1714
|
+
except Exception as e:
|
|
1715
|
+
print(f"\033[31mFailed to close Kernel WebSocket: {e}\033[0m")
|
|
1716
|
+
|
|
1717
|
+
# 触发 uvicorn 优雅关闭(让 uvicorn 自然退出,不要 sys.exit)
|
|
1718
|
+
if self._uvicorn_server:
|
|
1719
|
+
print(f"Triggering uvicorn graceful shutdown")
|
|
1720
|
+
self._uvicorn_server.should_exit = True
|
|
1721
|
+
else:
|
|
1722
|
+
print(f"\033[31mWarning: uvicorn_server not set\033[0m")
|
|
1723
|
+
|
|
1724
|
+
async def _publish_event(self, ws, event: str, data: dict = None):
|
|
1725
|
+
"""Publish event via event.publish RPC (fire-and-forget)."""
|
|
1726
|
+
await self._rpc_call(ws, "event.publish", {
|
|
1727
|
+
"event_id": str(uuid.uuid4()),
|
|
1728
|
+
"event": event,
|
|
1729
|
+
"data": data or {},
|
|
1730
|
+
}, wait_response=False)
|
|
1731
|
+
|
|
1732
|
+
async def _test_event_loop(self):
|
|
1733
|
+
try:
|
|
1734
|
+
while True:
|
|
1735
|
+
await asyncio.sleep(10)
|
|
1736
|
+
if self._ws:
|
|
1737
|
+
await self._publish_event(self._ws, "kite_console.test", {
|
|
1738
|
+
"message": "test event from kite_console",
|
|
1739
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1740
|
+
})
|
|
1741
|
+
except Exception:
|
|
1742
|
+
pass
|