@xmanrui/dsh-im 4.3.0 → 4.5.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 -2
- package/README.md +2 -2
- package/lib/client.js +976 -634
- package/lib/index.js +241 -238
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +351 -0
- package/plugin-src/client/channel-card-meta.js +2 -1
- package/plugin-src/client/channels/dingtalk/api.js +5 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -0
- package/plugin-src/client/channels/feishu/api.js +5 -0
- package/plugin-src/client/channels/feishu/index.js +1 -0
- package/plugin-src/client/channels/qq/api.js +5 -0
- package/plugin-src/client/channels/qq/index.js +1 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -0
- package/plugin-src/client/channels/shared/token-channel.js +1 -0
- package/plugin-src/client/channels/telegram/api.js +2 -17
- package/plugin-src/client/channels/telegram/index.js +0 -112
- package/plugin-src/client/channels/telegram/styles.js +0 -28
- package/plugin-src/client/channels/wecom/api.js +5 -0
- package/plugin-src/client/channels/wecom/index.js +1 -0
- package/plugin-src/client/channels/weixin/api.js +5 -0
- package/plugin-src/client/channels/weixin/index.js +1 -0
- package/plugin-src/client/channels/whatsapp/api.js +4 -10
- package/plugin-src/client/channels/whatsapp/index.js +1 -125
- package/plugin-src/client/channels/whatsapp/styles.js +0 -25
- package/plugin-src/client/context-enhancement.js +19 -5
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +61 -2
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +50 -4
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +39 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
- package/plugin-src/host/channels/qq/production.mjs +12 -1
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
- package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -1
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -1
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
- package/plugin-src/host/channels/wecom/production.mjs +12 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
- package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
- package/plugin-src/host/modern-harness-api.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +70 -12
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
- package/src/channels/discord/discord-runtime.mjs +29 -1
- package/src/channels/feishu/bridge.mjs +207 -82
- package/src/channels/feishu/feishu-cards.mjs +2 -2
- package/src/channels/feishu/feishu-channel.mjs +36 -6
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +67 -20
- package/src/channels/qq/qq-runtime.mjs +20 -2
- package/src/channels/shared/access-policy.mjs +210 -0
- package/src/channels/shared/bot-workspace-store.mjs +181 -33
- package/src/channels/shared/command-permission.mjs +32 -0
- package/src/channels/shared/compact-command.mjs +5 -2
- package/src/channels/shared/context-enhancement.mjs +9 -4
- package/src/channels/shared/harness-client.mjs +6 -0
- package/src/channels/shared/i18n-en/feishu.mjs +3 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -1
- package/src/channels/shared/i18n-en/shared-b.mjs +6 -3
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/inbound-access.mjs +43 -0
- package/src/channels/shared/session-title.mjs +74 -0
- package/src/channels/shared/text-harness-bridge.mjs +54 -6
- package/src/channels/shared/workspace-command.mjs +26 -5
- package/src/channels/shared/workspace-session.mjs +28 -2
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +6 -13
- package/src/channels/wecom/wecom-bridge.mjs +57 -6
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +67 -19
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { t } from './i18n.mjs';
|
|
2
|
+
import {
|
|
3
|
+
COMMAND_PERMISSION_DENIED_MESSAGE,
|
|
4
|
+
evaluateInboundAccess,
|
|
5
|
+
} from './inbound-access.mjs';
|
|
2
6
|
import { captureContextEnhancement, enhanceContextContent } from './context-enhancement.mjs';
|
|
3
7
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
4
8
|
import { runCompactCommand } from './compact-command.mjs';
|
|
@@ -129,6 +133,7 @@ export class TextHarnessBridge {
|
|
|
129
133
|
#harness;
|
|
130
134
|
#state;
|
|
131
135
|
#contextEnhancement;
|
|
136
|
+
#accessPolicy;
|
|
132
137
|
#status;
|
|
133
138
|
#logger;
|
|
134
139
|
#replyTimeoutMs;
|
|
@@ -149,6 +154,7 @@ export class TextHarnessBridge {
|
|
|
149
154
|
harness,
|
|
150
155
|
state,
|
|
151
156
|
contextEnhancement,
|
|
157
|
+
accessPolicy,
|
|
152
158
|
status = createTextBridgeStatus(),
|
|
153
159
|
logger = console,
|
|
154
160
|
replyTimeoutMs = 600_000,
|
|
@@ -162,6 +168,7 @@ export class TextHarnessBridge {
|
|
|
162
168
|
this.#harness = harness;
|
|
163
169
|
this.#state = state;
|
|
164
170
|
this.#contextEnhancement = contextEnhancement;
|
|
171
|
+
this.#accessPolicy = accessPolicy;
|
|
165
172
|
this.#status = status;
|
|
166
173
|
this.#logger = logger;
|
|
167
174
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
@@ -176,7 +183,7 @@ export class TextHarnessBridge {
|
|
|
176
183
|
return structuredClone(this.#status);
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
accept(message, { contextSnapshot } = {}) {
|
|
186
|
+
accept(message, { contextSnapshot, accessDecision } = {}) {
|
|
180
187
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
181
188
|
const conversationId = cleanText(message?.conversationId);
|
|
182
189
|
const kind = message?.kind === 'group' ? 'group' : 'direct';
|
|
@@ -187,6 +194,40 @@ export class TextHarnessBridge {
|
|
|
187
194
|
|| this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
|
|
188
195
|
return Promise.resolve();
|
|
189
196
|
}
|
|
197
|
+
// Preserve the channel trigger boundary. Access policy never turns an
|
|
198
|
+
// unaddressed group message into a denial reply.
|
|
199
|
+
if (kind === 'group' && normalized.addressed !== true) {
|
|
200
|
+
this.#status.messagesRejected += 1;
|
|
201
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
202
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
203
|
+
return this.#finishLocalMessage(normalized, messageId, null);
|
|
204
|
+
}
|
|
205
|
+
if (this.#accessPolicy || accessDecision) {
|
|
206
|
+
const hasImages = hasInboundImages(normalized);
|
|
207
|
+
const hasFiles = hasInboundFiles(normalized);
|
|
208
|
+
const decision = accessDecision ?? evaluateInboundAccess(this.#accessPolicy, {
|
|
209
|
+
conversationType: kind,
|
|
210
|
+
senderIds: [senderId, cleanText(normalized.senderAlternateId)].filter(Boolean),
|
|
211
|
+
text: normalized.content,
|
|
212
|
+
hasImages,
|
|
213
|
+
hasFiles,
|
|
214
|
+
});
|
|
215
|
+
if (!decision.allowed) {
|
|
216
|
+
this.#status.messagesRejected += 1;
|
|
217
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
218
|
+
// Mark policy denials so a webhook replay cannot repeat local work or
|
|
219
|
+
// a command-permission notice.
|
|
220
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
221
|
+
return this.#finishLocalMessage(
|
|
222
|
+
normalized,
|
|
223
|
+
messageId,
|
|
224
|
+
decision.reason === 'command-not-allowed'
|
|
225
|
+
? t(COMMAND_PERMISSION_DENIED_MESSAGE)
|
|
226
|
+
: null,
|
|
227
|
+
{ recordReceived: decision.reason === 'command-not-allowed' },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
190
231
|
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined
|
|
191
232
|
? captureContextEnhancement(this.#contextEnhancement, message?.kind)
|
|
192
233
|
: contextSnapshot);
|
|
@@ -350,13 +391,15 @@ export class TextHarnessBridge {
|
|
|
350
391
|
return this.#enqueueMessage(normalized, messageId, senderId, key);
|
|
351
392
|
}
|
|
352
393
|
|
|
353
|
-
#finishLocalMessage(message, messageId, reply) {
|
|
394
|
+
#finishLocalMessage(message, messageId, reply, { recordReceived = true } = {}) {
|
|
354
395
|
let task;
|
|
355
396
|
task = (async () => {
|
|
356
397
|
if (this.#state.hasSeen(messageId)) return;
|
|
357
398
|
await this.#state.markSeen(messageId);
|
|
358
|
-
|
|
359
|
-
|
|
399
|
+
if (recordReceived) {
|
|
400
|
+
this.#status.messagesReceived += 1;
|
|
401
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
402
|
+
}
|
|
360
403
|
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
|
361
404
|
this.#status.lastError = null;
|
|
362
405
|
})().catch(async (error) => {
|
|
@@ -545,7 +588,7 @@ export class TextHarnessBridge {
|
|
|
545
588
|
t('/new 开启一个全新会话'),
|
|
546
589
|
t('/compact 压缩当前会话的较早上下文'),
|
|
547
590
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
548
|
-
t('/workspace
|
|
591
|
+
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
549
592
|
t('/workspacelist 列出工作区绝对路径'),
|
|
550
593
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
551
594
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
@@ -630,12 +673,16 @@ export class TextHarnessBridge {
|
|
|
630
673
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
631
674
|
: undefined;
|
|
632
675
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
676
|
+
let contextEnhanced = false;
|
|
633
677
|
if (snapshot) {
|
|
634
|
-
|
|
678
|
+
const originalContent = content ?? text;
|
|
679
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
635
680
|
channel: this.#descriptor.key,
|
|
636
681
|
senderId,
|
|
637
682
|
senderName: message.contextSource?.()?.senderName,
|
|
683
|
+
conversationTitle: message.contextSource?.()?.conversationTitle,
|
|
638
684
|
}));
|
|
685
|
+
contextEnhanced = content !== originalContent;
|
|
639
686
|
}
|
|
640
687
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
641
688
|
harness: this.#harness,
|
|
@@ -643,6 +690,7 @@ export class TextHarnessBridge {
|
|
|
643
690
|
key: conversationKey,
|
|
644
691
|
text,
|
|
645
692
|
content,
|
|
693
|
+
contextEnhanced,
|
|
646
694
|
createOptions: this.#signal ? { signal: this.#signal } : undefined,
|
|
647
695
|
existsOptions: this.#signal ? { signal: this.#signal } : undefined,
|
|
648
696
|
askOptions: {
|
|
@@ -142,7 +142,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
142
142
|
`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
|
|
143
143
|
)),
|
|
144
144
|
'',
|
|
145
|
-
t('切换用法:/workspace
|
|
145
|
+
t('切换用法:/workspace 工作区序号或绝对路径'),
|
|
146
146
|
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
|
147
147
|
];
|
|
148
148
|
const message = lines.join('\n');
|
|
@@ -353,7 +353,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
356
|
-
if (
|
|
356
|
+
if (!isWorkspaceCommand(text)) return null;
|
|
357
357
|
const command = text.trim();
|
|
358
358
|
if (SESSION_BIND_PREFIX.test(command)) {
|
|
359
359
|
return runSessionBindCommand(command, harness, conversationKey);
|
|
@@ -366,18 +366,30 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
|
366
366
|
if (!match) return null;
|
|
367
367
|
const workspace = match[1]?.trim();
|
|
368
368
|
if (!workspace) {
|
|
369
|
-
return commandResult(t('用法:/workspace
|
|
369
|
+
return commandResult(t('用法:/workspace 工作区序号或绝对路径'));
|
|
370
370
|
}
|
|
371
371
|
if (typeof harness?.switchWorkspace !== 'function') {
|
|
372
372
|
return commandResult(t('当前机器人暂不支持切换工作区。'));
|
|
373
373
|
}
|
|
374
374
|
try {
|
|
375
|
-
|
|
375
|
+
let selected = workspace;
|
|
376
|
+
if (/^\d+$/u.test(workspace)) {
|
|
377
|
+
if (typeof harness?.listWorkspaces !== 'function') {
|
|
378
|
+
return commandResult(t('当前机器人暂不支持按序号选择工作区。'));
|
|
379
|
+
}
|
|
380
|
+
const { paths } = await workspacePathSnapshot(harness);
|
|
381
|
+
const position = Number(workspace);
|
|
382
|
+
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
383
|
+
return commandResult(t('工作区序号不存在,请先执行 /workspacelist。'));
|
|
384
|
+
}
|
|
385
|
+
selected = paths[position - 1];
|
|
386
|
+
}
|
|
387
|
+
const current = await harness.switchWorkspace(selected);
|
|
376
388
|
return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
|
|
377
389
|
} catch (error) {
|
|
378
390
|
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
379
391
|
return commandResult(t(`{message}
|
|
380
|
-
用法:/workspace
|
|
392
|
+
用法:/workspace 工作区序号或绝对路径`, { message: error.message }));
|
|
381
393
|
}
|
|
382
394
|
if (error?.code === 'workspace-bot-not-found') {
|
|
383
395
|
return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
|
|
@@ -385,3 +397,12 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
|
385
397
|
throw error;
|
|
386
398
|
}
|
|
387
399
|
}
|
|
400
|
+
|
|
401
|
+
export function isWorkspaceCommand(text) {
|
|
402
|
+
if (typeof text !== 'string') return false;
|
|
403
|
+
const command = text.trim();
|
|
404
|
+
return SESSION_BIND_PREFIX.test(command)
|
|
405
|
+
|| SESSION_LIST_COMMAND.test(command)
|
|
406
|
+
|| WORKSPACE_LIST_COMMAND.test(command)
|
|
407
|
+
|| WORKSPACE_COMMAND.test(command);
|
|
408
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
2
|
+
import { initialSessionTitle } from './session-title.mjs';
|
|
2
3
|
|
|
3
4
|
export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
|
|
4
5
|
|
|
@@ -6,7 +7,7 @@ function workspaceSession(harness, sessionId) {
|
|
|
6
7
|
if (typeof harness.workspaceSession === 'function') {
|
|
7
8
|
return harness.workspaceSession(sessionId);
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
const session = {
|
|
10
11
|
sessionId,
|
|
11
12
|
sessionExists: (...args) => harness.sessionExists(sessionId, ...args),
|
|
12
13
|
models: (...args) => harness.getSessionModels(sessionId, ...args),
|
|
@@ -16,7 +17,11 @@ function workspaceSession(harness, sessionId) {
|
|
|
16
17
|
stopActiveTurn: (...args) => harness.stopActiveTurn(sessionId, ...args),
|
|
17
18
|
steerActiveTurn: (...args) => harness.steerActiveTurn(sessionId, ...args),
|
|
18
19
|
ask: (...args) => harness.ask(sessionId, ...args),
|
|
19
|
-
}
|
|
20
|
+
};
|
|
21
|
+
if (typeof harness.renameSession === 'function') {
|
|
22
|
+
session.renameTitle = (...args) => harness.renameSession(sessionId, ...args);
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze(session);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
async function sessionExists(session, options) {
|
|
@@ -42,10 +47,21 @@ export async function askInWorkspaceSession({
|
|
|
42
47
|
key,
|
|
43
48
|
text,
|
|
44
49
|
content,
|
|
50
|
+
contextEnhanced = false,
|
|
45
51
|
createOptions,
|
|
46
52
|
existsOptions,
|
|
47
53
|
askOptions,
|
|
48
54
|
}) {
|
|
55
|
+
const initialTitle = contextEnhanced
|
|
56
|
+
? initialSessionTitle({
|
|
57
|
+
text,
|
|
58
|
+
content,
|
|
59
|
+
files: typeof askOptions === 'object' ? askOptions?.files : undefined,
|
|
60
|
+
})
|
|
61
|
+
: null;
|
|
62
|
+
const renameSignal = createOptions?.signal
|
|
63
|
+
?? (typeof askOptions === 'object' ? askOptions?.signal : undefined);
|
|
64
|
+
const renameOptions = renameSignal ? { signal: renameSignal } : undefined;
|
|
49
65
|
while (true) {
|
|
50
66
|
try {
|
|
51
67
|
const binding = await withSessionBindingLock(state, key, async () => {
|
|
@@ -55,6 +71,16 @@ export async function askInWorkspaceSession({
|
|
|
55
71
|
sessionId = await createSession(harness, createOptions);
|
|
56
72
|
if (await state.setSession(key, sessionId) === false) return null;
|
|
57
73
|
session = workspaceSession(harness, sessionId);
|
|
74
|
+
if (initialTitle && typeof session.renameTitle === 'function') {
|
|
75
|
+
try {
|
|
76
|
+
await session.renameTitle(initialTitle, renameOptions);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error?.code === WORKSPACE_SESSION_STALE || renameOptions?.signal?.aborted) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
console.warn('[dsh-im] unable to set the initial Session title:', error?.message ?? error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
58
84
|
}
|
|
59
85
|
return { sessionId, session };
|
|
60
86
|
});
|
|
@@ -347,6 +347,7 @@ export class SlackRuntime {
|
|
|
347
347
|
#harness;
|
|
348
348
|
#state;
|
|
349
349
|
#contextEnhancement;
|
|
350
|
+
#accessPolicy;
|
|
350
351
|
#logger;
|
|
351
352
|
#replyTimeoutMs;
|
|
352
353
|
#connectTimeoutMs;
|
|
@@ -371,6 +372,7 @@ export class SlackRuntime {
|
|
|
371
372
|
harness,
|
|
372
373
|
state,
|
|
373
374
|
contextEnhancement,
|
|
375
|
+
accessPolicy,
|
|
374
376
|
logger = console,
|
|
375
377
|
replyTimeoutMs = 600_000,
|
|
376
378
|
connectTimeoutMs = 20_000,
|
|
@@ -387,6 +389,7 @@ export class SlackRuntime {
|
|
|
387
389
|
this.#harness = harness;
|
|
388
390
|
this.#state = state;
|
|
389
391
|
this.#contextEnhancement = contextEnhancement;
|
|
392
|
+
this.#accessPolicy = accessPolicy;
|
|
390
393
|
this.#logger = logger;
|
|
391
394
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
392
395
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -464,6 +467,7 @@ export class SlackRuntime {
|
|
|
464
467
|
harness: this.#harness,
|
|
465
468
|
state: this.#state,
|
|
466
469
|
contextEnhancement: this.#contextEnhancement,
|
|
470
|
+
accessPolicy: this.#accessPolicy,
|
|
467
471
|
status: this.#status,
|
|
468
472
|
logger: this.#logger,
|
|
469
473
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from './telegram-rich-message.mjs';
|
|
15
15
|
import {
|
|
16
16
|
TELEGRAM_ACCESS_MODES,
|
|
17
|
-
normalizeTelegramAccessPolicy,
|
|
18
17
|
} from './config-store.mjs';
|
|
19
18
|
|
|
20
19
|
export const TELEGRAM_COMMAND_MENU = Object.freeze([
|
|
@@ -167,6 +166,7 @@ export function normalizeTelegramUpdate(update, {
|
|
|
167
166
|
senderName: [message.from?.first_name, message.from?.last_name]
|
|
168
167
|
.filter((value) => typeof value === 'string' && value.trim())
|
|
169
168
|
.map((value) => value.trim()).join(' ') || message.from?.username,
|
|
169
|
+
conversationTitle: direct ? undefined : message.chat?.title,
|
|
170
170
|
}),
|
|
171
171
|
senderIsBot: message.from?.is_bot === true,
|
|
172
172
|
kind: direct ? 'direct' : 'group',
|
|
@@ -669,12 +669,11 @@ export class TelegramRuntime {
|
|
|
669
669
|
#harness;
|
|
670
670
|
#state;
|
|
671
671
|
#contextEnhancement;
|
|
672
|
+
#accessPolicy;
|
|
672
673
|
#logger;
|
|
673
674
|
#replyTimeoutMs;
|
|
674
675
|
#createApi;
|
|
675
676
|
#createHttpTransport;
|
|
676
|
-
#accessMode;
|
|
677
|
-
#allowedPrivateUserIds;
|
|
678
677
|
#status = createTelegramRuntimeStatus();
|
|
679
678
|
#httpTransport = null;
|
|
680
679
|
#api = null;
|
|
@@ -689,6 +688,7 @@ export class TelegramRuntime {
|
|
|
689
688
|
harness,
|
|
690
689
|
state,
|
|
691
690
|
contextEnhancement,
|
|
691
|
+
accessPolicy,
|
|
692
692
|
logger = console,
|
|
693
693
|
replyTimeoutMs = 600_000,
|
|
694
694
|
createApi = (options) => new TelegramApi(options),
|
|
@@ -702,13 +702,11 @@ export class TelegramRuntime {
|
|
|
702
702
|
this.#harness = harness;
|
|
703
703
|
this.#state = state;
|
|
704
704
|
this.#contextEnhancement = contextEnhancement;
|
|
705
|
+
this.#accessPolicy = accessPolicy;
|
|
705
706
|
this.#logger = logger;
|
|
706
707
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
707
708
|
this.#createApi = createApi;
|
|
708
709
|
this.#createHttpTransport = createHttpTransport;
|
|
709
|
-
const accessPolicy = normalizeTelegramAccessPolicy(config);
|
|
710
|
-
this.#accessMode = accessPolicy.accessMode;
|
|
711
|
-
this.#allowedPrivateUserIds = new Set(accessPolicy.allowedUsers);
|
|
712
710
|
}
|
|
713
711
|
|
|
714
712
|
get status() {
|
|
@@ -805,6 +803,7 @@ export class TelegramRuntime {
|
|
|
805
803
|
harness: this.#harness,
|
|
806
804
|
state: this.#state,
|
|
807
805
|
contextEnhancement: this.#contextEnhancement,
|
|
806
|
+
accessPolicy: this.#accessPolicy,
|
|
808
807
|
status: this.#status,
|
|
809
808
|
logger: this.#logger,
|
|
810
809
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -865,10 +864,7 @@ export class TelegramRuntime {
|
|
|
865
864
|
loadFile: (fileId, options) => this.#api.downloadFile({ fileId, ...options }),
|
|
866
865
|
loadFileStream: (fileId, options) => this.#api.downloadFileStream({ fileId, ...options }),
|
|
867
866
|
});
|
|
868
|
-
if (message
|
|
869
|
-
accessMode: this.#accessMode,
|
|
870
|
-
allowedPrivateUserIds: this.#allowedPrivateUserIds,
|
|
871
|
-
})) {
|
|
867
|
+
if (message) {
|
|
872
868
|
void this.#bridge.accept(message, { contextSnapshot }).catch((error) => {
|
|
873
869
|
if (signal.aborted) return;
|
|
874
870
|
this.#logger.error?.(
|
|
@@ -876,9 +872,6 @@ export class TelegramRuntime {
|
|
|
876
872
|
error,
|
|
877
873
|
);
|
|
878
874
|
});
|
|
879
|
-
} else if (message) {
|
|
880
|
-
this.#status.messagesRejected += 1;
|
|
881
|
-
this.#status.lastRejectedAt = new Date().toISOString();
|
|
882
875
|
}
|
|
883
876
|
cursor = update.update_id + 1;
|
|
884
877
|
await this.#state.setCursor(cursor);
|
|
@@ -51,6 +51,10 @@ import {
|
|
|
51
51
|
messageFailureText,
|
|
52
52
|
setLastMessageFailure,
|
|
53
53
|
} from '../shared/message-failure.mjs';
|
|
54
|
+
import {
|
|
55
|
+
COMMAND_PERMISSION_DENIED_MESSAGE,
|
|
56
|
+
evaluateInboundAccess,
|
|
57
|
+
} from '../shared/inbound-access.mjs';
|
|
54
58
|
import { t } from '../shared/i18n.mjs';
|
|
55
59
|
|
|
56
60
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
@@ -64,7 +68,7 @@ function helpText() {
|
|
|
64
68
|
t('/new 开启一个全新会话'),
|
|
65
69
|
t('/compact 压缩当前会话的较早上下文'),
|
|
66
70
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
67
|
-
t('/workspace
|
|
71
|
+
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
68
72
|
t('/workspacelist 列出工作区绝对路径'),
|
|
69
73
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
70
74
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
@@ -490,6 +494,7 @@ export class WecomHarnessBridge {
|
|
|
490
494
|
#harness;
|
|
491
495
|
#state;
|
|
492
496
|
#contextEnhancement;
|
|
497
|
+
#accessPolicy;
|
|
493
498
|
#status;
|
|
494
499
|
#logger;
|
|
495
500
|
#replyTimeoutMs;
|
|
@@ -512,6 +517,7 @@ export class WecomHarnessBridge {
|
|
|
512
517
|
harness,
|
|
513
518
|
state,
|
|
514
519
|
contextEnhancement,
|
|
520
|
+
accessPolicy,
|
|
515
521
|
status = createWecomBridgeStatus(),
|
|
516
522
|
logger = console,
|
|
517
523
|
replyTimeoutMs = 600_000,
|
|
@@ -530,6 +536,7 @@ export class WecomHarnessBridge {
|
|
|
530
536
|
this.#harness = harness;
|
|
531
537
|
this.#state = state;
|
|
532
538
|
this.#contextEnhancement = contextEnhancement;
|
|
539
|
+
this.#accessPolicy = accessPolicy;
|
|
533
540
|
this.#status = status;
|
|
534
541
|
this.#logger = logger;
|
|
535
542
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
@@ -557,16 +564,28 @@ export class WecomHarnessBridge {
|
|
|
557
564
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
558
565
|
|
|
559
566
|
const key = conversationKey(frame);
|
|
567
|
+
const pending = this.#pendingInteractions.get(key);
|
|
568
|
+
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
569
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
570
|
+
const conversationType = body.chattype === 'single' ? 'direct' : 'group';
|
|
571
|
+
const access = evaluateInboundAccess(this.#accessPolicy, {
|
|
572
|
+
conversationType,
|
|
573
|
+
senderIds: senderId,
|
|
574
|
+
text: commandText,
|
|
575
|
+
hasImages: hasInboundImages(commandMessage),
|
|
576
|
+
hasFiles: hasInboundFiles(commandMessage),
|
|
577
|
+
});
|
|
578
|
+
if (!access.allowed) {
|
|
579
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
580
|
+
return this.#finishAccessDecision(frame, messageId, chatId, access);
|
|
581
|
+
}
|
|
560
582
|
this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
|
|
561
583
|
this.#contextEnhancement,
|
|
562
|
-
|
|
584
|
+
conversationType,
|
|
563
585
|
));
|
|
564
586
|
if (body.chattype === 'single') {
|
|
565
587
|
rememberConnectionTestTarget(this.#state, { chatId });
|
|
566
588
|
}
|
|
567
|
-
const pending = this.#pendingInteractions.get(key);
|
|
568
|
-
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
569
|
-
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
570
589
|
const batchCommand = isBatchInputCommand(commandText);
|
|
571
590
|
const batchStatus = this.#batchInputs.status(key);
|
|
572
591
|
if (batchCommand && body.chattype === 'group') {
|
|
@@ -760,6 +779,34 @@ export class WecomHarnessBridge {
|
|
|
760
779
|
return task;
|
|
761
780
|
}
|
|
762
781
|
|
|
782
|
+
#finishAccessDecision(frame, messageId, chatId, access) {
|
|
783
|
+
let task;
|
|
784
|
+
task = Promise.resolve().then(async () => {
|
|
785
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
786
|
+
await this.#state.markSeen(messageId);
|
|
787
|
+
if (access.reason === 'command-not-allowed') {
|
|
788
|
+
this.#status.messagesReceived += 1;
|
|
789
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
790
|
+
await this.#sendImmediate(frame, chatId, t(COMMAND_PERMISSION_DENIED_MESSAGE));
|
|
791
|
+
this.#status.messagesReplied += 1;
|
|
792
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
793
|
+
} else {
|
|
794
|
+
this.#status.messagesRejected += 1;
|
|
795
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
796
|
+
}
|
|
797
|
+
this.#status.lastError = null;
|
|
798
|
+
}).catch((error) => {
|
|
799
|
+
if (this.#signal?.aborted) return;
|
|
800
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
801
|
+
this.#logger.error?.('[dsh-im:wecom] failed to apply inbound access policy', error);
|
|
802
|
+
}).finally(() => {
|
|
803
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
804
|
+
this.#commandTasks.delete(task);
|
|
805
|
+
});
|
|
806
|
+
this.#commandTasks.add(task);
|
|
807
|
+
return task;
|
|
808
|
+
}
|
|
809
|
+
|
|
763
810
|
async waitForIdle() {
|
|
764
811
|
await Promise.allSettled([
|
|
765
812
|
...this.#queues.values(),
|
|
@@ -960,11 +1007,14 @@ export class WecomHarnessBridge {
|
|
|
960
1007
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
961
1008
|
: undefined;
|
|
962
1009
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
1010
|
+
let contextEnhanced = false;
|
|
963
1011
|
if (snapshot) {
|
|
964
|
-
|
|
1012
|
+
const originalContent = content ?? text;
|
|
1013
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
965
1014
|
channel: 'wecom',
|
|
966
1015
|
senderId,
|
|
967
1016
|
}));
|
|
1017
|
+
contextEnhanced = content !== originalContent;
|
|
968
1018
|
}
|
|
969
1019
|
await this.#state.markSeen(messageId);
|
|
970
1020
|
promptRecorded = true;
|
|
@@ -974,6 +1024,7 @@ export class WecomHarnessBridge {
|
|
|
974
1024
|
key,
|
|
975
1025
|
text,
|
|
976
1026
|
content,
|
|
1027
|
+
contextEnhanced,
|
|
977
1028
|
createOptions: { signal: this.#signal },
|
|
978
1029
|
existsOptions: { signal: this.#signal },
|
|
979
1030
|
askOptions: {
|
|
@@ -29,6 +29,7 @@ export class WecomRuntime {
|
|
|
29
29
|
#harness;
|
|
30
30
|
#state;
|
|
31
31
|
#contextEnhancement;
|
|
32
|
+
#accessPolicy;
|
|
32
33
|
#logger;
|
|
33
34
|
#replyTimeoutMs;
|
|
34
35
|
#connectTimeoutMs;
|
|
@@ -47,6 +48,7 @@ export class WecomRuntime {
|
|
|
47
48
|
harness,
|
|
48
49
|
state,
|
|
49
50
|
contextEnhancement,
|
|
51
|
+
accessPolicy,
|
|
50
52
|
logger = console,
|
|
51
53
|
replyTimeoutMs = 600_000,
|
|
52
54
|
connectTimeoutMs = 20_000,
|
|
@@ -61,6 +63,7 @@ export class WecomRuntime {
|
|
|
61
63
|
this.#harness = harness;
|
|
62
64
|
this.#state = state;
|
|
63
65
|
this.#contextEnhancement = contextEnhancement;
|
|
66
|
+
this.#accessPolicy = accessPolicy;
|
|
64
67
|
this.#logger = logger;
|
|
65
68
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
66
69
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -111,6 +114,7 @@ export class WecomRuntime {
|
|
|
111
114
|
harness: this.#harness,
|
|
112
115
|
state: this.#state,
|
|
113
116
|
contextEnhancement: this.#contextEnhancement,
|
|
117
|
+
accessPolicy: this.#accessPolicy,
|
|
114
118
|
status: this.#status,
|
|
115
119
|
logger: this.#logger,
|
|
116
120
|
replyTimeoutMs: this.#replyTimeoutMs,
|