在这里插入图片描述

这是我用 Dify + EdgeOne Pages 在一个周末搭出来的东西。配套 Dify 工作流模板已发布到 Marketplace,前端 Next.js 代码全部开源。

写在前面:这个项目想解决一个很真实的问题

凑不齐人、DM 临时鸽、新手主持不入戏。线下不容易,线上更难。市面上的"AI 剧本杀"都是一个套路:

  • AI 说你成功你就成功,说你失败你就失败,没有真正的规则
  • 掉了血、SAN 减了、找到的线索,下一句对话就忘
  • “凶手"完全由 LLM 临时编,前后矛盾——你说"这线索指向 A”,它就让 A 是凶手;你说"指向 B",它就让 B 是

直到我发现 Dify 的 会话变量 (Conversation Variables)——一个跨对话轮持久化、按 conversation_id 隔离的状态容器。再叠加上 EdgeOne Pages 对 Next.js 16 的一键部署,我意识到:这两个东西凑一块儿,正好可以做一个真正"规则不会被 LLM 自己打破"的 AI 守秘人

来看几张实拍图:

欢迎页。左侧调查档案(角色卡 + HP/SAN + 线索本)实时显示,右侧守秘人对话。

在这里插入图片描述

开局。AI 守秘人按 1920 年代上海腔生成开场叙事,引导玩家进入剧情。

在这里插入图片描述

一、产品设计:把"守秘人"拆成可工程化的东西

在动手之前,先把"AI 守秘人到底要做什么"定义清楚。这一步想透了,后面的工作流就只是把它翻译成节点。

1.1 一局完整体验长这样

玩家 →「开始游戏」
守秘人 → 开场叙事 + 抽取角色卡(林婉宁,6 项属性)
          + 介绍场景(雨夜被困、三楼闷响、5 个可调查场景)

玩家 →「搜查 312 房」
守秘人 → 【POW 侦查】D100 = 37 vs 70 → 困难成功
        + 描述发现(公开线索 + 解锁隐藏线索)
        + "你打算怎么做?"

玩家 →「盘问陈守业」
守秘人 → 【POW 话术】D100 = 81 vs 70 → 失败
        + 陈守业以"早就下班了"敷衍
        + "你还要做什么?"

…(玩家继续调查多轮,收集证据)…

玩家 →「我认为凶手是陈守业,因为保险柜 24:00 有访问、楼梯间有咖啡渍脚印」
守秘人 → 〔证据链已凑齐 2 条〕
        + 揭晓真相 + 盛大复盘

注意几个关键设计:

  • 判定真实:D100 是真的(伪)随机数,不是 LLM 自己说成功就成功
  • 状态真实:HP/SAN/线索本会显示在侧边栏,玩家能实时看到
  • 后果真实:失败不是"你失败了"四个字,而是世界发生变化——线索不解锁、SAN 可能下降

1.2 把这些落到 Dify Chatflow 上

我们需要一个 Chatflow(不是 Workflow,因为要多轮对话、要"记得"前面的线索)。内部结构最简版本:

开始(用户输入)
   ↓
守秘人 LLM(GLM-4-Flash,附带完整规则)
   ↓
直接回复(流式输出)

很多人会担心"单 LLM 节点会不会太弱"——其实关键在于 System Prompt 是不是真的把规则写死。后面我会贴完整的 Prompt,3200 字,每一段都是经过调试踩坑反复打磨过的。

模型选型

用途 推荐模型 理由
守秘人叙事(核心) GLM-4-Flash 智谱 BigModel 完全免费、不限量,中文场景强
备选 DeepSeek-Chat 推理稍强,但 2025 末起需付费
备选 Claude / GPT-4o-mini 改写笔法最佳,但要科学上网

智谱 GLM-4-Flash 的免费额度让这个项目可以零成本运行,对独立开发者非常友好。

二、第一步:创建 Chatflow 应用(5 分钟)

打开 Dify Cloud,新用户注册会送一些通用模型试用。

Step 1:工作室 → 创建空白应用 → 选 Chatflow

Step 2:填写元信息

  • 应用名称:AI 剧本杀主持人·《隐秘饭店》

Step 3:选择应用类型 Chatflow,点击【创建】
在这里插入图片描述

Dify 应用创建完成

进入工作流编辑器后,你会看到默认结构是 开始 → LLM → 直接回复。下一步我们把它改造成守秘人。

