Skip to content

feat(web): 支持聊天内联交付链接与主题网站图标 - #1367

Merged
qqqqqf-q merged 9 commits into
mainfrom
xxx
Sep 24, 2026
Merged

qqqqqf-q merged 9 commits into
mainfrom
xxx

Conversation

@qqqqqf-q

@qqqqqf-q qqqqqf-q commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Author

  • Human
  • Agent

Type

  • bug
  • feat
  • test

Summary

聊天交付的 /data/... 文件路径原先会跳到 Memoh 网站路径,工作区预览也可能落到用户电脑的 localhost。本次统一识别文件、工作区 localhost 和外部 HTTP(S) 链接:文件打开工作区文件页,localhost 打开工作区浏览器,外链在用户浏览器打开;工作区不可用时提示错误,不回退到错误的本机地址。

内联链接采用主题色、紧贴文字的图标和共享 @felinic/ui 即时 Tooltip,移除重复原生 title;静止时无下划线,悬停和键盘焦点显示 1px 细点下划线(此样式作用于所有 markstream 渲染的 Markdown 链接)。

外链图标由服务端 /site-icon 受限探测:只抓取链接所在站点的根路径(用户分享的路径和 query 不发给第三方),一次返回 light/dark 两套图标,匹配站点 <link media="(prefers-color-scheme: …)"> 声明并优先 SVG;所有站点同一套规则,没有站点专用逻辑。只靠 JS 切换图标的站点(如 GitHub)拿到的是静态图标;兜底规则:若某主题下选中的图标是单色 SVG(≤8KB,不含 <style>/图片等无法静态判断的内容)且与该主题背景(近似为纯白/纯黑)的对比度低于 WCAG 1.4.11 的 3:1,服务端额外返回 light_mask/dark_mask(SVG 的 data URL),前端用 CSS mask 以链接色填充其形状。多色、位图或本就清晰的图标原样显示,不加底板。服务端按 origin 缓存(显式默认端口归一,找到 24h;站点有响应但无可用页面 1h;超时、5xx、429 视为暂时失败只缓存 5 分钟;最多 1024 条;私网或非法重定向属于确定失败,按 1h 缓存),单次探测整体限时 10s、全局最多 8 个并发(排队超时返回空且不缓存),图标 URL 限长 2048;响应的浏览器缓存时长跟随服务端条目剩余时间(上限 1h)并合并并发请求,只允许 80/443 端口,拒绝私网、回环、CGNAT、NAT64 等地址并在连接时固定已校验 IP;198.18.0.0/15 有意放行,因为 Clash/Surge 的 fake-IP 模式会把所有域名解析到该网段。前端按 origin 共享一次查询,切换主题不重新请求;无图标或加载失败时回退 Globe。

隐私说明:图标图片本身由用户浏览器直接从第三方站点加载(referrerpolicy="no-referrer"),第三方可看到用户 IP;本 PR 未做服务端图片代理。

原生 harness 说明真实文件路径和已验证预览 URL 的格式。交付写法(同一成果的预览、源码、下载默认融入同一句,多个独立成果才用列表)放在 ContainerProvider.Usage 中,只在 Web/Desktop、本地工作区且有读写/执行工具时注入,静态 mode_chat.md 与 base 相同(遵循 AGENTS.md「工具用法跟随工具」)。未增加卡片协议或改写模型回复。

同步到 Cloud 须知

共享能力落在 OSS,没有直接改 Cloud。后续同步由其他人执行,以下是在 Cloud origin/submodule/memoh 上核对并模拟 3-way 合并得到的结果。已把本 PR diff 套到 Cloud submodule/memoh(d5d894512)上,在本地 Cloud 栈跑通了 BFF 链路和 UI(见 Validation「本地 Cloud 栈」一节,未推送该合并分支);生产出口能否访问海外站点尚未验证。

不需要适配的部分

  • 路由:BFF /api/memoh/ 整体转发(packages/bff/internal/handler/handler.go、memoh_proxy.go),/site-icon 不需要登记。Cloud 模式下它和其他接口一样,要求会话和 Team 身份;前端 SDK client 会自动带 X-Team-ID。
  • 工作区:Cloud 的 E2B 沙箱在 memoh 中属于 native target,所以 Web/Desktop 会话会注入新的链接说明。它依赖的文件查看器(container/fs 接口)和 localhost 预览(*.browser.app.memoh.net 通配预览)Cloud 都已经具备。

