@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.
- package/LICENSE +21 -0
- package/README.md +51 -16
- package/dist/bot.d.ts +151 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +276 -0
- package/dist/bot.js.map +1 -0
- package/dist/chat-bots-store.d.ts +105 -0
- package/dist/chat-bots-store.d.ts.map +1 -0
- package/dist/chat-bots-store.js +245 -0
- package/dist/chat-bots-store.js.map +1 -0
- package/dist/feishu-channel.d.ts +95 -0
- package/dist/feishu-channel.d.ts.map +1 -0
- package/dist/feishu-channel.js +407 -0
- package/dist/feishu-channel.js.map +1 -0
- package/dist/feishu-gate.d.ts +82 -0
- package/dist/feishu-gate.d.ts.map +1 -0
- package/dist/feishu-gate.js +259 -0
- package/dist/feishu-gate.js.map +1 -0
- package/dist/feishu-mcp-tools.d.ts +39 -0
- package/dist/feishu-mcp-tools.d.ts.map +1 -0
- package/dist/feishu-mcp-tools.js +153 -0
- package/dist/feishu-mcp-tools.js.map +1 -0
- package/dist/feishu-message.d.ts +51 -0
- package/dist/feishu-message.d.ts.map +1 -0
- package/dist/feishu-message.js +298 -0
- package/dist/feishu-message.js.map +1 -0
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +5 -5
- package/dist/inbound.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/os.d.ts +11 -0
- package/dist/internal/os.d.ts.map +1 -0
- package/dist/internal/os.js +30 -0
- package/dist/internal/os.js.map +1 -0
- package/dist/introduce.d.ts +101 -0
- package/dist/introduce.d.ts.map +1 -0
- package/dist/introduce.js +183 -0
- package/dist/introduce.js.map +1 -0
- package/dist/provider-ref.d.ts +8 -0
- package/dist/provider-ref.d.ts.map +1 -0
- package/dist/provider-ref.js +8 -0
- package/dist/provider-ref.js.map +1 -0
- package/dist/provider.d.ts +43 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +152 -0
- package/dist/provider.js.map +1 -0
- 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
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
##
|
|
12
|
+
## What it owns
|
|
10
13
|
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
`@excitedjs/
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
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
|
package/dist/bot.js.map
ADDED
|
@@ -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
|