spike(dify): WU3 游戏开发节点形态A 搭建配方 + DSL 骨架 + ssrf_proxy 阻塞留痕
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

能力已证(直连 :9501→202→agentscope 真跑);但实测坐实 dify HTTP 节点经内建
ssrf_proxy(squid)出网,deny to_private_networks 拦掉 100.64/10 → 打 :9501 返 403。
两个创始人门:①放行 ssrf_proxy 到 worker 端点(共享基建+安全姿态,附最小侵入 squid
例外+回滚)②dify console 登录(凭据档缺,须创始人给)。含握手级超时/dify- 前缀去重/
budget 死字段/隔离靠专用栈等读码红线。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-07 20:34:02 -07:00
parent 7e7ac272b2
commit 87b0c085b7
2 changed files with 172 additions and 0 deletions

View File

@ -0,0 +1,79 @@
# dify 游戏开发节点(形态 A / a-min)—— 搭建配方与可复现留痕
本目录是内测 WU3「dify workflow 驱动 agentscope 生成」形态 A 的落地留痕。设计与边界见 [`docs/agent-specs/2026-07-07-内测-WU3-dify游戏开发节点-设计.md`](../../docs/agent-specs/2026-07-07-内测-WU3-dify游戏开发节点-设计.md);这里只放「照着做就能搭起来」的配方、一份可导入的 workflow 定义骨架,以及搭建时踩到的一个真配置阻塞。
## 一句话
在 mini-infra 那套已部署的 dify(`:18080`)里搭一条最小 workflow:开始节点收一句话创意 `brief` → 一个 HTTP Request「游戏开发节点」把它组成后端早已定义的 §6.1 job、POST 给便宜档 cheap-worker `:9501/generate` → 结束节点回显那次投递的响应。worker 入队即返 202,agentscope 在后台真跑一局,产物落在 worker 的 `game_dir`。后端一行不改。
## 现状:能力已证,但有一个必须先解的真阻塞
**能力这一层是通的。** 从 dify 宿主 mini-infra(`100.64.0.8`)直接 `curl --noproxy` 打一份最小 a-min job 到 mini-desktop(`100.64.0.7`)的 `:9501/generate`,worker 收下返 202、agentscope 真跑、worker 收口日志报 `status=succeeded`——这条端到端此前已实测走通。job 契约、生成核心、落盘全是现货,dify 只是又一个「组 job 的调用方」。
**但 dify 的 HTTP Request 节点不是直连,它经 dify 自带的 SSRF 代理出网,而那个代理默认把内网 worker 拦死。** dify 部署自带一个 squid 反 SSRF 代理(容器 `muse-dify-ssrf_proxy-1`,`ssrf_proxy:3128`),所有 HTTP 节点的出站请求都从它走。它的访问规则里有这么一条,且排在放行规则之前:
```
http_access deny to_private_networks # to_private_networks 含 100.64.0.0/10(Tailscale CGN 段)
http_access allow allowed_domains # 仅 .marketplace.dify.ai
http_access allow client_localnet
...
```
squid 首个匹配即生效,`100.64.0.7` 落在 `100.64.0.0/10` 里,先撞上 `deny to_private_networks` 就被拒,后面的放行规则根本到不了。实测坐实:从 dify-api 容器经 `ssrf_proxy:3128` 打 `100.64.0.7:9501` 返 **403**,而同样经它打允许域 `marketplace.dify.ai` 返 200。所以**直接在 dify UI 里配好 HTTP 节点点运行,会得到 403、连不上 worker**——这不是节点配错,是 dify 的 SSRF 防护按设计拦掉了所有私网目标(Tailscale 的 `100.64/10` 也算私网)。设计 §3.2/§3.5 预警过「HTTP 节点经代理出网需确认目标 IP 放行」,这里把它坐实了。
## 两个只有创始人能拍/能给的门
1. **放行 ssrf_proxy 到 worker 端点(共享基建 + 安全姿态变更,须创始人拍)。** 最小侵入的修法是在 squid 的 `deny to_private_networks` 之前插一条只放行这一个 host:port 的例外,而不是关掉整个 SSRF 防护:
```
# 在 muse-dify-ssrf_proxy 的 squid.conf 里,deny to_private_networks 之前加:
acl gen_worker dst 100.64.0.7
http_access allow gen_worker
```
然后 `docker exec muse-dify-ssrf_proxy-1 squid -k reconfigure`(或重启该容器)。这只对生成 worker 那一个内网地址开一道窄缝,其余私网仍全拦,回滚就是删掉这两行再 reconfigure。但它毕竟是在改一套别人今天刚建起来、且承担 SSRF 防护职责的共享 dify 实例,又是把一个零鉴权、会真花 new-api 钱的内网入口对 dify 放开(设计 §3.5 已指出 `:9501` 绑 `0.0.0.0`、无鉴权、成本型 DoS 面靠主机网络隔离兜底),所以属安全相关决定,交创始人拍,不擅动。
2. **dify console 登录(凭据档里没有,须创始人给)。** dify 已 setup 完成(owner 建于 2026-07-07),但 [`docs/内网凭据与端点.md`](../../docs/内网凭据与端点.md) 里没有 dify 的 console 账号密码。没有它无法登进 dify 控制台去搭/导入 workflow,也无法用 console API 导入下面这份 DSL。setup 已完成故不能再走 `/console/api/setup` 重建 owner。拿到登录后,导入或手搭都是几分钟的活。
## 搭建步骤(拿到上面两个门之后)
**第一步 · worker 就位(已满足)。** dev 生成机 mini-desktop 上 cheap-worker(`:9501`)与 cheap Service(`:8300`)在跑,`:9501` 从 mini-infra TCP 可达、`GET /generate` 返 404(POST-only,证明在跑)。a-min 形态不带 `userToken`、不走回调,driver 用进程级全局 `NEWAPI_KEY`——dev 单一生成栈天然就是 dify 专用栈,与真实 create 路的 per-user 计费(WU2)在同栈上互斥,内测按「dev 单栈天然独立」成立(设计 §3.4)。
**第二步 · 放行 ssrf_proxy(门①)。** 见上。验证 = 从 dify-api 容器经 `ssrf_proxy:3128` 打 `:9501/generate` 不再 403。
**第三步 · dify 里建 workflow(门②)。** 登进 dify 控制台,导入 `workflow-游戏开发节点-formA.yml`(或按下节手搭三节点)。
**第四步 · 手动跑一次验收。** 在 dify 里给 `brief` 填一句创意点运行。成功判据 = HTTP 节点拿到 **202**(不是等生成完成——生成结果不从这个响应回来),随后到 mini-desktop 看 worker 收口日志出现 `status=succeeded`、`games/amgen-<gameId>/` 落定 `src/` 产物。注意别把「`ls src/` 非空」当成功证据:scaffold 会在真生成前先克隆 `_template/src`(7 文件),一次 scaffold 成功但生成失败的 run 同样 src/ 非空。
## HTTP「游戏开发节点」配置(手搭时照填)
- **方法 / URL**:`POST http://100.64.0.7:9501/generate`(dev worker 在 mini-desktop;若 worker 迁机改这个 IP)。
- **超时**:握手级,约 **10 秒**。这是异步投递面,worker 入队立即返 202,真生成在后台跑几十秒到几分钟。**绝不能**把超时设成等生成完成的长值——结果经落盘/回调回流,不从这个 HTTP 响应回来。
- **Body(application/json)**:一份 §6.1 job,a-min 形态省略 `callback`。便宜档生成核心真正只读 `brief` 与 `gameId`,其余字段给合理默认即可,缺了 worker 也不会失败:
```json
{
"job_id": "dify-{{#sys.workflow_run_id#}}",
"traceId": "dify-{{#sys.workflow_run_id#}}",
"tier": "cheap",
"kind": "generate",
"templateId": "generic",
"brief": "{{#start.brief#}}",
"model": "MiniMax-M2.7",
"gameId": "dify-{{#sys.workflow_run_id#}}",
"budget": { "maxYuan": 0.15, "maxLlmCalls": 8 },
"idempotency_key":"dify-{{#sys.workflow_run_id#}}",
"deadline_ms": 600000
}
```
几条写死的红线(读码坐实,别踩):
- **`traceId` 必带 `dify-` 前缀**做命名空间隔离,且令 `job_id`=`traceId`=`idempotency_key`=同一个 `dify-<workflow_run_id>`。worker 按 `job_id`(缺则 `traceId`)在进程级 `_seen` 去重:同键重投直接返 202、不重入、不重复烧 token——所以 dify 侧对同一次运行配 HTTP 重试是**安全**的(重试复用同 `workflow_run_id`)。反过来,一次**真正的重新生成必须是一次新的 dify 运行**(新 `workflow_run_id` = 新 job_id);别在节点里每次重试新造 uuid(那才会重复生成),也别以为同 job_id 再 POST 能拿到新产物(会被静默去重照返 202)。`_seen` 是进程级、永不淘汰、worker 重启即清空。
- **`budget.maxYuan` 是死字段,不承载封顶**,别据它以为「单局最多 ¥0.15」。单局真实成本上界由服务端 `cheap_budget` 固定的 ¥10 软停 / ¥15 硬地板决定(调用方 override 被直接 pop 掉)。dify 路真正的额度兜底只有一条:给这个 dify 专用 new-api token 在 new-api 侧配用量配额上限(按 ¥15/局估,配 ¥150 日额约 10 局/日触顶)。
- **不要试图从 dify 节点透传 token 做 per-user 计费**——driver 的 key 只从进程级 env `NEWAPI_KEY` 取,job 里没有任何 token 位,透传做不到。隔离靠「这台 dev 栈就是 dify 专用栈」,不靠 job 自带凭据。
- **鉴权**:`/generate` 当前对入站不鉴权,信任边界靠内网不可外达。别为形态 A 单独给 worker 加鉴权(重复造设计已论证不该碰的服务间信任面);风险面(零鉴权 + 会花钱的内网入口)靠 `:9501` 所在主机 Tailscale-only/NAT/防火墙收口。
## 文件
- `workflow-游戏开发节点-formA.yml` —— dify 1.15.0 workflow 定义骨架(start → http-request → end)。**未经实测导入**(缺 console 登录无法验证),导入后须在 dify UI 内核对节点变量引用与超时,别当成拿来即跑。它是给「拿到 dify 登录后省手搭」的起点,手搭配方以本文上一节为准。

