mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 08:10:50 +08:00
feat: add private chat batch input commands
This commit is contained in:
parent
f6cc1cae30
commit
068cf57259
28 changed files with 2665 additions and 219 deletions
209
src/channels/shared/batch-input.mjs
Normal file
209
src/channels/shared/batch-input.mjs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
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 }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -234,14 +234,15 @@ export default {
|
|||
'/watch ID 关注会话(完成后推送)': '/watch ID Watch a session (push on completion)',
|
||||
'/watchlist 关注列表': '/watchlist List watched sessions',
|
||||
'/unwatch ID 取消关注': '/unwatch ID Stop watching a session',
|
||||
'📦 批量输入(仅私聊)': '📦 Batch input (direct messages only)',
|
||||
'🤖 预设 / 模型': '🤖 Presets / models',
|
||||
'/models 列出模型': '/models List models',
|
||||
'🎮 任务控制': '🎮 Task controls',
|
||||
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
||||
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
|
||||
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
|
||||
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/repair` — 修复卡片按钮':
|
||||
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/repair` — repair card buttons',
|
||||
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 修复卡片按钮':
|
||||
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — repair card buttons',
|
||||
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**修复 · **6**帮助':
|
||||
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Repair · **6** Help',
|
||||
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
||||
|
|
|
|||
|
|
@ -96,4 +96,53 @@ export default {
|
|||
|
||||
// agent-preset.mjs
|
||||
'Agent Preset 无效。': 'Invalid Agent Preset.',
|
||||
|
||||
// batch-input.mjs
|
||||
'/batch 开始批量输入(仅私聊,最多 10 条文字)':
|
||||
'/batch Start batch input (direct messages only, up to 10 text messages)',
|
||||
'/send 提交当前批次': '/send Submit the current batch',
|
||||
'/cancel 取消当前批次': '/cancel Cancel the current batch',
|
||||
'开始批量输入(仅私聊)': 'Start batch input (direct messages only)',
|
||||
'提交当前批次': 'Submit the current batch',
|
||||
'取消当前批次': 'Cancel the current batch',
|
||||
'当前已处于批量输入模式,已收集 {count}/{limit} 条。\n请发送 /send 提交或 /cancel 取消。':
|
||||
'Batch input is already active with {count}/{limit} messages collected.\nSend /send to submit or /cancel to cancel.',
|
||||
'当前已处于批量输入模式,已收集 {count}/{limit} 条。\n完成后发送 /send,取消请发送 /cancel。':
|
||||
'Batch input is already active with {count}/{limit} messages collected.\nSend /send when finished or /cancel to cancel.',
|
||||
'[消息 {index}]': '[Message {index}]',
|
||||
'以下是用户通过批量输入模式发送的多条内容,请按顺序作为同一次输入统一处理。':
|
||||
'The user sent the following messages in batch input mode. Process them in order as one input.',
|
||||
'批量输入模式仅支持私聊,请在与机器人的私聊中使用。':
|
||||
'Batch input is available only in direct messages. Please use it in a direct chat with the bot.',
|
||||
'当前聊天有正在运行的任务、待回答问题或待审批请求。\n请先完成当前交互或发送 /stop,再使用 /batch。':
|
||||
'This chat has a running task, unanswered question, or pending approval.\nFinish the current interaction or send /stop before using /batch.',
|
||||
'用法:/{command}(不带参数)': 'Usage: /{command} (without arguments)',
|
||||
'批量输入命令仅支持纯文字,请移除图片或文件后重试。':
|
||||
'Batch input commands support text only. Remove the image or file and try again.',
|
||||
'当前没有待提交的批量内容,请先发送 /batch。':
|
||||
'There is no batch to submit. Send /batch first.',
|
||||
'当前没有正在进行的批量输入。': 'There is no active batch input.',
|
||||
'已进入批量输入模式,最多可发送 {limit} 条文字。\n完成后发送 /send,取消请发送 /cancel。':
|
||||
'Batch input started. You can send up to {limit} text messages.\nSend /send when finished or /cancel to cancel.',
|
||||
'批量输入模式目前仅支持文字,这条消息未收录。\n请继续发送文字,或使用 /send、/cancel。':
|
||||
'Batch input currently supports text only, so this message was not collected.\nContinue with text, or use /send or /cancel.',
|
||||
'当前批次正在提交,请勿重复发送 /send。':
|
||||
'The current batch is being submitted. Do not send /send again.',
|
||||
'批量内容已经提交,无法取消。\n如需停止当前任务,请发送 /stop。':
|
||||
'The batch has already been submitted and cannot be cancelled.\nSend /stop if you need to stop the current task.',
|
||||
'当前批次正在提交,请等待处理完成后再开启新批次。':
|
||||
'The current batch is being submitted. Wait for it to finish before starting another batch.',
|
||||
'已取消批量输入。': 'Batch input cancelled.',
|
||||
'已取消批量输入,共丢弃 {count} 条消息。':
|
||||
'Batch input cancelled; {count} messages were discarded.',
|
||||
'当前批次还没有内容,请先发送文字,或使用 /cancel 取消。':
|
||||
'The current batch is empty. Send some text first or use /cancel.',
|
||||
'当前正在批量输入,请先发送 /send 提交或 /cancel 取消。':
|
||||
'Batch input is active. Send /send to submit or /cancel to cancel first.',
|
||||
'当前批次已满,这条消息未收录。\n请先发送 /send 提交或 /cancel 取消,然后重新发送这条消息。':
|
||||
'The current batch is full, so this message was not collected.\nSend /send or /cancel first, then resend this message.',
|
||||
'已收集 {count}/{limit} 条,当前批次已满,请发送 /send 提交或 /cancel 取消。':
|
||||
'Collected {count}/{limit} messages. The batch is full; send /send or /cancel.',
|
||||
'批量内容提交失败,已保留 {count} 条消息。\n请再次发送 /send 重试或 /cancel 取消。':
|
||||
'Batch submission failed; {count} messages were retained.\nSend /send to retry or /cancel to cancel.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ import {
|
|||
} from './preset-command.mjs';
|
||||
import { askInWorkspaceSession } from './workspace-session.mjs';
|
||||
import { HarnessApprovalQueue } from './harness-approval.mjs';
|
||||
import {
|
||||
BatchInputManager,
|
||||
batchInputBusyMessage,
|
||||
batchInputGroupUnsupportedMessage,
|
||||
isBatchInputCommand,
|
||||
} from './batch-input.mjs';
|
||||
import {
|
||||
hasInboundImages,
|
||||
imagePromptUserMessage,
|
||||
|
|
@ -119,6 +125,7 @@ export class TextHarnessBridge {
|
|||
#approvalTasks = new Set();
|
||||
#commandTasks = new Set();
|
||||
#approvals;
|
||||
#batches = new BatchInputManager();
|
||||
|
||||
constructor({
|
||||
descriptor,
|
||||
|
|
@ -173,7 +180,54 @@ export class TextHarnessBridge {
|
|||
const key = `${kind}:${conversationId}`;
|
||||
const pending = this.#pendingInteractions.get(key);
|
||||
const text = cleanText(normalized.content);
|
||||
const commandRunner = hasInboundFiles(normalized) ? null : isControlCommand(text)
|
||||
const batchCommand = isBatchInputCommand(text);
|
||||
if (batchCommand && normalized.kind === 'group' && normalized.addressed === true) {
|
||||
return this.#finishLocalMessage(
|
||||
normalized,
|
||||
messageId,
|
||||
batchInputGroupUnsupportedMessage(),
|
||||
);
|
||||
}
|
||||
if (batchCommand && normalized.kind === 'direct') {
|
||||
const exactBatch = /^\/batch$/iu.test(text);
|
||||
if (exactBatch && (
|
||||
this.#queues.has(key)
|
||||
|| Boolean(pending)
|
||||
|| this.#approvals.hasPending(key)
|
||||
)) {
|
||||
return this.#finishLocalMessage(normalized, messageId, batchInputBusyMessage());
|
||||
}
|
||||
const batch = this.#batches.handle(key, text, {
|
||||
plainText: Boolean(text)
|
||||
&& normalized.plainText !== false
|
||||
&& !hasInboundImages(normalized)
|
||||
&& !hasInboundFiles(normalized),
|
||||
});
|
||||
if (batch.handled) {
|
||||
if (batch.kind === 'submit') {
|
||||
return this.#enqueueMessage({
|
||||
...normalized,
|
||||
content: batch.prompt,
|
||||
batchSubmission: { token: batch.token },
|
||||
}, messageId, senderId, key);
|
||||
}
|
||||
return this.#finishLocalMessage(normalized, messageId, batch.message);
|
||||
}
|
||||
} else if (normalized.kind === 'direct'
|
||||
&& this.#batches.status(key).phase === 'collecting') {
|
||||
const batch = this.#batches.handle(key, text, {
|
||||
plainText: Boolean(text)
|
||||
&& normalized.plainText !== false
|
||||
&& !hasInboundImages(normalized)
|
||||
&& !hasInboundFiles(normalized),
|
||||
});
|
||||
if (batch.handled) {
|
||||
return this.#finishLocalMessage(normalized, messageId, batch.message);
|
||||
}
|
||||
}
|
||||
const collectingBatch = normalized.kind === 'direct'
|
||||
&& this.#batches.status(key).phase === 'collecting';
|
||||
const commandRunner = collectingBatch || hasInboundFiles(normalized) ? null : isControlCommand(text)
|
||||
? runControlCommand
|
||||
: (isModelCommand(text)
|
||||
? runModelCommand
|
||||
|
|
@ -254,6 +308,30 @@ export class TextHarnessBridge {
|
|||
return this.#enqueueMessage(normalized, messageId, senderId, key);
|
||||
}
|
||||
|
||||
#finishLocalMessage(message, messageId, reply) {
|
||||
let task;
|
||||
task = (async () => {
|
||||
if (this.#state.hasSeen(messageId)) return;
|
||||
await this.#state.markSeen(messageId);
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
||||
this.#status.lastError = null;
|
||||
})().catch(async (error) => {
|
||||
if (this.#signal?.aborted) return;
|
||||
this.#status.lastError = error?.message ?? String(error);
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to process a batch input message:`,
|
||||
error,
|
||||
);
|
||||
}).finally(() => {
|
||||
this.#acceptedMessageIds.delete(messageId);
|
||||
this.#commandTasks.delete(task);
|
||||
});
|
||||
this.#commandTasks.add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
#enqueueMessage(message, messageId, senderId, key, {
|
||||
releaseMessageId = true,
|
||||
alreadyRecorded = false,
|
||||
|
|
@ -373,6 +451,7 @@ export class TextHarnessBridge {
|
|||
|
||||
const target = message.replyTarget;
|
||||
const text = cleanText(message.content);
|
||||
const batchSubmission = message.batchSubmission;
|
||||
let stream = null;
|
||||
let semanticStream = false;
|
||||
try {
|
||||
|
|
@ -411,6 +490,9 @@ export class TextHarnessBridge {
|
|||
t('/preset --default 跟随 Host 默认'),
|
||||
t('/stop 停止当前任务'),
|
||||
t('/steer 补充指令 纠偏当前任务'),
|
||||
t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
|
||||
t('/send 提交当前批次'),
|
||||
t('/cancel 取消当前批次'),
|
||||
t('/status 检查连接状态'),
|
||||
t('/help 显示本帮助'),
|
||||
].join('\n'));
|
||||
|
|
@ -508,6 +590,9 @@ export class TextHarnessBridge {
|
|||
files: message.files,
|
||||
},
|
||||
});
|
||||
if (batchSubmission) {
|
||||
this.#batches.complete(conversationKey, batchSubmission.token);
|
||||
}
|
||||
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
||||
const visibleAnswer = fileOnlyCompletion
|
||||
? t(FILE_ONLY_COMPLETION_TEXT)
|
||||
|
|
@ -579,7 +664,14 @@ export class TextHarnessBridge {
|
|||
}
|
||||
return delivery.receipt;
|
||||
} catch (error) {
|
||||
if (error?.code === 'turn-stopped') {
|
||||
const turnStopped = error?.code === 'turn-stopped';
|
||||
if (batchSubmission && turnStopped) {
|
||||
this.#batches.complete(conversationKey, batchSubmission.token);
|
||||
}
|
||||
const failedBatch = batchSubmission && !turnStopped
|
||||
? this.#batches.fail(conversationKey, batchSubmission.token)
|
||||
: null;
|
||||
if (turnStopped) {
|
||||
if (stream) {
|
||||
try {
|
||||
await stream.finish(t('已停止。'));
|
||||
|
|
@ -607,6 +699,23 @@ export class TextHarnessBridge {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
if (failedBatch?.retained) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to submit a batch input:`,
|
||||
error,
|
||||
);
|
||||
if (await presentStreamFailure(failedBatch.message)) return;
|
||||
stream?.cancel?.();
|
||||
try {
|
||||
await this.#bot.sendText(target, failedBatch.message);
|
||||
} catch (sendError) {
|
||||
this.#logger.error?.(
|
||||
`[dsh-im:${this.#descriptor.key}] failed to send the batch retry notice:`,
|
||||
sendError,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const imageErrorMessage = imagePromptUserMessage(error);
|
||||
if (imageErrorMessage) {
|
||||
if (await presentStreamFailure(imageErrorMessage)) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue