mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 10:33:21 +08:00
feat: add bounded history previews across nine channels
Add shared /history parsing, read-only session access, message filtering, and bounded replies. Default to three messages, cap at five, and exclude saved history commands. Include channel integration tests, translations, command help, and the Issue #62 implementation plan.
This commit is contained in:
parent
bf0e157cd4
commit
8c6c31a1b6
23 changed files with 1965 additions and 221 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '../shared/harness-question.mjs';
|
||||
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -67,6 +68,7 @@ const HELP_TEXT_LINES = [
|
|||
'直接发送文字、图片或文件即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/compact 压缩当前会话的较早上下文',
|
||||
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
|
|
@ -522,7 +524,8 @@ export class DingtalkHarnessBridge {
|
|||
));
|
||||
}
|
||||
}
|
||||
const commandRunner = hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
||||
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
||||
: hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
|
|
@ -789,6 +792,7 @@ export class DingtalkHarnessBridge {
|
|||
key,
|
||||
{
|
||||
signal: this.#signal,
|
||||
isDirect: String(message.conversationType) === '1',
|
||||
hasImages: hasInboundImages(prompt),
|
||||
hasFiles: hasInboundFiles(prompt),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
isBatchInputCommand,
|
||||
} from '../shared/batch-input.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -620,12 +621,15 @@ export class FeishuHarnessBridge {
|
|||
.finally(() => this.#acceptedMessageIds.delete(messageId));
|
||||
return processing;
|
||||
}
|
||||
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
||||
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
||||
: hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
: (isPresetCommand(commandText) ? runPresetCommand : null));
|
||||
if (commandRunner && addressed) {
|
||||
// In all-message group mode, history must still be refused locally rather
|
||||
// than becoming a normal prompt when no mention is present.
|
||||
if (commandRunner && (addressed || commandRunner === runHistoryCommand)) {
|
||||
const processing = this.#processFastCommand(
|
||||
event,
|
||||
messageId,
|
||||
|
|
@ -913,6 +917,7 @@ export class FeishuHarnessBridge {
|
|||
key,
|
||||
{
|
||||
signal: this.#signal,
|
||||
isDirect: event.message.chat_type === 'p2p',
|
||||
hasImages: hasInboundImages(message),
|
||||
hasFiles: hasInboundFiles(message),
|
||||
pendingInteraction: this.#hasPendingInteraction(key),
|
||||
|
|
@ -2083,7 +2088,10 @@ export class FeishuHarnessBridge {
|
|||
try {
|
||||
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
||||
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || t('暂无标题');
|
||||
await this.#send(chatId, t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }));
|
||||
await this.#send(chatId, [
|
||||
t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }),
|
||||
t('发送 /history 查看最近对话。'),
|
||||
].join('\n'));
|
||||
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
||||
} catch (error) {
|
||||
await this.#sendFailure(chatId, error, {
|
||||
|
|
|
|||
|
|
@ -497,6 +497,7 @@ export function menuHelpText() {
|
|||
'/status 连接状态',
|
||||
'/version 查看插件版本',
|
||||
'/compact 压缩当前会话上下文',
|
||||
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
||||
'/archived on/off 会话列表显示/隐藏归档',
|
||||
'',
|
||||
'👁 关注',
|
||||
|
|
@ -593,6 +594,7 @@ export function helpCard(extraTextLines = []) {
|
|||
{ tag: 'div', text: markdown([
|
||||
t(HELP_TEXT_COMMANDS),
|
||||
t('`/version` — 查看插件版本'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
].join('\n') + extraText) },
|
||||
{ tag: 'hr' },
|
||||
{ tag: 'div', text: markdown(t(HELP_NUMBER_FALLBACK)) },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -78,6 +79,7 @@ function helpText() {
|
|||
t('直接发送文字、图片或文件即可继续当前会话。'),
|
||||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
|
|
@ -468,7 +470,8 @@ export class QqHarnessBridge {
|
|||
return this.#finishBatchResult(message, messageId, result);
|
||||
}
|
||||
}
|
||||
const commandRunner = hasQqFileAttachments(message) ? null : isControlCommand(commandText)
|
||||
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
||||
: hasQqFileAttachments(message) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
|
|
@ -602,6 +605,7 @@ export class QqHarnessBridge {
|
|||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
const result = await runner(text, this.#harness, this.#state, key, {
|
||||
signal: this.#signal,
|
||||
isDirect: message.kind === 'c2c',
|
||||
hasImages: hasQqImageAttachments(message),
|
||||
hasFiles: hasQqFileAttachments(message),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|
|
|
|||
|
|
@ -834,6 +834,9 @@ export function createBotWorkspaceScope(
|
|||
models(...args) {
|
||||
return invokeCurrentSession('getSessionModels', args, 'model listing');
|
||||
},
|
||||
readHistory(...args) {
|
||||
return invokeCurrentSession('readSessionHistory', args, 'history read');
|
||||
},
|
||||
selectModel(...args) {
|
||||
return invokeCurrentSession('selectSessionModel', args, 'model selection');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -911,6 +911,19 @@ export class HarnessClient {
|
|||
}
|
||||
}
|
||||
|
||||
async readSessionHistory(sessionId, { maxMessages = 50, beforeSeq, timeoutMs = 10_000, ...options } = {}) {
|
||||
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
||||
if (!Number.isSafeInteger(maxMessages) || maxMessages < 1
|
||||
|| (beforeSeq !== undefined && (!Number.isSafeInteger(beforeSeq) || beforeSeq < 0))) {
|
||||
throw new TypeError('Invalid history pagination');
|
||||
}
|
||||
return this.rpc('session.history', {
|
||||
sessionId,
|
||||
maxMessages,
|
||||
...(beforeSeq === undefined ? {} : { beforeSeq }),
|
||||
}, timeoutMs, options);
|
||||
}
|
||||
|
||||
async sessionExists(sessionId, options = {}) {
|
||||
try {
|
||||
await this.rpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
|
||||
|
|
|
|||
193
src/channels/shared/history-command.mjs
Normal file
193
src/channels/shared/history-command.mjs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import { t } from './i18n.mjs';
|
||||
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
||||
const HISTORY_COMMAND = /^\/history(?=$|\s)([\s\S]*)$/iu;
|
||||
const HISTORY_USAGE = '用法:/history [数量](默认 3 条,最多 5 条)';
|
||||
const MAX_MESSAGES = 5;
|
||||
const PAGE_SIZE = 50;
|
||||
const MAX_PAGES = 3;
|
||||
const READ_TIMEOUT_MS = 10_000;
|
||||
|
||||
function commandResult(message) {
|
||||
return { handled: true, message, messages: splitWorkspaceCommandMessage(message) };
|
||||
}
|
||||
|
||||
function previewText(content) {
|
||||
if (!Array.isArray(content)) throw new TypeError('Invalid history message content');
|
||||
return content.flatMap((block) => {
|
||||
if (block?.type === 'text' && typeof block.text === 'string') return [block.text];
|
||||
if (block?.type === 'image') return [t('[图片]')];
|
||||
if (block?.type === 'file') return [t('[文件]')];
|
||||
return [];
|
||||
}).join('\n').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '').trim()
|
||||
|| t('本条没有可预览的文字。');
|
||||
}
|
||||
|
||||
function visibleMessages(events) {
|
||||
const messages = [];
|
||||
const assistants = new Map();
|
||||
for (const event of events) {
|
||||
const data = event.data;
|
||||
if (event.type === 'user/message' && event.surfaceOp === 'append'
|
||||
&& data?.source?.kind === 'user') {
|
||||
// Older sessions may contain commands saved before the local fast path
|
||||
// existed. Exclude them before counting, so normal messages fill the limit.
|
||||
if (isHistoryCommand(previewText(data.content))) continue;
|
||||
messages.push({ seq: event.seq, role: 'user', content: data.content });
|
||||
} else if (event.type === 'assistant/message' && event.surfaceOp === 'append'
|
||||
&& Number.isSafeInteger(data?.turn) && data.turn >= 0) {
|
||||
// Remember even an empty/interrupted final message: never substitute an
|
||||
// earlier tool-step explanation for the turn's final answer.
|
||||
assistants.set(data.turn, event);
|
||||
} else if (event.type === 'turn/end') {
|
||||
const assistant = assistants.get(data?.turn);
|
||||
assistants.delete(data?.turn);
|
||||
if ((data?.reason?.kind ?? data?.reason) !== 'completed'
|
||||
|| !assistant || assistant.data.interrupted === true) continue;
|
||||
messages.push({
|
||||
seq: assistant.seq,
|
||||
role: 'assistant',
|
||||
content: assistant.data.message?.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
return messages.sort((left, right) => left.seq - right.seq);
|
||||
}
|
||||
|
||||
function truncateText(text, limit) {
|
||||
if (text.length <= limit) return text;
|
||||
const suffix = t('(已截断)');
|
||||
let end = Math.max(0, limit - suffix.length);
|
||||
const last = text.charCodeAt(end - 1);
|
||||
if (last >= 0xd800 && last <= 0xdbff) end -= 1;
|
||||
return `${text.slice(0, Math.max(0, end))}${suffix}`;
|
||||
}
|
||||
|
||||
function formatHistory(sessionId, records, requested, hasMore) {
|
||||
const shortId = sessionId.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '').slice(0, 12);
|
||||
const header = t('会话历史|{session}|最近 {count} 条', { session: shortId, count: records.length });
|
||||
const footer = [t('以上为历史记录,不是本次新回复。')];
|
||||
if (records.length < requested) {
|
||||
footer.unshift(hasMore
|
||||
? t('本次有限读取中仅找到 {count} 条可预览消息。', { count: records.length })
|
||||
: t('当前会话仅有 {count} 条可预览消息。', { count: records.length }));
|
||||
}
|
||||
const entries = records.map((record) => ({ ...record, text: previewText(record.content) }));
|
||||
let bodyLimit = 500;
|
||||
let result;
|
||||
do {
|
||||
const sections = entries.map((entry, index) => (
|
||||
`${index + 1}. ${entry.role === 'user' ? t('用户') : t('助手')}\n${truncateText(entry.text, bodyLimit)}`
|
||||
));
|
||||
result = commandResult([header, ...sections, footer.join('\n')].join('\n\n'));
|
||||
bodyLimit = Math.floor(bodyLimit / 2);
|
||||
} while (bodyLimit > 0 && (result.message.length > 3_000 || result.messages.length > 3));
|
||||
return result;
|
||||
}
|
||||
|
||||
function historyErrorMessage(error) {
|
||||
const code = error?.code ?? error?.failure?.code;
|
||||
if (code === 'session-not-found') return t('当前聊天绑定的会话已不存在,请重新绑定会话。');
|
||||
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found'
|
||||
|| code === 'session-binding-changed') return t('会话、工作区或机器人状态已发生变化,请重新执行 /history。');
|
||||
if (code === 'harness-api-not-found') return t('当前 Harness 暂不支持读取会话历史。');
|
||||
if (error?.name === 'AbortError') return t('历史读取已取消。');
|
||||
if (error?.name === 'TimeoutError' || code === 'harness-timeout') return t('读取历史超时,请稍后重试。');
|
||||
return t('暂时无法读取会话历史,请稍后重试。');
|
||||
}
|
||||
|
||||
export function isHistoryCommand(text) {
|
||||
return typeof text === 'string' && HISTORY_COMMAND.test(text.trim());
|
||||
}
|
||||
|
||||
/** Read the current binding only; never create/resume a session or prompt the model. */
|
||||
export async function runHistoryCommand(text, harness, state, key, {
|
||||
signal: callerSignal,
|
||||
isDirect = false,
|
||||
hasImages = false,
|
||||
hasFiles = false,
|
||||
} = {}) {
|
||||
if (!isHistoryCommand(text)) return null;
|
||||
const argument = HISTORY_COMMAND.exec(text.trim())[1].trim();
|
||||
if (argument && (!/^\d+$/u.test(argument) || /^0+$/u.test(argument))) {
|
||||
return commandResult(t(HISTORY_USAGE));
|
||||
}
|
||||
const count = argument ? Math.min(Number(argument), MAX_MESSAGES) : 3;
|
||||
if (!isDirect) return commandResult(t('请在与机器人的私聊中使用 /history。'));
|
||||
if (hasImages || hasFiles) return commandResult(t('/history 仅支持文字命令,请移除图片或文件后重试。'));
|
||||
const sessionId = state?.sessionFor?.(key);
|
||||
if (typeof sessionId !== 'string' || !sessionId) {
|
||||
return commandResult(t('当前聊天尚未绑定会话,请先发送消息或使用 /session 绑定会话。'));
|
||||
}
|
||||
|
||||
try {
|
||||
const session = harness?.workspaceSession?.(sessionId);
|
||||
if (typeof session?.readHistory !== 'function') {
|
||||
return commandResult(t('当前 Harness 暂不支持读取会话历史。'));
|
||||
}
|
||||
const timeout = AbortSignal.timeout(READ_TIMEOUT_MS);
|
||||
const signal = callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout;
|
||||
const deadline = Date.now() + READ_TIMEOUT_MS;
|
||||
const events = new Map();
|
||||
let beforeSeq;
|
||||
let snapshotEnd;
|
||||
let records = [];
|
||||
let hasMore = false;
|
||||
for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {
|
||||
signal.throwIfAborted();
|
||||
if (Date.now() >= deadline) throw new DOMException('History read timed out', 'TimeoutError');
|
||||
const page = await session.readHistory({
|
||||
maxMessages: PAGE_SIZE,
|
||||
...(beforeSeq === undefined ? {} : { beforeSeq }),
|
||||
timeoutMs: Math.max(1, deadline - Date.now()),
|
||||
signal,
|
||||
});
|
||||
signal.throwIfAborted();
|
||||
if (Date.now() >= deadline) throw new DOMException('History read timed out', 'TimeoutError');
|
||||
if (state.sessionFor(key) !== sessionId) {
|
||||
const error = new Error('Session binding changed during history read');
|
||||
error.code = 'session-binding-changed';
|
||||
throw error;
|
||||
}
|
||||
if (!page || !Array.isArray(page.events) || typeof page.hasMore !== 'boolean') {
|
||||
throw new TypeError('Invalid history page');
|
||||
}
|
||||
let oldestSeq = beforeSeq ?? Infinity;
|
||||
for (const entry of page.events) {
|
||||
const event = entry?.event;
|
||||
if (!event || !Number.isSafeInteger(event.seq) || event.seq < 0
|
||||
|| typeof event.type !== 'string' || !event.type) throw new TypeError('Invalid history event');
|
||||
// Subsequent pages must not extend the snapshot taken by the first read.
|
||||
if (snapshotEnd !== undefined && event.seq > snapshotEnd) continue;
|
||||
const previous = events.get(event.seq);
|
||||
if (previous) {
|
||||
if (!isDeepStrictEqual(previous, event)) throw new TypeError('Conflicting history events');
|
||||
} else {
|
||||
if (beforeSeq !== undefined && event.seq >= beforeSeq) throw new TypeError('Invalid history cursor');
|
||||
events.set(event.seq, event);
|
||||
}
|
||||
oldestSeq = Math.min(oldestSeq, event.seq);
|
||||
}
|
||||
const ordered = [...events.values()].sort((left, right) => left.seq - right.seq);
|
||||
snapshotEnd ??= ordered.at(-1)?.seq;
|
||||
hasMore = page.hasMore;
|
||||
if (hasMore && (!Number.isFinite(oldestSeq) || oldestSeq === beforeSeq)) {
|
||||
throw new TypeError('History cursor did not advance');
|
||||
}
|
||||
records = visibleMessages(ordered).slice(-count);
|
||||
if (records.length >= count || !hasMore) break;
|
||||
beforeSeq = oldestSeq;
|
||||
}
|
||||
if (records.length === 0) {
|
||||
return commandResult(hasMore
|
||||
? t('本次有限读取中未找到可预览的历史消息。')
|
||||
: t('当前会话暂无可预览的历史消息。'));
|
||||
}
|
||||
return formatHistory(sessionId, records, count, hasMore);
|
||||
} catch (error) {
|
||||
return commandResult(historyErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ export default {
|
|||
'当前消息缺少可绑定的会话上下文。':
|
||||
'The current message lacks a conversation context to bind to.',
|
||||
'当前聊天已绑定会话:': 'This chat is now bound to the Session:',
|
||||
'发送 /history 查看最近对话。': 'Send /history to preview recent conversation messages.',
|
||||
'标题:{title}': 'Title: {title}',
|
||||
'归档:{archived}': 'Archived: {archived}',
|
||||
'是': 'Yes',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,45 @@
|
|||
// English translations (shared-c area). Keys are exact Chinese literals passed to t().
|
||||
export default {
|
||||
// history-command.mjs / command help
|
||||
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)':
|
||||
'/history [count] Preview recent messages (default 3, maximum 5)',
|
||||
'用法:/history [数量](默认 3 条,最多 5 条)':
|
||||
'Usage: /history [count] (default 3, maximum 5)',
|
||||
'[图片]': '[Image]',
|
||||
'[文件]': '[File]',
|
||||
'本条没有可预览的文字。': 'This message has no text to preview.',
|
||||
'(已截断)': ' (truncated)',
|
||||
'会话历史|{session}|最近 {count} 条':
|
||||
'Session history | {session} | Recent messages: {count}',
|
||||
'以上为历史记录,不是本次新回复。':
|
||||
'These are history records, not a new reply.',
|
||||
'本次有限读取中仅找到 {count} 条可预览消息。':
|
||||
'Messages available to preview within this bounded read: {count}.',
|
||||
'当前会话仅有 {count} 条可预览消息。':
|
||||
'Messages available to preview in this Session: {count}.',
|
||||
'用户': 'User',
|
||||
'助手': 'Assistant',
|
||||
'当前聊天绑定的会话已不存在,请重新绑定会话。':
|
||||
'The Session bound to this chat no longer exists. Please bind a Session again.',
|
||||
'会话、工作区或机器人状态已发生变化,请重新执行 /history。':
|
||||
'The Session, workspace, or bot state has changed. Please run /history again.',
|
||||
'当前 Harness 暂不支持读取会话历史。':
|
||||
'This Harness does not support reading Session history.',
|
||||
'历史读取已取消。': 'History reading was cancelled.',
|
||||
'读取历史超时,请稍后重试。': 'Reading history timed out. Please try again later.',
|
||||
'暂时无法读取会话历史,请稍后重试。':
|
||||
'Unable to read Session history right now. Please try again later.',
|
||||
'请在与机器人的私聊中使用 /history。':
|
||||
'Please use /history in a direct chat with the bot.',
|
||||
'/history 仅支持文字命令,请移除图片或文件后重试。':
|
||||
'/history supports text commands only. Remove images or files and try again.',
|
||||
'当前聊天尚未绑定会话,请先发送消息或使用 /session 绑定会话。':
|
||||
'This chat has no bound Session. Send a message or use /session to bind one first.',
|
||||
'本次有限读取中未找到可预览的历史消息。':
|
||||
'No history messages were available to preview within this bounded read.',
|
||||
'当前会话暂无可预览的历史消息。':
|
||||
'This Session has no history messages available to preview yet.',
|
||||
|
||||
// harness-approval.mjs
|
||||
'请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。':
|
||||
'Please reply exactly with 「批准」 (approve) or 「拒绝」 (reject). Also accepted: 同意 / 不同意 / yes / no.',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { t } from './i18n.mjs';
|
||||
import { runWorkspaceCommand } from './workspace-command.mjs';
|
||||
import { runCompactCommand } from './compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from './history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -260,7 +261,8 @@ export class TextHarnessBridge {
|
|||
}
|
||||
const collectingBatch = normalized.kind === 'direct'
|
||||
&& this.#batches.status(key).phase === 'collecting';
|
||||
const commandRunner = collectingBatch || hasInboundFiles(normalized) ? null : isControlCommand(text)
|
||||
const commandRunner = collectingBatch ? null : isHistoryCommand(text) ? runHistoryCommand
|
||||
: hasInboundFiles(normalized) ? null : isControlCommand(text)
|
||||
? runControlCommand
|
||||
: (isModelCommand(text)
|
||||
? runModelCommand
|
||||
|
|
@ -417,6 +419,7 @@ export class TextHarnessBridge {
|
|||
key,
|
||||
{
|
||||
signal: this.#signal,
|
||||
isDirect: message.kind === 'direct',
|
||||
hasImages: hasInboundImages(message),
|
||||
hasFiles: hasInboundFiles(message),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|
|
@ -529,6 +532,7 @@ export class TextHarnessBridge {
|
|||
t('直接发送文字、图片或文件即可继续当前会话。'),
|
||||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
t('标题:{title}', { title }),
|
||||
`ID:${boundSessionId}`,
|
||||
t('归档:{archived}', { archived: bound?.archived === true ? t('是') : t('否') }),
|
||||
t('发送 /history 查看最近对话。'),
|
||||
].join('\n');
|
||||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
isBatchInputCommand,
|
||||
} from '../shared/batch-input.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -61,6 +62,7 @@ function helpText() {
|
|||
t('直接发送文字、图片或文件即可继续当前会话。'),
|
||||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
|
|
@ -591,7 +593,8 @@ export class WecomHarnessBridge {
|
|||
return this.#finishBatchResult(frame, messageId, chatId, result);
|
||||
}
|
||||
}
|
||||
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
||||
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
||||
: hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
|
|
@ -768,6 +771,7 @@ export class WecomHarnessBridge {
|
|||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
const result = await runner(message.content, this.#harness, this.#state, key, {
|
||||
signal: this.#signal,
|
||||
isDirect: bodyOf(frame).chattype === 'single',
|
||||
hasImages: hasInboundImages(message),
|
||||
hasFiles: hasInboundFiles(message),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
isBatchInputCommand,
|
||||
} from '../shared/batch-input.mjs';
|
||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
|
|
@ -68,6 +69,7 @@ const HELP_TEXT = () => [
|
|||
t('直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。'),
|
||||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
|
|
@ -364,7 +366,8 @@ export class WeixinHarnessBridge {
|
|||
);
|
||||
}
|
||||
}
|
||||
const commandRunner = hasWeixinFileItems(message) ? null : isControlCommand(commandText)
|
||||
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
||||
: hasWeixinFileItems(message) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
|
|
@ -541,6 +544,7 @@ export class WeixinHarnessBridge {
|
|||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
const result = await runner(text, this.#harness, this.#state, key, {
|
||||
signal: this.#signal,
|
||||
isDirect: true,
|
||||
hasImages: hasWeixinImageItems(message),
|
||||
hasFiles: hasWeixinFileItems(message),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue