spike(dify): WU3 游戏开发节点形态A 搭建配方 + DSL 骨架 + ssrf_proxy 阻塞留痕
能力已证(直连 :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:
parent
7e7ac272b2
commit
87b0c085b7
79
spikes/dify-game-dev-node/README.md
Normal file
79
spikes/dify-game-dev-node/README.md
Normal 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 登录后省手搭」的起点,手搭配方以本文上一节为准。
|
||||||
93
spikes/dify-game-dev-node/workflow-游戏开发节点-formA.yml
Normal file
93
spikes/dify-game-dev-node/workflow-游戏开发节点-formA.yml
Normal 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]
|
||||||
Loading…
x
Reference in New Issue
Block a user