Jelajahi Sumber

vent-web-nav: 修正链接行为结论——182面板实测HTML被转义不可用,默认输出改回Markdown链接;新标签页打开确认为平台前端渲染器必改项(附一行配置)

leftcollen 1 Minggu lalu
induk
melakukan
07b64b9e5a

+ 25 - 17
vent-web-nav/ADAPTATION.md

@@ -39,23 +39,31 @@ Header: X-Access-Token: <JWT>
 
 **与现有实现的替换**:实测平台智能助手现有"帮我打开风门监测页面"回答的链接点击后跳到首页模型页(指向错误)。接入本 skill 后,工具直接返回 `pages.json` 中实测正确的 path,替换旧知识/旧 prompt 中关于页面导航的部分。
 
-### 新标签页打开(链接行为,实测结论 2026-09-07)
-
-- **平台聊天面板支持 HTML 渲染**:实测 `<a href="..." target="_blank">` 输出后被渲染为蓝色可点击链接 → skill 默认输出 HTML 锚点,点击**新标签页打开**,不关闭当前页面与 AI 面板;
-- **注意(实测踩坑)**:让智能体"转述"URL 会把端口写错(8092→6092),因此 SKILL.md 规定 URL 必须逐字符来自 base_url+path 拼接,禁止凭记忆复述;
-- **宿主剥离 HTML 时**(降级 Markdown 链接),新标签行为由宿主前端一行配置实现:
-  ```js
-  // markdown-it:所有链接统一新标签页
-  const defaultLink = md.renderer.rules.link_open ||
-    ((t, i, o, e, s) => s.renderToken(t, i, o));
-  md.renderer.rules.link_open = (t, i, o, e, s) => {
-    t[i].attrSet('target', '_blank'); t[i].attrSet('rel', 'noopener');
-    return defaultLink(t, i, o, e, s);
-  };
-  // marked 等价写法
-  // renderer.link = (href, title, text) => `<a href="${href}" target="_blank" rel="noopener">${text}</a>`;
-  ```
-- 某些嵌入式 webview(如应用内嵌浏览器)会拦截 `window.open`/弹窗,属宿主环境策略,不影响普通浏览器(Chrome/Edge)中 `target="_blank"` 的标准行为。
+### 新标签页打开(链接行为,两轮实测结论 2026-09-07)
+
+**目标**:用户点击智能体给出的链接在新标签页打开,不覆盖当前页面、不关闭 AI 面板。
+
+**实测事实(182 环境平台聊天面板)**:
+- ✅ Markdown 链接 `[名](url)` 正常渲染为蓝色可点击链接;
+- ❌ **HTML 标签被转义为文本显示**(`<a href=...>` 以等宽字体原样出现),skill 侧输出 HTML 锚点不可行;
+- ❌ Markdown 渲染出的链接无 `target` 属性 → 普通浏览器中点击**同窗跳转覆盖当前页**(用户实测报告的行为);
+- ⚠️ 让智能体转述 URL 会把端口写错(8092→6092),因此 SKILL.md 规定 URL 必须逐字符来自 base_url+path 拼接。
+
+**修复方案(平台前端必改项,一行配置)**:在 AI 面板的消息渲染器中给所有链接统一加 `target="_blank"`:
+```js
+// markdown-it:所有链接统一新标签页
+const defaultLink = md.renderer.rules.link_open ||
+  ((t, i, o, e, s) => s.renderToken(t, i, o));
+md.renderer.rules.link_open = (t, i, o, e, s) => {
+  t[i].attrSet('target', '_blank'); t[i].attrSet('rel', 'noopener');
+  return defaultLink(t, i, o, e, s);
+};
+// marked 等价写法
+// renderer.link = (href, title, text) => `<a href="${href}" target="_blank" rel="noopener">${text}</a>`;
+```
+改完无需动 skill——skill 默认输出 Markdown 链接,前端加 target 后即全量生效。
+
+**备注**:若未来面板升级为支持 HTML 渲染(验证方法:让 AI 输出一个 HTML 锚点看是否渲染成蓝色链接而非代码文本),可切换 skill 为 `<a target="_blank">` 输出模式(见 SKILL.md 链接输出格式)。某些嵌入式 webview(应用内嵌浏览器)会拦截弹窗/新窗口,属宿主环境策略,不影响 Chrome/Edge 等普通浏览器。
 
 ### 方式 B:纯 prompt + 知识注入(降级方案)
 

+ 1 - 1
vent-web-nav/GUIDE.md

@@ -168,7 +168,7 @@ python references/sync_pages.py <JWT_TOKEN>
 确认宿主聊天面板支持 Markdown 链接(平台智能助手实测支持,蓝色可点击);输出必须是 `[名称](完整URL)` 格式。
 
 **Q4b:怎么让链接在新标签页打开(不覆盖当前页面)?**
-skill 默认输出 `<a href="..." target="_blank">` HTML 锚点(平台面板实测支持 HTML 渲染,点击新标签页打开且不关 AI 面板)。若接入环境剥离 HTML 降级为 Markdown 链接,由宿主前端给渲染器统一加 `target="_blank"`——一行配置代码见 ADAPTATION.md"新标签页打开"章节。另:智能体不得凭记忆转述 URL(实测会把端口 8092 写成 6092),链接必须逐字符来自注册表拼接。
+两轮实测结论:平台聊天面板**会转义 HTML**(`<a target="_blank">` 显示为代码文本,不可用),但 Markdown 链接正常渲染且无 target 属性 → 同窗跳转。**此问题 skill 侧无法单独解决,需平台前端在消息渲染器中给链接统一加 `target="_blank"`**——一行配置代码见 ADAPTATION.md"新标签页打开"章节,改完 skill 所有链接即全量新标签页打开。另:智能体不得凭记忆转述 URL(实测会把端口 8092 写成 6092),链接必须逐字符来自注册表拼接。
 
 **Q5:怎么知道哪些页面还没操作指引?**
 `pages.json` 顶部 stats 里有 `with_usage` 计数;逐条看 `usage: null` 的就是待补页(素材参考《通防平台页面学习记忆.md》)。

+ 10 - 11
vent-web-nav/SKILL.md

@@ -27,10 +27,9 @@ description: 通防平台网页指引。当用户要求打开/跳转/进入平
 
 - 拼接后不得出现 `//`;path 的查询参数(`?id=...&deviceType=...`)保持原样——设备实例 ID 是平台默认设备,不得删改;
 - **URL 必须逐字符来自 base_url + path 的拼接结果,禁止凭记忆复述或重打 URL**(实测转述会把端口号写错,如 8092→6092);输出前自查一遍与数据源一致;
-- **链接输出格式(新标签页打开,两层)**:
-  - 首选 HTML 锚点(平台聊天面板实测支持 HTML 渲染,新标签页打开、不关闭当前页面与 AI 面板):
-    `<a href="{完整地址}" target="_blank">{页面名}</a>`
-  - 宿主剥离 HTML 时降级为 Markdown:`[{页面名}]({完整地址})`(此模式跳转行为由宿主前端决定,见 ADAPTATION.md"新标签页打开"章节);
+- **链接输出格式**:
+  - 默认 Markdown:`[{页面名}]({完整地址})`(平台聊天面板实测:Markdown 链接渲染为蓝色可点击;HTML 标签会被转义成文本,不可用);
+  - **新标签页打开**:Markdown 链接默认同窗跳转,要实现新标签页需宿主前端在渲染链接时统一加 `target="_blank"`(一行配置,代码见 ADAPTATION.md"新标签页打开"章节,平台前端必改项);宿主已验证支持 HTML 渲染时,可改用 `<a href="{完整地址}" target="_blank">{页面名}</a>` 形式由 skill 侧直达新标签页;
   - 任何模式下都不要只给纯文本相对路径。
 
 ## 工具定义(模式一)
@@ -78,15 +77,15 @@ GET {base_url}/modelreq/sys/permissionNew/getUserPermissionByToken?token=<JWT>
 ```
 已为您找到【{页面名}】页面({分类}·{子类}):
 📖 功能:{desc}
-🔗 立即打开(新标签页):<a href="{完整地址}" target="_blank">{页面名}</a>
+🔗 立即打开:[{页面名}]({完整地址})
 ```
 
 4. **歧义呈现(默认)**:多候选得分接近时(如"网络解算"同时命中【首页3D(三维模型)】与【实时网络解算】),**并列输出 2-3 个候选,每个候选带一句区分说明 + 链接**,用户直接点选,不擅自替用户选择、也不强制追问:
 
 ```
-「{用户话语}」匹配到 {N} 个页面,请按需点选(新标签页打开)
-1. <a href="{完整地址}" target="_blank">{页面名}</a> —— {一句话区分说明(如何与其他候选不同)}
-2. <a href="{完整地址}" target="_blank">{页面名}</a> —— {区分说明}
+「{用户话语}」匹配到 {N} 个页面,请按需点选:
+1. [{页面名}]({完整地址}) —— {一句话区分说明(如何与其他候选不同)}
+2. [{页面名}]({完整地址}) —— {区分说明}
 ```
 
    区分说明应点出关键差异维度(是否带三维模型、仪表盘式/大屏式、监测/控制/分析等),可从 desc 与 note 提炼。