需要人工处理

  • 出网:/site-icon 由 memoh server 直接访问第三方站点根路径,自行解析 DNS 并直连已校验的 IP,不读取 HTTP(S)_PROXY(原因写在 site_icon.go 的注释里)。
    • Cloud 的 memoh 部署在 cn-shanghai-e,经 off-cloud router 出网,deploy 下没有针对它的 NetworkPolicy。
    • 能否访问海外站点尚未实测,建议同步后在 memoh pod 里调用一次 /site-icon?url=https://github.com/。访问不到时只会退回 Globe 图标,不会报错。
    • 接口没有按用户限流,任何 Team 成员都能让 server 用生产出口 IP 访问公网站点根路径;按 origin 的缓存会限制重复请求。
  • 合并冲突:container.go、cmd/agent/module.go 和前端组件都能干净合并。有冲突的是:
    • go.mod:Cloud 已直接依赖 golang.org/x/net v0.55.0,保留该版本,不要降回 v0.50.0;把 golang.org/x/sync 改为直接依赖,然后 go mod tidy。
    • packages/sdk/src/*.gen.ts、spec/*:生成文件,不要手工合并,合并后重新生成。注意:Cloud 的 openapi-ts 重新生成会删掉手写的 packages/sdk/src/workspace-rebuild.ts,生成后要把它恢复。
    • apps/web/src/i18n/locales/{en,ja,zh}.json:大部分冲突来自 Cloud 尚未合入本 PR base 之前的 OSS 提交;解决时保留 chat.workspaceLinkUnavailable。
  • 产品确认:localhost 预览打不开时,现在的行为是提示错误,不再回退到打开用户本机的 localhost。Cloud 中没有 workspace manage 权限的成员点击预览链接只会看到这个提示。

UI skill 规则另见配套 PR,不在本 PR 更新 gitlink。

代码量自审:当前 head 84db6cacd 相对 base 共 28 个文件 +1447/-55。84db6cacd(review 修复)9 个文件 +80/-39:其中 prompt 相关测试与 mode_chat.md 还原到 base(-7 净),其余为资源上限、失败分类、SVG color 继承、前端加载判断与路径解析,均为生产代码。以下为更早口径(base 18a101e93,head e7a5604e0,未含 7fbf6b534 及 b8277a2ca;b8277a2ca 自身 8 个文件 +306/-7,其中新增 site_icon_mask.go 263 行(含注释)、生成的 SDK/OpenAPI 18 行、其余生产代码 +25/-7(与生成文件合计 -7)):28 个文件,+1075/-58。测试(含注释)6 个文件 +363/-5;生成的 SDK/OpenAPI 6 个文件 +146/-3;生产代码 16 个文件 +566/-50,排除空行和纯注释行(prompt Markdown 标题行也被排除)后 +455/-31,净增 424。无单独机械性变更。主要增量:图标探测与 SSRF 限制、按 origin 缓存与请求合并(site_icon.go)、链接分类与工作区打开(workspace-link.ts/useWorkspaceLink.ts)、链接组件、prompt 说明。未发现重复实现。重点 review URL 路由、图标缓存/失败回退、工具说明注入边界。

Related Issues

无关联 issue。配套规则:felinics/ui#29

Validation

84db6cacd(Agent 执行,未经人工 QA):独立 review 意见的修复。

  • 后端:图标 URL 限长、10s 整体时限、8 并发上限;http://[::1]/、http://127.0.0.1/ 实测按 1h 缓存(此前按暂时失败 5 分钟);SVG 的 color 属性参与 currentColor 判色。
  • 前端:useSiteIcon 按当前 src 记录加载/失败(原先用浏览器规范化后的 img.src 比较,可能永远不匹配而停在占位图标);/data/a.md#L10 这类链接去掉 ?/# 再解析文件路径。
  • Prompt:交付写法移出静态 mode_chat.md,改由工具说明注入;prompt 体积基线回到 base 的 1212 token。该改动改变了此前人工确认过横排链接时的注入位置;用户已用真实模型重测,结果见 Human QA 一节。Agent 核对:运行中的 server 二进制包含该说明,该轮上下文 24 个片段全部入选、无裁剪。
  • gofmt、golangci-lint(handlers、agent/tool)、go test -race ./internal/handlers ./internal/agent/tool ./internal/agent/runtime/native 通过;Vitest 4 个文件 31 项通过;改动文件 eslint、vue-tsc 无报错。重启 QA server 后浏览器自检:浅色 GitHub 原图、深色 GitHub 为链接色蒙版,B 站图片加载完成、占位隐藏,Vue 回退 Globe。
  • 未处理的 review 项:SSRF 前缀表未覆盖 IPv4-compatible、6to4、240/4、fec0::/10、文档网段;未设置 User-Agent;文件预览中 README 相对 /docs/x 链接会按工作区根解析;openFile 逻辑有两处相似实现。

b8277a2ca(Agent 执行,未经人工 QA):单色 SVG 低对比度染色兜底。

  • 真实站点直调 handler:GitHub 只返回 dark_mask(#24292E 对黑底约 1.4:1,对白底约 14.6:1 不染色);B 站、x.com 为 favicon.ico,不探测 SVG,也不返回蒙版。vercel.com、notion.so 在本机经代理握手超时,返回空结果并回退 Globe(与此前的慢站点现象相同)。
  • 本地 QA 环境 /api/site-icon 返回 dark_mask。Agent 浏览器自检:深色主题下两个 GitHub 链接渲染为链接紫色的蒙版(oklch(0.72 0.16 290),16px),浅色主题下用原黑色 SVG,B 站两个主题都用原图,Vue 回退 Globe。
  • go test -race ./internal/handlers -run 'SiteIcon|Icon'、golangci-lint run ./internal/handlers 通过;Vitest 27 个文件 332 项通过;改动文件 eslint 和 vue-tsc 无报错;swagger/SDK 已重新生成。按仓库约定未新增单元测试,蒙版判定只做了上述实站验证。

7fbf6b534(Agent 执行,未经人工 QA):删除 GitHub 专用的 data-base-href 规则,所有站点按同一套 <link> 声明选图标,GitHub 深色模式下现在显示其静态黑色图标(有意接受);md-link 的图标查询和加载状态拆到 useSiteIcon;缓存键归一显式默认端口;新增测试锁定 markstream 注入开关覆盖 LinkNode prop 的行为(去掉 provide 后该测试失败,已实测)。go test -race ./internal/handlers -run 'SiteIcon|Icon'、golangci-lint run ./internal/handlers、上述 3 个 Vitest 文件共 29 项通过;web typecheck 在改动文件无报错。

本次更新(663f68903→e7a5604e0,图标按 origin 探测、细点下划线、暂时失败短缓存)由 Agent 执行:

  • 本地 QA 环境直接调用接口:GitHub 响应 Cache-Control: private, max-age=3600;不可解析的域名返回空结果,max-age=299。
  • 在本地 QA 环境(本机开着 fake-IP 代理)实测 /site-icon:GitHub 返回深浅两套 SVG,B 站返回 favicon.ico。vuejs.org、MDN 经代理 TLS 握手需 5.5 秒以上,超过 5 秒握手超时,返回空并回退 Globe(属本机网络延迟,未调整超时)。
  • Go:go test -race ./internal/handlers -run 'SiteIcon|Icon' 通过,覆盖 origin 归一化、并发请求只抓一次、跨路径复用、未找到结果缓存与过期、暂时失败(超时/5xx/429)短缓存及对应的 Cache-Control、缓存上限、端口与特殊网段拦截、主题图标解析;golangci-lint run ./internal/handlers 0 问题;提交钩子的定向 lint/test 通过。
  • Vitest(仓库根运行):Markdown 链接、链接分类、浏览器地址 3 个文件共 28 项通过(e7a5604e0 实测),新增“同站多个链接只查询一次、切换主题不重新请求”“空结果在之后的渲染中重新查询”。
  • 重新生成 swagger 与 SDK,@memohai/sdk typecheck 通过。全量 @memohai/web typecheck 在本地仍有大量未改文件的既有报错,本次改动文件无报错,未宣称全量通过。
  • 细点下划线只在静态复刻页面截图核对过深浅两色,未在真实应用中截图;新版图标接口未在运行中的应用里实测。

本地 Cloud 栈(Agent 执行,非人工 QA;本地提交 f601d45de = 本 PR diff 套到 Cloud submodule/memoh d5d894512 上,隔离 compose project,未推送;早于 7fbf6b534,其中 GitHub 返回深浅两套图标的结果已不再适用,其余结论不受影响):

  • 经 BFF /api/memoh/site-icon(会话 cookie + X-Team-ID):GitHub 深链返回 favicon.svg/favicon-dark.svg,冷请求约 3.3s,命中缓存 0.14s,max-age=3600;B 站返回 favicon.ico;10.0.0.1、localhost:8080 直接返回空、不发请求;不可解析域名返回空、max-age=299。缺 X-Team-ID 返回 400,未登录返回 401。
  • UI:聊天记录是直接写进数据面数据库的固定消息,不是模型生成的。其中外链显示主题色和站点图标,不存在的站点和 localhost 回退 Globe,/data/report.md 显示文件图标。深色切到浅色后 GitHub 换成黑色图标,site-icon 请求保持 3 次(每个 origin 一次)。图片带 referrerpolicy=no-referrer;悬停显示细点下划线和 URL Tooltip。
  • 点击:外链在新标签页打开 GitHub。文件链接打开应用内 report.md 文件页,但因本地没有 E2B 沙箱,内容一直停在加载中。localhost 链接调用 container/browser/sessions 返回 200,并打开应用内 localhost:3000 预览页,页面显示预览鉴权 401(同样因为没有沙箱),没有打开用户本机。侧栏的 "The workspace could not be reached" 来自 container/fs/list 503,和本 PR 无关。
  • 未验证:生产上海出口能否访问海外站点;有真实沙箱时文件内容和预览页能否加载。

此前版本的验证(0c97d553a,仍适用于链接路由和 prompt 部分):

  • 本地隔离环境 http://127.0.0.1:8096/bot/links-qa,本分支 Web + Server + Channel;原数据库未修改。
  • Agent 浏览器验证:文件查看、工作区网页预览及计数器交互、外链打开、Tooltip、深浅主题图标。DeepSeek V4 Flash 实际问候收发通过;用户另行完成网页生成测试并确认横排链接结果。
  • 模型曾出现竖排链接,调整后的用户测试得到横排;prompt 引导不保证每次输出完全相同。
  • 首次全量 Go 测试中,未修改的 TestMemoryRuntimeManagerContract/recovers_subscriber_buffer_overflow 在 5 秒等待后失败;单独重跑通过。
  • 全局 mise run lint 被未修改的 tool-call-diff-panel.vue 中 animate-spin 超出既有基线挡住。
  • favicon 探测不会执行网站脚本;仅通过脚本切换图标的网站(包括 GitHub)不保证取得主题变体。没有主题资源的网站保留其原图标。

Screenshots / Recordings

以下截图已上传到独立证据分支,不进入功能 diff。上方截图拍摄于 0c97d553a;04690564c 已由用户在本地 QA 环境确认浅色/深色下 GitHub、B 站图标正确、慢站点回退 Globe(截图见下)。之后的 1db4d3042、68021c05f、e7a5604e0 只把暂时失败的缓存时长(服务端与浏览器 Cache-Control)从 1h 缩到 5 分钟、让前端对空结果稍后重新查询,并补充注释,不改变正常路径;这些提交未再做人工 QA。7fbf6b534 删除了 GitHub 专用规则,改变了上面人工确认过的 GitHub 深色图标,当时取消了 Human QA 勾选;b8277a2ca 加入染色兜底后,用户已在本地 QA 环境确认深浅两色图标均无问题,重新勾选。已经自动测试,并在本地 QA 环境用接口确认 GitHub 返回 max-age=3600、不可解析域名返回空结果和 max-age=299。

human qa favicon 1
human qa favicon 2

用户确认的真实模型横排结果:
inline result

浅色 / 深色图标:
light
dark

共享 Tooltip、工作区预览和文件页:
tooltip
preview
file

rebase 后再次确认文件链接进入工作区源码页:
rebased file

Human QA

  • Human QA passed

图标部分由用户在 b8277a2ca 确认;84db6cacd 移动链接交付说明的注入位置后,用户在本地 QA 环境用真实模型重新生成计数器网页:第一次出现标题加竖排列表,第二次试玩、源码、截图三个链接横排在同一行,用户确认通过。模型输出仍有波动,提示词引导不保证每次一致。

@github-actions github-actions Bot added change:server Changes backend code, configuration, or API contracts change:web Changes web frontend or shared frontend packages feat Adds or improves functionality size:M PR size uses the larger of added or deleted lines, excluding generated files needs:format Description needs template corrections; removed automatically once fixed labels Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

@qqqqqf-q Please complete the following information using the template:

  • Select exactly one option for "Author": Human / Agent.

Editing the description triggers another check; format feedback does not block or cancel code CI.

- /site-icon 只抓取链接所在站点根路径,路径与 query 不再外发;一次返回 light/dark 两套图标
- 服务端按 origin 缓存(命中 24h、未找到 1h、上限 1024)并合并并发请求;只允许 80/443 端口,补充 CGNAT/NAT64/基准测试等特殊网段拦截
- 前端按 origin 共享一次查询,切换主题直接取另一套图标,不再重新请求
- 链接悬停与键盘焦点由实线下划线改为 1px 细点
- 浏览器地址报错文案补上 [::1]
@github-actions github-actions Bot removed the needs:format Description needs template corrections; removed automatically once fixed label Sep 24, 2026
Clash、Surge 等 TUN/fake-IP 模式会把所有域名解析到 198.18.0.0/15,拦截该网段会让这类环境下所有外链都拿不到图标。
@github-actions github-actions Bot added the needs:format Description needs template corrections; removed automatically once fixed label Sep 24, 2026
@qqqqqf-q
qqqqqf-q marked this pull request as ready for review September 24, 2026 09:59
@qqqqqf-q
qqqqqf-q requested review from a team September 24, 2026 09:59
- 服务端不再为 GitHub 拼接 data-base-href 深色图标;所有站点按同一套 <link> 声明规则选图标
- 每个 icon <link> 只解析、校验一次
- 缓存键去掉显式默认端口,https://host:443/ 与 https://host/ 共用一条
- md-link 的图标查询与加载状态移入 useSiteIcon
- 测试锁定 markstream 注入开关覆盖 LinkNode prop 的行为,防止其弹层与共享 Tooltip 叠加
只靠脚本切换主题图标的站点(如 GitHub)在另一主题下只剩一个几乎看不见的单色图标。服务端对单色 SVG 按 WCAG 3:1 判断对比度,不足时附带 data URL 蒙版,前端用链接色填充其形状;多色、位图和本就清晰的图标不变。
@github-actions github-actions Bot added size:L PR size uses the larger of added or deleted lines, excluding generated files and removed size:M PR size uses the larger of added or deleted lines, excluding generated files labels Sep 24, 2026
- 图标 URL 限长 2048,探测整体限时 10s,全局最多 8 个并发
- 私网或非法重定向按确定失败缓存 1h;SVG 判色支持 color/currentColor
- useSiteIcon 按当前 src 记录加载结果,避免浏览器规范化 URL 导致不匹配
- 文件链接去掉 ?/# 后再解析路径
- 交付链接写法从静态 mode_chat.md 移到 ContainerProvider.Usage,仅在 Web/Desktop 且有工作区工具时注入
@github-actions github-actions Bot added needs:format Description needs template corrections; removed automatically once fixed and removed needs:format Description needs template corrections; removed automatically once fixed labels Sep 24, 2026
@qqqqqf-q
qqqqqf-q merged commit 67740ed into main Sep 24, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:server Changes backend code, configuration, or API contracts change:web Changes web frontend or shared frontend packages feat Adds or improves functionality needs:format Description needs template corrections; removed automatically once fixed size:L PR size uses the larger of added or deleted lines, excluding generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant