11import { rm } from "node:fs/promises" ;
22import os from "node:os" ;
33import path from "node:path" ;
4+ import { definePluginEntry , type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry" ;
45import {
56 normalizeLowercaseStringOrEmpty ,
67 normalizeOptionalString ,
78} from "openclaw/plugin-sdk/text-runtime" ;
8- import {
9- clearDeviceBootstrapTokens ,
10- definePluginEntry ,
11- issueDeviceBootstrapToken ,
12- listDevicePairing ,
13- PAIRING_SETUP_BOOTSTRAP_PROFILE ,
14- renderQrPngDataUrl ,
15- writeQrPngTempFile ,
16- revokeDeviceBootstrapToken ,
17- resolveGatewayBindUrl ,
18- resolveGatewayPort ,
19- resolvePreferredOpenClawTmpDir ,
20- runPluginCommandWithTimeout ,
21- resolveTailnetHostWithRunner ,
22- type OpenClawPluginApi ,
23- } from "./api.js" ;
24- import {
25- armPairNotifyOnce ,
26- formatPendingRequests ,
27- handleNotifyCommand ,
28- registerPairingNotifierService ,
29- } from "./notify.js" ;
30- import {
31- approvePendingPairingRequest ,
32- selectPendingApprovalRequest ,
33- } from "./pair-command-approve.js" ;
34- import {
35- buildMissingPairingScopeReply ,
36- resolvePairingCommandAuthState ,
37- } from "./pair-command-auth.js" ;
9+
10+ type DevicePairApiModule = typeof import ( "./api.js" ) ;
11+ type NotifyModule = typeof import ( "./notify.js" ) ;
12+ type PairCommandApproveModule = typeof import ( "./pair-command-approve.js" ) ;
13+ type PairCommandAuthModule = typeof import ( "./pair-command-auth.js" ) ;
14+
15+ let devicePairApiModulePromise : Promise < DevicePairApiModule > | undefined ;
16+ let notifyModulePromise : Promise < NotifyModule > | undefined ;
17+ let pairCommandApproveModulePromise : Promise < PairCommandApproveModule > | undefined ;
18+ let pairCommandAuthModulePromise : Promise < PairCommandAuthModule > | undefined ;
19+
20+ function loadDevicePairApiModule ( ) : Promise < DevicePairApiModule > {
21+ devicePairApiModulePromise ??= import ( "./api.js" ) ;
22+ return devicePairApiModulePromise ;
23+ }
24+
25+ function loadNotifyModule ( ) : Promise < NotifyModule > {
26+ notifyModulePromise ??= import ( "./notify.js" ) ;
27+ return notifyModulePromise ;
28+ }
29+
30+ function loadPairCommandApproveModule ( ) : Promise < PairCommandApproveModule > {
31+ pairCommandApproveModulePromise ??= import ( "./pair-command-approve.js" ) ;
32+ return pairCommandApproveModulePromise ;
33+ }
34+
35+ function loadPairCommandAuthModule ( ) : Promise < PairCommandAuthModule > {
36+ pairCommandAuthModulePromise ??= import ( "./pair-command-auth.js" ) ;
37+ return pairCommandAuthModulePromise ;
38+ }
3839
3940function formatDurationMinutes ( expiresAtMs : number ) : string {
4041 const msRemaining = Math . max ( 0 , expiresAtMs - Date . now ( ) ) ;
@@ -254,6 +255,8 @@ function pickTailnetIPv4(): string | null {
254255}
255256
256257async function resolveTailnetHost ( ) : Promise < string | null > {
258+ const { resolveTailnetHostWithRunner, runPluginCommandWithTimeout } =
259+ await loadDevicePairApiModule ( ) ;
257260 return await resolveTailnetHostWithRunner ( ( argv , opts ) =>
258261 runPluginCommandWithTimeout ( {
259262 argv,
@@ -307,6 +310,7 @@ function resolveRequiredAuthLabel(
307310}
308311
309312async function resolveGatewayUrl ( api : OpenClawPluginApi ) : Promise < ResolveUrlResult > {
313+ const { resolveGatewayBindUrl, resolveGatewayPort } = await loadDevicePairApiModule ( ) ;
310314 const cfg = api . config ;
311315 const pluginCfg = ( api . pluginConfig ?? { } ) as DevicePairPluginConfig ;
312316 const scheme = resolveScheme ( cfg ) ;
@@ -511,6 +515,8 @@ function issuesPairSetupCode(action: string): boolean {
511515}
512516
513517async function issueSetupPayload ( url : string ) : Promise < SetupPayload > {
518+ const { issueDeviceBootstrapToken, PAIRING_SETUP_BOOTSTRAP_PROFILE } =
519+ await loadDevicePairApiModule ( ) ;
514520 const issuedBootstrap = await issueDeviceBootstrapToken ( {
515521 profile : PAIRING_SETUP_BOOTSTRAP_PROFILE ,
516522 } ) ;
@@ -558,7 +564,19 @@ export default definePluginEntry({
558564 name : "Device Pair" ,
559565 description : "QR/bootstrap pairing helpers for OpenClaw devices" ,
560566 register ( api : OpenClawPluginApi ) {
561- registerPairingNotifierService ( api ) ;
567+ let notifierService : ReturnType < NotifyModule [ "createPairingNotifierService" ] > | undefined ;
568+ api . registerService ( {
569+ id : "device-pair-notifier" ,
570+ start : async ( ctx ) => {
571+ const { createPairingNotifierService } = await loadNotifyModule ( ) ;
572+ notifierService = createPairingNotifierService ( api ) ;
573+ await notifierService . start ( ctx ) ;
574+ } ,
575+ stop : async ( ctx ) => {
576+ await notifierService ?. stop ?.( ctx ) ;
577+ notifierService = undefined ;
578+ } ,
579+ } ) ;
562580
563581 api . registerCommand ( {
564582 name : "pair" ,
@@ -571,6 +589,8 @@ export default definePluginEntry({
571589 const gatewayClientScopes = Array . isArray ( ctx . gatewayClientScopes )
572590 ? ctx . gatewayClientScopes
573591 : undefined ;
592+ const { buildMissingPairingScopeReply, resolvePairingCommandAuthState } =
593+ await loadPairCommandAuthModule ( ) ;
574594 const authState = resolvePairingCommandAuthState ( {
575595 channel : ctx . channel ,
576596 gatewayClientScopes,
@@ -582,12 +602,17 @@ export default definePluginEntry({
582602 ) ;
583603
584604 if ( action === "status" || action === "pending" ) {
605+ const [ { listDevicePairing } , { formatPendingRequests } ] = await Promise . all ( [
606+ loadDevicePairApiModule ( ) ,
607+ loadNotifyModule ( ) ,
608+ ] ) ;
585609 const list = await listDevicePairing ( ) ;
586610 return { text : formatPendingRequests ( list . pending ) } ;
587611 }
588612
589613 if ( action === "notify" ) {
590614 const notifyAction = normalizeLowercaseStringOrEmpty ( tokens [ 1 ] ) || "status" ;
615+ const { handleNotifyCommand } = await loadNotifyModule ( ) ;
591616 return await handleNotifyCommand ( {
592617 api,
593618 ctx,
@@ -599,6 +624,10 @@ export default definePluginEntry({
599624 if ( authState . isMissingInternalPairingPrivilege ) {
600625 return buildMissingPairingScopeReply ( ) ;
601626 }
627+ const [
628+ { listDevicePairing } ,
629+ { approvePendingPairingRequest, selectPendingApprovalRequest } ,
630+ ] = await Promise . all ( [ loadDevicePairApiModule ( ) , loadPairCommandApproveModule ( ) ] ) ;
602631 const list = await listDevicePairing ( ) ;
603632 const selected = selectPendingApprovalRequest ( {
604633 pending : list . pending ,
@@ -621,6 +650,7 @@ export default definePluginEntry({
621650 if ( authState . isMissingInternalPairingPrivilege ) {
622651 return buildMissingPairingScopeReply ( ) ;
623652 }
653+ const { clearDeviceBootstrapTokens } = await loadDevicePairApiModule ( ) ;
624654 const cleared = await clearDeviceBootstrapTokens ( ) ;
625655 return {
626656 text :
@@ -651,6 +681,7 @@ export default definePluginEntry({
651681
652682 if ( channel === "telegram" && target ) {
653683 try {
684+ const { armPairNotifyOnce } = await loadNotifyModule ( ) ;
654685 autoNotifyArmed = await armPairNotifyOnce ( { api, ctx } ) ;
655686 } catch ( err ) {
656687 api . logger . warn ?.(
@@ -672,6 +703,8 @@ export default definePluginEntry({
672703 if ( target && canSendQrPngToChannel ( channel ) ) {
673704 let qrFilePath : string | undefined ;
674705 try {
706+ const { resolvePreferredOpenClawTmpDir, writeQrPngTempFile } =
707+ await loadDevicePairApiModule ( ) ;
675708 qrFilePath = (
676709 await writeQrPngTempFile ( setupCode , {
677710 tmpRoot : resolvePreferredOpenClawTmpDir ( ) ,
@@ -697,6 +730,7 @@ export default definePluginEntry({
697730 } ;
698731 }
699732 } catch ( err ) {
733+ const { revokeDeviceBootstrapToken } = await loadDevicePairApiModule ( ) ;
700734 api . logger . warn ?.(
701735 `device-pair: QR image send failed channel=${ channel } , falling back (${ ( err as Error ) ?. message ?? err } )` ,
702736 ) ;
@@ -716,8 +750,10 @@ export default definePluginEntry({
716750 if ( channel === "webchat" ) {
717751 let qrDataUrl : string ;
718752 try {
753+ const { renderQrPngDataUrl } = await loadDevicePairApiModule ( ) ;
719754 qrDataUrl = await renderQrPngDataUrl ( setupCode ) ;
720755 } catch ( err ) {
756+ const { revokeDeviceBootstrapToken } = await loadDevicePairApiModule ( ) ;
721757 api . logger . warn ?.(
722758 `device-pair: webchat QR render failed, falling back (${ ( err as Error ) ?. message ?? err } )` ,
723759 ) ;
0 commit comments