mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 13:43:31 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba9d2f0afd | |||
| 6ce835703e | |||
| 666980ad8f | |||
| bc8e81307e | |||
| 053534feaa |
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.5.5"
|
||||
version: "v1.5.6"
|
||||
# 服务器配置
|
||||
server:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
|
||||
@@ -657,6 +657,7 @@ func setupRoutes(
|
||||
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
|
||||
protected.GET("/hitl/pending", agentHandler.ListHITLPending)
|
||||
protected.POST("/hitl/decision", agentHandler.DecideHITLInterrupt)
|
||||
protected.POST("/hitl/dismiss", agentHandler.DismissHITLInterrupt)
|
||||
protected.GET("/hitl/config/:conversationId", agentHandler.GetHITLConversationConfig)
|
||||
protected.PUT("/hitl/config", agentHandler.UpsertHITLConversationConfig)
|
||||
protected.POST("/hitl/tool-whitelist", agentHandler.MergeHITLGlobalToolWhitelist)
|
||||
|
||||
@@ -88,7 +88,20 @@ CREATE TABLE IF NOT EXISTS hitl_conversation_configs (
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
updated_at DATETIME NOT NULL
|
||||
);`)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On startup, cancel all orphaned pending interrupts from previous process.
|
||||
// Their in-memory channels are gone, so they can never be resolved.
|
||||
res, err := m.db.Exec(`UPDATE hitl_interrupts SET status='cancelled', decision='reject',
|
||||
decision_comment='process restarted', decided_at=CURRENT_TIMESTAMP WHERE status='pending'`)
|
||||
if err != nil {
|
||||
m.logger.Warn("failed to cancel orphaned HITL interrupts", zap.Error(err))
|
||||
} else if n, _ := res.RowsAffected(); n > 0 {
|
||||
m.logger.Info("cancelled orphaned HITL interrupts from previous process", zap.Int64("count", n))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeHitlMode(mode string) string {
|
||||
@@ -587,6 +600,43 @@ func (h *AgentHandler) DecideHITLInterrupt(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *AgentHandler) DismissHITLInterrupt(c *gin.Context) {
|
||||
var req struct {
|
||||
InterruptID string `json:"interruptId" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if h.hitlManager == nil {
|
||||
c.JSON(500, gin.H{"error": "hitl manager unavailable"})
|
||||
return
|
||||
}
|
||||
res, err := h.db.Exec(`UPDATE hitl_interrupts SET status='cancelled', decision='reject',
|
||||
decision_comment='dismissed by user', decided_at=CURRENT_TIMESTAMP
|
||||
WHERE id=? AND status='pending'`, req.InterruptID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
c.JSON(404, gin.H{"error": "interrupt not found or already resolved"})
|
||||
return
|
||||
}
|
||||
// Also drain from in-memory map if present
|
||||
h.hitlManager.mu.Lock()
|
||||
if p, ok := h.hitlManager.pending[req.InterruptID]; ok {
|
||||
delete(h.hitlManager.pending, req.InterruptID)
|
||||
select {
|
||||
case p.decideCh <- hitlDecision{Decision: "reject", Comment: "dismissed by user"}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
h.hitlManager.mu.Unlock()
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *AgentHandler) interceptHITLForEinoTool(runCtx context.Context, cancelRun context.CancelCauseFunc, conversationID, assistantMessageID string, sendEventFunc func(eventType, message string, data interface{}), toolName, arguments string) (string, error) {
|
||||
payload := map[string]interface{}{
|
||||
"toolName": toolName,
|
||||
|
||||
+132
-10
@@ -964,7 +964,33 @@ header {
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hitl-sidebar-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hitl-sidebar-body {
|
||||
overflow: hidden;
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
margin-top: 10px;
|
||||
transition: max-height 0.3s ease, opacity 0.2s ease, margin-top 0.3s ease;
|
||||
}
|
||||
|
||||
.hitl-sidebar-collapsed .hitl-sidebar-body {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.hitl-sidebar-collapsed .hitl-apply-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hitl-sidebar-heading {
|
||||
@@ -1144,12 +1170,18 @@ header {
|
||||
.hitl-config-input {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
border: 1px solid var(--border-color);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 0 10px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.hitl-config-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #0066ff);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.hitl-pending-list {
|
||||
@@ -1159,43 +1191,133 @@ header {
|
||||
}
|
||||
|
||||
.hitl-pending-item {
|
||||
border: 1px solid rgba(99, 102, 241, 0.25);
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
padding: 16px;
|
||||
background: #f8fbff;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.hitl-pending-item:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 102, 255, 0.08);
|
||||
}
|
||||
|
||||
.hitl-pending-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hitl-pending-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hitl-tool-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
background: var(--accent-color, #0066ff);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hitl-mode-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
.hitl-mode-tag--review_edit {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.hitl-pending-meta {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hitl-pending-payload {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
margin: 0 0 4px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hitl-pending-item-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hitl-dismiss-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: var(--text-secondary, #64748b);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.hitl-dismiss-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.hitl-pending-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.hitl-edit-args {
|
||||
width: 100%;
|
||||
min-height: 76px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
padding: 8px;
|
||||
padding: 10px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.hitl-edit-args:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #0066ff);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.hitl-input-help {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
color: var(--accent-color, #0066ff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,23 @@ if (typeof window !== 'undefined') {
|
||||
window.updateHitlStatusUI = updateHitlStatusUI;
|
||||
}
|
||||
|
||||
function toggleHitlSidebarCard() {
|
||||
var card = document.getElementById('hitl-sidebar-card');
|
||||
if (!card) return;
|
||||
card.classList.toggle('hitl-sidebar-collapsed');
|
||||
try {
|
||||
localStorage.setItem('hitl-sidebar-collapsed', card.classList.contains('hitl-sidebar-collapsed') ? '1' : '0');
|
||||
} catch (e) {}
|
||||
}
|
||||
window.toggleHitlSidebarCard = toggleHitlSidebarCard;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var card = document.getElementById('hitl-sidebar-card');
|
||||
if (card && localStorage.getItem('hitl-sidebar-collapsed') === '0') {
|
||||
card.classList.remove('hitl-sidebar-collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
function getAgentModeLabelForValue(mode) {
|
||||
if (typeof window.t === 'function') {
|
||||
switch (mode) {
|
||||
|
||||
+34
-10
@@ -282,21 +282,29 @@ async function refreshHitlPending() {
|
||||
const preview = payload.length > 280 ? (payload.slice(0, 280) + '...') : payload;
|
||||
const mode = String(item.mode || '').trim().toLowerCase();
|
||||
const allowEdit = mode === 'review_edit';
|
||||
var escId = escapeHtml(String(item.id || ''));
|
||||
var qId = JSON.stringify(String(item.id || '')).replace(/"/g, '"');
|
||||
var qConv = JSON.stringify(String(item.conversationId || '')).replace(/"/g, '"');
|
||||
return (
|
||||
'<div class="hitl-pending-item">' +
|
||||
'<div class="hitl-pending-item-header">' +
|
||||
'<strong>' + escapeHtml(item.toolName || '-') + '</strong>' +
|
||||
'<span>' + escapeHtml(item.mode || '-') + '</span>' +
|
||||
'<div class="hitl-pending-item-title">' +
|
||||
'<span class="hitl-tool-badge">' + escapeHtml(item.toolName || '-') + '</span>' +
|
||||
'<span class="hitl-mode-tag hitl-mode-tag--' + escapeHtml(mode) + '">' + escapeHtml(item.mode || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div><small>conversation: ' + escapeHtml(item.conversationId || '-') + '</small></div>' +
|
||||
'<pre style="white-space:pre-wrap;max-height:160px;overflow:auto;">' + escapeHtml(preview) + '</pre>' +
|
||||
'<button class="hitl-dismiss-btn" title="忽略" onclick="dismissHitlItem(' + qId + ')">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="hitl-pending-meta">会话:' + escapeHtml(item.conversationId || '-') + '</div>' +
|
||||
'<pre class="hitl-pending-payload">' + escapeHtml(preview) + '</pre>' +
|
||||
(allowEdit
|
||||
? ('<div class="hitl-input-help">审查编辑模式:可填写 JSON 对象覆盖参数,示例:{"command":"ls -la"}</div>' +
|
||||
'<textarea id="hitl-edit-' + escapeHtml(String(item.id || '')) + '" class="hitl-edit-args" placeholder=\'{"command":"ls -la"}\'></textarea>')
|
||||
? ('<div class="hitl-input-help">审查编辑模式:可填写 JSON 对象覆盖参数。示例:{"command":"ls -la"}</div>' +
|
||||
'<textarea id="hitl-edit-' + escId + '" class="hitl-edit-args" placeholder=\'{"command":"ls -la"}\'></textarea>')
|
||||
: '<div class="hitl-input-help">审批模式:仅通过/拒绝,不支持改参。</div>') +
|
||||
'<div class="hitl-input-help">备注(可选):建议写审批依据。</div>' +
|
||||
'<input id="hitl-comment-' + escId + '" class="hitl-config-input hitl-inline-comment" type="text" placeholder="例如:允许只读命令">' +
|
||||
'<div class="hitl-pending-actions">' +
|
||||
'<button class="btn-primary" onclick="submitHitlDecision(' + JSON.stringify(String(item.id || '')) + ',\'approve\',' + JSON.stringify(String(item.conversationId || '')) + ')">通过</button>' +
|
||||
'<button class="btn-secondary" onclick="submitHitlDecision(' + JSON.stringify(String(item.id || '')) + ',\'reject\',' + JSON.stringify(String(item.conversationId || '')) + ')">拒绝</button>' +
|
||||
'<button class="btn-secondary" onclick="submitHitlDecision(' + qId + ',"reject",' + qConv + ')">拒绝</button>' +
|
||||
'<button class="btn-primary" onclick="submitHitlDecision(' + qId + ',"approve",' + qConv + ')">通过</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
@@ -307,7 +315,8 @@ async function refreshHitlPending() {
|
||||
}
|
||||
|
||||
async function submitHitlDecision(interruptId, decision, conversationIdOpt) {
|
||||
const comment = prompt('审批备注(可选)') || '';
|
||||
const commentBox = document.getElementById('hitl-comment-' + interruptId);
|
||||
const comment = (commentBox && commentBox.value) ? commentBox.value.trim() : '';
|
||||
let editedArguments = null;
|
||||
const editBox = document.getElementById('hitl-edit-' + interruptId);
|
||||
if (editBox && editBox.value && editBox.value.trim()) {
|
||||
@@ -332,7 +341,7 @@ async function submitHitlDecisionWithPayload(interruptId, decision, comment, edi
|
||||
if (!resp.ok) {
|
||||
const errText = await readHitlApiError(resp);
|
||||
if (resp.status === 409 && (errText.indexOf('already resolved') >= 0 || errText.indexOf('not found') >= 0)) {
|
||||
refreshHitlPending();
|
||||
await dismissHitlItem(interruptId, true);
|
||||
return true;
|
||||
}
|
||||
alert('提交失败:' + errText);
|
||||
@@ -363,9 +372,24 @@ async function readHitlApiError(resp) {
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissHitlItem(interruptId, silent) {
|
||||
try {
|
||||
await hitlApiFetch('/api/hitl/dismiss', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interruptId: interruptId })
|
||||
});
|
||||
} catch (e) {
|
||||
if (!silent) { console.warn('dismissHitlItem', e); }
|
||||
}
|
||||
refreshHitlPending();
|
||||
}
|
||||
|
||||
window.refreshHitlPending = refreshHitlPending;
|
||||
window.submitHitlDecision = submitHitlDecision;
|
||||
window.submitHitlDecisionWithPayload = submitHitlDecisionWithPayload;
|
||||
window.dismissHitlItem = dismissHitlItem;
|
||||
window.followAgentRunAfterHitlDecision = followAgentRunAfterHitlDecision;
|
||||
|
||||
window.addEventListener('hitl-interrupt', function () {
|
||||
|
||||
+26
-23
@@ -117,10 +117,9 @@
|
||||
<div class="nav-item" data-page="hitl">
|
||||
<div class="nav-item-content" data-title="人机协同" onclick="switchPage('hitl')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="7" r="3"></circle>
|
||||
<circle cx="16" cy="7" r="3"></circle>
|
||||
<path d="M2 20c0-3 2.5-5 6-5s6 2 6 5"></path>
|
||||
<path d="M10 20h12"></path>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<polyline points="16 11 18 13 22 9"></polyline>
|
||||
</svg>
|
||||
<span data-i18n="nav.hitl">人机协同</span>
|
||||
</div>
|
||||
@@ -511,8 +510,8 @@
|
||||
<div id="conversations-list" class="conversations-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hitl-sidebar-card" id="hitl-sidebar-card">
|
||||
<div class="hitl-sidebar-card-header">
|
||||
<div class="hitl-sidebar-card hitl-sidebar-collapsed" id="hitl-sidebar-card">
|
||||
<div class="hitl-sidebar-card-header" onclick="toggleHitlSidebarCard()">
|
||||
<div class="hitl-sidebar-heading">
|
||||
<span class="hitl-sidebar-icon" aria-hidden="true">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -525,24 +524,28 @@
|
||||
<span class="hitl-sidebar-subtitle" data-i18n="chat.hitlCardSubtitle">审批与白名单</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
|
||||
<span data-i18n="chat.hitlApply">应用</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
|
||||
<div class="hitl-sidebar-config">
|
||||
<div class="hitl-config-field">
|
||||
<label class="hitl-config-label" for="hitl-mode-select" data-i18n="chat.hitlModeLabel">模式</label>
|
||||
<select id="hitl-mode-select" class="hitl-config-select">
|
||||
<option value="off" data-i18n="chat.hitlModeOff">关闭</option>
|
||||
<option value="approval" data-i18n="chat.hitlModeApproval">审批模式</option>
|
||||
<option value="review_edit" data-i18n="chat.hitlModeReviewEdit">审查编辑</option>
|
||||
</select>
|
||||
<div class="hitl-sidebar-header-actions">
|
||||
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="event.stopPropagation(); window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
|
||||
<span data-i18n="chat.hitlApply">应用</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hitl-config-field hitl-config-field--tools">
|
||||
<label class="hitl-config-label" for="hitl-sensitive-tools" data-i18n="chat.hitlWhitelistTools">白名单工具(免审批,逗号分隔)</label>
|
||||
<textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
|
||||
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
|
||||
</div>
|
||||
<div class="hitl-sidebar-body" id="hitl-sidebar-body">
|
||||
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
|
||||
<div class="hitl-sidebar-config">
|
||||
<div class="hitl-config-field">
|
||||
<label class="hitl-config-label" for="hitl-mode-select" data-i18n="chat.hitlModeLabel">模式</label>
|
||||
<select id="hitl-mode-select" class="hitl-config-select">
|
||||
<option value="off" data-i18n="chat.hitlModeOff">关闭</option>
|
||||
<option value="approval" data-i18n="chat.hitlModeApproval">审批模式</option>
|
||||
<option value="review_edit" data-i18n="chat.hitlModeReviewEdit">审查编辑</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="hitl-config-field hitl-config-field--tools">
|
||||
<label class="hitl-config-label" for="hitl-sensitive-tools" data-i18n="chat.hitlWhitelistTools">白名单工具(免审批,逗号分隔)</label>
|
||||
<textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
|
||||
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user