@excitedjs/feishu-channel 0.0.1 → 0.2.0-alpha.g2b7e2d09fbdc

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -16
  3. package/dist/bot.d.ts +151 -0
  4. package/dist/bot.d.ts.map +1 -0
  5. package/dist/bot.js +276 -0
  6. package/dist/bot.js.map +1 -0
  7. package/dist/chat-bots-store.d.ts +105 -0
  8. package/dist/chat-bots-store.d.ts.map +1 -0
  9. package/dist/chat-bots-store.js +245 -0
  10. package/dist/chat-bots-store.js.map +1 -0
  11. package/dist/feishu-channel.d.ts +95 -0
  12. package/dist/feishu-channel.d.ts.map +1 -0
  13. package/dist/feishu-channel.js +407 -0
  14. package/dist/feishu-channel.js.map +1 -0
  15. package/dist/feishu-gate.d.ts +82 -0
  16. package/dist/feishu-gate.d.ts.map +1 -0
  17. package/dist/feishu-gate.js +259 -0
  18. package/dist/feishu-gate.js.map +1 -0
  19. package/dist/feishu-mcp-tools.d.ts +39 -0
  20. package/dist/feishu-mcp-tools.d.ts.map +1 -0
  21. package/dist/feishu-mcp-tools.js +153 -0
  22. package/dist/feishu-mcp-tools.js.map +1 -0
  23. package/dist/feishu-message.d.ts +51 -0
  24. package/dist/feishu-message.d.ts.map +1 -0
  25. package/dist/feishu-message.js +298 -0
  26. package/dist/feishu-message.js.map +1 -0
  27. package/dist/inbound.d.ts.map +1 -1
  28. package/dist/inbound.js +5 -5
  29. package/dist/inbound.js.map +1 -1
  30. package/dist/index.d.ts +16 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +16 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/os.d.ts +11 -0
  35. package/dist/internal/os.d.ts.map +1 -0
  36. package/dist/internal/os.js +30 -0
  37. package/dist/internal/os.js.map +1 -0
  38. package/dist/introduce.d.ts +101 -0
  39. package/dist/introduce.d.ts.map +1 -0
  40. package/dist/introduce.js +183 -0
  41. package/dist/introduce.js.map +1 -0
  42. package/dist/provider-ref.d.ts +8 -0
  43. package/dist/provider-ref.d.ts.map +1 -0
  44. package/dist/provider-ref.js +8 -0
  45. package/dist/provider-ref.js.map +1 -0
  46. package/dist/provider.d.ts +43 -0
  47. package/dist/provider.d.ts.map +1 -0
  48. package/dist/provider.js +152 -0
  49. package/dist/provider.js.map +1 -0
  50. package/package.json +17 -21
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 excitedjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,29 +1,64 @@
1
1
  # @excitedjs/feishu-channel
2
2
 
3
- The **dreamux-side channel layer**: stateful orchestration on top of
4
- [`@excitedjs/feishu-transport`](../feishu-transport).
3
+ The built-in Feishu **`ChannelProvider`** for [Dreamux](../../dreamux) the
4
+ package behind the `builtin:feishu` provider reference. It implements the
5
+ neutral `@excitedjs/dreamux-types` channel contract on top of
6
+ [`@excitedjs/feishu-transport`](../feishu-transport), which stays the sole owner
7
+ of the Lark SDK.
5
8
 
