@silicaclaw/cli 2026.3.19-9 → 2026.3.20-10
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/CHANGELOG.md +182 -0
- package/DEMO_GUIDE.md +1 -1
- package/INSTALL.md +53 -13
- package/README.md +106 -23
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +180 -14
- package/apps/local-console/dist/apps/local-console/src/server.js +1499 -267
- package/apps/local-console/dist/config/silicaclaw-defaults.json +19 -0
- package/apps/local-console/dist/packages/core/src/index.d.ts +2 -0
- package/apps/local-console/dist/packages/core/src/index.js +2 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.js +40 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.js +74 -0
- package/apps/local-console/dist/packages/core/src/profile.js +2 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/apps/local-console/dist/packages/core/src/socialConfig.js +9 -5
- package/apps/local-console/dist/packages/core/src/types.d.ts +40 -0
- package/apps/local-console/dist/packages/network/src/realPreview.js +6 -2
- package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.js +116 -10
- package/apps/local-console/dist/packages/network/src/transport/udpLanBroadcastTransport.js +2 -1
- package/apps/local-console/dist/packages/network/src/types.d.ts +4 -0
- package/apps/local-console/dist/packages/network/src/webrtcPreview.js +5 -1
- package/apps/local-console/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
- package/apps/local-console/dist/packages/storage/src/repos.d.ts +13 -1
- package/apps/local-console/dist/packages/storage/src/repos.js +19 -1
- package/apps/local-console/dist/packages/storage/src/socialRuntimeRepo.js +8 -4
- package/apps/local-console/public/app/app.js +486 -12
- package/apps/local-console/public/app/events.js +61 -2
- package/apps/local-console/public/app/network.js +176 -35
- package/apps/local-console/public/app/overview.js +75 -53
- package/apps/local-console/public/app/shell.js +18 -34
- package/apps/local-console/public/app/social.js +495 -93
- package/apps/local-console/public/app/styles.css +309 -15
- package/apps/local-console/public/app/template.js +182 -51
- package/apps/local-console/public/app/translations.js +476 -266
- package/apps/local-console/src/server.ts +1669 -271
- package/apps/public-explorer/dist/apps/public-explorer/src/server.d.ts +1 -0
- package/apps/public-explorer/dist/apps/public-explorer/src/server.js +41 -0
- package/apps/public-explorer/dist/config/silicaclaw-defaults.json +19 -0
- package/apps/public-explorer/public/app/app.js +22 -2
- package/apps/public-explorer/public/app/template.js +4 -4
- package/apps/public-explorer/public/app/translations.js +29 -29
- package/apps/public-explorer/src/server.ts +11 -1
- package/config/silicaclaw-defaults.json +19 -0
- package/dist/apps/local-console/src/server.d.ts +1 -0
- package/dist/apps/local-console/src/server.js +555 -0
- package/docs/NEW_USER_INSTALL.md +14 -10
- package/docs/NEW_USER_OPERATIONS.md +9 -9
- package/docs/OPENCLAW_BRIDGE.md +22 -7
- package/docs/OPENCLAW_BRIDGE_ZH.md +21 -6
- package/docs/RELEASE_CHECKLIST.md +95 -0
- package/node_modules/@silicaclaw/core/dist/config/silicaclaw-defaults.json +19 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/crypto.d.ts +6 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/crypto.js +50 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/directory.d.ts +17 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/directory.js +145 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/identity.d.ts +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/identity.js +18 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.d.ts +14 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.js +30 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/indexing.d.ts +6 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/indexing.js +43 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/presence.d.ts +4 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/presence.js +23 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.d.ts +4 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.js +41 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.js +106 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialConfig.d.ts +100 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialConfig.js +300 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialMessage.d.ts +19 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialMessage.js +69 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialResolver.d.ts +46 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialResolver.js +237 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialTemplate.d.ts +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/socialTemplate.js +90 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/types.d.ts +99 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/types.js +2 -0
- package/node_modules/@silicaclaw/core/src/index.ts +2 -0
- package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
- package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
- package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
- package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
- package/node_modules/@silicaclaw/core/src/socialConfig.ts +7 -5
- package/node_modules/@silicaclaw/core/src/types.ts +44 -0
- package/node_modules/@silicaclaw/network/dist/config/silicaclaw-defaults.json +19 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/messageEnvelope.d.ts +28 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/messageEnvelope.js +36 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/peerDiscovery.d.ts +43 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/peerDiscovery.js +2 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/topicCodec.d.ts +4 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/topicCodec.js +2 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/transport.d.ts +36 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/transport.js +2 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.d.ts +5 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.js +24 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonTopicCodec.d.ts +5 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonTopicCodec.js +12 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.d.ts +28 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.js +144 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/index.d.ts +14 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/index.js +30 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/localEventBus.d.ts +9 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/localEventBus.js +47 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/mock.d.ts +8 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/mock.js +24 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/realPreview.d.ts +105 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/realPreview.js +331 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +178 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +548 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/transport/udpLanBroadcastTransport.d.ts +23 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/transport/udpLanBroadcastTransport.js +154 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +10 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/types.js +2 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/webrtcPreview.d.ts +163 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/webrtcPreview.js +848 -0
- package/node_modules/@silicaclaw/network/src/realPreview.ts +3 -2
- package/node_modules/@silicaclaw/network/src/relayPreview.ts +125 -12
- package/node_modules/@silicaclaw/network/src/transport/udpLanBroadcastTransport.ts +2 -1
- package/node_modules/@silicaclaw/network/src/types.ts +2 -0
- package/node_modules/@silicaclaw/network/src/webrtcPreview.ts +2 -1
- package/node_modules/@silicaclaw/storage/config/silicaclaw-defaults.json +19 -0
- package/node_modules/@silicaclaw/storage/dist/config/silicaclaw-defaults.json +19 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/crypto.d.ts +6 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/crypto.js +50 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/directory.d.ts +17 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/directory.js +145 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/identity.d.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/identity.js +18 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +14 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +30 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/indexing.d.ts +6 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/indexing.js +43 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/presence.d.ts +4 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/presence.js +23 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.d.ts +4 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +41 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +106 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialConfig.d.ts +100 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialConfig.js +300 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialMessage.d.ts +19 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialMessage.js +69 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialResolver.d.ts +46 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialResolver.js +237 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialTemplate.d.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialTemplate.js +90 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +99 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.js +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/index.d.ts +3 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/index.js +19 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/jsonRepo.d.ts +7 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/jsonRepo.js +29 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +73 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +85 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/socialRuntimeRepo.d.ts +5 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/socialRuntimeRepo.js +57 -0
- package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.js +8 -4
- package/node_modules/@silicaclaw/storage/src/repos.ts +31 -1
- package/node_modules/@silicaclaw/storage/src/socialRuntimeRepo.ts +5 -4
- package/node_modules/@silicaclaw/storage/tsconfig.json +1 -6
- package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +165 -0
- package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -0
- package/openclaw-skills/silicaclaw-bridge-setup/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +27 -0
- package/openclaw-skills/silicaclaw-bridge-setup/references/owner-dialogue-cheatsheet-zh.md +58 -0
- package/openclaw-skills/silicaclaw-bridge-setup/references/runtime-setup.md +43 -0
- package/openclaw-skills/silicaclaw-bridge-setup/references/troubleshooting.md +24 -0
- package/openclaw-skills/silicaclaw-broadcast/SKILL.md +150 -0
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/agents/openai.yaml +2 -2
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +4 -3
- package/openclaw-skills/silicaclaw-broadcast/references/owner-dialogue-cheatsheet-zh.md +81 -0
- package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
- package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
- package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
- package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
- package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
- package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
- package/openclaw-skills/silicaclaw-owner-push/SKILL.md +235 -0
- package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -0
- package/openclaw-skills/silicaclaw-owner-push/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-owner-push/manifest.json +30 -0
- package/openclaw-skills/silicaclaw-owner-push/references/owner-dialogue-cheatsheet-zh.md +87 -0
- package/openclaw-skills/silicaclaw-owner-push/references/push-routing-policy.md +43 -0
- package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +44 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +356 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/send-to-owner-via-openclaw.mjs +69 -0
- package/package.json +5 -1
- package/packages/core/dist/config/silicaclaw-defaults.json +19 -0
- package/packages/core/dist/packages/core/src/crypto.d.ts +6 -0
- package/packages/core/dist/packages/core/src/crypto.js +50 -0
- package/packages/core/dist/packages/core/src/directory.d.ts +17 -0
- package/packages/core/dist/packages/core/src/directory.js +145 -0
- package/packages/core/dist/packages/core/src/identity.d.ts +2 -0
- package/packages/core/dist/packages/core/src/identity.js +18 -0
- package/packages/core/dist/packages/core/src/index.d.ts +14 -0
- package/packages/core/dist/packages/core/src/index.js +30 -0
- package/packages/core/dist/packages/core/src/indexing.d.ts +6 -0
- package/packages/core/dist/packages/core/src/indexing.js +43 -0
- package/packages/core/dist/packages/core/src/presence.d.ts +4 -0
- package/packages/core/dist/packages/core/src/presence.js +23 -0
- package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/core/dist/packages/core/src/profile.d.ts +4 -0
- package/packages/core/dist/packages/core/src/profile.js +41 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.js +106 -0
- package/packages/core/dist/packages/core/src/socialConfig.d.ts +100 -0
- package/packages/core/dist/packages/core/src/socialConfig.js +300 -0
- package/packages/core/dist/packages/core/src/socialMessage.d.ts +19 -0
- package/packages/core/dist/packages/core/src/socialMessage.js +69 -0
- package/packages/core/dist/packages/core/src/socialResolver.d.ts +46 -0
- package/packages/core/dist/packages/core/src/socialResolver.js +237 -0
- package/packages/core/dist/packages/core/src/socialTemplate.d.ts +2 -0
- package/packages/core/dist/packages/core/src/socialTemplate.js +90 -0
- package/packages/core/dist/packages/core/src/types.d.ts +99 -0
- package/packages/core/dist/packages/core/src/types.js +2 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/privateCrypto.ts +57 -0
- package/packages/core/src/privateMessage.ts +101 -0
- package/packages/core/src/profile.ts +2 -0
- package/packages/core/src/publicProfileSummary.ts +7 -0
- package/packages/core/src/socialConfig.ts +7 -5
- package/packages/core/src/types.ts +44 -0
- package/packages/network/dist/config/silicaclaw-defaults.json +19 -0
- package/packages/network/dist/packages/network/src/abstractions/messageEnvelope.d.ts +28 -0
- package/packages/network/dist/packages/network/src/abstractions/messageEnvelope.js +36 -0
- package/packages/network/dist/packages/network/src/abstractions/peerDiscovery.d.ts +43 -0
- package/packages/network/dist/packages/network/src/abstractions/peerDiscovery.js +2 -0
- package/packages/network/dist/packages/network/src/abstractions/topicCodec.d.ts +4 -0
- package/packages/network/dist/packages/network/src/abstractions/topicCodec.js +2 -0
- package/packages/network/dist/packages/network/src/abstractions/transport.d.ts +36 -0
- package/packages/network/dist/packages/network/src/abstractions/transport.js +2 -0
- package/packages/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.d.ts +5 -0
- package/packages/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.js +24 -0
- package/packages/network/dist/packages/network/src/codec/jsonTopicCodec.d.ts +5 -0
- package/packages/network/dist/packages/network/src/codec/jsonTopicCodec.js +12 -0
- package/packages/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.d.ts +28 -0
- package/packages/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.js +144 -0
- package/packages/network/dist/packages/network/src/index.d.ts +14 -0
- package/packages/network/dist/packages/network/src/index.js +30 -0
- package/packages/network/dist/packages/network/src/localEventBus.d.ts +9 -0
- package/packages/network/dist/packages/network/src/localEventBus.js +47 -0
- package/packages/network/dist/packages/network/src/mock.d.ts +8 -0
- package/packages/network/dist/packages/network/src/mock.js +24 -0
- package/packages/network/dist/packages/network/src/realPreview.d.ts +105 -0
- package/packages/network/dist/packages/network/src/realPreview.js +331 -0
- package/packages/network/dist/packages/network/src/relayPreview.d.ts +178 -0
- package/packages/network/dist/packages/network/src/relayPreview.js +548 -0
- package/packages/network/dist/packages/network/src/transport/udpLanBroadcastTransport.d.ts +23 -0
- package/packages/network/dist/packages/network/src/transport/udpLanBroadcastTransport.js +154 -0
- package/packages/network/dist/packages/network/src/types.d.ts +10 -0
- package/packages/network/dist/packages/network/src/types.js +2 -0
- package/packages/network/dist/packages/network/src/webrtcPreview.d.ts +163 -0
- package/packages/network/dist/packages/network/src/webrtcPreview.js +848 -0
- package/packages/network/src/realPreview.ts +3 -2
- package/packages/network/src/relayPreview.ts +125 -12
- package/packages/network/src/transport/udpLanBroadcastTransport.ts +2 -1
- package/packages/network/src/types.ts +2 -0
- package/packages/network/src/webrtcPreview.ts +2 -1
- package/packages/storage/config/silicaclaw-defaults.json +19 -0
- package/packages/storage/dist/config/silicaclaw-defaults.json +19 -0
- package/packages/storage/dist/packages/core/src/crypto.d.ts +6 -0
- package/packages/storage/dist/packages/core/src/crypto.js +50 -0
- package/packages/storage/dist/packages/core/src/directory.d.ts +17 -0
- package/packages/storage/dist/packages/core/src/directory.js +145 -0
- package/packages/storage/dist/packages/core/src/identity.d.ts +2 -0
- package/packages/storage/dist/packages/core/src/identity.js +18 -0
- package/packages/storage/dist/packages/core/src/index.d.ts +14 -0
- package/packages/storage/dist/packages/core/src/index.js +30 -0
- package/packages/storage/dist/packages/core/src/indexing.d.ts +6 -0
- package/packages/storage/dist/packages/core/src/indexing.js +43 -0
- package/packages/storage/dist/packages/core/src/presence.d.ts +4 -0
- package/packages/storage/dist/packages/core/src/presence.js +23 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/storage/dist/packages/core/src/profile.d.ts +4 -0
- package/packages/storage/dist/packages/core/src/profile.js +41 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.js +106 -0
- package/packages/storage/dist/packages/core/src/socialConfig.d.ts +100 -0
- package/packages/storage/dist/packages/core/src/socialConfig.js +300 -0
- package/packages/storage/dist/packages/core/src/socialMessage.d.ts +19 -0
- package/packages/storage/dist/packages/core/src/socialMessage.js +69 -0
- package/packages/storage/dist/packages/core/src/socialResolver.d.ts +46 -0
- package/packages/storage/dist/packages/core/src/socialResolver.js +237 -0
- package/packages/storage/dist/packages/core/src/socialTemplate.d.ts +2 -0
- package/packages/storage/dist/packages/core/src/socialTemplate.js +90 -0
- package/packages/storage/dist/packages/core/src/types.d.ts +99 -0
- package/packages/storage/dist/packages/core/src/types.js +2 -0
- package/packages/storage/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
- package/packages/storage/dist/packages/storage/src/index.d.ts +3 -0
- package/packages/storage/dist/packages/storage/src/index.js +19 -0
- package/packages/storage/dist/packages/storage/src/jsonRepo.d.ts +7 -0
- package/packages/storage/dist/packages/storage/src/jsonRepo.js +29 -0
- package/packages/storage/dist/packages/storage/src/repos.d.ts +73 -0
- package/packages/storage/dist/packages/storage/src/repos.js +85 -0
- package/packages/storage/dist/packages/storage/src/socialRuntimeRepo.d.ts +5 -0
- package/packages/storage/dist/packages/storage/src/socialRuntimeRepo.js +57 -0
- package/packages/storage/dist/socialRuntimeRepo.js +8 -4
- package/packages/storage/src/repos.ts +31 -1
- package/packages/storage/src/socialRuntimeRepo.ts +5 -4
- package/packages/storage/tsconfig.json +1 -6
- package/scripts/functional-check.mjs +35 -6
- package/scripts/install-openclaw-skill.mjs +9 -2
- package/scripts/openclaw-bridge-adapter.mjs +3 -1
- package/scripts/openclaw-bridge-client.mjs +3 -1
- package/scripts/openclaw-runtime-demo.mjs +3 -1
- package/scripts/quickstart.sh +14 -10
- package/scripts/release-pack.mjs +59 -1
- package/scripts/silicaclaw-cli.mjs +166 -51
- package/scripts/silicaclaw-gateway.mjs +410 -84
- package/scripts/validate-openclaw-skill.mjs +98 -21
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
-
import { execFile, spawnSync } from "child_process";
|
|
3
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
|
-
import { hostname } from "os";
|
|
7
|
+
import { homedir, hostname } from "os";
|
|
8
8
|
import { promisify } from "util";
|
|
9
|
+
import defaults from "../../../config/silicaclaw-defaults.json";
|
|
9
10
|
import {
|
|
10
11
|
AgentIdentity,
|
|
11
12
|
DirectoryState,
|
|
12
13
|
IndexRefRecord,
|
|
14
|
+
PrivateEncryptionKeyPair,
|
|
15
|
+
PrivateMessageReceiptRecord,
|
|
16
|
+
PrivateMessageRecord,
|
|
13
17
|
PresenceRecord,
|
|
14
18
|
ProfileInput,
|
|
15
19
|
PublicProfile,
|
|
16
20
|
PublicProfileSummary,
|
|
17
21
|
SignedProfileRecord,
|
|
18
22
|
buildPublicProfileSummary,
|
|
19
|
-
buildIndexRecords,
|
|
20
23
|
cleanupExpiredPresence,
|
|
21
24
|
createDefaultProfileInput,
|
|
22
25
|
createEmptyDirectoryState,
|
|
23
26
|
createIdentity,
|
|
27
|
+
createPrivateEncryptionKeyPair,
|
|
24
28
|
dedupeIndex,
|
|
29
|
+
decryptPrivatePayload,
|
|
25
30
|
ensureDefaultSocialMd,
|
|
31
|
+
encryptPrivatePayload,
|
|
26
32
|
ingestIndexRecord,
|
|
27
33
|
ingestPresenceRecord,
|
|
28
34
|
ingestProfileRecord,
|
|
29
35
|
isAgentOnline,
|
|
36
|
+
rebuildIndexForProfile,
|
|
30
37
|
loadSocialConfig,
|
|
31
38
|
getSocialConfigSearchPaths,
|
|
32
39
|
resolveIdentityWithSocial,
|
|
33
40
|
resolveProfileInputWithSocial,
|
|
34
41
|
searchDirectory,
|
|
42
|
+
signPrivateMessage,
|
|
43
|
+
signPrivateMessageReceipt,
|
|
35
44
|
signSocialMessage,
|
|
36
45
|
signSocialMessageObservation,
|
|
37
46
|
signPresence,
|
|
@@ -44,6 +53,8 @@ import {
|
|
|
44
53
|
verifySocialMessage,
|
|
45
54
|
verifySocialMessageObservation,
|
|
46
55
|
verifyPresence,
|
|
56
|
+
verifyPrivateMessage,
|
|
57
|
+
verifyPrivateMessageReceipt,
|
|
47
58
|
verifyProfile,
|
|
48
59
|
} from "@silicaclaw/core";
|
|
49
60
|
import {
|
|
@@ -60,6 +71,9 @@ import {
|
|
|
60
71
|
CacheRepo,
|
|
61
72
|
IdentityRepo,
|
|
62
73
|
LogRepo,
|
|
74
|
+
PrivateEncryptionKeyRepo,
|
|
75
|
+
PrivateMessageReceiptRepo,
|
|
76
|
+
PrivateMessageRepo,
|
|
63
77
|
ProfileRepo,
|
|
64
78
|
SocialMessageGovernanceConfig,
|
|
65
79
|
SocialMessageGovernanceRepo,
|
|
@@ -79,21 +93,36 @@ const NETWORK_MAX_PAST_DRIFT_MS = Number(process.env.NETWORK_MAX_PAST_DRIFT_MS |
|
|
|
79
93
|
const NETWORK_HEARTBEAT_INTERVAL_MS = Number(process.env.NETWORK_HEARTBEAT_INTERVAL_MS || 12_000);
|
|
80
94
|
const NETWORK_PEER_STALE_AFTER_MS = Number(process.env.NETWORK_PEER_STALE_AFTER_MS || 45_000);
|
|
81
95
|
const OPENCLAW_GATEWAY_HOST = "127.0.0.1";
|
|
82
|
-
const
|
|
96
|
+
const DEFAULT_NETWORK_MODE = defaults.network.default_mode as "global-preview";
|
|
97
|
+
const DEFAULT_NETWORK_NAMESPACE = defaults.network.default_namespace;
|
|
98
|
+
const DEFAULT_NETWORK_PORT = defaults.ports.network_default;
|
|
99
|
+
const DEFAULT_GLOBAL_SIGNALING_URL = defaults.network.global_preview.relay_url;
|
|
100
|
+
const DEFAULT_GLOBAL_ROOM = defaults.network.global_preview.room;
|
|
101
|
+
const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
|
|
102
|
+
const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
|
|
83
103
|
const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
|
|
104
|
+
const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
|
|
105
|
+
const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
|
|
84
106
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
107
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
85
108
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
86
109
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
87
110
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
88
111
|
const NETWORK_MODE = process.env.NETWORK_MODE || "";
|
|
89
|
-
const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL ||
|
|
112
|
+
const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL || DEFAULT_GLOBAL_SIGNALING_URL;
|
|
90
113
|
const WEBRTC_SIGNALING_URLS = process.env.WEBRTC_SIGNALING_URLS || "";
|
|
91
|
-
const WEBRTC_ROOM = process.env.WEBRTC_ROOM ||
|
|
114
|
+
const WEBRTC_ROOM = process.env.WEBRTC_ROOM || DEFAULT_GLOBAL_ROOM;
|
|
92
115
|
const WEBRTC_SEED_PEERS = process.env.WEBRTC_SEED_PEERS || "";
|
|
93
116
|
const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
94
117
|
const PROFILE_VERSION = "v0.9";
|
|
95
118
|
const SOCIAL_MESSAGE_TOPIC = "social.message";
|
|
96
119
|
const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
|
|
120
|
+
const PRIVATE_MESSAGE_TOPIC = "private.message";
|
|
121
|
+
const PRIVATE_MESSAGE_RECEIPT_TOPIC = "private.message.receipt";
|
|
122
|
+
const PRIVATE_MESSAGE_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_HISTORY_LIMIT || 1000);
|
|
123
|
+
const PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT || 2000);
|
|
124
|
+
const PRIVATE_MESSAGE_QUERY_LIMIT = Number(process.env.PRIVATE_MESSAGE_QUERY_LIMIT || 100);
|
|
125
|
+
const PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS = Number(process.env.PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS || 750);
|
|
97
126
|
const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
|
|
98
127
|
const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
|
|
99
128
|
const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
|
|
@@ -105,6 +134,14 @@ const SOCIAL_MESSAGE_DUPLICATE_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_DUP
|
|
|
105
134
|
const SOCIAL_MESSAGE_MAX_FUTURE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_FUTURE_MS || 30_000);
|
|
106
135
|
const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS || 15 * 60_000);
|
|
107
136
|
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
137
|
+
const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
|
|
138
|
+
const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
|
|
139
|
+
const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(
|
|
140
|
+
process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000
|
|
141
|
+
);
|
|
142
|
+
const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(
|
|
143
|
+
process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000
|
|
144
|
+
);
|
|
108
145
|
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
|
|
109
146
|
dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
|
|
110
147
|
);
|
|
@@ -115,23 +152,79 @@ const execFileAsync = promisify(execFile);
|
|
|
115
152
|
const OPENCLAW_SKILL_NAME = "silicaclaw-broadcast";
|
|
116
153
|
|
|
117
154
|
function readWorkspaceVersion(workspaceRoot: string): string {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
const candidates = [
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
process.cwd(),
|
|
158
|
+
resolve(__dirname, "..", "..", ".."),
|
|
159
|
+
resolve(__dirname, "..", "..", "..", ".."),
|
|
160
|
+
].filter((dir, index, list) => dir && list.indexOf(dir) === index);
|
|
161
|
+
for (const candidate of candidates) {
|
|
162
|
+
const pkgFile = resolve(candidate, "package.json");
|
|
163
|
+
if (existsSync(pkgFile)) {
|
|
164
|
+
try {
|
|
165
|
+
const pkg = JSON.parse(readFileSync(pkgFile, "utf8")) as { version?: string; name?: string };
|
|
166
|
+
if (pkg.version && (pkg.name === "@silicaclaw/cli" || existsSync(resolve(candidate, "apps", "local-console")))) {
|
|
167
|
+
return String(pkg.version);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// ignore
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const versionFile = resolve(candidate, "VERSION");
|
|
174
|
+
if (existsSync(versionFile)) {
|
|
175
|
+
const raw = readFileSync(versionFile, "utf8").trim();
|
|
176
|
+
if (raw) return raw;
|
|
125
177
|
}
|
|
126
|
-
}
|
|
127
|
-
const versionFile = resolve(workspaceRoot, "VERSION");
|
|
128
|
-
if (existsSync(versionFile)) {
|
|
129
|
-
const raw = readFileSync(versionFile, "utf8").trim();
|
|
130
|
-
if (raw) return raw;
|
|
131
178
|
}
|
|
132
179
|
return "unknown";
|
|
133
180
|
}
|
|
134
181
|
|
|
182
|
+
function normalizeVersionText(value: unknown): string {
|
|
183
|
+
const text = String(value || "").trim();
|
|
184
|
+
return text.startsWith("v") ? text.slice(1) : text;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatBytesToMiB(value: number): number {
|
|
188
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function tokenizeVersion(value: unknown): Array<number | string> {
|
|
192
|
+
return normalizeVersionText(value)
|
|
193
|
+
.split(/[^0-9A-Za-z]+/)
|
|
194
|
+
.map((token) => token.trim())
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.map((token) => (/^\d+$/.test(token) ? Number(token) : token.toLowerCase()));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function compareVersionTokens(left: unknown, right: unknown): number {
|
|
200
|
+
const leftTokens = tokenizeVersion(left);
|
|
201
|
+
const rightTokens = tokenizeVersion(right);
|
|
202
|
+
const maxLength = Math.max(leftTokens.length, rightTokens.length);
|
|
203
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
204
|
+
const leftToken = leftTokens[index];
|
|
205
|
+
const rightToken = rightTokens[index];
|
|
206
|
+
if (leftToken === undefined && rightToken === undefined) return 0;
|
|
207
|
+
if (leftToken === undefined) return -1;
|
|
208
|
+
if (rightToken === undefined) return 1;
|
|
209
|
+
if (typeof leftToken === "number" && typeof rightToken === "number") {
|
|
210
|
+
if (leftToken !== rightToken) return leftToken > rightToken ? 1 : -1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const leftText = String(leftToken);
|
|
214
|
+
const rightText = String(rightToken);
|
|
215
|
+
if (leftText !== rightText) return leftText.localeCompare(rightText);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function userNpmCacheDir(): string {
|
|
221
|
+
return resolve(homedir(), ".silicaclaw", "npm-cache");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function userShimPath(): string {
|
|
225
|
+
return resolve(homedir(), ".silicaclaw", "bin", "silicaclaw");
|
|
226
|
+
}
|
|
227
|
+
|
|
135
228
|
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
136
229
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
137
230
|
return cwd;
|
|
@@ -143,7 +236,36 @@ function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
|
143
236
|
return cwd;
|
|
144
237
|
}
|
|
145
238
|
|
|
239
|
+
function resolveProjectRoot(appRoot: string, cwd = process.cwd()): string {
|
|
240
|
+
const envAppRoot = String(process.env.SILICACLAW_APP_DIR || "").trim();
|
|
241
|
+
if (
|
|
242
|
+
envAppRoot &&
|
|
243
|
+
existsSync(resolve(envAppRoot, "apps", "local-console", "package.json")) &&
|
|
244
|
+
existsSync(resolve(envAppRoot, "package.json"))
|
|
245
|
+
) {
|
|
246
|
+
return resolve(envAppRoot);
|
|
247
|
+
}
|
|
248
|
+
const envRoot = String(process.env.SILICACLAW_WORKSPACE_DIR || "").trim();
|
|
249
|
+
if (envRoot) {
|
|
250
|
+
return resolve(envRoot);
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
existsSync(resolve(appRoot, "apps", "local-console", "package.json")) &&
|
|
254
|
+
existsSync(resolve(appRoot, "package.json"))
|
|
255
|
+
) {
|
|
256
|
+
return appRoot;
|
|
257
|
+
}
|
|
258
|
+
if (!existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
259
|
+
return resolve(cwd);
|
|
260
|
+
}
|
|
261
|
+
return appRoot;
|
|
262
|
+
}
|
|
263
|
+
|
|
146
264
|
function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string {
|
|
265
|
+
const home = process.env.HOME || homedir();
|
|
266
|
+
if (home) {
|
|
267
|
+
return resolve(home, ".silicaclaw", "local-console");
|
|
268
|
+
}
|
|
147
269
|
const appRoot = resolve(workspaceRoot, "apps", "local-console");
|
|
148
270
|
if (existsSync(resolve(appRoot, "package.json"))) {
|
|
149
271
|
return appRoot;
|
|
@@ -151,6 +273,13 @@ function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string
|
|
|
151
273
|
return cwd;
|
|
152
274
|
}
|
|
153
275
|
|
|
276
|
+
function defaultOpenClawSourceDir(rootDir: string): string {
|
|
277
|
+
if (existsSync(resolve(rootDir, "openclaw.mjs")) || existsSync(resolve(rootDir, "package.json"))) {
|
|
278
|
+
return rootDir;
|
|
279
|
+
}
|
|
280
|
+
return resolve(rootDir, "..", "openclaw");
|
|
281
|
+
}
|
|
282
|
+
|
|
154
283
|
function resolveExecutableInPath(binName: string): string | null {
|
|
155
284
|
const pathValue = String(process.env.PATH || "").trim();
|
|
156
285
|
if (!pathValue) return null;
|
|
@@ -213,6 +342,50 @@ function summarizeSkillReadme(filePath: string) {
|
|
|
213
342
|
}
|
|
214
343
|
}
|
|
215
344
|
|
|
345
|
+
function readDialogueCheatsheetPreview(filePath: string, limit = 6) {
|
|
346
|
+
if (!filePath || !existsSync(filePath)) return [];
|
|
347
|
+
try {
|
|
348
|
+
return readFileSync(filePath, "utf8")
|
|
349
|
+
.split(/\r?\n/)
|
|
350
|
+
.map((line) => line.trim())
|
|
351
|
+
.filter((line) => line.startsWith("- "))
|
|
352
|
+
.map((line) => line.slice(2).trim())
|
|
353
|
+
.filter(Boolean)
|
|
354
|
+
.slice(0, limit);
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readDialogueCheatsheetSections(filePath: string, maxSections = 3, maxItemsPerSection = 5) {
|
|
361
|
+
if (!filePath || !existsSync(filePath)) return [];
|
|
362
|
+
try {
|
|
363
|
+
const lines = readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
364
|
+
const sections: Array<{ title: string; items: string[] }> = [];
|
|
365
|
+
let current: { title: string; items: string[] } | null = null;
|
|
366
|
+
for (const rawLine of lines) {
|
|
367
|
+
const line = rawLine.trim();
|
|
368
|
+
if (line.startsWith("## ")) {
|
|
369
|
+
if (current && current.items.length) sections.push(current);
|
|
370
|
+
current = { title: line.slice(3).trim(), items: [] };
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (line.startsWith("- ")) {
|
|
374
|
+
if (!current) {
|
|
375
|
+
current = { title: "Examples", items: [] };
|
|
376
|
+
}
|
|
377
|
+
if (current.items.length < maxItemsPerSection) {
|
|
378
|
+
current.items.push(line.slice(2).trim());
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (current && current.items.length) sections.push(current);
|
|
383
|
+
return sections.slice(0, maxSections);
|
|
384
|
+
} catch {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
216
389
|
function detectOpenClawInstallation(workspaceRoot: string) {
|
|
217
390
|
const workspaceDir = resolve(workspaceRoot, ".openclaw");
|
|
218
391
|
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
@@ -265,7 +438,7 @@ function detectOpenClawInstallation(workspaceRoot: string) {
|
|
|
265
438
|
|
|
266
439
|
function readOpenClawConfiguredGateway(workspaceRoot: string) {
|
|
267
440
|
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
268
|
-
const defaultSourceDir =
|
|
441
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
269
442
|
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
270
443
|
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
271
444
|
const explicitConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || "").trim();
|
|
@@ -307,45 +480,66 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
|
|
|
307
480
|
} as const;
|
|
308
481
|
}
|
|
309
482
|
|
|
310
|
-
function
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
const stdout = String(result.stdout || "");
|
|
316
|
-
const lines = stdout
|
|
317
|
-
.split("\n")
|
|
318
|
-
.map((line) => line.trim())
|
|
319
|
-
.filter(Boolean);
|
|
483
|
+
function resolveOpenClawStatusCommand(workspaceRoot: string) {
|
|
484
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
485
|
+
if (explicitBin) {
|
|
486
|
+
return { cmd: explicitBin, args: ["status"] } as const;
|
|
487
|
+
}
|
|
320
488
|
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
489
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
490
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
491
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
492
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
493
|
+
if (sourceEntry) {
|
|
494
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
498
|
+
if (commandPath) {
|
|
499
|
+
return { cmd: commandPath, args: ["status"] } as const;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
|
|
506
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
507
|
+
if (explicitBin) {
|
|
508
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
|
|
509
|
+
}
|
|
343
510
|
|
|
344
|
-
const
|
|
345
|
-
const
|
|
511
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
512
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
513
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
514
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
515
|
+
if (sourceEntry) {
|
|
516
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
520
|
+
if (commandPath) {
|
|
521
|
+
return { cmd: commandPath, args: ["gateway", "probe"] } as const;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function detectOpenClawRuntime(workspaceRoot: string) {
|
|
528
|
+
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
529
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
530
|
+
const statusLooksConfigured = Boolean(
|
|
531
|
+
statusCommand ||
|
|
532
|
+
configuredGateway.config_path ||
|
|
533
|
+
detectOpenClawInstallation(workspaceRoot).detected
|
|
534
|
+
);
|
|
535
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
536
|
+
const gatewayProbe = spawnSync(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
346
537
|
encoding: "utf8",
|
|
538
|
+
timeout: 1200,
|
|
347
539
|
});
|
|
348
|
-
const
|
|
540
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
541
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
542
|
+
const gatewayLines = gatewayStatusStdout
|
|
349
543
|
.split("\n")
|
|
350
544
|
.map((line) => line.trim())
|
|
351
545
|
.filter(Boolean);
|
|
@@ -355,14 +549,9 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
355
549
|
const parts = line.split(/\s+/);
|
|
356
550
|
const pid = Number(parts[1] || 0);
|
|
357
551
|
const command = parts[0] || "";
|
|
358
|
-
const lowerCommand = command.toLowerCase();
|
|
359
552
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
360
553
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
361
554
|
if (!pid || !command || !portMatch) return null;
|
|
362
|
-
const isOpenClawListener =
|
|
363
|
-
openclawPids.has(pid) ||
|
|
364
|
-
lowerCommand.includes("openclaw");
|
|
365
|
-
if (!isOpenClawListener) return null;
|
|
366
555
|
const port = Number(portMatch[1]);
|
|
367
556
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
368
557
|
return {
|
|
@@ -373,46 +562,106 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
373
562
|
};
|
|
374
563
|
})
|
|
375
564
|
.filter((item): item is { pid: number; ppid: number; port: number; command: string } => Boolean(item));
|
|
565
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
566
|
+
let processes: Array<{ pid: number; ppid: number; command: string }> = gatewayListeners.map((item) => ({
|
|
567
|
+
pid: item.pid,
|
|
568
|
+
ppid: item.ppid,
|
|
569
|
+
command: item.command,
|
|
570
|
+
}));
|
|
571
|
+
let processResult: ReturnType<typeof spawnSync> | null = null;
|
|
572
|
+
if (!gatewayProbeOk) {
|
|
573
|
+
processResult = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
574
|
+
encoding: "utf8",
|
|
575
|
+
timeout: 1200,
|
|
576
|
+
});
|
|
577
|
+
const stdout = String(processResult.stdout || "");
|
|
578
|
+
const lines = stdout
|
|
579
|
+
.split("\n")
|
|
580
|
+
.map((line) => line.trim())
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
processes = lines
|
|
583
|
+
.map((line) => {
|
|
584
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
585
|
+
if (!match) return null;
|
|
586
|
+
const command = match[3] || "";
|
|
587
|
+
const lower = command.toLowerCase();
|
|
588
|
+
const isOpenClaw =
|
|
589
|
+
lower.includes(" openclaw ") ||
|
|
590
|
+
lower.endsWith(" openclaw") ||
|
|
591
|
+
lower.includes("/openclaw ") ||
|
|
592
|
+
lower.includes("openclaw.mjs") ||
|
|
593
|
+
lower.includes("openclaw gateway") ||
|
|
594
|
+
lower.includes("openclaw agent") ||
|
|
595
|
+
lower.includes("openclaw message");
|
|
596
|
+
if (!isOpenClaw) return null;
|
|
597
|
+
return {
|
|
598
|
+
pid: Number(match[1]),
|
|
599
|
+
ppid: Number(match[2]),
|
|
600
|
+
command,
|
|
601
|
+
};
|
|
602
|
+
})
|
|
603
|
+
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
604
|
+
}
|
|
605
|
+
|
|
376
606
|
const preferredListener =
|
|
377
607
|
gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
378
608
|
gatewayListeners[0] ||
|
|
379
609
|
null;
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
for (const process of [...processes, ...gatewayListeners]) {
|
|
383
|
-
if (!combinedProcesses.has(process.pid)) {
|
|
384
|
-
combinedProcesses.set(process.pid, process);
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const current = combinedProcesses.get(process.pid);
|
|
388
|
-
if (current && current.command.length < process.command.length) {
|
|
389
|
-
combinedProcesses.set(process.pid, process);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const allProcesses = Array.from(combinedProcesses.values());
|
|
393
|
-
const gatewayReachable = gatewayListeners.length > 0;
|
|
610
|
+
const allProcesses = processes.slice(0, 10);
|
|
611
|
+
const gatewayReachable = gatewayProbeOk;
|
|
394
612
|
const detectionNotes = [];
|
|
395
|
-
if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
396
613
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
397
|
-
detectionNotes.push(String(
|
|
614
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
615
|
+
}
|
|
616
|
+
if (processResult && processResult.status !== 0) {
|
|
617
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
398
618
|
}
|
|
399
619
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
400
620
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
401
621
|
|
|
402
622
|
return {
|
|
403
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
623
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
404
624
|
process_count: allProcesses.length,
|
|
405
625
|
processes: allProcesses.slice(0, 10),
|
|
406
626
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
407
627
|
gateway_url: gatewayUrl,
|
|
408
628
|
gateway_port: gatewayPort,
|
|
409
629
|
gateway_reachable: gatewayReachable,
|
|
630
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
631
|
+
status_ok: statusLooksConfigured,
|
|
632
|
+
status_summary: statusLooksConfigured
|
|
633
|
+
? configuredGateway.config_path
|
|
634
|
+
? `configured via ${configuredGateway.config_path}`
|
|
635
|
+
: statusCommand
|
|
636
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
637
|
+
: "OpenClaw environment detected"
|
|
638
|
+
: null,
|
|
639
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
640
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
641
|
+
gateway_probe_summary: gatewayProbeOk
|
|
642
|
+
? gatewayStatusStdout
|
|
643
|
+
.split("\n")
|
|
644
|
+
.map((line) => line.trim())
|
|
645
|
+
.filter(Boolean)
|
|
646
|
+
.slice(0, 4)
|
|
647
|
+
.join(" | ")
|
|
648
|
+
: null,
|
|
410
649
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
411
650
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
412
651
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
413
652
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
414
653
|
detection_mode:
|
|
415
|
-
|
|
654
|
+
gatewayProbeOk
|
|
655
|
+
? (
|
|
656
|
+
processes.length > 0 && gatewayReachable
|
|
657
|
+
? "gateway-probe+process+gateway"
|
|
658
|
+
: gatewayReachable
|
|
659
|
+
? "gateway-probe+gateway"
|
|
660
|
+
: processes.length > 0
|
|
661
|
+
? "gateway-probe+process"
|
|
662
|
+
: "gateway-probe"
|
|
663
|
+
)
|
|
664
|
+
: processes.length > 0 && gatewayReachable
|
|
416
665
|
? "process+gateway"
|
|
417
666
|
: gatewayReachable
|
|
418
667
|
? "gateway"
|
|
@@ -453,7 +702,7 @@ function detectOwnerDeliveryStatus(params: {
|
|
|
453
702
|
const ownerAccount = String(process.env.OPENCLAW_OWNER_ACCOUNT || "").trim();
|
|
454
703
|
const explicitOpenClawBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
455
704
|
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
456
|
-
const defaultSourceDir =
|
|
705
|
+
const defaultSourceDir = defaultOpenClawSourceDir(params.workspaceRoot);
|
|
457
706
|
const openclawSourceDir = configuredSourceDir || defaultSourceDir;
|
|
458
707
|
const openclawSourceEntry = existingPathOrNull(resolve(openclawSourceDir, "openclaw.mjs"));
|
|
459
708
|
const openclawCommandResolvable = Boolean(explicitOpenClawBin || resolveExecutableInPath("openclaw") || openclawSourceEntry);
|
|
@@ -512,10 +761,18 @@ function hasMeaningfulJson(filePath: string): boolean {
|
|
|
512
761
|
}
|
|
513
762
|
}
|
|
514
763
|
|
|
515
|
-
function migrateLegacyDataIfNeeded(
|
|
516
|
-
const
|
|
764
|
+
function migrateLegacyDataIfNeeded(appRoot: string, projectRoot: string, storageRoot: string): void {
|
|
765
|
+
const homeDir = process.env.HOME || homedir();
|
|
766
|
+
const legacyNpxAppRoots = collectLegacyNpxAppRoots(homeDir);
|
|
517
767
|
const targetDataDir = resolve(storageRoot, "data");
|
|
518
|
-
|
|
768
|
+
const legacyDataDirs = [
|
|
769
|
+
resolve(appRoot, "data"),
|
|
770
|
+
resolve(appRoot, "apps", "local-console", "data"),
|
|
771
|
+
resolve(projectRoot, "data"),
|
|
772
|
+
resolve(projectRoot, "apps", "local-console", "data"),
|
|
773
|
+
resolve(process.cwd(), "data"),
|
|
774
|
+
...legacyNpxAppRoots.map((root) => resolve(root, "apps", "local-console", "data")),
|
|
775
|
+
].filter((dir, index, list) => list.indexOf(dir) === index && dir !== targetDataDir);
|
|
519
776
|
const files = [
|
|
520
777
|
"identity.json",
|
|
521
778
|
"profile.json",
|
|
@@ -525,16 +782,65 @@ function migrateLegacyDataIfNeeded(workspaceRoot: string, storageRoot: string):
|
|
|
525
782
|
"social-message-observations.json",
|
|
526
783
|
];
|
|
527
784
|
for (const file of files) {
|
|
528
|
-
const src = resolve(legacyDataDir, file);
|
|
529
785
|
const dst = resolve(targetDataDir, file);
|
|
530
|
-
if (!existsSync(src)) continue;
|
|
531
786
|
if (hasMeaningfulJson(dst)) continue;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
787
|
+
for (const legacyDataDir of legacyDataDirs) {
|
|
788
|
+
const src = resolve(legacyDataDir, file);
|
|
789
|
+
if (!existsSync(src)) continue;
|
|
790
|
+
if (!hasMeaningfulJson(src)) continue;
|
|
791
|
+
mkdirSync(targetDataDir, { recursive: true });
|
|
792
|
+
copyFileSync(src, dst);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const targetDotDir = resolve(storageRoot, ".silicaclaw");
|
|
798
|
+
const legacyDotDirs = [
|
|
799
|
+
resolve(appRoot, ".silicaclaw"),
|
|
800
|
+
resolve(appRoot, "apps", "local-console", ".silicaclaw"),
|
|
801
|
+
resolve(projectRoot, ".silicaclaw"),
|
|
802
|
+
resolve(projectRoot, "apps", "local-console", ".silicaclaw"),
|
|
803
|
+
resolve(process.cwd(), ".silicaclaw"),
|
|
804
|
+
...legacyNpxAppRoots.map((root) => resolve(root, "apps", "local-console", ".silicaclaw")),
|
|
805
|
+
].filter((dir, index, list) => list.indexOf(dir) === index && dir !== targetDotDir);
|
|
806
|
+
const dotFiles = ["social.runtime.json", "social.message-governance.json"];
|
|
807
|
+
for (const file of dotFiles) {
|
|
808
|
+
const dst = resolve(targetDotDir, file);
|
|
809
|
+
if (hasMeaningfulJson(dst)) continue;
|
|
810
|
+
for (const legacyDotDir of legacyDotDirs) {
|
|
811
|
+
const src = resolve(legacyDotDir, file);
|
|
812
|
+
if (!existsSync(src)) continue;
|
|
813
|
+
if (!hasMeaningfulJson(src)) continue;
|
|
814
|
+
mkdirSync(targetDotDir, { recursive: true });
|
|
815
|
+
copyFileSync(src, dst);
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
535
818
|
}
|
|
536
819
|
}
|
|
537
820
|
|
|
821
|
+
function collectLegacyNpxAppRoots(homeDir: string): string[] {
|
|
822
|
+
const cacheRoots = [
|
|
823
|
+
resolve(homeDir, ".silicaclaw", "npm-cache", "_npx"),
|
|
824
|
+
resolve(homeDir, ".npm", "_npx"),
|
|
825
|
+
];
|
|
826
|
+
const roots: string[] = [];
|
|
827
|
+
for (const cacheRoot of cacheRoots) {
|
|
828
|
+
if (!existsSync(cacheRoot)) continue;
|
|
829
|
+
let entries: string[] = [];
|
|
830
|
+
try {
|
|
831
|
+
entries = readdirSync(cacheRoot);
|
|
832
|
+
} catch {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
for (const entry of entries) {
|
|
836
|
+
const candidate = resolve(cacheRoot, entry, "node_modules", "@silicaclaw", "cli");
|
|
837
|
+
if (!existsSync(resolve(candidate, "apps", "local-console"))) continue;
|
|
838
|
+
roots.push(candidate);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return Array.from(new Set(roots));
|
|
842
|
+
}
|
|
843
|
+
|
|
538
844
|
function parseListEnv(raw: string): string[] {
|
|
539
845
|
return raw
|
|
540
846
|
.split(/[,\n]/g)
|
|
@@ -575,6 +881,7 @@ type IntegrationStatusSummary = {
|
|
|
575
881
|
};
|
|
576
882
|
|
|
577
883
|
type SocialMessageView = SocialMessageRecord & {
|
|
884
|
+
avatar_url?: string;
|
|
578
885
|
is_self: boolean;
|
|
579
886
|
online: boolean;
|
|
580
887
|
last_seen_at: number | null;
|
|
@@ -584,6 +891,17 @@ type SocialMessageView = SocialMessageRecord & {
|
|
|
584
891
|
delivery_status: "local-only" | "remote-observed";
|
|
585
892
|
};
|
|
586
893
|
|
|
894
|
+
type PrivateMessageView = {
|
|
895
|
+
message_id: string;
|
|
896
|
+
conversation_id: string;
|
|
897
|
+
from_agent_id: string;
|
|
898
|
+
to_agent_id: string;
|
|
899
|
+
body: string;
|
|
900
|
+
created_at: number;
|
|
901
|
+
is_self: boolean;
|
|
902
|
+
delivery_status: "sent" | "received" | "read";
|
|
903
|
+
};
|
|
904
|
+
|
|
587
905
|
type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
|
|
588
906
|
|
|
589
907
|
type OpenClawBridgeStatus = {
|
|
@@ -627,11 +945,17 @@ type OpenClawBridgeStatus = {
|
|
|
627
945
|
gateway_url: string;
|
|
628
946
|
gateway_port: number;
|
|
629
947
|
gateway_reachable: boolean;
|
|
948
|
+
status_command: string | null;
|
|
949
|
+
status_ok: boolean;
|
|
950
|
+
status_summary: string | null;
|
|
951
|
+
gateway_probe_command: string | null;
|
|
952
|
+
gateway_probe_ok: boolean;
|
|
953
|
+
gateway_probe_summary: string | null;
|
|
630
954
|
configured_gateway_url: string;
|
|
631
955
|
configured_gateway_port: number;
|
|
632
956
|
configured_gateway_bind: string | null;
|
|
633
957
|
configured_gateway_config_path: string | null;
|
|
634
|
-
detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
|
|
958
|
+
detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
|
|
635
959
|
};
|
|
636
960
|
skill_learning: {
|
|
637
961
|
available: boolean;
|
|
@@ -699,6 +1023,7 @@ type OpenClawBridgeConfigView = {
|
|
|
699
1023
|
|
|
700
1024
|
export class LocalNodeService {
|
|
701
1025
|
private workspaceRoot: string;
|
|
1026
|
+
private projectRoot: string;
|
|
702
1027
|
private storageRoot: string;
|
|
703
1028
|
private identityRepo: IdentityRepo;
|
|
704
1029
|
private profileRepo: ProfileRepo;
|
|
@@ -707,6 +1032,9 @@ export class LocalNodeService {
|
|
|
707
1032
|
private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
|
|
708
1033
|
private socialMessageRepo: SocialMessageRepo;
|
|
709
1034
|
private socialMessageObservationRepo: SocialMessageObservationRepo;
|
|
1035
|
+
private privateMessageRepo: PrivateMessageRepo;
|
|
1036
|
+
private privateMessageReceiptRepo: PrivateMessageReceiptRepo;
|
|
1037
|
+
private privateEncryptionKeyRepo: PrivateEncryptionKeyRepo;
|
|
710
1038
|
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
711
1039
|
|
|
712
1040
|
private identity: AgentIdentity | null = null;
|
|
@@ -714,15 +1042,31 @@ export class LocalNodeService {
|
|
|
714
1042
|
private directory: DirectoryState = createEmptyDirectoryState();
|
|
715
1043
|
private socialMessages: SocialMessageRecord[] = [];
|
|
716
1044
|
private socialMessageObservations: SocialMessageObservationRecord[] = [];
|
|
1045
|
+
private privateMessages: PrivateMessageRecord[] = [];
|
|
1046
|
+
private privateMessageReceipts: PrivateMessageReceiptRecord[] = [];
|
|
1047
|
+
private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
|
|
1048
|
+
private privatePeerRoutes: Record<string, string> = {};
|
|
1049
|
+
private privateMessageBodyCache = new Map<string, string>();
|
|
717
1050
|
private messageGovernance: RuntimeMessageGovernance;
|
|
1051
|
+
private privateMessagesPersistDirty = false;
|
|
1052
|
+
private privateMessageReceiptsPersistDirty = false;
|
|
1053
|
+
private privateMessagesPersistTimer: NodeJS.Timeout | null = null;
|
|
1054
|
+
private privateMessageReceiptsPersistTimer: NodeJS.Timeout | null = null;
|
|
718
1055
|
|
|
719
1056
|
private receivedCount = 0;
|
|
720
1057
|
private broadcastCount = 0;
|
|
721
1058
|
private lastMessageAt = 0;
|
|
722
1059
|
private lastBroadcastAt = 0;
|
|
1060
|
+
private lastProfileBroadcastAt = 0;
|
|
1061
|
+
private lastProfileBroadcastSignature = "";
|
|
1062
|
+
private lastReplayBroadcastAt = 0;
|
|
1063
|
+
private lastReplayBroadcastSignature = "";
|
|
723
1064
|
private lastBroadcastErrorAt = 0;
|
|
724
1065
|
private lastBroadcastError: string | null = null;
|
|
725
1066
|
private broadcastFailureCount = 0;
|
|
1067
|
+
private consecutiveBroadcastFailures = 0;
|
|
1068
|
+
private lastBroadcastRecoveryAttemptAt = 0;
|
|
1069
|
+
private broadcastRecoveryInFlight = false;
|
|
726
1070
|
private broadcaster: NodeJS.Timeout | null = null;
|
|
727
1071
|
private subscriptionsBound = false;
|
|
728
1072
|
private broadcastEnabled = true;
|
|
@@ -741,7 +1085,7 @@ export class LocalNodeService {
|
|
|
741
1085
|
|
|
742
1086
|
private network: NetworkAdapter;
|
|
743
1087
|
private adapterMode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview";
|
|
744
|
-
private networkMode: "local" | "lan" | "global-preview" =
|
|
1088
|
+
private networkMode: "local" | "lan" | "global-preview" = DEFAULT_NETWORK_MODE;
|
|
745
1089
|
private networkNamespace: string;
|
|
746
1090
|
private networkPort: number | null;
|
|
747
1091
|
private socialConfig: SocialConfig;
|
|
@@ -755,17 +1099,24 @@ export class LocalNodeService {
|
|
|
755
1099
|
"silicaclaw-existing";
|
|
756
1100
|
private resolvedOpenClawIdentityPath: string | null = null;
|
|
757
1101
|
private webrtcSignalingUrls: string[] = [];
|
|
758
|
-
private webrtcRoom =
|
|
1102
|
+
private webrtcRoom = DEFAULT_GLOBAL_ROOM;
|
|
759
1103
|
private webrtcSeedPeers: string[] = [];
|
|
760
1104
|
private webrtcBootstrapHints: string[] = [];
|
|
761
1105
|
private webrtcBootstrapSources: string[] = [];
|
|
1106
|
+
private networkStarted = false;
|
|
1107
|
+
private networkStartupError: string | null = null;
|
|
1108
|
+
private networkReconnectTimer: NodeJS.Timeout | null = null;
|
|
1109
|
+
private networkReconnectDelayMs = 5_000;
|
|
762
1110
|
private appVersion = "unknown";
|
|
1111
|
+
private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
|
|
1112
|
+
private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
|
|
763
1113
|
|
|
764
|
-
constructor(options?: { workspaceRoot?: string; storageRoot?: string }) {
|
|
1114
|
+
constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
|
|
765
1115
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
1116
|
+
this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
|
|
766
1117
|
this.storageRoot = options?.storageRoot || resolveStorageRoot(this.workspaceRoot);
|
|
767
1118
|
this.appVersion = readWorkspaceVersion(this.workspaceRoot);
|
|
768
|
-
migrateLegacyDataIfNeeded(this.workspaceRoot, this.storageRoot);
|
|
1119
|
+
migrateLegacyDataIfNeeded(this.workspaceRoot, this.projectRoot, this.storageRoot);
|
|
769
1120
|
|
|
770
1121
|
this.identityRepo = new IdentityRepo(this.storageRoot);
|
|
771
1122
|
this.profileRepo = new ProfileRepo(this.storageRoot);
|
|
@@ -774,19 +1125,22 @@ export class LocalNodeService {
|
|
|
774
1125
|
this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
|
|
775
1126
|
this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
|
|
776
1127
|
this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
|
|
1128
|
+
this.privateMessageRepo = new PrivateMessageRepo(this.storageRoot);
|
|
1129
|
+
this.privateMessageReceiptRepo = new PrivateMessageReceiptRepo(this.storageRoot);
|
|
1130
|
+
this.privateEncryptionKeyRepo = new PrivateEncryptionKeyRepo(this.storageRoot);
|
|
777
1131
|
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
778
1132
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
779
1133
|
|
|
780
|
-
let loadedSocial = loadSocialConfig(this.
|
|
1134
|
+
let loadedSocial = loadSocialConfig(this.projectRoot);
|
|
781
1135
|
if (!loadedSocial.meta.found) {
|
|
782
|
-
ensureDefaultSocialMd(this.
|
|
1136
|
+
ensureDefaultSocialMd(this.projectRoot, {
|
|
783
1137
|
display_name: this.getDefaultDisplayName(),
|
|
784
1138
|
bio: "Local AI agent connected to SilicaClaw",
|
|
785
1139
|
tags: ["openclaw", "local-first"],
|
|
786
|
-
mode:
|
|
1140
|
+
mode: DEFAULT_NETWORK_MODE,
|
|
787
1141
|
public_enabled: false,
|
|
788
1142
|
});
|
|
789
|
-
loadedSocial = loadSocialConfig(this.
|
|
1143
|
+
loadedSocial = loadSocialConfig(this.projectRoot);
|
|
790
1144
|
this.initState.social_auto_created = true;
|
|
791
1145
|
}
|
|
792
1146
|
this.socialConfig = loadedSocial.config;
|
|
@@ -795,8 +1149,8 @@ export class LocalNodeService {
|
|
|
795
1149
|
this.socialParseError = loadedSocial.meta.parse_error;
|
|
796
1150
|
this.socialRawFrontmatter = loadedSocial.raw_frontmatter;
|
|
797
1151
|
|
|
798
|
-
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE ||
|
|
799
|
-
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT ||
|
|
1152
|
+
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || DEFAULT_NETWORK_NAMESPACE;
|
|
1153
|
+
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || DEFAULT_NETWORK_PORT);
|
|
800
1154
|
this.applyResolvedNetworkConfig();
|
|
801
1155
|
const resolved = this.buildNetworkAdapter();
|
|
802
1156
|
this.network = resolved.adapter;
|
|
@@ -804,36 +1158,42 @@ export class LocalNodeService {
|
|
|
804
1158
|
this.networkPort = resolved.port;
|
|
805
1159
|
}
|
|
806
1160
|
|
|
1161
|
+
private getCachedOpenClawRuntime() {
|
|
1162
|
+
const now = Date.now();
|
|
1163
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
1164
|
+
return this.openclawRuntimeCache.value;
|
|
1165
|
+
}
|
|
1166
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
1167
|
+
this.openclawRuntimeCache = {
|
|
1168
|
+
value,
|
|
1169
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
1170
|
+
};
|
|
1171
|
+
return value;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private invalidateOpenClawCaches() {
|
|
1175
|
+
this.openclawRuntimeCache = null;
|
|
1176
|
+
this.openclawBridgeStatusCache = null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
807
1179
|
async start(): Promise<void> {
|
|
808
1180
|
await this.hydrateFromDisk();
|
|
809
1181
|
|
|
810
1182
|
this.bindNetworkSubscriptions();
|
|
811
|
-
await this.
|
|
812
|
-
await this.log(
|
|
813
|
-
"info",
|
|
814
|
-
`Local node started (${this.adapterMode}, mode=${this.networkMode}, signaling=${this.webrtcSignalingUrls[0] || "-"}, room=${this.webrtcRoom})`
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
if (this.profile?.public_enabled && this.broadcastEnabled) {
|
|
818
|
-
try {
|
|
819
|
-
await this.broadcastNow("adapter_start");
|
|
820
|
-
} catch (error) {
|
|
821
|
-
await this.log(
|
|
822
|
-
"warn",
|
|
823
|
-
`Initial broadcast failed: ${error instanceof Error ? error.message : String(error)}`
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
this.startBroadcastLoop();
|
|
1183
|
+
await this.startNetworkAdapterWithRetry("adapter_start");
|
|
829
1184
|
}
|
|
830
1185
|
|
|
831
1186
|
async stop(): Promise<void> {
|
|
1187
|
+
this.clearNetworkReconnectTimer();
|
|
832
1188
|
if (this.broadcaster) {
|
|
833
1189
|
clearInterval(this.broadcaster);
|
|
834
1190
|
this.broadcaster = null;
|
|
835
1191
|
}
|
|
836
|
-
await this.
|
|
1192
|
+
await this.flushPrivatePersistence();
|
|
1193
|
+
if (this.networkStarted) {
|
|
1194
|
+
await this.network.stop();
|
|
1195
|
+
}
|
|
1196
|
+
this.networkStarted = false;
|
|
837
1197
|
}
|
|
838
1198
|
|
|
839
1199
|
private ensureLocalDirectoryBaseline(): void {
|
|
@@ -848,12 +1208,11 @@ export class LocalNodeService {
|
|
|
848
1208
|
}
|
|
849
1209
|
|
|
850
1210
|
getOverview() {
|
|
851
|
-
this.
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
).length;
|
|
1211
|
+
const discovered = this.search("");
|
|
1212
|
+
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
1213
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1214
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1215
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
857
1216
|
|
|
858
1217
|
return {
|
|
859
1218
|
app_version: this.appVersion,
|
|
@@ -864,12 +1223,21 @@ export class LocalNodeService {
|
|
|
864
1223
|
last_broadcast_error_at: this.lastBroadcastErrorAt,
|
|
865
1224
|
last_broadcast_error: this.lastBroadcastError,
|
|
866
1225
|
broadcast_failure_count: this.broadcastFailureCount,
|
|
867
|
-
discovered_count:
|
|
1226
|
+
discovered_count: discovered.length,
|
|
868
1227
|
online_count: onlineCount,
|
|
869
|
-
offline_count: Math.max(0,
|
|
1228
|
+
offline_count: Math.max(0, discovered.length - onlineCount),
|
|
870
1229
|
init_state: this.initState,
|
|
871
1230
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
872
1231
|
onboarding: this.getOnboardingSummary(),
|
|
1232
|
+
openclaw: {
|
|
1233
|
+
detected: openclawInstallation.detected,
|
|
1234
|
+
running: openclawRuntime.running,
|
|
1235
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
1236
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
1237
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
1238
|
+
status_ok: openclawRuntime.status_ok,
|
|
1239
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
1240
|
+
},
|
|
873
1241
|
social: {
|
|
874
1242
|
found: this.socialFound,
|
|
875
1243
|
enabled: this.socialConfig.enabled,
|
|
@@ -888,7 +1256,9 @@ export class LocalNodeService {
|
|
|
888
1256
|
}
|
|
889
1257
|
|
|
890
1258
|
getNetworkSummary() {
|
|
891
|
-
const
|
|
1259
|
+
const network = this.getResolvedRealtimeNetworkSummary();
|
|
1260
|
+
const diagnostics = network.diagnostics;
|
|
1261
|
+
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
892
1262
|
const peerCount = diagnostics?.peers.total ?? 0;
|
|
893
1263
|
|
|
894
1264
|
return {
|
|
@@ -916,30 +1286,34 @@ export class LocalNodeService {
|
|
|
916
1286
|
real_preview_stats: diagnostics?.stats ?? null,
|
|
917
1287
|
real_preview_transport_stats: diagnostics?.transport_stats ?? null,
|
|
918
1288
|
real_preview_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
919
|
-
webrtc_preview:
|
|
1289
|
+
webrtc_preview: relayCapable
|
|
920
1290
|
? {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1291
|
+
started: this.networkStarted,
|
|
1292
|
+
startup_error: this.networkStartupError,
|
|
1293
|
+
signaling_url: network.signaling_url,
|
|
1294
|
+
signaling_endpoints: network.signaling_endpoints,
|
|
1295
|
+
room: network.room,
|
|
1296
|
+
bootstrap_sources: network.bootstrap_sources,
|
|
1297
|
+
seed_peers_count: network.seed_peers_count,
|
|
1298
|
+
discovery_events_total: diagnostics?.discovery_events_total ?? 0,
|
|
1299
|
+
last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
|
|
1300
|
+
active_webrtc_peers: diagnostics?.active_webrtc_peers ?? 0,
|
|
1301
|
+
reconnect_attempts_total: diagnostics?.reconnect_attempts_total ?? 0,
|
|
1302
|
+
last_join_at: diagnostics?.last_join_at ?? 0,
|
|
1303
|
+
last_poll_at: diagnostics?.last_poll_at ?? 0,
|
|
1304
|
+
last_publish_at: diagnostics?.last_publish_at ?? 0,
|
|
1305
|
+
last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
|
|
1306
|
+
last_error_at: diagnostics?.last_error_at ?? 0,
|
|
1307
|
+
last_error: diagnostics?.last_error ?? null,
|
|
936
1308
|
}
|
|
937
1309
|
: null,
|
|
938
1310
|
};
|
|
939
1311
|
}
|
|
940
1312
|
|
|
941
1313
|
getNetworkConfig() {
|
|
942
|
-
const
|
|
1314
|
+
const network = this.getResolvedRealtimeNetworkSummary();
|
|
1315
|
+
const diagnostics = network.diagnostics;
|
|
1316
|
+
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
943
1317
|
return {
|
|
944
1318
|
adapter: this.adapterMode,
|
|
945
1319
|
mode: this.networkMode,
|
|
@@ -953,23 +1327,25 @@ export class LocalNodeService {
|
|
|
953
1327
|
},
|
|
954
1328
|
limits: diagnostics?.limits ?? null,
|
|
955
1329
|
adapter_config: diagnostics?.config ?? null,
|
|
956
|
-
adapter_extra:
|
|
1330
|
+
adapter_extra: relayCapable
|
|
957
1331
|
? {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1332
|
+
started: this.networkStarted,
|
|
1333
|
+
startup_error: this.networkStartupError,
|
|
1334
|
+
signaling_url: network.signaling_url,
|
|
1335
|
+
signaling_endpoints: network.signaling_endpoints,
|
|
1336
|
+
room: network.room,
|
|
1337
|
+
bootstrap_sources: network.bootstrap_sources,
|
|
1338
|
+
seed_peers_count: network.seed_peers_count,
|
|
1339
|
+
discovery_events_total: diagnostics?.discovery_events_total ?? 0,
|
|
1340
|
+
last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
|
|
1341
|
+
connection_states_summary: diagnostics?.connection_states_summary ?? null,
|
|
1342
|
+
datachannel_states_summary: diagnostics?.datachannel_states_summary ?? null,
|
|
1343
|
+
last_join_at: diagnostics?.last_join_at ?? 0,
|
|
1344
|
+
last_poll_at: diagnostics?.last_poll_at ?? 0,
|
|
1345
|
+
last_publish_at: diagnostics?.last_publish_at ?? 0,
|
|
1346
|
+
last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
|
|
1347
|
+
last_error_at: diagnostics?.last_error_at ?? 0,
|
|
1348
|
+
last_error: diagnostics?.last_error ?? null,
|
|
973
1349
|
}
|
|
974
1350
|
: null,
|
|
975
1351
|
env: {
|
|
@@ -997,16 +1373,19 @@ export class LocalNodeService {
|
|
|
997
1373
|
this.adapterMode === "real-preview"
|
|
998
1374
|
? "lan-preview"
|
|
999
1375
|
: this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview"
|
|
1000
|
-
? "
|
|
1376
|
+
? "global-preview"
|
|
1001
1377
|
: "local-process",
|
|
1002
1378
|
mode_explainer: this.getModeExplainer(),
|
|
1003
1379
|
};
|
|
1004
1380
|
}
|
|
1005
1381
|
|
|
1006
1382
|
getNetworkStats() {
|
|
1007
|
-
const
|
|
1383
|
+
const network = this.getResolvedRealtimeNetworkSummary();
|
|
1384
|
+
const diagnostics = network.diagnostics;
|
|
1385
|
+
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1008
1386
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1009
1387
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1388
|
+
const memory = process.memoryUsage();
|
|
1010
1389
|
|
|
1011
1390
|
return {
|
|
1012
1391
|
adapter: this.adapterMode,
|
|
@@ -1031,34 +1410,54 @@ export class LocalNodeService {
|
|
|
1031
1410
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1032
1411
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1033
1412
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1034
|
-
|
|
1413
|
+
runtime_diagnostics: {
|
|
1414
|
+
memory_mib: {
|
|
1415
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1416
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1417
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1418
|
+
external: formatBytesToMiB(memory.external),
|
|
1419
|
+
},
|
|
1420
|
+
directory: {
|
|
1421
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1422
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1423
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1424
|
+
},
|
|
1425
|
+
social: {
|
|
1426
|
+
message_count: this.socialMessages.length,
|
|
1427
|
+
observation_count: this.socialMessageObservations.length,
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1035
1431
|
? {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1432
|
+
started: this.networkStarted,
|
|
1433
|
+
startup_error: this.networkStartupError,
|
|
1434
|
+
signaling_url: network.signaling_url,
|
|
1435
|
+
signaling_endpoints: network.signaling_endpoints,
|
|
1436
|
+
room: network.room,
|
|
1437
|
+
bootstrap_sources: network.bootstrap_sources,
|
|
1438
|
+
seed_peers_count: network.seed_peers_count,
|
|
1439
|
+
discovery_events_total: diagnostics?.discovery_events_total ?? 0,
|
|
1440
|
+
last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
|
|
1441
|
+
connection_states_summary: diagnostics?.connection_states_summary ?? null,
|
|
1442
|
+
datachannel_states_summary: diagnostics?.datachannel_states_summary ?? null,
|
|
1443
|
+
signaling_messages_sent_total: diagnostics?.signaling_messages_sent_total ?? null,
|
|
1444
|
+
signaling_messages_received_total: diagnostics?.signaling_messages_received_total ?? null,
|
|
1445
|
+
reconnect_attempts_total: diagnostics?.reconnect_attempts_total ?? null,
|
|
1446
|
+
active_webrtc_peers: diagnostics?.active_webrtc_peers ?? null,
|
|
1447
|
+
last_join_at: diagnostics?.last_join_at ?? 0,
|
|
1448
|
+
last_poll_at: diagnostics?.last_poll_at ?? 0,
|
|
1449
|
+
last_publish_at: diagnostics?.last_publish_at ?? 0,
|
|
1450
|
+
last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
|
|
1451
|
+
last_error_at: diagnostics?.last_error_at ?? 0,
|
|
1452
|
+
last_error: diagnostics?.last_error ?? null,
|
|
1055
1453
|
}
|
|
1056
1454
|
: null,
|
|
1057
1455
|
};
|
|
1058
1456
|
}
|
|
1059
1457
|
|
|
1060
1458
|
getPeersSummary() {
|
|
1061
|
-
const
|
|
1459
|
+
const network = this.getResolvedRealtimeNetworkSummary();
|
|
1460
|
+
const diagnostics = network.diagnostics;
|
|
1062
1461
|
if (!diagnostics) {
|
|
1063
1462
|
return {
|
|
1064
1463
|
adapter: this.adapterMode,
|
|
@@ -1081,11 +1480,13 @@ export class LocalNodeService {
|
|
|
1081
1480
|
components: diagnostics.components,
|
|
1082
1481
|
limits: diagnostics.limits,
|
|
1083
1482
|
diagnostics_summary: {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1483
|
+
started: this.networkStarted,
|
|
1484
|
+
startup_error: this.networkStartupError,
|
|
1485
|
+
signaling_url: network.signaling_url,
|
|
1486
|
+
signaling_endpoints: network.signaling_endpoints,
|
|
1487
|
+
room: network.room,
|
|
1488
|
+
bootstrap_sources: network.bootstrap_sources,
|
|
1489
|
+
seed_peers_count: network.seed_peers_count,
|
|
1089
1490
|
discovery_events_total: diagnostics.discovery_events_total ?? 0,
|
|
1090
1491
|
last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
|
|
1091
1492
|
connection_states_summary: diagnostics.connection_states_summary ?? null,
|
|
@@ -1132,15 +1533,102 @@ export class LocalNodeService {
|
|
|
1132
1533
|
getRuntimePaths() {
|
|
1133
1534
|
return {
|
|
1134
1535
|
workspace_root: this.workspaceRoot,
|
|
1536
|
+
project_root: this.projectRoot,
|
|
1135
1537
|
storage_root: this.storageRoot,
|
|
1136
1538
|
data_dir: resolve(this.storageRoot, "data"),
|
|
1137
1539
|
social_runtime_path: resolve(this.storageRoot, ".silicaclaw", "social.runtime.json"),
|
|
1138
1540
|
local_console_public_dir: resolve(this.workspaceRoot, "apps", "local-console", "public"),
|
|
1139
|
-
social_lookup_paths: getSocialConfigSearchPaths(this.
|
|
1541
|
+
social_lookup_paths: getSocialConfigSearchPaths(this.projectRoot),
|
|
1140
1542
|
social_source_path: this.socialSourcePath,
|
|
1141
1543
|
};
|
|
1142
1544
|
}
|
|
1143
1545
|
|
|
1546
|
+
getAppUpdateStatus() {
|
|
1547
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1548
|
+
const fallback = {
|
|
1549
|
+
current_version: currentVersion,
|
|
1550
|
+
latest_version: currentVersion,
|
|
1551
|
+
update_available: false,
|
|
1552
|
+
channel: "latest",
|
|
1553
|
+
platform: process.platform,
|
|
1554
|
+
checked_at: Date.now(),
|
|
1555
|
+
can_update: true,
|
|
1556
|
+
check_error: null as string | null,
|
|
1557
|
+
};
|
|
1558
|
+
try {
|
|
1559
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1560
|
+
cwd: this.projectRoot,
|
|
1561
|
+
encoding: "utf8",
|
|
1562
|
+
env: {
|
|
1563
|
+
...process.env,
|
|
1564
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1565
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1566
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
if ((result.status ?? 1) !== 0) {
|
|
1570
|
+
return {
|
|
1571
|
+
...fallback,
|
|
1572
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1576
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1577
|
+
return {
|
|
1578
|
+
...fallback,
|
|
1579
|
+
latest_version: latestVersion,
|
|
1580
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1581
|
+
};
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
return {
|
|
1584
|
+
...fallback,
|
|
1585
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1591
|
+
const status = this.getAppUpdateStatus();
|
|
1592
|
+
if (!status.update_available || !status.latest_version) {
|
|
1593
|
+
return {
|
|
1594
|
+
started: false,
|
|
1595
|
+
target_version: status.latest_version || status.current_version,
|
|
1596
|
+
platform: process.platform,
|
|
1597
|
+
reason: status.check_error || "already_current",
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const shimPath = userShimPath();
|
|
1601
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1602
|
+
const useShim = existsSync(shimPath);
|
|
1603
|
+
if (!useShim && !existsSync(scriptPath)) {
|
|
1604
|
+
return {
|
|
1605
|
+
started: false,
|
|
1606
|
+
target_version: status.latest_version,
|
|
1607
|
+
platform: process.platform,
|
|
1608
|
+
reason: "missing_cli_script",
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1612
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1613
|
+
const child = spawn(command, args, {
|
|
1614
|
+
cwd: this.projectRoot,
|
|
1615
|
+
detached: true,
|
|
1616
|
+
stdio: "ignore",
|
|
1617
|
+
env: {
|
|
1618
|
+
...process.env,
|
|
1619
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1620
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1621
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1622
|
+
},
|
|
1623
|
+
});
|
|
1624
|
+
child.unref();
|
|
1625
|
+
return {
|
|
1626
|
+
started: true,
|
|
1627
|
+
target_version: status.latest_version,
|
|
1628
|
+
platform: process.platform,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1144
1632
|
getIntegrationSummary() {
|
|
1145
1633
|
const status = this.getIntegrationStatus();
|
|
1146
1634
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1175,9 +1663,10 @@ export class LocalNodeService {
|
|
|
1175
1663
|
|
|
1176
1664
|
getIntegrationStatus(): IntegrationStatusSummary {
|
|
1177
1665
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1666
|
+
const runtimeReady = this.socialFound && runtimeGenerated && !this.socialParseError;
|
|
1667
|
+
const connected = runtimeReady && this.networkStarted;
|
|
1668
|
+
const configured = runtimeReady && this.socialConfig.enabled;
|
|
1669
|
+
const running = configured && this.broadcastEnabled && this.networkStarted;
|
|
1181
1670
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
1182
1671
|
const discoveryEnabled =
|
|
1183
1672
|
this.socialConfig.discovery.discoverable &&
|
|
@@ -1199,6 +1688,10 @@ export class LocalNodeService {
|
|
|
1199
1688
|
? "running"
|
|
1200
1689
|
: !configured
|
|
1201
1690
|
? "not configured"
|
|
1691
|
+
: this.networkReconnectTimer
|
|
1692
|
+
? "reconnecting to relay"
|
|
1693
|
+
: this.networkStartupError
|
|
1694
|
+
? this.networkStartupError
|
|
1202
1695
|
: !this.broadcastEnabled
|
|
1203
1696
|
? "broadcast paused"
|
|
1204
1697
|
: "not running";
|
|
@@ -1244,20 +1737,36 @@ export class LocalNodeService {
|
|
|
1244
1737
|
}
|
|
1245
1738
|
|
|
1246
1739
|
async setNetworkModeRuntime(mode: "local" | "lan" | "global-preview") {
|
|
1247
|
-
const
|
|
1740
|
+
const before = {
|
|
1741
|
+
mode: this.networkMode,
|
|
1742
|
+
adapter: this.adapterMode,
|
|
1743
|
+
namespace: this.networkNamespace,
|
|
1744
|
+
port: this.networkPort,
|
|
1745
|
+
};
|
|
1248
1746
|
if (mode !== "local" && mode !== "lan" && mode !== "global-preview") {
|
|
1249
1747
|
throw new Error("invalid_network_mode");
|
|
1250
1748
|
}
|
|
1251
1749
|
this.socialConfig.network.mode = mode;
|
|
1252
1750
|
this.socialConfig.network.adapter = this.adapterForMode(mode);
|
|
1253
1751
|
this.applyResolvedNetworkConfig();
|
|
1254
|
-
|
|
1752
|
+
|
|
1753
|
+
const needsRestart =
|
|
1754
|
+
before.mode !== this.networkMode ||
|
|
1755
|
+
before.adapter !== this.socialConfig.network.adapter ||
|
|
1756
|
+
before.namespace !== this.networkNamespace ||
|
|
1757
|
+
(before.port ?? null) !== (this.networkPort ?? null);
|
|
1758
|
+
|
|
1759
|
+
if (needsRestart) {
|
|
1760
|
+
await this.restartNetworkAdapter("set_network_mode_runtime");
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
this.socialNetworkRequiresRestart = false;
|
|
1255
1764
|
await this.writeSocialRuntime();
|
|
1256
1765
|
return {
|
|
1257
1766
|
mode: this.networkMode,
|
|
1258
|
-
adapter: this.
|
|
1259
|
-
network_requires_restart:
|
|
1260
|
-
note: "Runtime mode updated. Existing social.md is unchanged.",
|
|
1767
|
+
adapter: this.adapterMode,
|
|
1768
|
+
network_requires_restart: false,
|
|
1769
|
+
note: "Runtime mode updated and adapter restarted. Existing social.md is unchanged.",
|
|
1261
1770
|
};
|
|
1262
1771
|
}
|
|
1263
1772
|
|
|
@@ -1272,7 +1781,7 @@ export class LocalNodeService {
|
|
|
1272
1781
|
this.socialConfig.network.adapter = "relay-preview";
|
|
1273
1782
|
this.socialConfig.network.signaling_url = signalingUrl;
|
|
1274
1783
|
this.socialConfig.network.signaling_urls = [signalingUrl];
|
|
1275
|
-
this.socialConfig.network.room = room ||
|
|
1784
|
+
this.socialConfig.network.room = room || DEFAULT_GLOBAL_ROOM;
|
|
1276
1785
|
this.applyResolvedNetworkConfig();
|
|
1277
1786
|
await this.restartNetworkAdapter("quick_connect_global_preview");
|
|
1278
1787
|
this.socialNetworkRequiresRestart = false;
|
|
@@ -1297,7 +1806,7 @@ export class LocalNodeService {
|
|
|
1297
1806
|
port: this.networkPort,
|
|
1298
1807
|
};
|
|
1299
1808
|
|
|
1300
|
-
const loaded = loadSocialConfig(this.
|
|
1809
|
+
const loaded = loadSocialConfig(this.projectRoot);
|
|
1301
1810
|
this.socialConfig = loaded.config;
|
|
1302
1811
|
this.socialSourcePath = loaded.meta.source_path;
|
|
1303
1812
|
this.socialFound = loaded.meta.found;
|
|
@@ -1319,13 +1828,18 @@ export class LocalNodeService {
|
|
|
1319
1828
|
before.namespace !== after.namespace ||
|
|
1320
1829
|
(before.port ?? null) !== (after.port ?? null);
|
|
1321
1830
|
|
|
1831
|
+
if (this.socialNetworkRequiresRestart) {
|
|
1832
|
+
await this.restartNetworkAdapter("reload_social_config");
|
|
1833
|
+
this.socialNetworkRequiresRestart = false;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1322
1836
|
await this.writeSocialRuntime();
|
|
1323
1837
|
|
|
1324
1838
|
return this.getSocialConfigView();
|
|
1325
1839
|
}
|
|
1326
1840
|
|
|
1327
1841
|
async generateDefaultSocialMd() {
|
|
1328
|
-
const result = ensureDefaultSocialMd(this.
|
|
1842
|
+
const result = ensureDefaultSocialMd(this.projectRoot, {
|
|
1329
1843
|
display_name: this.getDefaultDisplayName(),
|
|
1330
1844
|
bio: "Local AI agent connected to SilicaClaw",
|
|
1331
1845
|
tags: ["openclaw", "local-first"],
|
|
@@ -1352,10 +1866,11 @@ export class LocalNodeService {
|
|
|
1352
1866
|
search(keyword: string): PublicProfileSummary[] {
|
|
1353
1867
|
this.ensureLocalDirectoryBaseline();
|
|
1354
1868
|
this.compactCacheInMemory();
|
|
1355
|
-
|
|
1869
|
+
const directMatches = searchDirectory(this.directory, keyword, { presenceTTLms: PRESENCE_TTL_MS }).map((profile) => {
|
|
1356
1870
|
const lastSeenAt = this.directory.presence[profile.agent_id] ?? 0;
|
|
1357
1871
|
return this.toPublicProfileSummary(profile, { last_seen_at: lastSeenAt });
|
|
1358
1872
|
});
|
|
1873
|
+
return this.mergeMessageOnlyAgentSummaries(directMatches, keyword);
|
|
1359
1874
|
}
|
|
1360
1875
|
|
|
1361
1876
|
getPublicProfilePreview(): PublicProfileSummary | null {
|
|
@@ -1412,6 +1927,7 @@ export class LocalNodeService {
|
|
|
1412
1927
|
return {
|
|
1413
1928
|
...message,
|
|
1414
1929
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1930
|
+
avatar_url: profile?.avatar_url || "",
|
|
1415
1931
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1416
1932
|
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1417
1933
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1431,18 +1947,149 @@ export class LocalNodeService {
|
|
|
1431
1947
|
};
|
|
1432
1948
|
}
|
|
1433
1949
|
|
|
1950
|
+
getPrivateMessagingState() {
|
|
1951
|
+
return {
|
|
1952
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1953
|
+
agent_id: this.identity?.agent_id || "",
|
|
1954
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1955
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1956
|
+
message_count: this.privateMessages.length,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
getPrivateConversations(): Array<{
|
|
1961
|
+
conversation_id: string;
|
|
1962
|
+
peer_agent_id: string;
|
|
1963
|
+
peer_display_name: string;
|
|
1964
|
+
peer_avatar_url: string;
|
|
1965
|
+
peer_public_key: string;
|
|
1966
|
+
last_message_at: number | null;
|
|
1967
|
+
unread_count: number;
|
|
1968
|
+
}> {
|
|
1969
|
+
const conversations = new Map<string, {
|
|
1970
|
+
conversation_id: string;
|
|
1971
|
+
peer_agent_id: string;
|
|
1972
|
+
peer_display_name: string;
|
|
1973
|
+
peer_avatar_url: string;
|
|
1974
|
+
peer_public_key: string;
|
|
1975
|
+
last_message_at: number | null;
|
|
1976
|
+
unread_count: number;
|
|
1977
|
+
}>();
|
|
1978
|
+
for (const message of this.privateMessages) {
|
|
1979
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1983
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
1987
|
+
const current = conversations.get(message.conversation_id);
|
|
1988
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
1989
|
+
conversations.set(message.conversation_id, {
|
|
1990
|
+
conversation_id: message.conversation_id,
|
|
1991
|
+
peer_agent_id: peerAgentId,
|
|
1992
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
1993
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
1994
|
+
peer_public_key: peerProfile?.private_encryption_public_key || "",
|
|
1995
|
+
last_message_at: nextLast,
|
|
1996
|
+
unread_count: current?.unread_count || 0,
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
|
|
2003
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
2004
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
2005
|
+
const receiptsByMessageId = new Map(
|
|
2006
|
+
this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
|
|
2007
|
+
);
|
|
2008
|
+
return this.privateMessages
|
|
2009
|
+
.filter((message) => {
|
|
2010
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
2014
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
2015
|
+
return false;
|
|
2016
|
+
}
|
|
2017
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
2018
|
+
})
|
|
2019
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
2020
|
+
.slice(-resolvedLimit)
|
|
2021
|
+
.map((message) => ({
|
|
2022
|
+
message_id: message.message_id,
|
|
2023
|
+
conversation_id: message.conversation_id,
|
|
2024
|
+
from_agent_id: message.from_agent_id,
|
|
2025
|
+
to_agent_id: message.to_agent_id,
|
|
2026
|
+
body: this.decryptPrivateMessageBody(message),
|
|
2027
|
+
created_at: message.created_at,
|
|
2028
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
2029
|
+
delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
|
|
2030
|
+
}));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
async sendPrivateMessage(input: {
|
|
2034
|
+
to_agent_id: string;
|
|
2035
|
+
recipient_encryption_public_key: string;
|
|
2036
|
+
body: string;
|
|
2037
|
+
}): Promise<{ sent: boolean; reason: string; message?: PrivateMessageView }> {
|
|
2038
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
2039
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
2040
|
+
}
|
|
2041
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
2042
|
+
const recipientKey = String(input.recipient_encryption_public_key || "").trim();
|
|
2043
|
+
const body = String(input.body || "").trim();
|
|
2044
|
+
if (toAgentId === this.identity.agent_id) {
|
|
2045
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
2046
|
+
}
|
|
2047
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
2048
|
+
if (!toAgentId || !toPeerId || !recipientKey || !body) {
|
|
2049
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
2050
|
+
}
|
|
2051
|
+
if (typeof this.network.sendDirect !== "function") {
|
|
2052
|
+
return { sent: false, reason: "direct_delivery_not_supported" };
|
|
2053
|
+
}
|
|
2054
|
+
const encrypted = encryptPrivatePayload({
|
|
2055
|
+
plaintext: body,
|
|
2056
|
+
recipient_public_key: recipientKey,
|
|
2057
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
2058
|
+
});
|
|
2059
|
+
const message = signPrivateMessage({
|
|
2060
|
+
identity: this.identity,
|
|
2061
|
+
message_id: createHash("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
2062
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
2063
|
+
to_agent_id: toAgentId,
|
|
2064
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
2065
|
+
recipient_encryption_public_key: recipientKey,
|
|
2066
|
+
ciphertext: encrypted.ciphertext,
|
|
2067
|
+
nonce: encrypted.nonce,
|
|
2068
|
+
created_at: Date.now(),
|
|
2069
|
+
});
|
|
2070
|
+
this.ingestPrivateMessage(message);
|
|
2071
|
+
await this.persistPrivateMessages();
|
|
2072
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
2073
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
2074
|
+
return { sent: true, reason: "sent", message: view };
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1434
2077
|
getOpenClawBridgeStatus(): OpenClawBridgeStatus {
|
|
2078
|
+
const now = Date.now();
|
|
2079
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
2080
|
+
return this.openclawBridgeStatusCache.value;
|
|
2081
|
+
}
|
|
1435
2082
|
const integration = this.getIntegrationStatus();
|
|
1436
|
-
const openclawInstallation = detectOpenClawInstallation(this.
|
|
1437
|
-
const openclawRuntime =
|
|
2083
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
2084
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1438
2085
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1439
2086
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1440
|
-
workspaceRoot: this.
|
|
2087
|
+
workspaceRoot: this.projectRoot,
|
|
1441
2088
|
connectedToSilicaclaw: integration.connected_to_silicaclaw,
|
|
1442
2089
|
openclawRunning: openclawRuntime.running,
|
|
1443
2090
|
skillInstalled: skillInstallation.installed,
|
|
1444
2091
|
});
|
|
1445
|
-
|
|
2092
|
+
const value: OpenClawBridgeStatus = {
|
|
1446
2093
|
enabled: this.socialConfig.enabled,
|
|
1447
2094
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1448
2095
|
public_enabled: integration.public_enabled,
|
|
@@ -1498,16 +2145,26 @@ export class LocalNodeService {
|
|
|
1498
2145
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1499
2146
|
},
|
|
1500
2147
|
};
|
|
2148
|
+
this.openclawBridgeStatusCache = {
|
|
2149
|
+
value,
|
|
2150
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
2151
|
+
};
|
|
2152
|
+
return value;
|
|
1501
2153
|
}
|
|
1502
2154
|
|
|
1503
|
-
async installOpenClawSkill() {
|
|
2155
|
+
async installOpenClawSkill(skillName?: string) {
|
|
1504
2156
|
const scriptPath = resolve(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
|
|
1505
|
-
const
|
|
2157
|
+
const args = [scriptPath];
|
|
2158
|
+
if (skillName) {
|
|
2159
|
+
args.push(`--skill=${skillName}`);
|
|
2160
|
+
}
|
|
2161
|
+
const { stdout } = await execFileAsync(process.execPath, args, {
|
|
1506
2162
|
cwd: this.workspaceRoot,
|
|
1507
|
-
env: process.env,
|
|
2163
|
+
env: { ...process.env, SILICACLAW_WORKSPACE_DIR: this.projectRoot },
|
|
1508
2164
|
maxBuffer: 1024 * 1024,
|
|
1509
2165
|
});
|
|
1510
2166
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
2167
|
+
this.invalidateOpenClawCaches();
|
|
1511
2168
|
return {
|
|
1512
2169
|
...parsed,
|
|
1513
2170
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1528,12 +2185,12 @@ export class LocalNodeService {
|
|
|
1528
2185
|
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
1529
2186
|
const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
|
|
1530
2187
|
const legacySkillDir = resolve(homeDir, "skills");
|
|
1531
|
-
const openclawSourceDir =
|
|
1532
|
-
const openclawRuntime =
|
|
2188
|
+
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
2189
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1533
2190
|
|
|
1534
2191
|
return {
|
|
1535
|
-
bridge_api_base:
|
|
1536
|
-
openclaw_detected: detectOpenClawInstallation(this.
|
|
2192
|
+
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
2193
|
+
openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
|
|
1537
2194
|
openclaw_running: openclawRuntime.running,
|
|
1538
2195
|
openclaw_gateway_host: OPENCLAW_GATEWAY_HOST,
|
|
1539
2196
|
openclaw_gateway_port: openclawRuntime.configured_gateway_port,
|
|
@@ -1542,7 +2199,7 @@ export class LocalNodeService {
|
|
|
1542
2199
|
openclaw_workspace_skill_dir: workspaceSkillDir,
|
|
1543
2200
|
openclaw_legacy_skill_dir: legacySkillDir,
|
|
1544
2201
|
silicaclaw_env_template_path: resolve(this.workspaceRoot, "openclaw-owner-forward.env.example"),
|
|
1545
|
-
recommended_skill_name: "silicaclaw-
|
|
2202
|
+
recommended_skill_name: "silicaclaw-bridge-setup",
|
|
1546
2203
|
recommended_install_command: "silicaclaw openclaw-skill-install",
|
|
1547
2204
|
recommended_owner_forward_env: {
|
|
1548
2205
|
OPENCLAW_SOURCE_DIR: openclawSourceDir,
|
|
@@ -1560,6 +2217,7 @@ export class LocalNodeService {
|
|
|
1560
2217
|
].join(" "),
|
|
1561
2218
|
notes: [
|
|
1562
2219
|
"Install and maintain the skill from SilicaClaw; do not edit OpenClaw core source for this integration.",
|
|
2220
|
+
"Use silicaclaw-bridge-setup first when OpenClaw still needs local install, readiness checks, or troubleshooting guidance.",
|
|
1563
2221
|
"OpenClaw learns broadcasts via the installed skill under ~/.openclaw/workspace/skills/.",
|
|
1564
2222
|
"Runtime detection prefers the actual OpenClaw gateway listener port, then falls back to OpenClaw's own openclaw.json gateway.port.",
|
|
1565
2223
|
"Owner delivery runs through OpenClaw's own message channel stack after the skill forwards a summary.",
|
|
@@ -1579,6 +2237,12 @@ export class LocalNodeService {
|
|
|
1579
2237
|
const skillPath = resolve(dir.path, "SKILL.md");
|
|
1580
2238
|
const versionPath = resolve(dir.path, "VERSION");
|
|
1581
2239
|
const manifest = readJsonFileSafe(manifestPath);
|
|
2240
|
+
const references = (manifest?.references && typeof manifest.references === "object")
|
|
2241
|
+
? manifest.references as Record<string, unknown>
|
|
2242
|
+
: null;
|
|
2243
|
+
const ownerDialogueCheatsheetPath = references?.owner_dialogue_cheatsheet_zh
|
|
2244
|
+
? resolve(dir.path, String(references.owner_dialogue_cheatsheet_zh))
|
|
2245
|
+
: null;
|
|
1582
2246
|
const name = String(manifest?.name || dir.name);
|
|
1583
2247
|
const capabilities = Array.isArray(manifest?.capabilities)
|
|
1584
2248
|
? manifest.capabilities.map((item) => String(item))
|
|
@@ -1598,6 +2262,9 @@ export class LocalNodeService {
|
|
|
1598
2262
|
skill_path: existsSync(skillPath) ? skillPath : null,
|
|
1599
2263
|
capabilities,
|
|
1600
2264
|
transport: manifest?.transport || null,
|
|
2265
|
+
owner_dialogue_cheatsheet_path: ownerDialogueCheatsheetPath && existsSync(ownerDialogueCheatsheetPath) ? ownerDialogueCheatsheetPath : null,
|
|
2266
|
+
owner_dialogue_examples_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetPreview(ownerDialogueCheatsheetPath) : [],
|
|
2267
|
+
owner_dialogue_sections_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetSections(ownerDialogueCheatsheetPath) : [],
|
|
1601
2268
|
installed_in_openclaw: installedInWorkspace || installedInLegacy,
|
|
1602
2269
|
install_mode: installedInWorkspace ? "workspace" : installedInLegacy ? "legacy" : "not_installed",
|
|
1603
2270
|
installed_path: installedInWorkspace ? installedWorkspacePath : installedInLegacy ? installedLegacyPath : null,
|
|
@@ -1612,6 +2279,12 @@ export class LocalNodeService {
|
|
|
1612
2279
|
const skillPath = resolve(dir.path, "SKILL.md");
|
|
1613
2280
|
const versionPath = resolve(dir.path, "VERSION");
|
|
1614
2281
|
const manifest = readJsonFileSafe(manifestPath);
|
|
2282
|
+
const references = (manifest?.references && typeof manifest.references === "object")
|
|
2283
|
+
? manifest.references as Record<string, unknown>
|
|
2284
|
+
: null;
|
|
2285
|
+
const ownerDialogueCheatsheetPath = references?.owner_dialogue_cheatsheet_zh
|
|
2286
|
+
? resolve(dir.path, String(references.owner_dialogue_cheatsheet_zh))
|
|
2287
|
+
: null;
|
|
1615
2288
|
return {
|
|
1616
2289
|
key: `${dir.install_mode}:${dir.name}`,
|
|
1617
2290
|
name: String(manifest?.name || dir.name),
|
|
@@ -1623,10 +2296,43 @@ export class LocalNodeService {
|
|
|
1623
2296
|
manifest_path: existsSync(manifestPath) ? manifestPath : null,
|
|
1624
2297
|
skill_path: existsSync(skillPath) ? skillPath : null,
|
|
1625
2298
|
capabilities: Array.isArray(manifest?.capabilities) ? manifest.capabilities.map((item) => String(item)) : [],
|
|
2299
|
+
owner_dialogue_cheatsheet_path: ownerDialogueCheatsheetPath && existsSync(ownerDialogueCheatsheetPath) ? ownerDialogueCheatsheetPath : null,
|
|
2300
|
+
owner_dialogue_examples_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetPreview(ownerDialogueCheatsheetPath) : [],
|
|
2301
|
+
owner_dialogue_sections_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetSections(ownerDialogueCheatsheetPath) : [],
|
|
1626
2302
|
bundled_source_path: bundledSkills.find((item) => item.name === String(manifest?.name || dir.name))?.source_path || null,
|
|
1627
2303
|
};
|
|
1628
2304
|
});
|
|
1629
2305
|
|
|
2306
|
+
const installedSkillVersions = new Map(installedSkills.map((item) => [item.name, item.version]));
|
|
2307
|
+
const bundledSkillsWithUpdateState = bundledSkills.map((skill) => {
|
|
2308
|
+
const installedVersion = installedSkillVersions.get(skill.name) || "";
|
|
2309
|
+
const updateAvailable = Boolean(
|
|
2310
|
+
skill.installed_in_openclaw &&
|
|
2311
|
+
installedVersion &&
|
|
2312
|
+
skill.version &&
|
|
2313
|
+
compareVersionTokens(installedVersion, skill.version) < 0
|
|
2314
|
+
);
|
|
2315
|
+
return {
|
|
2316
|
+
...skill,
|
|
2317
|
+
installed_version: installedVersion || null,
|
|
2318
|
+
update_available: updateAvailable,
|
|
2319
|
+
};
|
|
2320
|
+
});
|
|
2321
|
+
const bundledSkillVersions = new Map(bundledSkillsWithUpdateState.map((item) => [item.name, item.version]));
|
|
2322
|
+
const installedSkillsWithUpdateState = installedSkills.map((skill) => {
|
|
2323
|
+
const bundledVersion = bundledSkillVersions.get(skill.name) || "";
|
|
2324
|
+
const updateAvailable = Boolean(
|
|
2325
|
+
bundledVersion &&
|
|
2326
|
+
skill.version &&
|
|
2327
|
+
compareVersionTokens(skill.version, bundledVersion) < 0
|
|
2328
|
+
);
|
|
2329
|
+
return {
|
|
2330
|
+
...skill,
|
|
2331
|
+
bundled_version: bundledVersion || null,
|
|
2332
|
+
update_available: updateAvailable,
|
|
2333
|
+
};
|
|
2334
|
+
});
|
|
2335
|
+
|
|
1630
2336
|
return {
|
|
1631
2337
|
openclaw: {
|
|
1632
2338
|
detected: bridge.openclaw_installation.detected,
|
|
@@ -1637,13 +2343,14 @@ export class LocalNodeService {
|
|
|
1637
2343
|
legacy_install_root: legacyInstallRoot,
|
|
1638
2344
|
},
|
|
1639
2345
|
summary: {
|
|
1640
|
-
bundled_count:
|
|
1641
|
-
installed_count:
|
|
1642
|
-
installed_bundled_count:
|
|
2346
|
+
bundled_count: bundledSkillsWithUpdateState.length,
|
|
2347
|
+
installed_count: installedSkillsWithUpdateState.length,
|
|
2348
|
+
installed_bundled_count: bundledSkillsWithUpdateState.filter((item) => item.installed_in_openclaw).length,
|
|
2349
|
+
update_available_count: bundledSkillsWithUpdateState.filter((item) => item.update_available).length,
|
|
1643
2350
|
},
|
|
1644
2351
|
install_action: bridge.skill_learning.install_action,
|
|
1645
|
-
bundled_skills:
|
|
1646
|
-
installed_skills:
|
|
2352
|
+
bundled_skills: bundledSkillsWithUpdateState,
|
|
2353
|
+
installed_skills: installedSkillsWithUpdateState,
|
|
1647
2354
|
};
|
|
1648
2355
|
}
|
|
1649
2356
|
|
|
@@ -1889,20 +2596,25 @@ export class LocalNodeService {
|
|
|
1889
2596
|
profile: this.profile,
|
|
1890
2597
|
};
|
|
1891
2598
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
1892
|
-
const
|
|
2599
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2600
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
1893
2601
|
|
|
1894
2602
|
try {
|
|
1895
|
-
|
|
2603
|
+
if (shouldPublishProfile) {
|
|
2604
|
+
await this.publish("profile", profileRecord);
|
|
2605
|
+
}
|
|
1896
2606
|
await this.publish("presence", presenceRecord);
|
|
1897
|
-
for (const
|
|
1898
|
-
await this.publish(
|
|
2607
|
+
for (const message of replayMessages) {
|
|
2608
|
+
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
1899
2609
|
}
|
|
1900
2610
|
} catch (error) {
|
|
1901
2611
|
const message = error instanceof Error ? error.message : String(error);
|
|
1902
2612
|
this.lastBroadcastErrorAt = Date.now();
|
|
1903
2613
|
this.lastBroadcastError = message;
|
|
1904
2614
|
this.broadcastFailureCount += 1;
|
|
2615
|
+
this.consecutiveBroadcastFailures += 1;
|
|
1905
2616
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2617
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
1906
2618
|
return { sent: false, reason: "publish_failed", error: message };
|
|
1907
2619
|
}
|
|
1908
2620
|
|
|
@@ -1910,19 +2622,75 @@ export class LocalNodeService {
|
|
|
1910
2622
|
this.broadcastCount += 1;
|
|
1911
2623
|
this.lastBroadcastError = null;
|
|
1912
2624
|
this.lastBroadcastErrorAt = 0;
|
|
2625
|
+
this.consecutiveBroadcastFailures = 0;
|
|
1913
2626
|
|
|
1914
2627
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
1915
2628
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
1916
|
-
for (const record of indexRecords) {
|
|
1917
|
-
this.directory = ingestIndexRecord(this.directory, record);
|
|
1918
|
-
}
|
|
1919
2629
|
this.compactCacheInMemory();
|
|
1920
2630
|
await this.persistCache();
|
|
1921
2631
|
|
|
1922
|
-
await this.log(
|
|
2632
|
+
await this.log(
|
|
2633
|
+
"info",
|
|
2634
|
+
`Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
|
|
2635
|
+
);
|
|
1923
2636
|
return { sent: true, reason };
|
|
1924
2637
|
}
|
|
1925
2638
|
|
|
2639
|
+
private shouldPublishProfileRecord(
|
|
2640
|
+
profileRecord: SignedProfileRecord,
|
|
2641
|
+
reason: string,
|
|
2642
|
+
now = Date.now()
|
|
2643
|
+
): boolean {
|
|
2644
|
+
if (reason !== "interval") {
|
|
2645
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2646
|
+
this.lastProfileBroadcastAt = now;
|
|
2647
|
+
return true;
|
|
2648
|
+
}
|
|
2649
|
+
const signature = profileRecord.profile.signature;
|
|
2650
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2651
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2652
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2656
|
+
this.lastProfileBroadcastAt = now;
|
|
2657
|
+
return true;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
|
|
2661
|
+
const recoveryThreshold = 3;
|
|
2662
|
+
const recoveryCooldownMs = 60_000;
|
|
2663
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
this.broadcastRecoveryInFlight = true;
|
|
2677
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2678
|
+
try {
|
|
2679
|
+
await this.log(
|
|
2680
|
+
"warn",
|
|
2681
|
+
`Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`
|
|
2682
|
+
);
|
|
2683
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2684
|
+
} catch (recoveryError) {
|
|
2685
|
+
await this.log(
|
|
2686
|
+
"error",
|
|
2687
|
+
`Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
2688
|
+
);
|
|
2689
|
+
} finally {
|
|
2690
|
+
this.broadcastRecoveryInFlight = false;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
1926
2694
|
private async hydrateFromDisk(): Promise<void> {
|
|
1927
2695
|
this.initState = {
|
|
1928
2696
|
identity_auto_created: false,
|
|
@@ -1939,7 +2707,7 @@ export class LocalNodeService {
|
|
|
1939
2707
|
socialConfig: this.socialConfig,
|
|
1940
2708
|
existingIdentity,
|
|
1941
2709
|
generatedIdentity: createIdentity(),
|
|
1942
|
-
rootDir: this.
|
|
2710
|
+
rootDir: this.projectRoot,
|
|
1943
2711
|
});
|
|
1944
2712
|
this.identity = resolvedIdentity.identity;
|
|
1945
2713
|
this.resolvedIdentitySource = resolvedIdentity.source;
|
|
@@ -1952,28 +2720,35 @@ export class LocalNodeService {
|
|
|
1952
2720
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
1953
2721
|
}
|
|
1954
2722
|
await this.identityRepo.set(this.identity);
|
|
2723
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || createPrivateEncryptionKeyPair();
|
|
2724
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
1955
2725
|
|
|
1956
2726
|
const existingProfile = await this.profileRepo.get();
|
|
1957
2727
|
const profileInput = resolveProfileInputWithSocial({
|
|
1958
2728
|
socialConfig: this.socialConfig,
|
|
1959
2729
|
agentId: this.identity.agent_id,
|
|
1960
2730
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
1961
|
-
rootDir: this.
|
|
2731
|
+
rootDir: this.projectRoot,
|
|
1962
2732
|
});
|
|
1963
|
-
this.profile = signProfile(
|
|
2733
|
+
this.profile = signProfile({
|
|
2734
|
+
...profileInput,
|
|
2735
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2736
|
+
}, this.identity);
|
|
1964
2737
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
1965
2738
|
this.initState.profile_auto_created = true;
|
|
1966
2739
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
1967
2740
|
}
|
|
1968
2741
|
await this.profileRepo.set(this.profile);
|
|
1969
2742
|
|
|
1970
|
-
this.directory =
|
|
2743
|
+
this.directory = createEmptyDirectoryState();
|
|
1971
2744
|
this.messageGovernance = {
|
|
1972
2745
|
...this.defaultMessageGovernance(),
|
|
1973
2746
|
...(await this.socialMessageGovernanceRepo.get()),
|
|
1974
2747
|
};
|
|
1975
2748
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
1976
2749
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2750
|
+
this.privateMessages = this.normalizePrivateMessages(await this.privateMessageRepo.get());
|
|
2751
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
1977
2752
|
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
1978
2753
|
this.compactCacheInMemory();
|
|
1979
2754
|
await this.persistCache();
|
|
@@ -1990,9 +2765,12 @@ export class LocalNodeService {
|
|
|
1990
2765
|
socialConfig: this.socialConfig,
|
|
1991
2766
|
agentId: this.identity.agent_id,
|
|
1992
2767
|
existingProfile: this.profile,
|
|
1993
|
-
rootDir: this.
|
|
2768
|
+
rootDir: this.projectRoot,
|
|
1994
2769
|
});
|
|
1995
|
-
const nextProfile = signProfile(
|
|
2770
|
+
const nextProfile = signProfile({
|
|
2771
|
+
...nextProfileInput,
|
|
2772
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2773
|
+
}, this.identity);
|
|
1996
2774
|
this.profile = nextProfile;
|
|
1997
2775
|
await this.profileRepo.set(nextProfile);
|
|
1998
2776
|
|
|
@@ -2057,7 +2835,8 @@ export class LocalNodeService {
|
|
|
2057
2835
|
|
|
2058
2836
|
private async onMessage(
|
|
2059
2837
|
topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
|
|
2060
|
-
data: unknown
|
|
2838
|
+
data: unknown,
|
|
2839
|
+
meta?: { peerId?: string }
|
|
2061
2840
|
): Promise<void> {
|
|
2062
2841
|
this.receivedCount += 1;
|
|
2063
2842
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
@@ -2075,6 +2854,9 @@ export class LocalNodeService {
|
|
|
2075
2854
|
return;
|
|
2076
2855
|
}
|
|
2077
2856
|
}
|
|
2857
|
+
if (meta?.peerId && record.profile.agent_id) {
|
|
2858
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2859
|
+
}
|
|
2078
2860
|
|
|
2079
2861
|
this.directory = ingestProfileRecord(this.directory, record);
|
|
2080
2862
|
this.compactCacheInMemory();
|
|
@@ -2094,6 +2876,9 @@ export class LocalNodeService {
|
|
|
2094
2876
|
return;
|
|
2095
2877
|
}
|
|
2096
2878
|
}
|
|
2879
|
+
if (meta?.peerId && record.agent_id) {
|
|
2880
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2881
|
+
}
|
|
2097
2882
|
|
|
2098
2883
|
this.directory = ingestPresenceRecord(this.directory, record);
|
|
2099
2884
|
this.compactCacheInMemory();
|
|
@@ -2110,6 +2895,13 @@ export class LocalNodeService {
|
|
|
2110
2895
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2111
2896
|
return;
|
|
2112
2897
|
}
|
|
2898
|
+
if (meta?.peerId && record.agent_id) {
|
|
2899
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2900
|
+
}
|
|
2901
|
+
if (this.hasSocialMessage(record.message_id)) {
|
|
2902
|
+
await this.publishObservationForMessage(record);
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2113
2905
|
const governanceReason = this.getIncomingSocialMessageRejectionReason(record);
|
|
2114
2906
|
if (governanceReason) {
|
|
2115
2907
|
await this.log("warn", `Rejected social message (${record.message_id.slice(0, 10)}): ${governanceReason}`);
|
|
@@ -2144,12 +2936,42 @@ export class LocalNodeService {
|
|
|
2144
2936
|
await this.persistCache();
|
|
2145
2937
|
}
|
|
2146
2938
|
|
|
2939
|
+
private async onDirectMessage(
|
|
2940
|
+
topic: "private.message" | "private.message.receipt",
|
|
2941
|
+
data: unknown,
|
|
2942
|
+
meta?: { peerId?: string }
|
|
2943
|
+
): Promise<void> {
|
|
2944
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2945
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2946
|
+
if (!record || !verifyPrivateMessage(record)) {
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
this.ingestPrivateMessage(record);
|
|
2953
|
+
await this.persistPrivateMessages();
|
|
2954
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
2959
|
+
if (!receipt || !verifyPrivateMessageReceipt(receipt)) {
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
2966
|
+
await this.persistPrivateMessageReceipts();
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2147
2969
|
private startBroadcastLoop(): void {
|
|
2148
2970
|
if (this.broadcaster) {
|
|
2149
2971
|
clearInterval(this.broadcaster);
|
|
2150
2972
|
}
|
|
2151
2973
|
|
|
2152
|
-
if (!this.broadcastEnabled) {
|
|
2974
|
+
if (!this.broadcastEnabled || !this.networkStarted) {
|
|
2153
2975
|
return;
|
|
2154
2976
|
}
|
|
2155
2977
|
|
|
@@ -2169,21 +2991,29 @@ export class LocalNodeService {
|
|
|
2169
2991
|
if (this.subscriptionsBound) {
|
|
2170
2992
|
return;
|
|
2171
2993
|
}
|
|
2172
|
-
this.network.subscribe("profile", (data: SignedProfileRecord) => {
|
|
2173
|
-
this.onMessage("profile", data);
|
|
2994
|
+
this.network.subscribe("profile", (data: SignedProfileRecord, meta?: { peerId?: string }) => {
|
|
2995
|
+
this.onMessage("profile", data, meta);
|
|
2174
2996
|
});
|
|
2175
|
-
this.network.subscribe("presence", (data: PresenceRecord) => {
|
|
2176
|
-
this.onMessage("presence", data);
|
|
2997
|
+
this.network.subscribe("presence", (data: PresenceRecord, meta?: { peerId?: string }) => {
|
|
2998
|
+
this.onMessage("presence", data, meta);
|
|
2177
2999
|
});
|
|
2178
|
-
this.network.subscribe("index", (data: IndexRefRecord) => {
|
|
2179
|
-
this.onMessage("index", data);
|
|
3000
|
+
this.network.subscribe("index", (data: IndexRefRecord, meta?: { peerId?: string }) => {
|
|
3001
|
+
this.onMessage("index", data, meta);
|
|
2180
3002
|
});
|
|
2181
|
-
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord) => {
|
|
2182
|
-
this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
|
|
3003
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord, meta?: { peerId?: string }) => {
|
|
3004
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2183
3005
|
});
|
|
2184
|
-
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord) => {
|
|
2185
|
-
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
|
|
3006
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
|
|
3007
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2186
3008
|
});
|
|
3009
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
3010
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3011
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
3012
|
+
});
|
|
3013
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3014
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
2187
3017
|
this.subscriptionsBound = true;
|
|
2188
3018
|
}
|
|
2189
3019
|
|
|
@@ -2271,6 +3101,7 @@ export class LocalNodeService {
|
|
|
2271
3101
|
}
|
|
2272
3102
|
|
|
2273
3103
|
private async restartNetworkAdapter(reason: string): Promise<void> {
|
|
3104
|
+
this.clearNetworkReconnectTimer();
|
|
2274
3105
|
const previous = this.network;
|
|
2275
3106
|
try {
|
|
2276
3107
|
await previous.stop();
|
|
@@ -2282,19 +3113,123 @@ export class LocalNodeService {
|
|
|
2282
3113
|
this.network = next.adapter;
|
|
2283
3114
|
this.adapterMode = next.mode;
|
|
2284
3115
|
this.networkPort = next.port;
|
|
2285
|
-
|
|
2286
|
-
await this.network.start();
|
|
3116
|
+
this.subscriptionsBound = false;
|
|
2287
3117
|
this.bindNetworkSubscriptions();
|
|
2288
|
-
this.
|
|
3118
|
+
await this.startNetworkAdapterWithRetry(reason);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
private clearNetworkReconnectTimer(): void {
|
|
3122
|
+
if (this.networkReconnectTimer) {
|
|
3123
|
+
clearTimeout(this.networkReconnectTimer);
|
|
3124
|
+
this.networkReconnectTimer = null;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
private async startNetworkAdapterWithRetry(reason: string): Promise<void> {
|
|
3129
|
+
this.clearNetworkReconnectTimer();
|
|
3130
|
+
try {
|
|
3131
|
+
await this.network.start();
|
|
3132
|
+
this.networkStarted = true;
|
|
3133
|
+
this.networkStartupError = null;
|
|
3134
|
+
this.networkReconnectDelayMs = 5_000;
|
|
3135
|
+
await this.log(
|
|
3136
|
+
"info",
|
|
3137
|
+
`Local node started (${this.adapterMode}, mode=${this.networkMode}, signaling=${this.webrtcSignalingUrls[0] || "-"}, room=${this.webrtcRoom})`
|
|
3138
|
+
);
|
|
3139
|
+
this.startBroadcastLoop();
|
|
3140
|
+
if (this.broadcastEnabled && this.profile?.public_enabled) {
|
|
3141
|
+
try {
|
|
3142
|
+
await this.broadcastNow(reason);
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
await this.log(
|
|
3145
|
+
"warn",
|
|
3146
|
+
`Initial broadcast failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
} catch (error) {
|
|
3151
|
+
this.networkStarted = false;
|
|
3152
|
+
this.networkStartupError = error instanceof Error ? error.message : String(error);
|
|
3153
|
+
await this.log(
|
|
3154
|
+
"warn",
|
|
3155
|
+
`Network start failed (${this.adapterMode}, mode=${this.networkMode}): ${this.networkStartupError}`
|
|
3156
|
+
);
|
|
3157
|
+
this.scheduleNetworkReconnect();
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
2289
3160
|
|
|
2290
|
-
|
|
2291
|
-
|
|
3161
|
+
private scheduleNetworkReconnect(): void {
|
|
3162
|
+
if (this.networkReconnectTimer) {
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const delayMs = this.networkReconnectDelayMs;
|
|
3166
|
+
this.networkReconnectTimer = setTimeout(() => {
|
|
3167
|
+
this.networkReconnectTimer = null;
|
|
3168
|
+
void this.startNetworkAdapterWithRetry("adapter_reconnect");
|
|
3169
|
+
}, delayMs);
|
|
3170
|
+
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
3174
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
3175
|
+
return 0;
|
|
3176
|
+
}
|
|
3177
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
3178
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
3179
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
3180
|
+
return 0;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
3184
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
3185
|
+
);
|
|
3186
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
3187
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
3188
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
3189
|
+
|
|
3190
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
3191
|
+
const keptRemoteProfiles = [
|
|
3192
|
+
...onlineRemoteProfiles,
|
|
3193
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
3194
|
+
];
|
|
3195
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
3196
|
+
const removedIds = remoteProfiles
|
|
3197
|
+
.map((profile) => profile.agent_id)
|
|
3198
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
3199
|
+
if (removedIds.length === 0) {
|
|
3200
|
+
return 0;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
const next = createEmptyDirectoryState();
|
|
3204
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
3205
|
+
if (selfProfile) {
|
|
3206
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
3207
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
3208
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
3209
|
+
next.presence[selfAgentId] = selfPresence;
|
|
3210
|
+
}
|
|
3211
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
3212
|
+
next.index = rebuilt.index;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
for (const profile of keptRemoteProfiles) {
|
|
3216
|
+
next.profiles[profile.agent_id] = profile;
|
|
3217
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
3218
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
3219
|
+
next.presence[profile.agent_id] = seenAt;
|
|
3220
|
+
}
|
|
3221
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
3222
|
+
next.index = rebuilt.index;
|
|
2292
3223
|
}
|
|
3224
|
+
|
|
3225
|
+
this.directory = dedupeIndex(next);
|
|
3226
|
+
return removedIds.length;
|
|
2293
3227
|
}
|
|
2294
3228
|
|
|
2295
3229
|
private compactCacheInMemory(): number {
|
|
2296
3230
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2297
3231
|
this.directory = dedupeIndex(cleaned.state);
|
|
3232
|
+
this.pruneRemoteProfilesInMemory();
|
|
2298
3233
|
return cleaned.removed;
|
|
2299
3234
|
}
|
|
2300
3235
|
|
|
@@ -2304,7 +3239,22 @@ export class LocalNodeService {
|
|
|
2304
3239
|
}
|
|
2305
3240
|
|
|
2306
3241
|
private async persistCache(): Promise<void> {
|
|
2307
|
-
|
|
3242
|
+
const persisted = createEmptyDirectoryState();
|
|
3243
|
+
if (this.profile) {
|
|
3244
|
+
const selfProfileRecord: SignedProfileRecord = {
|
|
3245
|
+
type: "profile",
|
|
3246
|
+
profile: this.profile,
|
|
3247
|
+
};
|
|
3248
|
+
this.directory = ingestProfileRecord(this.directory, selfProfileRecord);
|
|
3249
|
+
persisted.profiles[this.profile.agent_id] = this.profile;
|
|
3250
|
+
const selfLastSeenAt = this.directory.presence[this.profile.agent_id];
|
|
3251
|
+
if (typeof selfLastSeenAt === "number" && Number.isFinite(selfLastSeenAt)) {
|
|
3252
|
+
persisted.presence[this.profile.agent_id] = selfLastSeenAt;
|
|
3253
|
+
}
|
|
3254
|
+
const indexed = rebuildIndexForProfile(persisted, this.profile);
|
|
3255
|
+
persisted.index = indexed.index;
|
|
3256
|
+
}
|
|
3257
|
+
await this.cacheRepo.set(persisted);
|
|
2308
3258
|
}
|
|
2309
3259
|
|
|
2310
3260
|
private async persistSocialMessages(): Promise<void> {
|
|
@@ -2315,6 +3265,57 @@ export class LocalNodeService {
|
|
|
2315
3265
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2316
3266
|
}
|
|
2317
3267
|
|
|
3268
|
+
private async persistPrivateMessages(): Promise<void> {
|
|
3269
|
+
this.privateMessagesPersistDirty = true;
|
|
3270
|
+
if (this.privateMessagesPersistTimer) {
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
3274
|
+
this.flushPrivateMessagesPersist().catch(() => {});
|
|
3275
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
private async persistPrivateMessageReceipts(): Promise<void> {
|
|
3279
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
3280
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
3284
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => {});
|
|
3285
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
private async flushPrivatePersistence(): Promise<void> {
|
|
3289
|
+
await Promise.all([
|
|
3290
|
+
this.flushPrivateMessagesPersist(),
|
|
3291
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
3292
|
+
]);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
private async flushPrivateMessagesPersist(): Promise<void> {
|
|
3296
|
+
if (this.privateMessagesPersistTimer) {
|
|
3297
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
3298
|
+
this.privateMessagesPersistTimer = null;
|
|
3299
|
+
}
|
|
3300
|
+
if (!this.privateMessagesPersistDirty) {
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
this.privateMessagesPersistDirty = false;
|
|
3304
|
+
await this.privateMessageRepo.set(this.privateMessages);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
private async flushPrivateMessageReceiptsPersist(): Promise<void> {
|
|
3308
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3309
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
3310
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
3311
|
+
}
|
|
3312
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
3316
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
2318
3319
|
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
2319
3320
|
await this.logRepo.append({
|
|
2320
3321
|
level,
|
|
@@ -2330,6 +3331,19 @@ export class LocalNodeService {
|
|
|
2330
3331
|
return (this.network as any).getDiagnostics();
|
|
2331
3332
|
}
|
|
2332
3333
|
|
|
3334
|
+
private getResolvedRealtimeNetworkSummary() {
|
|
3335
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
3336
|
+
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
3337
|
+
return {
|
|
3338
|
+
diagnostics,
|
|
3339
|
+
signaling_url: diagnostics?.signaling_url ?? (relayCapable ? this.webrtcSignalingUrls[0] ?? null : null),
|
|
3340
|
+
signaling_endpoints: diagnostics?.signaling_endpoints ?? (relayCapable ? this.webrtcSignalingUrls : []),
|
|
3341
|
+
room: diagnostics?.room ?? (relayCapable ? this.webrtcRoom : null),
|
|
3342
|
+
bootstrap_sources: diagnostics?.bootstrap_sources ?? (relayCapable ? this.webrtcBootstrapSources : []),
|
|
3343
|
+
seed_peers_count: diagnostics?.seed_peers_count ?? this.webrtcSeedPeers.length,
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
|
|
2333
3347
|
private toPublicProfileSummary(
|
|
2334
3348
|
profile: PublicProfile,
|
|
2335
3349
|
options?: { last_seen_at?: number }
|
|
@@ -2358,6 +3372,7 @@ export class LocalNodeService {
|
|
|
2358
3372
|
|
|
2359
3373
|
return buildPublicProfileSummary({
|
|
2360
3374
|
profile,
|
|
3375
|
+
is_self: isSelf,
|
|
2361
3376
|
online,
|
|
2362
3377
|
last_seen_at: lastSeenAt || null,
|
|
2363
3378
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2373,6 +3388,73 @@ export class LocalNodeService {
|
|
|
2373
3388
|
});
|
|
2374
3389
|
}
|
|
2375
3390
|
|
|
3391
|
+
private mergeMessageOnlyAgentSummaries(
|
|
3392
|
+
summaries: PublicProfileSummary[],
|
|
3393
|
+
keyword: string
|
|
3394
|
+
): PublicProfileSummary[] {
|
|
3395
|
+
const normalizedKeyword = String(keyword || "").trim().toLowerCase();
|
|
3396
|
+
const knownAgentIds = new Set(summaries.map((item) => item.agent_id));
|
|
3397
|
+
const messageOnly: PublicProfileSummary[] = [];
|
|
3398
|
+
|
|
3399
|
+
for (const message of this.socialMessages) {
|
|
3400
|
+
if (!message?.agent_id || knownAgentIds.has(message.agent_id)) {
|
|
3401
|
+
continue;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const displayName = String(message.display_name || "Unnamed").trim() || "Unnamed";
|
|
3405
|
+
if (normalizedKeyword) {
|
|
3406
|
+
const haystacks = [
|
|
3407
|
+
displayName.toLowerCase(),
|
|
3408
|
+
message.agent_id.toLowerCase(),
|
|
3409
|
+
String(message.topic || "").toLowerCase(),
|
|
3410
|
+
];
|
|
3411
|
+
if (!haystacks.some((value) => value.includes(normalizedKeyword))) {
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
knownAgentIds.add(message.agent_id);
|
|
3417
|
+
messageOnly.push(
|
|
3418
|
+
buildPublicProfileSummary({
|
|
3419
|
+
profile: {
|
|
3420
|
+
agent_id: message.agent_id,
|
|
3421
|
+
display_name: displayName,
|
|
3422
|
+
bio: "Seen from signed public message. Profile/presence not synced yet.",
|
|
3423
|
+
tags: ["message-only"],
|
|
3424
|
+
avatar_url: "",
|
|
3425
|
+
public_enabled: true,
|
|
3426
|
+
updated_at: message.created_at,
|
|
3427
|
+
signature: "",
|
|
3428
|
+
},
|
|
3429
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
3430
|
+
online: false,
|
|
3431
|
+
last_seen_at: null,
|
|
3432
|
+
network_mode: "unknown",
|
|
3433
|
+
openclaw_bound: false,
|
|
3434
|
+
profile_version: PROFILE_VERSION,
|
|
3435
|
+
public_key_fingerprint: null,
|
|
3436
|
+
verified_profile: false,
|
|
3437
|
+
now: Date.now(),
|
|
3438
|
+
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
3439
|
+
})
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
return [...summaries, ...messageOnly].sort((a, b) => {
|
|
3444
|
+
if (a.online !== b.online) {
|
|
3445
|
+
return a.online ? -1 : 1;
|
|
3446
|
+
}
|
|
3447
|
+
if (a.updated_at !== b.updated_at) {
|
|
3448
|
+
return b.updated_at - a.updated_at;
|
|
3449
|
+
}
|
|
3450
|
+
const byName = a.display_name.localeCompare(b.display_name);
|
|
3451
|
+
if (byName !== 0) {
|
|
3452
|
+
return byName;
|
|
3453
|
+
}
|
|
3454
|
+
return a.agent_id.localeCompare(b.agent_id);
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
|
|
2376
3458
|
private fingerprintPublicKey(publicKey: string): string {
|
|
2377
3459
|
const digest = createHash("sha256").update(publicKey, "utf8").digest("hex");
|
|
2378
3460
|
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
@@ -2381,6 +3463,22 @@ export class LocalNodeService {
|
|
|
2381
3463
|
private getOnboardingSummary() {
|
|
2382
3464
|
const summary = this.getIntegrationSummary();
|
|
2383
3465
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
3466
|
+
const nextSteps: string[] = [];
|
|
3467
|
+
if (!String(this.profile?.display_name || "").trim()) {
|
|
3468
|
+
nextSteps.push("Update display name in Profile page");
|
|
3469
|
+
}
|
|
3470
|
+
if (!publicEnabled) {
|
|
3471
|
+
nextSteps.push("Enable Public Enabled in Profile");
|
|
3472
|
+
}
|
|
3473
|
+
if (!summary.running) {
|
|
3474
|
+
nextSteps.push("Start broadcast in Network");
|
|
3475
|
+
}
|
|
3476
|
+
if (!summary.discoverable) {
|
|
3477
|
+
nextSteps.push("Announce node once after the network is running");
|
|
3478
|
+
}
|
|
3479
|
+
if (nextSteps.length === 0) {
|
|
3480
|
+
nextSteps.push("Node is public and discoverable");
|
|
3481
|
+
}
|
|
2384
3482
|
return {
|
|
2385
3483
|
first_run: Boolean(
|
|
2386
3484
|
this.initState.social_auto_created ||
|
|
@@ -2392,10 +3490,7 @@ export class LocalNodeService {
|
|
|
2392
3490
|
mode: this.networkMode,
|
|
2393
3491
|
public_enabled: publicEnabled,
|
|
2394
3492
|
can_enable_public_discovery: !publicEnabled,
|
|
2395
|
-
next_steps:
|
|
2396
|
-
"Update display name in Profile page",
|
|
2397
|
-
"Export social.md from Social Config",
|
|
2398
|
-
],
|
|
3493
|
+
next_steps: nextSteps,
|
|
2399
3494
|
};
|
|
2400
3495
|
}
|
|
2401
3496
|
|
|
@@ -2420,7 +3515,7 @@ export class LocalNodeService {
|
|
|
2420
3515
|
};
|
|
2421
3516
|
}
|
|
2422
3517
|
return {
|
|
2423
|
-
mode:
|
|
3518
|
+
mode: DEFAULT_NETWORK_MODE,
|
|
2424
3519
|
short_label: "Relay preview",
|
|
2425
3520
|
summary: "Uses the public relay preview room so public nodes can find each other across the internet.",
|
|
2426
3521
|
};
|
|
@@ -2450,14 +3545,14 @@ export class LocalNodeService {
|
|
|
2450
3545
|
this.socialConfig.network.mode ||
|
|
2451
3546
|
(modeEnv === "local" || modeEnv === "lan" || modeEnv === "global-preview"
|
|
2452
3547
|
? modeEnv
|
|
2453
|
-
:
|
|
3548
|
+
: DEFAULT_NETWORK_MODE);
|
|
2454
3549
|
|
|
2455
3550
|
this.networkMode = resolvedMode;
|
|
2456
|
-
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE ||
|
|
2457
|
-
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT ||
|
|
3551
|
+
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || DEFAULT_NETWORK_NAMESPACE;
|
|
3552
|
+
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || DEFAULT_NETWORK_PORT);
|
|
2458
3553
|
|
|
2459
|
-
const builtInGlobalSignalingUrls = [
|
|
2460
|
-
const builtInGlobalRoom =
|
|
3554
|
+
const builtInGlobalSignalingUrls = [DEFAULT_GLOBAL_SIGNALING_URL];
|
|
3555
|
+
const builtInGlobalRoom = DEFAULT_GLOBAL_ROOM;
|
|
2461
3556
|
|
|
2462
3557
|
const signalingUrlsSocial = dedupeStrings(this.socialConfig.network.signaling_urls || []);
|
|
2463
3558
|
const signalingUrlSocial = String(this.socialConfig.network.signaling_url || "").trim();
|
|
@@ -2482,8 +3577,8 @@ export class LocalNodeService {
|
|
|
2482
3577
|
signalingUrls = builtInGlobalSignalingUrls;
|
|
2483
3578
|
signalingSource = "built-in-defaults:global-preview.signaling_urls";
|
|
2484
3579
|
} else {
|
|
2485
|
-
signalingUrls = [
|
|
2486
|
-
signalingSource =
|
|
3580
|
+
signalingUrls = [DEFAULT_GLOBAL_SIGNALING_URL];
|
|
3581
|
+
signalingSource = `default:${DEFAULT_GLOBAL_SIGNALING_URL}`;
|
|
2487
3582
|
}
|
|
2488
3583
|
|
|
2489
3584
|
const roomSocial = String(this.socialConfig.network.room || "").trim();
|
|
@@ -2492,14 +3587,14 @@ export class LocalNodeService {
|
|
|
2492
3587
|
roomSocial ||
|
|
2493
3588
|
roomEnv ||
|
|
2494
3589
|
(this.networkMode === "global-preview" ? builtInGlobalRoom : "") ||
|
|
2495
|
-
|
|
3590
|
+
DEFAULT_GLOBAL_ROOM;
|
|
2496
3591
|
const roomSource = roomSocial
|
|
2497
3592
|
? "social.md:network.room"
|
|
2498
3593
|
: roomEnv
|
|
2499
3594
|
? "env:WEBRTC_ROOM"
|
|
2500
3595
|
: this.networkMode === "global-preview"
|
|
2501
3596
|
? "built-in-defaults:global-preview.room"
|
|
2502
|
-
:
|
|
3597
|
+
: `default:${DEFAULT_GLOBAL_ROOM}`;
|
|
2503
3598
|
|
|
2504
3599
|
const seedPeersSocial = dedupeStrings(this.socialConfig.network.seed_peers || []);
|
|
2505
3600
|
const seedPeersEnv = dedupeStrings(parseListEnv(WEBRTC_SEED_PEERS));
|
|
@@ -2537,6 +3632,34 @@ export class LocalNodeService {
|
|
|
2537
3632
|
.trim();
|
|
2538
3633
|
}
|
|
2539
3634
|
|
|
3635
|
+
private buildPrivateConversationId(leftAgentId: string, rightAgentId: string): string {
|
|
3636
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
private decryptPrivateMessageBody(message: PrivateMessageRecord): string {
|
|
3640
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3641
|
+
if (typeof cached === "string") {
|
|
3642
|
+
return cached;
|
|
3643
|
+
}
|
|
3644
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3645
|
+
return "[encrypted]";
|
|
3646
|
+
}
|
|
3647
|
+
const decrypted = decryptPrivatePayload({
|
|
3648
|
+
ciphertext: message.ciphertext,
|
|
3649
|
+
nonce: message.nonce,
|
|
3650
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3651
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3652
|
+
}) || "[encrypted]";
|
|
3653
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3654
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3655
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3656
|
+
if (firstKey) {
|
|
3657
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
return decrypted;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
2540
3663
|
private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
|
|
2541
3664
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2542
3665
|
}
|
|
@@ -2558,6 +3681,38 @@ export class LocalNodeService {
|
|
|
2558
3681
|
return this.messageGovernance.blocked_terms.some((term) => normalized.includes(term));
|
|
2559
3682
|
}
|
|
2560
3683
|
|
|
3684
|
+
private hasSocialMessage(messageId: string): boolean {
|
|
3685
|
+
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
|
|
3689
|
+
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
3690
|
+
if (!this.identity || maxCount === 0) {
|
|
3691
|
+
return [];
|
|
3692
|
+
}
|
|
3693
|
+
const replayable = this.socialMessages
|
|
3694
|
+
.filter((item) => (
|
|
3695
|
+
item.agent_id === this.identity?.agent_id &&
|
|
3696
|
+
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
|
|
3697
|
+
))
|
|
3698
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3699
|
+
.slice(-maxCount);
|
|
3700
|
+
if (!replayable.length) {
|
|
3701
|
+
this.lastReplayBroadcastSignature = "";
|
|
3702
|
+
return [];
|
|
3703
|
+
}
|
|
3704
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3705
|
+
const isIntervalReplay = reason === "interval";
|
|
3706
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3707
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3708
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3709
|
+
return [];
|
|
3710
|
+
}
|
|
3711
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3712
|
+
this.lastReplayBroadcastAt = now;
|
|
3713
|
+
return replayable;
|
|
3714
|
+
}
|
|
3715
|
+
|
|
2561
3716
|
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
2562
3717
|
return this.socialMessages.some((item) => (
|
|
2563
3718
|
item.agent_id === agentId &&
|
|
@@ -2630,6 +3785,177 @@ export class LocalNodeService {
|
|
|
2630
3785
|
await this.persistSocialMessageObservations();
|
|
2631
3786
|
}
|
|
2632
3787
|
|
|
3788
|
+
private async sendPrivateMessageReceipt(message: PrivateMessageRecord, replyPeerId?: string): Promise<void> {
|
|
3789
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const receipt = signPrivateMessageReceipt({
|
|
3793
|
+
identity: this.identity,
|
|
3794
|
+
receipt_id: createHash("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3795
|
+
message_id: message.message_id,
|
|
3796
|
+
conversation_id: message.conversation_id,
|
|
3797
|
+
to_agent_id: message.from_agent_id,
|
|
3798
|
+
status: "received",
|
|
3799
|
+
created_at: Date.now(),
|
|
3800
|
+
});
|
|
3801
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3802
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3803
|
+
await this.persistPrivateMessageReceipts();
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
private normalizeIncomingPrivateMessage(value: unknown): PrivateMessageRecord | null {
|
|
3807
|
+
if (typeof value !== "object" || value === null) {
|
|
3808
|
+
return null;
|
|
3809
|
+
}
|
|
3810
|
+
const record = value as Partial<PrivateMessageRecord>;
|
|
3811
|
+
const createdAt = Number(record.created_at || 0);
|
|
3812
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3813
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3814
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3815
|
+
if (
|
|
3816
|
+
record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3817
|
+
!String(record.message_id || "").trim() ||
|
|
3818
|
+
!conversationId ||
|
|
3819
|
+
!fromAgentId ||
|
|
3820
|
+
!toAgentId ||
|
|
3821
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3822
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3823
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3824
|
+
!String(record.ciphertext || "").trim() ||
|
|
3825
|
+
!String(record.nonce || "").trim() ||
|
|
3826
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3827
|
+
!String(record.signature || "").trim() ||
|
|
3828
|
+
!Number.isFinite(createdAt)
|
|
3829
|
+
) {
|
|
3830
|
+
return null;
|
|
3831
|
+
}
|
|
3832
|
+
if (fromAgentId === toAgentId) {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3836
|
+
return null;
|
|
3837
|
+
}
|
|
3838
|
+
return {
|
|
3839
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3840
|
+
message_id: String(record.message_id).trim(),
|
|
3841
|
+
conversation_id: conversationId,
|
|
3842
|
+
from_agent_id: fromAgentId,
|
|
3843
|
+
to_agent_id: toAgentId,
|
|
3844
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3845
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3846
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3847
|
+
cipher_scheme: "nacl-box-v1",
|
|
3848
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3849
|
+
nonce: String(record.nonce).trim(),
|
|
3850
|
+
created_at: createdAt,
|
|
3851
|
+
signature: String(record.signature).trim(),
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
private normalizePrivateMessages(items: unknown): PrivateMessageRecord[] {
|
|
3856
|
+
if (!Array.isArray(items)) {
|
|
3857
|
+
return [];
|
|
3858
|
+
}
|
|
3859
|
+
const deduped = new Set<string>();
|
|
3860
|
+
return items
|
|
3861
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
3862
|
+
.filter((item): item is PrivateMessageRecord => Boolean(item))
|
|
3863
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3864
|
+
.filter((item) => {
|
|
3865
|
+
if (deduped.has(item.message_id)) {
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
3868
|
+
deduped.add(item.message_id);
|
|
3869
|
+
return true;
|
|
3870
|
+
})
|
|
3871
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
private normalizeIncomingPrivateMessageReceipt(value: unknown): PrivateMessageReceiptRecord | null {
|
|
3875
|
+
if (typeof value !== "object" || value === null) {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
const record = value as Partial<PrivateMessageReceiptRecord>;
|
|
3879
|
+
const createdAt = Number(record.created_at || 0);
|
|
3880
|
+
const status = String(record.status || "").trim();
|
|
3881
|
+
if (
|
|
3882
|
+
record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
3883
|
+
!String(record.receipt_id || "").trim() ||
|
|
3884
|
+
!String(record.message_id || "").trim() ||
|
|
3885
|
+
!String(record.conversation_id || "").trim() ||
|
|
3886
|
+
!String(record.from_agent_id || "").trim() ||
|
|
3887
|
+
!String(record.to_agent_id || "").trim() ||
|
|
3888
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3889
|
+
(status !== "received" && status !== "read") ||
|
|
3890
|
+
!String(record.signature || "").trim() ||
|
|
3891
|
+
!Number.isFinite(createdAt)
|
|
3892
|
+
) {
|
|
3893
|
+
return null;
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
3897
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
3898
|
+
message_id: String(record.message_id).trim(),
|
|
3899
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
3900
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
3901
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
3902
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3903
|
+
status: status as "received" | "read",
|
|
3904
|
+
created_at: createdAt,
|
|
3905
|
+
signature: String(record.signature).trim(),
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
private normalizePrivateMessageReceipts(items: unknown): PrivateMessageReceiptRecord[] {
|
|
3910
|
+
if (!Array.isArray(items)) {
|
|
3911
|
+
return [];
|
|
3912
|
+
}
|
|
3913
|
+
const deduped = new Set<string>();
|
|
3914
|
+
return items
|
|
3915
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
3916
|
+
.filter((item): item is PrivateMessageReceiptRecord => Boolean(item))
|
|
3917
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3918
|
+
.filter((item) => {
|
|
3919
|
+
if (deduped.has(item.receipt_id)) {
|
|
3920
|
+
return false;
|
|
3921
|
+
}
|
|
3922
|
+
deduped.add(item.receipt_id);
|
|
3923
|
+
return true;
|
|
3924
|
+
})
|
|
3925
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
private hasPrivateMessage(messageId: string): boolean {
|
|
3929
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
private ingestPrivateMessage(message: PrivateMessageRecord): void {
|
|
3933
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
3934
|
+
if (existing >= 0) {
|
|
3935
|
+
this.privateMessages[existing] = message;
|
|
3936
|
+
} else {
|
|
3937
|
+
this.privateMessages.push(message);
|
|
3938
|
+
}
|
|
3939
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
3940
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
3941
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
3942
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
3943
|
+
if (!validIds.has(key)) {
|
|
3944
|
+
this.privateMessageBodyCache.delete(key);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
|
|
3950
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
3951
|
+
if (existing >= 0) {
|
|
3952
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
3953
|
+
} else {
|
|
3954
|
+
this.privateMessageReceipts.push(receipt);
|
|
3955
|
+
}
|
|
3956
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
2633
3959
|
private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
|
|
2634
3960
|
if (typeof value !== "object" || value === null) {
|
|
2635
3961
|
return null;
|
|
@@ -2867,20 +4193,19 @@ function renderBootstrapScript(payload: unknown): string {
|
|
|
2867
4193
|
if (data.pillBroadcastClassName) pillBroadcast.className = data.pillBroadcastClassName;
|
|
2868
4194
|
}
|
|
2869
4195
|
setHtml('overviewCards', data.overviewCardsHtml || '');
|
|
2870
|
-
setText('agentsCountHint', data.agentsCountHintText || '0
|
|
2871
|
-
setHtml('agentsWrap', data.agentsWrapHtml || '<div class="label">No discovered
|
|
4196
|
+
setText('agentsCountHint', data.agentsCountHintText || '0 nodes');
|
|
4197
|
+
setHtml('agentsWrap', data.agentsWrapHtml || '<div class="label">No discovered nodes yet.</div>');
|
|
2872
4198
|
})();
|
|
2873
4199
|
</script>`;
|
|
2874
4200
|
}
|
|
2875
4201
|
|
|
2876
4202
|
export async function main() {
|
|
2877
4203
|
const app = express();
|
|
2878
|
-
const port = Number(process.env.PORT ||
|
|
4204
|
+
const port = Number(process.env.PORT || defaults.ports.local_console);
|
|
2879
4205
|
const staticDir = resolveLocalConsoleStaticDir();
|
|
2880
4206
|
const staticIndexFile = resolve(staticDir, "index.html");
|
|
2881
4207
|
|
|
2882
4208
|
const node = new LocalNodeService();
|
|
2883
|
-
await node.start();
|
|
2884
4209
|
|
|
2885
4210
|
app.use(cors({ origin: true }));
|
|
2886
4211
|
app.use(express.json());
|
|
@@ -2909,6 +4234,48 @@ export async function main() {
|
|
|
2909
4234
|
sendOk(res, node.getRuntimePaths());
|
|
2910
4235
|
});
|
|
2911
4236
|
|
|
4237
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
4238
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
4239
|
+
});
|
|
4240
|
+
|
|
4241
|
+
app.post(
|
|
4242
|
+
"/api/app/update",
|
|
4243
|
+
asyncRoute(async (_req, res) => {
|
|
4244
|
+
const status = node.getAppUpdateStatus();
|
|
4245
|
+
if (!status.update_available || !status.latest_version) {
|
|
4246
|
+
sendOk(
|
|
4247
|
+
res,
|
|
4248
|
+
{
|
|
4249
|
+
started: false,
|
|
4250
|
+
current_version: status.current_version,
|
|
4251
|
+
latest_version: status.latest_version,
|
|
4252
|
+
platform: status.platform,
|
|
4253
|
+
reason: status.check_error || "already_current",
|
|
4254
|
+
},
|
|
4255
|
+
{ message: "Already on the latest version" }
|
|
4256
|
+
);
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
sendOk(
|
|
4260
|
+
res,
|
|
4261
|
+
{
|
|
4262
|
+
started: true,
|
|
4263
|
+
current_version: status.current_version,
|
|
4264
|
+
target_version: status.latest_version,
|
|
4265
|
+
platform: status.platform,
|
|
4266
|
+
},
|
|
4267
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
4268
|
+
);
|
|
4269
|
+
setTimeout(() => {
|
|
4270
|
+
try {
|
|
4271
|
+
node.startAppUpdate();
|
|
4272
|
+
} catch {
|
|
4273
|
+
// best effort after response has been sent
|
|
4274
|
+
}
|
|
4275
|
+
}, 150);
|
|
4276
|
+
})
|
|
4277
|
+
);
|
|
4278
|
+
|
|
2912
4279
|
app.put(
|
|
2913
4280
|
"/api/profile",
|
|
2914
4281
|
asyncRoute(async (req, res) => {
|
|
@@ -3051,6 +4418,34 @@ export async function main() {
|
|
|
3051
4418
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3052
4419
|
});
|
|
3053
4420
|
|
|
4421
|
+
app.get("/api/private/state", (_req, res) => {
|
|
4422
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
4423
|
+
});
|
|
4424
|
+
|
|
4425
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
4426
|
+
sendOk(res, node.getPrivateConversations());
|
|
4427
|
+
});
|
|
4428
|
+
|
|
4429
|
+
app.get("/api/private/messages", (req, res) => {
|
|
4430
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
4431
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
4432
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
4433
|
+
});
|
|
4434
|
+
|
|
4435
|
+
app.post(
|
|
4436
|
+
"/api/private/messages/send",
|
|
4437
|
+
asyncRoute(async (req, res) => {
|
|
4438
|
+
const result = await node.sendPrivateMessage({
|
|
4439
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
4440
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
4441
|
+
body: String(req.body?.body || ""),
|
|
4442
|
+
});
|
|
4443
|
+
sendOk(res, result, {
|
|
4444
|
+
message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
|
|
4445
|
+
});
|
|
4446
|
+
})
|
|
4447
|
+
);
|
|
4448
|
+
|
|
3054
4449
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3055
4450
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3056
4451
|
});
|
|
@@ -3083,9 +4478,10 @@ export async function main() {
|
|
|
3083
4478
|
|
|
3084
4479
|
app.post(
|
|
3085
4480
|
"/api/openclaw/bridge/skill-install",
|
|
3086
|
-
asyncRoute(async (
|
|
4481
|
+
asyncRoute(async (req, res) => {
|
|
3087
4482
|
try {
|
|
3088
|
-
const
|
|
4483
|
+
const skillName = String(req.body?.skill_name || "").trim();
|
|
4484
|
+
const result = await node.installOpenClawSkill(skillName || undefined);
|
|
3089
4485
|
sendOk(res, result, {
|
|
3090
4486
|
message: "OpenClaw skill installed",
|
|
3091
4487
|
});
|
|
@@ -3133,7 +4529,7 @@ export async function main() {
|
|
|
3133
4529
|
const agentId = req.params.agentId;
|
|
3134
4530
|
const profile = state.profiles[agentId];
|
|
3135
4531
|
if (!profile) {
|
|
3136
|
-
sendError(res, 404, "AGENT_NOT_FOUND", "
|
|
4532
|
+
sendError(res, 404, "AGENT_NOT_FOUND", "Node not found", { agent_id: agentId });
|
|
3137
4533
|
return;
|
|
3138
4534
|
}
|
|
3139
4535
|
|
|
@@ -3169,7 +4565,7 @@ export async function main() {
|
|
|
3169
4565
|
.join("");
|
|
3170
4566
|
const agentsWrapHtml =
|
|
3171
4567
|
discovered.length === 0
|
|
3172
|
-
? `<div class="label">No discovered
|
|
4568
|
+
? `<div class="label">No discovered nodes yet.</div>`
|
|
3173
4569
|
: `
|
|
3174
4570
|
<table class="table">
|
|
3175
4571
|
<thead><tr><th>Name</th><th>Agent ID</th><th>Status</th><th>Updated</th></tr></thead>
|
|
@@ -3205,7 +4601,7 @@ export async function main() {
|
|
|
3205
4601
|
pillBroadcastText: overview.broadcast_enabled ? "broadcast: running" : "broadcast: paused",
|
|
3206
4602
|
pillBroadcastClassName: `pill ${overview.broadcast_enabled ? "ok" : "warn"}`,
|
|
3207
4603
|
overviewCardsHtml,
|
|
3208
|
-
agentsCountHintText: `${discovered.length}
|
|
4604
|
+
agentsCountHintText: `${discovered.length} nodes discovered`,
|
|
3209
4605
|
agentsWrapHtml,
|
|
3210
4606
|
integrationStatusText: `Connected to SilicaClaw: ${integration.connected_to_silicaclaw ? "yes" : "no"} · Network mode: ${integration.network_mode || "-"} · Public discovery: ${integration.public_enabled ? "enabled" : "disabled"}`,
|
|
3211
4607
|
integrationStatusClassName: `integration-strip ${integration.connected_to_silicaclaw && integration.public_enabled ? "ok" : "warn"}`,
|
|
@@ -3230,6 +4626,8 @@ export async function main() {
|
|
|
3230
4626
|
console.log(`SilicaClaw local-console running: http://localhost:${port}`);
|
|
3231
4627
|
});
|
|
3232
4628
|
|
|
4629
|
+
await node.start();
|
|
4630
|
+
|
|
3233
4631
|
process.on("SIGINT", async () => {
|
|
3234
4632
|
await node.stop();
|
|
3235
4633
|
process.exit(0);
|