#  🪄 dsh-photo-skins

中文 | [English](README.md)

一次导入，自动取色，让每一张照片成为 DSH Web GUI 的专属皮肤

DSH Web GUI 的照片皮肤插件：导入你自己的照片，把它变成这个界面的皮肤。照片铺
在 GUI 后面作为背景层，并自动提取照片主色作为界面强调色与明暗遮罩色调。渲染
可调：填充方式（铺满/完整显示）、模糊（0-100px，全局或按「空对话/有内容」分区）、
压暗（0-100%）与「跟随照片取色」开关。

0831更新：插件更新升级内置「联网与数据抓取」板块

![Photo skins demo](screenshots/Interface_2.png)

## ✨ 功能

### 📥 导入照片

![Photo skins demo](screenshots/Plugin_Path.png)
文件选择器、拖拽、粘贴，任选其一
支持 PNG / JPG / WebP / GIF，单张上限 25MB

### 👗 试穿 / 应用 / 删除

在首级设置区「照片皮肤」卡片中集中管理
「试穿」临时预览，不影响已经应用的照片
「应用」一键写入皮肤，「删除」一键清除本地副本

### 🎨 自动取色

照片主色自动成为界面强调色
生成明暗两套遮罩色调，让可读性遮罩带上照片自身的色调
输出 CSS 变量 --dsw-photo-accent / --dsw-photo-accent-soft / --dsw-photo-accent-contrast

### 🖼️ 渲染自由调节

![Photo skins demo](screenshots/Interface_chat.png)
填充方式：铺满（cover）/ 完整显示（contain）
模糊：0–100px，支持全局或拆分
压暗：0–100%，保证前景内容可读
跟随照片取色：随时开关

### 💾 本地持久化

已应用的照片经 photo-skins 设置命名空间持久化，刷新后依然生效

## 🚀 安装  

从 npm：

```sh
dsh plugin --profile web add dsh-photo-skins
```

从本仓库安装（开发调试）：

```sh
pnpm install && pnpm build
dsh plugin --profile web add link:.
```

然后刷新正在运行的 Web GUI（若新设置区没有出现，重启 `dsh web`）。

## 存储位置

照片保存在 `<DSH_HOME>/photo-skins/<id>/`（`original.<ext>` 加一份
`manifest.json`，记录显示名、类型、大小与导入时间）

## 增强联网与数据抓取（Web-Reach）

Web-Reach 是插件内第二个模块，内置免费调用，无需官方Fetch计费，适合配置：网关模型、三方中转、私有部署等

Agent 在对话中判断需要联网信息时会自动调用：

- `web_search`：作为 `ctx.web` 的三个搜索端（provider）之一接入——DuckDuckGo
  （零 key；基于抓取，需本机可直连 duckduckgo.com）、Exa 免费（托管 MCP，默认后端，
  卡片中标「推荐」）、Exa API key（需保存密钥），设置卡片里三选一。
- `web_fetch`：Jina Reader 把任意公开网页读成 Markdown（无需 key）。
- `rss_fetch`：解析 RSS 2.0 / Atom 订阅源为其条目（无需 key）。

联网搜索是一个「主开关」：对话输入框上方的「联网搜索」开关会显示当前后端并控制
开关，它与设置卡片共用同一个 `web-reach` 命名空间，二者选择实时同步。开启时
（默认）本插件把 `ctx.web` 的 `searchProviderId` 指向所选后端，于是官方
`web_search`都会改走我们的后端；关闭时恢复官方 DeepSeek 原生搜索。

选择「Exa API key」时需填写密钥。密钥存储于 `web-reach` 设置命名空间并标记为 secret，线路上脱敏、
浏览器端不回读明文（只写不读）。搜索词与网页地址会发往第三方服务（DuckDuckGo /
Exa / Jina Reader），数据会离开本机。详见下方安全模型。

## 安全模型

- 所有路由（`/api/photo-skins/*`）均带同源围栏（Sec-Fetch-Site / Origin）：
  跨站网页无法读取、导入或删除照片。
- 上传按文件魔数校验，不看文件名与声明类型——改名的 SVG 或可执行文件会被拒绝
  （415）。SVG 因脚本风险刻意不支持。
- 上传上限 25MB（413），原子写入（tmp + rename）。
- 存储 id 由本插件生成并过白名单正则，任何路径都不可能逃出存储目录。
- Web-Reach 的搜索 provider 与工具在宿主进程内运行（不暴露任何浏览器路由），每次
  出站抓取都带 SSRF 防护：仅允许公开 http(s) 地址，拒绝环回 / 链路本地 / 私有 /
  保留网段、云元数据主机名与 IP 混淆形式；重定向逐跳校验并限次；响应超时 15-20s、
  体积上限 2MiB；provider/工具执行遵守本轮对话的取消信号。Exa 托管 MCP 端点
  （https://mcp.exa.ai/mcp）同样先经 SSRF 门禁。
- DuckDuckGo 搜索路径只发送纯文本搜索词（不是 URL），并把解码后的结果网址回传给
  模型，不会去抓取这些网址。
- Exa API key 为可选的 `role('secret')` 设置字段：在线路边界剥离，浏览器端拿不到
  明文；provider 仅在宿主侧、执行时读取。选了「Exa API key」但没保存 key 时，该
  provider 不可用，搜索会显式报错而非静默降级。
- Exa 免费（MCP）走 Exa 官方托管 MCP 端点，当前宣称无需 key；该端点的免 key 状态
  未经本插件验证，随时可能变化。


## 本地开发
环境要求：Node.js ^22.19 || >=24，pnpm

```sh
pnpm install
pnpm typecheck   # tsc --noEmit
pnpm test        # vitest run
pnpm build       # tsdown -> lib/（host 半区）+ lib/client.js（浏览器 bundle）
```

构建产出面向 `window.__ModuleLoader__` 的 closure-factory 浏览器 bundle，
lightningcss 内联 CSS Modules、`@deepseek-ai` 平台模块外部化。

## 许可

BSD-3-Clause。涉及的第三方来源见 THIRD_PARTY_NOTICES.md。
