@ganglion/weacpx-channel-feishu 0.2.1 → 0.3.0
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/README.md +17 -0
- package/dist/channel.d.ts +4 -0
- package/dist/completion-notice.d.ts +1 -0
- package/dist/inbound.d.ts +18 -0
- package/dist/index.js +77 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,3 +72,20 @@ While the agent is processing, the user can send `stop`, `/stop`, `abort`, `停
|
|
|
72
72
|
2. Renders an "已停止" final state on the streaming card, or sends a "已停止当前任务。" reply in static mode.
|
|
73
73
|
3. Removes the typing reaction added to the user's original message.
|
|
74
74
|
|
|
75
|
+
## Real-time session switching & background execution
|
|
76
|
+
|
|
77
|
+
Each inbound prompt is **bound at dispatch time** to whatever session the chat is currently on, then runs on a **per-session lane**:
|
|
78
|
+
|
|
79
|
+
- **Different sessions run concurrently.** Switching to another session (`/use`/`/ss`) while a task is in flight lets you use the new session immediately — turns on different sessions don't block each other.
|
|
80
|
+
- **Same-session turns serialize**, preserving order within a session.
|
|
81
|
+
- **Switch and cancel commands preempt.** `/use`, `/ss`, `/cancel`, `/stop` run on a **control lane**, so they take effect right away even while a prompt is running (the running prompt keeps going in the background — see below).
|
|
82
|
+
|
|
83
|
+
When you switch away from a running session, its turn keeps executing in the background. Feishu uses **"B-semantics"**, which differs from the WeChat channel:
|
|
84
|
+
|
|
85
|
+
- The backgrounded session has its **own streaming card** that keeps refreshing **to completion in the chat timeline** — it is *not* gated/suppressed. The result simply stays on that card.
|
|
86
|
+
- On completion, a short ping is sent to the chat: `✅ <alias> 已完成` (or `⚠️ <alias> 失败`). Unlike WeChat, there is **no `/use 查看结果`** suffix — there is nothing to replay, because the card already holds the result.
|
|
87
|
+
- Switching **back** to that session does **not** re-send the result.
|
|
88
|
+
- `/sessions` marks sessions with an unfinished/unread background completion using `●`.
|
|
89
|
+
|
|
90
|
+
`/cancel <alias>` (and `/stop <alias>`) target a specific session's in-flight turn by alias — fuzzy alias resolution applies, the same as `/use`.
|
|
91
|
+
|
package/dist/channel.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export declare class FeishuChannel implements MessageChannelRuntime {
|
|
|
15
15
|
private agent;
|
|
16
16
|
private quota;
|
|
17
17
|
private logger;
|
|
18
|
+
private sessions;
|
|
19
|
+
private activeTurns;
|
|
20
|
+
private readonly executor;
|
|
18
21
|
private readonly activeTasks;
|
|
19
22
|
private readonly permissionNotifier;
|
|
20
23
|
private readonly config;
|
|
@@ -50,6 +53,7 @@ export declare class FeishuChannel implements MessageChannelRuntime {
|
|
|
50
53
|
* mark it suppressed.
|
|
51
54
|
*/
|
|
52
55
|
private registerActiveTask;
|
|
56
|
+
private sendBackgroundCompletionNotice;
|
|
53
57
|
private runTurn;
|
|
54
58
|
private trySeedStreamingCard;
|
|
55
59
|
private deliverResponse;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildFeishuCompletionNotice(displayAlias: string, status: "done" | "error"): string;
|
package/dist/inbound.d.ts
CHANGED
|
@@ -7,6 +7,24 @@ export declare function parseFeishuConversationId(chatKey: string): {
|
|
|
7
7
|
chatId: string;
|
|
8
8
|
threadId?: string;
|
|
9
9
|
} | null;
|
|
10
|
+
/**
|
|
11
|
+
* Builds the chat-route metadata weacpx records for the current coordinator
|
|
12
|
+
* session. The host requires `chatType` to be `"direct"` or `"group"`, but
|
|
13
|
+
* Feishu reports `chat_type` as `"p2p"` (direct) or `"group"`, so `"p2p"` and
|
|
14
|
+
* any unexpected value normalize to `"direct"`. Without this, interactive
|
|
15
|
+
* Feishu turns recorded a route with no `chatType`, which blocked the in-session
|
|
16
|
+
* scheduled_create/list/cancel tools and group-owner command authorization.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildFeishuRouteMetadata(input: {
|
|
19
|
+
chatType: string | undefined;
|
|
20
|
+
senderOpenId?: string;
|
|
21
|
+
chatId: string;
|
|
22
|
+
}): {
|
|
23
|
+
channel: "feishu";
|
|
24
|
+
chatType: "direct" | "group";
|
|
25
|
+
senderId?: string;
|
|
26
|
+
groupId?: string;
|
|
27
|
+
};
|
|
10
28
|
export declare function shouldHandleFeishuMessage(input: {
|
|
11
29
|
event: FeishuMessageEvent;
|
|
12
30
|
botOpenId?: string;
|
package/dist/index.js
CHANGED
|
@@ -77967,6 +77967,7 @@ var require_lib2 = __commonJS((exports) => {
|
|
|
77967
77967
|
|
|
77968
77968
|
// packages/channel-feishu/src/channel.ts
|
|
77969
77969
|
import path5 from "node:path";
|
|
77970
|
+
import { createConversationExecutor, resolveTurnLane, toDisplaySessionAlias } from "weacpx/plugin-api";
|
|
77970
77971
|
|
|
77971
77972
|
// packages/channel-feishu/src/tuning.ts
|
|
77972
77973
|
var DEFAULT_FEISHU_TUNING = {
|
|
@@ -78269,19 +78270,6 @@ var queues = new Map;
|
|
|
78269
78270
|
function buildFeishuQueueKey(accountId, chatId, threadId) {
|
|
78270
78271
|
return threadId ? `${accountId}:${chatId}:thread:${threadId}` : `${accountId}:${chatId}`;
|
|
78271
78272
|
}
|
|
78272
|
-
function enqueueFeishuChatTask(input) {
|
|
78273
|
-
const key = buildFeishuQueueKey(input.accountId, input.chatId, input.threadId);
|
|
78274
|
-
const previous = queues.get(key) ?? Promise.resolve();
|
|
78275
|
-
const status = queues.has(key) ? "queued" : "immediate";
|
|
78276
|
-
const promise = previous.then(input.task, input.task);
|
|
78277
|
-
queues.set(key, promise);
|
|
78278
|
-
const cleanup = () => {
|
|
78279
|
-
if (queues.get(key) === promise)
|
|
78280
|
-
queues.delete(key);
|
|
78281
|
-
};
|
|
78282
|
-
promise.then(cleanup, cleanup);
|
|
78283
|
-
return { status, promise };
|
|
78284
|
-
}
|
|
78285
78273
|
function clearFeishuQueueForAccount(accountId) {
|
|
78286
78274
|
const prefix = `${accountId}:`;
|
|
78287
78275
|
for (const key of queues.keys()) {
|
|
@@ -78290,6 +78278,11 @@ function clearFeishuQueueForAccount(accountId) {
|
|
|
78290
78278
|
}
|
|
78291
78279
|
}
|
|
78292
78280
|
|
|
78281
|
+
// packages/channel-feishu/src/completion-notice.ts
|
|
78282
|
+
function buildFeishuCompletionNotice(displayAlias, status) {
|
|
78283
|
+
return status === "done" ? `✅ ${displayAlias} 已完成` : `⚠️ ${displayAlias} 失败`;
|
|
78284
|
+
}
|
|
78285
|
+
|
|
78293
78286
|
// packages/channel-feishu/src/inbound.ts
|
|
78294
78287
|
function parseFeishuText(raw) {
|
|
78295
78288
|
try {
|
|
@@ -78312,6 +78305,15 @@ function parseFeishuConversationId(chatKey) {
|
|
|
78312
78305
|
}
|
|
78313
78306
|
return { accountId: parts[1], chatId: parts[2] };
|
|
78314
78307
|
}
|
|
78308
|
+
function buildFeishuRouteMetadata(input) {
|
|
78309
|
+
const isGroup = input.chatType === "group";
|
|
78310
|
+
return {
|
|
78311
|
+
channel: "feishu",
|
|
78312
|
+
chatType: isGroup ? "group" : "direct",
|
|
78313
|
+
...input.senderOpenId ? { senderId: input.senderOpenId } : {},
|
|
78314
|
+
...isGroup ? { groupId: input.chatId } : {}
|
|
78315
|
+
};
|
|
78316
|
+
}
|
|
78315
78317
|
function shouldHandleFeishuMessage(input) {
|
|
78316
78318
|
const text = input.parsedText ?? parseFeishuText(input.event.message.content);
|
|
78317
78319
|
if (input.event.message.message_type !== "text" && !input.allowMediaOnly && text.trim().length === 0) {
|
|
@@ -80276,6 +80278,9 @@ class FeishuChannel {
|
|
|
80276
80278
|
agent = null;
|
|
80277
80279
|
quota = null;
|
|
80278
80280
|
logger = null;
|
|
80281
|
+
sessions = null;
|
|
80282
|
+
activeTurns = null;
|
|
80283
|
+
executor = createConversationExecutor();
|
|
80279
80284
|
activeTasks = new Map;
|
|
80280
80285
|
permissionNotifier;
|
|
80281
80286
|
config;
|
|
@@ -80311,6 +80316,8 @@ class FeishuChannel {
|
|
|
80311
80316
|
this.agent = input.agent;
|
|
80312
80317
|
this.quota = input.quota;
|
|
80313
80318
|
this.logger = input.logger;
|
|
80319
|
+
this.sessions = input.sessions ?? null;
|
|
80320
|
+
this.activeTurns = input.activeTurns ?? null;
|
|
80314
80321
|
const eligible = this.config.accounts.filter((account) => account.enabled && account.configured);
|
|
80315
80322
|
await input.logger.info("feishu.start", "starting feishu channel", {
|
|
80316
80323
|
accountCount: eligible.length,
|
|
@@ -80543,33 +80550,34 @@ class FeishuChannel {
|
|
|
80543
80550
|
initialSkipped: converted.skippedNotes
|
|
80544
80551
|
});
|
|
80545
80552
|
const requestText = appendSkippedAttachmentNotes(decision.text, skipped);
|
|
80553
|
+
const isSlash = requestText.trim().startsWith("/");
|
|
80554
|
+
const boundAlias = isSlash ? undefined : this.sessions?.peekCurrentSessionAlias(chatKey) ?? undefined;
|
|
80555
|
+
const lane = resolveTurnLane(requestText);
|
|
80546
80556
|
const { active, abortController } = this.registerActiveTask({
|
|
80547
80557
|
accountId,
|
|
80548
80558
|
chatId,
|
|
80549
80559
|
messageId,
|
|
80550
80560
|
queueKey,
|
|
80551
80561
|
senderOpenId: event.sender?.sender_id?.open_id,
|
|
80552
|
-
chatType: event.message.chat_type
|
|
80562
|
+
chatType: event.message.chat_type,
|
|
80563
|
+
boundAlias
|
|
80553
80564
|
});
|
|
80554
|
-
|
|
80565
|
+
if (boundAlias)
|
|
80566
|
+
this.activeTurns?.markActive(chatKey, boundAlias);
|
|
80567
|
+
await this.executor.run(chatKey, lane, () => this.runTurn({
|
|
80568
|
+
runtime,
|
|
80555
80569
|
accountId,
|
|
80556
80570
|
chatId,
|
|
80557
|
-
|
|
80558
|
-
|
|
80559
|
-
|
|
80560
|
-
|
|
80561
|
-
|
|
80562
|
-
|
|
80563
|
-
|
|
80564
|
-
|
|
80565
|
-
|
|
80566
|
-
|
|
80567
|
-
media,
|
|
80568
|
-
active,
|
|
80569
|
-
abortController
|
|
80570
|
-
})
|
|
80571
|
-
});
|
|
80572
|
-
await run.promise;
|
|
80571
|
+
chatType: event.message.chat_type,
|
|
80572
|
+
chatKey,
|
|
80573
|
+
queueKey,
|
|
80574
|
+
messageId,
|
|
80575
|
+
requestText,
|
|
80576
|
+
media,
|
|
80577
|
+
active,
|
|
80578
|
+
abortController,
|
|
80579
|
+
boundAlias
|
|
80580
|
+
}), boundAlias);
|
|
80573
80581
|
}
|
|
80574
80582
|
async tryHandleAbortTrigger(input) {
|
|
80575
80583
|
const { event, runtime, queueKey, accountId, chatId, messageId } = input;
|
|
@@ -80604,7 +80612,7 @@ class FeishuChannel {
|
|
|
80604
80612
|
return false;
|
|
80605
80613
|
}
|
|
80606
80614
|
registerActiveTask(input) {
|
|
80607
|
-
const { accountId, chatId, messageId, queueKey, senderOpenId, chatType } = input;
|
|
80615
|
+
const { accountId, chatId, messageId, queueKey, senderOpenId, chatType, boundAlias } = input;
|
|
80608
80616
|
const abortController = new AbortController;
|
|
80609
80617
|
const active = {
|
|
80610
80618
|
accountId,
|
|
@@ -80612,6 +80620,7 @@ class FeishuChannel {
|
|
|
80612
80620
|
messageId,
|
|
80613
80621
|
senderOpenId,
|
|
80614
80622
|
chatType,
|
|
80623
|
+
boundAlias,
|
|
80615
80624
|
typingState: { messageId, reactionId: null },
|
|
80616
80625
|
abortController,
|
|
80617
80626
|
suppressed: false,
|
|
@@ -80622,8 +80631,26 @@ class FeishuChannel {
|
|
|
80622
80631
|
this.activeTasks.set(queueKey, stack);
|
|
80623
80632
|
return { active, abortController };
|
|
80624
80633
|
}
|
|
80634
|
+
async sendBackgroundCompletionNotice(input) {
|
|
80635
|
+
const text = buildFeishuCompletionNotice(toDisplaySessionAlias(input.boundAlias), input.status);
|
|
80636
|
+
try {
|
|
80637
|
+
await this.sendReplyWithGuard({
|
|
80638
|
+
runtime: input.runtime,
|
|
80639
|
+
chatId: input.chatId,
|
|
80640
|
+
replyToMessageId: input.messageId,
|
|
80641
|
+
text
|
|
80642
|
+
});
|
|
80643
|
+
} catch (error) {
|
|
80644
|
+
await this.logger?.error("feishu.bg_notice.failed", "failed to send background completion notice", {
|
|
80645
|
+
chatId: input.chatId,
|
|
80646
|
+
boundAlias: input.boundAlias,
|
|
80647
|
+
message: error instanceof Error ? error.message : String(error)
|
|
80648
|
+
});
|
|
80649
|
+
}
|
|
80650
|
+
}
|
|
80625
80651
|
async runTurn(input) {
|
|
80626
|
-
const { runtime, accountId, chatId, chatType, chatKey, queueKey, messageId, requestText, media, active, abortController } = input;
|
|
80652
|
+
const { runtime, accountId, chatId, chatType, chatKey, queueKey, messageId, requestText, media, active, abortController, boundAlias } = input;
|
|
80653
|
+
let turnStatus = "skipped";
|
|
80627
80654
|
try {
|
|
80628
80655
|
if (!this.agent)
|
|
80629
80656
|
return;
|
|
@@ -80670,6 +80697,10 @@ class FeishuChannel {
|
|
|
80670
80697
|
text: requestText,
|
|
80671
80698
|
...media.length > 0 ? { media } : {},
|
|
80672
80699
|
replyContextToken: messageId,
|
|
80700
|
+
metadata: {
|
|
80701
|
+
...buildFeishuRouteMetadata({ chatType, senderOpenId: active.senderOpenId, chatId }),
|
|
80702
|
+
...boundAlias ? { boundSessionAlias: boundAlias } : {}
|
|
80703
|
+
},
|
|
80673
80704
|
reply: safeReply,
|
|
80674
80705
|
...active.cardController ? {
|
|
80675
80706
|
onToolEvent: (event) => {
|
|
@@ -80688,13 +80719,26 @@ class FeishuChannel {
|
|
|
80688
80719
|
if (active.suppressed)
|
|
80689
80720
|
return;
|
|
80690
80721
|
await this.deliverResponse({ runtime, accountId, chatId, messageId, active, response });
|
|
80722
|
+
turnStatus = "done";
|
|
80691
80723
|
} catch (error) {
|
|
80692
80724
|
if (active.cardController && !active.cardController.isTerminated()) {
|
|
80693
80725
|
await active.cardController.fail(error instanceof Error ? error.message : String(error));
|
|
80694
80726
|
}
|
|
80727
|
+
turnStatus = "error";
|
|
80695
80728
|
throw error;
|
|
80696
80729
|
}
|
|
80697
80730
|
} finally {
|
|
80731
|
+
if (boundAlias) {
|
|
80732
|
+
this.activeTurns?.markInactive(chatKey, boundAlias);
|
|
80733
|
+
if (turnStatus !== "skipped" && this.sessions && this.sessions.peekCurrentSessionAlias(chatKey) !== boundAlias) {
|
|
80734
|
+
await this.sessions.setBackgroundResult(chatKey, boundAlias, {
|
|
80735
|
+
text: "",
|
|
80736
|
+
status: turnStatus,
|
|
80737
|
+
finished_at: new Date().toISOString()
|
|
80738
|
+
});
|
|
80739
|
+
await this.sendBackgroundCompletionNotice({ runtime, chatId, messageId, boundAlias, status: turnStatus });
|
|
80740
|
+
}
|
|
80741
|
+
}
|
|
80698
80742
|
const stack = this.activeTasks.get(queueKey);
|
|
80699
80743
|
if (stack) {
|
|
80700
80744
|
const i = stack.indexOf(active);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ganglion/weacpx-channel-feishu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Feishu channel plugin for weacpx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": ["weacpx", "feishu", "channel", "plugin"],
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": ["dist", "README.md"],
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"weacpx": ">=0.
|
|
23
|
+
"weacpx": ">=0.7.0-0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependenciesMeta": {
|
|
26
26
|
"weacpx": {
|