@nettee/beacon 0.2.5 → 0.2.7
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/outcome/content.js +12 -1
- package/dist/run/orchestrator.js +29 -12
- package/dist/schedule-trigger.js +2 -1
- 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/outcome/content.js
CHANGED
|
@@ -117,9 +117,20 @@ export function mergeOutcome(current, patch) {
|
|
|
117
117
|
: {}),
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
/** Stable English summary when the Agent settles without closing reply/no_reply. */
|
|
121
|
+
export const MISSING_REPLY_OUTCOME_SUMMARY = "Expected exactly one of `reply` or `no_reply`, but the Agent Runtime settled without submitting either";
|
|
122
|
+
export function isMissingReplyOutcomeError(error) {
|
|
123
|
+
if (!(error instanceof Error))
|
|
124
|
+
return false;
|
|
125
|
+
if (error.message === MISSING_REPLY_OUTCOME_SUMMARY)
|
|
126
|
+
return true;
|
|
127
|
+
// Legacy summaries from earlier Beacon builds
|
|
128
|
+
return (/settled without submitting reply or no_reply/i.test(error.message) ||
|
|
129
|
+
/submitting a (Final Outcome|reply or no_reply)/i.test(error.message));
|
|
130
|
+
}
|
|
120
131
|
export function completeOutcome(partial) {
|
|
121
132
|
if (!partial.reply) {
|
|
122
|
-
throw new Error(
|
|
133
|
+
throw new Error(MISSING_REPLY_OUTCOME_SUMMARY);
|
|
123
134
|
}
|
|
124
135
|
return {
|
|
125
136
|
reply: partial.reply,
|
package/dist/run/orchestrator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { isMissingReplyOutcomeError, MISSING_REPLY_OUTCOME_SUMMARY, } from "../outcome/content.js";
|
|
3
4
|
import { inboundOutOfRoleReply } from "../outcome/instructions.js";
|
|
4
5
|
import { PiRuntimeError } from "../runtime/pi-rpc.js";
|
|
5
6
|
import { agentSystemPromptTrigger, buildAgentSystemPrompt, profilePromptDigest, } from "../runtime/system-prompt.js";
|
|
@@ -39,11 +40,24 @@ function summary(error) {
|
|
|
39
40
|
const value = error instanceof Error ? error.message : String(error);
|
|
40
41
|
return value.slice(0, 4096) || "unknown failure";
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
/** User-visible (Feishu/admin) failure text. Keep codes stable; explain missing outcomes. */
|
|
44
|
+
export function formatFailureReplyText(runId, failure) {
|
|
45
|
+
if (failure?.code === "outcome_missing") {
|
|
46
|
+
return [
|
|
47
|
+
`处理失败(run_id=${runId},code=outcome_missing):`,
|
|
48
|
+
"期望恰好调用一次 `reply` 或 `no_reply`,但 Agent 已结束且两者均未提交。",
|
|
49
|
+
].join("");
|
|
50
|
+
}
|
|
51
|
+
if (failure) {
|
|
52
|
+
return `处理失败(run_id=${runId},code=${failure.code}),请查看 Beacon 本地记录。`;
|
|
53
|
+
}
|
|
54
|
+
return `处理失败(run_id=${runId}),请查看 Beacon 本地记录。`;
|
|
55
|
+
}
|
|
56
|
+
function failureOutcome(runId, failure) {
|
|
43
57
|
return {
|
|
44
58
|
reply: {
|
|
45
59
|
kind: "text",
|
|
46
|
-
text:
|
|
60
|
+
text: formatFailureReplyText(runId, failure),
|
|
47
61
|
},
|
|
48
62
|
};
|
|
49
63
|
}
|
|
@@ -169,7 +183,10 @@ export class RunOrchestrator {
|
|
|
169
183
|
}
|
|
170
184
|
}
|
|
171
185
|
async fail(triggerKey, runId, code, error, submitted) {
|
|
172
|
-
const detail =
|
|
186
|
+
const detail = isMissingReplyOutcomeError(error)
|
|
187
|
+
? MISSING_REPLY_OUTCOME_SUMMARY
|
|
188
|
+
: summary(error);
|
|
189
|
+
const failure = { code, summary: detail };
|
|
173
190
|
await this.options.store.update(triggerKey, (current) => ({
|
|
174
191
|
...current,
|
|
175
192
|
run: {
|
|
@@ -177,11 +194,11 @@ export class RunOrchestrator {
|
|
|
177
194
|
this.newRun(runId, "queued", this.triggerFor(current))),
|
|
178
195
|
state: "failed",
|
|
179
196
|
finishedAt: this.timestamp(),
|
|
180
|
-
failure
|
|
197
|
+
failure,
|
|
181
198
|
},
|
|
182
199
|
finalOutcome: {
|
|
183
200
|
origin: "beacon_failure",
|
|
184
|
-
content: failureOutcome(runId),
|
|
201
|
+
content: failureOutcome(runId, failure),
|
|
185
202
|
submittedAt: this.timestamp(),
|
|
186
203
|
},
|
|
187
204
|
...(submitted ? { feedback: submitted } : {}),
|
|
@@ -258,8 +275,7 @@ export class RunOrchestrator {
|
|
|
258
275
|
submission.cancel();
|
|
259
276
|
const code = error instanceof PiRuntimeError
|
|
260
277
|
? error.code
|
|
261
|
-
: error
|
|
262
|
-
/submitting a (Final Outcome|reply or no_reply)/.test(error.message)
|
|
278
|
+
: isMissingReplyOutcomeError(error)
|
|
263
279
|
? "outcome_missing"
|
|
264
280
|
: "runtime_exit_failed";
|
|
265
281
|
await this.fail(triggerKey, runId, code, error, submitted);
|
|
@@ -272,15 +288,16 @@ export class RunOrchestrator {
|
|
|
272
288
|
input = await normalize();
|
|
273
289
|
}
|
|
274
290
|
catch (error) {
|
|
291
|
+
const failure = {
|
|
292
|
+
code: "trigger_normalization_failed",
|
|
293
|
+
summary: summary(error),
|
|
294
|
+
};
|
|
275
295
|
await this.options.store.update(triggerKey, (current) => ({
|
|
276
296
|
...current,
|
|
277
|
-
run: this.newRun(runId, "failed", this.triggerFor(current),
|
|
278
|
-
code: "trigger_normalization_failed",
|
|
279
|
-
summary: summary(error),
|
|
280
|
-
}),
|
|
297
|
+
run: this.newRun(runId, "failed", this.triggerFor(current), failure),
|
|
281
298
|
finalOutcome: {
|
|
282
299
|
origin: "beacon_failure",
|
|
283
|
-
content: failureOutcome(runId),
|
|
300
|
+
content: failureOutcome(runId, failure),
|
|
284
301
|
submittedAt: this.timestamp(),
|
|
285
302
|
},
|
|
286
303
|
}));
|
package/dist/schedule-trigger.js
CHANGED
|
@@ -49,7 +49,8 @@ export async function triggerScheduleOnce(options) {
|
|
|
49
49
|
if (record.run.state !== "succeeded" || !record.finalOutcome) {
|
|
50
50
|
const failure = record.run.failure?.code ??
|
|
51
51
|
(!record.finalOutcome ? "outcome_missing" : "unknown");
|
|
52
|
-
|
|
52
|
+
const detail = record.run.failure?.summary;
|
|
53
|
+
throw new Error(`Schedule Trigger failed (run_id=${record.run.runId}): ${failure}${detail ? ` — ${detail}` : ""}`);
|
|
53
54
|
}
|
|
54
55
|
return record;
|
|
55
56
|
}
|