@@ -104,7 +103,7 @@ GET {base_url}/modelreq/sys/permissionNew/getUserPermissionByToken?token=<JWT>
 
 ## 一、通防监控(共N页)
 ### 智能通风
-- <a href="{完整地址}" target="_blank">{页面名}</a> —— {desc}
+- [{页面名}]({完整地址}) —— {desc}
 ...
 ```
 
@@ -123,7 +122,7 @@ GET {base_url}/modelreq/sys/permissionNew/getUserPermissionByToken?token=<JWT>
 
 ```
 ✅ 网址前缀已更换:{旧前缀} → {新前缀}
-示例(平台首页3D):<a href="{完整示例地址}" target="_blank">{完整示例地址}</a>
+示例(平台首页3D):[{完整示例地址}]({完整示例地址})
 之后所有页面链接均使用新前缀。
 ```
 
@@ -139,7 +138,7 @@ GET {base_url}/modelreq/sys/permissionNew/getUserPermissionByToken?token=<JWT>
 
 ```
 【{页面名}】操作指引:
-🔗 页面:<a href="{完整地址}" target="_blank">{页面名}</a>
+🔗 页面:[{页面名}]({完整地址})
 1. {步骤1}
 2. {步骤2}
 ...

+ 7 - 7
vent-web-nav/references/simulate_output.py

@@ -28,7 +28,7 @@ print("-" * 60)
 p = next(x for x in hits if x["id"] == "gate")
 print(f'已为您找到【{p["name"]}】页面({p["category"]}·{p["group"]}):')
 print(f'📖 功能:{p["desc"]}')
-print(f'🔗 立即打开(新标签页):<a href="{base}{p["path"]}" target="_blank">{p["name"]}</a>')
+print(f'🔗 立即打开:[{p["name"]}]({base}{p["path"]})')
 
 # ===== 场景 A2:给我网页地图(瓦斯类筛选) =====
 print()
@@ -40,7 +40,7 @@ gate = p  # 保存场景A1找到的风门页,供A4使用
 print("# 🗺️ 通防平台网页地图(瓦斯监控)")
 print(f"当前部署地址:{base}(说\"将网址换成 xxx\"可切换)\n")
 for gp in gas:
-    print(f'- <a href="{base}{gp["path"]}" target="_blank">{gp["name"]}</a> —— {gp["desc"]}')
+    print(f'- [{gp["name"]}]({base}{gp["path"]}) —— {gp["desc"]}')
 
 # ===== 场景 A3:将网址换成 http://10.120.3.15:8092 =====
 print()
@@ -50,7 +50,7 @@ print("-" * 60)
 old, new = base, "http://10.120.3.15:8092"
 home = next(p for p in vis if p["id"] == "model-3d")
 print(f"✅ 网址前缀已更换:{old} → {new}")
-print(f'示例(平台首页3D):<a href="{new}{home["path"]}" target="_blank">{new}{home["path"]}</a>')
+print(f'示例(平台首页3D):[{new}{home["path"]}]({new}{home["path"]})')
 print("之后所有页面链接均使用新前缀。")
 
 # ===== 场景 A4:风门怎么远程开 =====
@@ -59,7 +59,7 @@ print("=" * 60)
 print("【模拟 A4】用户:风门怎么远程开?")
 print("-" * 60)
 print(f'【{gate["name"]}】操作指引:')
-print(f'🔗 页面:<a href="{base}{gate["path"]}" target="_blank">{gate["name"]}</a>')
+print(f'🔗 页面:[{gate["name"]}]({base}{gate["path"]})')
 for i, step in enumerate(gate["usage"], 1):
     print(f"{i}. {step}")
 print(f'💡 提示:{gate["usage_tip"]}')
@@ -72,11 +72,11 @@ print("-" * 60)
 hits2 = find("网络解算")
 if len(hits2) == 1:
     p2 = hits2[0]
-    print(f'已为您找到【{p2["name"]}】页面:\n🔗 立即打开(新标签页):<a href="{base}{p2["path"]}" target="_blank">{p2["name"]}</a>')
+    print(f'已为您找到【{p2["name"]}】页面:\n🔗 立即打开:[{p2["name"]}]({base}{p2["path"]})')
 else:
-    print(f'「网络解算」匹配到 {len(hits2)} 个页面,请按需点选(新标签页打开):')
+    print(f'「网络解算」匹配到 {len(hits2)} 个页面,请按需点选:')
     for i, p2 in enumerate(hits2[:3], 1):
-        print(f'{i}. <a href="{base}{p2["path"]}" target="_blank">{p2["name"]}</a> —— {p2["desc"]}')
+        print(f'{i}. [{p2["name"]}]({base}{p2["path"]}) —— {p2["desc"]}')
 
 # ===== 附加:全量地图规模 =====
 print()