View File

@ -0,0 +1,93 @@
# dify 1.15.0 workflow DSL 骨架 —— 内测「游戏开发节点」形态 A / a-min
# 用途:拿到 dify console 登录后,经「导入 DSL」省去手搭三节点的起点。
# ⚠️ 未经实测导入(缺 console 登录无法验证)。导入后必须在 dify UI 内核对:
# ① HTTP 节点变量引用 {{#start.brief#}} / {{#sys.workflow_run_id#}} 是否解析;
# ② 超时设为握手级(约 10s,只等 202/503),绝不设成等生成完成的长超时;
# ③ 目标 IP 100.64.0.7 与 dev worker 实际所在机一致;
# ④ ssrf_proxy 已放行该内网端点(见 README「门①」),否则节点跑出来是 403。
# 手搭配方以 README「HTTP 游戏开发节点配置」一节为准,本文件从属于它。
app:
name: 游戏开发节点-形态A
description: 内测·把一句话创意组 §6.1 job 直连 cheap-worker :9501/generate,由 agentscope 生成小游戏(a-min:无 userToken、无回调,产物落 worker game_dir)
icon: "🎮"
icon_background: "#FFEAD5"
mode: workflow
use_icon_as_answer_icon: false
kind: app
version: 0.1.5
workflow:
conversation_variables: []
environment_variables: []
features: {}
graph:
edges:
- id: start-to-http
source: startNode
target: gameDevNode
sourceHandle: source
targetHandle: target
type: custom
- id: http-to-end
source: gameDevNode
target: endNode
sourceHandle: source
targetHandle: target
type: custom
nodes:
- id: startNode
type: custom
position: { x: 80, y: 240 }
data:
type: start
title: 开始
variables:
- variable: brief
label: 一句话创意
type: paragraph
required: true
max_length: 1000
- id: gameDevNode
type: custom
position: { x: 400, y: 240 }
data:
type: http-request
title: 游戏开发节点
desc: 组 §6.1 job POST 给 cheap-worker :9501/generate(握手级超时,只等 202)
method: post
url: http://100.64.0.7:9501/generate
authorization:
type: no-auth
config: null
headers: "Content-Type:application/json"
params: ""
body:
type: json
data: |
{
"job_id": "dify-{{#sys.workflow_run_id#}}",
"traceId": "dify-{{#sys.workflow_run_id#}}",
"tier": "cheap",
"kind": "generate",
"templateId": "generic",
"brief": "{{#start.brief#}}",
"model": "MiniMax-M2.7",
"gameId": "dify-{{#sys.workflow_run_id#}}",
"budget": { "maxYuan": 0.15, "maxLlmCalls": 8 },
"idempotency_key": "dify-{{#sys.workflow_run_id#}}",
"deadline_ms": 600000
}
timeout:
max_connect_timeout: 5
max_read_timeout: 10
max_write_timeout: 10
- id: endNode
type: custom
position: { x: 720, y: 240 }
data:
type: end
title: 结束
outputs:
- variable: accept_status
value_selector: [gameDevNode, status_code]
- variable: accept_body
value_selector: [gameDevNode, body]