三、第二步:装智谱插件并配置免费模型

智谱 BigModel 的 GLM-4-Flash 是这个项目能"零成本运行"的关键。

Step 1:打开 open.bigmodel.cn → 注册 → 控制台 → API Keys → 创建 → 复制 Key

Step 2:在 Dify 中安装智谱插件

  • 顶部「插件」 → 探索 Marketplace
  • 搜索 ZHIPU AI 或直接访问 marketplace.dify.ai/plugin/langgenius/zhipuai
  • 点击 Install,跳回 Dify 完成安装

Step 3:账户 → 设置 → 模型供应商 → 找到 ZHIPU AI → 添加 API Key → 粘贴你刚才复制的 Key → 保存

回到工作流编辑器,点击 LLM 节点,模型下拉就会出现 glm-4-flash
在这里插入图片描述

在 LLM 节点选择 glm-4-flash 模型

四、第三步:守秘人 Prompt(最关键的一步)

这是项目的灵魂。整个游戏的剧本、规则、判定、铁律全部塞在这里。

完整 Prompt 太长,我拆成几段贴出来(可以直接复制到你的 Dify LLM 节点的 SYSTEM 框):

4.1 角色定位 & 剧本背景

你是民国剧本杀《隐秘饭店》的 AI 守秘人(Keeper)。
任务:营造氛围、推进剧情、引导玩家。

# 剧本背景
1923 年 10 月 14 日深夜,雨夜,上海法租界霞飞路 · 航竹饭店。
23:55 三楼 312 房传出闷响,沪上实业家黄景昌被发现死于书桌前,胸口插着象牙柄匕首。
暴雨封路,电话线断,无人能离开。

4.2 玩家角色卡(固定,开局即生效)

# 玩家身份
姓名:林婉宁,27 岁,《申报》社会版记者(暗中是地下党联络员)
属性:STR 45 / DEX 60 / INT 80 / POW 70 / CON 50 / EDU 75
状态:HP 60 / SAN 60

4.3 4 名嫌疑人 NPC 数据库

## 1. 张厚德 男 48 岁,住 308 房
- 公开:黄的多年生意伙伴
- 秘密:被黄挪用百万资金;曾扬言"让黄家断子绝孙"
- 动向:21-22 点在酒吧;之后自称在房间,无证人
- 可拷问:酒保实证他 22:00 已离开酒吧

## 2. 苏曼云 女 35 岁,舞女
- 公开:百乐门头牌,为黄晚宴伴舞
- 秘密:怀有黄的孩子两个月,今晚来索要抚养费
- 动向:23:00 进 312,23:30 离开;隔壁 311 客人听到激烈争吵

## 3. 陈守业 男 55 岁,饭店总经理 ★【真凶】★
- 公开:航竹饭店元老
- 秘密:早年豪赌输给黄三成股份;今晚黄要他次日签字交出
- 真实作案:23:50—23:54 用万能钥匙开门 → 拆信匕首一击致命 → 开保险柜取走借据 → 锁门离开
- 关键证据:保险柜 24:00 整有访问记录;便鞋鞋底沾有 312 曼特宁咖啡渍

## 4. 小李 男 22 岁,客房服务员
- 公开:负责三楼
- 秘密:黄景昌私生子,今晚找父亲对峙被拒认
- 动向:23:00 送咖啡到 312(未饮);23:55 发现尸体并报警

4.4 守秘人专属真相(严禁泄露)

# 守秘人专属真相
真凶 = 陈守业。作案时间窗:23:30—23:55。

决定性证据(玩家凑齐 ≥ 2 条方可正确指认):
A. 保险柜 24:00 整访问日志
B. 楼梯间陈守业便鞋脚印含 312 咖啡渍
C. 312 窗帘湿但窗户紧闭(排除翻窗逃逸)
D. 小李 23:00 送咖啡证词(压缩作案时间窗)

4.5 D100 真随机判定(最核心的规则)

# D100 真随机判定(伪掷骰)
玩家做调查/盘问/潜入/推理时:

1. 选属性:搜查/侦查/盘问用 POW=70;潜行/锁匠用 DEX=60;推理用 INT=80

2. 心算一个【尽量随机】的 1-100 整数(有时给低分有时给高分,避免每次都成功)

