Dify 前端开发代理 @langgenius/dev-proxy 全解析:路由配置、热重载与 Cookie 重写机制
在 Dify 仓库中,前端(web/)开发时常常需要把 /console/api、/api 等请求转发到线上或本地后端。packages/dev-proxy 目录下的 @langgenius/dev-proxy 就是为这类场景设计的通用 Hono 开发代理:它不内置任何产品专属路由、Cookie 名称或环境变量约定,所有代理路径与上游目标都在本地配置文件中显式声明。读完本文,你将掌握该包的完整安装与 CLI 用法、配置结构与路由匹配规则、CORS 策略、Cookie 重写(含 __Host-/__Secure- 前缀与多目标作用域隔离)的实现细节,并能直接参考 Dify 仓库自身的前端代理配置来搭建自己的开发链路。
一、包定位与安装
从 README 的描述看,这个包的核心定位是「generic Hono-based development proxy」:基于 Hono 框架(实际依赖见 package.json 中的 hono 与 @hono/node-server),只做 HTTP/WebSocket 转发,不包含任何 Dify 专属逻辑。产品相关的配置全部外置到使用方的配置文件里。
安装与脚本接入方式:
pnpm add -D @langgenius/dev-proxy
在前端项目的 package.json 中添加脚本:
{
"scripts": {
"dev:proxy": "dev-proxy --config ./dev-proxy.config.ts --env-file ./.env.local"
}
}
然后运行 pnpm dev:proxy。Dify 仓库自身就是这样使用的:web/package.json 中声明了 "dev:proxy": "dev-proxy --config ./dev-proxy.config.ts --env-file ./.env.local",并把 @langgenius/dev-proxy 作为 workspace:* 依赖引入,配置文件即 web/dev-proxy.config.ts。
需要注意运行环境:package.json 中 engines 字段要求 node ^24.20.0,且构建走 vite-plus(vp pack / vp test)。
二、CLI 参数与热重载行为
CLI 入口为 dev-proxy,支持以下选项(与 cli.ts 中 printUsage 输出的帮助文本一致):
dev-proxy --config ./dev-proxy.config.ts
--config、-c:配置文件路径,默认dev-proxy.config.ts;--env-file:在求值配置文件之前先加载环境变量文件;--host:覆盖配置中的server.host;--port:覆盖配置中的server.port;--watch:监听配置文件与环境变量文件变更并热重载(默认开启);--no-watch:禁用配置与环境文件的热重载;--help、-h:打印帮助。
README 特别强调:不支持 --target。上游目标必须写在配置文件里,保证「路由 + 上游」的映射关系显式可查。
从源码看热重载的完整机制在 cli.ts 的 createDevProxyRuntime 中:
- 默认通过
watchDevProxyConfig(基于c12的watchConfig)监听配置文件,若指定了--env-file,再用chokidar单独监听该 env 文件; - 变更到达后调用
enqueueReload,任务被串行排队(reloadTask = reloadTask.then(...)),避免并发重载; - 重载时先比较解析出的 host/port:若不变,仅调用
runtime.updateConfig(nextConfig)在运行进程内替换 Hono 应用与 WebSocket 升级处理器,日志输出reloaded config changes;若 host 或 port 变了,则关闭旧服务器并startDevProxyServer重新监听,日志输出restarted on http://host:port after ...。
这与 README「Behavior」一节的描述吻合:路由、CORS、target、cookie 重写变更在运行进程中生效,而监听地址变化会重建服务器。另外 CLI 注册了 SIGINT/SIGTERM 清理钩子,退出前会关闭 env 监听、config 监听和所有已 upgrade 的 WebSocket socket。
参数解析本身在 config.ts 的 parseDevProxyCliArgs:支持 --name=value 内联写法,遇到未识别的选项会直接抛错 Unsupported dev proxy option;端口经 resolvePort 校验必须是 1–65535 的整数。默认值也在这里定义:DEFAULT_PROXY_HOST = '127.0.0.1'、DEFAULT_PROXY_PORT = 5001。
三、配置结构(Config Shape)
配置文件支持 .ts、.mts、.js、.mjs 四种格式,由 c12 加载。最小完整示例(README 原文):
import { defineDevProxyConfig } from '@langgenius/dev-proxy'
export default defineDevProxyConfig({
server: {
host: '127.0.0.1',
port: 5001,
},
routes: [
{
paths: '/api',
target: 'https://example.com',
},
],
cors: {
allowedOrigins: 'local',
},
})
defineDevProxyConfig 在 config.ts 中只是一个恒等函数,作用是提供类型推导。真正的结构定义在 types.ts:
server:可选,host(字符串)与port(数字);routes:必填,DevProxyRoute[]。每项包含paths(单个字符串或字符串数组)、target(上游 URL)、可选的cookieRewrite(选项对象或false显式禁用);cors:可选,allowedOrigins取值为'local'或显式 Origin 数组。
加载完成后 assertDevProxyConfig 会做运行时断言:配置必须是对象且必须包含 routes 数组,否则启动即报错(Dev proxy config must include a routes array.)。
路由匹配规则:声明顺序优先 + 前缀包含
README 明确了匹配语义:routes 按声明顺序匹配,第一条命中的路由生效;每个配置的 path 同时匹配精确路径及其所有子路径,即 paths: '/api' 会命中 /api、/api/apps、/api/apps/123。
源码印证在 server.ts 的 findProxyRoute:
routes.find((route) =>
normalizeRoutePaths(route.paths).some(
(routePath) => requestPath === routePath || requestPath.startsWith(`${routePath}/`),
),
)
注意 startsWith(routePath + '/') 的写法:/api-v2 不会误命中 /api 路由,避免了朴素前缀匹配的经典坑。Hono 侧则通过 app.all(path) 与 app.all(path + '/*') 两条路由注册来覆盖精确路径和子路径(registerProxyRoute),并对不以 / 开头的路径直接抛错。
因此当两组路由存在包含关系时,README 给出的实践就是「更具体的放前面」:
routes: [
{ paths: '/api/enterprise', target: 'http://127.0.0.1:5003' },
{ paths: '/api', target: 'http://127.0.0.1:5002' },
]
Dify 自身的 web/dev-proxy.config.ts 正是这一原则的现实案例:把 /console/api/enterprise、/api/enterprise、/admin-api、/mfa、/scim 等更具体的路径指向企业版上游 DEV_PROXY_ENTERPRISE_TARGET,随后才是 /console/api 与通用 /api 路由,且 /socket.io 也显式归入 console 路由以支持 WebSocket。
上游 URL 拼接
转发时的 URL 构造由 buildUpstreamUrl(target, requestPath, search) 完成:若请求路径本身以 target 的 path 为前缀(例如 target 是 https://example.com/base、请求为 /base/api),则直接使用请求路径,避免拼成 /base/base/api;否则把请求路径追加到 target path 之后。查询串原样保留。
四、CORS 策略:默认信任本地 Origin
默认情况下(allowedOrigins: 'local'),代理对来自本地开发 Origin 的带凭证 CORS 请求放行。源码中本地宿主名单是 server.ts 里的:
const LOCAL_DEV_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
命中本地 Origin 时,响应会写入 Access-Control-Allow-Origin: <origin>、Access-Control-Allow-Credentials: true,并追加 Vary: Origin。若要收紧到指定来源,配置成数组即可:
cors: {
allowedOrigins: ['http://localhost:3000'],
}
此时 isAllowedDevOrigin 只做精确的 includes(origin) 判断,其余 Origin 的响应不带任何 CORS 头。
预检请求由代理自身处理,不转发给上游:OPTIONS 请求直接返回 204,并带上 Access-Control-Allow-Methods: GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS;Access-Control-Allow-Headers 会回显客户端的 Access-Control-Request-Headers,缺省值为 Authorization, Content-Type, X-CSRF-Token。源码中还有一个针对私有网络请求的细节:若客户端带了 Access-Control-Request-Private-Network: true,代理会回 Access-Control-Allow-Private-Network: true——这正是本地前端直连 127.0.0.1 上游时浏览器「private network access」预检所需。
WebSocket Upgrade 请求同样受同一 Origin 策略约束:createWebSocketUpgradeHandler 在升级握手阶段就检查 Origin,不合法直接回 403 Forbidden,未命中路由回 404。
五、两个典型场景
场景一:本地前端经单台代理访问线上后端
前端请求 http://127.0.0.1:5001/api/apps,代理转发到 https://cloud.example.com/api/apps:
import { defineDevProxyConfig } from '@langgenius/dev-proxy'
const target = process.env.DEV_PROXY_TARGET || 'https://cloud.example.com'
export default defineDevProxyConfig({
server: {
host: process.env.DEV_PROXY_HOST || '127.0.0.1',
port: Number(process.env.DEV_PROXY_PORT || 5001),
},
routes: [
{
paths: '/api',
target,
},
],
})
配合可选的 .env 文件:
DEV_PROXY_TARGET=https://cloud.example.com
DEV_PROXY_HOST=127.0.0.1
DEV_PROXY_PORT=5001
启动命令 dev-proxy --config ./dev-proxy.config.ts --env-file ./.env.local。这里 --env-file 的加载时机很关键:从 config.ts 的 createC12ConfigOptions 看,env 文件在 c12 求值配置文件之前注入(dotenv 选项开启 interpolate: true),因此 process.env.DEV_PROXY_TARGET 在配置脚本执行时已经可用。并且该 env 文件被持续监听,编辑后代理自动重载,无需手动重启。
场景二:一个前端对接两个本地后端
例如 /console/api/* 走本地 console 后端 http://127.0.0.1:5001,/api/* 走本地公共 API 后端 http://127.0.0.1:5002:
import { defineDevProxyConfig } from '@langgenius/dev-proxy'
const consoleApiTarget = process.env.DEV_PROXY_CONSOLE_API_TARGET || 'http://127.0.0.1:5001'
const publicApiTarget = process.env.DEV_PROXY_PUBLIC_API_TARGET || 'http://127.0.0.1:5002'
export default defineDevProxyConfig({
server: {
host: process.env.DEV_PROXY_HOST || '127.0.0.1',
port: Number(process.env.DEV_PROXY_PORT || 8082),
},
routes: [
{
paths: '/console/api',
target: consoleApiTarget,
},
{
paths: '/api',
target: publicApiTarget,
},
],
})
对应的 .env:
DEV_PROXY_CONSOLE_API_TARGET=http://127.0.0.1:5001
DEV_PROXY_PUBLIC_API_TARGET=http://127.0.0.1:5002
DEV_PROXY_HOST=127.0.0.1
DEV_PROXY_PORT=8082
六、Cookie 重写:让线上安全 Cookie 在本地 HTTP 下工作
这是该包最有技术含量的部分。问题背景:线上后端(如 HTTPS 站点)通常会签发带 __Host- 或 __Secure- 前缀的安全 Cookie——浏览器要求这类 Cookie 必须是 Secure、Path=/ 且不带 Domain 属性。而本地开发走 http://localhost,浏览器根本不会保存 Secure Cookie,登录态直接失效。cookieRewrite 就是为此设计的 opt-in 机制,且完全由配置驱动:包本身不认识任何应用级 Cookie 名称。
基本用法
import type { CookieRewriteOptions } from '@langgenius/dev-proxy'
import { defineDevProxyConfig } from '@langgenius/dev-proxy'
const cookieRewrite: CookieRewriteOptions = {
hostPrefixCookies: ['access_token', 'refresh_token', /^passport-/],
}
export default defineDevProxyConfig({
routes: [
{
paths: '/api',
target: 'https://cloud.example.com',
cookieRewrite,
},
],
})
hostPrefixCookies:需要参与「Host 前缀转换」的 Cookie 名单,支持字符串精确匹配和正则(如/^passport-/),匹配发生在去除安全前缀之后的名字上(shouldUseHostPrefix先stripSecureCookiePrefix再比对);- 设置
cookieRewrite: false可对某条路由显式关闭重写。
双向重写流程
从 cookies.ts 的实现看,重写是双向的:
本地 → 上游(rewriteCookieHeaderForUpstream):浏览器发出的普通 Cookie(如 access_token=xxx)会被加回 __Host- 前缀再发给上游——当上游 target 是 HTTPS 时 useHostPrefix: true(见 server.ts 中 useHostPrefix: targetUrl.protocol === 'https:')。已有的 __Host- 名字保持原样,__Secure- 名字则统一改写为 __Host- 形态。这样上游按线上协议校验 Cookie 时不会拒绝。
上游 → 本地(rewriteSetCookieHeadersForLocal):上游 Set-Cookie 里的安全 Cookie 在写给本地浏览器前会被「降级」:
- 剥离
__Host-/__Secure-前缀(toLocalCookieName); - 删除
Domain=、Secure、Partitioned属性,否则浏览器在http://localhost下会拒收; SameSite=None改写为SameSite=Lax(SameSite=None同样要求Secure);Path=统一归一为Path=/。
多目标作用域隔离:localCookieScope: 'target-origin'
当一个本地代理同时指向多个线上目标时(比如 Dify 前端同时代理公共云和企业版),同一个 access_token 不能跨目标串用。此时启用目标作用域:
const cookieRewrite: CookieRewriteOptions = {
hostPrefixCookies: ['access_token', 'csrf_token', 'refresh_token'],
localCookieScope: 'target-origin',
csrfHeader: {
cookieName: 'csrf_token',
headerName: 'X-CSRF-Token',
},
}
从 cookies.ts 看其机制:
- 对每个 target 的 origin 做 FNV-1a 哈希得到作用域 key(
hashScope),本地 Cookie 以dev_proxy_<hash>_<原cookie名>的形式存储(toScopedLocalCookieName),即不同上游的登录 Cookie 在本地浏览器中天然隔离; - 转发时只把「当前 target 作用域」的 Cookie 还原成上游名字发出去,其余作用域的 Cookie 以及非作用域但命中
hostPrefixCookies的旧 Cookie 一律剔除(返回undefined后被 filter 掉),防止把 A 站的 token 泄漏给 B 站; - 若配置了
csrfHeader,代理会用当前作用域下的csrf_tokenCookie 值覆盖请求头中的X-CSRF-Token(有值则 set,无值则 delete),解决前端残留了旧 CSRF 头导致上游校验失败的问题。
Dify 前端的 web/dev-proxy.config.ts 就是这套机制的生产级示例:hostPrefixCookies 覆盖 access_token、csrf_token、refresh_token、webapp_access_token 及 /^passport-/,启用 localCookieScope: 'target-origin',并声明 csrfHeader 映射 csrf_token → X-CSRF-Token;三条路由(enterprise 组、console 组、public /api)全部挂载同一份 difyCookieRewrite。
七、转发行为与底层实现细节
README「Behavior」一节列出的行为,在 server.ts 中均有对应实现,这里补充源码级的细节:
- 路径前缀保留:匹配的路径前缀原样转发,不做重写或剥离;
- WebSocket 支持:Upgrade 请求复用同一套路由、Origin 策略和 Cookie 重写逻辑,走
http/https原生的upgrade事件(createWebSocketUpgradeHandler),成功握手后把上下游 socket 双向pipe起来;上游不可达时向客户端回502 Bad Gateway。Dify 配置里显式代理/socket.io即依赖此能力; - 请求体流式转发:非 GET/HEAD 请求直接把
request.raw.body作为 fetch 的 body 并设duplex: 'half',大文件上传不会先在内存中缓冲; - Hop-by-hop 头清理:转发前删除
connection、keep-alive、proxy-authenticate、proxy-authorization、te、trailer、transfer-encoding、upgrade等逐跳头,并且额外尊重Connection:头中列出的自定义逐跳头(createHopByHopHeaderNames);同时删除host头、强制accept-encoding: identity,并要求上游若携带origin则改写为目标 origin; - 响应侧:上游响应头同样清理逐跳头,并丢弃
content-encoding与content-length(因为上游已被要求 identity 编码,且 body 会重新流经 Hono 响应管线),set-cookie单独提取以便重写后逐条重新 append; - 错误处理:Hono 全局
onError捕获上游请求失败,统一返回502与Upstream proxy request failed.文本,并保留 CORS 头以便本地前端能读取错误。
测试覆盖方面,包内自带 config.spec.ts、server.spec.ts、cookies.spec.ts 与 cli.spec.ts 四组单元测试,分别验证 CLI 参数解析、路由/URL 构造、Cookie 双向重写和服务器行为,可作为修改或二次开发时的行为基线。
八、小结
@langgenius/dev-proxy 的设计哲学是「约定极少、配置即文档」:零内置路由、零内置 Cookie 知识,把复杂度集中在三处能力上——显式的路由/上游映射、面向本地开发的带凭证 CORS、以及把线上安全 Cookie 协议适配到本地 HTTP 环境的 Cookie 重写。其热重载(host/port 不变时进程内热更新、变化时重建服务器)让 --env-file 驱动的切换上游目标变得几乎无感。在 Dify 仓库中,它是 web/ 前端对接云端的标准开发通道;如果你在自己的前端项目中遇到「本地前端 + 线上/多后端 + 安全 Cookie」的组合问题,这个包的实现(尤其是 cookies.ts 的双向重写与作用域隔离)提供了可以直接借鉴的完整参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00