@nettee/beacon 0.2.6 → 0.2.8
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/feishu/intake.js +11 -0
- package/dist/feishu/mentions.js +102 -0
- package/dist/run/orchestrator.js +58 -6
- package/dist/runtime/pi-rpc.js +24 -0
- package/package.json +1 -1
package/dist/feishu/intake.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldProcessFeishuInbound } from "./mentions.js";
|
|
1
2
|
import { buildFeishuTriggerInput, } from "./trigger-input.js";
|
|
2
3
|
export class FeishuIntake {
|
|
3
4
|
options;
|
|
@@ -10,6 +11,16 @@ export class FeishuIntake {
|
|
|
10
11
|
(event.message.chat_type !== "p2p" && event.message.chat_type !== "group")) {
|
|
11
12
|
return "ignored";
|
|
12
13
|
}
|
|
14
|
+
// Temporary hard cut: group @All (without a direct bot @) never claims or runs.
|
|
15
|
+
if (!shouldProcessFeishuInbound({
|
|
16
|
+
chatType: event.message.chat_type,
|
|
17
|
+
mentions: event.message.mentions,
|
|
18
|
+
messageType: event.message.message_type,
|
|
19
|
+
content: event.message.content,
|
|
20
|
+
})) {
|
|
21
|
+
console.log(`[beacon] ignored group event without direct bot mention message_id=${event.message.message_id}`);
|
|
22
|
+
return "ignored";
|
|
23
|
+
}
|
|
13
24
|
if (!event.event_id)
|
|
14
25
|
throw new Error("Feishu event_id is required");
|
|
15
26
|
const claim = await this.options.store.claim({
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary hard cut: group @All must not start a Beacon run.
|
|
3
|
+
* Prefer Feishu `message.mentions`; fall back to content placeholders.
|
|
4
|
+
*/
|
|
5
|
+
function normalize(value) {
|
|
6
|
+
return value?.trim().toLowerCase() ?? "";
|
|
7
|
+
}
|
|
8
|
+
/** True when a Feishu mention entry is @All / @所有人 / @_all. */
|
|
9
|
+
export function isFeishuAtAllMention(mention) {
|
|
10
|
+
const key = normalize(mention.key);
|
|
11
|
+
if (key === "@_all" || key === "@all")
|
|
12
|
+
return true;
|
|
13
|
+
const name = mention.name?.trim() ?? "";
|
|
14
|
+
const nameLower = name.toLowerCase();
|
|
15
|
+
if (name === "所有人" || nameLower === "all" || nameLower === "@all") {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const userId = normalize(mention.id?.user_id);
|
|
19
|
+
if (userId === "all" || userId === "@_all")
|
|
20
|
+
return true;
|
|
21
|
+
const openId = mention.id?.open_id?.trim() ?? "";
|
|
22
|
+
const unionId = mention.id?.union_id?.trim() ?? "";
|
|
23
|
+
// Empty-identity @_all-shaped rows sometimes omit a clean key.
|
|
24
|
+
if (!openId &&
|
|
25
|
+
!unionId &&
|
|
26
|
+
!userId &&
|
|
27
|
+
(key.includes("all") || name.includes("所有人"))) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function walkForAtAll(value) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const trimmed = value.trim().toLowerCase();
|
|
35
|
+
return (trimmed === "@_all" ||
|
|
36
|
+
trimmed === "@all" ||
|
|
37
|
+
trimmed === "all" ||
|
|
38
|
+
/@_all\b/i.test(value));
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.some((entry) => walkForAtAll(entry));
|
|
42
|
+
}
|
|
43
|
+
if (value && typeof value === "object") {
|
|
44
|
+
const record = value;
|
|
45
|
+
if (record.tag === "at") {
|
|
46
|
+
const userId = normalize(typeof record.user_id === "string" ? record.user_id : undefined);
|
|
47
|
+
if (userId === "all" || userId === "@_all")
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return Object.values(record).some((entry) => walkForAtAll(entry));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** Detect @_all / at-all markers inside raw Feishu message content JSON. */
|
|
55
|
+
export function contentIndicatesFeishuAtAll(content) {
|
|
56
|
+
if (!content)
|
|
57
|
+
return false;
|
|
58
|
+
if (/@_all\b/i.test(content))
|
|
59
|
+
return true;
|
|
60
|
+
try {
|
|
61
|
+
return walkForAtAll(JSON.parse(content));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return /@_all\b/i.test(content);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A direct bot (or other entity) @mention — anything in `mentions` that is not @All.
|
|
69
|
+
* With mention-only Feishu scopes, non-@All mentions are the bot itself.
|
|
70
|
+
*/
|
|
71
|
+
export function hasDirectFeishuMention(mentions) {
|
|
72
|
+
if (!mentions?.length)
|
|
73
|
+
return false;
|
|
74
|
+
return mentions.some((mention) => {
|
|
75
|
+
if (isFeishuAtAllMention(mention))
|
|
76
|
+
return false;
|
|
77
|
+
if (mention.mentioned_type === "bot")
|
|
78
|
+
return true;
|
|
79
|
+
// Explicit user @ is not a bot mention.
|
|
80
|
+
if (mention.mentioned_type === "user")
|
|
81
|
+
return false;
|
|
82
|
+
const openId = mention.id?.open_id?.trim() ?? "";
|
|
83
|
+
const userId = mention.id?.user_id?.trim() ?? "";
|
|
84
|
+
const unionId = mention.id?.union_id?.trim() ?? "";
|
|
85
|
+
// mentioned_type omitted: any non-@All identity counts (mention-only bots).
|
|
86
|
+
return Boolean(openId || userId || unionId);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Beacon-layer gate for inbound Feishu events.
|
|
91
|
+
* - DM (p2p): always process
|
|
92
|
+
* - Group: process only when a non-@All mention is present
|
|
93
|
+
* - Pure @All (with or without @_all content): ignore
|
|
94
|
+
* - @All + @bot together: process (direct mention wins)
|
|
95
|
+
*/
|
|
96
|
+
export function shouldProcessFeishuInbound(input) {
|
|
97
|
+
if (input.chatType === "p2p")
|
|
98
|
+
return true;
|
|
99
|
+
if (input.chatType !== "group")
|
|
100
|
+
return false;
|
|
101
|
+
return hasDirectFeishuMention(input.mentions);
|
|
102
|
+
}
|
package/dist/run/orchestrator.js
CHANGED
|
@@ -40,13 +40,54 @@ function summary(error) {
|
|
|
40
40
|
const value = error instanceof Error ? error.message : String(error);
|
|
41
41
|
return value.slice(0, 4096) || "unknown failure";
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
const FAILURE_DETAIL_LIMIT = 400;
|
|
44
|
+
const THINKING_SNIPPET_LIMIT = 120;
|
|
45
|
+
function clipForFeishu(value, max) {
|
|
46
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
47
|
+
if (trimmed.length <= max)
|
|
48
|
+
return trimmed;
|
|
49
|
+
return `${trimmed.slice(0, Math.max(0, max - 1))}…`;
|
|
50
|
+
}
|
|
51
|
+
/** Compact settle-side hint for outcome_missing (Feishu / admin). */
|
|
52
|
+
export function formatSettleFailureHint(parts) {
|
|
53
|
+
const bits = [];
|
|
54
|
+
if (parts.stopReason?.trim()) {
|
|
55
|
+
bits.push(`末轮 stopReason=${parts.stopReason.trim()}`);
|
|
56
|
+
}
|
|
57
|
+
const thought = parts.thinking?.trim() || parts.text?.trim();
|
|
58
|
+
if (thought) {
|
|
59
|
+
bits.push(`thinking:「${clipForFeishu(thought, THINKING_SNIPPET_LIMIT)}」`);
|
|
60
|
+
}
|
|
61
|
+
return bits.length > 0 ? bits.join(";") : undefined;
|
|
62
|
+
}
|
|
63
|
+
function detailBeyondMissingSummary(summaryText) {
|
|
64
|
+
if (summaryText === MISSING_REPLY_OUTCOME_SUMMARY)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (summaryText.startsWith(MISSING_REPLY_OUTCOME_SUMMARY)) {
|
|
67
|
+
const rest = summaryText
|
|
68
|
+
.slice(MISSING_REPLY_OUTCOME_SUMMARY.length)
|
|
69
|
+
.replace(/^[\s.;]+/, "")
|
|
70
|
+
.trim();
|
|
71
|
+
return rest ? clipForFeishu(rest, FAILURE_DETAIL_LIMIT) : undefined;
|
|
72
|
+
}
|
|
73
|
+
return clipForFeishu(summaryText, FAILURE_DETAIL_LIMIT);
|
|
74
|
+
}
|
|
75
|
+
/** User-visible (Feishu/admin) failure text. Keep codes stable; prefer concrete reasons. */
|
|
44
76
|
export function formatFailureReplyText(runId, failure) {
|
|
45
77
|
if (failure?.code === "outcome_missing") {
|
|
46
|
-
|
|
78
|
+
const parts = [
|
|
47
79
|
`处理失败(run_id=${runId},code=outcome_missing):`,
|
|
48
80
|
"期望恰好调用一次 `reply` 或 `no_reply`,但 Agent 已结束且两者均未提交。",
|
|
49
|
-
]
|
|
81
|
+
];
|
|
82
|
+
const detail = failure.summary
|
|
83
|
+
? detailBeyondMissingSummary(failure.summary)
|
|
84
|
+
: undefined;
|
|
85
|
+
if (detail)
|
|
86
|
+
parts.push(`详情:${detail}`);
|
|
87
|
+
return parts.join("");
|
|
88
|
+
}
|
|
89
|
+
if (failure?.summary?.trim()) {
|
|
90
|
+
return `处理失败(run_id=${runId},code=${failure.code}):${clipForFeishu(failure.summary, FAILURE_DETAIL_LIMIT)}`;
|
|
50
91
|
}
|
|
51
92
|
if (failure) {
|
|
52
93
|
return `处理失败(run_id=${runId},code=${failure.code}),请查看 Beacon 本地记录。`;
|
|
@@ -182,10 +223,15 @@ export class RunOrchestrator {
|
|
|
182
223
|
await this.deliverField(triggerKey, "notifyDelivery", current.notifyTarget, outcome.notify, create && current.notifyDelivery === undefined);
|
|
183
224
|
}
|
|
184
225
|
}
|
|
185
|
-
async fail(triggerKey, runId, code, error, submitted) {
|
|
186
|
-
|
|
226
|
+
async fail(triggerKey, runId, code, error, submitted, settle) {
|
|
227
|
+
let detail = isMissingReplyOutcomeError(error)
|
|
187
228
|
? MISSING_REPLY_OUTCOME_SUMMARY
|
|
188
229
|
: summary(error);
|
|
230
|
+
if (code === "outcome_missing") {
|
|
231
|
+
const hint = formatSettleFailureHint(settle ?? {});
|
|
232
|
+
if (hint)
|
|
233
|
+
detail = `${detail}. ${hint}`;
|
|
234
|
+
}
|
|
189
235
|
const failure = { code, summary: detail };
|
|
190
236
|
await this.options.store.update(triggerKey, (current) => ({
|
|
191
237
|
...current,
|
|
@@ -236,6 +282,7 @@ export class RunOrchestrator {
|
|
|
236
282
|
run: { ...current.run, state: "running" },
|
|
237
283
|
}));
|
|
238
284
|
const submission = this.options.outcomes.openRun();
|
|
285
|
+
let settle;
|
|
239
286
|
try {
|
|
240
287
|
const completion = await this.options.runAgent({
|
|
241
288
|
prompt: promptFor(input),
|
|
@@ -250,6 +297,11 @@ export class RunOrchestrator {
|
|
|
250
297
|
name: `Beacon ${this.options.profile.id} ${runId}`,
|
|
251
298
|
},
|
|
252
299
|
});
|
|
300
|
+
settle = {
|
|
301
|
+
stopReason: completion.stopReason,
|
|
302
|
+
thinking: completion.thinking,
|
|
303
|
+
text: completion.text,
|
|
304
|
+
};
|
|
253
305
|
const submitted = submission.takeFeedback();
|
|
254
306
|
const outcome = normalizeAgentOutcome(input, submission.take(), record.notifyTarget);
|
|
255
307
|
await this.options.store.update(triggerKey, (current) => ({
|
|
@@ -278,7 +330,7 @@ export class RunOrchestrator {
|
|
|
278
330
|
: isMissingReplyOutcomeError(error)
|
|
279
331
|
? "outcome_missing"
|
|
280
332
|
: "runtime_exit_failed";
|
|
281
|
-
await this.fail(triggerKey, runId, code, error, submitted);
|
|
333
|
+
await this.fail(triggerKey, runId, code, error, submitted, settle);
|
|
282
334
|
}
|
|
283
335
|
}
|
|
284
336
|
async process(triggerKey, normalize) {
|
package/dist/runtime/pi-rpc.js
CHANGED
|
@@ -45,6 +45,27 @@ function collectText(message) {
|
|
|
45
45
|
.join("")
|
|
46
46
|
.trim();
|
|
47
47
|
}
|
|
48
|
+
/** Collect thinking / reasoning blocks from an assistant message content array. */
|
|
49
|
+
export function collectThinking(content) {
|
|
50
|
+
return content
|
|
51
|
+
.map((block) => {
|
|
52
|
+
if (!isObject(block))
|
|
53
|
+
return "";
|
|
54
|
+
if (block.type === "thinking") {
|
|
55
|
+
if (typeof block.thinking === "string")
|
|
56
|
+
return block.thinking;
|
|
57
|
+
if (typeof block.text === "string")
|
|
58
|
+
return block.text;
|
|
59
|
+
}
|
|
60
|
+
if (block.type === "reasoning" && typeof block.text === "string") {
|
|
61
|
+
return block.text;
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
64
|
+
})
|
|
65
|
+
.filter((part) => part.trim().length > 0)
|
|
66
|
+
.join("\n")
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
48
69
|
function buildArguments(request) {
|
|
49
70
|
const args = ["--mode", "rpc", "--no-approve"];
|
|
50
71
|
if (request.session) {
|
|
@@ -257,10 +278,13 @@ export async function runPiAgent(request, options = {}) {
|
|
|
257
278
|
fail(new Error("Pi completed without a textual final response"));
|
|
258
279
|
return;
|
|
259
280
|
}
|
|
281
|
+
const thinking = collectThinking(finalMessage.content);
|
|
260
282
|
const result = {
|
|
261
283
|
text,
|
|
262
284
|
provider: finalMessage.provider,
|
|
263
285
|
model: finalMessage.model,
|
|
286
|
+
stopReason: finalMessage.stopReason,
|
|
287
|
+
...(thinking ? { thinking } : {}),
|
|
264
288
|
};
|
|
265
289
|
finished = true;
|
|
266
290
|
clearTimeout(timer);
|