@xmanrui/dsh-im 4.5.0 → 4.7.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.en.md +2 -0
- package/README.md +2 -0
- package/lib/client.js +4 -1
- package/lib/index.js +249 -240
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +167 -20
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +9 -2
- package/src/channels/dingtalk/state-store.mjs +98 -0
- package/src/channels/discord/discord-api.mjs +7 -0
- package/src/channels/discord/discord-runtime.mjs +97 -2
- package/src/channels/feishu/bridge.mjs +44 -13
- package/src/channels/feishu/feishu-cards.mjs +2 -0
- package/src/channels/feishu/message-utils.mjs +229 -0
- package/src/channels/qq/qq-bridge.mjs +49 -6
- package/src/channels/shared/batch-input.mjs +3 -3
- package/src/channels/shared/harness-client.mjs +82 -30
- package/src/channels/shared/i18n-en/feishu.mjs +2 -2
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +2 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +6 -4
- package/src/channels/shared/image-prompt.mjs +51 -0
- package/src/channels/shared/semantic/reply-reference.mjs +153 -0
- package/src/channels/shared/session-reply-recovery.mjs +104 -0
- package/src/channels/shared/session-title.mjs +1 -1
- package/src/channels/shared/text-harness-bridge.mjs +13 -6
- package/src/channels/shared/workspace-command.mjs +22 -3
- package/src/channels/slack/manifest.mjs +3 -0
- package/src/channels/slack/slack-api.mjs +18 -0
- package/src/channels/slack/slack-runtime.mjs +56 -0
- package/src/channels/telegram/telegram-runtime.mjs +117 -2
- package/src/channels/wecom/wecom-bridge.mjs +50 -7
- package/src/channels/weixin/state-store.mjs +110 -0
- package/src/channels/weixin/weixin-api.mjs +86 -2
- package/src/channels/weixin/weixin-bridge.mjs +97 -9
- package/src/channels/weixin/weixin-runtime.mjs +26 -6
- package/src/channels/whatsapp/whatsapp-runtime.mjs +56 -0
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
hasInboundImages,
|
|
12
12
|
imagePromptDiagnostic,
|
|
13
13
|
imagePromptUserMessage,
|
|
14
|
-
promptContentForMessage,
|
|
15
14
|
} from '../shared/image-prompt.mjs';
|
|
15
|
+
import {
|
|
16
|
+
hasReplyReference,
|
|
17
|
+
promptContentForInboundMessage,
|
|
18
|
+
} from '../shared/semantic/reply-reference.mjs';
|
|
16
19
|
import {
|
|
17
20
|
hasInboundFiles,
|
|
18
21
|
inboundFileUserMessage,
|
|
@@ -44,7 +47,12 @@ import {
|
|
|
44
47
|
isPresetCommand,
|
|
45
48
|
runPresetCommand,
|
|
46
49
|
} from '../shared/preset-command.mjs';
|
|
47
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
parseSessionListArgument,
|
|
52
|
+
resolveSessionListWorkspace,
|
|
53
|
+
runWorkspaceCommand,
|
|
54
|
+
workspacePathSnapshot,
|
|
55
|
+
} from '../shared/workspace-command.mjs';
|
|
48
56
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
49
57
|
import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
|
|
50
58
|
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
@@ -157,6 +165,7 @@ const WORKSPACE_HELP_LINES = [
|
|
|
157
165
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
158
166
|
'/workspacelist 列出工作区绝对路径',
|
|
159
167
|
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
168
|
+
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
|
160
169
|
];
|
|
161
170
|
|
|
162
171
|
/** Safe user-facing text for bind/workspace failures (no raw messages). */
|
|
@@ -626,7 +635,9 @@ export class FeishuHarnessBridge {
|
|
|
626
635
|
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
627
636
|
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
628
637
|
: this.#batchInputs.handle(key, batchText, {
|
|
629
|
-
plainText: event.message.message_type === 'text'
|
|
638
|
+
plainText: event.message.message_type === 'text'
|
|
639
|
+
&& Boolean(batchText)
|
|
640
|
+
&& !hasReplyReference(commandMessage),
|
|
630
641
|
});
|
|
631
642
|
if (result.handled) {
|
|
632
643
|
if (result.kind === 'submit') {
|
|
@@ -1032,11 +1043,12 @@ export class FeishuHarnessBridge {
|
|
|
1032
1043
|
const text = message.content;
|
|
1033
1044
|
const hasImages = hasInboundImages(message);
|
|
1034
1045
|
const hasFiles = hasInboundFiles(message);
|
|
1046
|
+
const hasReply = hasReplyReference(message);
|
|
1035
1047
|
// 命令识别对 text 与纯文本 post 一视同仁:post 富文本若仅含单个
|
|
1036
1048
|
// 文本段落(如复制粘贴的 /new),同样按命令处理;带图片/文件不认。
|
|
1037
1049
|
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
|
1038
1050
|
const commandText = !hasImages && !hasFiles && text ? text.trim() : null;
|
|
1039
|
-
if (!text && !hasImages && !hasFiles) {
|
|
1051
|
+
if (!text && !hasImages && !hasFiles && !hasReply) {
|
|
1040
1052
|
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'), { replyTo: event.message.message_id });
|
|
1041
1053
|
return;
|
|
1042
1054
|
}
|
|
@@ -1079,8 +1091,18 @@ export class FeishuHarnessBridge {
|
|
|
1079
1091
|
return;
|
|
1080
1092
|
}
|
|
1081
1093
|
if (SESSION_LIST_PREFIX.test(commandText)) {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1094
|
+
const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
|
|
1095
|
+
const request = parseSessionListArgument(argument);
|
|
1096
|
+
if (request.error) {
|
|
1097
|
+
await this.#send(event.message.chat_id, request.error, { replyTo: event.message.message_id });
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
await this.#showSessions(
|
|
1101
|
+
{ chatId: event.message.chat_id, key, replyTo: event.message.message_id },
|
|
1102
|
+
request.selector || null,
|
|
1103
|
+
0,
|
|
1104
|
+
{ limit: request.limit },
|
|
1105
|
+
);
|
|
1084
1106
|
return;
|
|
1085
1107
|
}
|
|
1086
1108
|
if (WORKSPACE_LIST_COMMAND.test(commandText)) {
|
|
@@ -1844,6 +1866,7 @@ export class FeishuHarnessBridge {
|
|
|
1844
1866
|
messageId = null,
|
|
1845
1867
|
sessionWorkspace = null,
|
|
1846
1868
|
sessionPage = 0,
|
|
1869
|
+
sessionLimit = null,
|
|
1847
1870
|
selections = [],
|
|
1848
1871
|
}) {
|
|
1849
1872
|
// Confirmations triggered by a card interaction stay anchored to the
|
|
@@ -1855,7 +1878,7 @@ export class FeishuHarnessBridge {
|
|
|
1855
1878
|
{ chatId, key, replyTo: messageId },
|
|
1856
1879
|
sessionWorkspace,
|
|
1857
1880
|
page,
|
|
1858
|
-
{ updateMessageId: messageId },
|
|
1881
|
+
{ updateMessageId: messageId, limit: sessionLimit },
|
|
1859
1882
|
);
|
|
1860
1883
|
return;
|
|
1861
1884
|
}
|
|
@@ -2034,7 +2057,7 @@ export class FeishuHarnessBridge {
|
|
|
2034
2057
|
{ chatId, key, replyTo: messageId },
|
|
2035
2058
|
sessionWorkspace,
|
|
2036
2059
|
sessionPage,
|
|
2037
|
-
{ updateMessageId: messageId },
|
|
2060
|
+
{ updateMessageId: messageId, limit: sessionLimit },
|
|
2038
2061
|
);
|
|
2039
2062
|
}
|
|
2040
2063
|
return;
|
|
@@ -2050,7 +2073,7 @@ export class FeishuHarnessBridge {
|
|
|
2050
2073
|
{ chatId, key, replyTo: messageId },
|
|
2051
2074
|
sessionWorkspace,
|
|
2052
2075
|
sessionPage,
|
|
2053
|
-
{ updateMessageId: messageId },
|
|
2076
|
+
{ updateMessageId: messageId, limit: sessionLimit },
|
|
2054
2077
|
);
|
|
2055
2078
|
}
|
|
2056
2079
|
}
|
|
@@ -2135,7 +2158,7 @@ export class FeishuHarnessBridge {
|
|
|
2135
2158
|
{ chatId, key, replyTo = null },
|
|
2136
2159
|
selector,
|
|
2137
2160
|
page = 0,
|
|
2138
|
-
{ updateMessageId = null } = {},
|
|
2161
|
+
{ updateMessageId = null, limit = null } = {},
|
|
2139
2162
|
) {
|
|
2140
2163
|
try {
|
|
2141
2164
|
const signal = this.#cardDataSignal();
|
|
@@ -2145,7 +2168,11 @@ export class FeishuHarnessBridge {
|
|
|
2145
2168
|
return;
|
|
2146
2169
|
}
|
|
2147
2170
|
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
|
|
2148
|
-
const
|
|
2171
|
+
const visibleSessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
|
2172
|
+
const sessionLimit = Number.isSafeInteger(limit) && limit > 0 ? limit : null;
|
|
2173
|
+
const sessions = sessionLimit === null
|
|
2174
|
+
? visibleSessions
|
|
2175
|
+
: visibleSessions.slice(0, sessionLimit);
|
|
2149
2176
|
const workspace = listed?.workspace ?? resolved.workspace;
|
|
2150
2177
|
if (sessions.length === 0) {
|
|
2151
2178
|
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
|
|
@@ -2172,6 +2199,7 @@ export class FeishuHarnessBridge {
|
|
|
2172
2199
|
// list response's workspace is display data and is not authoritative.
|
|
2173
2200
|
sessionWorkspace: resolved.workspace,
|
|
2174
2201
|
sessionPage: safePage,
|
|
2202
|
+
sessionLimit,
|
|
2175
2203
|
},
|
|
2176
2204
|
);
|
|
2177
2205
|
} catch (error) {
|
|
@@ -2240,6 +2268,9 @@ export class FeishuHarnessBridge {
|
|
|
2240
2268
|
sessionPage: Number.isSafeInteger(options.sessionPage) && options.sessionPage >= 0
|
|
2241
2269
|
? options.sessionPage
|
|
2242
2270
|
: 0,
|
|
2271
|
+
sessionLimit: Number.isSafeInteger(options.sessionLimit) && options.sessionLimit > 0
|
|
2272
|
+
? options.sessionLimit
|
|
2273
|
+
: null,
|
|
2243
2274
|
});
|
|
2244
2275
|
if (this.#cardKeys.size > 200) {
|
|
2245
2276
|
const oldest = this.#cardKeys.keys().next().value;
|
|
@@ -3289,8 +3320,8 @@ export class FeishuHarnessBridge {
|
|
|
3289
3320
|
askCompleted = true;
|
|
3290
3321
|
onAskComplete?.();
|
|
3291
3322
|
};
|
|
3292
|
-
let content = hasInboundImages(message)
|
|
3293
|
-
? await
|
|
3323
|
+
let content = hasInboundImages(message) || hasReplyReference(message)
|
|
3324
|
+
? await promptContentForInboundMessage(message, { signal: this.#signal })
|
|
3294
3325
|
: undefined;
|
|
3295
3326
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
3296
3327
|
let contextEnhanced = false;
|
|
@@ -488,6 +488,7 @@ export function menuHelpText() {
|
|
|
488
488
|
'',
|
|
489
489
|
'📋 会话 / 工作区',
|
|
490
490
|
'/sessionlist 或 /sessions 列出工作区会话',
|
|
491
|
+
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
|
491
492
|
'/session ID 绑定已有会话',
|
|
492
493
|
'/workspacelist 列出工作区',
|
|
493
494
|
'/workspace 工作区序号或绝对路径 切换工作区',
|
|
@@ -554,6 +555,7 @@ const HELP_TEXT_COMMANDS = [
|
|
|
554
555
|
'`/new` — 开启全新会话',
|
|
555
556
|
'`/session ID` — 绑定已有会话',
|
|
556
557
|
'`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
|
|
558
|
+
'`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话',
|
|
557
559
|
'`/workspace 工作区序号或绝对路径` — 切换工作区',
|
|
558
560
|
'`/workspacelist` — 列出工作区',
|
|
559
561
|
'`/status` — 查看连接状态',
|
|
@@ -2,8 +2,14 @@ import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
|
2
2
|
import { t } from '../shared/i18n.mjs';
|
|
3
3
|
|
|
4
4
|
const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
|
5
|
+
const FEISHU_CARD_MESSAGE_CONTENT_TYPE = 'raw_card_content';
|
|
5
6
|
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
|
6
7
|
const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
|
|
8
|
+
const FEISHU_CARD_TEXT_MAX_DEPTH = 12;
|
|
9
|
+
const FEISHU_CARD_TEXT_MAX_NODES = 1_000;
|
|
10
|
+
const FEISHU_CARD_UNAVAILABLE_TEXTS = new Set([
|
|
11
|
+
'请升级至最新版本客户端,以查看内容',
|
|
12
|
+
]);
|
|
7
13
|
const FEISHU_IMAGE_PERMISSION_MESSAGE =
|
|
8
14
|
'飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
|
|
9
15
|
|
|
@@ -56,6 +62,124 @@ function nonEmptyString(value) {
|
|
|
56
62
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
function objectRecord(value) {
|
|
66
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function jsonRecord(value) {
|
|
70
|
+
if (objectRecord(value)) return value;
|
|
71
|
+
if (typeof value !== 'string') return null;
|
|
72
|
+
try {
|
|
73
|
+
return objectRecord(JSON.parse(value));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function interactiveCardRoot(parsed) {
|
|
80
|
+
const root = objectRecord(parsed);
|
|
81
|
+
if (!root) return null;
|
|
82
|
+
// raw_card_content wraps CardKit entities in json_card. Keep direct Card
|
|
83
|
+
// 1.0/2.0 payloads readable as well for historical messages and fixtures.
|
|
84
|
+
return jsonRecord(root.json_card) ?? jsonRecord(root.card) ?? root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cardProperty(value) {
|
|
88
|
+
const record = objectRecord(value);
|
|
89
|
+
return objectRecord(record?.property) ?? record;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cardTextContent(property) {
|
|
93
|
+
const i18n = objectRecord(property?.i18nContent);
|
|
94
|
+
const content = nonEmptyString(i18n?.zh_cn)
|
|
95
|
+
?? nonEmptyString(i18n?.en_us)
|
|
96
|
+
?? nonEmptyString(i18n?.ja_jp)
|
|
97
|
+
?? nonEmptyString(property?.content)
|
|
98
|
+
?? nonEmptyString(property?.text);
|
|
99
|
+
return content && !FEISHU_CARD_UNAVAILABLE_TEXTS.has(content) ? content : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cardElementText(
|
|
103
|
+
element,
|
|
104
|
+
{ depth = 0, inline = false, budget = { remaining: FEISHU_CARD_TEXT_MAX_NODES } } = {},
|
|
105
|
+
) {
|
|
106
|
+
if (depth > FEISHU_CARD_TEXT_MAX_DEPTH || budget.remaining <= 0) return '';
|
|
107
|
+
budget.remaining -= 1;
|
|
108
|
+
if (Array.isArray(element)) {
|
|
109
|
+
return element
|
|
110
|
+
.map((part) => cardElementText(part, {
|
|
111
|
+
depth: depth + 1,
|
|
112
|
+
inline: Array.isArray(part),
|
|
113
|
+
budget,
|
|
114
|
+
}))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join(inline ? ' ' : '\n');
|
|
117
|
+
}
|
|
118
|
+
const value = objectRecord(element);
|
|
119
|
+
if (!value) return '';
|
|
120
|
+
const property = cardProperty(value);
|
|
121
|
+
if (!property) return '';
|
|
122
|
+
const tag = String(value.tag ?? '').toLowerCase();
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
tag === 'markdown'
|
|
126
|
+
|| tag === 'markdown_v1'
|
|
127
|
+
|| tag === 'lark_md'
|
|
128
|
+
|| tag === 'plain_text'
|
|
129
|
+
|| tag === 'text'
|
|
130
|
+
) {
|
|
131
|
+
const content = cardTextContent(property);
|
|
132
|
+
if (content) return content;
|
|
133
|
+
const nested = cardElementText(property.elements, {
|
|
134
|
+
depth: depth + 1,
|
|
135
|
+
inline: true,
|
|
136
|
+
budget,
|
|
137
|
+
});
|
|
138
|
+
if (nested || tag !== 'markdown_v1') return nested;
|
|
139
|
+
return cardElementText(value.fallback ?? property.fallback, {
|
|
140
|
+
depth: depth + 1,
|
|
141
|
+
inline: true,
|
|
142
|
+
budget,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (tag === 'a' || tag === 'link' || tag === 'button') {
|
|
146
|
+
if (typeof property.text === 'string') return nonEmptyString(property.text) ?? '';
|
|
147
|
+
return cardElementText(property.text, { depth: depth + 1, inline: true, budget });
|
|
148
|
+
}
|
|
149
|
+
if (tag === 'div') {
|
|
150
|
+
return [
|
|
151
|
+
cardElementText(property.text, { depth: depth + 1, budget }),
|
|
152
|
+
cardElementText(property.fields, { depth: depth + 1, budget }),
|
|
153
|
+
].filter(Boolean).join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Traverse visible layout containers only. Deliberately ignore callback
|
|
157
|
+
// values, form state, URLs, ids, template variables and other hidden data.
|
|
158
|
+
return ['elements', 'columns', 'fields', 'children', 'actions']
|
|
159
|
+
.map((key) => cardElementText(property[key], { depth: depth + 1, budget }))
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function interactiveCardText(parsed) {
|
|
165
|
+
const card = interactiveCardRoot(parsed);
|
|
166
|
+
if (!card) return '';
|
|
167
|
+
const budget = { remaining: FEISHU_CARD_TEXT_MAX_NODES };
|
|
168
|
+
const header = cardProperty(card.header);
|
|
169
|
+
const title = [
|
|
170
|
+
cardElementText(header?.title, { inline: true, budget }),
|
|
171
|
+
cardElementText(header?.subtitle, { inline: true, budget }),
|
|
172
|
+
].filter(Boolean).join('\n') || nonEmptyString(card.title) || '';
|
|
173
|
+
const body = cardProperty(card.body);
|
|
174
|
+
const elements = Array.isArray(body?.elements)
|
|
175
|
+
? body.elements
|
|
176
|
+
: Array.isArray(card.elements)
|
|
177
|
+
? card.elements
|
|
178
|
+
: null;
|
|
179
|
+
const content = cardElementText(elements, { budget });
|
|
180
|
+
return [title, content].filter(Boolean).join('\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
59
183
|
function postContent(event, parsed = parsedMessageContent(event)) {
|
|
60
184
|
if (event?.message?.message_type !== 'post') return null;
|
|
61
185
|
if (!parsed) return null;
|
|
@@ -283,6 +407,109 @@ function feishuFileSource(event, client, file) {
|
|
|
283
407
|
};
|
|
284
408
|
}
|
|
285
409
|
|
|
410
|
+
function feishuReplyTargetId(event) {
|
|
411
|
+
const parentId = nonEmptyString(event?.message?.parent_id);
|
|
412
|
+
if (parentId) return parentId;
|
|
413
|
+
const rootId = nonEmptyString(event?.message?.root_id);
|
|
414
|
+
const messageId = nonEmptyString(event?.message?.message_id);
|
|
415
|
+
return rootId && rootId !== messageId ? rootId : null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function feishuReplyAttachments(messageType, parsed, post) {
|
|
419
|
+
if (messageType === 'post') {
|
|
420
|
+
return (post?.imageKeys ?? []).map(() => ({ kind: 'image' }));
|
|
421
|
+
}
|
|
422
|
+
if (messageType === 'image') return [{ kind: 'image' }];
|
|
423
|
+
if (messageType === 'file') {
|
|
424
|
+
return [{
|
|
425
|
+
kind: 'file',
|
|
426
|
+
...(nonEmptyString(parsed?.file_name) ? { name: nonEmptyString(parsed.file_name) } : {}),
|
|
427
|
+
}];
|
|
428
|
+
}
|
|
429
|
+
if (messageType === 'audio') return [{ kind: 'audio' }];
|
|
430
|
+
if (messageType === 'media') {
|
|
431
|
+
return [{
|
|
432
|
+
kind: 'video',
|
|
433
|
+
...(nonEmptyString(parsed?.file_name) ? { name: nonEmptyString(parsed.file_name) } : {}),
|
|
434
|
+
}];
|
|
435
|
+
}
|
|
436
|
+
if (messageType === 'sticker') return [{ kind: 'other' }];
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function feishuReplyReference(event, client) {
|
|
441
|
+
const messageId = feishuReplyTargetId(event);
|
|
442
|
+
if (!messageId) return null;
|
|
443
|
+
const chatId = nonEmptyString(event?.message?.chat_id);
|
|
444
|
+
return {
|
|
445
|
+
messageId,
|
|
446
|
+
async load({ signal } = {}) {
|
|
447
|
+
signal?.throwIfAborted();
|
|
448
|
+
let response;
|
|
449
|
+
try {
|
|
450
|
+
response = await client?.im?.v1?.message?.get?.({
|
|
451
|
+
path: { message_id: messageId },
|
|
452
|
+
params: {
|
|
453
|
+
with_sender_name: true,
|
|
454
|
+
card_msg_content_type: FEISHU_CARD_MESSAGE_CONTENT_TYPE,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
} catch (error) {
|
|
458
|
+
signal?.throwIfAborted();
|
|
459
|
+
if (await feishuProviderCode(error, signal) === FEISHU_MISSING_MESSAGE_SCOPE_CODE) {
|
|
460
|
+
return { messageId, unavailableReason: 'permission-denied' };
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
signal?.throwIfAborted();
|
|
465
|
+
if (providerCode(response) === FEISHU_MISSING_MESSAGE_SCOPE_CODE) {
|
|
466
|
+
return { messageId, unavailableReason: 'permission-denied' };
|
|
467
|
+
}
|
|
468
|
+
if (providerCode(response) !== null && providerCode(response) !== 0) {
|
|
469
|
+
return { messageId, unavailableReason: 'not-delivered' };
|
|
470
|
+
}
|
|
471
|
+
const item = response?.data?.items?.find?.(
|
|
472
|
+
(candidate) => nonEmptyString(candidate?.message_id) === messageId,
|
|
473
|
+
);
|
|
474
|
+
if (!item) return { messageId, unavailableReason: 'not-found' };
|
|
475
|
+
if (item.deleted) return { messageId, unavailableReason: 'deleted' };
|
|
476
|
+
const itemChatId = nonEmptyString(item.chat_id);
|
|
477
|
+
if (!chatId || !itemChatId || itemChatId !== chatId) {
|
|
478
|
+
return { messageId, unavailableReason: 'not-found' };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const messageType = nonEmptyString(item.msg_type) ?? '';
|
|
482
|
+
const quotedEvent = {
|
|
483
|
+
message: {
|
|
484
|
+
message_id: messageId,
|
|
485
|
+
message_type: messageType,
|
|
486
|
+
content: item.body?.content,
|
|
487
|
+
mentions: item.mentions ?? [],
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const parsed = parsedMessageContent(quotedEvent);
|
|
491
|
+
const post = postContent(quotedEvent, parsed);
|
|
492
|
+
const quoted = extractInboundMessage(quotedEvent, client);
|
|
493
|
+
const content = messageType === 'interactive'
|
|
494
|
+
? interactiveCardText(parsed)
|
|
495
|
+
: quoted.content;
|
|
496
|
+
const attachments = feishuReplyAttachments(messageType, parsed, post);
|
|
497
|
+
return {
|
|
498
|
+
messageId,
|
|
499
|
+
...(nonEmptyString(item.sender?.id) ? { authorId: nonEmptyString(item.sender.id) } : {}),
|
|
500
|
+
...(nonEmptyString(item.sender?.sender_name)
|
|
501
|
+
? { authorName: nonEmptyString(item.sender.sender_name) }
|
|
502
|
+
: {}),
|
|
503
|
+
...(content ? { content } : {}),
|
|
504
|
+
attachments,
|
|
505
|
+
...(messageType === 'interactive' && !content && attachments.length === 0
|
|
506
|
+
? { unavailableReason: 'unsupported' }
|
|
507
|
+
: {}),
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
286
513
|
export function extractInboundMessage(event, client) {
|
|
287
514
|
const messageType = event?.message?.message_type;
|
|
288
515
|
const parsed = parsedMessageContent(event);
|
|
@@ -292,10 +519,12 @@ export function extractInboundMessage(event, client) {
|
|
|
292
519
|
: null;
|
|
293
520
|
const imageKeys = standaloneImageKey ? [standaloneImageKey] : post?.imageKeys ?? [];
|
|
294
521
|
const file = messageType === 'file' ? feishuFileSource(event, client, parsed) : null;
|
|
522
|
+
const replyTo = feishuReplyReference(event, client);
|
|
295
523
|
return {
|
|
296
524
|
content: messageType === 'text' ? extractText(event) ?? '' : post?.text ?? '',
|
|
297
525
|
images: imageKeys.map((key) => feishuImageSource(event, client, key)),
|
|
298
526
|
files: file ? [file] : [],
|
|
527
|
+
...(replyTo ? { replyTo } : {}),
|
|
299
528
|
};
|
|
300
529
|
}
|
|
301
530
|
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
hasInboundImages,
|
|
34
34
|
imagePromptDiagnostic,
|
|
35
35
|
imagePromptUserMessage,
|
|
36
|
-
promptContentForMessage,
|
|
37
36
|
} from '../shared/image-prompt.mjs';
|
|
38
37
|
import {
|
|
39
38
|
hasInboundFiles,
|
|
@@ -44,6 +43,10 @@ import {
|
|
|
44
43
|
trackOutboundArtifactProviderPromise,
|
|
45
44
|
} from '../shared/semantic/artifact.mjs';
|
|
46
45
|
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
46
|
+
import {
|
|
47
|
+
hasReplyReference,
|
|
48
|
+
promptContentForInboundMessage,
|
|
49
|
+
} from '../shared/semantic/reply-reference.mjs';
|
|
47
50
|
import {
|
|
48
51
|
createDeliveryReceipt,
|
|
49
52
|
providerMessageIdsFor,
|
|
@@ -88,6 +91,7 @@ function helpText() {
|
|
|
88
91
|
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
89
92
|
t('/workspacelist 列出工作区绝对路径'),
|
|
90
93
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
94
|
+
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
|
91
95
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
92
96
|
t('/models 按序号列出所有可用模型'),
|
|
93
97
|
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
|
@@ -145,6 +149,37 @@ function hasQqFileAttachments(message) {
|
|
|
145
149
|
&& message.attachments.some((attachment) => !isQqImageAttachment(attachment));
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
function qqAttachmentKind(attachment) {
|
|
153
|
+
const mediaType = attachmentMediaType(attachment);
|
|
154
|
+
if (mediaType?.startsWith('image/')) return 'image';
|
|
155
|
+
if (mediaType?.startsWith('audio/')) return 'audio';
|
|
156
|
+
if (mediaType?.startsWith('video/')) return 'video';
|
|
157
|
+
return 'file';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function qqReplyReference(message) {
|
|
161
|
+
const refMsgIdx = nonEmptyString(message?.refMsgIdx);
|
|
162
|
+
if (!refMsgIdx) return null;
|
|
163
|
+
const element = Array.isArray(message?.msgElements) ? message.msgElements[0] : null;
|
|
164
|
+
const sourceAttachments = Array.isArray(element?.attachments) ? element.attachments : [];
|
|
165
|
+
const attachments = sourceAttachments.map((attachment) => {
|
|
166
|
+
const name = nonEmptyString(attachment?.filename);
|
|
167
|
+
return { kind: qqAttachmentKind(attachment), ...(name ? { name } : {}) };
|
|
168
|
+
});
|
|
169
|
+
const asrText = sourceAttachments
|
|
170
|
+
.filter((attachment) => qqAttachmentKind(attachment) === 'audio')
|
|
171
|
+
.map((attachment) => nonEmptyString(attachment?.asr_refer_text))
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join('\n');
|
|
174
|
+
const content = asrText || nonEmptyString(element?.content);
|
|
175
|
+
return {
|
|
176
|
+
messageId: refMsgIdx,
|
|
177
|
+
...(content ? { content } : {}),
|
|
178
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
179
|
+
...(!content && attachments.length === 0 ? { unavailableReason: 'not-delivered' } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
148
183
|
async function fetchQqFileBuffer(url, { fetchImpl, signal }) {
|
|
149
184
|
const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
|
|
150
185
|
const response = await fetchImpl(new URL(normalizedUrl), {
|
|
@@ -196,7 +231,13 @@ export function qqInboundMessage(message, { fetchImpl = fetch } = {}) {
|
|
|
196
231
|
},
|
|
197
232
|
});
|
|
198
233
|
}
|
|
199
|
-
|
|
234
|
+
const replyTo = qqReplyReference(message);
|
|
235
|
+
return {
|
|
236
|
+
content: safeText(message),
|
|
237
|
+
images,
|
|
238
|
+
files,
|
|
239
|
+
...(replyTo ? { replyTo } : {}),
|
|
240
|
+
};
|
|
200
241
|
}
|
|
201
242
|
|
|
202
243
|
function nonEmptyString(value) {
|
|
@@ -493,7 +534,8 @@ export class QqHarnessBridge {
|
|
|
493
534
|
: this.#batchInputs.handle(key, commandText, {
|
|
494
535
|
plainText: Boolean(commandText)
|
|
495
536
|
&& !hasQqImageAttachments(message)
|
|
496
|
-
&& !hasQqFileAttachments(message)
|
|
537
|
+
&& !hasQqFileAttachments(message)
|
|
538
|
+
&& !qqReplyReference(message),
|
|
497
539
|
});
|
|
498
540
|
if (result.handled) {
|
|
499
541
|
if (result.kind === 'submit') {
|
|
@@ -785,10 +827,11 @@ export class QqHarnessBridge {
|
|
|
785
827
|
const text = promptMessage.content;
|
|
786
828
|
const hasImages = hasInboundImages(promptMessage);
|
|
787
829
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
830
|
+
const hasReply = hasReplyReference(promptMessage);
|
|
788
831
|
let stream = null;
|
|
789
832
|
let batchSettled = batchSubmission === null;
|
|
790
833
|
try {
|
|
791
|
-
if (!text && !hasImages && !hasFiles) {
|
|
834
|
+
if (!text && !hasImages && !hasFiles && !hasReply) {
|
|
792
835
|
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
793
836
|
await markMessageSeen();
|
|
794
837
|
return;
|
|
@@ -836,8 +879,8 @@ export class QqHarnessBridge {
|
|
|
836
879
|
return;
|
|
837
880
|
}
|
|
838
881
|
|
|
839
|
-
let content = hasImages
|
|
840
|
-
? await
|
|
882
|
+
let content = hasImages || hasReply
|
|
883
|
+
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
|
841
884
|
: undefined;
|
|
842
885
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
843
886
|
let contextEnhanced = false;
|
|
@@ -74,7 +74,7 @@ export class BatchInputManager {
|
|
|
74
74
|
if (!batch) {
|
|
75
75
|
if (!name) return { handled: false };
|
|
76
76
|
if (!plainText) {
|
|
77
|
-
return result('unsupported-content', t('
|
|
77
|
+
return result('unsupported-content', t('批量输入命令仅支持纯文字,请移除图片、文件或引用消息后重试。'));
|
|
78
78
|
}
|
|
79
79
|
if (name === 'send') {
|
|
80
80
|
return result('no-batch', t('当前没有待提交的批量内容,请先发送 /batch。'));
|
|
@@ -91,7 +91,7 @@ export class BatchInputManager {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (!plainText && (batch.phase === 'collecting' || name)) {
|
|
94
|
-
return result('unsupported-content', t(
|
|
94
|
+
return result('unsupported-content', t(`批量输入模式目前仅支持文字,不支持图片、文件或引用消息,这条消息未收录。
|
|
95
95
|
请继续发送文字,或使用 /send、/cancel。`), {
|
|
96
96
|
count: batch.messages.length,
|
|
97
97
|
limit: BATCH_INPUT_LIMIT,
|
|
@@ -146,7 +146,7 @@ export class BatchInputManager {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (typeof text !== 'string') {
|
|
149
|
-
return result('unsupported-content', t(
|
|
149
|
+
return result('unsupported-content', t(`批量输入模式目前仅支持文字,不支持图片、文件或引用消息,这条消息未收录。
|
|
150
150
|
请继续发送文字,或使用 /send、/cancel。`), {
|
|
151
151
|
count: batch.messages.length,
|
|
152
152
|
limit: BATCH_INPUT_LIMIT,
|