6
- > **Status: PR0 scaffold.** No business logic yet. See
7
- > [issue #25](https://github.com/excitedjs/dreamux/issues/25).
9
+ `@excitedjs/dreamux` depends on this package by default and resolves
10
+ `builtin:feishu` to it, so the Feishu channel ships out of the box.
8
11
 
9
- ## Scope (when filled in by later PRs)
12
+ ## What it owns
10
13
 
11
- - **① Filter** @-mention gate (`isBotAddressed`) + access gate (allowlist),
12
- orchestrating the core's pure `gate()`.
13
- - **② Map + forward** conversation→Codex-thread mapping (`conversationKey`
14
- thread) and forwarding inbound to the engine / outbound back to Feishu.
14
+ - The live Feishu channel **session** (bot start/close) above raw Lark JSAPI
15
+ calls.
16
+ - **Access / trust** behavior: the @-mention/allowlist gate and the chat-bots
17
+ store, read and written under a host-supplied state directory.
18
+ - **Inbound normalization**: turning Feishu events into agent-facing channel
19
+ results, including the `<channel source="feishu" …>` envelope and
20
+ `<attachment>` blocks.
21
+ - **Attachment handling**: downloading inbound attachments after the host access
22
+ gate allows delivery, plus cache layout, path sanitization, permissions, and
23
+ honest fallback references when a resource is not downloaded.
24
+ - The Feishu MCP **tool backing**: the `reply` / `react` / `list_chat_bots`
25
+ tool parsing and handlers.
15
26
 
16
- This is dreamux's counterpart of claudemux's proxy/daemon. The two are
17
- symmetric but **never depend on each other** — both depend only on
18
- `@excitedjs/feishu-transport`. The engine (`DispatcherRuntime`) stays in
19
- `@excitedjs/dreamux` and implements this layer's `InboundSink` / `OutboundPort`
20
- interfaces, so there is no dependency cycle.
27
+ ## What it does not own
28
+
29
+ - It never imports `@excitedjs/dreamux` core, and never imports the Lark SDK
30
+ directly platform calls go through `@excitedjs/feishu-transport`. Both
31
+ boundaries are enforced by `tests/import-boundary.test.ts`.
32
+ - Dispatcher lifecycle, agent/Codex process supervision, routing, binding state,
33
+ authorization, Team lifecycle, and the Feishu MCP **server descriptor** /
34
+ admin-method routing stay in `@excitedjs/dreamux`. The host supplies the bot
35
+ secret / app id and the state / cache directories; the package reconstructs no
36
+ Dreamux host layout or path contract.
37
+
38
+ ## Public API
39
+
40
+ - `createFeishuChannelProvider()` plus the default-exported provider factory —
41
+ builds the neutral `ChannelProvider` the generic channel loader registers for
42
+ `builtin:feishu`. Its `createSession` returns a contract-valid `ChannelSession`
43
+ (`reply` / `react` / `resolveTarget` / `tools` / `handleTool` /
44
+ `messageBelongsToTarget`).
45
+ - The session class plus the gate, chat-bots store, message formatter, MCP tool
46
+ parser, and bot helpers, used by the core adapter that drives the production
47
+ host-shaped session path.
48
+
49
+ > Production note: `@excitedjs/dreamux` drives this package through a thin
50
+ > core-owned adapter that uses the richer host-shaped session API (a
51
+ > result-returning inbound submitter the reaction ledger keys off). The neutral
52
+ > `ChannelSession.start(routes)` path is real and contract-tested, but is not the
53
+ > production wiring today. See
54
+ > [`.agents/decisions/npm-package-split-and-channel-targets.md`](../../../.agents/decisions/npm-package-split-and-channel-targets.md).
21
55
 
22
56
  ## Build / test
23
57
 
24
- Built via rush in topological order (core first):
58
+ Built and tested via rush in topological order (dependencies first):
25
59
 
26
60
  ```sh
27
61
  node common/scripts/install-run-rush.js update
28
- node common/scripts/install-run-rush.js build
62
+ node common/scripts/install-run-rush.js build --to @excitedjs/feishu-channel
63
+ node common/scripts/install-run-rush.js test --to @excitedjs/feishu-channel
29
64
  ```
package/dist/bot.d.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The `FeishuBot` adapter — one per Dispatcher (D3: 1 Dispatcher = 1 Bot).
3
+ *
4
+ * Since issue #25 PR1 this is a thin adapter over `@excitedjs/feishu-transport`
5
+ * (the shared platform-I/O core): all Feishu SDK I/O — the inbound WebSocket,
6
+ * markdown→card render, content parse, the outbound message API — lives in the
7
+ * core, the single importer of `@larksuiteoapi/node-sdk`. This file only shapes
8
+ * the core's surface into the `FeishuBot` interface the server already wires:
9
+ * - `start(routes)` takes one handler per Feishu event type (issue #62 seam):
10
+ * `onMessage` for `im.message.receive_v1` (normalized via the core's
11
+ * `parseInbound` into a `FeishuInboundEvent`) and an optional
12
+ * `onBotMemberAdded` for `im.chat.member.bot.added_v1`. Each route awaits
13
+ * its handler, so the server gates and submits accepted inbound before the
14
+ * SDK acks.
15
+ * - `send(target, text)` delegates to the core transport, preserving reply
16
+ * threading / @-back metadata from the in-memory inbound batch.
17
+ * - `botOpenId` surfaces the core transport's `selfId`.
18
+ *
19
+ * Tests inject a `FakeFeishuBot` via `createFakeFeishuBot()` instead of opening
20
+ * a live connection.
21
+ */
22
+ import { type FeishuBotMemberAddedEvent, type FeishuMessageResourceFetcher, type FeishuMessageResourceRequest, type FeishuMessageResourceResponse, type FeishuTransport, type InboundResource, type Mention, type OutboundTarget, type TransportLogger } from '@excitedjs/feishu-transport';
23
+ import type { FeishuInviteMembersInput, FeishuInviteMembersResult } from '@excitedjs/feishu-transport';
24
+ export type { FeishuInviteMembersInput, FeishuInviteMembersResult, } from '@excitedjs/feishu-transport';
25
+ export interface FeishuInboundEvent {
26
+ messageId: string;
27
+ chatId: string;
28
+ chatType: string;
29
+ senderId: string;
30
+ /**
31
+ * The sender's `union_id`, when Feishu provides it. Diagnostic only — it is
32
+ * surfaced in inbound-drop logs to help tell "same bot, different app-scoped
33
+ * open_id" apart from "different entity", and is never used for access
34
+ * gating. Absent when Feishu omits it.
35
+ */
36
+ senderUnionId?: string;
37
+ senderType: string;
38
+ /**
39
+ * Best-effort display name seam for future enrichers. Feishu
40
+ * im.message.receive_v1 does not provide this in the native event envelope,
41
+ * so the normal value is intentionally an empty string.
42
+ */
43
+ senderName: string;
44
+ messageType: string;
45
+ /** Raw JSON-encoded content as Feishu delivered it. */
46
+ rawContent: string;
47
+ /** Parsed text after the core's content flattening / mention substitution. */
48
+ parsedText: string;
49
+ /** Structured Feishu resources discovered in the message content. */
50
+ resources?: InboundResource[];
51
+ mentions: Mention[];
52
+ createTime: string;
53
+ /** The full original Feishu event payload (for storage / audit). */
54
+ raw: unknown;
55
+ }
56
+ export type InboundHandler = (event: FeishuInboundEvent) => void | Promise<void>;
57
+ export type BotMemberAddedHandler = (event: FeishuBotMemberAddedEvent) => void | Promise<void>;
58
+ /**
59
+ * The typed event-route seam (issue #62 Phase 1). `start` takes one handler per
60
+ * Feishu event type instead of a single message handler, so a new event type is
61
+ * wired by adding a field here and a transport route, without growing branches
62
+ * in `Server`. This is a small typed seam, not yet a generic
63
+ * `eventType -> handler` registry; if a third event type lands, promote this to
64
+ * a map. Each route still awaits its handler before the SDK acks
65
+ * (queue-before-ACK).
66
+ */
67
+ export interface FeishuInboundRoutes {
68
+ /** `im.message.receive_v1` — a chat message. */
69
+ onMessage: InboundHandler;
70
+ /** `im.chat.member.bot.added_v1` — the bot was added to a chat. Optional. */
71
+ onBotMemberAdded?: BotMemberAddedHandler;
72
+ }
73
+ export interface FeishuSendResult {
74
+ /** message_id of each card sent, in order. Empty if Feishu omitted ids. */
75
+ messageIds: string[];
76
+ }
77
+ export interface FeishuBot extends FeishuMessageResourceFetcher {
78
+ readonly appId: string;
79
+ readonly botOpenId: string | undefined;
80
+ start(routes: FeishuInboundRoutes): Promise<void>;
81
+ send(target: OutboundTarget, text: string): Promise<FeishuSendResult>;
82
+ inviteMembers(input: FeishuInviteMembersInput): Promise<FeishuInviteMembersResult>;
83
+ addReaction(messageId: string, emoji: string): Promise<string>;
84
+ removeReaction(messageId: string, reactionId: string): Promise<void>;
85
+ fetchMessageResource(request: FeishuMessageResourceRequest): Promise<FeishuMessageResourceResponse>;
86
+ close(): Promise<void>;
87
+ }
88
+ export interface CreateBotOptions {
89
+ appId: string;
90
+ appSecret: string;
91
+ /**
92
+ * Structured logger for the underlying transport's own diagnostics (Lark SDK
93
+ * logging, WebSocket connection lifecycle, best-effort fetch/close failures).
94
+ * Forwarded verbatim to `createFeishuTransport`. Omit to keep the transport's
95
+ * historical stderr behavior. The server injects the dispatcher's
96
+ * per-dispatcher channel logger here so connection/SDK lines land in
97
+ * `logs/feishu-channel/<id>.log` alongside the host's own channel decisions.
98
+ */
99
+ logger?: TransportLogger;
100
+ }
101
+ export interface CreateFeishuBotDeps {
102
+ createTransport?: (opts: CreateBotOptions) => FeishuTransport;
103
+ }
104
+ export interface ChannelOutboundTarget {
105
+ /** Stable channel-local conversation id. */
106
+ conversationId: string;
107
+ /** Optional channel-local source message to thread under. */
108
+ replyTo?: string;
109
+ /** Optional channel-local participants to bring into the reply. */
110
+ mentionUsers?: string[];
111
+ /** Optional host/runtime routing hint, opaque to the channel adapter. */
112
+ conversationKey?: string;
113
+ }
114
+ export declare function createFeishuBot(opts: CreateBotOptions, deps?: CreateFeishuBotDeps): FeishuBot;
115
+ export declare function channelOutboundToFeishuTarget(target: ChannelOutboundTarget): OutboundTarget;
116
+ export interface FakeFeishuBot extends FeishuBot {
117
+ readonly sentMessages: Array<{
118
+ chatId: string;
119
+ target: OutboundTarget;
120
+ text: string;
121
+ messageIds: string[];
122
+ }>;
123
+ readonly reactions: Array<{
124
+ messageId: string;
125
+ emoji: string;
126
+ reactionId: string;
127
+ }>;
128
+ readonly removedReactions: Array<{
129
+ messageId: string;
130
+ reactionId: string;
131
+ }>;
132
+ /** Combined add/remove timeline, in call order (tests assert ordering). */
133
+ readonly reactionOps: Array<{
134
+ op: 'add';
135
+ messageId: string;
136
+ emoji: string;
137
+ reactionId: string;
138
+ } | {
139
+ op: 'remove';
140
+ messageId: string;
141
+ reactionId: string;
142
+ }>;
143
+ inject(event: FeishuInboundEvent): Promise<void>;
144
+ injectBotMemberAdded(event: FeishuBotMemberAddedEvent): Promise<void>;
145
+ setSendError(err: Error | null): void;
146
+ setReactionError(err: Error | null): void;
147
+ setRemoveReactionError(err: Error | null): void;
148
+ setMessageResource(fileKey: string, resource: FeishuMessageResourceResponse | Error | null): void;
149
+ }
150
+ export declare function createFakeFeishuBot(appId?: string): FakeFeishuBot;
151
+ //# sourceMappingURL=bot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
package/dist/bot.js ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The `FeishuBot` adapter — one per Dispatcher (D3: 1 Dispatcher = 1 Bot).
3
+ *
4
+ * Since issue #25 PR1 this is a thin adapter over `@excitedjs/feishu-transport`
5
+ * (the shared platform-I/O core): all Feishu SDK I/O — the inbound WebSocket,
6
+ * markdown→card render, content parse, the outbound message API — lives in the
7
+ * core, the single importer of `@larksuiteoapi/node-sdk`. This file only shapes
8
+ * the core's surface into the `FeishuBot` interface the server already wires:
9
+ * - `start(routes)` takes one handler per Feishu event type (issue #62 seam):
10
+ * `onMessage` for `im.message.receive_v1` (normalized via the core's
11
+ * `parseInbound` into a `FeishuInboundEvent`) and an optional
12
+ * `onBotMemberAdded` for `im.chat.member.bot.added_v1`. Each route awaits
13
+ * its handler, so the server gates and submits accepted inbound before the
14
+ * SDK acks.
15
+ * - `send(target, text)` delegates to the core transport, preserving reply
16
+ * threading / @-back metadata from the in-memory inbound batch.
17
+ * - `botOpenId` surfaces the core transport's `selfId`.
18
+ *
19
+ * Tests inject a `FakeFeishuBot` via `createFakeFeishuBot()` instead of opening
20
+ * a live connection.
21
+ */
22
+ import { BOT_MEMBER_ADDED_EVENT_TYPE, createFeishuTransport, narrowMetaFromEvent, normalizeBotMemberAddedEvent, parseInbound, toChannelInbound, } from '@excitedjs/feishu-transport';
23
+ /** The Feishu event_type carrying inbound chat messages. */
24
+ const IM_MESSAGE_EVENT_TYPE = 'im.message.receive_v1';
25
+ export function createFeishuBot(opts, deps = {}) {
26
+ const transport = deps.createTransport?.(opts) ??
27
+ createFeishuTransport({
28
+ appId: opts.appId,
29
+ appSecret: opts.appSecret,
30
+ },
31
+ // Forward the host's logger so the transport's own SDK / connection
32
+ // diagnostics fold into the per-dispatcher channel log. `undefined` keeps
33
+ // the transport's default stderr behavior, so always passing the option
34
+ // object is safe and keeps the real wiring path explicit.
35
+ { logger: opts.logger });
36
+ return {
37
+ get appId() {
38
+ return transport.appId;
39
+ },
40
+ get botOpenId() {
41
+ return transport.selfId;
42
+ },
43
+ async start(routes) {
44
+ // The core opens the WebSocket and awaits each route handler before the
45
+ // SDK acks; awaiting here keeps gate/submission work before ACK.
46
+ // `start` rejects if the connection does not come up, so the server's
47
+ // try/catch can fail the dispatcher loudly rather than leave it dark.
48
+ const table = {
49
+ [IM_MESSAGE_EVENT_TYPE]: async (raw) => {
50
+ const event = normalizeInboundEvent(raw);
51
+ if (event === null)
52
+ return;
53
+ await routes.onMessage(event);
54
+ },
55
+ };
56
+ if (routes.onBotMemberAdded !== undefined) {
57
+ const onBotMemberAdded = routes.onBotMemberAdded;
58
+ table[BOT_MEMBER_ADDED_EVENT_TYPE] = async (raw) => {
59
+ const event = normalizeBotMemberAddedEvent(raw);
60
+ if (event === null)
61
+ return;
62
+ await onBotMemberAdded(event);
63
+ };
64
+ }
65
+ await transport.start(table);
66
+ },
67
+ async send(target, text) {
68
+ const { messageIds } = await transport.send(target, text);
69
+ return { messageIds };
70
+ },
71
+ inviteMembers(input) {
72
+ return transport.inviteMembers(input);
73
+ },
74
+ addReaction(messageId, emoji) {
75
+ return transport.addReaction(messageId, emoji);
76
+ },
77
+ removeReaction(messageId, reactionId) {
78
+ return transport.removeReaction(messageId, reactionId);
79
+ },
80
+ fetchMessageResource(request) {
81
+ return transport.fetchMessageResource(request);
82
+ },
83
+ close() {
84
+ return transport.close();
85
+ },
86
+ };
87
+ }
88
+ export function channelOutboundToFeishuTarget(target) {
89
+ return {
90
+ chatId: target.conversationId,
91
+ ...(target.replyTo !== undefined
92
+ ? { replyToMessageId: target.replyTo }
93
+ : {}),
94
+ ...(target.mentionUsers !== undefined
95
+ ? { mentionUserIds: target.mentionUsers }
96
+ : {}),
97
+ ...(target.conversationKey !== undefined
98
+ ? { conversationKey: target.conversationKey }
99
+ : {}),
100
+ };
101
+ }
102
+ /**
103
+ * Reshape a raw `im.message.receive_v1` payload into a `FeishuInboundEvent`,
104
+ * using the core's `parseInbound` + `narrowMetaFromEvent` + `toChannelInbound`
105
+ * for content flattening and event-envelope metadata. Returns `null` for a
106
+ * payload missing the message_id or chat_id that make it routable.
107
+ */
108
+ function normalizeInboundEvent(raw) {
109
+ if (!raw || typeof raw !== 'object')
110
+ return null;
111
+ const root = raw;
112
+ const event = (root['event'] ?? root);
113
+ const message = (event['message'] ?? {});
114
+ const messageType = message['message_type'] ?? '';
115
+ const rawContent = message['content'] ?? '';
116
+ const mentions = message['mentions'] ?? [];
117
+ const parsed = parseInbound({
118
+ message_type: messageType,
119
+ content: rawContent,
120
+ mentions,
121
+ });
122
+ const payload = toChannelInbound({
123
+ ...parsed,
124
+ meta: narrowMetaFromEvent(raw),
125
+ });
126
+ const messageId = payload.meta['message_id'] ?? '';
127
+ const chatId = payload.meta['chat_id'] ?? '';
128
+ const chatType = payload.meta['chat_type'] ?? '';
129
+ const senderId = payload.meta['sender_id'] ?? '';
130
+ const senderUnionId = payload.meta['sender_union_id'] ?? '';
131
+ const senderType = payload.meta['sender_type'] ?? '';
132
+ const createTime = payload.meta['create_time'] ?? '';
133
+ const senderName = extractSenderName(raw);
134
+ if (messageId === '' || chatId === '')
135
+ return null;
136
+ return {
137
+ messageId,
138
+ chatId,
139
+ chatType,
140
+ senderId,
141
+ ...(senderUnionId !== '' ? { senderUnionId } : {}),
142
+ senderType,
143
+ senderName,
144
+ messageType,
145
+ rawContent,
146
+ parsedText: payload.text,
147
+ resources: parsed.resources ?? [],
148
+ mentions,
149
+ createTime,
150
+ raw,
151
+ };
152
+ }
153
+ function extractSenderName(raw) {
154
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
155
+ return '';
156
+ const root = raw;
157
+ const event = asRecord(root['event']) ?? root;
158
+ const sender = asRecord(event['sender']);
159
+ if (sender === undefined)
160
+ return '';
161
+ return firstString(sender['sender_name'], sender['display_name'], sender['name'], sender['user_name']);
162
+ }
163
+ function firstString(...values) {
164
+ for (const value of values) {
165
+ if (typeof value === 'string')
166
+ return value;
167
+ }
168
+ return '';
169
+ }
170
+ function asRecord(value) {
171
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
172
+ ? value
173
+ : undefined;
174
+ }
175
+ export function createFakeFeishuBot(appId = 'fake-bot') {
176
+ const sent = [];
177
+ let routes = null;
178
+ let nextMessageId = 1;
179
+ let nextReactionId = 1;
180
+ let sendError = null;
181
+ let reactionError = null;
182
+ let removeReactionError = null;
183
+ const messageResources = new Map();
184
+ const openId = `fake-open-id-${appId}`;
185
+ const reactions = [];
186
+ const removedReactions = [];
187
+ const reactionOps = [];
188
+ return {
189
+ appId,
190
+ get botOpenId() {
191
+ return openId;
192
+ },
193
+ async start(r) {
194
+ routes = r;
195
+ },
196
+ async send(target, text) {
197
+ if (sendError !== null) {
198
+ throw sendError;
199
+ }
200
+ const id = `message-fake-${nextMessageId++}`;
201
+ sent.push({ chatId: target.chatId, target, text, messageIds: [id] });
202
+ return { messageIds: [id] };
203
+ },
204
+ async inviteMembers(input) {
205
+ return { addedOpenIds: input.userOpenIds };
206
+ },
207
+ async addReaction(messageId, emoji) {
208
+ if (reactionError !== null) {
209
+ throw reactionError;
210
+ }
211
+ const reactionId = `reaction-fake-${nextReactionId++}`;
212
+ reactions.push({ messageId, emoji, reactionId });
213
+ reactionOps.push({ op: 'add', messageId, emoji, reactionId });
214
+ return reactionId;
215
+ },
216
+ async removeReaction(messageId, reactionId) {
217
+ if (removeReactionError !== null) {
218
+ throw removeReactionError;
219
+ }
220
+ removedReactions.push({ messageId, reactionId });
221
+ reactionOps.push({ op: 'remove', messageId, reactionId });
222
+ },
223
+ async fetchMessageResource(request) {
224
+ const resource = messageResources.get(request.fileKey);
225
+ if (resource === undefined) {
226
+ throw new Error(`no fake Feishu resource for key ${request.fileKey}`);
227
+ }
228
+ if (resource instanceof Error)
229
+ throw resource;
230
+ return resource;
231
+ },
232
+ async close() {
233
+ routes = null;
234
+ },
235
+ get sentMessages() {
236
+ return sent;
237
+ },
238
+ get reactions() {
239
+ return reactions;
240
+ },
241
+ get removedReactions() {
242
+ return removedReactions;
243
+ },
244
+ get reactionOps() {
245
+ return reactionOps;
246
+ },
247
+ async inject(event) {
248
+ if (routes === null)
249
+ throw new Error('fake bot not started');
250
+ await routes.onMessage(event);
251
+ },
252
+ async injectBotMemberAdded(event) {
253
+ if (routes === null)
254
+ throw new Error('fake bot not started');
255
+ await routes.onBotMemberAdded?.(event);
256
+ },
257
+ setSendError(err) {
258
+ sendError = err;
259
+ },
260
+ setReactionError(err) {
261
+ reactionError = err;
262
+ },
263
+ setRemoveReactionError(err) {
264
+ removeReactionError = err;
265
+ },
266
+ setMessageResource(fileKey, resource) {
267
+ if (resource === null) {
268
+ messageResources.delete(fileKey);
269
+ }
270
+ else {
271
+ messageResources.set(fileKey, resource);
272
+ }
273
+ },
274
+ };
275
+ }
276
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Per-dispatcher peer-bot awareness/trust store.
3
+ *
4
+ * Two sets are tracked separately, per chat_id, and they must never be
5
+ * conflated (issue #62 hard contract):
6
+ *
7
+ * - `known` — bots passively observed sending messages in an authorized
8
+ * chat. Awareness only; observing a bot NEVER grants it trust.
9
+ * - `trusted` — bots introduced by an allowlisted sender running `/introduce`.
10
+ * The gate consults this set (and only this set) when deciding
11
+ * whether a peer bot's group message may be delivered.
12
+ *
13
+ * `baseline` bookkeeping records `im.chat.member.bot.added_v1` events so the
14
+ * host can later inject a one-shot "bots in this group" context; it is
15
+ * idempotent by Feishu event id. (The one-shot context injection itself is a
16
+ * follow-up; this store keeps the durable bookkeeping the contract needs.)
17
+ *
18
+ * One JSON file per dispatcher, keyed by chat_id, so no chat_id ever has to be
19
+ * sanitized into a path segment. Owner-only (0600); writes are atomic.
20
+ */
21
+ export interface ChatBotsEntry {
22
+ /** Passively observed peer-bot open_ids — awareness only, never trust. */
23
+ known: string[];
24
+ /** Introduced peer-bot open_ids — the gate's trust set for this chat. */
25
+ trusted: string[];
26
+ /** Best-effort open_id → display name map for known/trusted bots. */
27
+ names: Record<string, string>;
28
+ /**
29
+ * Set when this chat has pending discovery context to inject once (a bot was
30
+ * added, or an `/introduce` newly trusted a bot). Consumed by the next
31
+ * delivered group message; see `pendingBaseline` / `clearBaselineIfCurrent`.
32
+ */
33
+ needsBaseline: boolean;
34
+ /**
35
+ * Monotonic counter bumped every time `needsBaseline` is (re)set. The deliver
36
+ * path snapshots it before enqueue and only clears the flag if it is still
37
+ * current, so a newer `/introduce` / bot-added event arriving mid-enqueue is
38
+ * not clobbered by a stale clear (issue #69, generation-safe one-shot clear).
39
+ */
40
+ baselineGeneration: number;
41
+ /** Recent bot-added event ids, for idempotent member-event handling. */
42
+ seenEventIds: string[];
43
+ }
44
+ export interface ChatBotsState {
45
+ version: 1;
46
+ chats: Record<string, ChatBotsEntry>;
47
+ }
48
+ export interface PeerBot {
49
+ openId: string;
50
+ name?: string;
51
+ }
52
+ /** The pending one-shot discovery context for one chat (issue #69). */
53
+ export interface PendingBaseline {
54
+ /** Whether this chat has discovery context waiting to be injected. */
55
+ needsBaseline: boolean;
56
+ /** Snapshot of the entry's generation, for a generation-safe clear. */
57
+ generation: number;
58
+ /** The chat's trusted peer bots (open_id + best-effort name). */
59
+ trusted: PeerBot[];
60
+ }
61
+ /** Known and trusted peer bots for one chat, for the `list_chat_bots` tool. */
62
+ export interface ChatBotsListing {
63
+ known: PeerBot[];
64
+ trusted: PeerBot[];
65
+ }
66
+ export declare function defaultChatBotsState(): ChatBotsState;
67
+ /**
68
+ * Load the store. Unlike the access store, a corrupt or unreadable chat-bots
69
+ * file is not security-critical — it only affects peer-bot discovery — so a
70
+ * load failure degrades to an empty store rather than throwing.
71
+ */
72
+ export declare function loadChatBots(stateDir: string): Promise<ChatBotsState>;
73
+ export declare function saveChatBots(stateDir: string, state: ChatBotsState): Promise<void>;
74
+ /** Record a passively observed peer bot as *known* (awareness only). */
75
+ export declare function observeKnownBot(stateDir: string, chatId: string, bot: PeerBot): Promise<void>;
76
+ /**
77
+ * Record peer bots introduced by an allowlisted `/introduce` as *trusted*.
78
+ * Trusted bots are also known. Returns the open_ids newly added to trust.
79
+ */
80
+ export declare function trustIntroducedBots(stateDir: string, chatId: string, bots: PeerBot[]): Promise<string[]>;
81
+ /**
82
+ * The chat's pending one-shot discovery context, snapshotted for a
83
+ * generation-safe clear (issue #69). The deliver path reads this before
84
+ * enqueue, injects the trusted bots when `needsBaseline`, and clears via
85
+ * `clearBaselineIfCurrent` only after a successful submission.
86
+ */
87
+ export declare function pendingBaseline(stateDir: string, chatId: string): Promise<PendingBaseline>;
88
+ /**
89
+ * Clear the pending-context flag only if the chat's generation still matches
90
+ * the snapshot taken before enqueue. A newer `/introduce` / bot-added event
91
+ * that arrived mid-enqueue bumps the generation, so this no-ops rather than
92
+ * dropping the newer pending context (issue #69).
93
+ */
94
+ export declare function clearBaselineIfCurrent(stateDir: string, chatId: string, generation: number): Promise<void>;
95
+ /** Known and trusted peer bots for one chat (the `list_chat_bots` tool). */
96
+ export declare function listChatBots(stateDir: string, chatId: string): Promise<ChatBotsListing>;
97
+ /** The trust set the gate consults for one chat. */
98
+ export declare function trustedBotIds(stateDir: string, chatId: string): Promise<Set<string>>;
99
+ /**
100
+ * Record an `im.chat.member.bot.added_v1` event: mark the chat as needing a
101
+ * baseline injection. Idempotent by event id — a redelivered event is a no-op.
102
+ * Returns true when the event was newly recorded.
103
+ */
104
+ export declare function recordBotAdded(stateDir: string, chatId: string, eventId: string): Promise<boolean>;
105
+ //# sourceMappingURL=chat-bots-store.d.ts.map