feat(v3): T29 收尾 + T31 dsh 功能对齐
T29 流式收尾:
- _stream_partial 每次流式调用前复位(防上次成功置位导致本次失败误抛不回退)
T31 dsh(deepseek-harness)功能对齐:
- OpenAICompatChat 重试退避:传输错误/408/429/5xx 指数退避(max_retries=2),4xx 不重试
- web_fetch 工具:公网 http/https 抓取,SSRF 防护(DNS 后拒绝私网/环回/链路本地/NAT64,
512KB/15s/12k 上限,二进制嗅探拒绝),agent.allow_net 开关(默认开)
- 原子写入:write_file/edit_file 临时文件 + os.replace(Windows EPERM 退避)
- 慢工具线程卸载:run_command/web_fetch/search_files 独立线程 + asyncio.sleep 轮询
- search_files:os.walk 修剪依赖目录(替代 rglob 全量物化),不跟随符号链接
- run_command:危险命令黑名单独立拦截 + 显式 COMSPEC/sh 解释器执行
- 重复调用提醒:同工具同参数第 3 次起回喂系统提示 + repeat_warning 事件
- 会话重命名:PATCH /agent/sessions/{sid} + 前端 ✎
- 前端:SettingsView 适配密钥打码(留空保留),SPA 重新构建
- 新增 tests/test_agent_features.py(9 项);全量 318 测试通过
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<span>{{ turnsOf(s) }} 轮</span>
|
||||
<span>·</span>
|
||||
<span>{{ fmtTime(s.updated_at) }}</span>
|
||||
<span class="sess-rename" title="重命名会话" @click.stop="renameSession(s.id)">✎</span>
|
||||
<span class="sess-del" title="删除会话" @click.stop="removeSession(s.id)">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +181,8 @@ import {
|
||||
startAgent, getAgentStatus, watchAgent, approveAgent, getConfig, updateConfig,
|
||||
getPool,
|
||||
browseFs, getAgentWorkspaces, openWorkspace,
|
||||
listAgentSessions, createAgentSession, getAgentSession, deleteAgentSession, cancelAgent,
|
||||
listAgentSessions, createAgentSession, getAgentSession, deleteAgentSession,
|
||||
renameAgentSession, cancelAgent,
|
||||
} from '@/api'
|
||||
import type { AgentEvent, PoolEntry, FsBrowse } from '@/api'
|
||||
|
||||
@@ -305,6 +307,16 @@ async function removeSession(sid: string) {
|
||||
refreshSessions()
|
||||
}
|
||||
|
||||
async function renameSession(sid: string) {
|
||||
const cur = sessions.value.find(s => s.id === sid)
|
||||
const title = prompt('新的会话名称:', cur?.title || '')
|
||||
if (title === null) return // 取消
|
||||
const trimmed = title.trim()
|
||||
if (!trimmed) { alert('名称不能为空'); return }
|
||||
await renameAgentSession(sid, trimmed)
|
||||
refreshSessions()
|
||||
}
|
||||
|
||||
// ── 工作区 ──────────────────────────────────────────────────────────────
|
||||
async function openPicker() {
|
||||
pickerOpen.value = true
|
||||
@@ -496,16 +508,19 @@ onMounted(async () => {
|
||||
margin-top: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
.sess-del {
|
||||
.sess-del,
|
||||
.sess-rename {
|
||||
margin-left: auto;
|
||||
color: var(--c-caption);
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
cursor: pointer; padding: 0 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sess-item:hover .sess-del { opacity: 1; }
|
||||
.sess-rename { margin-left: 0; }
|
||||
.sess-item:hover .sess-del,
|
||||
.sess-item:hover .sess-rename { opacity: 1; }
|
||||
.sess-del:hover { color: var(--c-err); background: var(--c-err-soft); }
|
||||
.sess-rename:hover { color: var(--c-accent, #2563eb); }
|
||||
.sess-empty { color: var(--c-caption); font-size: 12px; text-align: center; padding: 20px 0; }
|
||||
|
||||
/* ---------- 对话区 ---------- */
|
||||
|
||||
@@ -331,9 +331,10 @@
|
||||
|
||||
<!-- API Key -->
|
||||
<div class="field full-width">
|
||||
<label>API Key(保存在本地 config/settings.json,不入代码库;优先级高于 .env)</label>
|
||||
<label>API Key{{ form.architect.api_key_set ? '(已设置,留空保留)' : '(保存在本地 config/settings.json,不入代码库;优先级高于 .env)' }}</label>
|
||||
<input v-model="form.architect.api_key" type="password"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx" autocomplete="off" />
|
||||
:placeholder="form.architect.api_key_set ? '已设置,留空保留原值' : 'sk-xxxxxxxxxxxxxxxx'"
|
||||
autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
@@ -867,6 +868,15 @@ function _safeAssign(target: any, source: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/** architect 段专用填充:api_key 服务端打码返回,已设置时清空输入框(留空=保留)。 */
|
||||
function _applyArchitect(arch: any) {
|
||||
if (!arch) return
|
||||
const { api_key, api_key_set, ...rest } = arch
|
||||
_safeAssign(form.architect, rest)
|
||||
form.architect.api_key_set = Boolean(api_key_set)
|
||||
form.architect.api_key = '' // 不把打码值带回表单,防止保存时覆盖真值
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loadError.value = ''
|
||||
try {
|
||||
@@ -880,7 +890,7 @@ async function load() {
|
||||
|
||||
// 安全填充表单(防止 API 返回 null 导致响应式丢失)
|
||||
if (cfg.worker) _safeAssign(form.worker, cfg.worker)
|
||||
if (cfg.architect) _safeAssign(form.architect, cfg.architect)
|
||||
if (cfg.architect) _applyArchitect(cfg.architect)
|
||||
if (cfg.pipeline) _safeAssign(form.pipeline, cfg.pipeline)
|
||||
|
||||
// 保存原始快照
|
||||
@@ -914,9 +924,9 @@ async function save() {
|
||||
// 空 api_key 不传给后端,保留服务端已有值
|
||||
if (!patch.architect.api_key) delete patch.architect.api_key
|
||||
const merged = await updateConfig(patch)
|
||||
// 用安全方式更新(保留响应式)
|
||||
// 用安全方式更新(保留响应式);architect 段走打码适配
|
||||
_safeAssign(form.worker, merged.worker || {})
|
||||
_safeAssign(form.architect, merged.architect || {})
|
||||
_applyArchitect(merged.architect || {})
|
||||
_safeAssign(form.pipeline, merged.pipeline || {})
|
||||
original.value = JSON.parse(JSON.stringify(form))
|
||||
saveMsg.value = { ok: true, msg: '✅ 设置已保存,管线已重建' }
|
||||
@@ -935,7 +945,7 @@ async function reset() {
|
||||
try {
|
||||
const merged = await resetConfig()
|
||||
_safeAssign(form.worker, merged.worker || {})
|
||||
_safeAssign(form.architect, merged.architect || {})
|
||||
_applyArchitect(merged.architect || {})
|
||||
_safeAssign(form.pipeline, merged.pipeline || {})
|
||||
original.value = JSON.parse(JSON.stringify(form))
|
||||
workerModels.value = []
|
||||
|
||||
Reference in New Issue
Block a user