mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 06:07:13 +08:00
feat(agent): add explicit active-turn steering
This commit is contained in:
@@ -485,6 +485,20 @@
|
||||
<i class="fas fa-microphone text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button id="steer-btn"
|
||||
class="hidden flex-shrink-0 w-10 h-10 items-center justify-center rounded-lg
|
||||
border border-primary-300 dark:border-primary-700
|
||||
text-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||
disabled:text-slate-300 dark:disabled:text-slate-600
|
||||
disabled:border-slate-200 dark:disabled:border-slate-700
|
||||
disabled:cursor-not-allowed cursor-pointer transition-colors duration-150"
|
||||
type="button"
|
||||
data-i18n-title="steer_active"
|
||||
data-i18n-aria-label="steer_active"
|
||||
aria-label="引导当前任务"
|
||||
title="引导当前任务">
|
||||
<i class="fas fa-arrow-turn-up text-sm"></i>
|
||||
</button>
|
||||
<button id="send-btn"
|
||||
class="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-lg
|
||||
bg-primary-400 text-white hover:bg-primary-500
|
||||
|
||||
@@ -123,6 +123,8 @@ const I18N = {
|
||||
slash_knowledge_off: '关闭知识库',
|
||||
slash_config: '查看当前配置',
|
||||
slash_cancel: '中止当前正在运行的 Agent 任务',
|
||||
slash_steer: '向当前正在运行的 Agent 任务注入引导指令',
|
||||
steer_active: '引导当前任务',
|
||||
slash_logs: '查看最近日志',
|
||||
slash_version: '查看版本',
|
||||
input_placeholder: '输入消息,或输入 / 使用指令',
|
||||
@@ -375,6 +377,8 @@ const I18N = {
|
||||
slash_knowledge_off: '關閉知識庫',
|
||||
slash_config: '檢視當前設定',
|
||||
slash_cancel: '中止當前正在執行的 Agent 任務',
|
||||
slash_steer: '向當前正在執行的 Agent 任務注入引導指令',
|
||||
steer_active: '引導當前任務',
|
||||
slash_logs: '檢視最近日誌',
|
||||
slash_version: '檢視版本',
|
||||
input_placeholder: '輸入訊息,或輸入 / 使用指令',
|
||||
@@ -626,6 +630,8 @@ const I18N = {
|
||||
slash_knowledge_off: 'Disable knowledge base',
|
||||
slash_config: 'Show current config',
|
||||
slash_cancel: 'Abort the running Agent task',
|
||||
slash_steer: 'Inject guidance into the running Agent task',
|
||||
steer_active: 'Steer active task',
|
||||
slash_logs: 'Show recent logs',
|
||||
slash_version: 'Show version',
|
||||
input_placeholder: 'Type a message, or press / for commands',
|
||||
@@ -816,6 +822,9 @@ function applyI18n() {
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
el.title = t(el.dataset['i18nTitle']);
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {
|
||||
el.setAttribute('aria-label', t(el.dataset['i18nAriaLabel']));
|
||||
});
|
||||
document.querySelectorAll('[data-tip-key]').forEach(el => {
|
||||
el.setAttribute('data-tooltip', t(el.dataset.tipKey));
|
||||
});
|
||||
@@ -1415,6 +1424,7 @@ startPolling();
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const steerBtn = document.getElementById('steer-btn');
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const folderInput = document.getElementById('folder-input');
|
||||
@@ -1749,6 +1759,7 @@ function setSendBtnCancelMode(requestId) {
|
||||
sendBtn.classList.add('send-btn-cancel');
|
||||
sendBtn.title = (currentLang === 'zh' ? '中止' : 'Cancel');
|
||||
sendBtn.innerHTML = '<i class="fas fa-stop text-sm"></i>';
|
||||
updateSteerBtnState();
|
||||
}
|
||||
|
||||
function resetSendBtnSendMode() {
|
||||
@@ -1757,9 +1768,61 @@ function resetSendBtnSendMode() {
|
||||
sendBtn.classList.remove('send-btn-cancel');
|
||||
sendBtn.title = '';
|
||||
sendBtn.innerHTML = '<i class="fas fa-paper-plane text-sm"></i>';
|
||||
steerBtn.classList.add('hidden');
|
||||
steerBtn.classList.remove('flex');
|
||||
steerBtn.disabled = true;
|
||||
updateSendBtnState();
|
||||
}
|
||||
|
||||
function updateSteerBtnState() {
|
||||
const active = sendBtnMode === 'cancel' && !!activeRequestId;
|
||||
steerBtn.classList.toggle('hidden', !active);
|
||||
steerBtn.classList.toggle('flex', active);
|
||||
steerBtn.disabled = !active || uploadingCount > 0 || !chatInput.value.trim();
|
||||
}
|
||||
|
||||
function steerActiveTask() {
|
||||
const instruction = chatInput.value.trim();
|
||||
if (!instruction || sendBtnMode !== 'cancel' || !activeRequestId) return;
|
||||
|
||||
inputHistory.push(instruction);
|
||||
historyIdx = -1;
|
||||
historySavedDraft = '';
|
||||
addUserMessage(`↪ ${instruction}`, new Date());
|
||||
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = '42px';
|
||||
chatInput.style.overflowY = 'hidden';
|
||||
updateSteerBtnState();
|
||||
|
||||
fetch('/message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
message: instruction,
|
||||
steer: true,
|
||||
stream: false,
|
||||
lang: currentLang,
|
||||
}),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.inline_reply) {
|
||||
addBotMessage(data.inline_reply, new Date());
|
||||
} else {
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('[steer] request failed', err);
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
})
|
||||
.finally(updateSteerBtnState);
|
||||
}
|
||||
|
||||
steerBtn.addEventListener('click', steerActiveTask);
|
||||
|
||||
function requestCancel() {
|
||||
const reqId = activeRequestId;
|
||||
if (!reqId) return;
|
||||
@@ -1795,10 +1858,12 @@ function updateSendBtnState() {
|
||||
resetSendBtnSendMode();
|
||||
} else {
|
||||
// Don't downgrade a genuinely active Cancel button on input edits.
|
||||
updateSteerBtnState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendBtn.disabled = uploadingCount > 0 || (!chatInput.value.trim() && pendingAttachments.length === 0);
|
||||
updateSteerBtnState();
|
||||
}
|
||||
|
||||
function renderAttachmentPreview() {
|
||||
@@ -2117,6 +2182,7 @@ const SLASH_COMMANDS = [
|
||||
{ cmd: '/knowledge off', desc: 'slash_knowledge_off' },
|
||||
{ cmd: '/config', desc: 'slash_config' },
|
||||
{ cmd: '/cancel', desc: 'slash_cancel' },
|
||||
{ cmd: '/steer ', desc: 'slash_steer' },
|
||||
{ cmd: '/logs', desc: 'slash_logs' },
|
||||
{ cmd: '/version', desc: 'slash_version' },
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
@@ -134,6 +135,36 @@ def _cancel_reply_text(cancelled: int, lang: str) -> str:
|
||||
return "Nothing to cancel." if en else "当前没有可中止的任务。"
|
||||
|
||||
|
||||
def _steer_reply_text(status, lang: str) -> str:
|
||||
from agent.protocol import SteerStatus
|
||||
|
||||
en = (lang or "").lower().startswith("en")
|
||||
messages = {
|
||||
SteerStatus.ACCEPTED: (
|
||||
"↪️ Active task redirected.", "↪️ 已引导当前任务。"
|
||||
),
|
||||
SteerStatus.INACTIVE: (
|
||||
"No active task to steer.", "当前没有可引导的任务。"
|
||||
),
|
||||
SteerStatus.CLOSING: (
|
||||
"The active task is already finishing.", "当前任务已结束,无法再引导。"
|
||||
),
|
||||
SteerStatus.AMBIGUOUS: (
|
||||
"Multiple tasks are active in this session; the steering target is ambiguous.",
|
||||
"当前会话有多个任务在运行,无法确定引导目标。",
|
||||
),
|
||||
SteerStatus.FULL: (
|
||||
"Too many steering updates are pending; try again after the agent processes them.",
|
||||
"引导指令过多,请等待当前任务处理后再试。",
|
||||
),
|
||||
SteerStatus.INVALID: (
|
||||
"Usage: /steer <instruction>", "用法:/steer <引导指令>"
|
||||
),
|
||||
}
|
||||
english, chinese = messages[status]
|
||||
return english if en else chinese
|
||||
|
||||
|
||||
def _get_upload_dir() -> str:
|
||||
from common.utils import expand_path
|
||||
ws_root = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
@@ -912,6 +943,37 @@ class WebChannel(ChatChannel):
|
||||
"inline_reply": msg_text,
|
||||
})
|
||||
|
||||
# Explicit steering also bypasses the normal session queue. The
|
||||
# Web button sends ``steer: true`` with raw input; typed /steer
|
||||
# commands use the same endpoint and semantics as IM channels.
|
||||
steer_requested = bool(json_data.get("steer", False))
|
||||
is_steer_command = (
|
||||
re.match(r"^/steer(?:\s|$)", stripped_prompt) is not None
|
||||
)
|
||||
if steer_requested or is_steer_command:
|
||||
instruction = (
|
||||
(prompt or "").strip()[len("/steer"):].strip()
|
||||
if is_steer_command
|
||||
else (prompt or "").strip()
|
||||
)
|
||||
from bridge.bridge import Bridge
|
||||
result = Bridge().get_agent_bridge().steer_session(
|
||||
session_id, instruction
|
||||
)
|
||||
lang = (json_data.get("lang") or "zh").lower()
|
||||
msg_text = _steer_reply_text(result.status, lang)
|
||||
logger.info(
|
||||
f"[WebChannel] steer fast-path: session={session_id}, "
|
||||
f"status={result.status.value}, lang={lang}"
|
||||
)
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"request_id": "",
|
||||
"stream": False,
|
||||
"steered": result.accepted,
|
||||
"inline_reply": msg_text,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# Append file references to the prompt (same format as QQ channel)
|
||||
if attachments:
|
||||
file_refs = []
|
||||
|
||||
Reference in New Issue
Block a user