新增: obscura 安装引导(prompt AI 看 hint 帮装 + 前端工具卡片 AI 帮装按钮)
This commit is contained in:
@@ -126,6 +126,10 @@ fn system_prompt_parts(lang: &str) -> (&'static str, &'static str, &'static str)
|
||||
/// - fetch_url = 读文档(URL→markdown,去 HTML 噪声);http_request = 调 API(POST/鉴权/原始响应)
|
||||
/// - 不瞎试 URL:先读文档提取端点,再验证;杜绝未读文档即对原始 URL 盲发请求
|
||||
///
|
||||
/// 末尾追加 obscura 安装引导子段:fetch_url render=true 未装 obscura 时,结果含
|
||||
/// `render_mode=static_fallback` + `hint` 字段(引导链接 github.com/h4ckf0r0day/obscura)。
|
||||
/// AI 看到 hint 即自主提示用户「装了能 JS 渲染 SPA/反爬」,征得同意后用 run_command 帮装。
|
||||
///
|
||||
/// 仅段加在 prefix 末尾(聚焦段后),不破坏现有结构。
|
||||
fn doc_exploration_strategy_section(lang: &str) -> &'static str {
|
||||
match lang {
|
||||
@@ -137,7 +141,18 @@ fn doc_exploration_strategy_section(lang: &str) -> &'static str {
|
||||
3. **Extract keys**: pull out base_url, provider_type, auth scheme, model list, endpoint paths — write them down explicitly before calling anything.\n\
|
||||
4. **Verify**: only now use `http_request` against the correct endpoint you extracted (e.g. `GET {base_url}/v1/models` with the auth header) to confirm it works.\n\
|
||||
5. **Apply**: configure the Provider / write code / transcribe into a file via `write_file`.\n\
|
||||
Rule: `fetch_url` is for *reading* a URL (docs/web/API description, markdown output); `http_request` is for *calling* an API (POST/auth/raw response). Never guess an endpoint — read the doc first, extract, then verify.\n",
|
||||
Rule: `fetch_url` is for *reading* a URL (docs/web/API description, markdown output); `http_request` is for *calling* an API (POST/auth/raw response). Never guess an endpoint — read the doc first, extract, then verify.\n\
|
||||
\n\
|
||||
### JS-rendered docs & the obscura install hint\n\
|
||||
`fetch_url` defaults to a static fetch (no JS). For SPA / anti-scrape / JS-rendered docs, the user (or you) can pass `render=true`. When obscura (the JS-rendering backend, github.com/h4ckf0r0day/obscura) is **not installed**, the result silently falls back to static and the returned JSON carries `render_mode: \"static_fallback\"` plus a `hint` field pointing to the install page. Watch the **`hint` field** specifically (not just `render_mode`) — it is the install signal:\n\
|
||||
1. **Notice**: if the `fetch_url` result has `hint` set (or `render_mode` is `static_fallback`), obscura is missing and the page may be under-rendered.\n\
|
||||
2. **Offer**: proactively tell the user — \"obscura is not installed; installing it lets fetch_url render JS-heavy SPA / anti-scrape docs. Want me to install it for you?\" Don't silently continue with the static fallback when the user clearly needs JS rendering.\n\
|
||||
3. **Install on consent**: once the user agrees, run the install yourself via `run_command` and stream the output so the user sees progress — pick whichever path fits the user's environment:\n\
|
||||
- Rust toolchain present: `cargo install obscura`\n\
|
||||
- otherwise: download the matching prebuilt binary from github.com/h4ckf0r0day/obscura releases and place it on PATH\n\
|
||||
(`run_command` is high-risk and requires user approval — the user's \"yes\" above is that approval; still announce the exact command before running.)\n\
|
||||
4. **Re-fetch**: after install succeeds, tell the user \"obscura is ready\" and retry `fetch_url` with `render=true` to get the properly rendered markdown.\n\
|
||||
This is a pure conversational guide — there is no UI button for it; you detect the hint and drive the install through dialogue.\n",
|
||||
_ => "\n\
|
||||
## 文档探索策略\n\
|
||||
收到用户给的 URL(产品页/API 文档/SDK 说明)时,按以下流程走,不要盲目对原始 URL 发 http_request 瞎试:\n\
|
||||
@@ -146,7 +161,18 @@ fn doc_exploration_strategy_section(lang: &str) -> &'static str {
|
||||
3. **提取关键**:把 base_url、provider_type、鉴权方式、模型清单、端点路径显式列出来,再决定调什么。\n\
|
||||
4. **验证**:此时才用 `http_request` 打你提取出的正确端点(如带鉴权头 `GET {base_url}/v1/models`)确认可用。\n\
|
||||
5. **应用**:配置 Provider / 编码 / 转写到文件(write_file)。\n\
|
||||
铁律:`fetch_url` 用于**读** URL(文档/网页/API 说明,输出 markdown);`http_request` 用于**调** API(POST/鉴权/原始响应)。严禁猜端点——先读文档提取,再验证。\n",
|
||||
铁律:`fetch_url` 用于**读** URL(文档/网页/API 说明,输出 markdown);`http_request` 用于**调** API(POST/鉴权/原始响应)。严禁猜端点——先读文档提取,再验证。\n\
|
||||
\n\
|
||||
### JS 渲染文档与 obscura 安装引导\n\
|
||||
`fetch_url` 默认走静态抓取(不跑 JS)。遇到 SPA / 反爬 / JS 渲染的文档,用户(或你)可传 `render=true`。当 obscura(JS 渲染后端,github.com/h4ckf0r0day/obscura)**未安装**时,结果会静默回退静态,返回的 JSON 带 `render_mode: \"static_fallback\"` 和一个指向安装页的 `hint` 字段。请**重点看 `hint` 字段**(而非只看 `render_mode`)——它就是安装信号:\n\
|
||||
1. **察觉**:若 `fetch_url` 结果带 `hint`(或 `render_mode` 为 `static_fallback`),说明 obscura 缺失,页面可能没渲染完整。\n\
|
||||
2. **提议**:主动告诉用户——「obscura 未安装,装了之后 fetch_url 就能渲染 JS 重的 SPA / 反爬文档。要我帮你装吗?」当用户明显需要 JS 渲染时,不要默默用静态回退糊弄过去。\n\
|
||||
3. **同意即装**:用户同意后,你自己用 `run_command` 执行安装并流式输出(让用户看到进度),按用户环境选合适路径:\n\
|
||||
- 有 Rust 工具链:`cargo install obscura`\n\
|
||||
- 否则:从 github.com/h4ckf0r0day/obscura releases 下载对应平台的预编译二进制,放到 PATH 上\n\
|
||||
(`run_command` 是高风险、需用户批准的工具——上面用户的「同意」即是批准;执行前仍要把确切命令告知用户。)\n\
|
||||
4. **重试**:装成功后告诉用户「obscura 就绪」,并用 `render=true` 重试 `fetch_url` 拿到渲染完整的 markdown。\n\
|
||||
这是纯对话式引导——没有专门的 UI 按钮,靠你识别 hint 字段、在对话中驱动安装。\n",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +425,26 @@ mod tests {
|
||||
assert!(en.contains("Extract keys"));
|
||||
assert!(en.contains("Verify"));
|
||||
assert!(en.contains("Apply"));
|
||||
|
||||
// obscura 安装引导子段(中/英)——看 hint 字段自主提示 + 帮装 + 重试
|
||||
// 核心:hint 字段是安装信号(非只看 render_mode),同意即用 run_command 装,装完重试 render=true。
|
||||
assert!(zh.contains("obscura"));
|
||||
assert!(zh.contains("hint"));
|
||||
assert!(zh.contains("static_fallback"));
|
||||
assert!(zh.contains("render_mode"));
|
||||
assert!(zh.contains("run_command"));
|
||||
assert!(zh.contains("cargo install obscura"));
|
||||
assert!(zh.contains("github.com/h4ckf0r0day/obscura"));
|
||||
assert!(zh.contains("render=true"));
|
||||
|
||||
assert!(en.contains("obscura"));
|
||||
assert!(en.contains("hint"));
|
||||
assert!(en.contains("static_fallback"));
|
||||
assert!(en.contains("render_mode"));
|
||||
assert!(en.contains("run_command"));
|
||||
assert!(en.contains("cargo install obscura"));
|
||||
assert!(en.contains("github.com/h4ckf0r0day/obscura"));
|
||||
assert!(en.contains("render=true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -98,13 +98,14 @@
|
||||
{{ tc.result || $t('aiTool.rejectedHint') }}
|
||||
</div>
|
||||
|
||||
<!-- 各工具结果展示(read_file/list_directory/search_files/write_file/patch_file/run_command/http_request/通用)
|
||||
委托 ToolResultBody 子组件,透传 tc + 内容展开态 + expand-content 冒泡 -->
|
||||
<!-- 各工具结果展示(read_file/list_directory/search_files/write_file/patch_file/run_command/http_request/fetch_url/通用)
|
||||
委托 ToolResultBody 子组件,透传 tc + 内容展开态 + expand-content/install-obscura 冒泡 -->
|
||||
<ToolResultBody
|
||||
v-if="tc.status === 'completed'"
|
||||
:tc="tc"
|
||||
:isContentExpanded="isContentExpanded"
|
||||
@expand-content="(id) => emit('expand-content', id)"
|
||||
@install-obscura="() => emit('install-obscura')"
|
||||
/>
|
||||
</div><!-- /.ai-tool-body -->
|
||||
|
||||
@@ -156,6 +157,11 @@ const emit = defineEmits<{
|
||||
toggle: [id: string]
|
||||
/** read_file "展开全部/收起" → 内容级展开 */
|
||||
'expand-content': [id: string]
|
||||
/**
|
||||
* fetch_url static_fallback(obscura 未装)时"AI 帮装"按钮 → 透传父级 → MessageList
|
||||
* 调 store.sendMessage 触发 AI 装流式。仅 fetch_url 工具会发此事件。
|
||||
*/
|
||||
'install-obscura': []
|
||||
/**
|
||||
* 审批按钮 → 父级转发 store.approveToolCall。
|
||||
* path_auth 审批链阶段3b:risk 类带 approved boolean;path 类带 decision('once'|'always'|'deny')。
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
@toggle="() => toggleCardExpand(tc, gi, ci)"
|
||||
@expand-content="toggleExpand"
|
||||
@approve="(e) => emit('approve', e)"
|
||||
@install-obscura="() => emit('install-obscura')"
|
||||
:class="{ 'ai-tool-card--group-hidden': group.calls.length >= 2 && isGroupCollapsed(gi) }"
|
||||
/>
|
||||
</template>
|
||||
@@ -69,6 +70,11 @@ const emit = defineEmits<{
|
||||
approve: [{ id: string; approved: boolean; decision?: 'once' | 'always' | 'deny' }]
|
||||
/** 批量审批 → 父组件调用 approveAll/rejectAll */
|
||||
'batch-approve': [decision: 'approve' | 'reject']
|
||||
/**
|
||||
* fetch_url static_fallback(obscura 未装)时"AI 帮装"按钮 → 透传父级 MessageList,
|
||||
* 调 store.sendMessage 触发 AI 装流式。仅 fetch_url 工具会发此事件。
|
||||
*/
|
||||
'install-obscura': []
|
||||
}>()
|
||||
|
||||
// 根元素 ref(scrollToFirstPending 查 DOM 用)
|
||||
|
||||
@@ -175,6 +175,40 @@
|
||||
<pre v-if="httpBodyExpanded && parsed?.body" class="ai-tool-cmd-output"><code>{{ parsed.body }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- fetch_url 结果:URL + title + render_mode 徽标 + 可折叠 markdown
|
||||
render_mode='static_fallback'(obscura 未装/失败)→ 显 obscura 未装提示 + "AI 帮装"按钮
|
||||
(按钮 emit install-obscura → MessageList 调 store.sendMessage 触发 AI 装流式) -->
|
||||
<div
|
||||
v-else-if="tc.name === 'fetch_url' && tc.result && tc.status === 'completed' && parsed"
|
||||
class="ai-tool-fetch-result"
|
||||
>
|
||||
<div class="ai-tool-fetch-line">
|
||||
<span class="ai-tool-http-method ai-tool-fetch-mode" :class="'ai-tool-fetch-mode--' + (parsed?.render_mode || 'static')">
|
||||
{{ parsed?.render_mode === 'obscura' ? $t('aiTool.fetchUrlRendered')
|
||||
: parsed?.render_mode === 'static_fallback' ? $t('aiTool.fetchUrlStatic')
|
||||
: $t('aiTool.fetchUrlStatic') }}
|
||||
</span>
|
||||
<code class="ai-tool-http-url">{{ parsed?.url }}</code>
|
||||
<span v-if="typeof parsed?.length === 'number' && parsed?.length > 0" class="ai-tool-file-meta">{{ parsed?.length }} {{ $t('aiTool.charsUnit') }}</span>
|
||||
<button
|
||||
v-if="parsed?.markdown"
|
||||
class="ai-tool-cmd-toggle"
|
||||
:title="fetchMdExpanded ? $t('aiTool.collapse') : $t('aiTool.expand')"
|
||||
@click="fetchMdExpanded = !fetchMdExpanded"
|
||||
>{{ fetchMdExpanded ? $t('aiTool.collapse') : $t('aiTool.expand') }}</button>
|
||||
</div>
|
||||
<div v-if="parsed?.title" class="ai-tool-fetch-title">{{ parsed?.title }}</div>
|
||||
<!-- obscura 未装引导(render=true 但回退静态):提示 + "AI 帮装"按钮 -->
|
||||
<div v-if="obscuraMissing" class="ai-tool-obscura-hint">
|
||||
<div class="ai-tool-obscura-hint-text">⚠ {{ parsed?.hint || $t('aiTool.fetchUrlFallbackHint') }}</div>
|
||||
<button class="ai-tool-btn ai-tool-btn--install" @click="onInstallObscura">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
{{ $t('aiTool.installObscura') }}
|
||||
</button>
|
||||
</div>
|
||||
<pre v-if="fetchMdExpanded && parsed?.markdown" class="ai-tool-cmd-output"><code>{{ parsed.markdown }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 通用 CRUD 结果 -->
|
||||
<div
|
||||
v-else-if="tc.result && tc.status === 'completed'"
|
||||
@@ -214,12 +248,19 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
/** read_file "展开全部/收起" → 透传父级(由 ToolCard 继续冒泡) */
|
||||
'expand-content': [id: string]
|
||||
/**
|
||||
* fetch_url static_fallback(obscura 未装)时"AI 帮装"按钮 → 透传父级 → MessageList
|
||||
* 调 store.sendMessage 触发 AI 装流式安装。仅 fetch_url 工具会发此事件。
|
||||
*/
|
||||
'install-obscura': []
|
||||
}>()
|
||||
|
||||
// run_command 输出折叠态:失败命令默认展开(便于看 stderr),成功默认折叠
|
||||
const cmdOutputExpanded = ref(false)
|
||||
// http_request 响应 body 折叠态:失败(非 2xx)默认展开便于看错误,成功默认折叠
|
||||
const httpBodyExpanded = ref(false)
|
||||
// fetch_url markdown 折叠态:默认折叠(可能很长,头部已有 title + 截断字符数摘要)
|
||||
const fetchMdExpanded = ref(false)
|
||||
|
||||
/** 一次解析结果供模板多次读(模板引用多次,computed 避免每次 patch 重 parse) */
|
||||
const parsed = computed(() => parseResult(props.tc.result))
|
||||
@@ -267,6 +308,31 @@ const grepHitLabel = computed(() => {
|
||||
return r ? formatGrep(r) : ''
|
||||
})
|
||||
|
||||
/**
|
||||
* fetch_url obscura 未装检测(render=true 但回退静态):
|
||||
* 后端 render=true 且 obscura 未装/失败/超时 → result.render_mode='static_fallback' + hint 含 obscura 引导。
|
||||
* 此 computed 供模板显"AI 帮装"按钮 + obscura 未装提示。
|
||||
* 检测条件:render_mode==='static_fallback'(后端保证此值仅 render 分支回退时设,普通静态是 'static')。
|
||||
* 双重确认 hint 含 obscura(防御后端未来改字段名),任一命中即视为未装。
|
||||
*/
|
||||
const obscuraMissing = computed(() => {
|
||||
if (props.tc.name !== 'fetch_url') return false
|
||||
const r = parsed.value
|
||||
if (!r) return false
|
||||
const mode = typeof r.render_mode === 'string' ? r.render_mode : ''
|
||||
const hint = typeof r.hint === 'string' ? r.hint : ''
|
||||
return mode === 'static_fallback' || hint.toLowerCase().includes('obscura')
|
||||
})
|
||||
|
||||
/**
|
||||
* fetch_url "AI 帮装 obscura" → emit 透传到 MessageList,调 store.sendMessage
|
||||
* 触发 AI run_command 装流式。安装完成后用户重试即可用 obscura 渲染。
|
||||
* 提示语足够具体(给出两种装法),AI 拿到能直接 run_command 不需再问。
|
||||
*/
|
||||
function onInstallObscura() {
|
||||
emit('install-obscura')
|
||||
}
|
||||
|
||||
// SW-260618-06: status 切到 completed 时按成功/失败初始化折叠态(失败默认展开便于看 stderr/错误 body)。
|
||||
// immediate 时若初始非 completed,两 ref 保持默认 false,无副作用。
|
||||
watch(() => props.tc.status, (s) => {
|
||||
@@ -684,4 +750,74 @@ watch(() => props.tc.status, (s) => {
|
||||
color: var(--df-warning, #f0c75e);
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
}
|
||||
|
||||
/* -- fetch_url 结果:复用 http-line 视觉 token;render_mode 徽标按 mode 着色 -- */
|
||||
.ai-tool-fetch-result {
|
||||
margin: 0;
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
.ai-tool-fetch-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(94,175,240,0.04);
|
||||
}
|
||||
.ai-tool-fetch-mode {
|
||||
/* 复用 .ai-tool-http-method 基样(template 已加该 class),此处仅覆盖 mode 着色 */
|
||||
font-size: 10px;
|
||||
}
|
||||
.ai-tool-fetch-mode--static,
|
||||
.ai-tool-fetch-mode--obscura {
|
||||
color: var(--df-success, #3ddb9c);
|
||||
}
|
||||
.ai-tool-fetch-mode--static_fallback {
|
||||
color: var(--df-warning, #f0c75e);
|
||||
}
|
||||
.ai-tool-fetch-title {
|
||||
padding: 4px 10px 2px;
|
||||
font-family: var(--df-font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--df-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
/* obscura 未装引导(static_fallback):warning 色调提示 + AI 帮装按钮 */
|
||||
.ai-tool-obscura-hint {
|
||||
margin: 4px 10px 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--df-warning-bg, rgba(240,199,94,0.08));
|
||||
border: 0.5px solid var(--df-warning, #f0c75e);
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.ai-tool-obscura-hint-text {
|
||||
font-family: var(--df-font-sans);
|
||||
font-size: 10.5px;
|
||||
color: var(--df-warning, #f0c75e);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* AI 帮装按钮(accent 色调,对齐 batch-approve 视觉) */
|
||||
.ai-tool-btn {
|
||||
align-self: flex-start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
border: none;
|
||||
background: var(--df-accent, #5b8def);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-family: var(--df-font-sans);
|
||||
transition: filter 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-tool-btn:hover { filter: brightness(1.1); }
|
||||
</style>
|
||||
|
||||
@@ -491,6 +491,24 @@ async function handleRetry(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch_url obscura 未装时"AI 帮装"按钮:自动触发 AI 对话让 AI run_command 装流式。
|
||||
* 提示语足够具体(给出 cargo install + GitHub releases 两种装法 + 装完告知用户重试),
|
||||
* AI 拿到能直接 run_command 不需再问;装完 obscura 即可用,用户重试 fetch_url 即走 JS 渲染。
|
||||
* 复用 store.sendMessage(经 useAiSend 暴露,触发 ai_chat_send → agentic loop)。
|
||||
* 失败 toast(对齐 handleRetry 错误处理模式)。
|
||||
*/
|
||||
async function onInstallObscura(): Promise<void> {
|
||||
try {
|
||||
await store.sendMessage(t('aiTool.obscuraInstallPrompt'))
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
emit('toast', { msg: t('aiChat.sendFailed', { msg }), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
// ── F-15 阶段2: 消息分段(归档段折叠) ──
|
||||
// ChatMessage.status 渲染语义(types.ts 未含 status 字段,前端经 cast 读写闭环):
|
||||
// null | undefined | 'active' → 正常消息(现状)
|
||||
@@ -748,6 +766,7 @@ defineExpose({
|
||||
:toolCalls="item.msg.toolCalls"
|
||||
@approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)"
|
||||
@batch-approve="(decision) => store.batchApprove(decision)"
|
||||
@install-obscura="onInstallObscura"
|
||||
/>
|
||||
|
||||
<!-- UX-2025-13:消息时间戳(相对时间+hover绝对时间),流式生成中不显示避免抖动 -->
|
||||
|
||||
@@ -99,6 +99,17 @@ export interface ToolResult {
|
||||
files?: string[]
|
||||
counts?: Array<{ file?: string; count?: number }>
|
||||
total_files?: number
|
||||
// fetch_url result fields(commands/ai/fetch_url.rs execute_fetch_url):
|
||||
// - markdown: 已清洗 + 截断的 markdown 主体
|
||||
// - title: HTML <title> 或 obscura markdown 首个 H1/首行(可能为 null)
|
||||
// - length: markdown 字符数(后端 result.length)
|
||||
// - render_mode: 'static' | 'static_fallback' | 'obscura'(obscura=成功用 JS 渲染)
|
||||
// static_fallback = render=true 但 obscura 未装/失败/超时 → 回退静态 + 附 hint(引导安装 obscura)
|
||||
// - hint: 仅 static_fallback 时存在,obscura 安装引导提示
|
||||
markdown?: string
|
||||
length?: number | null
|
||||
render_mode?: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
/** 内联 SVG 图标字符串(经 v-html 注入,常量安全;图标库改造见决策记录) */
|
||||
@@ -496,6 +507,7 @@ const TOOL_FORMATTERS: Record<string, (r: ToolResult) => string> = {
|
||||
bind_directory: formatBindDir,
|
||||
run_workflow: formatRunWorkflow,
|
||||
http_request: formatHttpResult,
|
||||
fetch_url: formatFetchUrl,
|
||||
grep: formatGrep,
|
||||
}
|
||||
|
||||
@@ -552,6 +564,43 @@ export function formatHttpResult(r: ToolResult): string {
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch_url 结果摘要(commands/ai/fetch_url.rs):URL + render_mode 标识 + markdown(截断)。
|
||||
*
|
||||
* 通用 fallback 文案(供 formatToolResult 在 ToolResultBody 无专用模板前/或解析失败时回退)。
|
||||
* 折叠态 header 文案由 summaryFetchUrl 单独提供(更短)。markdown 主体由 ToolResultBody
|
||||
* fetch_url 专用模板渲染(可折叠 + obscura 引导按钮),此 formatter 仅做兜底文本。
|
||||
*
|
||||
* - render_mode='obscura':成功用 obscura JS 渲染,显已渲染标识。
|
||||
* - render_mode='static_fallback':render=true 但 obscura 未装/失败 → 回退静态,显 hint。
|
||||
* - render_mode='static':普通静态渲染(默认)。
|
||||
*/
|
||||
export function formatFetchUrl(r: ToolResult): string {
|
||||
const url = typeof r.url === 'string' ? r.url : ''
|
||||
const title = typeof r.title === 'string' && r.title ? r.title : ''
|
||||
const mode = typeof r.render_mode === 'string' ? r.render_mode : 'static'
|
||||
const parts: string[] = []
|
||||
if (title) parts.push(t('aiTool.fetchUrlTitle', { title }))
|
||||
parts.push(url)
|
||||
if (mode === 'obscura') {
|
||||
parts.push(t('aiTool.fetchUrlRendered'))
|
||||
} else if (mode === 'static_fallback') {
|
||||
parts.push(t('aiTool.fetchUrlFallbackHint'))
|
||||
}
|
||||
const md = typeof r.markdown === 'string' ? r.markdown : ''
|
||||
if (md) {
|
||||
const MAX_LINES = 20
|
||||
const lines = md.split('\n')
|
||||
if (lines.length > MAX_LINES) {
|
||||
parts.push(lines.slice(0, MAX_LINES).join('\n'))
|
||||
parts.push(t('aiTool.commandOutputLines', { n: lines.length }))
|
||||
} else {
|
||||
parts.push(md)
|
||||
}
|
||||
}
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃)。
|
||||
* UX-260616-04:覆盖范围与 formatToolResult 对齐(header+body 一致);
|
||||
@@ -628,6 +677,21 @@ function summaryHttpRequest(r: ToolResult): string {
|
||||
: t('aiTool.httpFailed', { method, status })
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch_url summary 版(折叠态 header 副标题):
|
||||
* - obscura 已装渲染成功 → "已 JS 渲染"
|
||||
* - static_fallback(obscura 未装/失败) → "obscura 未装,已静态回退"(红色提示,引导安装)
|
||||
* - static(默认静态) → 显 markdown 截断字符数或空(无 markdown 静默)
|
||||
* 返空串 → header 不显副标题(对齐其他 summary 缺数据返空)。
|
||||
*/
|
||||
function summaryFetchUrl(r: ToolResult): string {
|
||||
const mode = typeof r.render_mode === 'string' ? r.render_mode : 'static'
|
||||
if (mode === 'obscura') return t('aiTool.fetchUrlRendered')
|
||||
if (mode === 'static_fallback') return t('aiTool.fetchUrlFallbackHint')
|
||||
const len = typeof r.length === 'number' ? r.length : 0
|
||||
return len > 0 ? t('aiTool.fetchUrlChars', { n: len }) : ''
|
||||
}
|
||||
|
||||
/** 工具结果摘要策略表(name → 纯函数)。仅 summary 独有的工具(create/update_project/restore/purge)在此登记。 */
|
||||
const TOOL_SUMMARIES: Record<string, (r: ToolResult) => string> = {
|
||||
list_projects: r => summaryList(r, 'list_projects'),
|
||||
@@ -654,6 +718,7 @@ const TOOL_SUMMARIES: Record<string, (r: ToolResult) => string> = {
|
||||
bind_directory: summaryBindDir,
|
||||
run_workflow: summaryRunWorkflow,
|
||||
http_request: summaryHttpRequest,
|
||||
fetch_url: summaryFetchUrl,
|
||||
grep: formatGrep,
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,12 @@ export function useToolCardHeader(getTc: () => AiToolCallInfo) {
|
||||
const host = httpHost(url)
|
||||
return host ? `${method} ${host}` : t('aiTool.httpRequestFallback')
|
||||
}
|
||||
case 'fetch_url': {
|
||||
// fetch_url 头部:获取 host(对齐 http_request 精简模式,折叠态一眼知抓哪个站点)
|
||||
const url = argString(tc.args, 'url')
|
||||
const host = httpHost(url)
|
||||
return host ? `${t('aiTool.fetchUrlPrefix')} ${host}` : t('aiTool.fetchUrlFallback')
|
||||
}
|
||||
default: return formatToolName(tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,8 @@ export default {
|
||||
regenerate: 'Regenerate',
|
||||
regenerateFailed: 'Regenerate failed: {msg}',
|
||||
regenerateUnavailable: 'No reply to regenerate',
|
||||
// fetch_url "Install obscura via AI" button triggers sendMessage; failure toast (mirrors editMessageFailed)
|
||||
sendFailed: 'Send failed: {msg}',
|
||||
|
||||
// ── UX-09: edit last user message and regenerate ──
|
||||
editMessage: 'Edit',
|
||||
|
||||
@@ -93,6 +93,26 @@ export default {
|
||||
httpTruncated: 'Response truncated ({bytes}), see details',
|
||||
httpRequestFallback: 'HTTP Request',
|
||||
foundN: 'Found {n}',
|
||||
// fetch_url result (render_mode badge + obscura install guidance)
|
||||
// - fetchUrlTitle: header subtitle when title present
|
||||
// - fetchUrlRendered: render_mode=obscura (JS render success) badge text
|
||||
// - fetchUrlStatic: render_mode=static/static_fallback (static) badge text
|
||||
// - fetchUrlFallbackHint: static_fallback (obscura missing/failed) summary, hints static fallback
|
||||
// - fetchUrlChars: static-mode markdown char count (collapsed subtitle)
|
||||
// - charsUnit: char unit (joined with length)
|
||||
// - installObscura: "Install via AI" button label
|
||||
// - obscuraInstallPrompt: dialog auto-sent to AI on button click (cargo install + releases options)
|
||||
fetchUrlTitle: 'Title: {title}',
|
||||
fetchUrlRendered: 'JS rendered',
|
||||
fetchUrlStatic: 'static',
|
||||
fetchUrlFallbackHint: 'obscura not installed, fell back to static mode (JS rendering unavailable)',
|
||||
fetchUrlChars: '{n} chars',
|
||||
charsUnit: 'chars',
|
||||
// fetch_url header display name (useToolCardHeader): host-shortened prefix + fallback when no url
|
||||
fetchUrlPrefix: 'Fetch',
|
||||
fetchUrlFallback: 'Fetch URL',
|
||||
installObscura: 'Install obscura via AI',
|
||||
obscuraInstallPrompt: 'Please install obscura (a Rust headless browser, used by fetch_url render=true for JS rendering). Try `cargo install obscura` first; if cargo is unavailable, download the prebuilt binary for this platform from the GitHub releases (github.com/h4ckf0r0day/obscura) and place it on PATH. After installing, verify with `obscura --version` and tell me I can retry the web fetch.',
|
||||
// grep cross-file content search results (F-260621): per output_mode phrasing
|
||||
grepHitsN: '{n} hits',
|
||||
grepFilesN: '{n} files',
|
||||
|
||||
@@ -151,6 +151,8 @@ export default {
|
||||
regenerate: '重新生成',
|
||||
regenerateFailed: '重新生成失败:{msg}',
|
||||
regenerateUnavailable: '没有可重新生成的回复',
|
||||
// fetch_url "AI 帮装 obscura" 按钮触发 sendMessage 失败的 toast(对齐 editMessageFailed 模式)
|
||||
sendFailed: '发送失败:{msg}',
|
||||
|
||||
// ── UX-09: 编辑末条 user 消息并重新生成 ──
|
||||
editMessage: '编辑',
|
||||
|
||||
@@ -94,6 +94,26 @@ export default {
|
||||
httpTruncated: '响应已截断({bytes}),详见下方',
|
||||
httpRequestFallback: 'HTTP 请求',
|
||||
foundN: '找到 {n} 项',
|
||||
// fetch_url 结果(render_mode 徽标 + obscura 引导)
|
||||
// - fetchUrlTitle: 有 title 时的 header 副标题
|
||||
// - fetchUrlRendered: render_mode=obscura(JS 渲染成功)徽标文案
|
||||
// - fetchUrlStatic: render_mode=static/static_fallback(静态)徽标文案
|
||||
// - fetchUrlFallbackHint: static_fallback(obscura 未装/失败)摘要,提示用户已静态回退
|
||||
// - fetchUrlChars: 静态模式 markdown 字符数(折叠态副标题)
|
||||
// - charsUnit: 字符单位(与 length 拼接)
|
||||
// - installObscura: "AI 帮装"按钮文案
|
||||
// - obscuraInstallPrompt: 点按钮后自动发给 AI 的对话提示(具体到 cargo install + releases 两种装法)
|
||||
fetchUrlTitle: '标题:{title}',
|
||||
fetchUrlRendered: '已 JS 渲染',
|
||||
fetchUrlStatic: '静态',
|
||||
fetchUrlFallbackHint: 'obscura 未装,已用静态模式(可增强 JS 渲染文档)',
|
||||
fetchUrlChars: '{n} 字符',
|
||||
charsUnit: '字符',
|
||||
// fetch_url 头部显示名(useToolCardHeader):host 精简前缀 + 无 url 时兜底
|
||||
fetchUrlPrefix: '获取',
|
||||
fetchUrlFallback: '获取网页',
|
||||
installObscura: 'AI 帮装 obscura',
|
||||
obscuraInstallPrompt: '请帮我安装 obscura(Rust 无头浏览器,用于 fetch_url 的 render=true JS 渲染)。优先尝试 `cargo install obscura`,若 cargo 不可用则从 GitHub releases(github.com/h4ckf0r0day/obscura)下载对应平台预编译二进制并放入 PATH。安装完成后用 `obscura --version` 验证,然后告诉我可以重试网页抓取了。',
|
||||
// grep 跨文件内容搜索结果(F-260621):按 output_mode 分文案
|
||||
grepHitsN: '命中 {n} 处',
|
||||
grepFilesN: '命中 {n} 文件',
|
||||
|
||||
Reference in New Issue
Block a user