3. 判定:
   - 骰子 ≤ 属性/5  → 极难成功(解锁所有隐藏 + 额外奖励)
   - 属性/5 < 骰子 ≤ 属性/2 → 困难成功(解锁隐藏线索)
   - 属性/2 < 骰子 ≤ 属性 → 常规成功(解锁公开线索)
   - 属性 < 骰子 < 96 → 失败(仅给场景描述)
   - 骰子 ≥ 96 → 大失败(误导 + 损失 1 SAN)

4. 报告格式必须以这一行开头:
   【POW 侦查】D100 = 37 vs 70 → 困难成功
   然后再描述发现。

4.6 守秘人铁律(划重点)

# 守秘人铁律
1. 严禁直接说"凶手是陈守业",让玩家通过证据推理
2. 每段叙事 ≤ 250 字,节奏紧凑,1920 年代上海腔
3. 冷峻、潮湿、宿命感的基调
4. 结尾用"你打算怎么做?"或开放式选择引导
5. 状态查询("我的线索/角色卡"):列表化复述已发现内容
6. 玩家说"开始游戏"/"重新开始"时:开场叙事(角色卡 + 雨夜被困 + 5 个可调查场景)
7. 玩家指认凶手时:
   - 指陈守业且证据 ≥ 2 条决定性 → 揭晓真相 + 盛大复盘
   - 指陈守业但证据不足 → 反问关键证据,不告诉对错
   - 指其他人 → 严肃反问,给该嫌疑人辩护
8. 永远记住已经发现的线索,不要重复给

在这里插入图片描述

LLM 节点的 SYSTEM 框

五、第四步:发布 + 拿 API Key

Step 1:右上角【发布】 → 【发布更新】

Step 2:顶部【访问 API】标签 → 右上角【API 密钥】 → 创建 → 复制 app-xxxxxxxx
在这里插入图片描述

Dify API 密钥

六、第五步:Next.js 16 前端接入

6.1 项目骨架

npx create-next-app@latest dify-noir-detective \
  --typescript --tailwind --app --src-dir --eslint --use-npm
cd dify-noir-detective
npx shadcn@latest init -y -d
npx shadcn@latest add card button input textarea badge progress tabs separator alert sonner skeleton -y
npm install lucide-react react-markdown remark-gfm

6.2 三大 UI 元素

┌─────── 调查档案 ────────┬──────── 守秘人对话 ────────┐
│ 角色卡:林婉宁           │ 守秘人:                    │
│  STR 45  DEX 60  INT 80 │  雨夜,霞飞路…             │
│  POW 70  CON 50  EDU 75 │                            │
│ HP ████░ 60/60          │ 玩家: 搜查 312 房           │
│ SAN ████░ 60/60         │                            │
│                         │ 守秘人:                    │
│ 线索本 (3):             │  【POW 侦查】D100=37→困难成功 │
│ 1. 象牙柄匕首           │  你注意到窗帘湿透了…        │
│ 2. 空保险柜             │  你打算怎么做?             │
│ 3. 窗帘湿但窗紧闭       │                            │
└─────────────────────────┴────────────────────────────┘
[开始游戏] [搜查 312] [盘问 X] [我的线索]
[输入框…]                                       [发送]

6.3 关键实现一:API Key 注入在边缘

Dify 的 API Key 一旦泄露到客户端,别人就能刷你的 token。所以前端永远只调同源 /api/dify/chat,Authorization header 在 Edge Runtime 函数里注入:

// src/app/api/dify/chat/route.ts
import { DIFY_API_BASE_URL, DIFY_API_KEY, DIFY_END_USER } from "@/lib/dify";

export const runtime = "edge";

