mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-13 06:30:45 +08:00
feat: add unified IM access policies
This commit is contained in:
parent
075d2713c1
commit
840e5aa857
89 changed files with 5573 additions and 1676 deletions
|
|
@ -57,6 +57,10 @@ import {
|
|||
messageFailureText,
|
||||
setLastMessageFailure,
|
||||
} from '../shared/message-failure.mjs';
|
||||
import {
|
||||
COMMAND_PERMISSION_DENIED_MESSAGE,
|
||||
evaluateInboundAccess,
|
||||
} from '../shared/inbound-access.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
||||
|
|
@ -260,6 +264,7 @@ export class WeixinHarnessBridge {
|
|||
#harness;
|
||||
#state;
|
||||
#contextEnhancement;
|
||||
#accessPolicy;
|
||||
#status;
|
||||
#logger;
|
||||
#replyTimeoutMs;
|
||||
|
|
@ -292,6 +297,7 @@ export class WeixinHarnessBridge {
|
|||
harness,
|
||||
state,
|
||||
contextEnhancement,
|
||||
accessPolicy,
|
||||
status = createWeixinBridgeStatus(),
|
||||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
|
|
@ -312,6 +318,7 @@ export class WeixinHarnessBridge {
|
|||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
this.#contextEnhancement = contextEnhancement;
|
||||
this.#accessPolicy = accessPolicy;
|
||||
this.#status = status;
|
||||
this.#logger = logger;
|
||||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
|
|
@ -332,6 +339,22 @@ export class WeixinHarnessBridge {
|
|||
const sender = nonEmptyString(message?.from_user_id);
|
||||
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
||||
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
||||
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
||||
const access = this.#accessPolicy
|
||||
? evaluateInboundAccess(this.#accessPolicy, {
|
||||
conversationType: 'direct',
|
||||
senderIds: sender,
|
||||
text: commandText,
|
||||
hasImages: hasWeixinImageItems(message),
|
||||
hasFiles: hasWeixinFileItems(message),
|
||||
})
|
||||
: sender === this.#ownerUserId
|
||||
? { allowed: true, reason: 'legacy-owner' }
|
||||
: { allowed: false, reason: 'sender-not-allowed' };
|
||||
if (!access.allowed) {
|
||||
this.#acceptedMessageIds.set(messageId, null);
|
||||
return this.#finishAccessDecision(messageId, sender, message, access);
|
||||
}
|
||||
this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
|
||||
this.#contextEnhancement,
|
||||
'direct',
|
||||
|
|
@ -343,11 +366,9 @@ export class WeixinHarnessBridge {
|
|||
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
||||
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
||||
const pending = this.#pendingInteractions.get(key);
|
||||
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
||||
const batchCommand = isBatchInputCommand(commandText);
|
||||
const batchStatus = this.#batchInputs.status(key);
|
||||
if (sender === this.#ownerUserId
|
||||
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
||||
if (batchCommand || batchStatus.phase === 'collecting') {
|
||||
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
||||
const result = exactBatchStart
|
||||
&& batchStatus.phase === 'idle'
|
||||
|
|
@ -380,7 +401,7 @@ export class WeixinHarnessBridge {
|
|||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
: (isPresetCommand(commandText) ? runPresetCommand : null));
|
||||
if (commandRunner && sender === this.#ownerUserId) {
|
||||
if (commandRunner) {
|
||||
let task;
|
||||
task = this.#processFastCommand(
|
||||
message,
|
||||
|
|
@ -467,12 +488,10 @@ export class WeixinHarnessBridge {
|
|||
alreadyRecorded = false,
|
||||
batchSubmission = null,
|
||||
} = {}) {
|
||||
const preparedMessage = message.from_user_id === this.#ownerUserId
|
||||
? prefetchInboundFiles(
|
||||
weixinInboundMessage(message, this.#api),
|
||||
{ signal: this.#signal },
|
||||
)
|
||||
: undefined;
|
||||
const preparedMessage = prefetchInboundFiles(
|
||||
weixinInboundMessage(message, this.#api),
|
||||
{ signal: this.#signal },
|
||||
);
|
||||
const previous = this.#queues.get(key) ?? Promise.resolve();
|
||||
const current = previous
|
||||
.catch(() => undefined)
|
||||
|
|
@ -518,6 +537,36 @@ export class WeixinHarnessBridge {
|
|||
return task;
|
||||
}
|
||||
|
||||
#finishAccessDecision(messageId, sender, message, access) {
|
||||
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
||||
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
||||
let task;
|
||||
task = Promise.resolve().then(async () => {
|
||||
if (this.#state.hasSeen(messageId)) return;
|
||||
await this.#state.markSeen(messageId);
|
||||
if (access.reason === 'command-not-allowed') {
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
await this.#send(sender, t(COMMAND_PERMISSION_DENIED_MESSAGE), contextToken, runId);
|
||||
this.#status.messagesReplied += 1;
|
||||
this.#status.lastReplyAt = new Date().toISOString();
|
||||
} else {
|
||||
this.#status.messagesRejected += 1;
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
}
|
||||
this.#status.lastError = null;
|
||||
}).catch((error) => {
|
||||
if (this.#signal?.aborted) return;
|
||||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#logger.error?.('[dsh-weixin] failed to apply inbound access policy:', error);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
this.#commandTasks.delete(task);
|
||||
});
|
||||
this.#commandTasks.add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async waitForIdle() {
|
||||
await Promise.allSettled([
|
||||
...this.#queues.values(),
|
||||
|
|
@ -590,12 +639,6 @@ export class WeixinHarnessBridge {
|
|||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
}
|
||||
if (sender !== this.#ownerUserId) {
|
||||
this.#status.messagesRejected += 1;
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
return;
|
||||
}
|
||||
|
||||
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
||||
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
||||
let batchSettled = batchSubmission === null;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export class WeixinRuntime {
|
|||
#harness;
|
||||
#state;
|
||||
#contextEnhancement;
|
||||
#accessPolicy;
|
||||
#logger;
|
||||
#replyTimeoutMs;
|
||||
#maxMessageChars;
|
||||
|
|
@ -126,6 +127,7 @@ export class WeixinRuntime {
|
|||
harness,
|
||||
state,
|
||||
contextEnhancement,
|
||||
accessPolicy,
|
||||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
maxMessageChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
||||
|
|
@ -140,6 +142,7 @@ export class WeixinRuntime {
|
|||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
this.#contextEnhancement = contextEnhancement;
|
||||
this.#accessPolicy = accessPolicy;
|
||||
this.#logger = logger;
|
||||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
this.#maxMessageChars = maxMessageChars;
|
||||
|
|
@ -182,6 +185,7 @@ export class WeixinRuntime {
|
|||
harness: this.#harness,
|
||||
state: this.#state,
|
||||
contextEnhancement: this.#contextEnhancement,
|
||||
accessPolicy: this.#accessPolicy,
|
||||
status: this.#status,
|
||||
logger: this.#logger,
|
||||
replyTimeoutMs: this.#replyTimeoutMs,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue