@rivus/gateway 0.18.0 → 0.18.1
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/bootstrap/herdr-feishu.js +24 -794
- package/dist/bootstrap/pi-feishu.js +991 -109
- package/dist/chunks/herdr-feishu-runtime.js +1993 -0
- package/dist/chunks/runtime-cache.js +1 -1
- package/package.json +4 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { S as deepFreeze$1, _ as loadMergedLocalEnvFile, a as createRivusModelManagementSocketServer, d as gatewayPackageDirectory, h as loadRivusDeploymentManifest, i as RIVUS_RELEASE_DESCRIPTOR, n as disposeRuntimeCacheEntries, o as createRivusModelManagementFailure, p as gatewaySkillsDirectory, r as invokeRuntimeControl, t as createRuntimeCache, v as resolveFeishuEndpointCredentials } from "../chunks/runtime-cache.js";
|
|
2
2
|
import { t as createBackgroundSessionToolContracts } from "../chunks/background-session-authority.js";
|
|
3
|
+
import { a as openTerminalBindingsStore, c as classifyFeishuCardOwnership, d as parseFeishuTerminalMessage, i as openTerminalMessageClaims, l as createFeishuTerminalDelivery, n as embeddedHerdrRouteSchema, o as terminalAgentSchema, s as createHerdrSocketClient, t as createHerdrFeishuBridgeRuntime, u as parseFeishuTerminalAction } from "../chunks/herdr-feishu-runtime.js";
|
|
3
4
|
import { createRandomId, createSha256Digest, createStableId } from "@rivus/platform/identity";
|
|
4
5
|
import { ATTACHMENT_RECOVERY_COMMAND, AgentEventHandlerFailed, AgentEventSinkFailed, AgentLoopFailed, AgentRunCancelled, ModelChangeBoundaryDeadlineError, RivusToolInputRejected, ToolExecutorInputRejected, ToolInvocationDenied, createAgentHarness, createAgentsMdInstructionsSource, createInvocationAuthority, createJsonlAgentEventLog, createLangfuseAgentTelemetry, createMemoryService, createProjectMemoryPromptPreparer, createRecoveryAction, createRivusMemoryToolDescriptor, createRunAdmission, createTelemetryContentRedactor, createToolBroker, createToolInputDigest, createWorkspaceInstructionsResolver, createWorkspaceRootHandle, normalizeAgentInvocation, openJsonlAgentMemoryRepository, openJsonlToolOperationLedger, remainingUntil, resolveInvocationAuthority, resolveLangfuseTelemetryConfig, restoreAgentHistory } from "@rivus/runtime";
|
|
5
6
|
import { homedir, tmpdir } from "node:os";
|
|
6
7
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { Cause, Deferred, Effect, Exit, Fiber, Result, Semaphore } from "effect";
|
|
8
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Result, Schema, Semaphore } from "effect";
|
|
8
9
|
import { access, appendFile, chmod, constants, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
9
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
11
|
import { appendJsonlRecord, isRecord, loadJsonlSnapshotStore, readPersistenceFile, writeAtomicTextFile, writePersistenceFile } from "@rivus/platform/persistence";
|
|
@@ -12909,6 +12910,982 @@ function settleCleanup(steps) {
|
|
|
12909
12910
|
});
|
|
12910
12911
|
}
|
|
12911
12912
|
//#endregion
|
|
12913
|
+
//#region src/adapters/feishu/inbound-delivery/jsonl-inbound-delivery-store.ts
|
|
12914
|
+
function openJsonlInboundDeliveryStore(options) {
|
|
12915
|
+
const codecOptions = {
|
|
12916
|
+
...options.acceptLegacySameAttemptClaims === void 0 ? {} : { acceptLegacySameAttemptClaims: options.acceptLegacySameAttemptClaims },
|
|
12917
|
+
label: options.label ?? "Inbound Delivery",
|
|
12918
|
+
payload: options.payload
|
|
12919
|
+
};
|
|
12920
|
+
return Effect.gen(function* () {
|
|
12921
|
+
const raw = yield* Effect.tryPromise({
|
|
12922
|
+
try: () => readPersistenceFile(options.filePath),
|
|
12923
|
+
catch: (cause) => cause
|
|
12924
|
+
});
|
|
12925
|
+
return createInMemoryDurableQueueStore({
|
|
12926
|
+
initial: yield* Effect.try({
|
|
12927
|
+
try: () => decodeInboundDeliverySnapshots(raw, codecOptions),
|
|
12928
|
+
catch: (cause) => cause
|
|
12929
|
+
}),
|
|
12930
|
+
persist: (delivery) => appendInboundDeliverySnapshot(options.filePath, delivery)
|
|
12931
|
+
});
|
|
12932
|
+
});
|
|
12933
|
+
}
|
|
12934
|
+
function decodeInboundDeliverySnapshots(raw, options) {
|
|
12935
|
+
if (raw === void 0) return [];
|
|
12936
|
+
const latest = /* @__PURE__ */ new Map();
|
|
12937
|
+
for (const [index, line] of raw.split("\n").entries()) {
|
|
12938
|
+
if (line.trim().length === 0) continue;
|
|
12939
|
+
let parsed;
|
|
12940
|
+
try {
|
|
12941
|
+
parsed = JSON.parse(line);
|
|
12942
|
+
} catch {
|
|
12943
|
+
throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
12944
|
+
}
|
|
12945
|
+
if (!isEnvelope(parsed)) throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
12946
|
+
let delivery;
|
|
12947
|
+
try {
|
|
12948
|
+
delivery = decodeDelivery(parsed.delivery, options.payload);
|
|
12949
|
+
} catch {
|
|
12950
|
+
throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
12951
|
+
}
|
|
12952
|
+
const previous = latest.get(delivery.id);
|
|
12953
|
+
if (delivery.revision !== (previous?.revision ?? 0) + 1) throw new Error(`invalid ${options.label} revision at line ${index + 1}`);
|
|
12954
|
+
try {
|
|
12955
|
+
if (previous) {
|
|
12956
|
+
if (!isValidPersistedTransition(previous, delivery, options.acceptLegacySameAttemptClaims ?? false)) throw new Error("transition");
|
|
12957
|
+
} else assertNewInboundDelivery(delivery);
|
|
12958
|
+
} catch {
|
|
12959
|
+
throw new Error(`invalid ${options.label} transition at line ${index + 1}`);
|
|
12960
|
+
}
|
|
12961
|
+
latest.set(delivery.id, delivery);
|
|
12962
|
+
}
|
|
12963
|
+
return [...latest.values()];
|
|
12964
|
+
}
|
|
12965
|
+
function encodeInboundDeliverySnapshot(delivery) {
|
|
12966
|
+
const snapshot = readInboundDeliverySnapshot(delivery);
|
|
12967
|
+
const persisted = {
|
|
12968
|
+
acceptedAt: snapshot.acceptedAt,
|
|
12969
|
+
attempts: snapshot.attempts,
|
|
12970
|
+
id: snapshot.id,
|
|
12971
|
+
laneKey: snapshot.laneKey,
|
|
12972
|
+
...snapshot.metadata === void 0 ? {} : { options: snapshot.metadata },
|
|
12973
|
+
payload: snapshot.payload,
|
|
12974
|
+
...snapshot.recovery === void 0 ? {} : { recovery: snapshot.recovery },
|
|
12975
|
+
revision: snapshot.revision,
|
|
12976
|
+
state: snapshot.state
|
|
12977
|
+
};
|
|
12978
|
+
return JSON.stringify({
|
|
12979
|
+
delivery: persisted,
|
|
12980
|
+
version: 1
|
|
12981
|
+
});
|
|
12982
|
+
}
|
|
12983
|
+
function decodeDelivery(value, payloadCodec) {
|
|
12984
|
+
if (!isRecord(value) || typeof value.acceptedAt !== "string" || typeof value.attempts !== "number" || typeof value.id !== "string" || typeof value.laneKey !== "string" || typeof value.revision !== "number") throw new Error("invalid Inbound Delivery snapshot");
|
|
12985
|
+
const metadata = payloadCodec.decodeMetadata(value.options);
|
|
12986
|
+
return readInboundDeliverySnapshot({
|
|
12987
|
+
acceptedAt: value.acceptedAt,
|
|
12988
|
+
attempts: value.attempts,
|
|
12989
|
+
id: value.id,
|
|
12990
|
+
laneKey: value.laneKey,
|
|
12991
|
+
...metadata === void 0 ? {} : { metadata },
|
|
12992
|
+
payload: payloadCodec.decodePayload(value.payload),
|
|
12993
|
+
...value.recovery === void 0 ? {} : { recovery: value.recovery },
|
|
12994
|
+
revision: value.revision,
|
|
12995
|
+
state: value.state
|
|
12996
|
+
});
|
|
12997
|
+
}
|
|
12998
|
+
function isEnvelope(value) {
|
|
12999
|
+
return isRecord(value) && value.version === 1 && "delivery" in value;
|
|
13000
|
+
}
|
|
13001
|
+
function isValidPersistedTransition(previous, next, acceptLegacySameAttemptClaims) {
|
|
13002
|
+
if (isValidInboundDeliveryTransition(previous, next)) return true;
|
|
13003
|
+
if (!acceptLegacySameAttemptClaims || previous.state.status !== "pending" || next.state.status !== "leased" || next.attempts !== previous.attempts) return false;
|
|
13004
|
+
return isValidInboundDeliveryTransition(previous, {
|
|
13005
|
+
...next,
|
|
13006
|
+
attempts: next.attempts + 1
|
|
13007
|
+
});
|
|
13008
|
+
}
|
|
13009
|
+
function appendInboundDeliverySnapshot(filePath, delivery) {
|
|
13010
|
+
return Effect.tryPromise({
|
|
13011
|
+
try: async () => {
|
|
13012
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
13013
|
+
await appendFile(filePath, `${encodeInboundDeliverySnapshot(delivery)}\n`, "utf8");
|
|
13014
|
+
},
|
|
13015
|
+
catch: (cause) => cause
|
|
13016
|
+
});
|
|
13017
|
+
}
|
|
13018
|
+
//#endregion
|
|
13019
|
+
//#region src/adapters/feishu/routing/feishu-conversation-router.ts
|
|
13020
|
+
const CAPACITY_REFUSAL = "队列已满,请用新消息重发;本条未提交。";
|
|
13021
|
+
const UNSUPPORTED_TYPE_REFUSAL = "会话模式暂不支持该消息类型。";
|
|
13022
|
+
/**
|
|
13023
|
+
* Single-entry Feishu conversation router: every message and callback is fenced
|
|
13024
|
+
* by the durable owner ledger before any destination runs, and the mode capture
|
|
13025
|
+
* plus owner admission share one bridge admission turn.
|
|
13026
|
+
*/
|
|
13027
|
+
function createFeishuConversationRouter(options) {
|
|
13028
|
+
const mode = options.bridge.conversation?.mode;
|
|
13029
|
+
const allowed = (identity) => options.actors.includes(identity.actorId) && options.chats.includes(identity.chatId);
|
|
13030
|
+
const freeze = (reason, messageId) => Effect.sync(() => {
|
|
13031
|
+
mode?.freeze();
|
|
13032
|
+
options.onObservation?.({
|
|
13033
|
+
...messageId === void 0 ? {} : { messageId },
|
|
13034
|
+
type: "route_frozen"
|
|
13035
|
+
});
|
|
13036
|
+
console.error(`Herdr route frozen: ${reason}`);
|
|
13037
|
+
});
|
|
13038
|
+
const freezeVerdict = (reason, messageId) => Effect.gen(function* () {
|
|
13039
|
+
yield* freeze(reason, messageId);
|
|
13040
|
+
return { kind: "frozen" };
|
|
13041
|
+
});
|
|
13042
|
+
/**
|
|
13043
|
+
* Refusals for owner uncertainty use a stable internal claim key: claiming the
|
|
13044
|
+
* original id would fabricate a Herdr fence that conflicts with a durable Pi
|
|
13045
|
+
* owner after restart, so the original owner identity stays untouched.
|
|
13046
|
+
*/
|
|
13047
|
+
const refusalClaimId = (identity) => `route-refusal:${JSON.stringify([options.appId, identity.messageId])}`;
|
|
13048
|
+
const refusalMessage = (identity) => ({
|
|
13049
|
+
id: refusalClaimId(identity),
|
|
13050
|
+
actorId: identity.actorId,
|
|
13051
|
+
chatId: identity.chatId,
|
|
13052
|
+
sentAt: identity.sentAt,
|
|
13053
|
+
text: ""
|
|
13054
|
+
});
|
|
13055
|
+
/** One admission turn: legacy fences → owner lookup → capture → durable owner → admit captured. */
|
|
13056
|
+
const admitSequence = (input) => Effect.gen(function* () {
|
|
13057
|
+
const { identity } = input;
|
|
13058
|
+
const legacy = yield* options.legacyOwnerOf(identity.messageId);
|
|
13059
|
+
if (legacy === "conflict") return yield* freezeVerdict("legacy Pi inbox and Herdr claim conflict", identity.messageId);
|
|
13060
|
+
const lookup = yield* Effect.result(options.ledger.lookup(identity.messageId));
|
|
13061
|
+
if (Result.isFailure(lookup)) return yield* freezeVerdict("owner ledger lookup failed", identity.messageId);
|
|
13062
|
+
if (lookup.success.kind === "opaque-fence") return yield* freezeVerdict("owner record is an opaque fence", identity.messageId);
|
|
13063
|
+
if (lookup.success.kind === "record") {
|
|
13064
|
+
const record = lookup.success.record;
|
|
13065
|
+
if (record.actorId !== identity.actorId || record.chatId !== identity.chatId) return yield* freezeVerdict("owner record does not match the incoming message source", identity.messageId);
|
|
13066
|
+
if (legacy === "pi" && record.decision === "herdr" || legacy === "herdr" && record.decision === "pi") return yield* freezeVerdict("historical fence and owner record disagree", identity.messageId);
|
|
13067
|
+
options.onObservation?.({
|
|
13068
|
+
messageId: identity.messageId,
|
|
13069
|
+
type: "route_skipped"
|
|
13070
|
+
});
|
|
13071
|
+
return record.decision === "pi" ? { kind: "pi" } : { kind: "skip" };
|
|
13072
|
+
}
|
|
13073
|
+
if (legacy === "herdr") {
|
|
13074
|
+
options.onObservation?.({
|
|
13075
|
+
messageId: identity.messageId,
|
|
13076
|
+
type: "route_skipped"
|
|
13077
|
+
});
|
|
13078
|
+
return { kind: "skip" };
|
|
13079
|
+
}
|
|
13080
|
+
let captured;
|
|
13081
|
+
let unsupportedType = false;
|
|
13082
|
+
if (input.parsed) captured = options.bridge.captureInAdmission(input.parsed);
|
|
13083
|
+
else if (legacy === "none" && mode?.isFrozen() && allowed(identity)) return { kind: "frozen" };
|
|
13084
|
+
else if (legacy === "none" && mode?.stateOf(identity.actorId, identity.chatId)) unsupportedType = true;
|
|
13085
|
+
const decision = legacy === "pi" || !unsupportedType && (captured === void 0 || captured.route.kind === "pass-through") ? "pi" : "herdr";
|
|
13086
|
+
const record = {
|
|
13087
|
+
appId: options.appId,
|
|
13088
|
+
messageId: identity.messageId,
|
|
13089
|
+
decision,
|
|
13090
|
+
actorId: identity.actorId,
|
|
13091
|
+
chatId: identity.chatId,
|
|
13092
|
+
at: Date.now(),
|
|
13093
|
+
...decision === "herdr" && captured?.route.kind === "locked-send" ? { target: captured.route.agent } : {}
|
|
13094
|
+
};
|
|
13095
|
+
const admitted = yield* Effect.result(options.ledger.admit(record));
|
|
13096
|
+
if (Result.isFailure(admitted)) return yield* freezeVerdict("owner admission failed", identity.messageId);
|
|
13097
|
+
if (admitted.success.kind === "existing-opaque") return yield* freezeVerdict("owner admission found an opaque fence", identity.messageId);
|
|
13098
|
+
if (admitted.success.kind === "existing") {
|
|
13099
|
+
const existing = admitted.success.record;
|
|
13100
|
+
if (legacy === "pi" && existing.decision === "herdr") return yield* freezeVerdict("historical fence and owner record disagree", identity.messageId);
|
|
13101
|
+
options.onObservation?.({
|
|
13102
|
+
messageId: identity.messageId,
|
|
13103
|
+
type: "route_skipped"
|
|
13104
|
+
});
|
|
13105
|
+
return existing.decision === "pi" ? { kind: "pi" } : { kind: "skip" };
|
|
13106
|
+
}
|
|
13107
|
+
if (decision === "pi") return { kind: "pi" };
|
|
13108
|
+
if (unsupportedType || captured === void 0) return {
|
|
13109
|
+
kind: "unsupported-type",
|
|
13110
|
+
chatId: identity.chatId
|
|
13111
|
+
};
|
|
13112
|
+
const outcome = yield* Effect.result(options.bridge.admitCapturedInAdmission(captured));
|
|
13113
|
+
if (Result.isFailure(outcome)) return yield* freezeVerdict("herdr event admission failed", identity.messageId);
|
|
13114
|
+
return {
|
|
13115
|
+
kind: "herdr",
|
|
13116
|
+
entry: {
|
|
13117
|
+
message: captured.message,
|
|
13118
|
+
outcome: outcome.success
|
|
13119
|
+
}
|
|
13120
|
+
};
|
|
13121
|
+
});
|
|
13122
|
+
/**
|
|
13123
|
+
* Runs the sequence inside one admission turn and publishes the shared freeze
|
|
13124
|
+
* for every full cause BEFORE the permit is released, so already queued work
|
|
13125
|
+
* and later captures observe the exact flag.
|
|
13126
|
+
*/
|
|
13127
|
+
const admitInAdmission = (input) => options.bridge.withAdmission(Effect.gen(function* () {
|
|
13128
|
+
const exit = yield* Effect.exit(admitSequence(input));
|
|
13129
|
+
if (Exit.isFailure(exit)) return yield* freezeVerdict("admission sequence failed", input.identity.messageId);
|
|
13130
|
+
return exit.value;
|
|
13131
|
+
}));
|
|
13132
|
+
/**
|
|
13133
|
+
* Owner uncertainty freezes; the visible refusal is admitted in a separate,
|
|
13134
|
+
* non-nested turn via the bridge's own frozen-refusal capture, so freshness,
|
|
13135
|
+
* authorization and the event claim stay with existing semantics. Delivery
|
|
13136
|
+
* happens only outside admission through the tracked reply lane. A failing
|
|
13137
|
+
* refusal claim keeps the freeze and reports a diagnostic: there is no
|
|
13138
|
+
* direct undeduped chat fallback and no destination retry.
|
|
13139
|
+
*/
|
|
13140
|
+
const deliverOwnerUncertainRefusal = async (identity) => {
|
|
13141
|
+
let outcome;
|
|
13142
|
+
try {
|
|
13143
|
+
outcome = await Effect.runPromise(options.bridge.withAdmission(Effect.gen(function* () {
|
|
13144
|
+
const captured = options.bridge.captureInAdmission(refusalMessage(identity));
|
|
13145
|
+
return yield* options.bridge.admitCapturedInAdmission(captured);
|
|
13146
|
+
})));
|
|
13147
|
+
} catch {
|
|
13148
|
+
outcome = void 0;
|
|
13149
|
+
}
|
|
13150
|
+
if (outcome !== void 0 && outcome.kind === "done" && outcome.result.kind === "reply") {
|
|
13151
|
+
await options.target.reply(identity.chatId, outcome.result.reply.text);
|
|
13152
|
+
return true;
|
|
13153
|
+
}
|
|
13154
|
+
options.onObservation?.({
|
|
13155
|
+
messageId: identity.messageId,
|
|
13156
|
+
type: "refusal_undelivered"
|
|
13157
|
+
});
|
|
13158
|
+
console.error(`Herdr owner-uncertain refusal not delivered for ${identity.messageId}`);
|
|
13159
|
+
return false;
|
|
13160
|
+
};
|
|
13161
|
+
const route = async (identity, parsed) => {
|
|
13162
|
+
let verdict;
|
|
13163
|
+
try {
|
|
13164
|
+
verdict = await Effect.runPromise(admitInAdmission({
|
|
13165
|
+
identity,
|
|
13166
|
+
parsed
|
|
13167
|
+
}));
|
|
13168
|
+
} catch {
|
|
13169
|
+
verdict = { kind: "frozen" };
|
|
13170
|
+
}
|
|
13171
|
+
switch (verdict.kind) {
|
|
13172
|
+
case "pi": return { kind: "pi" };
|
|
13173
|
+
case "skip": return { kind: "skip" };
|
|
13174
|
+
case "frozen":
|
|
13175
|
+
await deliverOwnerUncertainRefusal(identity);
|
|
13176
|
+
return { kind: "frozen" };
|
|
13177
|
+
case "unsupported-type":
|
|
13178
|
+
await options.target.reply(verdict.chatId, UNSUPPORTED_TYPE_REFUSAL);
|
|
13179
|
+
return { kind: "consumed" };
|
|
13180
|
+
case "herdr":
|
|
13181
|
+
if (!await options.target.execute(verdict.entry)) await options.target.reply(verdict.entry.message.chatId, CAPACITY_REFUSAL);
|
|
13182
|
+
return { kind: "consumed" };
|
|
13183
|
+
}
|
|
13184
|
+
};
|
|
13185
|
+
const routeAwaited = async (identity, parsed) => {
|
|
13186
|
+
if (options.target.runAdmission === void 0) return route(identity, parsed);
|
|
13187
|
+
const admission = await options.target.runAdmission(() => route(identity, parsed));
|
|
13188
|
+
if (admission.kind === "stopped") throw new Error("Herdr intake is stopped; event not admitted");
|
|
13189
|
+
return admission.value;
|
|
13190
|
+
};
|
|
13191
|
+
const acceptedResult = (messageId) => ({
|
|
13192
|
+
accepted: true,
|
|
13193
|
+
messageId,
|
|
13194
|
+
pending: 0
|
|
13195
|
+
});
|
|
13196
|
+
const skippedResult = (messageId) => ({
|
|
13197
|
+
accepted: false,
|
|
13198
|
+
messageId,
|
|
13199
|
+
pending: 0,
|
|
13200
|
+
reason: "duplicate"
|
|
13201
|
+
});
|
|
13202
|
+
const refusedResult = (messageId) => ({
|
|
13203
|
+
accepted: false,
|
|
13204
|
+
messageId,
|
|
13205
|
+
pending: 0,
|
|
13206
|
+
reason: "ignored"
|
|
13207
|
+
});
|
|
13208
|
+
const deadCardResponse = () => ({ toast: {
|
|
13209
|
+
content: "This card cannot be handled; run /herdr again for a fresh one.",
|
|
13210
|
+
i18n: {
|
|
13211
|
+
en_us: "This card cannot be handled; run /herdr again for a fresh one.",
|
|
13212
|
+
zh_cn: "此卡片已失效或无法识别;请重新执行 /herdr 获取新卡片。"
|
|
13213
|
+
},
|
|
13214
|
+
type: "error"
|
|
13215
|
+
} });
|
|
13216
|
+
const notReceivedCardResponse = () => ({ toast: {
|
|
13217
|
+
content: "The intake is full; this click was not received. Try again in a moment.",
|
|
13218
|
+
i18n: {
|
|
13219
|
+
en_us: "The intake is full; this click was not received. Try again in a moment.",
|
|
13220
|
+
zh_cn: "处理通道已满,本次点击未接收;请稍后重试。"
|
|
13221
|
+
},
|
|
13222
|
+
type: "error"
|
|
13223
|
+
} });
|
|
13224
|
+
const frozenCardResponse = () => ({ toast: {
|
|
13225
|
+
content: "Routing is paused; this click was not executed.",
|
|
13226
|
+
i18n: {
|
|
13227
|
+
en_us: "Routing is paused; this click was not executed.",
|
|
13228
|
+
zh_cn: "会话路由已暂停;本次点击未执行。"
|
|
13229
|
+
},
|
|
13230
|
+
type: "warning"
|
|
13231
|
+
} });
|
|
13232
|
+
const receivedCardResponse = () => ({ toast: {
|
|
13233
|
+
content: "Received; the result arrives as a chat reply.",
|
|
13234
|
+
i18n: {
|
|
13235
|
+
en_us: "Received; the result arrives as a chat reply.",
|
|
13236
|
+
zh_cn: "已接收,结果将以会话回复送达。"
|
|
13237
|
+
},
|
|
13238
|
+
type: "info"
|
|
13239
|
+
} });
|
|
13240
|
+
const duplicateCardResponse = () => ({ toast: {
|
|
13241
|
+
content: "Already handled",
|
|
13242
|
+
i18n: {
|
|
13243
|
+
en_us: "Already handled",
|
|
13244
|
+
zh_cn: "该操作已处理过"
|
|
13245
|
+
},
|
|
13246
|
+
type: "info"
|
|
13247
|
+
} });
|
|
13248
|
+
const piCardErrorResponse = () => ({ toast: {
|
|
13249
|
+
content: "Card action failed",
|
|
13250
|
+
i18n: {
|
|
13251
|
+
en_us: "Card action failed",
|
|
13252
|
+
zh_cn: "卡片操作失败"
|
|
13253
|
+
},
|
|
13254
|
+
type: "error"
|
|
13255
|
+
} });
|
|
13256
|
+
return {
|
|
13257
|
+
/**
|
|
13258
|
+
* Message entry for the merged dispatcher table. Decides ownership before
|
|
13259
|
+
* any destination; the original Pi handler runs only for Pi-owned events.
|
|
13260
|
+
*/
|
|
13261
|
+
routeMessage: (piReceive) => async (payload) => {
|
|
13262
|
+
const normalized = normalizeReceiveMessagePayload(payload);
|
|
13263
|
+
if (!shouldAcceptFeishuEndpointMessage(normalized, options.groupPolicy, options.botOpenId)) return piReceive(payload);
|
|
13264
|
+
const identity = readMessageIdentity(normalized.event);
|
|
13265
|
+
if (!identity) return piReceive(payload);
|
|
13266
|
+
const parsed = parseFeishuTerminalMessage(normalized.event);
|
|
13267
|
+
switch ((await routeAwaited(identity, parsed)).kind) {
|
|
13268
|
+
case "pi": return piReceive(payload);
|
|
13269
|
+
case "skip": return skippedResult(identity.messageId);
|
|
13270
|
+
case "frozen": return refusedResult(identity.messageId);
|
|
13271
|
+
case "consumed": return acceptedResult(identity.messageId);
|
|
13272
|
+
}
|
|
13273
|
+
},
|
|
13274
|
+
/**
|
|
13275
|
+
* Card callback entry: ownership comes only from explicit markers. Pi cards
|
|
13276
|
+
* always reach the original handler, in every mode and while frozen.
|
|
13277
|
+
*/
|
|
13278
|
+
routeCardAction: (piCardAction) => async (event) => {
|
|
13279
|
+
const ownership = classifyFeishuCardOwnership(event);
|
|
13280
|
+
if (ownership === "pi") {
|
|
13281
|
+
if (!piCardAction) return piCardErrorResponse();
|
|
13282
|
+
return piCardAction(event);
|
|
13283
|
+
}
|
|
13284
|
+
if (ownership !== "herdr") return deadCardResponse();
|
|
13285
|
+
const message = parseFeishuTerminalAction(event, {});
|
|
13286
|
+
if (!message) return deadCardResponse();
|
|
13287
|
+
const identity = {
|
|
13288
|
+
actorId: message.actorId,
|
|
13289
|
+
chatId: message.chatId,
|
|
13290
|
+
messageId: message.id,
|
|
13291
|
+
messageType: "text",
|
|
13292
|
+
sentAt: message.sentAt
|
|
13293
|
+
};
|
|
13294
|
+
if (options.target.scheduleAdmission !== void 0) {
|
|
13295
|
+
const scheduled = await options.target.scheduleAdmission(() => route(identity, message).then(() => void 0));
|
|
13296
|
+
if (scheduled === "accepted") return receivedCardResponse();
|
|
13297
|
+
if (scheduled === "full") return notReceivedCardResponse();
|
|
13298
|
+
return frozenCardResponse();
|
|
13299
|
+
}
|
|
13300
|
+
switch ((await route(identity, message)).kind) {
|
|
13301
|
+
case "pi": return piCardAction ? piCardAction(event) : piCardErrorResponse();
|
|
13302
|
+
case "skip": return duplicateCardResponse();
|
|
13303
|
+
case "frozen": return frozenCardResponse();
|
|
13304
|
+
case "consumed": return receivedCardResponse();
|
|
13305
|
+
}
|
|
13306
|
+
},
|
|
13307
|
+
/** Bot menu events are Herdr-owned by their configured event_key. */
|
|
13308
|
+
routeMenuEvent: async (event, menuChats) => {
|
|
13309
|
+
const message = parseFeishuTerminalAction(event, menuChats);
|
|
13310
|
+
if (!message) return;
|
|
13311
|
+
await routeAwaited({
|
|
13312
|
+
actorId: message.actorId,
|
|
13313
|
+
chatId: message.chatId,
|
|
13314
|
+
messageId: message.id,
|
|
13315
|
+
messageType: "text",
|
|
13316
|
+
sentAt: message.sentAt
|
|
13317
|
+
}, message);
|
|
13318
|
+
}
|
|
13319
|
+
};
|
|
13320
|
+
}
|
|
13321
|
+
/**
|
|
13322
|
+
* Wraps the endpoint's real event dispatcher: Pi handlers stay under their own
|
|
13323
|
+
* keys, the three routed keys are replaced once, every other key is preserved,
|
|
13324
|
+
* and the underlying `register` return value reaches the WebSocket start
|
|
13325
|
+
* unchanged.
|
|
13326
|
+
*/
|
|
13327
|
+
function createRoutedFeishuEventDispatcher(inner, route, menuChats) {
|
|
13328
|
+
return { register: (handlers) => {
|
|
13329
|
+
const merged = {
|
|
13330
|
+
...handlers,
|
|
13331
|
+
"im.message.receive_v1": (payload) => route.message(handlers["im.message.receive_v1"])(payload),
|
|
13332
|
+
"card.action.trigger": (payload) => route.card(handlers["card.action.trigger"])(payload),
|
|
13333
|
+
"application.bot.menu_v6": (payload) => route.menu(payload, menuChats)
|
|
13334
|
+
};
|
|
13335
|
+
return inner.register(merged);
|
|
13336
|
+
} };
|
|
13337
|
+
}
|
|
13338
|
+
/** Normalized event shape: {message, sender} at the top level. */
|
|
13339
|
+
const messageIdentitySchema = Schema.Struct({
|
|
13340
|
+
message: Schema.optional(Schema.Struct({
|
|
13341
|
+
chat_id: Schema.optional(Schema.String),
|
|
13342
|
+
message_id: Schema.optional(Schema.String),
|
|
13343
|
+
message_type: Schema.optional(Schema.String),
|
|
13344
|
+
create_time: Schema.optional(Schema.String)
|
|
13345
|
+
})),
|
|
13346
|
+
sender: Schema.optional(Schema.Struct({ sender_id: Schema.optional(Schema.Struct({ open_id: Schema.optional(Schema.String) })) }))
|
|
13347
|
+
});
|
|
13348
|
+
const decodeMessageIdentity = Schema.decodeUnknownSync(messageIdentitySchema);
|
|
13349
|
+
/** Builds the gateway owner record identity from a normalized event payload. */
|
|
13350
|
+
function readMessageIdentity(event) {
|
|
13351
|
+
let decoded;
|
|
13352
|
+
try {
|
|
13353
|
+
decoded = decodeMessageIdentity(event);
|
|
13354
|
+
} catch {
|
|
13355
|
+
return;
|
|
13356
|
+
}
|
|
13357
|
+
const message = decoded.message;
|
|
13358
|
+
const messageId = message?.message_id;
|
|
13359
|
+
const chatId = message?.chat_id;
|
|
13360
|
+
const actorId = decoded.sender?.sender_id?.open_id;
|
|
13361
|
+
if (!messageId || !chatId || !actorId) return;
|
|
13362
|
+
return {
|
|
13363
|
+
actorId,
|
|
13364
|
+
chatId,
|
|
13365
|
+
messageId,
|
|
13366
|
+
messageType: message?.message_type ?? "",
|
|
13367
|
+
sentAt: Number(message?.create_time)
|
|
13368
|
+
};
|
|
13369
|
+
}
|
|
13370
|
+
//#endregion
|
|
13371
|
+
//#region src/adapters/outbound/persistence/message-gateway/file-route-admission-ledger.ts
|
|
13372
|
+
const admissionRecordSchema = Schema.Struct({
|
|
13373
|
+
version: Schema.Literal(1),
|
|
13374
|
+
appId: Schema.String,
|
|
13375
|
+
messageId: Schema.String,
|
|
13376
|
+
decision: Schema.Literals(["pi", "herdr"]),
|
|
13377
|
+
actorId: Schema.String,
|
|
13378
|
+
chatId: Schema.String,
|
|
13379
|
+
at: Schema.Number,
|
|
13380
|
+
target: Schema.optional(terminalAgentSchema)
|
|
13381
|
+
});
|
|
13382
|
+
const decodeAdmissionRecord = Schema.decodeUnknownSync(admissionRecordSchema);
|
|
13383
|
+
const isErrnoCode$1 = Schema.Struct({ code: Schema.String });
|
|
13384
|
+
/** Admissions and state directories reject any group/other permission or symlink. */
|
|
13385
|
+
function assertPrivateDirectory$1(info, label) {
|
|
13386
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error(`${label} is not a regular directory`);
|
|
13387
|
+
if (info.uid !== process.getuid?.() || (info.mode & 63) !== 0) throw new Error(`Unsafe ${label} permissions or owner`);
|
|
13388
|
+
}
|
|
13389
|
+
function assertPrivateOwnerFile(info) {
|
|
13390
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("Route admission owner is not a regular file");
|
|
13391
|
+
if (info.uid !== process.getuid?.() || (info.mode & 127) !== 0) throw new Error("Unsafe route admission owner permissions or owner");
|
|
13392
|
+
}
|
|
13393
|
+
/**
|
|
13394
|
+
* Create the final-level admissions child inside an already-provisioned state
|
|
13395
|
+
* root (the shared claims opener owns the root) and sync the root entry durably.
|
|
13396
|
+
* A missing root is a startup failure with guidance — never an ancestor walk.
|
|
13397
|
+
*/
|
|
13398
|
+
async function createPrivateChildDirectory(path, mode, label, stateRoot) {
|
|
13399
|
+
try {
|
|
13400
|
+
await mkdir(path, {
|
|
13401
|
+
recursive: false,
|
|
13402
|
+
mode
|
|
13403
|
+
});
|
|
13404
|
+
} catch (error) {
|
|
13405
|
+
if (Schema.is(isErrnoCode$1)(error) && error.code === "ENOENT") throw new Error(`Cannot create ${label}: the state directory ${stateRoot} does not exist; open the shared claims store first`);
|
|
13406
|
+
if (!(Schema.is(isErrnoCode$1)(error) && error.code === "EEXIST")) throw error;
|
|
13407
|
+
}
|
|
13408
|
+
assertPrivateDirectory$1(await lstat(path), label);
|
|
13409
|
+
}
|
|
13410
|
+
async function syncDirectoryEntry(path) {
|
|
13411
|
+
const handle = await open(path, "r");
|
|
13412
|
+
try {
|
|
13413
|
+
await handle.sync();
|
|
13414
|
+
} finally {
|
|
13415
|
+
await handle.close();
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
async function syncStateDirectory(path, label) {
|
|
13419
|
+
try {
|
|
13420
|
+
await syncDirectoryEntry(path);
|
|
13421
|
+
} catch (error) {
|
|
13422
|
+
throw new Error(`Cannot sync ${label}`, { cause: error });
|
|
13423
|
+
}
|
|
13424
|
+
}
|
|
13425
|
+
/**
|
|
13426
|
+
* File-backed cross-route admission ledger (one irrevocable owner file per
|
|
13427
|
+
* appId/messageId).
|
|
13428
|
+
*
|
|
13429
|
+
* Lock ownership: the caller must already hold the shared state-directory lifetime
|
|
13430
|
+
* lock (`openTerminalMessageClaims`) for this directory and keep it for the
|
|
13431
|
+
* store's lifetime; this factory never acquires or releases a lock itself and
|
|
13432
|
+
* never opens a connection. Crash recovery is manual: lock files and
|
|
13433
|
+
* owner files are preserved; a lock is removed only after its owner is verified
|
|
13434
|
+
* stopped, never by PID/age-based theft.
|
|
13435
|
+
*/
|
|
13436
|
+
async function openFileRouteAdmissionLedger(directory, appId) {
|
|
13437
|
+
const admissionsDirectory = join(directory, "route-admissions");
|
|
13438
|
+
await createPrivateChildDirectory(admissionsDirectory, 448, "route admission directory", directory);
|
|
13439
|
+
await syncStateDirectory(directory, "state directory");
|
|
13440
|
+
const ownerPath = (messageId) => join(admissionsDirectory, `${createHash("sha256").update(JSON.stringify([appId, messageId])).digest("hex")}.json`);
|
|
13441
|
+
const syncAdmissionsDirectory = async () => {
|
|
13442
|
+
const handle = await open(admissionsDirectory, "r");
|
|
13443
|
+
try {
|
|
13444
|
+
await handle.sync();
|
|
13445
|
+
} finally {
|
|
13446
|
+
await handle.close();
|
|
13447
|
+
}
|
|
13448
|
+
};
|
|
13449
|
+
const safeStatOwner = async (messageId) => {
|
|
13450
|
+
const path = ownerPath(messageId);
|
|
13451
|
+
assertPrivateOwnerFile(await lstat(path));
|
|
13452
|
+
return path;
|
|
13453
|
+
};
|
|
13454
|
+
const decodeOwner = (raw, messageId) => {
|
|
13455
|
+
let parsed;
|
|
13456
|
+
try {
|
|
13457
|
+
parsed = JSON.parse(raw);
|
|
13458
|
+
} catch {
|
|
13459
|
+
return { kind: "opaque-fence" };
|
|
13460
|
+
}
|
|
13461
|
+
let record;
|
|
13462
|
+
try {
|
|
13463
|
+
const validated = decodeAdmissionRecord(parsed);
|
|
13464
|
+
record = {
|
|
13465
|
+
appId: validated.appId,
|
|
13466
|
+
messageId: validated.messageId,
|
|
13467
|
+
decision: validated.decision,
|
|
13468
|
+
actorId: validated.actorId,
|
|
13469
|
+
chatId: validated.chatId,
|
|
13470
|
+
at: validated.at,
|
|
13471
|
+
...validated.target === void 0 ? {} : { target: validated.target }
|
|
13472
|
+
};
|
|
13473
|
+
} catch {
|
|
13474
|
+
return { kind: "opaque-fence" };
|
|
13475
|
+
}
|
|
13476
|
+
if (record.appId !== appId || record.messageId !== messageId) throw new Error("Route admission owner belongs to another app or event");
|
|
13477
|
+
return {
|
|
13478
|
+
kind: "record",
|
|
13479
|
+
record
|
|
13480
|
+
};
|
|
13481
|
+
};
|
|
13482
|
+
/**
|
|
13483
|
+
* One safe flow for lookup and EEXIST: an existing owner is stat-validated
|
|
13484
|
+
* first (unsafe files fail before being read or synced); only a valid complete
|
|
13485
|
+
* record is re-synchronized (owner file, admissions directory, closes) before
|
|
13486
|
+
* it is returned, because visibility alone is not durable proof. Partial or
|
|
13487
|
+
* corrupt owners return their opaque fence immediately; failures propagate —
|
|
13488
|
+
* once an owner has been observed by lstat, even ENOENT on the read or during
|
|
13489
|
+
* recovery is a failure, never a downgrade to missing.
|
|
13490
|
+
*/
|
|
13491
|
+
/** Shared read/decode/fsync/finally-close plus admissions-dir resync for an already stat-validated owner. */
|
|
13492
|
+
const readAndSyncOwner = async (validatedPath, messageId) => {
|
|
13493
|
+
const lookup = decodeOwner(await readFile(validatedPath, "utf8"), messageId);
|
|
13494
|
+
if (lookup.kind !== "record") return lookup;
|
|
13495
|
+
const handle = await open(validatedPath, "r+");
|
|
13496
|
+
try {
|
|
13497
|
+
await handle.sync();
|
|
13498
|
+
} finally {
|
|
13499
|
+
await handle.close();
|
|
13500
|
+
}
|
|
13501
|
+
await syncAdmissionsDirectory();
|
|
13502
|
+
return lookup;
|
|
13503
|
+
};
|
|
13504
|
+
const loadExisting = async (messageId) => {
|
|
13505
|
+
const path = await safeStatOwner(messageId);
|
|
13506
|
+
return readAndSyncOwner(path, messageId);
|
|
13507
|
+
};
|
|
13508
|
+
return {
|
|
13509
|
+
lookup: (messageId) => Effect.uninterruptible(Effect.tryPromise({
|
|
13510
|
+
try: async () => {
|
|
13511
|
+
let path;
|
|
13512
|
+
try {
|
|
13513
|
+
path = await safeStatOwner(messageId);
|
|
13514
|
+
} catch (error) {
|
|
13515
|
+
if (Schema.is(isErrnoCode$1)(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
13516
|
+
throw error;
|
|
13517
|
+
}
|
|
13518
|
+
return await readAndSyncOwner(path, messageId);
|
|
13519
|
+
},
|
|
13520
|
+
catch: (error) => error
|
|
13521
|
+
})),
|
|
13522
|
+
admit: (input) => Effect.uninterruptible(Effect.tryPromise({
|
|
13523
|
+
try: async () => {
|
|
13524
|
+
const record = decodeAdmissionRecord({
|
|
13525
|
+
...input,
|
|
13526
|
+
version: 1
|
|
13527
|
+
});
|
|
13528
|
+
if (record.appId !== appId) throw new Error("Route admission record belongs to another app");
|
|
13529
|
+
if (record.decision === "pi" && record.target !== void 0) throw new Error("A pi route admission never carries a Herdr target");
|
|
13530
|
+
const path = ownerPath(record.messageId);
|
|
13531
|
+
let handle;
|
|
13532
|
+
try {
|
|
13533
|
+
handle = await open(path, "wx", 384);
|
|
13534
|
+
} catch (error) {
|
|
13535
|
+
if (Schema.is(isErrnoCode$1)(error) && error.code === "EEXIST") {
|
|
13536
|
+
const lookup = await loadExisting(record.messageId);
|
|
13537
|
+
return lookup.kind === "record" ? {
|
|
13538
|
+
kind: "existing",
|
|
13539
|
+
record: lookup.record
|
|
13540
|
+
} : { kind: "existing-opaque" };
|
|
13541
|
+
}
|
|
13542
|
+
throw error;
|
|
13543
|
+
}
|
|
13544
|
+
try {
|
|
13545
|
+
await handle.writeFile(JSON.stringify({
|
|
13546
|
+
...record,
|
|
13547
|
+
version: 1
|
|
13548
|
+
}), "utf8");
|
|
13549
|
+
await handle.sync();
|
|
13550
|
+
await syncAdmissionsDirectory();
|
|
13551
|
+
} catch (error) {
|
|
13552
|
+
await handle.close().catch(() => void 0);
|
|
13553
|
+
throw error;
|
|
13554
|
+
}
|
|
13555
|
+
await handle.close();
|
|
13556
|
+
return {
|
|
13557
|
+
kind: "created",
|
|
13558
|
+
record
|
|
13559
|
+
};
|
|
13560
|
+
},
|
|
13561
|
+
catch: (error) => error
|
|
13562
|
+
}))
|
|
13563
|
+
};
|
|
13564
|
+
}
|
|
13565
|
+
//#endregion
|
|
13566
|
+
//#region src/adapters/outbound/persistence/terminal-session/terminal-conversation-states.ts
|
|
13567
|
+
const modeStateSchema = Schema.Union([Schema.Struct({
|
|
13568
|
+
kind: Schema.Literal("selection"),
|
|
13569
|
+
actorId: Schema.String,
|
|
13570
|
+
chatId: Schema.String,
|
|
13571
|
+
since: Schema.Number
|
|
13572
|
+
}), Schema.Struct({
|
|
13573
|
+
kind: Schema.Literal("locked"),
|
|
13574
|
+
actorId: Schema.String,
|
|
13575
|
+
chatId: Schema.String,
|
|
13576
|
+
since: Schema.Number,
|
|
13577
|
+
handle: Schema.String,
|
|
13578
|
+
agent: terminalAgentSchema,
|
|
13579
|
+
stale: Schema.Boolean
|
|
13580
|
+
})]);
|
|
13581
|
+
const snapshotSchema = Schema.Struct({
|
|
13582
|
+
version: Schema.Literal(1),
|
|
13583
|
+
appId: Schema.String,
|
|
13584
|
+
states: Schema.Array(modeStateSchema)
|
|
13585
|
+
});
|
|
13586
|
+
const decodeSnapshot = Schema.decodeUnknownSync(snapshotSchema);
|
|
13587
|
+
const isErrnoCode = Schema.Struct({ code: Schema.String });
|
|
13588
|
+
function assertStateCollection(states) {
|
|
13589
|
+
if (states.length > 64) throw new Error("Conversation state snapshot exceeds capacity");
|
|
13590
|
+
if (new Set(states.map((state) => `${state.actorId}\u0000${state.chatId}`)).size !== states.length) throw new Error("Conversation state snapshot contains duplicate conversations");
|
|
13591
|
+
}
|
|
13592
|
+
function assertPrivateDirectory(info) {
|
|
13593
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Conversation state directory is not a regular directory");
|
|
13594
|
+
if (info.uid !== process.getuid?.() || (info.mode & 63) !== 0) throw new Error("Unsafe conversation state directory permissions or owner");
|
|
13595
|
+
}
|
|
13596
|
+
function assertPrivateFile(info, label) {
|
|
13597
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
13598
|
+
if (info.uid !== process.getuid?.() || (info.mode & 127) !== 0) throw new Error(`Unsafe ${label} permissions or owner`);
|
|
13599
|
+
}
|
|
13600
|
+
/**
|
|
13601
|
+
* Versioned per-app conversation mode snapshot (selection/locked states) with a
|
|
13602
|
+
* durable initialization marker.
|
|
13603
|
+
*
|
|
13604
|
+
* Lock ownership: the caller must already hold the shared state-directory lifetime
|
|
13605
|
+
* lock (`openTerminalMessageClaims`) for this directory and keep it for the
|
|
13606
|
+
* store's lifetime; this factory never acquires a lock, releases one, or opens a
|
|
13607
|
+
* connection. Crash recovery is manual: an interrupted
|
|
13608
|
+
* initialization or a snapshot missing after initialization refuses startup
|
|
13609
|
+
* (fail closed) instead of erasing modes; an operator removes a stale lock only
|
|
13610
|
+
* after the previous owner is verified stopped.
|
|
13611
|
+
*/
|
|
13612
|
+
async function openTerminalConversationStates(directory, appId) {
|
|
13613
|
+
assertPrivateDirectory(await lstat(directory));
|
|
13614
|
+
const digest = createHash("sha256").update(appId).digest("hex");
|
|
13615
|
+
const snapshotPath = join(directory, `conversation-states-${digest}.json`);
|
|
13616
|
+
const markerPath = join(directory, `.conversation-states-initialized-${digest}`);
|
|
13617
|
+
const safeFile = async (path, label) => {
|
|
13618
|
+
const info = await lstat(path);
|
|
13619
|
+
if (!info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
13620
|
+
assertPrivateFile(info, label);
|
|
13621
|
+
};
|
|
13622
|
+
const readStates = async () => {
|
|
13623
|
+
await safeFile(snapshotPath, "Conversation state snapshot");
|
|
13624
|
+
let snapshot;
|
|
13625
|
+
try {
|
|
13626
|
+
snapshot = decodeSnapshot(JSON.parse(await readFile(snapshotPath, "utf8")));
|
|
13627
|
+
} catch (error) {
|
|
13628
|
+
throw new Error("Cannot restore conversation states", { cause: error });
|
|
13629
|
+
}
|
|
13630
|
+
if (snapshot.appId !== appId) throw new Error("Conversation state snapshot belongs to another app");
|
|
13631
|
+
const states = snapshot.states;
|
|
13632
|
+
assertStateCollection(states);
|
|
13633
|
+
return states;
|
|
13634
|
+
};
|
|
13635
|
+
const durableSyncDirectory = async () => {
|
|
13636
|
+
const handle = await open(directory, "r");
|
|
13637
|
+
try {
|
|
13638
|
+
await handle.sync();
|
|
13639
|
+
} finally {
|
|
13640
|
+
await handle.close();
|
|
13641
|
+
}
|
|
13642
|
+
};
|
|
13643
|
+
const createMarker = async () => {
|
|
13644
|
+
const marker = await open(markerPath, "wx", 384);
|
|
13645
|
+
try {
|
|
13646
|
+
await marker.writeFile(JSON.stringify({
|
|
13647
|
+
version: 1,
|
|
13648
|
+
appId
|
|
13649
|
+
}));
|
|
13650
|
+
await marker.sync();
|
|
13651
|
+
await durableSyncDirectory();
|
|
13652
|
+
await marker.close();
|
|
13653
|
+
} catch (error) {
|
|
13654
|
+
await marker.close().catch(() => void 0);
|
|
13655
|
+
throw error;
|
|
13656
|
+
}
|
|
13657
|
+
};
|
|
13658
|
+
const markerStat = await lstat(markerPath).catch((error) => {
|
|
13659
|
+
if (Schema.is(isErrnoCode)(error) && error.code === "ENOENT") return;
|
|
13660
|
+
throw error;
|
|
13661
|
+
});
|
|
13662
|
+
if (markerStat !== void 0) {
|
|
13663
|
+
if (!markerStat.isFile()) throw new Error("Conversation state initialization marker is not a regular file");
|
|
13664
|
+
assertPrivateFile(markerStat, "Conversation state initialization marker");
|
|
13665
|
+
}
|
|
13666
|
+
const snapshotStat = await lstat(snapshotPath).catch((error) => {
|
|
13667
|
+
if (Schema.is(isErrnoCode)(error) && error.code === "ENOENT") return;
|
|
13668
|
+
throw error;
|
|
13669
|
+
});
|
|
13670
|
+
let initial;
|
|
13671
|
+
if (markerStat === void 0 && snapshotStat === void 0) {
|
|
13672
|
+
await createMarker();
|
|
13673
|
+
initial = [];
|
|
13674
|
+
await writeAtomicTextFile(snapshotPath, JSON.stringify({
|
|
13675
|
+
version: 1,
|
|
13676
|
+
appId,
|
|
13677
|
+
states: []
|
|
13678
|
+
}), {
|
|
13679
|
+
mode: 384,
|
|
13680
|
+
durable: true
|
|
13681
|
+
});
|
|
13682
|
+
} else {
|
|
13683
|
+
if (markerStat === void 0) throw new Error("Conversation state initialization is ambiguous; refusing startup");
|
|
13684
|
+
if (snapshotStat === void 0) throw new Error("Conversation state snapshot is missing after initialization");
|
|
13685
|
+
initial = await readStates();
|
|
13686
|
+
await writeAtomicTextFile(snapshotPath, JSON.stringify({
|
|
13687
|
+
version: 1,
|
|
13688
|
+
appId,
|
|
13689
|
+
states: initial
|
|
13690
|
+
}), {
|
|
13691
|
+
mode: 384,
|
|
13692
|
+
durable: true
|
|
13693
|
+
});
|
|
13694
|
+
}
|
|
13695
|
+
return {
|
|
13696
|
+
initial,
|
|
13697
|
+
save: (states) => Effect.uninterruptible(Effect.tryPromise({
|
|
13698
|
+
try: async () => {
|
|
13699
|
+
assertStateCollection(states);
|
|
13700
|
+
const validated = decodeSnapshot({
|
|
13701
|
+
version: 1,
|
|
13702
|
+
appId,
|
|
13703
|
+
states
|
|
13704
|
+
});
|
|
13705
|
+
await writeAtomicTextFile(snapshotPath, JSON.stringify({
|
|
13706
|
+
version: 1,
|
|
13707
|
+
appId,
|
|
13708
|
+
states: validated.states
|
|
13709
|
+
}), {
|
|
13710
|
+
mode: 384,
|
|
13711
|
+
durable: true
|
|
13712
|
+
});
|
|
13713
|
+
},
|
|
13714
|
+
catch: (error) => error
|
|
13715
|
+
}))
|
|
13716
|
+
};
|
|
13717
|
+
}
|
|
13718
|
+
//#endregion
|
|
13719
|
+
//#region src/bootstrap/feishu/embedded-herdr-route.ts
|
|
13720
|
+
const herdrRouteConfigSchema = embeddedHerdrRouteSchema;
|
|
13721
|
+
/**
|
|
13722
|
+
* Parses RIVUS_HERDR_ROUTE_CONFIG and fails the assembly unless exactly one
|
|
13723
|
+
* enabled endpoint resolves to the configured appId. A missing variable keeps
|
|
13724
|
+
* the deployment entirely on the Pi path.
|
|
13725
|
+
*/
|
|
13726
|
+
async function resolveHerdrRouteConfig(env, manifest) {
|
|
13727
|
+
const configPath = env.RIVUS_HERDR_ROUTE_CONFIG?.trim();
|
|
13728
|
+
if (!configPath) return;
|
|
13729
|
+
if (!isAbsolute(configPath)) throw new Error(`RIVUS_HERDR_ROUTE_CONFIG must be an absolute path: ${configPath}`);
|
|
13730
|
+
let config;
|
|
13731
|
+
try {
|
|
13732
|
+
const raw = await readFile(configPath, "utf8");
|
|
13733
|
+
config = Schema.decodeUnknownSync(herdrRouteConfigSchema)(JSON.parse(raw));
|
|
13734
|
+
} catch (error) {
|
|
13735
|
+
throw new Error(`Invalid RIVUS_HERDR_ROUTE_CONFIG at ${configPath}`, { cause: error });
|
|
13736
|
+
}
|
|
13737
|
+
if ((config.sessions?.length ?? 0) > 0 === (config.discoverSessionsIn !== void 0)) throw new Error("RIVUS_HERDR_ROUTE_CONFIG requires exactly one of sessions or discoverSessionsIn");
|
|
13738
|
+
for (const [actor, chat] of Object.entries(config.menuChats ?? {})) if (!config.allowedActors.includes(actor) || !config.allowedChats.includes(chat)) throw new Error("Herdr route menu destinations must be allowlisted");
|
|
13739
|
+
const matched = manifest.endpoints.filter((endpoint) => {
|
|
13740
|
+
if (!endpoint.enabled) return false;
|
|
13741
|
+
try {
|
|
13742
|
+
return resolveFeishuEndpointCredentials(endpoint.credentialRef, env).appId === config.appId;
|
|
13743
|
+
} catch {
|
|
13744
|
+
return false;
|
|
13745
|
+
}
|
|
13746
|
+
});
|
|
13747
|
+
if (matched.length !== 1) throw new Error(`RIVUS_HERDR_ROUTE_CONFIG appId must match exactly one enabled endpoint (matched ${matched.length})`);
|
|
13748
|
+
return {
|
|
13749
|
+
appId: config.appId,
|
|
13750
|
+
endpointId: matched[0].id,
|
|
13751
|
+
allowedActors: config.allowedActors,
|
|
13752
|
+
allowedChats: config.allowedChats,
|
|
13753
|
+
stateDirectory: config.stateDirectory,
|
|
13754
|
+
writable: config.writable === true,
|
|
13755
|
+
...config.sessions === void 0 ? {} : { sessions: config.sessions },
|
|
13756
|
+
...config.discoverSessionsIn === void 0 ? {} : { discoverSessionsIn: config.discoverSessionsIn },
|
|
13757
|
+
...config.menuChats === void 0 ? {} : { menuChats: config.menuChats },
|
|
13758
|
+
...config.fontFiles === void 0 ? {} : { fontFiles: config.fontFiles }
|
|
13759
|
+
};
|
|
13760
|
+
}
|
|
13761
|
+
/**
|
|
13762
|
+
* Startup-built pre-upgrade Pi fence: the same inbox file decoded with the same
|
|
13763
|
+
* codec the live store uses, reduced to received ids of any status. Corruption
|
|
13764
|
+
* fails the endpoint assembly before any WebSocket starts.
|
|
13765
|
+
*/
|
|
13766
|
+
async function buildLegacyPiInboxIndex(filePath) {
|
|
13767
|
+
const deliveries = decodeInboundDeliverySnapshots(await readPersistenceFile(filePath), {
|
|
13768
|
+
acceptLegacySameAttemptClaims: true,
|
|
13769
|
+
label: "Feishu inbox",
|
|
13770
|
+
payload: feishuInboundDeliveryPayloadCodec
|
|
13771
|
+
});
|
|
13772
|
+
return new Set(deliveries.map((delivery) => delivery.id));
|
|
13773
|
+
}
|
|
13774
|
+
/**
|
|
13775
|
+
* Opens every embedded Herdr store (claims lock, bindings, mode snapshot,
|
|
13776
|
+
* owner ledger) plus the connection-less runtime and router. The opener
|
|
13777
|
+
* Promise is allocated lazily inside the Effect and joined on interruption:
|
|
13778
|
+
* a cancelled hand still waits for the stores it may have acquired and
|
|
13779
|
+
* releases them instead of leaving an ownerless lock holder behind.
|
|
13780
|
+
*/
|
|
13781
|
+
function openEmbeddedHerdrRoute(input) {
|
|
13782
|
+
return Effect.suspend(() => {
|
|
13783
|
+
const opener = buildEmbeddedHerdrRoute(input);
|
|
13784
|
+
return Effect.onInterrupt(Effect.tryPromise({
|
|
13785
|
+
catch: (error) => error,
|
|
13786
|
+
try: () => opener
|
|
13787
|
+
}), () => Effect.promise(() => opener.then((opened) => opened.runtime.stop().then(() => void 0, () => void 0), () => void 0)));
|
|
13788
|
+
});
|
|
13789
|
+
}
|
|
13790
|
+
async function buildEmbeddedHerdrRoute(input) {
|
|
13791
|
+
const { herdr } = input;
|
|
13792
|
+
return (async () => {
|
|
13793
|
+
const legacyPiIds = await buildLegacyPiInboxIndex(join(input.endpointStateDirectory, "feishu-inbox.jsonl"));
|
|
13794
|
+
const claims = await openTerminalMessageClaims(herdr.stateDirectory, herdr.appId);
|
|
13795
|
+
const closeClaims = () => claims.close().catch(() => void 0);
|
|
13796
|
+
let bindings;
|
|
13797
|
+
let states;
|
|
13798
|
+
let ledger;
|
|
13799
|
+
let stopState;
|
|
13800
|
+
let runtime;
|
|
13801
|
+
let router;
|
|
13802
|
+
try {
|
|
13803
|
+
bindings = await openTerminalBindingsStore(herdr.stateDirectory, herdr.appId);
|
|
13804
|
+
states = await openTerminalConversationStates(herdr.stateDirectory, herdr.appId);
|
|
13805
|
+
ledger = await openFileRouteAdmissionLedger(herdr.stateDirectory, herdr.appId);
|
|
13806
|
+
stopState = { stopped: false };
|
|
13807
|
+
runtime = createHerdrFeishuBridgeRuntime({
|
|
13808
|
+
sessions: createHerdrSocketClient(herdr.sessions ?? [], 5e3, herdr.discoverSessionsIn),
|
|
13809
|
+
actors: herdr.allowedActors,
|
|
13810
|
+
chats: herdr.allowedChats,
|
|
13811
|
+
writable: herdr.writable,
|
|
13812
|
+
claim: claims.claim,
|
|
13813
|
+
bindings,
|
|
13814
|
+
conversation: {
|
|
13815
|
+
epoch: randomUUID(),
|
|
13816
|
+
states
|
|
13817
|
+
},
|
|
13818
|
+
delivery: createFeishuTerminalDelivery(new Lark.Client({
|
|
13819
|
+
appId: herdr.appId,
|
|
13820
|
+
appSecret: input.appSecret,
|
|
13821
|
+
domain: input.baseUrl,
|
|
13822
|
+
loggerLevel: Lark.LoggerLevel.error
|
|
13823
|
+
}), { isStopped: () => stopState.stopped }),
|
|
13824
|
+
...herdr.fontFiles === void 0 ? {} : { fontFiles: herdr.fontFiles },
|
|
13825
|
+
stopState,
|
|
13826
|
+
closeStores: () => claims.close()
|
|
13827
|
+
});
|
|
13828
|
+
router = createFeishuConversationRouter({
|
|
13829
|
+
appId: herdr.appId,
|
|
13830
|
+
bridge: runtime.bridge,
|
|
13831
|
+
ledger,
|
|
13832
|
+
actors: herdr.allowedActors,
|
|
13833
|
+
chats: herdr.allowedChats,
|
|
13834
|
+
legacyOwnerOf: (messageId) => Effect.gen(function* () {
|
|
13835
|
+
const claimed = yield* claims.hasClaim(messageId);
|
|
13836
|
+
const pi = legacyPiIds.has(messageId);
|
|
13837
|
+
if (pi && claimed) return "conflict";
|
|
13838
|
+
if (pi) return "pi";
|
|
13839
|
+
return claimed ? "herdr" : "none";
|
|
13840
|
+
}),
|
|
13841
|
+
groupPolicy: input.groupPolicy,
|
|
13842
|
+
botOpenId: input.botOpenId,
|
|
13843
|
+
target: runtime
|
|
13844
|
+
});
|
|
13845
|
+
} catch (error) {
|
|
13846
|
+
await closeClaims();
|
|
13847
|
+
throw error;
|
|
13848
|
+
}
|
|
13849
|
+
return {
|
|
13850
|
+
runtime,
|
|
13851
|
+
routedDispatcher: () => createRoutedFeishuEventDispatcher(new Lark.EventDispatcher({}), {
|
|
13852
|
+
message: router.routeMessage,
|
|
13853
|
+
card: router.routeCardAction,
|
|
13854
|
+
menu: router.routeMenuEvent
|
|
13855
|
+
}, herdr.menuChats ?? {})
|
|
13856
|
+
};
|
|
13857
|
+
})();
|
|
13858
|
+
}
|
|
13859
|
+
function wrapEmbeddedHerdrEndpointLifecycle(endpoint, runtime) {
|
|
13860
|
+
return {
|
|
13861
|
+
running: () => endpoint.running(),
|
|
13862
|
+
start: () => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
13863
|
+
yield* Effect.tryPromise({
|
|
13864
|
+
catch: (error) => error,
|
|
13865
|
+
try: () => runtime.start()
|
|
13866
|
+
});
|
|
13867
|
+
const startExit = yield* Effect.exit(restore(Effect.suspend(() => endpoint.start())));
|
|
13868
|
+
if (Exit.isFailure(startExit)) {
|
|
13869
|
+
yield* Effect.exit(Effect.tryPromise({
|
|
13870
|
+
catch: () => void 0,
|
|
13871
|
+
try: () => runtime.stop()
|
|
13872
|
+
}));
|
|
13873
|
+
return yield* Effect.failCause(startExit.cause);
|
|
13874
|
+
}
|
|
13875
|
+
})),
|
|
13876
|
+
stop: () => Effect.uninterruptible(Effect.gen(function* () {
|
|
13877
|
+
yield* Effect.sync(() => runtime.stopIntake());
|
|
13878
|
+
const endpointExit = yield* Effect.exit(endpoint.stop());
|
|
13879
|
+
const drainExit = yield* Effect.exit(Effect.tryPromise({
|
|
13880
|
+
catch: (error) => error,
|
|
13881
|
+
try: () => runtime.completeStop()
|
|
13882
|
+
}));
|
|
13883
|
+
if (Exit.isFailure(endpointExit)) return yield* Effect.failCause(endpointExit.cause);
|
|
13884
|
+
if (Exit.isFailure(drainExit)) return yield* Effect.failCause(drainExit.cause);
|
|
13885
|
+
}))
|
|
13886
|
+
};
|
|
13887
|
+
}
|
|
13888
|
+
//#endregion
|
|
12912
13889
|
//#region src/adapters/feishu/run-presentation/feishu-presentation-preparation.ts
|
|
12913
13890
|
function createFeishuPresentationPreparation(options) {
|
|
12914
13891
|
return (run) => Effect.gen(function* () {
|
|
@@ -15146,112 +16123,6 @@ function append(filePath, record) {
|
|
|
15146
16123
|
});
|
|
15147
16124
|
}
|
|
15148
16125
|
//#endregion
|
|
15149
|
-
//#region src/adapters/feishu/inbound-delivery/jsonl-inbound-delivery-store.ts
|
|
15150
|
-
function openJsonlInboundDeliveryStore(options) {
|
|
15151
|
-
const codecOptions = {
|
|
15152
|
-
...options.acceptLegacySameAttemptClaims === void 0 ? {} : { acceptLegacySameAttemptClaims: options.acceptLegacySameAttemptClaims },
|
|
15153
|
-
label: options.label ?? "Inbound Delivery",
|
|
15154
|
-
payload: options.payload
|
|
15155
|
-
};
|
|
15156
|
-
return Effect.gen(function* () {
|
|
15157
|
-
const raw = yield* Effect.tryPromise({
|
|
15158
|
-
try: () => readPersistenceFile(options.filePath),
|
|
15159
|
-
catch: (cause) => cause
|
|
15160
|
-
});
|
|
15161
|
-
return createInMemoryDurableQueueStore({
|
|
15162
|
-
initial: yield* Effect.try({
|
|
15163
|
-
try: () => decodeInboundDeliverySnapshots(raw, codecOptions),
|
|
15164
|
-
catch: (cause) => cause
|
|
15165
|
-
}),
|
|
15166
|
-
persist: (delivery) => appendInboundDeliverySnapshot(options.filePath, delivery)
|
|
15167
|
-
});
|
|
15168
|
-
});
|
|
15169
|
-
}
|
|
15170
|
-
function decodeInboundDeliverySnapshots(raw, options) {
|
|
15171
|
-
if (raw === void 0) return [];
|
|
15172
|
-
const latest = /* @__PURE__ */ new Map();
|
|
15173
|
-
for (const [index, line] of raw.split("\n").entries()) {
|
|
15174
|
-
if (line.trim().length === 0) continue;
|
|
15175
|
-
let parsed;
|
|
15176
|
-
try {
|
|
15177
|
-
parsed = JSON.parse(line);
|
|
15178
|
-
} catch {
|
|
15179
|
-
throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
15180
|
-
}
|
|
15181
|
-
if (!isEnvelope(parsed)) throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
15182
|
-
let delivery;
|
|
15183
|
-
try {
|
|
15184
|
-
delivery = decodeDelivery(parsed.delivery, options.payload);
|
|
15185
|
-
} catch {
|
|
15186
|
-
throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
|
|
15187
|
-
}
|
|
15188
|
-
const previous = latest.get(delivery.id);
|
|
15189
|
-
if (delivery.revision !== (previous?.revision ?? 0) + 1) throw new Error(`invalid ${options.label} revision at line ${index + 1}`);
|
|
15190
|
-
try {
|
|
15191
|
-
if (previous) {
|
|
15192
|
-
if (!isValidPersistedTransition(previous, delivery, options.acceptLegacySameAttemptClaims ?? false)) throw new Error("transition");
|
|
15193
|
-
} else assertNewInboundDelivery(delivery);
|
|
15194
|
-
} catch {
|
|
15195
|
-
throw new Error(`invalid ${options.label} transition at line ${index + 1}`);
|
|
15196
|
-
}
|
|
15197
|
-
latest.set(delivery.id, delivery);
|
|
15198
|
-
}
|
|
15199
|
-
return [...latest.values()];
|
|
15200
|
-
}
|
|
15201
|
-
function encodeInboundDeliverySnapshot(delivery) {
|
|
15202
|
-
const snapshot = readInboundDeliverySnapshot(delivery);
|
|
15203
|
-
const persisted = {
|
|
15204
|
-
acceptedAt: snapshot.acceptedAt,
|
|
15205
|
-
attempts: snapshot.attempts,
|
|
15206
|
-
id: snapshot.id,
|
|
15207
|
-
laneKey: snapshot.laneKey,
|
|
15208
|
-
...snapshot.metadata === void 0 ? {} : { options: snapshot.metadata },
|
|
15209
|
-
payload: snapshot.payload,
|
|
15210
|
-
...snapshot.recovery === void 0 ? {} : { recovery: snapshot.recovery },
|
|
15211
|
-
revision: snapshot.revision,
|
|
15212
|
-
state: snapshot.state
|
|
15213
|
-
};
|
|
15214
|
-
return JSON.stringify({
|
|
15215
|
-
delivery: persisted,
|
|
15216
|
-
version: 1
|
|
15217
|
-
});
|
|
15218
|
-
}
|
|
15219
|
-
function decodeDelivery(value, payloadCodec) {
|
|
15220
|
-
if (!isRecord(value) || typeof value.acceptedAt !== "string" || typeof value.attempts !== "number" || typeof value.id !== "string" || typeof value.laneKey !== "string" || typeof value.revision !== "number") throw new Error("invalid Inbound Delivery snapshot");
|
|
15221
|
-
const metadata = payloadCodec.decodeMetadata(value.options);
|
|
15222
|
-
return readInboundDeliverySnapshot({
|
|
15223
|
-
acceptedAt: value.acceptedAt,
|
|
15224
|
-
attempts: value.attempts,
|
|
15225
|
-
id: value.id,
|
|
15226
|
-
laneKey: value.laneKey,
|
|
15227
|
-
...metadata === void 0 ? {} : { metadata },
|
|
15228
|
-
payload: payloadCodec.decodePayload(value.payload),
|
|
15229
|
-
...value.recovery === void 0 ? {} : { recovery: value.recovery },
|
|
15230
|
-
revision: value.revision,
|
|
15231
|
-
state: value.state
|
|
15232
|
-
});
|
|
15233
|
-
}
|
|
15234
|
-
function isEnvelope(value) {
|
|
15235
|
-
return isRecord(value) && value.version === 1 && "delivery" in value;
|
|
15236
|
-
}
|
|
15237
|
-
function isValidPersistedTransition(previous, next, acceptLegacySameAttemptClaims) {
|
|
15238
|
-
if (isValidInboundDeliveryTransition(previous, next)) return true;
|
|
15239
|
-
if (!acceptLegacySameAttemptClaims || previous.state.status !== "pending" || next.state.status !== "leased" || next.attempts !== previous.attempts) return false;
|
|
15240
|
-
return isValidInboundDeliveryTransition(previous, {
|
|
15241
|
-
...next,
|
|
15242
|
-
attempts: next.attempts + 1
|
|
15243
|
-
});
|
|
15244
|
-
}
|
|
15245
|
-
function appendInboundDeliverySnapshot(filePath, delivery) {
|
|
15246
|
-
return Effect.tryPromise({
|
|
15247
|
-
try: async () => {
|
|
15248
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
15249
|
-
await appendFile(filePath, `${encodeInboundDeliverySnapshot(delivery)}\n`, "utf8");
|
|
15250
|
-
},
|
|
15251
|
-
catch: (cause) => cause
|
|
15252
|
-
});
|
|
15253
|
-
}
|
|
15254
|
-
//#endregion
|
|
15255
16126
|
//#region src/adapters/feishu/conversation-session/json-feishu-session-store.ts
|
|
15256
16127
|
function openJsonFeishuSessionStore(options) {
|
|
15257
16128
|
return Effect.tryPromise({
|
|
@@ -17017,6 +17888,7 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17017
17888
|
manifest
|
|
17018
17889
|
};
|
|
17019
17890
|
})));
|
|
17891
|
+
const herdrRoute = await resolveHerdrRouteConfig(context.env, manifest);
|
|
17020
17892
|
const memoryTenantId = context.env.RIVUS_MEMORY_TENANT_ID?.trim() || "local";
|
|
17021
17893
|
const request = createJsonFetchRequest();
|
|
17022
17894
|
const attachmentCapabilities = /* @__PURE__ */ new Map();
|
|
@@ -17323,6 +18195,15 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17323
18195
|
config
|
|
17324
18196
|
});
|
|
17325
18197
|
const prepareCardTarget = (run) => cardRollover.prepareRun(run);
|
|
18198
|
+
const herdr = herdrRoute !== void 0 && input.endpointId === herdrRoute.endpointId ? herdrRoute : void 0;
|
|
18199
|
+
const embedded = herdr ? yield* openEmbeddedHerdrRoute({
|
|
18200
|
+
herdr,
|
|
18201
|
+
appSecret: credentials.appSecret,
|
|
18202
|
+
baseUrl: input.definition.baseUrl,
|
|
18203
|
+
endpointStateDirectory: endpointState,
|
|
18204
|
+
groupPolicy: input.definition.groupPolicy,
|
|
18205
|
+
botOpenId
|
|
18206
|
+
}) : void 0;
|
|
17326
18207
|
const endpoint = createFeishuDeploymentEndpoint({
|
|
17327
18208
|
agentId: input.agentId,
|
|
17328
18209
|
acknowledgeSteering: (messageId) => reactions.add(messageId),
|
|
@@ -17333,7 +18214,7 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17333
18214
|
finalizeRun: (runId) => cardRollover.rollover.releaseRun(runId),
|
|
17334
18215
|
cancel: input.cancel,
|
|
17335
18216
|
endpointId: input.endpointId,
|
|
17336
|
-
eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
18217
|
+
eventDispatcher: embedded ? createLazyFeishuWebSocketEventDispatcher(embedded.routedDispatcher) : createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
17337
18218
|
groupPolicy: input.definition.groupPolicy,
|
|
17338
18219
|
handle: input.handle,
|
|
17339
18220
|
inboxStore: inbox,
|
|
@@ -17361,7 +18242,8 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17361
18242
|
workerConcurrency: 4
|
|
17362
18243
|
});
|
|
17363
18244
|
interactionRegistry.register(input.endpointId, interactions);
|
|
17364
|
-
return endpoint;
|
|
18245
|
+
if (embedded === void 0) return endpoint;
|
|
18246
|
+
return wrapEmbeddedHerdrEndpointLifecycle(endpoint, embedded.runtime);
|
|
17365
18247
|
}).pipe(Effect.catchDefect((error) => Effect.fail(error))),
|
|
17366
18248
|
createRuntime: (input) => Effect.gen(function* () {
|
|
17367
18249
|
const instanceState = join(STATE_DIR, "instances", input.instanceId);
|