diff --git a/src-tauri/src/commands/ai/prompt.rs b/src-tauri/src/commands/ai/prompt.rs index 0dd1e6b..1af5d77 100644 --- a/src-tauri/src/commands/ai/prompt.rs +++ b/src-tauri/src/commands/ai/prompt.rs @@ -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] diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue index 2e33a36..d26fc3e 100644 --- a/src/components/ToolCard.vue +++ b/src/components/ToolCard.vue @@ -98,13 +98,14 @@ {{ tc.result || $t('aiTool.rejectedHint') }} - + @@ -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')。 diff --git a/src/components/ToolCardList.vue b/src/components/ToolCardList.vue index 6c0eeec..b459387 100644 --- a/src/components/ToolCardList.vue +++ b/src/components/ToolCardList.vue @@ -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) }" /> @@ -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 用) diff --git a/src/components/ToolResultBody.vue b/src/components/ToolResultBody.vue index 3310ab2..5c0e71f 100644 --- a/src/components/ToolResultBody.vue +++ b/src/components/ToolResultBody.vue @@ -175,6 +175,40 @@
{{ parsed.body }}
+ +
+
+ + {{ parsed?.render_mode === 'obscura' ? $t('aiTool.fetchUrlRendered') + : parsed?.render_mode === 'static_fallback' ? $t('aiTool.fetchUrlStatic') + : $t('aiTool.fetchUrlStatic') }} + + {{ parsed?.url }} + {{ parsed?.length }} {{ $t('aiTool.charsUnit') }} + +
+
{{ parsed?.title }}
+ +
+
⚠ {{ parsed?.hint || $t('aiTool.fetchUrlFallbackHint') }}
+ +
+
{{ parsed.markdown }}
+
+
() // 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); } diff --git a/src/components/ai/MessageList.vue b/src/components/ai/MessageList.vue index fe01238..88864f6 100644 --- a/src/components/ai/MessageList.vue +++ b/src/components/ai/MessageList.vue @@ -491,6 +491,24 @@ async function handleRetry(): Promise { } } +/** + * 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 { + 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" /> diff --git a/src/composables/ai/useToolCard.ts b/src/composables/ai/useToolCard.ts index 860a164..9573684 100644 --- a/src/composables/ai/useToolCard.ts +++ b/src/composables/ai/useToolCard.ts @@ -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 或 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, } diff --git a/src/composables/ai/useToolCardHeader.ts b/src/composables/ai/useToolCardHeader.ts index 4a76346..79585e8 100644 --- a/src/composables/ai/useToolCardHeader.ts +++ b/src/composables/ai/useToolCardHeader.ts @@ -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) } } diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index bc5638a..ee9c638 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -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', diff --git a/src/i18n/en/aiTool.ts b/src/i18n/en/aiTool.ts index a12f16c..833d723 100644 --- a/src/i18n/en/aiTool.ts +++ b/src/i18n/en/aiTool.ts @@ -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', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index ef34d5b..8cb5642 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -151,6 +151,8 @@ export default { regenerate: '重新生成', regenerateFailed: '重新生成失败:{msg}', regenerateUnavailable: '没有可重新生成的回复', + // fetch_url "AI 帮装 obscura" 按钮触发 sendMessage 失败的 toast(对齐 editMessageFailed 模式) + sendFailed: '发送失败:{msg}', // ── UX-09: 编辑末条 user 消息并重新生成 ── editMessage: '编辑', diff --git a/src/i18n/zh-CN/aiTool.ts b/src/i18n/zh-CN/aiTool.ts index 80d26e9..8f842b7 100644 --- a/src/i18n/zh-CN/aiTool.ts +++ b/src/i18n/zh-CN/aiTool.ts @@ -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} 文件',