@xmanrui/dsh-im 4.2.0 → 4.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.en.md +4 -2
- package/README.md +4 -2
- package/THIRD_PARTY_NOTICES.md +25 -1
- package/lib/client.js +352 -196
- package/lib/index.js +222 -220
- package/package.json +3 -2
- package/plugin-src/client/context-enhancement.js +207 -102
- package/plugin-src/client/i18n.js +15 -2
- package/plugin-src/client/styles.js +9 -4
- package/plugin-src/host/build.mjs +1 -0
- package/scripts/verify-package.mjs +9 -3
- package/src/channels/feishu/bridge.mjs +98 -40
- package/src/channels/feishu/message-utils.mjs +5 -0
- package/src/channels/qq/qq-runtime.mjs +5 -1
- package/src/channels/shared/context-enhancement.mjs +73 -20
- package/src/channels/telegram/telegram-api.mjs +5 -1
- package/src/channels/telegram/telegram-http.mjs +21 -0
- package/src/channels/telegram/telegram-runtime.mjs +20 -2
|
@@ -686,7 +686,7 @@ export class FeishuHarnessBridge {
|
|
|
686
686
|
? pending.queue
|
|
687
687
|
: null,
|
|
688
688
|
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
689
|
-
send: (text) => this.#send(event.message.chat_id, text),
|
|
689
|
+
send: (text) => this.#send(event.message.chat_id, text, { replyTo: event.message.message_id }),
|
|
690
690
|
});
|
|
691
691
|
if (approvalReply) {
|
|
692
692
|
const processing = approvalReply.process(async () => {
|
|
@@ -851,7 +851,7 @@ export class FeishuHarnessBridge {
|
|
|
851
851
|
if (error?.code === 'turn-stopped') {
|
|
852
852
|
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
853
853
|
if (error?.batchInputMessage) {
|
|
854
|
-
await this.#send(event.message.chat_id, error.batchInputMessage).catch(() => undefined);
|
|
854
|
+
await this.#send(event.message.chat_id, error.batchInputMessage, { replyTo: event.message.message_id }).catch(() => undefined);
|
|
855
855
|
}
|
|
856
856
|
return;
|
|
857
857
|
}
|
|
@@ -940,7 +940,7 @@ export class FeishuHarnessBridge {
|
|
|
940
940
|
]);
|
|
941
941
|
}
|
|
942
942
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
943
|
-
if (reply) await this.#send(event.message.chat_id, reply);
|
|
943
|
+
if (reply) await this.#send(event.message.chat_id, reply, { replyTo: event.message.message_id });
|
|
944
944
|
}
|
|
945
945
|
this.#status.lastError = null;
|
|
946
946
|
}
|
|
@@ -964,7 +964,7 @@ export class FeishuHarnessBridge {
|
|
|
964
964
|
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
|
965
965
|
const commandText = !hasImages && !hasFiles && text ? text.trim() : null;
|
|
966
966
|
if (!text && !hasImages && !hasFiles) {
|
|
967
|
-
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
|
|
967
|
+
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'), { replyTo: event.message.message_id });
|
|
968
968
|
return;
|
|
969
969
|
}
|
|
970
970
|
|
|
@@ -973,11 +973,11 @@ export class FeishuHarnessBridge {
|
|
|
973
973
|
return;
|
|
974
974
|
}
|
|
975
975
|
if (commandText === '/help') {
|
|
976
|
-
await this.#send(event.message.chat_id, menuHelpText());
|
|
976
|
+
await this.#send(event.message.chat_id, menuHelpText(), { replyTo: event.message.message_id });
|
|
977
977
|
return;
|
|
978
978
|
}
|
|
979
979
|
if (MENU_COMMAND.test(commandText)) {
|
|
980
|
-
await this.#sendMenuCard(key, event.message.chat_id);
|
|
980
|
+
await this.#sendMenuCard(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
981
981
|
return;
|
|
982
982
|
}
|
|
983
983
|
if (commandText === '/new') {
|
|
@@ -985,53 +985,54 @@ export class FeishuHarnessBridge {
|
|
|
985
985
|
await this.#send(
|
|
986
986
|
event.message.chat_id,
|
|
987
987
|
t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'),
|
|
988
|
+
{ replyTo: event.message.message_id },
|
|
988
989
|
);
|
|
989
990
|
return;
|
|
990
991
|
}
|
|
991
992
|
await this.#state.clearSession(key);
|
|
992
|
-
await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'));
|
|
993
|
-
await this.#sendMenuCard(key, event.message.chat_id);
|
|
993
|
+
await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'), { replyTo: event.message.message_id });
|
|
994
|
+
await this.#sendMenuCard(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
994
995
|
return;
|
|
995
996
|
}
|
|
996
997
|
if (commandText === '/status') {
|
|
997
|
-
await this.#showStatusText(key, event.message.chat_id);
|
|
998
|
+
await this.#showStatusText(key, event.message.chat_id, event.message.message_id);
|
|
998
999
|
return;
|
|
999
1000
|
}
|
|
1000
1001
|
if (commandText === '/compact') {
|
|
1001
1002
|
const compactCommand = await runCompactCommand(commandText, this.#harness, this.#state, key, { signal: this.#signal });
|
|
1002
1003
|
if (compactCommand) {
|
|
1003
|
-
await this.#send(event.message.chat_id, compactCommand.message);
|
|
1004
|
+
await this.#send(event.message.chat_id, compactCommand.message, { replyTo: event.message.message_id });
|
|
1004
1005
|
}
|
|
1005
1006
|
return;
|
|
1006
1007
|
}
|
|
1007
1008
|
if (SESSION_LIST_PREFIX.test(commandText)) {
|
|
1008
1009
|
const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
|
|
1009
|
-
await this.#showSessions({ chatId: event.message.chat_id, key }, selector, 0);
|
|
1010
|
+
await this.#showSessions({ chatId: event.message.chat_id, key, replyTo: event.message.message_id }, selector, 0);
|
|
1010
1011
|
return;
|
|
1011
1012
|
}
|
|
1012
1013
|
if (WORKSPACE_LIST_COMMAND.test(commandText)) {
|
|
1013
|
-
await this.#showWorkspaces({ chatId: event.message.chat_id, key });
|
|
1014
|
+
await this.#showWorkspaces({ chatId: event.message.chat_id, key, replyTo: event.message.message_id });
|
|
1014
1015
|
return;
|
|
1015
1016
|
}
|
|
1016
1017
|
if (WATCH_COMMAND.test(commandText)) {
|
|
1017
1018
|
const target = (WATCH_COMMAND.exec(commandText)?.[1] ?? '').trim() || null;
|
|
1018
|
-
await this.#runWatch(key, event.message.chat_id, target);
|
|
1019
|
+
await this.#runWatch(key, event.message.chat_id, target, { replyTo: event.message.message_id });
|
|
1019
1020
|
return;
|
|
1020
1021
|
}
|
|
1021
1022
|
if (UNWATCH_COMMAND.test(commandText)) {
|
|
1022
1023
|
const target = (UNWATCH_COMMAND.exec(commandText)?.[1] ?? '').trim() || null;
|
|
1023
|
-
await this.#runUnwatch(key, event.message.chat_id, target);
|
|
1024
|
+
await this.#runUnwatch(key, event.message.chat_id, target, { replyTo: event.message.message_id });
|
|
1024
1025
|
return;
|
|
1025
1026
|
}
|
|
1026
1027
|
if (WATCHLIST_COMMAND.test(commandText)) {
|
|
1027
|
-
await this.#showWatchList(key, event.message.chat_id);
|
|
1028
|
+
await this.#showWatchList(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
1028
1029
|
return;
|
|
1029
1030
|
}
|
|
1030
1031
|
if (ARCHIVED_COMMAND.test(commandText)) {
|
|
1031
1032
|
const match = ARCHIVED_COMMAND.exec(commandText);
|
|
1032
1033
|
const value = match[1]?.toLowerCase();
|
|
1033
1034
|
if (value !== 'on' && value !== 'off') {
|
|
1034
|
-
await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'));
|
|
1035
|
+
await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'), { replyTo: event.message.message_id });
|
|
1035
1036
|
return;
|
|
1036
1037
|
}
|
|
1037
1038
|
if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
|
|
@@ -1040,6 +1041,7 @@ export class FeishuHarnessBridge {
|
|
|
1040
1041
|
await this.#send(
|
|
1041
1042
|
event.message.chat_id,
|
|
1042
1043
|
value === 'on' ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
|
|
1044
|
+
{ replyTo: event.message.message_id },
|
|
1043
1045
|
);
|
|
1044
1046
|
return;
|
|
1045
1047
|
}
|
|
@@ -1059,7 +1061,7 @@ export class FeishuHarnessBridge {
|
|
|
1059
1061
|
: await runWorkspaceCommand(text, this.#harness, key);
|
|
1060
1062
|
if (workspaceCommand) {
|
|
1061
1063
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
1062
|
-
await this.#send(event.message.chat_id, reply);
|
|
1064
|
+
await this.#send(event.message.chat_id, reply, { replyTo: event.message.message_id });
|
|
1063
1065
|
}
|
|
1064
1066
|
return;
|
|
1065
1067
|
}
|
|
@@ -1073,7 +1075,7 @@ export class FeishuHarnessBridge {
|
|
|
1073
1075
|
{ signal: this.#signal },
|
|
1074
1076
|
);
|
|
1075
1077
|
if (compactCommand) {
|
|
1076
|
-
await this.#send(event.message.chat_id, compactCommand.message);
|
|
1078
|
+
await this.#send(event.message.chat_id, compactCommand.message, { replyTo: event.message.message_id });
|
|
1077
1079
|
return;
|
|
1078
1080
|
}
|
|
1079
1081
|
|
|
@@ -2030,7 +2032,7 @@ export class FeishuHarnessBridge {
|
|
|
2030
2032
|
}
|
|
2031
2033
|
|
|
2032
2034
|
async #showSessions(
|
|
2033
|
-
{ chatId, key },
|
|
2035
|
+
{ chatId, key, replyTo = null },
|
|
2034
2036
|
selector,
|
|
2035
2037
|
page = 0,
|
|
2036
2038
|
{ updateMessageId = null } = {},
|
|
@@ -2039,14 +2041,14 @@ export class FeishuHarnessBridge {
|
|
|
2039
2041
|
const signal = this.#cardDataSignal();
|
|
2040
2042
|
const resolved = await resolveSessionListWorkspace(selector ?? '', this.#harness, { signal });
|
|
2041
2043
|
if (resolved.error) {
|
|
2042
|
-
await this.#send(chatId, resolved.error);
|
|
2044
|
+
await this.#send(chatId, resolved.error, { replyTo });
|
|
2043
2045
|
return;
|
|
2044
2046
|
}
|
|
2045
2047
|
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
|
|
2046
2048
|
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
|
2047
2049
|
const workspace = listed?.workspace ?? resolved.workspace;
|
|
2048
2050
|
if (sessions.length === 0) {
|
|
2049
|
-
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }));
|
|
2051
|
+
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
|
|
2050
2052
|
return;
|
|
2051
2053
|
}
|
|
2052
2054
|
const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
|
|
@@ -2065,6 +2067,7 @@ export class FeishuHarnessBridge {
|
|
|
2065
2067
|
{
|
|
2066
2068
|
key,
|
|
2067
2069
|
updateMessageId,
|
|
2070
|
+
replyTo,
|
|
2068
2071
|
// Keep the canonical selector result for later page callbacks. The
|
|
2069
2072
|
// list response's workspace is display data and is not authoritative.
|
|
2070
2073
|
sessionWorkspace: resolved.workspace,
|
|
@@ -2076,7 +2079,7 @@ export class FeishuHarnessBridge {
|
|
|
2076
2079
|
}
|
|
2077
2080
|
}
|
|
2078
2081
|
|
|
2079
|
-
async #showWorkspaces({ chatId, key }, { updateMessageId = null } = {}) {
|
|
2082
|
+
async #showWorkspaces({ chatId, key, replyTo = null }, { updateMessageId = null } = {}) {
|
|
2080
2083
|
try {
|
|
2081
2084
|
const { current, paths } = await workspacePathSnapshot(
|
|
2082
2085
|
this.#harness,
|
|
@@ -2086,7 +2089,7 @@ export class FeishuHarnessBridge {
|
|
|
2086
2089
|
await this.#sendCard(
|
|
2087
2090
|
chatId,
|
|
2088
2091
|
workspaceListCard(paths, current),
|
|
2089
|
-
{ key, updateMessageId },
|
|
2092
|
+
{ key, updateMessageId, replyTo },
|
|
2090
2093
|
);
|
|
2091
2094
|
} catch (error) {
|
|
2092
2095
|
await this.#sendFailure(chatId, error, { logLabel: 'workspace list' });
|
|
@@ -2144,6 +2147,7 @@ export class FeishuHarnessBridge {
|
|
|
2144
2147
|
|
|
2145
2148
|
async #sendCard(chatId, cardJson, options = {}) {
|
|
2146
2149
|
const updateMessageId = nonEmptyString(options.updateMessageId);
|
|
2150
|
+
const replyTo = nonEmptyString(options.replyTo);
|
|
2147
2151
|
|
|
2148
2152
|
if (updateMessageId) {
|
|
2149
2153
|
try {
|
|
@@ -2161,6 +2165,28 @@ export class FeishuHarnessBridge {
|
|
|
2161
2165
|
}
|
|
2162
2166
|
}
|
|
2163
2167
|
|
|
2168
|
+
// A brand-new card triggered by an inbound message is delivered as a
|
|
2169
|
+
// threaded reply so it lands inside the same Feishu topic. Falls back to
|
|
2170
|
+
// a plain chat message when the referenced message is gone.
|
|
2171
|
+
const content = cardJson;
|
|
2172
|
+
if (replyTo) {
|
|
2173
|
+
try {
|
|
2174
|
+
const response = await this.#client.im.v1.message.reply({
|
|
2175
|
+
path: { message_id: replyTo },
|
|
2176
|
+
data: { msg_type: 'interactive', content },
|
|
2177
|
+
});
|
|
2178
|
+
if (response?.code && response.code !== 0) {
|
|
2179
|
+
throw new Error(`Feishu card reply failed: ${response.msg || response.code}`);
|
|
2180
|
+
}
|
|
2181
|
+
const repliedMessageId = nonEmptyString(response?.data?.message_id);
|
|
2182
|
+
if (repliedMessageId) {
|
|
2183
|
+
this.#rememberCardRoute(repliedMessageId, chatId, options);
|
|
2184
|
+
return repliedMessageId;
|
|
2185
|
+
}
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
this.#logger.warn?.('[dsh-feishu] threaded card reply failed; sending a plain card:', error?.message ?? String(error));
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2164
2190
|
const response = await this.#client.im.v1.message.create({
|
|
2165
2191
|
params: { receive_id_type: 'chat_id' },
|
|
2166
2192
|
data: { receive_id: chatId, msg_type: 'interactive', content: cardJson },
|
|
@@ -2173,7 +2199,7 @@ export class FeishuHarnessBridge {
|
|
|
2173
2199
|
return messageId;
|
|
2174
2200
|
}
|
|
2175
2201
|
|
|
2176
|
-
async #sendMenuCard(key, chatId, { updateMessageId = null } = {}) {
|
|
2202
|
+
async #sendMenuCard(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2177
2203
|
let currentSessionId = null;
|
|
2178
2204
|
let directSessionTitle = null;
|
|
2179
2205
|
try {
|
|
@@ -2268,7 +2294,7 @@ export class FeishuHarnessBridge {
|
|
|
2268
2294
|
currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
|
|
2269
2295
|
sessions, archiveVisible, presetCatalog, modelCatalog,
|
|
2270
2296
|
}),
|
|
2271
|
-
{ key, updateMessageId },
|
|
2297
|
+
{ key, updateMessageId, replyTo },
|
|
2272
2298
|
);
|
|
2273
2299
|
}
|
|
2274
2300
|
|
|
@@ -2350,7 +2376,7 @@ export class FeishuHarnessBridge {
|
|
|
2350
2376
|
/**
|
|
2351
2377
|
* Gather system status and show the status card.
|
|
2352
2378
|
*/
|
|
2353
|
-
async #showStatusText(key, chatId) {
|
|
2379
|
+
async #showStatusText(key, chatId, replyTo = null) {
|
|
2354
2380
|
try {
|
|
2355
2381
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
2356
2382
|
const lines = [t('连接正常')];
|
|
@@ -2369,7 +2395,7 @@ export class FeishuHarnessBridge {
|
|
|
2369
2395
|
: (settings.agentPreset || t('跟随默认')),
|
|
2370
2396
|
}));
|
|
2371
2397
|
}
|
|
2372
|
-
await this.#send(chatId, lines.join('\n'));
|
|
2398
|
+
await this.#send(chatId, lines.join('\n'), { replyTo });
|
|
2373
2399
|
} catch (error) {
|
|
2374
2400
|
await this.#sendFailure(chatId, error, { logLabel: 'status text' });
|
|
2375
2401
|
}
|
|
@@ -2787,11 +2813,12 @@ export class FeishuHarnessBridge {
|
|
|
2787
2813
|
notify = true,
|
|
2788
2814
|
validatedTarget = null,
|
|
2789
2815
|
workspaceHint = null,
|
|
2816
|
+
replyTo = null,
|
|
2790
2817
|
} = {}) {
|
|
2791
2818
|
const watchRequestedAt = Date.now();
|
|
2792
2819
|
const reply = async (message) => {
|
|
2793
2820
|
if (!notify) return;
|
|
2794
|
-
await this.#send(chatId, message).catch((error) => {
|
|
2821
|
+
await this.#send(chatId, message, { replyTo }).catch((error) => {
|
|
2795
2822
|
this.#logger.warn?.('[dsh-feishu] watch notification failed:', error.message);
|
|
2796
2823
|
});
|
|
2797
2824
|
};
|
|
@@ -2854,10 +2881,10 @@ export class FeishuHarnessBridge {
|
|
|
2854
2881
|
return { ok: true, changed: !existingEntry, entry: this.#state.watchEntry?.(key, resolved.sessionId) };
|
|
2855
2882
|
}
|
|
2856
2883
|
|
|
2857
|
-
async #runUnwatch(key, chatId, target, { notify = true } = {}) {
|
|
2884
|
+
async #runUnwatch(key, chatId, target, { notify = true, replyTo = null } = {}) {
|
|
2858
2885
|
const reply = async (message) => {
|
|
2859
2886
|
if (!notify) return;
|
|
2860
|
-
await this.#send(chatId, message).catch((error) => {
|
|
2887
|
+
await this.#send(chatId, message, { replyTo }).catch((error) => {
|
|
2861
2888
|
this.#logger.warn?.('[dsh-feishu] unwatch notification failed:', error.message);
|
|
2862
2889
|
});
|
|
2863
2890
|
};
|
|
@@ -2883,7 +2910,7 @@ export class FeishuHarnessBridge {
|
|
|
2883
2910
|
return { ok: true, changed: true, entry };
|
|
2884
2911
|
}
|
|
2885
2912
|
|
|
2886
|
-
async #showWatchList(key, chatId, { updateMessageId = null } = {}) {
|
|
2913
|
+
async #showWatchList(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2887
2914
|
const entries = this.#state.watchEntries?.(key) ?? [];
|
|
2888
2915
|
// 收集可选会话(用于「添加关注」多选下拉);失败则传空数组 → 只渲染移除/列表。
|
|
2889
2916
|
let availableSessions = [];
|
|
@@ -2913,6 +2940,7 @@ export class FeishuHarnessBridge {
|
|
|
2913
2940
|
{
|
|
2914
2941
|
key,
|
|
2915
2942
|
updateMessageId,
|
|
2943
|
+
replyTo,
|
|
2916
2944
|
sessionWorkspace: currentWorkspace,
|
|
2917
2945
|
},
|
|
2918
2946
|
);
|
|
@@ -3068,17 +3096,18 @@ export class FeishuHarnessBridge {
|
|
|
3068
3096
|
actor: senderOpenId(event),
|
|
3069
3097
|
chatId: event.message.chat_id,
|
|
3070
3098
|
requiresMention: event.message.chat_type !== 'p2p',
|
|
3099
|
+
replyToMessageId: event.message.message_id,
|
|
3071
3100
|
}),
|
|
3072
3101
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
3073
3102
|
files,
|
|
3074
3103
|
};
|
|
3075
3104
|
}
|
|
3076
3105
|
|
|
3077
|
-
async #sendAnswerText(chatId, answer, { deliveryId, presentation }) {
|
|
3106
|
+
async #sendAnswerText(chatId, answer, { deliveryId, presentation, replyTo = null }) {
|
|
3078
3107
|
const providerMessageIds = [];
|
|
3079
3108
|
for (const chunk of splitText(answer)) {
|
|
3080
3109
|
this.#signal?.throwIfAborted();
|
|
3081
|
-
const messageId = await this.#send(chatId, chunk);
|
|
3110
|
+
const messageId = await this.#send(chatId, chunk, { replyTo });
|
|
3082
3111
|
if (messageId) providerMessageIds.push(messageId);
|
|
3083
3112
|
}
|
|
3084
3113
|
return createDeliveryReceipt({
|
|
@@ -3182,6 +3211,7 @@ export class FeishuHarnessBridge {
|
|
|
3182
3211
|
{
|
|
3183
3212
|
deliveryId: messageId,
|
|
3184
3213
|
presentation: 'feishu-text',
|
|
3214
|
+
replyTo: messageId,
|
|
3185
3215
|
},
|
|
3186
3216
|
);
|
|
3187
3217
|
} catch (error) {
|
|
@@ -3252,6 +3282,7 @@ export class FeishuHarnessBridge {
|
|
|
3252
3282
|
{
|
|
3253
3283
|
deliveryId: messageId,
|
|
3254
3284
|
presentation: 'feishu-text-fallback',
|
|
3285
|
+
replyTo: messageId,
|
|
3255
3286
|
},
|
|
3256
3287
|
);
|
|
3257
3288
|
} catch (fallbackError) {
|
|
@@ -3302,6 +3333,7 @@ export class FeishuHarnessBridge {
|
|
|
3302
3333
|
{
|
|
3303
3334
|
deliveryId: messageId,
|
|
3304
3335
|
presentation: 'feishu-text-fallback',
|
|
3336
|
+
replyTo: messageId,
|
|
3305
3337
|
},
|
|
3306
3338
|
);
|
|
3307
3339
|
} catch (fallbackError) {
|
|
@@ -3368,11 +3400,11 @@ export class FeishuHarnessBridge {
|
|
|
3368
3400
|
const pending = this.#pendingInteractions.get(key);
|
|
3369
3401
|
if (!pending || pending !== expected || pending.submitting) {
|
|
3370
3402
|
if (this.#isResolvedQuestionReply(event, key)) {
|
|
3371
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3403
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3372
3404
|
return;
|
|
3373
3405
|
}
|
|
3374
3406
|
if (claimed && (!pending || pending !== expected)) {
|
|
3375
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT());
|
|
3407
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id });
|
|
3376
3408
|
return;
|
|
3377
3409
|
}
|
|
3378
3410
|
return this.#enqueueMessage(event, messageId, key, processingReaction, {
|
|
@@ -3430,7 +3462,7 @@ export class FeishuHarnessBridge {
|
|
|
3430
3462
|
if (error?.code === 'interaction-not-pending') {
|
|
3431
3463
|
this.#rememberResolvedInteraction(key, pending);
|
|
3432
3464
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
3433
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3465
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3434
3466
|
return;
|
|
3435
3467
|
}
|
|
3436
3468
|
pending.submitting = false;
|
|
@@ -3448,12 +3480,13 @@ export class FeishuHarnessBridge {
|
|
|
3448
3480
|
actor,
|
|
3449
3481
|
chatId,
|
|
3450
3482
|
requiresMention,
|
|
3483
|
+
replyToMessageId,
|
|
3451
3484
|
}) {
|
|
3452
3485
|
if (await this.#approvals.handleRequested(interaction, {
|
|
3453
3486
|
key,
|
|
3454
3487
|
actor,
|
|
3455
3488
|
requiresMention,
|
|
3456
|
-
send: (text) => this.#send(chatId, text),
|
|
3489
|
+
send: (text) => this.#send(chatId, text, { replyTo: replyToMessageId }),
|
|
3457
3490
|
})) return;
|
|
3458
3491
|
|
|
3459
3492
|
// Approval requests return above; the existing question state machine stays unchanged.
|
|
@@ -3484,6 +3517,7 @@ export class FeishuHarnessBridge {
|
|
|
3484
3517
|
await this.#send(
|
|
3485
3518
|
chatId,
|
|
3486
3519
|
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
3520
|
+
{ replyTo: replyToMessageId },
|
|
3487
3521
|
).catch(() => undefined);
|
|
3488
3522
|
return;
|
|
3489
3523
|
}
|
|
@@ -3519,6 +3553,7 @@ export class FeishuHarnessBridge {
|
|
|
3519
3553
|
answers: [],
|
|
3520
3554
|
index: 0,
|
|
3521
3555
|
chatId,
|
|
3556
|
+
replyToMessageId,
|
|
3522
3557
|
queue: null,
|
|
3523
3558
|
claimedReplyMessageId: null,
|
|
3524
3559
|
submitting: false,
|
|
@@ -3553,6 +3588,9 @@ export class FeishuHarnessBridge {
|
|
|
3553
3588
|
pending.questions.length,
|
|
3554
3589
|
{ requiresMention: pending.requiresMention },
|
|
3555
3590
|
),
|
|
3591
|
+
// Reply to the message that started the turn so the question lands in
|
|
3592
|
+
// the same Feishu thread/topic instead of the group's default area.
|
|
3593
|
+
{ replyTo: pending.replyToMessageId },
|
|
3556
3594
|
);
|
|
3557
3595
|
if (messageId) {
|
|
3558
3596
|
pending.questionMessageIds.add(messageId);
|
|
@@ -3585,7 +3623,7 @@ export class FeishuHarnessBridge {
|
|
|
3585
3623
|
await this.#state.markSeen(messageId);
|
|
3586
3624
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
3587
3625
|
this.#status.messagesReceived += 1;
|
|
3588
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3626
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3589
3627
|
}
|
|
3590
3628
|
|
|
3591
3629
|
#takePendingInteraction(key, interactionId) {
|
|
@@ -3651,13 +3689,33 @@ export class FeishuHarnessBridge {
|
|
|
3651
3689
|
else processingReaction.success();
|
|
3652
3690
|
}
|
|
3653
3691
|
|
|
3654
|
-
async #send(chatId, text) {
|
|
3692
|
+
async #send(chatId, text, { replyTo } = {}) {
|
|
3693
|
+
const content = JSON.stringify({ text });
|
|
3694
|
+
if (replyTo) {
|
|
3695
|
+
try {
|
|
3696
|
+
const response = await this.#client.im.v1.message.reply({
|
|
3697
|
+
path: { message_id: replyTo },
|
|
3698
|
+
data: { msg_type: 'text', content },
|
|
3699
|
+
});
|
|
3700
|
+
if (response?.code && response.code !== 0) {
|
|
3701
|
+
throw new Error(`Feishu reply failed: ${response.msg || response.code}`);
|
|
3702
|
+
}
|
|
3703
|
+
return nonEmptyString(response?.data?.message_id);
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
// The referenced message may be gone (recalled/deleted); keep the
|
|
3706
|
+
// delivery promise by falling back to a plain chat message.
|
|
3707
|
+
this.#logger.warn?.(
|
|
3708
|
+
'[dsh-feishu] threaded reply failed; falling back to a plain message:',
|
|
3709
|
+
error?.message ?? String(error),
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3655
3713
|
const response = await this.#client.im.v1.message.create({
|
|
3656
3714
|
params: { receive_id_type: 'chat_id' },
|
|
3657
3715
|
data: {
|
|
3658
3716
|
receive_id: chatId,
|
|
3659
3717
|
msg_type: 'text',
|
|
3660
|
-
content
|
|
3718
|
+
content,
|
|
3661
3719
|
},
|
|
3662
3720
|
});
|
|
3663
3721
|
if (response?.code && response.code !== 0) {
|
|
@@ -16,6 +16,11 @@ export function conversationKey(event) {
|
|
|
16
16
|
}
|
|
17
17
|
const chatId = event?.message?.chat_id;
|
|
18
18
|
if (!chatId) throw new Error('Feishu group event has no chat id');
|
|
19
|
+
// Topic groups: every message belongs to a thread, so key the session per
|
|
20
|
+
// thread to keep each topic's Harness conversation isolated. Regular group
|
|
21
|
+
// chats carry no thread_id and keep the single shared `group:<chat_id>` key.
|
|
22
|
+
const threadId = event?.message?.thread_id;
|
|
23
|
+
if (typeof threadId === 'string' && threadId.trim()) return `group:${chatId}:thread:${threadId}`;
|
|
19
24
|
return `group:${chatId}`;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QQBot, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
|
1
|
+
import { QQBot, contentSanitizer, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
connectionTestTarget,
|
|
@@ -171,6 +171,10 @@ export class QqRuntime {
|
|
|
171
171
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
172
172
|
signal: controller.signal,
|
|
173
173
|
});
|
|
174
|
+
// QQ delivers emoji/face messages as opaque `<faceType=..,faceId="..",ext="..">`
|
|
175
|
+
// tags. Parse them into readable text so the Harness sees what the sender
|
|
176
|
+
// actually meant instead of an unusable markup fragment.
|
|
177
|
+
bot.use(contentSanitizer({ parseFaceTags: true }));
|
|
174
178
|
bot.use?.(this.#typingMiddleware({
|
|
175
179
|
keepAlive: true,
|
|
176
180
|
predicate: (ctx) => this.#config.ownerUserOpenid === '*'
|
|
@@ -4,6 +4,12 @@ export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
|
|
4
4
|
]);
|
|
5
5
|
|
|
6
6
|
export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
|
|
7
|
+
export const CONTEXT_GROUP_GUIDANCE_EXAMPLE = `仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。
|
|
8
|
+
当前消息来自群聊,请使用严肃、克制、简洁的表达方式。`;
|
|
9
|
+
export const CONTEXT_DIRECT_GUIDANCE_EXAMPLE = `仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。
|
|
10
|
+
当前消息来自私聊,可以使用更轻松、幽默、详细的表达方式。`;
|
|
11
|
+
|
|
12
|
+
// Kept for integrations that imported the original combined example.
|
|
7
13
|
export const CONTEXT_GUIDANCE_EXAMPLE = `仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。
|
|
8
14
|
conversationType是群聊时回复严肃一点,conversationType是私聊时回复一定要幽默搞笑,像周星驰的电影一样搞笑`;
|
|
9
15
|
|
|
@@ -11,13 +17,21 @@ conversationType是群聊时回复严肃一点,conversationType是私聊时回
|
|
|
11
17
|
export const DEFAULT_CONTEXT_GUIDANCE = CONTEXT_GUIDANCE_EXAMPLE;
|
|
12
18
|
|
|
13
19
|
export const DEFAULT_CONTEXT_ENHANCEMENT_CONFIG = Object.freeze({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
group: Object.freeze({
|
|
21
|
+
enabled: false,
|
|
22
|
+
fields: Object.freeze(['senderId']),
|
|
23
|
+
guidance: '',
|
|
24
|
+
}),
|
|
25
|
+
direct: Object.freeze({
|
|
26
|
+
enabled: false,
|
|
27
|
+
fields: Object.freeze(['senderId']),
|
|
28
|
+
guidance: '',
|
|
29
|
+
}),
|
|
18
30
|
});
|
|
19
31
|
|
|
20
|
-
const CONFIG_KEYS = ['
|
|
32
|
+
const CONFIG_KEYS = ['group', 'direct'];
|
|
33
|
+
const SCOPE_KEYS = ['enabled', 'fields', 'guidance'];
|
|
34
|
+
const LEGACY_CONFIG_KEYS = ['groupEnabled', 'directEnabled', 'fields', 'guidance'];
|
|
21
35
|
const CHANNELS = new Set([
|
|
22
36
|
'wecom', 'weixin', 'feishu', 'dingtalk', 'qq',
|
|
23
37
|
'slack', 'telegram', 'discord', 'whatsapp',
|
|
@@ -31,16 +45,19 @@ function invalidConfig(message) {
|
|
|
31
45
|
return error;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
function hasExactKeys(input, keys) {
|
|
49
|
+
return input && typeof input === 'object' && !Array.isArray(input)
|
|
50
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(input))
|
|
51
|
+
&& Reflect.ownKeys(input).length === keys.length
|
|
52
|
+
&& keys.every((key) => Object.hasOwn(input, key));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateContextEnhancementScope(input) {
|
|
56
|
+
if (!hasExactKeys(input, SCOPE_KEYS)) {
|
|
40
57
|
throw invalidConfig('请提交完整的上下文增强设置。');
|
|
41
58
|
}
|
|
42
|
-
const {
|
|
43
|
-
if (typeof
|
|
59
|
+
const { enabled, fields, guidance } = input;
|
|
60
|
+
if (typeof enabled !== 'boolean') {
|
|
44
61
|
throw invalidConfig('群聊和私聊开关必须是布尔值。');
|
|
45
62
|
}
|
|
46
63
|
if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
|
|
@@ -50,19 +67,51 @@ export function validateContextEnhancementConfig(input) {
|
|
|
50
67
|
throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
|
|
51
68
|
}
|
|
52
69
|
return Object.freeze({
|
|
53
|
-
|
|
54
|
-
directEnabled,
|
|
70
|
+
enabled,
|
|
55
71
|
fields: Object.freeze(CONTEXT_ENHANCEMENT_FIELDS.filter((field) => fields.includes(field))),
|
|
56
72
|
guidance: guidance.trim() ? guidance : '',
|
|
57
73
|
});
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
/** Validate the complete atomic save, preserving explicit empty selections/text. */
|
|
77
|
+
export function validateContextEnhancementConfig(input) {
|
|
78
|
+
if (!hasExactKeys(input, CONFIG_KEYS)) {
|
|
79
|
+
throw invalidConfig('请提交完整的上下文增强设置。');
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
group: validateContextEnhancementScope(input.group),
|
|
83
|
+
direct: validateContextEnhancementScope(input.direct),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function migrateLegacyContextEnhancementConfig(input) {
|
|
88
|
+
if (!hasExactKeys(input, LEGACY_CONFIG_KEYS)) {
|
|
89
|
+
throw invalidConfig('请提交完整的上下文增强设置。');
|
|
90
|
+
}
|
|
91
|
+
return validateContextEnhancementConfig({
|
|
92
|
+
group: {
|
|
93
|
+
enabled: input.groupEnabled,
|
|
94
|
+
fields: input.fields,
|
|
95
|
+
guidance: input.guidance,
|
|
96
|
+
},
|
|
97
|
+
direct: {
|
|
98
|
+
enabled: input.directEnabled,
|
|
99
|
+
fields: input.fields,
|
|
100
|
+
guidance: input.guidance,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
/** Missing or damaged enhancement settings must never break an existing bot. */
|
|
61
106
|
export function normalizeContextEnhancementConfig(input) {
|
|
62
107
|
try {
|
|
63
108
|
return validateContextEnhancementConfig(input);
|
|
64
109
|
} catch {
|
|
65
|
-
|
|
110
|
+
try {
|
|
111
|
+
return migrateLegacyContextEnhancementConfig(input);
|
|
112
|
+
} catch {
|
|
113
|
+
return DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
|
|
114
|
+
}
|
|
66
115
|
}
|
|
67
116
|
}
|
|
68
117
|
|
|
@@ -71,11 +120,15 @@ export function captureContextEnhancement(provider, conversationType) {
|
|
|
71
120
|
if (conversationType !== 'group' && conversationType !== 'direct') return null;
|
|
72
121
|
try {
|
|
73
122
|
const settings = provider?.getSettings?.();
|
|
74
|
-
const
|
|
75
|
-
|
|
123
|
+
const legacyEnabledKey = conversationType === 'group' ? 'groupEnabled' : 'directEnabled';
|
|
124
|
+
const enabled = Object.hasOwn(settings ?? {}, conversationType)
|
|
125
|
+
? settings?.[conversationType]?.enabled
|
|
126
|
+
: settings?.[legacyEnabledKey];
|
|
127
|
+
if (enabled !== true) return null;
|
|
76
128
|
const config = normalizeContextEnhancementConfig(settings);
|
|
77
|
-
|
|
78
|
-
|
|
129
|
+
const scope = config[conversationType];
|
|
130
|
+
if (scope.enabled !== true) return null;
|
|
131
|
+
return Object.freeze({ config: scope, botId: provider.botId, conversationType });
|
|
79
132
|
} catch {
|
|
80
133
|
return null;
|
|
81
134
|
}
|
|
@@ -105,19 +105,23 @@ export const COMMANDS_MENU_BUTTON = Object.freeze({ type: 'commands' });
|
|
|
105
105
|
export class TelegramApi {
|
|
106
106
|
#token;
|
|
107
107
|
#fetch;
|
|
108
|
+
#FormDataImpl;
|
|
108
109
|
#baseUrl;
|
|
109
110
|
#fileUploadTimeoutMs;
|
|
110
111
|
|
|
111
112
|
constructor({
|
|
112
113
|
token,
|
|
113
114
|
fetchImpl = fetch,
|
|
115
|
+
FormDataImpl = globalThis.FormData,
|
|
114
116
|
baseUrl = DEFAULT_BASE_URL,
|
|
115
117
|
fileUploadTimeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS,
|
|
116
118
|
}) {
|
|
117
119
|
if (!validTelegramToken(token)) throw new TypeError('Telegram Bot Token is invalid');
|
|
118
120
|
if (typeof fetchImpl !== 'function') throw new TypeError('TelegramApi requires fetch');
|
|
121
|
+
if (typeof FormDataImpl !== 'function') throw new TypeError('TelegramApi requires FormData');
|
|
119
122
|
this.#token = token.trim();
|
|
120
123
|
this.#fetch = fetchImpl;
|
|
124
|
+
this.#FormDataImpl = FormDataImpl;
|
|
121
125
|
this.#baseUrl = new URL(baseUrl);
|
|
122
126
|
this.#fileUploadTimeoutMs = positiveTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
|
123
127
|
}
|
|
@@ -273,7 +277,7 @@ export class TelegramApi {
|
|
|
273
277
|
|| !Buffer.isBuffer(file.bytes)) {
|
|
274
278
|
throw new TypeError(`A Telegram ${mediaLabel} is required`);
|
|
275
279
|
}
|
|
276
|
-
const payload = new
|
|
280
|
+
const payload = new this.#FormDataImpl();
|
|
277
281
|
payload.append('chat_id', String(chatId));
|
|
278
282
|
payload.append(
|
|
279
283
|
fieldName,
|