mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 22:03:18 +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
|
|
@ -58,6 +58,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 CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
||||
|
|
@ -374,6 +378,7 @@ export class DingtalkHarnessBridge {
|
|||
#harness;
|
||||
#state;
|
||||
#contextEnhancement;
|
||||
#accessPolicy;
|
||||
#status;
|
||||
#logger;
|
||||
#replyTimeoutMs;
|
||||
|
|
@ -397,6 +402,7 @@ export class DingtalkHarnessBridge {
|
|||
harness,
|
||||
state,
|
||||
contextEnhancement,
|
||||
accessPolicy,
|
||||
status = createDingtalkBridgeStatus(),
|
||||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
|
|
@ -415,6 +421,7 @@ export class DingtalkHarnessBridge {
|
|||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
this.#contextEnhancement = contextEnhancement;
|
||||
this.#accessPolicy = accessPolicy;
|
||||
this.#status = status;
|
||||
this.#logger = logger;
|
||||
this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
|
||||
|
|
@ -439,17 +446,13 @@ export class DingtalkHarnessBridge {
|
|||
const sender = senderStaffId(message);
|
||||
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
||||
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
||||
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
|
||||
this.#contextEnhancement,
|
||||
message.conversationType === '1' || message.conversationType === 1 ? 'direct'
|
||||
: message.conversationType === '2' || message.conversationType === 2 ? 'group' : null,
|
||||
) : contextSnapshot);
|
||||
const conversationType = String(message.conversationType) === '2' ? 'group'
|
||||
: String(message.conversationType) === '1' ? 'direct' : null;
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = conversationKey(message, sender);
|
||||
} catch {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
increment(this.#status, 'messagesRejected');
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
return Promise.resolve();
|
||||
|
|
@ -461,9 +464,6 @@ export class DingtalkHarnessBridge {
|
|||
} catch {
|
||||
// An unsafe reply route must never be able to submit an approval.
|
||||
}
|
||||
if (sessionWebhook && String(message.conversationType) !== '2') {
|
||||
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
||||
}
|
||||
const pending = this.#pendingInteractions.get(key);
|
||||
const promptMessage = dingtalkInboundMessage(message, {
|
||||
api: this.#api,
|
||||
|
|
@ -473,6 +473,26 @@ export class DingtalkHarnessBridge {
|
|||
const commandText = nonEmptyString(promptMessage.content) ?? '';
|
||||
const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
|
||||
const direct = String(message.conversationType) !== '2';
|
||||
if (addressed) {
|
||||
const access = evaluateInboundAccess(this.#accessPolicy, {
|
||||
conversationType,
|
||||
senderIds: sender,
|
||||
text: commandText,
|
||||
hasImages: hasInboundImages(promptMessage),
|
||||
hasFiles: hasInboundFiles(promptMessage),
|
||||
});
|
||||
if (!access.allowed) {
|
||||
this.#acceptedMessageIds.set(messageId, null);
|
||||
return this.#finishAccessDecision(message, messageId, sessionWebhook, access);
|
||||
}
|
||||
}
|
||||
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
|
||||
this.#contextEnhancement,
|
||||
conversationType,
|
||||
) : contextSnapshot);
|
||||
if (sessionWebhook && direct) {
|
||||
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
||||
}
|
||||
const statusReaction = sessionWebhook && addressed ? this.#startStatusReaction(message) : null;
|
||||
const finish = (task) => Promise.resolve(task).then(
|
||||
(value) => {
|
||||
|
|
@ -855,6 +875,38 @@ export class DingtalkHarnessBridge {
|
|||
return task;
|
||||
}
|
||||
|
||||
#finishAccessDecision(message, messageId, sessionWebhook, access) {
|
||||
let task;
|
||||
task = Promise.resolve().then(async () => {
|
||||
if (this.#state.hasSeen(messageId)) return;
|
||||
await this.#state.markSeen(messageId);
|
||||
if (access.reason === 'command-not-allowed' && sessionWebhook) {
|
||||
increment(this.#status, 'messagesReceived');
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
await this.#send(
|
||||
sessionWebhook,
|
||||
t(COMMAND_PERMISSION_DENIED_MESSAGE),
|
||||
this.#atUsersFor(message),
|
||||
);
|
||||
increment(this.#status, 'messagesReplied');
|
||||
this.#status.lastReplyAt = new Date().toISOString();
|
||||
} else {
|
||||
increment(this.#status, 'messagesRejected');
|
||||
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-dingtalk] failed to apply inbound access policy', error);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
this.#commandTasks.delete(task);
|
||||
});
|
||||
this.#commandTasks.add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async #process(message, messageId, sender, key, {
|
||||
alreadyRecorded = false,
|
||||
preparedMessage,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export class DingtalkRuntime {
|
|||
#harness;
|
||||
#state;
|
||||
#contextEnhancement;
|
||||
#accessPolicy;
|
||||
#logger;
|
||||
#replyTimeoutMs;
|
||||
#maxMessageChars;
|
||||
|
|
@ -152,6 +153,7 @@ export class DingtalkRuntime {
|
|||
harness,
|
||||
state,
|
||||
contextEnhancement,
|
||||
accessPolicy,
|
||||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
maxMessageChars = 4_000,
|
||||
|
|
@ -170,6 +172,7 @@ export class DingtalkRuntime {
|
|||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
this.#contextEnhancement = contextEnhancement;
|
||||
this.#accessPolicy = accessPolicy;
|
||||
this.#logger = logger;
|
||||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
this.#maxMessageChars = maxMessageChars;
|
||||
|
|
@ -238,6 +241,7 @@ export class DingtalkRuntime {
|
|||
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