dsh-caduceus 0.1.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -72
- package/README.zh.md +131 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +155 -5
- package/dist/bridge/types.d.ts +22 -0
- package/dist/commands/parse.d.ts +0 -11
- package/dist/commands/parse.js +5 -1
- package/dist/config.d.ts +52 -1
- package/dist/config.js +29 -0
- package/dist/gate.d.ts +3 -1
- package/dist/gate.js +6 -5
- package/dist/index.js +20 -1
- package/dist/interactive/approval.d.ts +9 -0
- package/dist/interactive/approval.js +85 -0
- package/dist/interactive/cards.d.ts +36 -0
- package/dist/interactive/cards.js +125 -0
- package/dist/interactive/pending.d.ts +29 -0
- package/dist/interactive/pending.js +39 -0
- package/dist/interactive/question.d.ts +25 -0
- package/dist/interactive/question.js +86 -0
- package/dist/interactive/runtime.d.ts +44 -0
- package/dist/interactive/runtime.js +147 -0
- package/dist/interactive/types.d.ts +50 -0
- package/dist/interactive/types.js +25 -0
- package/dist/mentions.d.ts +15 -0
- package/dist/mentions.js +21 -0
- package/dist/transport/feishu-api.d.ts +9 -1
- package/dist/transport/lark.d.ts +7 -1
- package/dist/transport/lark.js +34 -2
- package/dist/transport/normalize.d.ts +22 -22
- package/dist/transport/normalize.js +83 -3
- package/dist/transport/reliable.d.ts +5 -1
- package/dist/transport/reliable.js +12 -0
- package/dist/transport/types.d.ts +15 -0
- package/package.json +3 -2
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pure normalization of a Feishu `im.message.receive_v1` payload.
|
|
3
|
+
*
|
|
4
|
+
* The topic identity is the whole point of this module: the bridge keys
|
|
5
|
+
* sessions by `(chatId, threadId)`, and the bot must answer in the same topic
|
|
6
|
+
* (design doc A6). Feishu puts the topic id in `thread_id`; older/related
|
|
7
|
+
* shapes only carry `root_id`, so the rule is:
|
|
8
|
+
*
|
|
9
|
+
* threadId = message.thread_id ?? message.root_id
|
|
10
|
+
*
|
|
11
|
+
* Non-human senders (`sender.sender_type !== 'user'`) are dropped FIRST, before
|
|
12
|
+
* any other judgement: a bot/app event (e.g. an app self-improvement notice)
|
|
13
|
+
* must never become a session prompt or a fallback title. Feishu events from
|
|
14
|
+
* humans carry `sender_type: 'user'`; a missing sender_type is treated as
|
|
15
|
+
* non-human on purpose (fail closed).
|
|
16
|
+
*
|
|
17
|
+
* Mention open ids are preserved so the bridge can enforce the group @ gate.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-caduceus/transport/normalize
|
|
20
|
+
*/
|
|
21
|
+
import { stripLeadingMentions } from '../mentions.js';
|
|
22
|
+
const AT_TAG_PATTERN = /<at[^>]*>(.*?)<\/at>/gs;
|
|
2
23
|
/** Message types the v0.1 surface handles (attachments are v0.2). */
|
|
3
24
|
const SUPPORTED_MESSAGE_TYPES = new Set(['text']);
|
|
4
25
|
/**
|
|
@@ -7,7 +28,7 @@ const SUPPORTED_MESSAGE_TYPES = new Set(['text']);
|
|
|
7
28
|
* @returns the normalized message, or `undefined` for unsupported/malformed
|
|
8
29
|
* payloads or empty text.
|
|
9
30
|
*/
|
|
10
|
-
export function normalizeMessageEvent(raw) {
|
|
31
|
+
export function normalizeMessageEvent(raw, botOpenId) {
|
|
11
32
|
// Priority #1: only human messages may enter a session.
|
|
12
33
|
const senderType = raw.sender?.sender_type;
|
|
13
34
|
if (senderType !== 'user')
|
|
@@ -25,7 +46,44 @@ export function normalizeMessageEvent(raw) {
|
|
|
25
46
|
catch {
|
|
26
47
|
return undefined;
|
|
27
48
|
}
|
|
28
|
-
|
|
49
|
+
// Preserve the display text inside <at> tags first: key -> @name replacement
|
|
50
|
+
// below needs the placeholder text to still be present.
|
|
51
|
+
text = text.replace(AT_TAG_PATTERN, (_match, inner) => ` @${inner.trim().replace(/^@/, '')} `);
|
|
52
|
+
text = text.replace(/\r\n?/g, '\n');
|
|
53
|
+
const rawMentions = message.mentions ?? [];
|
|
54
|
+
const entries = rawMentions
|
|
55
|
+
.map((mention) => ({
|
|
56
|
+
key: mention.key,
|
|
57
|
+
name: mention.name,
|
|
58
|
+
openId: mention.id?.open_id,
|
|
59
|
+
}))
|
|
60
|
+
.filter((entry) => entry.key !== undefined || entry.name !== undefined || entry.openId !== undefined);
|
|
61
|
+
if (entries.length === 0) {
|
|
62
|
+
// No mention metadata: keep the previous behaviour (strip placeholders).
|
|
63
|
+
text = text.trim();
|
|
64
|
+
text = stripLeadingMentions(text).trim();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// 1) Remove the bot's own mention(s) entirely (agent need not see "@self").
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const own = botOpenId !== undefined && botOpenId.length > 0 && entry.openId === botOpenId;
|
|
70
|
+
if (!own)
|
|
71
|
+
continue;
|
|
72
|
+
if (entry.key !== undefined && entry.key.length > 0)
|
|
73
|
+
text = text.split(entry.key).join(' ');
|
|
74
|
+
if (entry.name !== undefined && entry.name.length > 0)
|
|
75
|
+
text = text.split(`@${entry.name}`).join(' ');
|
|
76
|
+
}
|
|
77
|
+
// 2) Replace remaining placeholder keys with @display-name (longest first,
|
|
78
|
+
// so @_user_10 is never clobbered by @_user_1).
|
|
79
|
+
const replaceable = entries
|
|
80
|
+
.filter((entry) => entry.name !== undefined && entry.name.length > 0 && entry.key !== undefined)
|
|
81
|
+
.sort((left, right) => (right.key?.length ?? 0) - (left.key?.length ?? 0));
|
|
82
|
+
for (const entry of replaceable) {
|
|
83
|
+
text = text.split(entry.key).join(`@${entry.name}`);
|
|
84
|
+
}
|
|
85
|
+
text = text.replace(/[ \t]{2,}/g, ' ').trim();
|
|
86
|
+
}
|
|
29
87
|
if (text.length === 0)
|
|
30
88
|
return undefined;
|
|
31
89
|
const threadId = firstNonEmpty(message.thread_id, message.root_id);
|
|
@@ -52,3 +110,25 @@ function firstNonEmpty(...values) {
|
|
|
52
110
|
}
|
|
53
111
|
return undefined;
|
|
54
112
|
}
|
|
113
|
+
/** Normalize Feishu's card click into the transport's CardAction. */
|
|
114
|
+
export function normalizeCardAction(raw) {
|
|
115
|
+
const operatorOpenId = raw.operator?.open_id ?? raw.open_id ?? '';
|
|
116
|
+
const messageId = raw.context?.open_message_id ?? raw.open_message_id ?? '';
|
|
117
|
+
const chatId = raw.context?.open_chat_id ?? raw.open_chat_id ?? '';
|
|
118
|
+
const value = raw.action?.value;
|
|
119
|
+
if (operatorOpenId.length === 0 || messageId.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (typeof value !== 'object' || value === null)
|
|
122
|
+
return undefined;
|
|
123
|
+
const rawForm = raw.action?.form_value;
|
|
124
|
+
const formValue = typeof rawForm === 'object' && rawForm !== null
|
|
125
|
+
? Object.fromEntries(Object.entries(rawForm).filter((entry) => typeof entry[1] === 'string'))
|
|
126
|
+
: undefined;
|
|
127
|
+
return {
|
|
128
|
+
operatorOpenId,
|
|
129
|
+
messageId,
|
|
130
|
+
chatId,
|
|
131
|
+
value,
|
|
132
|
+
...(formValue === undefined || Object.keys(formValue).length === 0 ? {} : { formValue }),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type LoggerLike } from '../logger.js';
|
|
2
2
|
import type { FeishuApi } from './feishu-api.js';
|
|
3
|
-
import type { FeishuCard, InboundMessage, ReplyOptions, ReplyPayload, SendOptions, SentMessage, Transport } from './types.js';
|
|
3
|
+
import type { CardAction, FeishuCard, InboundMessage, ReplyOptions, ReplyPayload, SendOptions, SentMessage, Transport } from './types.js';
|
|
4
4
|
export interface ReliableTransportOptions {
|
|
5
5
|
readonly logger?: LoggerLike;
|
|
6
6
|
/** Idempotency-key factory (injectable for deterministic tests). */
|
|
@@ -16,8 +16,10 @@ export declare class ReliableTransport implements Transport {
|
|
|
16
16
|
start(): Promise<void>;
|
|
17
17
|
stop(): Promise<void>;
|
|
18
18
|
onMessage(handler: (message: InboundMessage) => void): void;
|
|
19
|
+
onCardAction(handler: (action: CardAction) => void): void;
|
|
19
20
|
/** Forward the bot open id when the underlying API resolved one. */
|
|
20
21
|
getBotOpenId(): string | undefined;
|
|
22
|
+
refreshBotOpenId(): Promise<string | undefined>;
|
|
21
23
|
getBotName(): string | undefined;
|
|
22
24
|
getConnectionStatus(): {
|
|
23
25
|
readonly ready: boolean;
|
|
@@ -30,6 +32,8 @@ export declare class ReliableTransport implements Transport {
|
|
|
30
32
|
};
|
|
31
33
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessage>;
|
|
32
34
|
sendCard(chatId: string, card: FeishuCard, options?: SendOptions): Promise<SentMessage>;
|
|
35
|
+
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
36
|
+
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
33
37
|
updateCard(messageId: string, card: FeishuCard): Promise<void>;
|
|
34
38
|
reply(chatId: string, messageId: string, payload: ReplyPayload, options?: ReplyOptions): Promise<SentMessage>;
|
|
35
39
|
}
|
|
@@ -37,10 +37,16 @@ export class ReliableTransport {
|
|
|
37
37
|
onMessage(handler) {
|
|
38
38
|
this.api.onMessage(handler);
|
|
39
39
|
}
|
|
40
|
+
onCardAction(handler) {
|
|
41
|
+
this.api.onCardAction?.(handler);
|
|
42
|
+
}
|
|
40
43
|
/** Forward the bot open id when the underlying API resolved one. */
|
|
41
44
|
getBotOpenId() {
|
|
42
45
|
return this.api.getBotOpenId?.();
|
|
43
46
|
}
|
|
47
|
+
refreshBotOpenId() {
|
|
48
|
+
return this.api.refreshBotOpenId?.() ?? Promise.resolve(this.api.getBotOpenId?.());
|
|
49
|
+
}
|
|
44
50
|
getBotName() {
|
|
45
51
|
return this.api.getBotName?.();
|
|
46
52
|
}
|
|
@@ -94,6 +100,12 @@ export class ReliableTransport {
|
|
|
94
100
|
uuid: options.uuid ?? this.uuidFactory(),
|
|
95
101
|
}));
|
|
96
102
|
}
|
|
103
|
+
addReaction(messageId, emojiType) {
|
|
104
|
+
return this.api.addReaction(messageId, emojiType);
|
|
105
|
+
}
|
|
106
|
+
removeReaction(messageId, reactionId) {
|
|
107
|
+
return this.api.removeReaction(messageId, reactionId);
|
|
108
|
+
}
|
|
97
109
|
async updateCard(messageId, card) {
|
|
98
110
|
assertUpdateMulti(card);
|
|
99
111
|
await this.#recordVoid('卡片', () => this.api.patchCard(messageId, card));
|
|
@@ -47,6 +47,15 @@ export interface InboundMessage {
|
|
|
47
47
|
/** Unix epoch milliseconds from Feishu; `0` when the event omitted it. */
|
|
48
48
|
readonly createdAt: number;
|
|
49
49
|
}
|
|
50
|
+
/** One interactive-card button click, normalized for the bridge. */
|
|
51
|
+
export interface CardAction {
|
|
52
|
+
readonly operatorOpenId: string;
|
|
53
|
+
readonly messageId: string;
|
|
54
|
+
readonly chatId: string;
|
|
55
|
+
readonly value: Record<string, unknown>;
|
|
56
|
+
/** Feishu form fields submitted with the action (`action.form_value`). */
|
|
57
|
+
readonly formValue?: Readonly<Record<string, string>>;
|
|
58
|
+
}
|
|
50
59
|
/** A created message. */
|
|
51
60
|
export interface SentMessage {
|
|
52
61
|
readonly messageId: string;
|
|
@@ -110,11 +119,17 @@ export interface Transport {
|
|
|
110
119
|
sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessage>;
|
|
111
120
|
sendCard(chatId: string, card: FeishuCard, options?: SendOptions): Promise<SentMessage>;
|
|
112
121
|
updateCard(messageId: string, card: FeishuCard): Promise<void>;
|
|
122
|
+
/** Add one emoji reaction; resolves the reaction id. */
|
|
123
|
+
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
124
|
+
/** Remove one reaction previously added by this bot. */
|
|
125
|
+
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
113
126
|
/**
|
|
114
127
|
* The bot's own open id when known (needed for the group @ gate). Optional:
|
|
115
128
|
* fake transports may omit it, and the real one resolves it lazily.
|
|
116
129
|
*/
|
|
117
130
|
getBotOpenId?(): string | undefined;
|
|
131
|
+
/** Retry bot identity resolution (group @ gate). */
|
|
132
|
+
refreshBotOpenId?(): Promise<string | undefined>;
|
|
118
133
|
/** Cached bot display name (resolved at startup; no extra API call). */
|
|
119
134
|
getBotName?(): string | undefined;
|
|
120
135
|
/** Long-connection status/start time, when known. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-caduceus",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Feishu/Lark surface plugin for DeepSeek Harness (dsh): one topic per session, SOUL persona sync, streaming process cards, /status and /doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"dist/",
|
|
38
38
|
"cordis.patch.yml",
|
|
39
39
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"README.zh.md"
|
|
41
42
|
],
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=22.13",
|