@ganglion/weacpx-channel-feishu 0.2.2 → 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/index.js +68 -34
- 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/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 {
|
|
@@ -80285,6 +80278,9 @@ class FeishuChannel {
|
|
|
80285
80278
|
agent = null;
|
|
80286
80279
|
quota = null;
|
|
80287
80280
|
logger = null;
|
|
80281
|
+
sessions = null;
|
|
80282
|
+
activeTurns = null;
|
|
80283
|
+
executor = createConversationExecutor();
|
|
80288
80284
|
activeTasks = new Map;
|
|
80289
80285
|
permissionNotifier;
|
|
80290
80286
|
config;
|
|
@@ -80320,6 +80316,8 @@ class FeishuChannel {
|
|
|
80320
80316
|
this.agent = input.agent;
|
|
80321
80317
|
this.quota = input.quota;
|
|
80322
80318
|
this.logger = input.logger;
|
|
80319
|
+
this.sessions = input.sessions ?? null;
|
|
80320
|
+
this.activeTurns = input.activeTurns ?? null;
|
|
80323
80321
|
const eligible = this.config.accounts.filter((account) => account.enabled && account.configured);
|
|
80324
80322
|
await input.logger.info("feishu.start", "starting feishu channel", {
|
|
80325
80323
|
accountCount: eligible.length,
|
|
@@ -80552,33 +80550,34 @@ class FeishuChannel {
|
|
|
80552
80550
|
initialSkipped: converted.skippedNotes
|
|
80553
80551
|
});
|
|
80554
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);
|
|
80555
80556
|
const { active, abortController } = this.registerActiveTask({
|
|
80556
80557
|
accountId,
|
|
80557
80558
|
chatId,
|
|
80558
80559
|
messageId,
|
|
80559
80560
|
queueKey,
|
|
80560
80561
|
senderOpenId: event.sender?.sender_id?.open_id,
|
|
80561
|
-
chatType: event.message.chat_type
|
|
80562
|
+
chatType: event.message.chat_type,
|
|
80563
|
+
boundAlias
|
|
80562
80564
|
});
|
|
80563
|
-
|
|
80565
|
+
if (boundAlias)
|
|
80566
|
+
this.activeTurns?.markActive(chatKey, boundAlias);
|
|
80567
|
+
await this.executor.run(chatKey, lane, () => this.runTurn({
|
|
80568
|
+
runtime,
|
|
80564
80569
|
accountId,
|
|
80565
80570
|
chatId,
|
|
80566
|
-
|
|
80567
|
-
|
|
80568
|
-
|
|
80569
|
-
|
|
80570
|
-
|
|
80571
|
-
|
|
80572
|
-
|
|
80573
|
-
|
|
80574
|
-
|
|
80575
|
-
|
|
80576
|
-
media,
|
|
80577
|
-
active,
|
|
80578
|
-
abortController
|
|
80579
|
-
})
|
|
80580
|
-
});
|
|
80581
|
-
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);
|
|
80582
80581
|
}
|
|
80583
80582
|
async tryHandleAbortTrigger(input) {
|
|
80584
80583
|
const { event, runtime, queueKey, accountId, chatId, messageId } = input;
|
|
@@ -80613,7 +80612,7 @@ class FeishuChannel {
|
|
|
80613
80612
|
return false;
|
|
80614
80613
|
}
|
|
80615
80614
|
registerActiveTask(input) {
|
|
80616
|
-
const { accountId, chatId, messageId, queueKey, senderOpenId, chatType } = input;
|
|
80615
|
+
const { accountId, chatId, messageId, queueKey, senderOpenId, chatType, boundAlias } = input;
|
|
80617
80616
|
const abortController = new AbortController;
|
|
80618
80617
|
const active = {
|
|
80619
80618
|
accountId,
|
|
@@ -80621,6 +80620,7 @@ class FeishuChannel {
|
|
|
80621
80620
|
messageId,
|
|
80622
80621
|
senderOpenId,
|
|
80623
80622
|
chatType,
|
|
80623
|
+
boundAlias,
|
|
80624
80624
|
typingState: { messageId, reactionId: null },
|
|
80625
80625
|
abortController,
|
|
80626
80626
|
suppressed: false,
|
|
@@ -80631,8 +80631,26 @@ class FeishuChannel {
|
|
|
80631
80631
|
this.activeTasks.set(queueKey, stack);
|
|
80632
80632
|
return { active, abortController };
|
|
80633
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
|
+
}
|
|
80634
80651
|
async runTurn(input) {
|
|
80635
|
-
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";
|
|
80636
80654
|
try {
|
|
80637
80655
|
if (!this.agent)
|
|
80638
80656
|
return;
|
|
@@ -80679,7 +80697,10 @@ class FeishuChannel {
|
|
|
80679
80697
|
text: requestText,
|
|
80680
80698
|
...media.length > 0 ? { media } : {},
|
|
80681
80699
|
replyContextToken: messageId,
|
|
80682
|
-
metadata:
|
|
80700
|
+
metadata: {
|
|
80701
|
+
...buildFeishuRouteMetadata({ chatType, senderOpenId: active.senderOpenId, chatId }),
|
|
80702
|
+
...boundAlias ? { boundSessionAlias: boundAlias } : {}
|
|
80703
|
+
},
|
|
80683
80704
|
reply: safeReply,
|
|
80684
80705
|
...active.cardController ? {
|
|
80685
80706
|
onToolEvent: (event) => {
|
|
@@ -80698,13 +80719,26 @@ class FeishuChannel {
|
|
|
80698
80719
|
if (active.suppressed)
|
|
80699
80720
|
return;
|
|
80700
80721
|
await this.deliverResponse({ runtime, accountId, chatId, messageId, active, response });
|
|
80722
|
+
turnStatus = "done";
|
|
80701
80723
|
} catch (error) {
|
|
80702
80724
|
if (active.cardController && !active.cardController.isTerminated()) {
|
|
80703
80725
|
await active.cardController.fail(error instanceof Error ? error.message : String(error));
|
|
80704
80726
|
}
|
|
80727
|
+
turnStatus = "error";
|
|
80705
80728
|
throw error;
|
|
80706
80729
|
}
|
|
80707
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
|
+
}
|
|
80708
80742
|
const stack = this.activeTasks.get(queueKey);
|
|
80709
80743
|
if (stack) {
|
|
80710
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": {
|