dsh-im-ops/src/channels/shared/batch-input.mjs

209 lines
7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { t } from './i18n.mjs';
export const BATCH_INPUT_LIMIT = 10;
const BATCH_COMMAND = /^\/(batch|send|cancel)(?=$|\s)/iu;
const EXACT_BATCH_COMMAND = /^\/(batch|send|cancel)$/iu;
function commandName(text) {
if (typeof text !== 'string') return null;
return BATCH_COMMAND.exec(text.trim())?.[1]?.toLowerCase() ?? null;
}
function result(kind, message, extra = {}) {
return { handled: true, kind, ...(message ? { message } : {}), ...extra };
}
function progressMessage(count) {
if (count === BATCH_INPUT_LIMIT) {
return t(`当前已处于批量输入模式,已收集 {count}/{limit} 条。
请发送 /send 提交或 /cancel 取消。`, { count, limit: BATCH_INPUT_LIMIT });
}
return t(`当前已处于批量输入模式,已收集 {count}/{limit} 条。
完成后发送 /send,取消请发送 /cancel。`, { count, limit: BATCH_INPUT_LIMIT });
}
function submissionPrompt(messages) {
const sections = messages.map((message, index) => (
`${t('[消息 {index}]', { index: index + 1 })}\n${message}`
));
return [
t('以下是用户通过批量输入模式发送的多条内容,请按顺序作为同一次输入统一处理。'),
...sections,
].join('\n\n');
}
export function isBatchInputCommand(text) {
return commandName(text) !== null;
}
export function batchInputGroupUnsupportedMessage() {
return t('批量输入模式仅支持私聊,请在与机器人的私聊中使用。');
}
export function batchInputBusyMessage() {
return t(`当前聊天有正在运行的任务、待回答问题或待审批请求。
请先完成当前交互或发送 /stop,再使用 /batch。`);
}
export class BatchInputManager {
#batches = new Map();
status(key) {
const batch = this.#batches.get(key);
if (!batch) {
return Object.freeze({ phase: 'idle', count: 0, limit: BATCH_INPUT_LIMIT, full: false });
}
return Object.freeze({
phase: batch.phase,
count: batch.messages.length,
limit: BATCH_INPUT_LIMIT,
full: batch.messages.length === BATCH_INPUT_LIMIT,
});
}
handle(key, text, { plainText = true } = {}) {
const batch = this.#batches.get(key);
const name = commandName(text);
const exact = typeof text === 'string' ? EXACT_BATCH_COMMAND.exec(text.trim()) : null;
if (name && !exact) {
return result('invalid-command', t('用法:/{command}(不带参数)', { command: name }));
}
if (!batch) {
if (!name) return { handled: false };
if (!plainText) {
return result('unsupported-content', t('批量输入命令仅支持纯文字,请移除图片或文件后重试。'));
}
if (name === 'send') {
return result('no-batch', t('当前没有待提交的批量内容,请先发送 /batch。'));
}
if (name === 'cancel') {
return result('no-batch', t('当前没有正在进行的批量输入。'));
}
this.#batches.set(key, { phase: 'collecting', messages: [], token: null });
return result('started', t(`已进入批量输入模式,最多可发送 {limit} 条文字。
完成后发送 /send,取消请发送 /cancel。`, { limit: BATCH_INPUT_LIMIT }), {
count: 0,
limit: BATCH_INPUT_LIMIT,
});
}
if (!plainText && (batch.phase === 'collecting' || name)) {
return result('unsupported-content', t(`批量输入模式目前仅支持文字,这条消息未收录。
请继续发送文字,或使用 /send、/cancel。`), {
count: batch.messages.length,
limit: BATCH_INPUT_LIMIT,
});
}
if (batch.phase === 'submitting') {
if (name === 'send') {
return result('submitting', t('当前批次正在提交,请勿重复发送 /send。'));
}
if (name === 'cancel') {
return result('submitting', t(`批量内容已经提交,无法取消。
如需停止当前任务,请发送 /stop。`));
}
if (name === 'batch') {
return result('submitting', t('当前批次正在提交,请等待处理完成后再开启新批次。'));
}
return { handled: false };
}
if (name === 'batch') {
return result('status', progressMessage(batch.messages.length), {
count: batch.messages.length,
limit: BATCH_INPUT_LIMIT,
});
}
if (name === 'cancel') {
const count = batch.messages.length;
this.#batches.delete(key);
return result('cancelled', count === 0
? t('已取消批量输入。')
: t('已取消批量输入,共丢弃 {count} 条消息。', { count }), { count });
}
if (name === 'send') {
if (batch.messages.length === 0) {
return result('empty', t('当前批次还没有内容,请先发送文字,或使用 /cancel 取消。'), {
count: 0,
});
}
const messages = Object.freeze([...batch.messages]);
const token = Object.freeze({});
batch.phase = 'submitting';
batch.token = token;
return result('submit', null, {
token,
messages,
prompt: submissionPrompt(messages),
count: messages.length,
});
}
if (typeof text !== 'string') {
return result('unsupported-content', t(`批量输入模式目前仅支持文字,这条消息未收录。
请继续发送文字,或使用 /send、/cancel。`), {
count: batch.messages.length,
limit: BATCH_INPUT_LIMIT,
});
}
if (text.trim().startsWith('/')) {
return result('blocked-command', t('当前正在批量输入,请先发送 /send 提交或 /cancel 取消。'), {
count: batch.messages.length,
limit: BATCH_INPUT_LIMIT,
});
}
if (batch.messages.length === BATCH_INPUT_LIMIT) {
return result('full', t(`当前批次已满,这条消息未收录。
请先发送 /send 提交或 /cancel 取消,然后重新发送这条消息。`), {
count: BATCH_INPUT_LIMIT,
limit: BATCH_INPUT_LIMIT,
});
}
batch.messages.push(text);
const count = batch.messages.length;
return result('collected', count === BATCH_INPUT_LIMIT
? t('已收集 {count}/{limit} 条,当前批次已满,请发送 /send 提交或 /cancel 取消。', {
count,
limit: BATCH_INPUT_LIMIT,
})
: null, {
count,
limit: BATCH_INPUT_LIMIT,
});
}
complete(key, token) {
const batch = this.#batches.get(key);
if (!batch || batch.phase !== 'submitting' || batch.token !== token) {
return Object.freeze({ completed: false });
}
const count = batch.messages.length;
this.#batches.delete(key);
return Object.freeze({ completed: true, count });
}
fail(key, token) {
const batch = this.#batches.get(key);
if (!batch || batch.phase !== 'submitting' || batch.token !== token) {
return Object.freeze({ retained: false });
}
batch.phase = 'collecting';
batch.token = null;
const count = batch.messages.length;
return Object.freeze({
retained: true,
count,
message: t(`批量内容提交失败,已保留 {count} 条消息。
请再次发送 /send 重试或 /cancel 取消。`, { count }),
});
}
}