lili 87b0c085b7
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
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>
2026-07-07 20:34:02 -07:00
..

dify 游戏开发节点(形态 A / a-min)—— 搭建配方与可复现留痕

本目录是内测 WU3「dify workflow 驱动 agentscope 生成」形态 A 的落地留痕。设计与边界见 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:3128100.64.0.7:9501403,而同样经它打允许域 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 已指出 :95010.0.0.0、无鉴权、成本型 DoS 面靠主机网络隔离兜底),所以属安全相关决定,交创始人拍,不擅动。

  2. dify console 登录(凭据档里没有,须创始人给)。 dify 已 setup 完成(owner 建于 2026-07-07),但 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=succeededgames/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。便宜档生成核心真正只读 briefgameId,其余字段给合理默认即可,缺了 worker 也不会失败:

    {
      "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 登录后省手搭」的起点,手搭配方以本文上一节为准。