@agentunion/kite 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/kite/checklists/feature-checklist.md +496 -0
- package/.claude/skills/kite/references/event-patterns.md +180 -0
- package/.claude/skills/kite/references/health-check.md +202 -0
- package/.claude/skills/kite/references/http-service.md +199 -0
- package/.claude/skills/kite/references/module-md-spec.md +172 -0
- package/.claude/skills/kite/references/multi-connection.md +147 -0
- package/.claude/skills/kite/references/rpc-patterns.md +199 -0
- package/.claude/skills/kite/references/shutdown-sequence.md +146 -0
- package/.claude/skills/kite/references/stdin-protocol.md +147 -0
- package/.claude/skills/kite/references/test-center-integration.md +178 -0
- package/.claude/skills/kite/references/ws-lifecycle.md +301 -0
- package/.claude/skills/kite/skill.md +272 -0
- package/.claude/skills/kite/templates/go/README.md +20 -0
- package/.claude/skills/kite/templates/node/entry.js +134 -0
- package/.claude/skills/kite/templates/node/module.md +16 -0
- package/.claude/skills/kite/templates/node/server.js +351 -0
- package/.claude/skills/kite/templates/node/server_http.js +90 -0
- package/.claude/skills/kite/templates/python/entry.py +425 -0
- package/.claude/skills/kite/templates/python/module.md +26 -0
- package/.claude/skills/kite/templates/python/server.py +447 -0
- package/.claude/skills/kite/templates/python/server_http.py +433 -0
- package/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/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 +151 -5
- package/kite_cli/utils/colors.py +153 -0
- package/kite_cli/utils/dependency_graph.py +209 -0
- package/kite_cli/utils/process.py +55 -0
- package/kite_cli/utils/progress.py +207 -0
- package/kite_cli/utils/table.py +101 -0
- package/launcher/count_lines.py +192 -43
- package/launcher/entry.py +4543 -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,1509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis entry point.
|
|
3
|
+
Reads boot_info from stdin, registers to Registry, starts Redis service.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import builtins
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import traceback
|
|
17
|
+
import uuid
|
|
18
|
+
import random
|
|
19
|
+
|
|
20
|
+
import websockets
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# System broadcast events (received by all modules, may not need handling)
|
|
24
|
+
SYSTEM_BROADCAST_EVENTS = {
|
|
25
|
+
"module.ready", "module.registered", "module.started", "module.stopped",
|
|
26
|
+
"module.crashed", "module.exiting", "module.offline",
|
|
27
|
+
"module.shutdown.ack", "module.shutdown.ready",
|
|
28
|
+
"system.ready", "registry.updated",
|
|
29
|
+
"system.instance.started", "system.instance.stopped",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Module configuration ──
|
|
37
|
+
|
|
38
|
+
def _load_module_config() -> dict:
|
|
39
|
+
"""Load module configuration from module.md frontmatter.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict with keys: name, preferred_port, advertise_ip
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
SystemExit: If module.md is invalid or name is non-compliant
|
|
46
|
+
"""
|
|
47
|
+
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
48
|
+
module_md = os.path.join(_this_dir, "module.md")
|
|
49
|
+
|
|
50
|
+
# Calculate relative path for error messages
|
|
51
|
+
project_root = os.environ.get("KITE_PROJECT", "")
|
|
52
|
+
if project_root and _this_dir.startswith(project_root):
|
|
53
|
+
rel_path = os.path.relpath(_this_dir, project_root)
|
|
54
|
+
else:
|
|
55
|
+
rel_path = _this_dir
|
|
56
|
+
|
|
57
|
+
# Default values (will be overridden if valid config exists)
|
|
58
|
+
result = {
|
|
59
|
+
"name": "",
|
|
60
|
+
"preferred_port": 0,
|
|
61
|
+
"advertise_ip": "0.0.0.0"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Check if module.md exists
|
|
65
|
+
if not os.path.exists(module_md):
|
|
66
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
67
|
+
print(f" Path: {rel_path}/module.md")
|
|
68
|
+
print(f" Reason: File not found")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with open(module_md, encoding="utf-8") as f:
|
|
73
|
+
text = f.read()
|
|
74
|
+
|
|
75
|
+
# Extract YAML frontmatter (between --- markers)
|
|
76
|
+
import re
|
|
77
|
+
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
78
|
+
if not m:
|
|
79
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
80
|
+
print(f" Path: {rel_path}/module.md")
|
|
81
|
+
print(f" Reason: Missing YAML frontmatter")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
# Parse YAML frontmatter
|
|
85
|
+
try:
|
|
86
|
+
import yaml
|
|
87
|
+
fm = yaml.safe_load(m.group(1)) or {}
|
|
88
|
+
except ImportError:
|
|
89
|
+
print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
93
|
+
print(f" Path: {rel_path}/module.md")
|
|
94
|
+
print(f" Reason: YAML parse error: {e}")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
# Validate 'name' field (required)
|
|
98
|
+
if "name" not in fm:
|
|
99
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
100
|
+
print(f" Path: {rel_path}/module.md")
|
|
101
|
+
print(f" Reason: Missing 'name' field")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
raw_name = str(fm["name"]).strip()
|
|
105
|
+
|
|
106
|
+
if not raw_name:
|
|
107
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
108
|
+
print(f" Path: {rel_path}/module.md")
|
|
109
|
+
print(f" Reason: Empty module name")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
# Validate name characters
|
|
113
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
|
|
114
|
+
|
|
115
|
+
if sanitized != raw_name:
|
|
116
|
+
invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
|
|
117
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
118
|
+
print(f" Path: {rel_path}/module.md")
|
|
119
|
+
print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
result["name"] = sanitized
|
|
123
|
+
|
|
124
|
+
# Extract optional fields
|
|
125
|
+
if "preferred_port" in fm:
|
|
126
|
+
try:
|
|
127
|
+
result["preferred_port"] = int(fm["preferred_port"])
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
if "advertise_ip" in fm:
|
|
132
|
+
result["advertise_ip"] = str(fm["advertise_ip"])
|
|
133
|
+
|
|
134
|
+
# max_connections(弹性连接上限)
|
|
135
|
+
try:
|
|
136
|
+
result["max_connections"] = max(1, min(10, int(fm.get("max_connections", 1))))
|
|
137
|
+
except (ValueError, TypeError):
|
|
138
|
+
result["max_connections"] = 1
|
|
139
|
+
|
|
140
|
+
# business_config(从 businesses 数组取)
|
|
141
|
+
if "businesses" in fm:
|
|
142
|
+
businesses = fm["businesses"]
|
|
143
|
+
if isinstance(businesses, list) and businesses:
|
|
144
|
+
config_file = businesses[0].get("config_file") if isinstance(businesses[0], dict) else None
|
|
145
|
+
if config_file:
|
|
146
|
+
result["business_config"] = str(config_file).strip()
|
|
147
|
+
|
|
148
|
+
except SystemExit:
|
|
149
|
+
raise # Re-raise exit to prevent catching by outer except
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
_module_config = _load_module_config()
|
|
157
|
+
MODULE_NAME = _module_config["name"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _SafeWriter:
|
|
161
|
+
"""Wraps a stream to silently swallow BrokenPipeError on write/flush."""
|
|
162
|
+
def __init__(self, stream):
|
|
163
|
+
self._stream = stream
|
|
164
|
+
|
|
165
|
+
def write(self, s):
|
|
166
|
+
try:
|
|
167
|
+
self._stream.write(s)
|
|
168
|
+
except (BrokenPipeError, OSError):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def flush(self):
|
|
172
|
+
try:
|
|
173
|
+
self._stream.flush()
|
|
174
|
+
except (BrokenPipeError, OSError):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def __getattr__(self, name):
|
|
178
|
+
return getattr(self._stream, name)
|
|
179
|
+
|
|
180
|
+
sys.stdout = _SafeWriter(sys.stdout)
|
|
181
|
+
sys.stderr = _SafeWriter(sys.stderr)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── Timestamped print + log file writer ──
|
|
185
|
+
# Independent implementation per module (no shared code dependency)
|
|
186
|
+
|
|
187
|
+
_builtin_print = builtins.print
|
|
188
|
+
_start_ts = time.monotonic()
|
|
189
|
+
_last_ts = time.monotonic()
|
|
190
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
191
|
+
_log_lock = threading.Lock()
|
|
192
|
+
_log_latest_path = None
|
|
193
|
+
_log_daily_path = None
|
|
194
|
+
_log_daily_date = ""
|
|
195
|
+
_log_dir = None
|
|
196
|
+
_crash_log_path = None
|
|
197
|
+
|
|
198
|
+
def _strip_ansi(s: str) -> str:
|
|
199
|
+
return _ANSI_RE.sub("", s)
|
|
200
|
+
|
|
201
|
+
def _resolve_daily_log_path():
|
|
202
|
+
"""Resolve daily log path based on current date."""
|
|
203
|
+
global _log_daily_path, _log_daily_date
|
|
204
|
+
if not _log_dir:
|
|
205
|
+
return
|
|
206
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
207
|
+
if today == _log_daily_date and _log_daily_path:
|
|
208
|
+
return
|
|
209
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
210
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
211
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
212
|
+
_log_daily_date = today
|
|
213
|
+
|
|
214
|
+
def _write_log(plain_line: str):
|
|
215
|
+
"""Write a plain-text line to both latest.log and daily log."""
|
|
216
|
+
with _log_lock:
|
|
217
|
+
if _log_latest_path:
|
|
218
|
+
try:
|
|
219
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
220
|
+
f.write(plain_line)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
_resolve_daily_log_path()
|
|
224
|
+
if _log_daily_path:
|
|
225
|
+
try:
|
|
226
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
227
|
+
f.write(plain_line)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
|
|
232
|
+
"""Write crash record to crashes.jsonl + daily crash archive."""
|
|
233
|
+
record = {
|
|
234
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
235
|
+
"module": MODULE_NAME,
|
|
236
|
+
"thread": thread_name or threading.current_thread().name,
|
|
237
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
238
|
+
"exception_message": str(exc_value),
|
|
239
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
240
|
+
"severity": severity,
|
|
241
|
+
"handled": handled,
|
|
242
|
+
"process_id": os.getpid(),
|
|
243
|
+
"platform": sys.platform,
|
|
244
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if exc_tb:
|
|
248
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
249
|
+
if tb_entries:
|
|
250
|
+
last = tb_entries[-1]
|
|
251
|
+
record["context"] = {
|
|
252
|
+
"function": last.name,
|
|
253
|
+
"file": os.path.basename(last.filename),
|
|
254
|
+
"line": last.lineno,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
258
|
+
|
|
259
|
+
if _crash_log_path:
|
|
260
|
+
try:
|
|
261
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
262
|
+
f.write(line)
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
if _log_dir:
|
|
267
|
+
try:
|
|
268
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
269
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
270
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
271
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
272
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
273
|
+
f.write(line)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
def _print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
278
|
+
"""Print crash summary to console (red highlight)."""
|
|
279
|
+
RED = "\033[91m"
|
|
280
|
+
RESET = "\033[0m"
|
|
281
|
+
|
|
282
|
+
if exc_tb:
|
|
283
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
284
|
+
if tb_entries:
|
|
285
|
+
last = tb_entries[-1]
|
|
286
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
287
|
+
else:
|
|
288
|
+
location = "unknown"
|
|
289
|
+
else:
|
|
290
|
+
location = "unknown"
|
|
291
|
+
|
|
292
|
+
prefix = f"[{MODULE_NAME}]"
|
|
293
|
+
if thread_name:
|
|
294
|
+
_builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
|
|
295
|
+
f"{exc_type.__name__} in {location}{RESET}")
|
|
296
|
+
else:
|
|
297
|
+
_builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
|
|
298
|
+
if _crash_log_path:
|
|
299
|
+
_builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
300
|
+
|
|
301
|
+
def _setup_exception_hooks():
|
|
302
|
+
"""Set up global exception hooks."""
|
|
303
|
+
_orig_excepthook = sys.excepthook
|
|
304
|
+
|
|
305
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
306
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
307
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
308
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
309
|
+
|
|
310
|
+
sys.excepthook = _excepthook
|
|
311
|
+
|
|
312
|
+
if hasattr(threading, "excepthook"):
|
|
313
|
+
def _thread_excepthook(args):
|
|
314
|
+
_write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
315
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
316
|
+
severity="error", handled=False)
|
|
317
|
+
_print_crash_summary(args.exc_type, args.exc_traceback,
|
|
318
|
+
thread_name=args.thread.name if args.thread else None)
|
|
319
|
+
|
|
320
|
+
threading.excepthook = _thread_excepthook
|
|
321
|
+
|
|
322
|
+
def _tprint(*args, **kwargs):
|
|
323
|
+
"""Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
|
|
324
|
+
global _last_ts
|
|
325
|
+
now = time.monotonic()
|
|
326
|
+
elapsed = now - _start_ts
|
|
327
|
+
delta = now - _last_ts
|
|
328
|
+
_last_ts = now
|
|
329
|
+
|
|
330
|
+
if elapsed < 1:
|
|
331
|
+
elapsed_str = f"{elapsed * 1000:.0f}ms"
|
|
332
|
+
elif elapsed < 100:
|
|
333
|
+
elapsed_str = f"{elapsed:.1f}s"
|
|
334
|
+
else:
|
|
335
|
+
elapsed_str = f"{elapsed:.0f}s"
|
|
336
|
+
|
|
337
|
+
if delta < 0.001:
|
|
338
|
+
delta_str = ""
|
|
339
|
+
elif delta < 1:
|
|
340
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
341
|
+
elif delta < 100:
|
|
342
|
+
delta_str = f"+{delta:.1f}s"
|
|
343
|
+
else:
|
|
344
|
+
delta_str = f"+{delta:.0f}s"
|
|
345
|
+
|
|
346
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
347
|
+
|
|
348
|
+
_builtin_print(*args, **kwargs)
|
|
349
|
+
|
|
350
|
+
if _log_latest_path or _log_daily_path:
|
|
351
|
+
sep = kwargs.get("sep", " ")
|
|
352
|
+
end = kwargs.get("end", "\n")
|
|
353
|
+
text = sep.join(str(a) for a in args)
|
|
354
|
+
prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
355
|
+
_write_log(prefix + _strip_ansi(text) + end)
|
|
356
|
+
|
|
357
|
+
builtins.print = _tprint
|
|
358
|
+
|
|
359
|
+
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
360
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
361
|
+
if _project_root not in sys.path:
|
|
362
|
+
sys.path.insert(0, _project_root)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _fmt_elapsed(t0: float) -> str:
|
|
367
|
+
d = time.monotonic() - t0
|
|
368
|
+
if d < 1:
|
|
369
|
+
return f"{d * 1000:.0f}ms"
|
|
370
|
+
if d < 10:
|
|
371
|
+
return f"{d:.1f}s"
|
|
372
|
+
return f"{d:.0f}s"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
|
|
376
|
+
"""Read a single kite message of expected type from stdin with timeout."""
|
|
377
|
+
result = [None]
|
|
378
|
+
|
|
379
|
+
def _read():
|
|
380
|
+
try:
|
|
381
|
+
line = sys.stdin.readline().strip()
|
|
382
|
+
if line:
|
|
383
|
+
msg = json.loads(line)
|
|
384
|
+
if isinstance(msg, dict) and msg.get("kite") == expected_type:
|
|
385
|
+
result[0] = msg
|
|
386
|
+
except Exception:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
390
|
+
t.start()
|
|
391
|
+
t.join(timeout=timeout)
|
|
392
|
+
return result[0]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# Global WS reference for publish_event callback
|
|
396
|
+
_ws_global = None
|
|
397
|
+
_shutting_down = False
|
|
398
|
+
_exit_code = 0 # Exit code for main() to use
|
|
399
|
+
|
|
400
|
+
# 弹性多连接
|
|
401
|
+
_extra_ws: dict = {} # slot → WebSocket(附加连接)
|
|
402
|
+
_extra_ws_tasks: dict = {} # slot → recv loop Task
|
|
403
|
+
_kernel_port = "" # 缓存 kernel_port,供 offer handler 使用
|
|
404
|
+
_pending_rpc: dict[str, asyncio.Future] = {} # rpc_id → Future(等待响应)
|
|
405
|
+
|
|
406
|
+
_has_registered = False
|
|
407
|
+
|
|
408
|
+
# Redis 特有
|
|
409
|
+
_redis_impl = None # Redis 实现实例(外部或内置)
|
|
410
|
+
_redis_ready = False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _is_auth_failure(e: Exception) -> bool:
|
|
414
|
+
"""Check if a WebSocket exception indicates authentication failure."""
|
|
415
|
+
if hasattr(e, 'rcvd') and e.rcvd is not None:
|
|
416
|
+
code = e.rcvd.code if hasattr(e.rcvd, 'code') else 0
|
|
417
|
+
return code in (4001, 4003)
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
async def main():
|
|
422
|
+
global _ws_global, _shutting_down
|
|
423
|
+
if _startup_config.get("mode") == "remote":
|
|
424
|
+
await _remote_mode_loop(_startup_config["gateway_url"], _startup_config["kite_token"], _startup_config["t0"])
|
|
425
|
+
else:
|
|
426
|
+
await _ws_loop(_startup_config["token"], _startup_config["kernel_port"], _startup_config["t0"])
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def _ws_loop(token: str, kernel_port: int, _t0: float):
|
|
430
|
+
"""Connect to Kernel with exponential backoff reconnection."""
|
|
431
|
+
global _shutting_down, _exit_code
|
|
432
|
+
retry_delay = 0.5
|
|
433
|
+
max_delay = 10.0
|
|
434
|
+
attempt = 0
|
|
435
|
+
cooldown_attempts = 0
|
|
436
|
+
|
|
437
|
+
while not _shutting_down:
|
|
438
|
+
try:
|
|
439
|
+
await _ws_connect(token, kernel_port, _t0)
|
|
440
|
+
retry_delay = 0.5
|
|
441
|
+
attempt = 0
|
|
442
|
+
cooldown_attempts = 0
|
|
443
|
+
except asyncio.CancelledError:
|
|
444
|
+
return
|
|
445
|
+
except Exception as e:
|
|
446
|
+
if _shutting_down:
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
code = _get_close_code(e)
|
|
450
|
+
|
|
451
|
+
# never: 永不重连
|
|
452
|
+
if code in (4001, 4003, 4004, 1008, 4010):
|
|
453
|
+
print(f"[redis] 致命错误 (code {code}),退出")
|
|
454
|
+
_exit_code = 1
|
|
455
|
+
_shutting_down = True
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# cooldown: 速率限制
|
|
459
|
+
if code == 4020:
|
|
460
|
+
cooldown_attempts += 1
|
|
461
|
+
if cooldown_attempts >= 5:
|
|
462
|
+
print(f"[redis] 速率限制重试 5 次,退出")
|
|
463
|
+
_exit_code = 1
|
|
464
|
+
_shutting_down = True
|
|
465
|
+
return
|
|
466
|
+
print(f"[redis] 速率限制,10.0s 后重试 ({cooldown_attempts}/5)")
|
|
467
|
+
await asyncio.sleep(10.0)
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# standard: 指数退避 + jitter
|
|
471
|
+
attempt += 1
|
|
472
|
+
jitter = retry_delay * 0.2 * random.random()
|
|
473
|
+
sleep_time = retry_delay + jitter
|
|
474
|
+
_write_crash(type(e), e, e.__traceback__, severity="error", handled=True)
|
|
475
|
+
print(f"[redis] 连接错误: {e}, {sleep_time:.1f}s 后重试 (attempt {attempt})")
|
|
476
|
+
|
|
477
|
+
_ws_global_clear()
|
|
478
|
+
if _shutting_down:
|
|
479
|
+
return
|
|
480
|
+
await asyncio.sleep(sleep_time if 'sleep_time' in locals() else retry_delay)
|
|
481
|
+
retry_delay = min(retry_delay * 2, max_delay)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _get_close_code(e: Exception) -> int:
|
|
485
|
+
"""从 websockets 异常中提取关闭码"""
|
|
486
|
+
if hasattr(e, 'rcvd') and e.rcvd is not None:
|
|
487
|
+
return getattr(e.rcvd, 'code', 0)
|
|
488
|
+
return 0
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _ws_global_clear():
|
|
492
|
+
global _ws_global
|
|
493
|
+
_ws_global = None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
async def _do_init(ws):
|
|
497
|
+
"""收到 system.require_init 后执行:启动 Redis → subscribe + register + module.ready"""
|
|
498
|
+
global _has_registered, _redis_impl, _redis_ready
|
|
499
|
+
reason = "startup" if not _has_registered else "recovery"
|
|
500
|
+
print(f"[redis] Received system.require_init (reason={reason})")
|
|
501
|
+
|
|
502
|
+
# 0. 启动 Redis 实现(如果还未启动)
|
|
503
|
+
if not _redis_impl:
|
|
504
|
+
await _start_redis_impl()
|
|
505
|
+
|
|
506
|
+
# 1. Subscribe to events
|
|
507
|
+
await _rpc_call(ws, "event.subscribe", {
|
|
508
|
+
"events": [
|
|
509
|
+
"system.ping",
|
|
510
|
+
"module.started",
|
|
511
|
+
"module.stopped",
|
|
512
|
+
"module.shutdown",
|
|
513
|
+
],
|
|
514
|
+
})
|
|
515
|
+
print(f"[redis] Subscribed to events")
|
|
516
|
+
|
|
517
|
+
# 2. Register to Kernel Registry via RPC
|
|
518
|
+
registry_data = {
|
|
519
|
+
"module_id": "redis",
|
|
520
|
+
"module_type": "service",
|
|
521
|
+
"launcher_id": "launcher",
|
|
522
|
+
"tools": {
|
|
523
|
+
"rpc": {
|
|
524
|
+
"module": {
|
|
525
|
+
"health": {"method": "health", "description": "健康检查"},
|
|
526
|
+
"status": {"method": "status", "description": "状态查询"},
|
|
527
|
+
"config": {
|
|
528
|
+
"get": {"method": "get_settings", "description": "获取配置"},
|
|
529
|
+
"update": {"method": "update_settings", "description": "更新配置"},
|
|
530
|
+
"reset": {"method": "reset_settings", "description": "恢复默认配置"},
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
"redis": {
|
|
534
|
+
"ping": {"method": "ping", "description": "Ping Redis"},
|
|
535
|
+
"get": {"method": "get", "description": "GET key"},
|
|
536
|
+
"set": {"method": "set", "description": "SET key value"},
|
|
537
|
+
"del": {"method": "del", "description": "DEL key"},
|
|
538
|
+
"expire": {"method": "expire", "description": "EXPIRE key seconds"},
|
|
539
|
+
"hget": {"method": "hget", "description": "HGET key field"},
|
|
540
|
+
"hset": {"method": "hset", "description": "HSET key field value"},
|
|
541
|
+
"hdel": {"method": "hdel", "description": "HDEL key field"},
|
|
542
|
+
"hgetall": {"method": "hgetall", "description": "HGETALL key"},
|
|
543
|
+
"sadd": {"method": "sadd", "description": "SADD key members"},
|
|
544
|
+
"srem": {"method": "srem", "description": "SREM key members"},
|
|
545
|
+
"smembers": {"method": "smembers", "description": "SMEMBERS key"},
|
|
546
|
+
"publish": {"method": "publish", "description": "PUBLISH channel message"},
|
|
547
|
+
"info": {"method": "info", "description": "Redis 连接信息"},
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
"events_publish": {
|
|
552
|
+
"system": {
|
|
553
|
+
"redis_ready": {"description": "Redis 服务已就绪"},
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
"events_subscribe": [
|
|
557
|
+
"module.started",
|
|
558
|
+
"module.stopped",
|
|
559
|
+
"module.shutdown",
|
|
560
|
+
],
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# 添加 Redis 连接信息(供模块直连)
|
|
564
|
+
if _redis_ready and _redis_impl:
|
|
565
|
+
registry_data["services"] = {
|
|
566
|
+
"redis": {
|
|
567
|
+
"host": _redis_impl.host,
|
|
568
|
+
"port": _redis_impl.port,
|
|
569
|
+
"protocol": "RESP",
|
|
570
|
+
"type": "external" if hasattr(_redis_impl, "_client") else "builtin",
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
await _rpc_call(ws, "registry.register", registry_data)
|
|
575
|
+
print(f"[redis] Registered to Kernel")
|
|
576
|
+
|
|
577
|
+
# 3. Publish module.ready
|
|
578
|
+
if not _shutting_down:
|
|
579
|
+
startup_time = time.monotonic() - _start_ts
|
|
580
|
+
await _publish_event(ws, "module.ready", {
|
|
581
|
+
"module_id": "redis",
|
|
582
|
+
"graceful_shutdown": True,
|
|
583
|
+
"startup_time": startup_time,
|
|
584
|
+
"reason": reason,
|
|
585
|
+
})
|
|
586
|
+
print(f"[redis] module.ready published (reason={reason})")
|
|
587
|
+
|
|
588
|
+
_has_registered = True
|
|
589
|
+
|
|
590
|
+
# Start test event loop in background
|
|
591
|
+
asyncio.create_task(_test_event_loop(ws))
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
async def _start_redis_impl():
|
|
595
|
+
"""启动 Redis 实现(外部或内置)"""
|
|
596
|
+
global _redis_impl, _redis_ready
|
|
597
|
+
|
|
598
|
+
# 从 module.md 读取业务配置文件名
|
|
599
|
+
config_file = _module_config.get("business_config")
|
|
600
|
+
config = {}
|
|
601
|
+
if config_file:
|
|
602
|
+
config_path = os.path.join(os.path.dirname(__file__), config_file)
|
|
603
|
+
if os.path.exists(config_path):
|
|
604
|
+
try:
|
|
605
|
+
import json5
|
|
606
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
607
|
+
config = json5.load(f)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
print(f"[redis] 配置文件读取失败: {e}")
|
|
610
|
+
|
|
611
|
+
redis_mode = config.get("redis_mode", "builtin")
|
|
612
|
+
redis_url = config.get("redis_url") or os.getenv("KITE_REDIS_URL")
|
|
613
|
+
allow_fallback = config.get("allow_fallback", True)
|
|
614
|
+
retry_initial = config.get("redis_retry_initial", 1.0)
|
|
615
|
+
retry_max = config.get("redis_retry_max", 30.0)
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
if redis_mode == "external" and redis_url:
|
|
619
|
+
print(f"[redis] 尝试连接外部 Redis: {redis_url}")
|
|
620
|
+
from redis_external import ExternalRedis
|
|
621
|
+
redis_password = config.get("redis_password") or os.getenv("KITE_REDIS_PASSWORD") or None
|
|
622
|
+
_redis_impl = ExternalRedis(redis_url, password=redis_password, retry_initial=retry_initial, retry_max=retry_max)
|
|
623
|
+
# allow_fallback 时有限重试(5次),否则无限重试
|
|
624
|
+
max_attempts = 5 if allow_fallback else 0
|
|
625
|
+
await _redis_impl.start(max_attempts=max_attempts)
|
|
626
|
+
_redis_ready = True
|
|
627
|
+
GREEN = "\033[92m"
|
|
628
|
+
RESET = "\033[0m"
|
|
629
|
+
print(f"{GREEN}[redis] ✓ Redis 已就绪: 外部 Redis | {_redis_impl.host}:{_redis_impl.port}{RESET}")
|
|
630
|
+
else:
|
|
631
|
+
print(f"[redis] 使用内置 Redis")
|
|
632
|
+
from redis_builtin import BuiltinRedis
|
|
633
|
+
_redis_impl = BuiltinRedis()
|
|
634
|
+
await _redis_impl.start()
|
|
635
|
+
_redis_ready = True
|
|
636
|
+
GREEN = "\033[92m"
|
|
637
|
+
RESET = "\033[0m"
|
|
638
|
+
print(f"{GREEN}[redis] ✓ Redis 已就绪: 内置 Redis | {_redis_impl.host}:{_redis_impl.port}{RESET}")
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
if redis_mode == "external" and allow_fallback:
|
|
642
|
+
print(f"[redis] 外部 Redis 连接失败,降级到内置 Redis: {e}")
|
|
643
|
+
try:
|
|
644
|
+
from redis_builtin import BuiltinRedis
|
|
645
|
+
_redis_impl = BuiltinRedis()
|
|
646
|
+
await _redis_impl.start()
|
|
647
|
+
_redis_ready = True
|
|
648
|
+
YELLOW = "\033[93m"
|
|
649
|
+
RESET = "\033[0m"
|
|
650
|
+
print(f"{YELLOW}[redis] ⚠ Redis 已降级: 内置 Redis | {_redis_impl.host}:{_redis_impl.port}{RESET}")
|
|
651
|
+
except Exception as e2:
|
|
652
|
+
RED = "\033[91m"
|
|
653
|
+
RESET = "\033[0m"
|
|
654
|
+
print(f"{RED}[redis] ✗ Redis 启动失败: {e2}{RESET}")
|
|
655
|
+
_write_crash(type(e2), e2, e2.__traceback__, severity="error", handled=True)
|
|
656
|
+
sys.exit(1)
|
|
657
|
+
else:
|
|
658
|
+
RED = "\033[91m"
|
|
659
|
+
RESET = "\033[0m"
|
|
660
|
+
print(f"{RED}[redis] ✗ Redis 启动失败(不允许降级): {e}{RESET}")
|
|
661
|
+
_write_crash(type(e), e, e.__traceback__, severity="error", handled=True)
|
|
662
|
+
sys.exit(1)
|
|
663
|
+
|
|
664
|
+
# 发布 system.redis_ready 事件
|
|
665
|
+
if _redis_ready and _redis_impl and _ws_global:
|
|
666
|
+
await _publish_event(_ws_global, "system.redis_ready", {
|
|
667
|
+
"host": _redis_impl.host,
|
|
668
|
+
"port": _redis_impl.port,
|
|
669
|
+
"protocol": "RESP",
|
|
670
|
+
})
|
|
671
|
+
print(f"[redis] Published system.redis_ready")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
async def _ws_connect(token: str, kernel_port: int, _t0: float):
|
|
675
|
+
"""Single WebSocket session: connect → auth → start receiver → wait for system.require_init."""
|
|
676
|
+
global _ws_global, _kernel_port
|
|
677
|
+
_kernel_port = kernel_port
|
|
678
|
+
|
|
679
|
+
ws_url = f"ws://127.0.0.1:{kernel_port}/ws?id=redis"
|
|
680
|
+
print(f"[redis] Connecting to Kernel: {ws_url}")
|
|
681
|
+
|
|
682
|
+
async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
|
|
683
|
+
# Send auth message first
|
|
684
|
+
auth_req = {
|
|
685
|
+
"jsonrpc": "2.0",
|
|
686
|
+
"id": "auth",
|
|
687
|
+
"method": "auth",
|
|
688
|
+
"params": {"token": token}
|
|
689
|
+
}
|
|
690
|
+
await ws.send(json.dumps(auth_req))
|
|
691
|
+
|
|
692
|
+
# Wait for auth response
|
|
693
|
+
auth_resp_raw = await asyncio.wait_for(ws.recv(), timeout=5)
|
|
694
|
+
auth_resp = json.loads(auth_resp_raw)
|
|
695
|
+
if "error" in auth_resp:
|
|
696
|
+
raise Exception(f"Auth failed: {auth_resp['error']}")
|
|
697
|
+
|
|
698
|
+
_ws_global = ws
|
|
699
|
+
print(f"[redis] Connected to Kernel ({_fmt_elapsed(_t0)})")
|
|
700
|
+
|
|
701
|
+
# Start receiver task — wait for system.require_init to trigger _do_init
|
|
702
|
+
receiver_task = asyncio.create_task(_ws_receiver(ws))
|
|
703
|
+
try:
|
|
704
|
+
# Wait for receiver to finish (connection closed or error)
|
|
705
|
+
await receiver_task
|
|
706
|
+
finally:
|
|
707
|
+
receiver_task.cancel()
|
|
708
|
+
for fut in _pending_rpc.values():
|
|
709
|
+
if not fut.done():
|
|
710
|
+
fut.set_result({"error": {"code": -32001, "message": "Connection lost"}})
|
|
711
|
+
_pending_rpc.clear()
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
async def _ws_receiver(ws):
|
|
715
|
+
"""Main connection receive loop: route events, RPC requests, and RPC responses.
|
|
716
|
+
Detects system.require_init to trigger _do_init."""
|
|
717
|
+
async for raw in ws:
|
|
718
|
+
try:
|
|
719
|
+
msg = json.loads(raw)
|
|
720
|
+
except (json.JSONDecodeError, TypeError):
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
has_method = "method" in msg
|
|
725
|
+
has_id = "id" in msg
|
|
726
|
+
|
|
727
|
+
if has_method and not has_id:
|
|
728
|
+
# 检测 system.require_init 事件
|
|
729
|
+
params = msg.get("params", {})
|
|
730
|
+
event_type = params.get("event", "")
|
|
731
|
+
if event_type == "system.require_init":
|
|
732
|
+
asyncio.create_task(_do_init(ws))
|
|
733
|
+
continue
|
|
734
|
+
# Event Notification — run in background to prevent deadlock
|
|
735
|
+
asyncio.create_task(_handle_event_notification(msg))
|
|
736
|
+
elif has_method and has_id:
|
|
737
|
+
# Incoming RPC request — run in background to prevent deadlock
|
|
738
|
+
asyncio.create_task(_handle_rpc_request(ws, msg))
|
|
739
|
+
elif not has_method and has_id:
|
|
740
|
+
_handle_rpc_response(msg)
|
|
741
|
+
except Exception as e:
|
|
742
|
+
print(f"[redis] 消息处理异常(已忽略): {e}")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
async def _rpc_call(ws, method: str, params: dict = None,
|
|
746
|
+
wait_response: bool = True, timeout: float = 3.0) -> dict:
|
|
747
|
+
"""JSON-RPC 2.0 request。默认等待响应。"""
|
|
748
|
+
rpc_id = str(uuid.uuid4())
|
|
749
|
+
msg = {"jsonrpc": "2.0", "id": rpc_id, "method": method}
|
|
750
|
+
if params:
|
|
751
|
+
msg["params"] = params
|
|
752
|
+
|
|
753
|
+
if not wait_response:
|
|
754
|
+
await ws.send(json.dumps(msg))
|
|
755
|
+
return {}
|
|
756
|
+
|
|
757
|
+
future = asyncio.get_event_loop().create_future()
|
|
758
|
+
_pending_rpc[rpc_id] = future
|
|
759
|
+
await ws.send(json.dumps(msg))
|
|
760
|
+
try:
|
|
761
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
762
|
+
except asyncio.TimeoutError:
|
|
763
|
+
_pending_rpc.pop(rpc_id, None)
|
|
764
|
+
return {"error": {"code": -32000, "message": f"RPC timeout: {method} ({timeout}s)"}}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _handle_rpc_response(msg: dict):
|
|
768
|
+
"""Route JSON-RPC response to pending future."""
|
|
769
|
+
rpc_id = msg.get("id")
|
|
770
|
+
future = _pending_rpc.pop(rpc_id, None)
|
|
771
|
+
if future and not future.done():
|
|
772
|
+
future.set_result(msg)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
async def _publish_event(ws, event: str, data: dict = None):
|
|
776
|
+
"""Publish event via event.publish RPC (fire-and-forget)."""
|
|
777
|
+
await _rpc_call(ws, "event.publish", {
|
|
778
|
+
"event_id": str(uuid.uuid4()),
|
|
779
|
+
"event": event,
|
|
780
|
+
"data": data or {},
|
|
781
|
+
}, wait_response=False)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
async def _handle_ping_event(data: dict):
|
|
785
|
+
"""Handle system.ping event and reply with system.pong."""
|
|
786
|
+
t1 = data.get("ping_time")
|
|
787
|
+
t2 = time.time()
|
|
788
|
+
|
|
789
|
+
await _publish_event(_ws_global, "system.pong", {
|
|
790
|
+
"module_id": MODULE_NAME,
|
|
791
|
+
"ping_time": t1,
|
|
792
|
+
"pong_time": t2,
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
async def _handle_event_notification(msg: dict):
|
|
797
|
+
"""Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
|
|
798
|
+
params = msg.get("params", {})
|
|
799
|
+
event_type = params.get("event", "")
|
|
800
|
+
data = params.get("data", {})
|
|
801
|
+
|
|
802
|
+
# Handle system.ping event
|
|
803
|
+
if event_type == "system.ping":
|
|
804
|
+
await _handle_ping_event(data)
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
# 弹性连接 offer/release
|
|
808
|
+
if event_type == "system.connection.offer":
|
|
809
|
+
asyncio.create_task(_handle_connection_offer(data))
|
|
810
|
+
return
|
|
811
|
+
if event_type == "system.connection.release":
|
|
812
|
+
asyncio.create_task(_handle_connection_release(data))
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
# Special handling for module.shutdown
|
|
816
|
+
if event_type == "module.shutdown":
|
|
817
|
+
target = data.get("module_id", "")
|
|
818
|
+
reason = data.get("reason", "")
|
|
819
|
+
# Handle both targeted shutdown (module_id == "redis") and broadcast shutdown (no module_id or launcher_lost)
|
|
820
|
+
if target == "redis" or not target or reason == "launcher_lost":
|
|
821
|
+
await _handle_shutdown()
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
# Layer 2: 忽略系统广播事件
|
|
825
|
+
if event_type in SYSTEM_BROADCAST_EVENTS:
|
|
826
|
+
return
|
|
827
|
+
|
|
828
|
+
# Layer 3: 警告未知事件(仅开发环境)
|
|
829
|
+
if os.environ.get("KITE_ENV") == "development":
|
|
830
|
+
print(f"[redis] Debug: Unhandled event: {event_type}")
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
async def _handle_rpc_request(ws, msg: dict):
|
|
834
|
+
"""Handle an incoming RPC request (redis.* methods)."""
|
|
835
|
+
rpc_id = msg.get("id", "")
|
|
836
|
+
method = msg.get("method", "")
|
|
837
|
+
params = msg.get("params", {})
|
|
838
|
+
|
|
839
|
+
handlers = {
|
|
840
|
+
"health": lambda p: _rpc_health(),
|
|
841
|
+
"status": lambda p: _rpc_status(),
|
|
842
|
+
"get_settings": lambda p: _rpc_get_settings(p),
|
|
843
|
+
"update_settings": lambda p: _rpc_update_settings(p),
|
|
844
|
+
"reset_settings": lambda p: _rpc_reset_settings(p),
|
|
845
|
+
"ping": lambda p: _rpc_redis_ping(p),
|
|
846
|
+
"get": lambda p: _rpc_redis_get(p),
|
|
847
|
+
"set": lambda p: _rpc_redis_set(p),
|
|
848
|
+
"del": lambda p: _rpc_redis_del(p),
|
|
849
|
+
"expire": lambda p: _rpc_redis_expire(p),
|
|
850
|
+
"hget": lambda p: _rpc_redis_hget(p),
|
|
851
|
+
"hset": lambda p: _rpc_redis_hset(p),
|
|
852
|
+
"hdel": lambda p: _rpc_redis_hdel(p),
|
|
853
|
+
"hgetall": lambda p: _rpc_redis_hgetall(p),
|
|
854
|
+
"sadd": lambda p: _rpc_redis_sadd(p),
|
|
855
|
+
"srem": lambda p: _rpc_redis_srem(p),
|
|
856
|
+
"smembers": lambda p: _rpc_redis_smembers(p),
|
|
857
|
+
"publish": lambda p: _rpc_redis_publish(p),
|
|
858
|
+
"info": lambda p: _rpc_redis_info(p),
|
|
859
|
+
}
|
|
860
|
+
handler = handlers.get(method)
|
|
861
|
+
if handler:
|
|
862
|
+
try:
|
|
863
|
+
result = await handler(params)
|
|
864
|
+
await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
|
|
865
|
+
except Exception as e:
|
|
866
|
+
await ws.send(json.dumps({
|
|
867
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
868
|
+
"error": {"code": -32603, "message": str(e)},
|
|
869
|
+
}))
|
|
870
|
+
else:
|
|
871
|
+
await ws.send(json.dumps({
|
|
872
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
873
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
874
|
+
}))
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
async def _rpc_health() -> dict:
|
|
878
|
+
"""RPC handler for redis.health."""
|
|
879
|
+
return {
|
|
880
|
+
"status": "healthy",
|
|
881
|
+
"redis_ready": _redis_ready,
|
|
882
|
+
"details": {
|
|
883
|
+
"uptime_seconds": round(time.monotonic() - _start_ts),
|
|
884
|
+
},
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
async def _rpc_status() -> dict:
|
|
889
|
+
"""RPC handler for redis.status."""
|
|
890
|
+
return {
|
|
891
|
+
"module_id": "redis",
|
|
892
|
+
"status": "running",
|
|
893
|
+
"redis_ready": _redis_ready,
|
|
894
|
+
"uptime": round(time.monotonic() - _start_ts),
|
|
895
|
+
"uptime_seconds": round(time.monotonic() - _start_ts),
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
# ── Configuration management helpers ──
|
|
900
|
+
|
|
901
|
+
def _read_module_md() -> tuple[dict, str]:
|
|
902
|
+
"""读取 module.md,返回 (frontmatter, body)"""
|
|
903
|
+
from pathlib import Path
|
|
904
|
+
md_path = Path(__file__).parent / "module.md"
|
|
905
|
+
text = md_path.read_text(encoding="utf-8")
|
|
906
|
+
|
|
907
|
+
# 提取 YAML frontmatter (--- ... ---)
|
|
908
|
+
m = re.match(r'^---\s*\n(.*?)\n---\s*\n?(.*)', text, re.DOTALL)
|
|
909
|
+
if not m:
|
|
910
|
+
return {}, text
|
|
911
|
+
|
|
912
|
+
import yaml
|
|
913
|
+
frontmatter = yaml.safe_load(m.group(1)) or {}
|
|
914
|
+
body = m.group(2)
|
|
915
|
+
return frontmatter, body
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _write_module_md(frontmatter: dict, body: str):
|
|
919
|
+
"""写入 module.md"""
|
|
920
|
+
from pathlib import Path
|
|
921
|
+
import yaml
|
|
922
|
+
md_path = Path(__file__).parent / "module.md"
|
|
923
|
+
fm_str = yaml.dump(frontmatter, allow_unicode=True, sort_keys=False, default_flow_style=False).rstrip()
|
|
924
|
+
content = f"---\n{fm_str}\n---\n{body}"
|
|
925
|
+
md_path.write_text(content, encoding="utf-8")
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
# ── RPC handlers for configuration management ──
|
|
929
|
+
|
|
930
|
+
async def _rpc_get_settings(params: dict) -> dict:
|
|
931
|
+
"""RPC handler for redis.get_settings — 获取模块的所有设置"""
|
|
932
|
+
frontmatter, _ = _read_module_md()
|
|
933
|
+
|
|
934
|
+
config = None
|
|
935
|
+
_businesses = frontmatter.get("businesses") or []
|
|
936
|
+
config_file = (_businesses[0].get("config_file") if isinstance(_businesses[0], dict) else None) if _businesses else None
|
|
937
|
+
if config_file:
|
|
938
|
+
config_path = os.path.join(os.path.dirname(__file__), config_file)
|
|
939
|
+
if os.path.exists(config_path):
|
|
940
|
+
try:
|
|
941
|
+
import json5
|
|
942
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
943
|
+
config = json5.load(f)
|
|
944
|
+
except Exception as e:
|
|
945
|
+
print(f"[redis] 读取业务配置失败: {e}")
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
"name": frontmatter.get("name", "redis"),
|
|
949
|
+
"display_name": frontmatter.get("display_name", ""),
|
|
950
|
+
"type": frontmatter.get("type", ""),
|
|
951
|
+
"state": frontmatter.get("state", "enabled"),
|
|
952
|
+
"version": frontmatter.get("version", ""),
|
|
953
|
+
"runtime": frontmatter.get("runtime", ""),
|
|
954
|
+
"entry": frontmatter.get("entry", ""),
|
|
955
|
+
"preferred_port": frontmatter.get("preferred_port"),
|
|
956
|
+
"advertise_ip": frontmatter.get("advertise_ip"),
|
|
957
|
+
"monitor": frontmatter.get("monitor"),
|
|
958
|
+
"events": frontmatter.get("events"),
|
|
959
|
+
"subscriptions": frontmatter.get("subscriptions"),
|
|
960
|
+
"depends_on": frontmatter.get("depends_on"),
|
|
961
|
+
"has_config": config is not None,
|
|
962
|
+
"config": config,
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
async def _rpc_update_settings(params: dict) -> dict:
|
|
967
|
+
"""RPC handler for redis.update_settings — 更新模块设置"""
|
|
968
|
+
metadata = params.get("metadata", {})
|
|
969
|
+
config = params.get("config", {})
|
|
970
|
+
|
|
971
|
+
# 更新 module.md frontmatter
|
|
972
|
+
if metadata:
|
|
973
|
+
frontmatter, body = _read_module_md()
|
|
974
|
+
for key, value in metadata.items():
|
|
975
|
+
frontmatter[key] = value
|
|
976
|
+
_write_module_md(frontmatter, body)
|
|
977
|
+
|
|
978
|
+
# 更新业务配置文件
|
|
979
|
+
if config:
|
|
980
|
+
frontmatter, _ = _read_module_md()
|
|
981
|
+
_businesses = frontmatter.get("businesses") or []
|
|
982
|
+
config_file = (_businesses[0].get("config_file") if isinstance(_businesses[0], dict) else None) if _businesses else None
|
|
983
|
+
if config_file:
|
|
984
|
+
config_path = os.path.join(os.path.dirname(__file__), config_file)
|
|
985
|
+
existing = {}
|
|
986
|
+
if os.path.exists(config_path):
|
|
987
|
+
try:
|
|
988
|
+
import json5
|
|
989
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
990
|
+
existing = json5.load(f) or {}
|
|
991
|
+
except Exception:
|
|
992
|
+
existing = {}
|
|
993
|
+
|
|
994
|
+
for flat_key, value in config.items():
|
|
995
|
+
keys = flat_key.split('.')
|
|
996
|
+
target = existing
|
|
997
|
+
for k in keys[:-1]:
|
|
998
|
+
if k not in target or not isinstance(target[k], dict):
|
|
999
|
+
target[k] = {}
|
|
1000
|
+
target = target[k]
|
|
1001
|
+
target[keys[-1]] = value
|
|
1002
|
+
|
|
1003
|
+
import json
|
|
1004
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
1005
|
+
f.write(json.dumps(existing, ensure_ascii=False, indent=2))
|
|
1006
|
+
|
|
1007
|
+
# 返回更新后的完整设置
|
|
1008
|
+
return await _rpc_get_settings({})
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
async def _rpc_reset_settings(params: dict) -> dict:
|
|
1012
|
+
"""RPC handler for redis.reset_settings — 恢复默认值"""
|
|
1013
|
+
fields = params.get("fields", [])
|
|
1014
|
+
reset_all = params.get("all", False)
|
|
1015
|
+
|
|
1016
|
+
# 默认值定义
|
|
1017
|
+
defaults = {
|
|
1018
|
+
"state": "enabled",
|
|
1019
|
+
"preferred_port": 20000,
|
|
1020
|
+
"advertise_ip": "127.0.0.1",
|
|
1021
|
+
"monitor": True,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
frontmatter, body = _read_module_md()
|
|
1025
|
+
|
|
1026
|
+
if reset_all:
|
|
1027
|
+
# 恢复所有字段
|
|
1028
|
+
for key, value in defaults.items():
|
|
1029
|
+
frontmatter[key] = value
|
|
1030
|
+
else:
|
|
1031
|
+
# 恢复指定字段
|
|
1032
|
+
for field in fields:
|
|
1033
|
+
if field in defaults:
|
|
1034
|
+
frontmatter[field] = defaults[field]
|
|
1035
|
+
|
|
1036
|
+
_write_module_md(frontmatter, body)
|
|
1037
|
+
|
|
1038
|
+
# 返回恢复后的设置
|
|
1039
|
+
return await _rpc_get_settings({})
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
# ── Redis RPC handlers ──
|
|
1043
|
+
|
|
1044
|
+
async def _rpc_redis_ping(params: dict) -> dict:
|
|
1045
|
+
"""RPC: redis.ping"""
|
|
1046
|
+
if not _redis_ready:
|
|
1047
|
+
raise Exception("Redis not ready")
|
|
1048
|
+
return {"status": "ok", "message": "PONG"}
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
async def _rpc_redis_get(params: dict) -> dict:
|
|
1052
|
+
"""RPC: redis.get"""
|
|
1053
|
+
if not _redis_ready or not _redis_impl:
|
|
1054
|
+
raise Exception("Redis not ready")
|
|
1055
|
+
key = params.get("key")
|
|
1056
|
+
if not key:
|
|
1057
|
+
raise Exception("Missing key parameter")
|
|
1058
|
+
value = await _redis_impl.get(key)
|
|
1059
|
+
return {"key": key, "value": value}
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
async def _rpc_redis_set(params: dict) -> dict:
|
|
1063
|
+
"""RPC: redis.set"""
|
|
1064
|
+
if not _redis_ready or not _redis_impl:
|
|
1065
|
+
raise Exception("Redis not ready")
|
|
1066
|
+
key = params.get("key")
|
|
1067
|
+
value = params.get("value")
|
|
1068
|
+
if not key:
|
|
1069
|
+
raise Exception("Missing key parameter")
|
|
1070
|
+
await _redis_impl.set(key, value)
|
|
1071
|
+
return {"status": "ok"}
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
async def _rpc_redis_hget(params: dict) -> dict:
|
|
1075
|
+
"""RPC: redis.hget"""
|
|
1076
|
+
if not _redis_ready or not _redis_impl:
|
|
1077
|
+
raise Exception("Redis not ready")
|
|
1078
|
+
key = params.get("key")
|
|
1079
|
+
field = params.get("field")
|
|
1080
|
+
if not key or not field:
|
|
1081
|
+
raise Exception("Missing key or field parameter")
|
|
1082
|
+
value = await _redis_impl.hget(key, field)
|
|
1083
|
+
return {"key": key, "field": field, "value": value}
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
async def _rpc_redis_hset(params: dict) -> dict:
|
|
1087
|
+
"""RPC: redis.hset"""
|
|
1088
|
+
if not _redis_ready or not _redis_impl:
|
|
1089
|
+
raise Exception("Redis not ready")
|
|
1090
|
+
key = params.get("key")
|
|
1091
|
+
field = params.get("field")
|
|
1092
|
+
value = params.get("value")
|
|
1093
|
+
if not key or not field:
|
|
1094
|
+
raise Exception("Missing key or field parameter")
|
|
1095
|
+
await _redis_impl.hset(key, field, value)
|
|
1096
|
+
return {"status": "ok"}
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
async def _rpc_redis_info(params: dict) -> dict:
|
|
1100
|
+
"""RPC: redis.info"""
|
|
1101
|
+
if not _redis_ready or not _redis_impl:
|
|
1102
|
+
raise Exception("Redis not ready")
|
|
1103
|
+
return {
|
|
1104
|
+
"host": _redis_impl.host,
|
|
1105
|
+
"port": _redis_impl.port,
|
|
1106
|
+
"protocol": "RESP",
|
|
1107
|
+
"type": "external" if hasattr(_redis_impl, "_client") else "builtin",
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
async def _rpc_redis_del(params: dict) -> dict:
|
|
1112
|
+
"""RPC: redis.del"""
|
|
1113
|
+
if not _redis_ready or not _redis_impl:
|
|
1114
|
+
raise Exception("Redis not ready")
|
|
1115
|
+
key = params.get("key")
|
|
1116
|
+
if not key:
|
|
1117
|
+
raise Exception("Missing key parameter")
|
|
1118
|
+
deleted = await _redis_impl.delete(key)
|
|
1119
|
+
return {"deleted": deleted}
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
async def _rpc_redis_expire(params: dict) -> dict:
|
|
1123
|
+
"""RPC: redis.expire"""
|
|
1124
|
+
if not _redis_ready or not _redis_impl:
|
|
1125
|
+
raise Exception("Redis not ready")
|
|
1126
|
+
key = params.get("key")
|
|
1127
|
+
seconds = params.get("seconds")
|
|
1128
|
+
if not key or seconds is None:
|
|
1129
|
+
raise Exception("Missing key or seconds parameter")
|
|
1130
|
+
result = await _redis_impl.expire(key, int(seconds))
|
|
1131
|
+
return {"result": result}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
async def _rpc_redis_hdel(params: dict) -> dict:
|
|
1135
|
+
"""RPC: redis.hdel"""
|
|
1136
|
+
if not _redis_ready or not _redis_impl:
|
|
1137
|
+
raise Exception("Redis not ready")
|
|
1138
|
+
key = params.get("key")
|
|
1139
|
+
field = params.get("field")
|
|
1140
|
+
if not key or not field:
|
|
1141
|
+
raise Exception("Missing key or field parameter")
|
|
1142
|
+
deleted = await _redis_impl.hdel(key, field)
|
|
1143
|
+
return {"deleted": deleted}
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
async def _rpc_redis_hgetall(params: dict) -> dict:
|
|
1147
|
+
"""RPC: redis.hgetall"""
|
|
1148
|
+
if not _redis_ready or not _redis_impl:
|
|
1149
|
+
raise Exception("Redis not ready")
|
|
1150
|
+
key = params.get("key")
|
|
1151
|
+
if not key:
|
|
1152
|
+
raise Exception("Missing key parameter")
|
|
1153
|
+
result = await _redis_impl.hgetall(key)
|
|
1154
|
+
# 内置返回 list [k,v,k,v], 外部返回 dict — 统一为 dict
|
|
1155
|
+
if isinstance(result, list):
|
|
1156
|
+
d = {}
|
|
1157
|
+
for i in range(0, len(result), 2):
|
|
1158
|
+
d[result[i]] = result[i + 1]
|
|
1159
|
+
return {"key": key, "data": d}
|
|
1160
|
+
return {"key": key, "data": result if isinstance(result, dict) else {}}
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
async def _rpc_redis_sadd(params: dict) -> dict:
|
|
1164
|
+
"""RPC: redis.sadd"""
|
|
1165
|
+
if not _redis_ready or not _redis_impl:
|
|
1166
|
+
raise Exception("Redis not ready")
|
|
1167
|
+
key = params.get("key")
|
|
1168
|
+
members = params.get("members") or []
|
|
1169
|
+
member = params.get("member")
|
|
1170
|
+
if member and not members:
|
|
1171
|
+
members = [member]
|
|
1172
|
+
if not key or not members:
|
|
1173
|
+
raise Exception("Missing key or member/members parameter")
|
|
1174
|
+
added = await _redis_impl.sadd(key, *members)
|
|
1175
|
+
return {"added": added}
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
async def _rpc_redis_srem(params: dict) -> dict:
|
|
1179
|
+
"""RPC: redis.srem"""
|
|
1180
|
+
if not _redis_ready or not _redis_impl:
|
|
1181
|
+
raise Exception("Redis not ready")
|
|
1182
|
+
key = params.get("key")
|
|
1183
|
+
members = params.get("members") or []
|
|
1184
|
+
member = params.get("member")
|
|
1185
|
+
if member and not members:
|
|
1186
|
+
members = [member]
|
|
1187
|
+
if not key or not members:
|
|
1188
|
+
raise Exception("Missing key or member/members parameter")
|
|
1189
|
+
removed = await _redis_impl.srem(key, *members)
|
|
1190
|
+
return {"removed": removed}
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
async def _rpc_redis_smembers(params: dict) -> dict:
|
|
1194
|
+
"""RPC: redis.smembers"""
|
|
1195
|
+
if not _redis_ready or not _redis_impl:
|
|
1196
|
+
raise Exception("Redis not ready")
|
|
1197
|
+
key = params.get("key")
|
|
1198
|
+
if not key:
|
|
1199
|
+
raise Exception("Missing key parameter")
|
|
1200
|
+
members = await _redis_impl.smembers(key)
|
|
1201
|
+
return {"key": key, "members": list(members) if members else []}
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
async def _rpc_redis_publish(params: dict) -> dict:
|
|
1205
|
+
"""RPC: redis.publish"""
|
|
1206
|
+
if not _redis_ready or not _redis_impl:
|
|
1207
|
+
raise Exception("Redis not ready")
|
|
1208
|
+
channel = params.get("channel")
|
|
1209
|
+
message = params.get("message")
|
|
1210
|
+
if not channel or message is None:
|
|
1211
|
+
raise Exception("Missing channel or message parameter")
|
|
1212
|
+
receivers = await _redis_impl.publish(channel, message)
|
|
1213
|
+
return {"receivers": receivers}
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
async def _handle_connection_offer(data):
|
|
1217
|
+
"""处理 Kernel 下发的 slot token,建立附加连接。"""
|
|
1218
|
+
slots = data.get("slots", {})
|
|
1219
|
+
for slot_str, info in slots.items():
|
|
1220
|
+
slot = int(slot_str)
|
|
1221
|
+
token = info.get("token", "")
|
|
1222
|
+
if not token or slot in _extra_ws:
|
|
1223
|
+
continue
|
|
1224
|
+
asyncio.create_task(_connect_slot(slot, token))
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
async def _connect_slot(slot, token):
|
|
1228
|
+
"""建立单个 slot 附加连接。"""
|
|
1229
|
+
ws_url = f"ws://127.0.0.1:{_kernel_port}/ws"
|
|
1230
|
+
try:
|
|
1231
|
+
ws = await websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=5)
|
|
1232
|
+
auth_req = {"jsonrpc": "2.0", "id": f"auth-slot-{slot}", "method": "auth", "params": {"token": token}}
|
|
1233
|
+
await ws.send(json.dumps(auth_req))
|
|
1234
|
+
resp = json.loads(await asyncio.wait_for(ws.recv(), timeout=5))
|
|
1235
|
+
if "error" in resp:
|
|
1236
|
+
await ws.close()
|
|
1237
|
+
return
|
|
1238
|
+
_extra_ws[slot] = ws
|
|
1239
|
+
_extra_ws_tasks[slot] = asyncio.create_task(_slot_recv_loop(slot, ws))
|
|
1240
|
+
print(f"[redis] Slot {slot} connected")
|
|
1241
|
+
except Exception as e:
|
|
1242
|
+
print(f"[redis] Slot {slot} connect failed: {e}")
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
async def _slot_recv_loop(slot, ws):
|
|
1246
|
+
"""附加连接的接收循环:与主连接平等处理所有消息。"""
|
|
1247
|
+
try:
|
|
1248
|
+
async for raw in ws:
|
|
1249
|
+
try:
|
|
1250
|
+
msg = json.loads(raw)
|
|
1251
|
+
except (json.JSONDecodeError, TypeError):
|
|
1252
|
+
continue
|
|
1253
|
+
try:
|
|
1254
|
+
has_method = "method" in msg
|
|
1255
|
+
has_id = "id" in msg
|
|
1256
|
+
|
|
1257
|
+
if has_method and not has_id:
|
|
1258
|
+
asyncio.create_task(_handle_event_notification(msg))
|
|
1259
|
+
elif has_method and has_id:
|
|
1260
|
+
asyncio.create_task(_handle_rpc_request(ws, msg))
|
|
1261
|
+
elif not has_method and has_id:
|
|
1262
|
+
_handle_rpc_response(msg)
|
|
1263
|
+
except Exception as e:
|
|
1264
|
+
print(f"[redis] Slot {slot} 消息处理异常(已忽略): {e}")
|
|
1265
|
+
except Exception as e:
|
|
1266
|
+
print(f"[redis] Slot {slot} 接收循环异常: {e}")
|
|
1267
|
+
finally:
|
|
1268
|
+
_extra_ws.pop(slot, None)
|
|
1269
|
+
_extra_ws_tasks.pop(slot, None)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
async def _handle_connection_release(data):
|
|
1273
|
+
"""Kernel 请求释放 slot,优雅关闭。"""
|
|
1274
|
+
for slot in data.get("slots", []):
|
|
1275
|
+
ws = _extra_ws.pop(slot, None)
|
|
1276
|
+
task = _extra_ws_tasks.pop(slot, None)
|
|
1277
|
+
if ws:
|
|
1278
|
+
try:
|
|
1279
|
+
await ws.close(code=1000, reason="release")
|
|
1280
|
+
except Exception:
|
|
1281
|
+
pass
|
|
1282
|
+
if task:
|
|
1283
|
+
task.cancel()
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
async def _handle_shutdown():
|
|
1287
|
+
"""Handle module.shutdown event — ack → exiting → cleanup → ready → exit."""
|
|
1288
|
+
global _shutting_down
|
|
1289
|
+
print("[redis] Received shutdown request")
|
|
1290
|
+
_shutting_down = True
|
|
1291
|
+
# Step 1: Send ack (立即确认收到)
|
|
1292
|
+
await _publish_event(_ws_global, "module.shutdown.ack", {"module_id": "redis"})
|
|
1293
|
+
# Step 2: Send module.exiting (开始清理)
|
|
1294
|
+
await _publish_event(_ws_global, "module.exiting", {
|
|
1295
|
+
"module_id": "redis",
|
|
1296
|
+
"type": "passive",
|
|
1297
|
+
"reason": "shutdown_requested",
|
|
1298
|
+
"restart": "auto",
|
|
1299
|
+
"action": "none",
|
|
1300
|
+
"timeout": 2.0,
|
|
1301
|
+
"restart_delay": 0.0,
|
|
1302
|
+
})
|
|
1303
|
+
# Step 3: Cleanup - 关闭 Redis 实现
|
|
1304
|
+
if _redis_impl:
|
|
1305
|
+
try:
|
|
1306
|
+
await _redis_impl.close()
|
|
1307
|
+
print("[redis] Redis implementation closed")
|
|
1308
|
+
except Exception as e:
|
|
1309
|
+
print(f"[redis] Failed to close Redis: {e}")
|
|
1310
|
+
|
|
1311
|
+
# 关闭所有附加连接
|
|
1312
|
+
for _s, _w in list(_extra_ws.items()):
|
|
1313
|
+
try:
|
|
1314
|
+
await _w.close(code=1000, reason="shutdown")
|
|
1315
|
+
except Exception:
|
|
1316
|
+
pass
|
|
1317
|
+
for _t in _extra_ws_tasks.values():
|
|
1318
|
+
_t.cancel()
|
|
1319
|
+
_extra_ws.clear()
|
|
1320
|
+
_extra_ws_tasks.clear()
|
|
1321
|
+
# Step 4: Send ready (清理完成)
|
|
1322
|
+
await _publish_event(_ws_global, "module.shutdown.ready", {"module_id": "redis"})
|
|
1323
|
+
print("[redis] Shutdown ready, exiting")
|
|
1324
|
+
|
|
1325
|
+
# 等待 Kernel 处理 shutdown.ready(防止 Close 帧抢先)
|
|
1326
|
+
await asyncio.sleep(0.1)
|
|
1327
|
+
|
|
1328
|
+
# Step 5: Close WebSocket connection gracefully
|
|
1329
|
+
if _ws_global:
|
|
1330
|
+
try:
|
|
1331
|
+
await _ws_global.close(code=1000, reason="Graceful shutdown")
|
|
1332
|
+
print("[redis] WebSocket closed")
|
|
1333
|
+
except Exception as e:
|
|
1334
|
+
print(f"[redis] Failed to close WebSocket: {e}")
|
|
1335
|
+
|
|
1336
|
+
# Note: Do NOT call sys.exit() in async context
|
|
1337
|
+
# Let the event loop naturally complete
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
async def _test_event_loop(ws):
|
|
1341
|
+
"""Publish a test event every 10 seconds."""
|
|
1342
|
+
try:
|
|
1343
|
+
while True:
|
|
1344
|
+
await asyncio.sleep(10)
|
|
1345
|
+
await _publish_event(ws, "redis.test", {
|
|
1346
|
+
"message": "test event from redis",
|
|
1347
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1348
|
+
})
|
|
1349
|
+
except Exception:
|
|
1350
|
+
pass
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
_startup_config = {} # populated by _validate_and_prepare()
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _validate_and_prepare() -> dict:
|
|
1357
|
+
"""同步启动验证:日志初始化、读取 token 和 kernel_port。
|
|
1358
|
+
失败直接 sys.exit(1),成功返回启动参数。在 asyncio.run() 之前调用。"""
|
|
1359
|
+
t0 = time.monotonic()
|
|
1360
|
+
kernel_port = os.environ.get("KITE_KERNEL_PORT")
|
|
1361
|
+
if kernel_port:
|
|
1362
|
+
return _prepare_local_mode(int(kernel_port), t0)
|
|
1363
|
+
config = _load_module_config()
|
|
1364
|
+
gateway_url = config.get("gateway_url") or os.environ.get("KITE_GATEWAY_URL")
|
|
1365
|
+
if gateway_url:
|
|
1366
|
+
return _prepare_remote_mode(gateway_url, t0)
|
|
1367
|
+
print(f"[{MODULE_NAME}] ERROR: No KITE_KERNEL_PORT and no gateway_url")
|
|
1368
|
+
sys.exit(1)
|
|
1369
|
+
|
|
1370
|
+
def _prepare_local_mode(kernel_port: int, t0: float) -> dict:
|
|
1371
|
+
global _log_dir, _log_latest_path, _crash_log_path
|
|
1372
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
1373
|
+
if module_data:
|
|
1374
|
+
_log_dir = os.path.join(module_data, "log")
|
|
1375
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
1376
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
1377
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
1378
|
+
try:
|
|
1379
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f: pass
|
|
1380
|
+
except Exception: _log_latest_path = None
|
|
1381
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
1382
|
+
try:
|
|
1383
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f: pass
|
|
1384
|
+
except Exception: _crash_log_path = None
|
|
1385
|
+
_resolve_daily_log_path()
|
|
1386
|
+
_setup_exception_hooks()
|
|
1387
|
+
line = sys.stdin.readline().strip()
|
|
1388
|
+
if not line:
|
|
1389
|
+
print(f"[{MODULE_NAME}] ERROR: stdin closed")
|
|
1390
|
+
sys.exit(1)
|
|
1391
|
+
try: msg = json.loads(line)
|
|
1392
|
+
except json.JSONDecodeError as e:
|
|
1393
|
+
print(f"[{MODULE_NAME}] ERROR: Invalid JSON: {e}")
|
|
1394
|
+
sys.exit(1)
|
|
1395
|
+
if "error" in msg:
|
|
1396
|
+
print(f"[{MODULE_NAME}] 启动失败: {msg.get('message')}")
|
|
1397
|
+
sys.exit(1)
|
|
1398
|
+
token = msg.get("token", "")
|
|
1399
|
+
if not token:
|
|
1400
|
+
print(f"[{MODULE_NAME}] ERROR: No token")
|
|
1401
|
+
sys.exit(1)
|
|
1402
|
+
print(f"[{MODULE_NAME}] Local mode: port={kernel_port}")
|
|
1403
|
+
return {"mode": "local", "token": token, "kernel_port": kernel_port, "t0": t0}
|
|
1404
|
+
|
|
1405
|
+
def _prepare_remote_mode(gateway_url: str, t0: float) -> dict:
|
|
1406
|
+
global _log_dir, _log_latest_path, _crash_log_path
|
|
1407
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
1408
|
+
data_dir = os.path.join(home, ".kite", "remote", MODULE_NAME)
|
|
1409
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
1410
|
+
_log_dir = os.path.join(data_dir, "log")
|
|
1411
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
1412
|
+
_log_latest_path = os.path.join(_log_dir, "latest.log")
|
|
1413
|
+
try:
|
|
1414
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f: pass
|
|
1415
|
+
except Exception: _log_latest_path = None
|
|
1416
|
+
_crash_log_path = os.path.join(_log_dir, "crashes.jsonl")
|
|
1417
|
+
try:
|
|
1418
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f: pass
|
|
1419
|
+
except Exception: _crash_log_path = None
|
|
1420
|
+
_resolve_daily_log_path()
|
|
1421
|
+
_setup_exception_hooks()
|
|
1422
|
+
kite_token = _get_kite_token(MODULE_NAME, gateway_url)
|
|
1423
|
+
print(f"[{MODULE_NAME}] Remote mode: gateway={gateway_url}")
|
|
1424
|
+
return {"mode": "remote", "gateway_url": gateway_url, "kite_token": kite_token, "t0": t0}
|
|
1425
|
+
|
|
1426
|
+
def _gateway_to_filename(gateway_url: str) -> str:
|
|
1427
|
+
try:
|
|
1428
|
+
from urllib.parse import urlparse
|
|
1429
|
+
parsed = urlparse(gateway_url)
|
|
1430
|
+
host = parsed.hostname or "unknown"
|
|
1431
|
+
port = parsed.port or (443 if parsed.scheme == "wss" else 80)
|
|
1432
|
+
return f"{host}-{port}.json".replace(":", "-").replace("/", "-")
|
|
1433
|
+
except Exception: return "default.json"
|
|
1434
|
+
|
|
1435
|
+
def _load_token_cache(module_name: str, gateway_url: str) -> dict | None:
|
|
1436
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
1437
|
+
token_file = os.path.join(home, ".kite", "remote", module_name, "tokens", _gateway_to_filename(gateway_url))
|
|
1438
|
+
if not os.path.exists(token_file): return None
|
|
1439
|
+
try:
|
|
1440
|
+
with open(token_file, "r") as f: return json.load(f)
|
|
1441
|
+
except Exception: return None
|
|
1442
|
+
|
|
1443
|
+
def _clear_token_cache(module_name: str, gateway_url: str):
|
|
1444
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
1445
|
+
token_file = os.path.join(home, ".kite", "remote", module_name, "tokens", _gateway_to_filename(gateway_url))
|
|
1446
|
+
if os.path.exists(token_file):
|
|
1447
|
+
try: os.remove(token_file)
|
|
1448
|
+
except Exception: pass
|
|
1449
|
+
|
|
1450
|
+
def _get_kite_token(module_name: str, gateway_url: str) -> str:
|
|
1451
|
+
token = os.environ.get("KITE_TOKEN")
|
|
1452
|
+
if token: return token
|
|
1453
|
+
cache = _load_token_cache(module_name, gateway_url)
|
|
1454
|
+
if cache: return cache["token"]
|
|
1455
|
+
print(f"[{module_name}] No token for {gateway_url}")
|
|
1456
|
+
print(f" export KITE_TOKEN=<token>")
|
|
1457
|
+
sys.exit(1)
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
async def _remote_mode_loop(gateway_url: str, kite_token: str, _t0: float):
|
|
1461
|
+
global _shutting_down, _exit_code
|
|
1462
|
+
retry_count = 0
|
|
1463
|
+
while not _shutting_down and retry_count < 10:
|
|
1464
|
+
try:
|
|
1465
|
+
await _remote_connect(gateway_url, kite_token, _t0)
|
|
1466
|
+
retry_count = 0
|
|
1467
|
+
except asyncio.CancelledError: break
|
|
1468
|
+
except Exception as e:
|
|
1469
|
+
if _shutting_down: break
|
|
1470
|
+
retry_count += 1
|
|
1471
|
+
await asyncio.sleep(min(2 ** retry_count, 60))
|
|
1472
|
+
if retry_count >= 10: _exit_code = 1
|
|
1473
|
+
|
|
1474
|
+
async def _remote_connect(gateway_url: str, kite_token: str, _t0: float):
|
|
1475
|
+
global _ws_global
|
|
1476
|
+
async with websockets.connect(gateway_url, open_timeout=10, ping_interval=30) as ws:
|
|
1477
|
+
challenge = json.loads(await ws.recv())
|
|
1478
|
+
auth_req = {"jsonrpc": "2.0", "id": "auth", "method": "auth.connect",
|
|
1479
|
+
"params": {"nonce": challenge["params"]["nonce"], "module_id": MODULE_NAME,
|
|
1480
|
+
"auth": {"method": "kite_token", "token": kite_token},
|
|
1481
|
+
"client": {"type": "module", "platform": sys.platform}}}
|
|
1482
|
+
await ws.send(json.dumps(auth_req))
|
|
1483
|
+
hello_resp = json.loads(await ws.recv())
|
|
1484
|
+
if "error" in hello_resp:
|
|
1485
|
+
error = hello_resp["error"]
|
|
1486
|
+
if error.get("code") in (4001, 4010, 4011):
|
|
1487
|
+
_clear_token_cache(MODULE_NAME, gateway_url)
|
|
1488
|
+
print(f"[{MODULE_NAME}] Token invalid, re-authenticate")
|
|
1489
|
+
sys.exit(1)
|
|
1490
|
+
raise Exception(f"Connection failed: {error}")
|
|
1491
|
+
print(f"[{MODULE_NAME}] Connected via Gateway")
|
|
1492
|
+
_ws_global = ws
|
|
1493
|
+
|
|
1494
|
+
# Start receiver task — wait for system.require_init to trigger _do_init
|
|
1495
|
+
receiver_task = asyncio.create_task(_ws_receiver(ws))
|
|
1496
|
+
try:
|
|
1497
|
+
await receiver_task
|
|
1498
|
+
finally:
|
|
1499
|
+
receiver_task.cancel()
|
|
1500
|
+
for fut in _pending_rpc.values():
|
|
1501
|
+
if not fut.done():
|
|
1502
|
+
fut.set_result({"error": {"code": -32001, "message": "Connection lost"}})
|
|
1503
|
+
_pending_rpc.clear()
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
if __name__ == "__main__":
|
|
1507
|
+
_startup_config = _validate_and_prepare()
|
|
1508
|
+
asyncio.run(main())
|
|
1509
|
+
sys.exit(_exit_code)
|