larkin 0.4.9 → 0.4.11
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/dist/app/setup.mjs +6 -6
- package/dist/runtime/pi-subagents-notification.mjs +161 -0
- package/dist/runtime/runtime-adapters.mjs +32 -1
- package/dist/runtime/runtime-host.cjs +250 -1
- package/dist/runtime/runtime-host.mjs +250 -1
- package/dist/setup/bot-register.mjs +94 -27
- package/dist/setup/setup-authorization-url.mjs +50 -0
- package/dist/setup/setup-bind.mjs +60 -22
- package/dist/setup/setup-official-cli-register.mjs +74 -0
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ export class RuntimeSessionRecoveryError extends Error {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
const MAX_DELIVERIES = 2048;
|
|
33
|
+
const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
|
|
33
34
|
const now = () => new Date().toISOString();
|
|
34
35
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
35
36
|
const REPLAY_FAILURE_CODES = new Set([
|
|
@@ -512,6 +513,12 @@ export function createRuntimeHost(options) {
|
|
|
512
513
|
queueMicrotask(() => { void retryPending(agent); });
|
|
513
514
|
}
|
|
514
515
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
516
|
+
// A completion queued during this submit has no turn-end if prompt
|
|
517
|
+
// rejected/threw before turn-start. Drain now that submitting is clear.
|
|
518
|
+
if (!agent.busy && !agent.turnInProgress && agent.backgroundCompletionQueue.length > 0
|
|
519
|
+
&& !agent.backgroundCompletionRetryTimer) {
|
|
520
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
521
|
+
}
|
|
515
522
|
}
|
|
516
523
|
};
|
|
517
524
|
const retryPending = async (agent) => {
|
|
@@ -590,6 +597,210 @@ export function createRuntimeHost(options) {
|
|
|
590
597
|
agent.retryAfterSubmit = true;
|
|
591
598
|
await retryPending(agent);
|
|
592
599
|
};
|
|
600
|
+
const clearBackgroundCompletionRetryTimer = (agent) => {
|
|
601
|
+
if (!agent.backgroundCompletionRetryTimer)
|
|
602
|
+
return;
|
|
603
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
604
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
605
|
+
};
|
|
606
|
+
const scheduleBackgroundCompletionDrain = (agent, delayMs = 0) => {
|
|
607
|
+
if (agent.stopped)
|
|
608
|
+
return;
|
|
609
|
+
if (delayMs > 0) {
|
|
610
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
611
|
+
return;
|
|
612
|
+
agent.backgroundCompletionRetryTimer = setTimeout(() => {
|
|
613
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
614
|
+
if (agent.stopped)
|
|
615
|
+
return;
|
|
616
|
+
void drainBackgroundCompletionQueue(agent);
|
|
617
|
+
}, delayMs);
|
|
618
|
+
agent.backgroundCompletionRetryTimer.unref?.();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
622
|
+
queueMicrotask(() => { void drainBackgroundCompletionQueue(agent); });
|
|
623
|
+
};
|
|
624
|
+
const dropBackgroundCompletion = (agent, completionKey) => {
|
|
625
|
+
if (!completionKey)
|
|
626
|
+
return;
|
|
627
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
628
|
+
agent.backgroundCompletionQueue.shift();
|
|
629
|
+
else {
|
|
630
|
+
const index = agent.backgroundCompletionQueue.indexOf(completionKey);
|
|
631
|
+
if (index >= 0)
|
|
632
|
+
agent.backgroundCompletionQueue.splice(index, 1);
|
|
633
|
+
}
|
|
634
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
635
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
636
|
+
agent.backgroundCompletionInFlight = null;
|
|
637
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
638
|
+
}
|
|
639
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
640
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
641
|
+
};
|
|
642
|
+
const scheduleBackgroundCompletionRetry = (agent) => {
|
|
643
|
+
const streak = agent.backgroundCompletionRejectStreak;
|
|
644
|
+
if (streak < BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT) {
|
|
645
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const delayedAttempt = streak - BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT;
|
|
649
|
+
if (delayedAttempt >= retryPolicy.maxAttempts) {
|
|
650
|
+
const exhausted = agent.backgroundCompletionQueue[0];
|
|
651
|
+
if (exhausted) {
|
|
652
|
+
log("background completion wake retries exhausted", exhausted);
|
|
653
|
+
dropBackgroundCompletion(agent, exhausted);
|
|
654
|
+
}
|
|
655
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
656
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const delay = Math.min(retryPolicy.maxDelayMs, retryPolicy.baseDelayMs * 2 ** delayedAttempt);
|
|
660
|
+
scheduleBackgroundCompletionDrain(agent, delay);
|
|
661
|
+
};
|
|
662
|
+
const isPersistentBackgroundWakeFailure = (event) => {
|
|
663
|
+
if (!event)
|
|
664
|
+
return false;
|
|
665
|
+
const category = event.errorCategory;
|
|
666
|
+
if (category === "auth" || category === "billing" || category === "quota")
|
|
667
|
+
return true;
|
|
668
|
+
return category === "provider" && event.retryable === false;
|
|
669
|
+
};
|
|
670
|
+
const failBackgroundCompletionWake = (agent, event) => {
|
|
671
|
+
const completionKey = agent.backgroundCompletionInFlight ?? agent.backgroundCompletionQueue[0];
|
|
672
|
+
if (!completionKey)
|
|
673
|
+
return;
|
|
674
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
675
|
+
if (isPersistentBackgroundWakeFailure(event)) {
|
|
676
|
+
log("background completion wake dropped", event?.errorCategory ?? "persistent");
|
|
677
|
+
dropBackgroundCompletion(agent, completionKey);
|
|
678
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
679
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
683
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
684
|
+
};
|
|
685
|
+
const rearmBackgroundCompletion = (agent, completionKey = agent.backgroundCompletionInFlight) => {
|
|
686
|
+
if (!completionKey)
|
|
687
|
+
return;
|
|
688
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
689
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
690
|
+
agent.backgroundCompletionInFlight = null;
|
|
691
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
692
|
+
}
|
|
693
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
694
|
+
agent.backgroundCompletionQueue.unshift(completionKey);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const commitBackgroundCompletion = (agent) => {
|
|
698
|
+
const completionKey = agent.backgroundCompletionInFlight;
|
|
699
|
+
if (!completionKey)
|
|
700
|
+
return;
|
|
701
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
702
|
+
agent.backgroundCompletionQueue.shift();
|
|
703
|
+
agent.backgroundCompletionInFlight = null;
|
|
704
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
705
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
706
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
707
|
+
};
|
|
708
|
+
const wakeOnBackgroundCompletion = async (agent) => {
|
|
709
|
+
// Caller already reserved submitting/busy so a concurrent idle drain cannot
|
|
710
|
+
// dequeue another head while ensureSession/prompt yields.
|
|
711
|
+
try {
|
|
712
|
+
const session = await ensureSession(agent);
|
|
713
|
+
if (agent.session !== session || agent.stopped)
|
|
714
|
+
return { status: "dropped" };
|
|
715
|
+
const input = options.promptBuilder.buildRuntimeInput("wake", crypto.randomUUID(), { wakeReason: "background subagent completed" });
|
|
716
|
+
const result = await session.prompt(input);
|
|
717
|
+
if (agent.session !== session || agent.stopped)
|
|
718
|
+
return { status: "dropped" };
|
|
719
|
+
if (result.status === "accepted") {
|
|
720
|
+
markSessionStable(agent, session);
|
|
721
|
+
return { status: "accepted", inputId: input.inputId };
|
|
722
|
+
}
|
|
723
|
+
agent.busy = false;
|
|
724
|
+
return { status: "retry" };
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
agent.busy = false;
|
|
728
|
+
log("background completion wake failed", error instanceof Error ? error.message : String(error));
|
|
729
|
+
return { status: "retry" };
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const drainBackgroundCompletionQueue = async (agent) => {
|
|
733
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
734
|
+
return;
|
|
735
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
736
|
+
return;
|
|
737
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
738
|
+
const proactive = agent.piProactiveCompaction
|
|
739
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
740
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
741
|
+
? agent.piProactiveCompaction : null;
|
|
742
|
+
if (proactive && await proactive === "failed")
|
|
743
|
+
return;
|
|
744
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
745
|
+
return;
|
|
746
|
+
if (agent.adapter.id === "pi" && !agent.busy && !agent.turnInProgress && agent.session) {
|
|
747
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
748
|
+
if (idleGate && await idleGate === "failed")
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
752
|
+
return;
|
|
753
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
754
|
+
return;
|
|
755
|
+
agent.submitting = true;
|
|
756
|
+
agent.busy = true;
|
|
757
|
+
const completionKey = agent.backgroundCompletionQueue[0];
|
|
758
|
+
try {
|
|
759
|
+
const outcome = await wakeOnBackgroundCompletion(agent);
|
|
760
|
+
if (outcome.status === "accepted") {
|
|
761
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
762
|
+
agent.backgroundCompletionInFlight = completionKey;
|
|
763
|
+
agent.backgroundCompletionWakeInputId = outcome.inputId;
|
|
764
|
+
// Keep the reject streak until the wake turn actually succeeds. An accepted
|
|
765
|
+
// prompt that later terminal-fails must not reset the bounded retry budget.
|
|
766
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
767
|
+
}
|
|
768
|
+
else if (outcome.status === "dropped") {
|
|
769
|
+
if (agent.stopped) {
|
|
770
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
771
|
+
agent.backgroundCompletionQueue.shift();
|
|
772
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
773
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
777
|
+
agent.busy = false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
agent.busy = false;
|
|
782
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
783
|
+
// Leave the item queued+deduped. Immediate microtask retries cover a
|
|
784
|
+
// brief reject; after that a bounded timer still drains because Pi will
|
|
785
|
+
// not re-emit the same completion.
|
|
786
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
agent.submitting = false;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const noteBackgroundCompletion = (agent, completionKey) => {
|
|
794
|
+
if (agent.backgroundCompletionKeys.has(completionKey))
|
|
795
|
+
return;
|
|
796
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
797
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
798
|
+
agent.backgroundCompletionQueue.push(completionKey);
|
|
799
|
+
}
|
|
800
|
+
if (!agent.busy && !agent.turnInProgress && !agent.submitting && !agent.backgroundCompletionInFlight) {
|
|
801
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
593
804
|
const scheduleRecreate = (agent, reason) => {
|
|
594
805
|
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
595
806
|
return;
|
|
@@ -612,6 +823,8 @@ export function createRuntimeHost(options) {
|
|
|
612
823
|
if (proactive && await proactive === "failed")
|
|
613
824
|
return;
|
|
614
825
|
await retryPending(agent);
|
|
826
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
827
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
615
828
|
}).catch((error) => {
|
|
616
829
|
const readiness = error instanceof RuntimePrerequisiteError ? error.readiness
|
|
617
830
|
: classifyRuntimePrerequisite(agent.adapter.id, error);
|
|
@@ -634,6 +847,7 @@ export function createRuntimeHost(options) {
|
|
|
634
847
|
agent.busy = false;
|
|
635
848
|
agent.submitting = false;
|
|
636
849
|
agent.generation += 1;
|
|
850
|
+
rearmBackgroundCompletion(agent);
|
|
637
851
|
agent.recreateReason = reason;
|
|
638
852
|
if (agent.stabilityTimer)
|
|
639
853
|
clearTimeout(agent.stabilityTimer);
|
|
@@ -654,6 +868,7 @@ export function createRuntimeHost(options) {
|
|
|
654
868
|
agent.busy = false;
|
|
655
869
|
agent.submitting = false;
|
|
656
870
|
agent.generation += 1;
|
|
871
|
+
rearmBackgroundCompletion(agent);
|
|
657
872
|
if (agent.stabilityTimer)
|
|
658
873
|
clearTimeout(agent.stabilityTimer);
|
|
659
874
|
agent.stabilityTimer = null;
|
|
@@ -910,6 +1125,15 @@ export function createRuntimeHost(options) {
|
|
|
910
1125
|
agent.busy = false;
|
|
911
1126
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
912
1127
|
reconcileAcceptedAtTurnEnd(agent);
|
|
1128
|
+
if (agent.backgroundCompletionInFlight) {
|
|
1129
|
+
if (agent.turnHadFailure)
|
|
1130
|
+
failBackgroundCompletionWake(agent);
|
|
1131
|
+
else
|
|
1132
|
+
commitBackgroundCompletion(agent);
|
|
1133
|
+
}
|
|
1134
|
+
if (agent.backgroundCompletionQueue.length > 0 && !agent.backgroundCompletionRetryTimer) {
|
|
1135
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
1136
|
+
}
|
|
913
1137
|
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
914
1138
|
if (recoveredAuthentication) {
|
|
915
1139
|
agent.authFailureActive = false;
|
|
@@ -929,12 +1153,24 @@ export function createRuntimeHost(options) {
|
|
|
929
1153
|
});
|
|
930
1154
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
931
1155
|
}
|
|
1156
|
+
else if (event.type === "runtime-observation") {
|
|
1157
|
+
if (event.phase === "completed" && typeof event.completionKey === "string") {
|
|
1158
|
+
noteBackgroundCompletion(agent, event.completionKey);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
932
1161
|
else if (event.type === "activity") {
|
|
933
1162
|
if (agent.turnInProgress && event.activity !== "internal")
|
|
934
1163
|
agent.turnHadAuthenticatedOutput = true;
|
|
935
1164
|
emit({ type: "activity", agentId: agent.config.agentId, activity: event.activity, activityKind: event.activity });
|
|
936
1165
|
}
|
|
937
1166
|
else if (event.type === "input-error") {
|
|
1167
|
+
if (agent.backgroundCompletionInFlight
|
|
1168
|
+
&& (!event.inputId || !agent.backgroundCompletionWakeInputId
|
|
1169
|
+
|| event.inputId === agent.backgroundCompletionWakeInputId)
|
|
1170
|
+
&& event.willRetry !== true) {
|
|
1171
|
+
agent.turnHadFailure = true;
|
|
1172
|
+
failBackgroundCompletionWake(agent, event);
|
|
1173
|
+
}
|
|
938
1174
|
const record = event.inputId
|
|
939
1175
|
? agent.records.get(event.inputId) ?? [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId)
|
|
940
1176
|
: undefined;
|
|
@@ -1261,10 +1497,13 @@ export function createRuntimeHost(options) {
|
|
|
1261
1497
|
clearTimeout(previous.retryTimer);
|
|
1262
1498
|
if (previous.stabilityTimer)
|
|
1263
1499
|
clearTimeout(previous.stabilityTimer);
|
|
1500
|
+
if (previous.backgroundCompletionRetryTimer)
|
|
1501
|
+
clearTimeout(previous.backgroundCompletionRetryTimer);
|
|
1264
1502
|
const candidate = {
|
|
1265
1503
|
...previous, config, adapter, session, launchId: crypto.randomUUID(), busy: false, submitting: false,
|
|
1266
1504
|
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
1267
1505
|
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
1506
|
+
backgroundCompletionRetryTimer: null,
|
|
1268
1507
|
disabledReason: null, configurationRecovery: null,
|
|
1269
1508
|
readiness: previous.authFailureActive ? previous.readiness : readiness,
|
|
1270
1509
|
};
|
|
@@ -1281,6 +1520,11 @@ export function createRuntimeHost(options) {
|
|
|
1281
1520
|
if (!candidate.authFailureActive) {
|
|
1282
1521
|
emit({ type: "agent-status", agentId: config.agentId, status: "active", readiness: candidate.readiness ?? readiness });
|
|
1283
1522
|
}
|
|
1523
|
+
// Commit clears any inherited backoff timer; reschedule so a queued
|
|
1524
|
+
// completion still drains on the new session.
|
|
1525
|
+
if (candidate.backgroundCompletionQueue.length > 0) {
|
|
1526
|
+
scheduleBackgroundCompletionDrain(candidate);
|
|
1527
|
+
}
|
|
1284
1528
|
await previous.session?.close("runtime candidate committed")
|
|
1285
1529
|
.catch((error) => log("previous runtime close after candidate commit failed", String(error)));
|
|
1286
1530
|
},
|
|
@@ -1613,7 +1857,10 @@ export function createRuntimeHost(options) {
|
|
|
1613
1857
|
piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
|
|
1614
1858
|
piProactiveCompactionFailedGeneration: null, readiness: null,
|
|
1615
1859
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
|
|
1616
|
-
promotedInboxUpdateIds: new Set()
|
|
1860
|
+
promotedInboxUpdateIds: new Set(),
|
|
1861
|
+
backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
|
|
1862
|
+
backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
|
|
1863
|
+
backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null };
|
|
1617
1864
|
const startupConsumed = [];
|
|
1618
1865
|
const startupQuarantined = [];
|
|
1619
1866
|
for (const record of agent.records.values()) {
|
|
@@ -1764,6 +2011,8 @@ export function createRuntimeHost(options) {
|
|
|
1764
2011
|
clearTimeout(agent.retryTimer);
|
|
1765
2012
|
if (agent.stabilityTimer)
|
|
1766
2013
|
clearTimeout(agent.stabilityTimer);
|
|
2014
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
2015
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
1767
2016
|
if (agent.busy)
|
|
1768
2017
|
await agent.session?.cancel(reason);
|
|
1769
2018
|
await agent.session?.close(reason);
|
|
@@ -18,6 +18,7 @@ import { configureBuiltinPiProviderModel, stageBuiltinPiProvider, validatePiBase
|
|
|
18
18
|
import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, createOfficialPiLogoutRuntime, createOfficialPiRegistryRuntime, listOfficialPiAuthProviders, logoutOfficialPiProvider, officialPiAuthStatus, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
19
19
|
import { documentCommentSubscriptionCapability, markDocumentCommentSubscriptionVerified, } from "../platform/callback-capability.mjs";
|
|
20
20
|
import { openPlatformHost, parseLarkinTenant, registerAppAccountsHost, } from "../feishu/platform-hosts.mjs";
|
|
21
|
+
import { authorizationUrlFailureMessage, presentAuthorizationUrl } from "./setup-authorization-url.mjs";
|
|
21
22
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
22
23
|
// @ts-expect-error bundled CommonJS dependency
|
|
23
24
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -466,8 +467,10 @@ export async function main() {
|
|
|
466
467
|
用法:
|
|
467
468
|
bun bot-register.mjs --auto [--tenant feishu|lark]
|
|
468
469
|
|
|
469
|
-
--tenant 在授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn
|
|
470
|
-
Lark 必须用 --tenant lark
|
|
470
|
+
--tenant 在授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn /page/launcher)。
|
|
471
|
+
Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走飞书 accounts,
|
|
472
|
+
打开 open.larksuite.com),不要打开 /page/launcher(ack 10074 / 链接已失效)。
|
|
473
|
+
扫码完成后凭证回传,无需手抄 App Secret。
|
|
471
474
|
|
|
472
475
|
完成后自动:按返回 appId 判定新旧 → 同步并校验 lark-cli bot 凭证 →
|
|
473
476
|
原子写 bots/<appId>.json(0600)→ 绑定 Agent。`);
|
|
@@ -486,11 +489,14 @@ Lark 必须用 --tenant lark 或交互选择,授权主机是 accounts.larksuit
|
|
|
486
489
|
die("--result-file 必须是配置根目录内的 .setup-result-<pid>.json");
|
|
487
490
|
}
|
|
488
491
|
if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
|
|
489
|
-
die("不支持 --app-id;机器人必须在飞书(Lark
|
|
492
|
+
die("不支持 --app-id;机器人必须在飞书(Lark)网页中选择。已有官方 lark-cli profile 时用 --from-cli-profile");
|
|
490
493
|
}
|
|
494
|
+
const fromCliProfile = flag("--from-cli-profile") || "";
|
|
495
|
+
if (fromCliProfile && !/^[A-Za-z0-9._-]{1,64}$/.test(fromCliProfile))
|
|
496
|
+
die("--from-cli-profile 非法");
|
|
491
497
|
if (!autoSelect)
|
|
492
498
|
die("setup 注册阶段必须由交互式选择流程启动");
|
|
493
|
-
|
|
499
|
+
let selectedTenant = await resolveSelectedTenant();
|
|
494
500
|
say(`[setup 1/5] 在${selectedTenant === "lark" ? " International Lark" : "中国飞书 Feishu"}网页选择已有机器人或创建新机器人`);
|
|
495
501
|
if (commentSubscription === "application") {
|
|
496
502
|
say(`! 已显式选择 ${commentSubscription} 维度评论订阅:一旦平台状态验证为已订阅,Bot 可见文档中实际送达的每条支持评论都会进入 Inbox 并唤醒 Agent,不要求 @Bot。`);
|
|
@@ -524,30 +530,91 @@ Lark 必须用 --tenant lark 或交互选择,授权主机是 accounts.larksuit
|
|
|
524
530
|
"docs:document.comment:create",
|
|
525
531
|
];
|
|
526
532
|
const TENANT_EVENTS = ["im.message.receive_v1", "im.message.message_read_v1", "drive.notice.comment_add_v1"];
|
|
533
|
+
const LARKIN_REGISTER_ADDONS = {
|
|
534
|
+
scopes: { tenant: TENANT_SCOPES },
|
|
535
|
+
events: { items: { tenant: TENANT_EVENTS } },
|
|
536
|
+
callbacks: { items: ["card.action.trigger"] },
|
|
537
|
+
};
|
|
527
538
|
let pollingCount = 0;
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
539
|
+
const presentUrl = (url, expireIn) => {
|
|
540
|
+
qrcode.generate(url, { small: true }, (code) => say(code));
|
|
541
|
+
say(`\n打开以下链接(或扫码),${Math.round(expireIn / 60)} 分钟内有效:\n\n ${url}\n\n[setup 2/5] 等待飞书(Lark)网页完成授权并回传凭证…`);
|
|
542
|
+
say(openBrowser(url) ? "[setup] 已在默认浏览器打开授权页" : "[setup] 未能自动打开浏览器,请手动打开上面的链接");
|
|
543
|
+
};
|
|
544
|
+
const importedSecret = typeof process.env.LARKIN_SETUP_APP_SECRET === "string"
|
|
545
|
+
? process.env.LARKIN_SETUP_APP_SECRET.trim() : "";
|
|
546
|
+
delete process.env.LARKIN_SETUP_APP_SECRET;
|
|
547
|
+
let result = null;
|
|
548
|
+
if (fromCliProfile) {
|
|
549
|
+
if (!importedSecret)
|
|
550
|
+
die("--from-cli-profile 需要环境变量 LARKIN_SETUP_APP_SECRET(App Secret 不进 argv)");
|
|
551
|
+
const official = officialCliForProfile(process.env);
|
|
552
|
+
const listed = spawnSync(official.command, [...official.argsPrefix, "profile", "list"], { encoding: "utf8", env: process.env });
|
|
553
|
+
let profiles = [];
|
|
554
|
+
try {
|
|
555
|
+
profiles = JSON.parse(listed.stdout || "[]");
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
die("读取 lark-cli profile list 失败");
|
|
559
|
+
}
|
|
560
|
+
const matched = profiles.find((profile) => profile.name === fromCliProfile);
|
|
561
|
+
if (matched == null || typeof matched.appId !== "string" || !APP_ID.test(matched.appId)) {
|
|
562
|
+
die(`找不到 lark-cli profile ${fromCliProfile}`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const brand = matched.brand === "lark" || matched.brand === "feishu" ? matched.brand : selectedTenant;
|
|
566
|
+
selectedTenant = brand;
|
|
567
|
+
result = { client_id: matched.appId, client_secret: importedSecret, user_info: { tenant_brand: brand } };
|
|
568
|
+
say(`[setup 1/5] 复用官方 lark-cli profile ${fromCliProfile} → ${matched.appId}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (selectedTenant === "lark") {
|
|
572
|
+
// Official lark-cli always begins on Feishu accounts, then presents
|
|
573
|
+
// open.larksuite.com/page/cli. Beginning on Lark accounts yields /page/launcher
|
|
574
|
+
// codes that open.larksuite.com ack rejects with 10074.
|
|
575
|
+
say("[setup 1/5] Lark 租户走官方 /page/cli 扫码授权;扫码完成后凭证回传,不要打开 /page/launcher");
|
|
576
|
+
const official = officialCliForProfile(process.env);
|
|
577
|
+
result = await registerApp({
|
|
578
|
+
source: "larkin",
|
|
579
|
+
domain: registerAppAccountsHost("feishu"),
|
|
580
|
+
larkDomain: registerAppAccountsHost("lark"),
|
|
581
|
+
addons: LARKIN_REGISTER_ADDONS,
|
|
582
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
583
|
+
const presented = presentAuthorizationUrl(url, { tenant: "lark", larkCliVersion: official.version });
|
|
584
|
+
if (/\/page\/launcher(?:\?|$)/.test(presented)) {
|
|
585
|
+
throw new Error("Lark 租户拒绝把 /page/launcher 交给浏览器");
|
|
586
|
+
}
|
|
587
|
+
presentUrl(presented, expireIn);
|
|
588
|
+
},
|
|
589
|
+
onStatusChange: ({ status, interval }) => {
|
|
590
|
+
if (status === "domain_switched")
|
|
591
|
+
say("[setup] 已切换到 Lark 域名");
|
|
592
|
+
if (status === "slow_down")
|
|
593
|
+
say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
594
|
+
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
595
|
+
say(`[setup] 仍在等待飞书(Lark)回传凭证(约 ${pollingCount / 12} 分钟)…`);
|
|
596
|
+
},
|
|
597
|
+
}).catch((error) => die(authorizationUrlFailureMessage(error)));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
result = await registerApp({
|
|
601
|
+
source: "larkin",
|
|
602
|
+
domain: registerAppAccountsHost(selectedTenant),
|
|
603
|
+
larkDomain: registerAppAccountsHost("lark"),
|
|
604
|
+
addons: LARKIN_REGISTER_ADDONS,
|
|
605
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
606
|
+
presentUrl(url, expireIn);
|
|
607
|
+
},
|
|
608
|
+
onStatusChange: ({ status, interval }) => {
|
|
609
|
+
if (status === "domain_switched")
|
|
610
|
+
say("[setup] 已切换到 Lark 域名");
|
|
611
|
+
if (status === "slow_down")
|
|
612
|
+
say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
613
|
+
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
614
|
+
say(`[setup] 仍在等待飞书(Lark)回传凭证(约 ${pollingCount / 12} 分钟)…`);
|
|
615
|
+
},
|
|
616
|
+
}).catch((error) => die(authorizationUrlFailureMessage(error)));
|
|
617
|
+
}
|
|
551
618
|
const { client_id: rawId, client_secret: rawSecret, user_info: userInfo } = result || {};
|
|
552
619
|
if (typeof rawId !== "string" || !APP_ID.test(rawId))
|
|
553
620
|
die("授权返回的 App ID 格式非法;未执行凭证同步、文件写入或 Agent 绑定");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { openPlatformHost } from "../feishu/platform-hosts.mjs";
|
|
2
|
+
// Official lark-cli presents /page/cli. node-sdk registerApp returns /page/launcher,
|
|
3
|
+
// which open.larksuite.com ack rejects with 10074 ("链接已失效") for Lark tenants.
|
|
4
|
+
export function officialCliSetupUrl(input) {
|
|
5
|
+
const userCode = input.userCode.trim();
|
|
6
|
+
if (!/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(userCode))
|
|
7
|
+
throw new Error("setup user_code 格式非法");
|
|
8
|
+
const version = input.larkCliVersion.trim();
|
|
9
|
+
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
10
|
+
throw new Error("lark-cli version 格式非法");
|
|
11
|
+
const url = new URL(`${openPlatformHost(input.tenant)}/page/cli`);
|
|
12
|
+
url.searchParams.set("user_code", userCode);
|
|
13
|
+
url.searchParams.set("lpv", version);
|
|
14
|
+
url.searchParams.set("ocv", version);
|
|
15
|
+
url.searchParams.set("from", "cli");
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
export function presentAuthorizationUrl(rawUrl, input) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(rawUrl);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("授权 URL 非法");
|
|
25
|
+
}
|
|
26
|
+
const userCode = parsed.searchParams.get("user_code") || "";
|
|
27
|
+
// Feishu China still authorizes on /page/launcher. Only International Lark
|
|
28
|
+
// must leave launcher (open.larksuite.com ack 10074 / 链接已失效).
|
|
29
|
+
if (input.tenant === "lark" && parsed.pathname === "/page/launcher") {
|
|
30
|
+
if (!userCode)
|
|
31
|
+
throw new Error("Lark 租户拒绝把 /page/launcher 交给浏览器");
|
|
32
|
+
const presented = new URL(officialCliSetupUrl({ tenant: "lark", userCode, larkCliVersion: input.larkCliVersion }));
|
|
33
|
+
// node-sdk registerApp encodes scopes/events/callbacks only on the landing URL
|
|
34
|
+
// (`addons=`). Keep that payload when rewriting launcher → /page/cli so Lark
|
|
35
|
+
// still requests the same Larkin addons Feishu already sends.
|
|
36
|
+
const addons = parsed.searchParams.get("addons");
|
|
37
|
+
if (addons)
|
|
38
|
+
presented.searchParams.set("addons", addons);
|
|
39
|
+
return presented.toString();
|
|
40
|
+
}
|
|
41
|
+
return parsed.toString();
|
|
42
|
+
}
|
|
43
|
+
export function authorizationUrlFailureMessage(error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
46
|
+
const description = error && typeof error === "object" && "description" in error
|
|
47
|
+
? String(error.description) : "";
|
|
48
|
+
const detail = [code, description, message].filter(Boolean).join(": ");
|
|
49
|
+
return `网页授权失败(${detail || "unknown"});未执行凭证同步、文件写入或 Agent 绑定。Lark 租户不要使用 /page/launcher(ack 10074 / 链接已失效),应走官方 lark-cli 的 /page/cli。`;
|
|
50
|
+
}
|