larkin 0.4.20 → 0.4.21
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/agent/missed-outbound-scan.cjs +123 -0
- package/dist/agent/missed-outbound-scan.mjs +112 -0
- package/dist/feishu/host-shell.cjs +9 -0
- package/dist/feishu/host-shell.mjs +9 -0
- package/dist/feishu/message-policy.cjs +2 -0
- package/dist/feishu/message-policy.mjs +2 -0
- package/dist/setup/bot-register.mjs +53 -27
- package/dist/setup/grant-scopes.mjs +13 -4
- package/dist/setup/tenant-scope-grant.mjs +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_MISSED_OUTBOUND_REPEAT = exports.DEFAULT_MISSED_OUTBOUND_TITLE = void 0;
|
|
7
|
+
exports.parseOmMessageId = parseOmMessageId;
|
|
8
|
+
exports.parseScanDeliveryTarget = parseScanDeliveryTarget;
|
|
9
|
+
exports.isDefaultMissedOutboundReminder = isDefaultMissedOutboundReminder;
|
|
10
|
+
exports.ensureDefaultMissedOutboundScanReminder = ensureDefaultMissedOutboundScanReminder;
|
|
11
|
+
exports.persistInboundScanTarget = persistInboundScanTarget;
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const reminder_store_js_1 = require("./reminder-store.cjs");
|
|
14
|
+
exports.DEFAULT_MISSED_OUTBOUND_TITLE = "Read scoped history from this reminder envelope's persisted deliveryTarget (chat: +chat-messages-list; thread: +threads-messages-list). Judge unanswered asks, undelivered follow-ups, and stalled work. On a hit, post a short status in the same conversation using that persisted deliveryTarget/anchor; otherwise stay silent. Never infer recipients from the title.";
|
|
15
|
+
exports.DEFAULT_MISSED_OUTBOUND_REPEAT = "every:15m";
|
|
16
|
+
const CHAT_TARGET = /^chat:(oc_[A-Za-z0-9]+)$/;
|
|
17
|
+
const THREAD_TARGET = /^thread:(oc_[A-Za-z0-9]+):(omt_[A-Za-z0-9]+)$/;
|
|
18
|
+
const OM_ID = /^om_[A-Za-z0-9]+$/;
|
|
19
|
+
function parseOmMessageId(value, label = "message-id") {
|
|
20
|
+
const id = String(value || "").trim();
|
|
21
|
+
if (!OM_ID.test(id))
|
|
22
|
+
throw new Error(`${label} 必须是严格 om_ 语法`);
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
function parseScanDeliveryTarget(target, anchor) {
|
|
26
|
+
const deliveryTarget = String(target || "").trim();
|
|
27
|
+
if (!deliveryTarget) {
|
|
28
|
+
throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
|
|
29
|
+
}
|
|
30
|
+
const chat = deliveryTarget.match(CHAT_TARGET);
|
|
31
|
+
if (chat) {
|
|
32
|
+
return {
|
|
33
|
+
deliveryTarget,
|
|
34
|
+
scope: "chat",
|
|
35
|
+
chatId: chat[1],
|
|
36
|
+
threadId: null,
|
|
37
|
+
deliveryAnchor: anchor ? parseOmMessageId(anchor, "delivery anchor") : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const thread = deliveryTarget.match(THREAD_TARGET);
|
|
41
|
+
if (thread) {
|
|
42
|
+
return {
|
|
43
|
+
deliveryTarget,
|
|
44
|
+
scope: "thread",
|
|
45
|
+
chatId: thread[1],
|
|
46
|
+
threadId: thread[2],
|
|
47
|
+
deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
throw new Error("delivery target 必须是 chat:oc_… 或 thread:oc_…:omt_…,禁止推断 DM");
|
|
51
|
+
}
|
|
52
|
+
function isDefaultMissedOutboundReminder(reminder) {
|
|
53
|
+
return reminder?.title === exports.DEFAULT_MISSED_OUTBOUND_TITLE
|
|
54
|
+
&& reminder.repeat === exports.DEFAULT_MISSED_OUTBOUND_REPEAT
|
|
55
|
+
&& reminder.deliveryMode === "user"
|
|
56
|
+
&& reminder.status === "scheduled";
|
|
57
|
+
}
|
|
58
|
+
function ensureDefaultMissedOutboundScanReminder(input) {
|
|
59
|
+
const parsed = parseScanDeliveryTarget(input.deliveryTarget, input.deliveryAnchor);
|
|
60
|
+
const recurrence = (0, reminder_store_js_1.parseRepeat)(exports.DEFAULT_MISSED_OUTBOUND_REPEAT);
|
|
61
|
+
if ("error" in recurrence)
|
|
62
|
+
throw new Error(recurrence.error);
|
|
63
|
+
const now = input.nowMs ?? Date.now();
|
|
64
|
+
const wantedAnchor = parsed.deliveryAnchor;
|
|
65
|
+
return (0, reminder_store_js_1.mutate)(input.storeFile, (store) => {
|
|
66
|
+
const sameIdentity = (reminder) => reminder.ownerAgentId === input.agentId
|
|
67
|
+
&& reminder.title === exports.DEFAULT_MISSED_OUTBOUND_TITLE
|
|
68
|
+
&& reminder.repeat === exports.DEFAULT_MISSED_OUTBOUND_REPEAT
|
|
69
|
+
&& reminder.deliveryMode === "user"
|
|
70
|
+
&& reminder.deliveryTarget === parsed.deliveryTarget;
|
|
71
|
+
const scheduled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "scheduled");
|
|
72
|
+
if (scheduled) {
|
|
73
|
+
if ((scheduled.deliveryAnchor || null) !== wantedAnchor)
|
|
74
|
+
scheduled.deliveryAnchor = wantedAnchor;
|
|
75
|
+
return { created: false, reminderId: scheduled.reminderId, rebuilt: false };
|
|
76
|
+
}
|
|
77
|
+
const canceled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "canceled");
|
|
78
|
+
if (canceled)
|
|
79
|
+
return { created: false, reminderId: canceled.reminderId, rebuilt: false };
|
|
80
|
+
const hadFired = store.reminders.some((reminder) => sameIdentity(reminder) && reminder.status === "fired");
|
|
81
|
+
store.reminders = store.reminders.filter((reminder) => !(sameIdentity(reminder) && reminder.status === "fired"));
|
|
82
|
+
const reminderId = (0, reminder_store_js_1.newId)();
|
|
83
|
+
store.reminders.push({
|
|
84
|
+
reminderId,
|
|
85
|
+
ownerAgentId: input.agentId,
|
|
86
|
+
title: exports.DEFAULT_MISSED_OUTBOUND_TITLE,
|
|
87
|
+
fireAt: (0, reminder_store_js_1.nowIso)(now + 15 * 60_000),
|
|
88
|
+
firedAt: null,
|
|
89
|
+
createdAt: (0, reminder_store_js_1.nowIso)(now),
|
|
90
|
+
status: "scheduled",
|
|
91
|
+
version: 1,
|
|
92
|
+
events: [],
|
|
93
|
+
deliveryTarget: parsed.deliveryTarget,
|
|
94
|
+
deliveryAnchor: wantedAnchor,
|
|
95
|
+
deliveryMode: "user",
|
|
96
|
+
repeat: exports.DEFAULT_MISSED_OUTBOUND_REPEAT,
|
|
97
|
+
});
|
|
98
|
+
return { created: true, reminderId, rebuilt: hadFired };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function persistInboundScanTarget(stateDir, event, agentId, storeFile = node_path_1.default.join(stateDir, "reminders.json")) {
|
|
102
|
+
if (event._sender_is_bot)
|
|
103
|
+
throw new Error("scan reminder 只接受 human inbound");
|
|
104
|
+
if (event._scan_authority !== true)
|
|
105
|
+
throw new Error("scan reminder 需要权威 chatType/senderIsBot");
|
|
106
|
+
if (event.chat_type !== "group")
|
|
107
|
+
throw new Error("scan reminder 只接受 group/thread,禁止 DM");
|
|
108
|
+
const chatId = String(event.chat_id || "");
|
|
109
|
+
if (!/^oc_[A-Za-z0-9]+$/.test(chatId)) {
|
|
110
|
+
throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
|
|
111
|
+
}
|
|
112
|
+
const threadId = event.thread_id ? String(event.thread_id) : "";
|
|
113
|
+
const parsed = threadId
|
|
114
|
+
? parseScanDeliveryTarget(`thread:${chatId}:${threadId}`, event.message_id)
|
|
115
|
+
: parseScanDeliveryTarget(`chat:${chatId}`, event.message_id || null);
|
|
116
|
+
ensureDefaultMissedOutboundScanReminder({
|
|
117
|
+
storeFile,
|
|
118
|
+
agentId,
|
|
119
|
+
deliveryTarget: parsed.deliveryTarget,
|
|
120
|
+
deliveryAnchor: parsed.deliveryAnchor,
|
|
121
|
+
});
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mutate, newId, nowIso, parseRepeat } from "./reminder-store.mjs";
|
|
3
|
+
export const DEFAULT_MISSED_OUTBOUND_TITLE = "Read scoped history from this reminder envelope's persisted deliveryTarget (chat: +chat-messages-list; thread: +threads-messages-list). Judge unanswered asks, undelivered follow-ups, and stalled work. On a hit, post a short status in the same conversation using that persisted deliveryTarget/anchor; otherwise stay silent. Never infer recipients from the title.";
|
|
4
|
+
export const DEFAULT_MISSED_OUTBOUND_REPEAT = "every:15m";
|
|
5
|
+
const CHAT_TARGET = /^chat:(oc_[A-Za-z0-9]+)$/;
|
|
6
|
+
const THREAD_TARGET = /^thread:(oc_[A-Za-z0-9]+):(omt_[A-Za-z0-9]+)$/;
|
|
7
|
+
const OM_ID = /^om_[A-Za-z0-9]+$/;
|
|
8
|
+
export function parseOmMessageId(value, label = "message-id") {
|
|
9
|
+
const id = String(value || "").trim();
|
|
10
|
+
if (!OM_ID.test(id))
|
|
11
|
+
throw new Error(`${label} 必须是严格 om_ 语法`);
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
export function parseScanDeliveryTarget(target, anchor) {
|
|
15
|
+
const deliveryTarget = String(target || "").trim();
|
|
16
|
+
if (!deliveryTarget) {
|
|
17
|
+
throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
|
|
18
|
+
}
|
|
19
|
+
const chat = deliveryTarget.match(CHAT_TARGET);
|
|
20
|
+
if (chat) {
|
|
21
|
+
return {
|
|
22
|
+
deliveryTarget,
|
|
23
|
+
scope: "chat",
|
|
24
|
+
chatId: chat[1],
|
|
25
|
+
threadId: null,
|
|
26
|
+
deliveryAnchor: anchor ? parseOmMessageId(anchor, "delivery anchor") : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const thread = deliveryTarget.match(THREAD_TARGET);
|
|
30
|
+
if (thread) {
|
|
31
|
+
return {
|
|
32
|
+
deliveryTarget,
|
|
33
|
+
scope: "thread",
|
|
34
|
+
chatId: thread[1],
|
|
35
|
+
threadId: thread[2],
|
|
36
|
+
deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
throw new Error("delivery target 必须是 chat:oc_… 或 thread:oc_…:omt_…,禁止推断 DM");
|
|
40
|
+
}
|
|
41
|
+
export function isDefaultMissedOutboundReminder(reminder) {
|
|
42
|
+
return reminder?.title === DEFAULT_MISSED_OUTBOUND_TITLE
|
|
43
|
+
&& reminder.repeat === DEFAULT_MISSED_OUTBOUND_REPEAT
|
|
44
|
+
&& reminder.deliveryMode === "user"
|
|
45
|
+
&& reminder.status === "scheduled";
|
|
46
|
+
}
|
|
47
|
+
export function ensureDefaultMissedOutboundScanReminder(input) {
|
|
48
|
+
const parsed = parseScanDeliveryTarget(input.deliveryTarget, input.deliveryAnchor);
|
|
49
|
+
const recurrence = parseRepeat(DEFAULT_MISSED_OUTBOUND_REPEAT);
|
|
50
|
+
if ("error" in recurrence)
|
|
51
|
+
throw new Error(recurrence.error);
|
|
52
|
+
const now = input.nowMs ?? Date.now();
|
|
53
|
+
const wantedAnchor = parsed.deliveryAnchor;
|
|
54
|
+
return mutate(input.storeFile, (store) => {
|
|
55
|
+
const sameIdentity = (reminder) => reminder.ownerAgentId === input.agentId
|
|
56
|
+
&& reminder.title === DEFAULT_MISSED_OUTBOUND_TITLE
|
|
57
|
+
&& reminder.repeat === DEFAULT_MISSED_OUTBOUND_REPEAT
|
|
58
|
+
&& reminder.deliveryMode === "user"
|
|
59
|
+
&& reminder.deliveryTarget === parsed.deliveryTarget;
|
|
60
|
+
const scheduled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "scheduled");
|
|
61
|
+
if (scheduled) {
|
|
62
|
+
if ((scheduled.deliveryAnchor || null) !== wantedAnchor)
|
|
63
|
+
scheduled.deliveryAnchor = wantedAnchor;
|
|
64
|
+
return { created: false, reminderId: scheduled.reminderId, rebuilt: false };
|
|
65
|
+
}
|
|
66
|
+
const canceled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "canceled");
|
|
67
|
+
if (canceled)
|
|
68
|
+
return { created: false, reminderId: canceled.reminderId, rebuilt: false };
|
|
69
|
+
const hadFired = store.reminders.some((reminder) => sameIdentity(reminder) && reminder.status === "fired");
|
|
70
|
+
store.reminders = store.reminders.filter((reminder) => !(sameIdentity(reminder) && reminder.status === "fired"));
|
|
71
|
+
const reminderId = newId();
|
|
72
|
+
store.reminders.push({
|
|
73
|
+
reminderId,
|
|
74
|
+
ownerAgentId: input.agentId,
|
|
75
|
+
title: DEFAULT_MISSED_OUTBOUND_TITLE,
|
|
76
|
+
fireAt: nowIso(now + 15 * 60_000),
|
|
77
|
+
firedAt: null,
|
|
78
|
+
createdAt: nowIso(now),
|
|
79
|
+
status: "scheduled",
|
|
80
|
+
version: 1,
|
|
81
|
+
events: [],
|
|
82
|
+
deliveryTarget: parsed.deliveryTarget,
|
|
83
|
+
deliveryAnchor: wantedAnchor,
|
|
84
|
+
deliveryMode: "user",
|
|
85
|
+
repeat: DEFAULT_MISSED_OUTBOUND_REPEAT,
|
|
86
|
+
});
|
|
87
|
+
return { created: true, reminderId, rebuilt: hadFired };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function persistInboundScanTarget(stateDir, event, agentId, storeFile = path.join(stateDir, "reminders.json")) {
|
|
91
|
+
if (event._sender_is_bot)
|
|
92
|
+
throw new Error("scan reminder 只接受 human inbound");
|
|
93
|
+
if (event._scan_authority !== true)
|
|
94
|
+
throw new Error("scan reminder 需要权威 chatType/senderIsBot");
|
|
95
|
+
if (event.chat_type !== "group")
|
|
96
|
+
throw new Error("scan reminder 只接受 group/thread,禁止 DM");
|
|
97
|
+
const chatId = String(event.chat_id || "");
|
|
98
|
+
if (!/^oc_[A-Za-z0-9]+$/.test(chatId)) {
|
|
99
|
+
throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
|
|
100
|
+
}
|
|
101
|
+
const threadId = event.thread_id ? String(event.thread_id) : "";
|
|
102
|
+
const parsed = threadId
|
|
103
|
+
? parseScanDeliveryTarget(`thread:${chatId}:${threadId}`, event.message_id)
|
|
104
|
+
: parseScanDeliveryTarget(`chat:${chatId}`, event.message_id || null);
|
|
105
|
+
ensureDefaultMissedOutboundScanReminder({
|
|
106
|
+
storeFile,
|
|
107
|
+
agentId,
|
|
108
|
+
deliveryTarget: parsed.deliveryTarget,
|
|
109
|
+
deliveryAnchor: parsed.deliveryAnchor,
|
|
110
|
+
});
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
@@ -51,6 +51,7 @@ const host_business_state_js_1 = require("./host-business-state.cjs");
|
|
|
51
51
|
const host_processing_eye_js_1 = require("./host-processing-eye.cjs");
|
|
52
52
|
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
53
53
|
const host_reminder_orchestrator_js_1 = require("../agent/host-reminder-orchestrator.cjs");
|
|
54
|
+
const missed_outbound_scan_js_1 = require("../agent/missed-outbound-scan.cjs");
|
|
54
55
|
const host_channel_business_js_1 = require("./host-channel-business.cjs");
|
|
55
56
|
const interaction_orchestrator_js_1 = require("./interaction-orchestrator.cjs");
|
|
56
57
|
const message_policy_js_1 = require("./message-policy.cjs");
|
|
@@ -385,6 +386,14 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
385
386
|
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
386
387
|
if (event.event_id)
|
|
387
388
|
seenEventIds.add(eventKey);
|
|
389
|
+
if (!event._sender_is_bot && event._scan_authority) {
|
|
390
|
+
try {
|
|
391
|
+
(0, missed_outbound_scan_js_1.persistInboundScanTarget)(agent.stateDir, event, agent.agentId);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
log(`inbound scan target 未持久化: ${error.message}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
388
397
|
if (append.status === "duplicate_consumed")
|
|
389
398
|
return null;
|
|
390
399
|
const inboxEnvelope = append.envelope;
|
|
@@ -11,6 +11,7 @@ import { HostEnvelopeProjector, HostStateProjection, SenderIdentityCache, projec
|
|
|
11
11
|
import { ProcessingEyeOrchestrator } from "./host-processing-eye.mjs";
|
|
12
12
|
import { projectInboxEnvelope, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
|
|
13
13
|
import { HostReminderOrchestrator } from "../agent/host-reminder-orchestrator.mjs";
|
|
14
|
+
import { persistInboundScanTarget } from "../agent/missed-outbound-scan.mjs";
|
|
14
15
|
import { HostChannelBusiness } from "./host-channel-business.mjs";
|
|
15
16
|
import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
|
|
16
17
|
import { targetFor } from "./message-policy.mjs";
|
|
@@ -345,6 +346,14 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
345
346
|
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
346
347
|
if (event.event_id)
|
|
347
348
|
seenEventIds.add(eventKey);
|
|
349
|
+
if (!event._sender_is_bot && event._scan_authority) {
|
|
350
|
+
try {
|
|
351
|
+
persistInboundScanTarget(agent.stateDir, event, agent.agentId);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
log(`inbound scan target 未持久化: ${error.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
348
357
|
if (append.status === "duplicate_consumed")
|
|
349
358
|
return null;
|
|
350
359
|
const inboxEnvelope = append.envelope;
|
|
@@ -39,6 +39,8 @@ function normalizeChannelMessage(message) {
|
|
|
39
39
|
_mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
|
|
40
40
|
_mention_all: Boolean(message.mentionAll),
|
|
41
41
|
_sender_is_bot: message.senderIsBot === true,
|
|
42
|
+
_scan_authority: typeof message.senderIsBot === "boolean"
|
|
43
|
+
&& (message.chatType === "group" || message.chatMode === "topic"),
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
function slug10(value) {
|
|
@@ -26,6 +26,8 @@ export function normalizeChannelMessage(message) {
|
|
|
26
26
|
_mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
|
|
27
27
|
_mention_all: Boolean(message.mentionAll),
|
|
28
28
|
_sender_is_bot: message.senderIsBot === true,
|
|
29
|
+
_scan_authority: typeof message.senderIsBot === "boolean"
|
|
30
|
+
&& (message.chatType === "group" || message.chatMode === "topic"),
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
export function slug10(value) {
|
|
@@ -19,6 +19,7 @@ import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime,
|
|
|
19
19
|
import { documentCommentSubscriptionCapability, markDocumentCommentSubscriptionVerified, } from "../platform/callback-capability.mjs";
|
|
20
20
|
import { openPlatformHost, parseLarkinTenant, registerAppAccountsHost, } from "../feishu/platform-hosts.mjs";
|
|
21
21
|
import { authorizationUrlFailureMessage, presentAuthorizationUrl } from "./setup-authorization-url.mjs";
|
|
22
|
+
import { missingGrantedTenantScopes } from "./tenant-scope-grant.mjs";
|
|
22
23
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
23
24
|
// @ts-expect-error bundled CommonJS dependency
|
|
24
25
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -28,6 +29,7 @@ const testFixture = process.env.LARKIN_TEST_BOT_REGISTER_MODULE
|
|
|
28
29
|
const registerApp = testFixture?.registerApp ?? channelRegisterApp;
|
|
29
30
|
const qrcode = testFixture?.qrcode ?? qrcodePackage;
|
|
30
31
|
const spawnSync = testFixture?.spawnSync ?? systemSpawnSync;
|
|
32
|
+
const spawnChild = testFixture?.spawn ?? systemSpawn;
|
|
31
33
|
const synchronizeAgentProfile = testFixture?.syncAgentProfile ?? syncAgentProfile;
|
|
32
34
|
const resolveOfficialCli = testFixture?.resolveOfficialLarkCli ?? resolveOfficialLarkCli;
|
|
33
35
|
const wait = testFixture?.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
@@ -306,7 +308,7 @@ async function runBindProcess(command, args) {
|
|
|
306
308
|
return spawnSync(command, [...args], { env: process.env, stdio: "inherit" }).status;
|
|
307
309
|
}
|
|
308
310
|
return await new Promise((resolve, reject) => {
|
|
309
|
-
const child =
|
|
311
|
+
const child = spawnChild(command, [...args], { env: process.env, stdio: "inherit" });
|
|
310
312
|
trackPendingChild(child);
|
|
311
313
|
let killTimer = null;
|
|
312
314
|
const abort = () => {
|
|
@@ -339,7 +341,7 @@ async function runBoundedCliProcess(command, args, env, label) {
|
|
|
339
341
|
return { status: result.status, stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
340
342
|
}
|
|
341
343
|
return await new Promise((resolve, reject) => {
|
|
342
|
-
const child =
|
|
344
|
+
const child = spawnChild(command, [...args], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
343
345
|
trackPendingChild(child);
|
|
344
346
|
let stdout = "";
|
|
345
347
|
let stderr = "";
|
|
@@ -641,12 +643,13 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
|
|
|
641
643
|
: commentSubscription === "none" || !priorSubscription
|
|
642
644
|
? { mode: "none", status: "safe-default", source: "setup-default", updatedAt: requestedAt }
|
|
643
645
|
: priorSubscription;
|
|
644
|
-
|
|
646
|
+
const injectedAgentChoice = testFixture?.collectSetupAgentChoice;
|
|
647
|
+
if (!flag("--runtime") && (!testFixture || injectedAgentChoice || process.env.LARKIN_TEST_ENABLE_AGENT_CHOICE === "1")) {
|
|
645
648
|
const existing = larkinConfig.loadConfig(process.env).config.agents[id];
|
|
646
649
|
// The official resolver performs a synchronous login-shell probe. Resolve it
|
|
647
650
|
// exactly once before the credential heartbeat lock becomes active.
|
|
648
651
|
resolvedSetupOfficialCli = resolveOfficialCli({ env: process.env });
|
|
649
|
-
const questioner = terminalSetupQuestioner();
|
|
652
|
+
const questioner = injectedAgentChoice ? { ask: async () => "", secret: async () => "" } : terminalSetupQuestioner();
|
|
650
653
|
// One setup-owned transaction starts before status/logout and remains active
|
|
651
654
|
// through selection, login, bind, readiness, and the final setup commit.
|
|
652
655
|
pendingPiAuthTransaction = beginBuiltinPiCredentialTransaction(CFG_DIR, id);
|
|
@@ -657,37 +660,43 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
|
|
|
657
660
|
report: (message) => say(message),
|
|
658
661
|
};
|
|
659
662
|
try {
|
|
660
|
-
const requested =
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
663
|
+
const requested = injectedAgentChoice
|
|
664
|
+
? await injectedAgentChoice()
|
|
665
|
+
: await collectSetupAgentChoice(questioner, existing, authServices);
|
|
666
|
+
const choice = injectedAgentChoice
|
|
667
|
+
? requested
|
|
668
|
+
: await recoverUnavailableExternalPi(requested, questioner, () => probeNativeRuntimeReadiness({
|
|
669
|
+
runtime: "pi", agentId: id, cwd: path.join(CFG_DIR, "agents", id), env: process.env,
|
|
670
|
+
}), (message) => say(`! ${message}`), authServices);
|
|
664
671
|
if (choice) {
|
|
665
672
|
let serializedChoice = choice;
|
|
666
673
|
if (choice.runtime === "pi" && choice.distribution === "builtin") {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
say(`正在通过捆绑官方 Pi 登录 ${providerId}(${authType})…`);
|
|
674
|
-
piRuntime = await createOfficialPiModelRuntime(CFG_DIR, id);
|
|
675
|
-
await runOfficialPiLogin(piRuntime, providerId, authType, createOfficialPiAuthInteraction({ questioner, report: (message) => say(message), openUrl: openBrowser }));
|
|
676
|
-
}
|
|
677
|
-
catch {
|
|
678
|
-
pendingPiAuthTransaction.rollback();
|
|
679
|
-
pendingPiAuthTransaction = null;
|
|
680
|
-
throw new Error(`官方 Pi ${providerId} 登录失败或已取消;credential/config 未修改`);
|
|
681
|
-
}
|
|
682
|
-
if (process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN !== "1") {
|
|
683
|
-
say("正在验证内置 Pi provider(受控单轮,不发送飞书(Lark)消息)…");
|
|
674
|
+
if (!injectedAgentChoice) {
|
|
675
|
+
const official = choice.preset === "official" ? choice : null;
|
|
676
|
+
const configured = official ? null : configureBuiltinPiProviderModel(CFG_DIR, id, choice);
|
|
677
|
+
const providerId = official?.providerId || configured.provider;
|
|
678
|
+
const authType = official?.authType || "api_key";
|
|
679
|
+
let piRuntime;
|
|
684
680
|
try {
|
|
685
|
-
|
|
681
|
+
say(`正在通过捆绑官方 Pi 登录 ${providerId}(${authType})…`);
|
|
682
|
+
piRuntime = await createOfficialPiModelRuntime(CFG_DIR, id);
|
|
683
|
+
await runOfficialPiLogin(piRuntime, providerId, authType, createOfficialPiAuthInteraction({ questioner, report: (message) => say(message), openUrl: openBrowser }));
|
|
686
684
|
}
|
|
687
685
|
catch {
|
|
688
686
|
pendingPiAuthTransaction.rollback();
|
|
689
687
|
pendingPiAuthTransaction = null;
|
|
690
|
-
throw new Error(`官方 Pi ${providerId}
|
|
688
|
+
throw new Error(`官方 Pi ${providerId} 登录失败或已取消;credential/config 未修改`);
|
|
689
|
+
}
|
|
690
|
+
if (process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN !== "1") {
|
|
691
|
+
say("正在验证内置 Pi provider(受控单轮,不发送飞书(Lark)消息)…");
|
|
692
|
+
try {
|
|
693
|
+
await verifyOfficialPiProviderTurn(piRuntime, choice.model);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
pendingPiAuthTransaction.rollback();
|
|
697
|
+
pendingPiAuthTransaction = null;
|
|
698
|
+
throw new Error(`官方 Pi ${providerId} readiness 失败;credential/config 未修改`);
|
|
699
|
+
}
|
|
691
700
|
}
|
|
692
701
|
}
|
|
693
702
|
serializedChoice = { ...choice, authCompleted: true, readinessCompleted: true };
|
|
@@ -850,6 +859,21 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
|
|
|
850
859
|
}
|
|
851
860
|
if (!verified)
|
|
852
861
|
throw new Error("Bot identity verification failed");
|
|
862
|
+
const scopeResult = await runIdentityProcess(official.command, [...official.argsPrefix, "api", "GET", "/open-apis/application/v6/scopes"], cliEnv);
|
|
863
|
+
let scopePayload;
|
|
864
|
+
try {
|
|
865
|
+
scopePayload = JSON.parse(scopeResult.stdout || "");
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
scopePayload = null;
|
|
869
|
+
}
|
|
870
|
+
const missing = missingGrantedTenantScopes(scopePayload);
|
|
871
|
+
if (scopeResult.status !== 0 || missing.length > 0) {
|
|
872
|
+
const reason = scopeResult.status !== 0
|
|
873
|
+
? `scopes API 失败 status=${scopeResult.status}`
|
|
874
|
+
: `缺 ${missing.join(", ")}`;
|
|
875
|
+
throw new Error(`Bot tenant scope 未就绪,${reason};+chat-list 成功不能当作可发群消息。请在开发者后台开通后重跑 setup,不要重建 Agent。`);
|
|
876
|
+
}
|
|
853
877
|
if (commentSubscription !== "preserve") {
|
|
854
878
|
const reconciled = await applyDocumentCommentSubscription({
|
|
855
879
|
mode: commentSubscription,
|
|
@@ -901,6 +925,8 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
|
|
|
901
925
|
}
|
|
902
926
|
catch (error) {
|
|
903
927
|
const diagnostic = errorMessage(error);
|
|
928
|
+
if (diagnostic.startsWith("Bot tenant scope 未就绪"))
|
|
929
|
+
die(diagnostic);
|
|
904
930
|
if (diagnostic.startsWith("document comment application subscription")
|
|
905
931
|
|| diagnostic.startsWith("document comment local verified-state persistence"))
|
|
906
932
|
say(`! ${diagnostic}`);
|
|
@@ -10,6 +10,7 @@ import * as larkinConfig from "../platform/config.mjs";
|
|
|
10
10
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
11
11
|
import { loadValidatedBotCredential } from "./run-credential-preflight.mjs";
|
|
12
12
|
import { parseLarkinTenant, registerAppAccountsHost } from "../feishu/platform-hosts.mjs";
|
|
13
|
+
import { presentAuthorizationUrl } from "./setup-authorization-url.mjs";
|
|
13
14
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
14
15
|
// @ts-expect-error bundled CommonJS dependency
|
|
15
16
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -108,18 +109,26 @@ export async function main() {
|
|
|
108
109
|
},
|
|
109
110
|
onQRCodeReady: (info) => {
|
|
110
111
|
const minutes = Math.max(1, Math.round(info.expireIn / 60));
|
|
112
|
+
const official = resolveManagedOfficialCli(selected, process.env);
|
|
113
|
+
const presented = presentAuthorizationUrl(info.url, {
|
|
114
|
+
tenant: TENANT,
|
|
115
|
+
larkCliVersion: official.command.version,
|
|
116
|
+
});
|
|
117
|
+
if (TENANT === "lark" && /\/page\/launcher(?:\?|$)/.test(presented)) {
|
|
118
|
+
throw new Error("Lark 租户拒绝把 /page/launcher 交给浏览器");
|
|
119
|
+
}
|
|
111
120
|
log("\n请用飞书(Lark)扫码或打开链接,确认给应用增补权限:\n");
|
|
112
|
-
qrcode.generate(
|
|
113
|
-
log(`\n链接: ${
|
|
121
|
+
qrcode.generate(presented, { small: true });
|
|
122
|
+
log(`\n链接: ${presented}\n有效期约 ${minutes} 分钟(⚠️ 需本进程保持轮询,否则 user_code 立即失效)\n`);
|
|
114
123
|
if (URL_FILE) {
|
|
115
124
|
try {
|
|
116
|
-
fs.writeFileSync(URL_FILE,
|
|
125
|
+
fs.writeFileSync(URL_FILE, presented);
|
|
117
126
|
}
|
|
118
127
|
catch (error) {
|
|
119
128
|
log(`[grant] 写 url-file 失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
|
-
sendUrlToChat(
|
|
131
|
+
sendUrlToChat(presented, minutes);
|
|
123
132
|
},
|
|
124
133
|
onStatusChange: (info) => log(`[grant] 状态: ${info.status}`),
|
|
125
134
|
domain: registerAppAccountsHost(TENANT),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Tenant scopes the freshness gate needs on the Bot application. */
|
|
2
|
+
export const FRESHNESS_REQUIRED_TENANT_SCOPES = ["im:message.group_msg"];
|
|
3
|
+
function asRows(payload) {
|
|
4
|
+
if (!payload || typeof payload !== "object")
|
|
5
|
+
return [];
|
|
6
|
+
const root = payload;
|
|
7
|
+
const raw = Array.isArray(root.data?.scopes) ? root.data.scopes
|
|
8
|
+
: Array.isArray(root.scopes) ? root.scopes
|
|
9
|
+
: [];
|
|
10
|
+
return raw.filter((row) => !!row && typeof row === "object");
|
|
11
|
+
}
|
|
12
|
+
/** grant_status 1 is granted on GET /open-apis/application/v6/scopes. */
|
|
13
|
+
export function missingGrantedTenantScopes(payload, required = FRESHNESS_REQUIRED_TENANT_SCOPES) {
|
|
14
|
+
const rows = asRows(payload);
|
|
15
|
+
return required.filter((name) => !rows.some((row) => row.scope_name === name && row.grant_status === 1));
|
|
16
|
+
}
|