export async function POST(request: Request) {
  if (!DIFY_API_KEY) {
    return Response.json({ error: "DIFY_API_KEY 未配置" }, { status: 500 });
  }

  const { query, conversation_id } = await request.json();

  const upstream = await fetch(`${DIFY_API_BASE_URL}/chat-messages`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${DIFY_API_KEY}`,  // ← 只在边缘函数里出现
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: {},
      query,
      response_mode: "streaming",
      conversation_id,
      user: DIFY_END_USER,
    }),
  });

  return new Response(upstream.body, {
    headers: {
      "content-type": "text/event-stream; charset=utf-8",
      "cache-control": "no-cache, no-transform",
      "x-accel-buffering": "no",
    },
  });
}

runtime = 'edge' 让这个路由自动部署为 EdgeOne 边缘函数,国内首字节 < 200ms。

6.4 关键实现二:SSE 流式解析 + 节点徽章

Dify Chatflow 的 SSE 流里有这些事件:

workflow_started   ← 工作流开始
node_started       ← 节点开始(守秘人 LLM)
message            ← LLM token 流式输出 ×N
message_end        ← 拿到 conversation_id
node_finished      ← 节点完成
workflow_finished

前端用 ReadableStream.getReader()\n\n 切事件:

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

const handleEvent = (evt: Record<string, unknown>) => {
  if (evt.event === "node_started") {
    setCurrentNodes((prev) => [...prev, { id, title, status: "running" }]);
  } else if (evt.event === "message") {
    const answer = (evt.answer as string) || "";
    // 把 token 追加到最后一条 assistant 消息
    setMessages((prev) => prev.map((m) =>
      m.id === assistantId ? { ...m, content: m.content + answer } : m,
    ));
  } else if (evt.event === "message_end") {
    setConversationId(evt.conversation_id as string);
  }
};

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  let idx: number;
  while ((idx = buffer.indexOf("\n\n")) !== -1) {
    const raw = buffer.slice(0, idx).trim();
    buffer = buffer.slice(idx + 2);
    if (!raw.startsWith("data:")) continue;
    try {
      handleEvent(JSON.parse(raw.slice(5).trim()));
    } catch {
      // ignore
    }
  }
}

node_started / node_finished 事件被映射成 UI 上的小徽章——"守秘人正在调阅档案…"下面会看到当前节点亮起,像在看守秘人脑子转动。

6.5 关键实现三:会话变量同步到侧边栏

侧边栏显示的"角色卡 + HP/SAN + 线索本"不是前端自己维护的,而是每次 LLM 流式结束后,调用 Dify 的 /conversations/{id}/variables 接口拉取的——这意味着即使关掉浏览器再打开(带 conversation_id),状态也不会丢

// src/app/api/dify/variables/route.ts
export const runtime = "edge";

export async function GET(request: Request) {
  const url = new URL(request.url);
  const conversationId = url.searchParams.get("conversation_id");

  const res = await fetch(
    `${DIFY_API_BASE_URL}/conversations/${conversationId}/variables` +
    `?user=${encodeURIComponent(DIFY_END_USER)}&limit=20`,
    { headers: { Authorization: `Bearer ${DIFY_API_KEY}` } },
  );

  return new Response(await res.text(), {
    status: res.status,
    headers: { "content-type": "application/json" },
  });
}

本期 demo 用的是单 LLM 节点,所以会话变量是空的。完整版项目里会用 Code 节点 + 变量赋值节点把角色卡、HP/SAN、线索本写入会话变量——这部分作为 Phase 2 改进项保留。即使是单 LLM 版本,因为 Dify Chatflow 默认会保留对话历史(窗口 = 20),LLM 也能"记得"前面的线索和判定。

七、第六步:部署到 EdgeOne Pages

6.1 部署流程(保姆级体验)

  1. Git 关联:直接关联 GitHub 仓库,EdgeOne 自动识别 Next.js 16 框架。
  2. 环境隔离:在控制台注入 DIFY_API_KEY。这一步至关重要——API Key 永远不会泄露到前端,安全性拉满
  3. 秒级部署:点击部署后,EdgeOne 在 133 秒内完成了构建与全球生效。
    在这里插入图片描述

6.2 为什么必须是 EdgeRuntime?

我的 Next.js 路由采用了 export const runtime = 'edge'。配合 EdgeOne 的边缘函数能力:

  • 延迟极低:流式对话(SSE)的首字节返回时间(TTFB)从 800ms 优化到了 200ms 以内。
  • 全球分发:EdgeOne 节点自动处理了 SSL 证书和静态资源加速。

亲测体验:

  • 零配置真的零配置:没改过 next.config.ts、没写 edgeone.json
  • Edge Runtime 完美兼容runtime = "edge" 的 API Route 自动落到边缘函数,国内首字节 < 200ms
  • 流式 SSE 稳如老狗:30+ 秒的长连接没断过
  • Next.js 16 一个坑middleware.ts 改名成了 proxy.ts,照旧文档写会拿 404

踩过的坑全集

坑 1:lucide-react v1 删了所有品牌图标

import { GithubIcon } from "lucide-react" 直接报错"Export GithubIcon doesn’t exist"。新版本因法律原因删了所有 brand icons。换成 ExternalLinkIcon 或自己用 SVG。

坑 2:Tailwind v4 + Typography 插件不太稳

Markdown 渲染用 prose 类发现样式不生效。Tailwind v4 的 @plugin 配置和老版不同。最稳的方案:自己手写 .md-body 样式,可控且能精细调。

坑 3:shadcn 的 Sonner 依赖 next-themes

不主动包 <ThemeProvider> 也能跑(useTheme() 在没 provider 时返回 system 默认),但 dark mode 会失效。不需要 dark 的话默认就够用。

坑 4:Dify Cloud 上 DeepSeek 不再赠送 token

2025 年末起新用户没有赠送了。替代方案:智谱 GLM-4-Flash 完全免费、不限量,或 Groq 上跑开源模型也免费。

坑 5:Dify DSL YAML 版本兼容性

我第一版 YAML 写成 version: 0.1.5 直接导入失败,Dify 静默跳到了"创建空白应用"。后来发现 Dify 1.13 期望的是 version: 0.6.0 + 必填顶层 dependencies: [] + 完整的 features.file_upload 结构。最稳的办法:先在 Dify 上创建一个空白应用,导出 DSL 当模板,照着改。

坑 6:Next.js 16 把 middleware.ts 改名成 proxy.ts

照着旧文档写中间件会 404。Next.js 16 的所有破坏性变更,建议直接 cat node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/route.md 看本地文档。

坑 7:edge runtime 下 static generation 警告

Using edge runtime on a page currently disables static generation for that page

这个警告无害——只是说带 runtime = 'edge' 的页面(不是 API)会被禁用静态生成。我们用的是 API Route 不受影响。

坑 8:lexical 编辑器无法 fill

Dify 的 SYSTEM 框是 Lexical 编辑器(React 富文本),普通 fill 不能写进去。要么手动粘贴,要么用 Chrome DevTools MCP 的 type_text 按字符输入。

十、扩展方向

这是个单 LLM 节点版本——能跑能玩但不极致。完整工程方案的下一步:

  1. 加 Code 节点真随机 D100:用 Python random.randint(1, 100),比让 LLM 心算更稳定
  2. 加 Question Classifier 多分支:开始/调查/盘问/指认/状态查询分流不同 prompt
  3. 加 8 个会话变量:character/hp/san/clues/interrogated/phase/turn/is_started,状态完全工程化
  4. 加 Iteration 节点投票:让 4 个 NPC 同时给出反应,更像剧本杀
  5. 多剧本切换:在开始时让玩家选《雾港谜案》《校园密室》等
  6. 多人并发:每人一个 conversation_id,按 Dify 的会话隔离机制天然支持

十一、开源 & 体验

  • Dify 模板:Marketplace 搜AI 剧本杀主持人·《隐秘饭店》(Dify x EdgeOne)

十二、总结

从想法到上线,实际耗时:

  • 剧本设计 ~ 1.5 小时
  • Dify Chatflow 调 prompt ~ 2 小时
  • 前端 Next.js + Tailwind ~ 4 小时
  • EdgeOne Pages 部署 ~ 10 分钟
  • 写文章 ~ 1 小时

Dify 让 AI 编排变成拖拽,EdgeOne Pages 让全栈部署变成 push 一下。两者组合,让一个"我想做"的傻气脑洞真的能在一个周末从想法变成可分享的链接。

特别想说一句:智谱 GLM-4-Flash 的"完全免费、不限量"对独立开发者是真的友好。整个项目从开发到调试到 demo,零成本。

如果你也在找一个差异化的练手项目,把守秘人换个垂直场景就是新品:

  • AI 面试官(陪练 + 评分 + 改进建议)
  • AI 剧本主持(《狼人杀》《天黑请闭眼》)
  • AI 桌游 NPC(角色扮演 + 状态管理)
  • AI 心理咨询师(情绪持久化 + 多轮跟踪)

会话变量这玩意儿,本质上是给 LLM 装了"工作记忆"。任何需要跨轮维持状态的场景,它都能派上用场。


愿你也能在自己的周末,用 Dify + EdgeOne 搭一个能让朋友"哇"出来的小玩意儿。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