把你身边的真实街区,一键生成一场可步行的实景推理案件。 真实 POI = 线索点 / 案发现场 / 嫌疑人据点;高德规划探案动线;地图即剧本。
一个为高德「智图创想」Skill 创作大赛打造的 LBS × AI 实景剧本杀引擎。给定一个街区中心,引擎从高德拉取真实地点作为舞台,生成一桩内部自洽的推理案件(受害者 / 嫌疑人 / 动机 / 不在场证明 / 环环相扣的线索链 / 真相),用真实步行路线把线索点串成一条探案动线,最终产出一张可交互的暗色「卷宗地图」——线索默认锁定,走到真实地点才解锁。
真机运行(上海「武康路·安福路」民国谍战案)截取的四个关键状态。暗色民国卷宗美学:案发现场红色信标 · 金色编号线索点 · 步行探案动线 · 自定义信息窗 · 右侧案件档案。
| ① 初始 · 线索全锁 🔒 | ② 解锁中 · 图文联动 |
|---|---|
![]() |
![]() |
| 进场即上锁,线索默认不可见——走到真实地点才解锁 | 点线索卡 / 地图点 → 地图飞行定位 + 探案动线逐段点亮 + 打字机揭示线索 |
| ③ 指认真凶 | ④ 结案 · 推理判定 |
|---|---|
![]() |
![]() |
| 集齐线索后点选嫌疑人指认(红环 ⊙ 已指认),结案按钮亮起 | 指认正确 → 「推理成功」判定 + 真相还原,可重新探案 |
选街区 + 题材
│
▼
高德周边搜真实 POI ──► 分类为 接头点/密信点/落脚点/案发现场
│
▼
案件内核生成案情 ──► 受害者 · 3–4 名嫌疑人(各带动机+不在场证明) · 4 段线索链 · 真相
│
▼
高德步行路线串联 ──► 一条真实可走的探案动线
│
▼
卷宗地图(交互):线索默认 🔒 → 走到地点(点标记/线索卡)→ 🔓 解锁线索
│
▼
集齐线索 → 「提交推理 · 结案」→ 真相大白
线索链按 接头点 →(动机)密信点 →(破不在场证明)落脚点 →(锁手法)案发现场 逐环收口,最终指认唯一真凶。
进场是一段「云开见日」电影级开场:拨云 → 地图从高空俯冲缩放到案发街区 → 侦探立绘滑入并朗读案情简报,随后才落入可玩的卷宗地图。
- 语音旁白:开场 4 句案情简报自动朗读,两层方案、可靠优先——
- 云端情感 TTS(选填):配了
AZURE_SPEECH_KEY时,构建期(npm start)用 Azure SSMLmstts:express-as情感标签把简报合成带怒吐 / 叹息 / 紧迫语气的 mp3 写入output/audio/,前端优先播放。 - 浏览器朗读(兜底):未配 Azure / 音频缺失 / 播放失败 → 自动降级到浏览器 Web Speech 本地音,保证有声。
- 云端情感 TTS(选填):配了
- 右下角可静音;旁白进行中可跳过。
- 性能加固:地图相机移动 / 结案弹窗期间临时关闭玻璃面板的
backdrop-filter,规避 WebGL 地图每帧重算毛玻璃导致的掉帧。
| 阶段 | 做了什么 | 关键实现 |
|---|---|---|
| 1. 中心定位 | 把街区名/坐标解析为中心点 | amap.resolveOrigin()(或直接吃 center.coord) |
| 2. 勘景 | 按角色检索词在中心半径内拉真实 POI 并分类、跨类去重 | amap.placeAround() × 6(4 角色 + 2 兜底池) |
| 3. 案件生成 | 纯逻辑、可种子复现,产出案情/人物/线索链/真相 | lib/mystery.mjs · generateCase() |
| 4. 探案动线 | 按线索顺序求真实步行路线(含 polyline) | amap.routeWithPolyline(..., "walk") × 3 |
| 5. 渲染落盘 | 注入卷宗模板,产出 case.json + 交互 index.html |
lib/render.mjs + assets/mystery.html |
单次跑通约 10 次高德 API 调用(1 定位 + 6 周边搜 + 3 步行动线)。客户端内置节流(≈3 QPS)与指数退避重试,迁就个人 Key 限流。
角色 → 真实 POI 检索词映射(scripts/mystery.mjs):
| 剧情角色 | 检索词 | 线索作用 |
|---|---|---|
| 接头点 ☕ | 咖啡馆 | 受害者死前最后约见 |
| 密信点 ✉ | 书店 | 揭示动机 + 凶手姓氏 |
| 落脚点 🛎 | 酒店 | 打破凶手不在场证明 |
| 案发现场 ⚑ | 公园 | 锁定手法与真凶 |
| 兜底池 | 餐厅、茶馆 | 地点不足时补位 |
amap-citymystery-skill/
├── 启动预览.cmd # Windows 一键启动器:自动起服务器 + 开浏览器(调用 scripts/preview.ps1)
├── scripts/
│ ├── mystery.mjs # 编排器:定位→勘景→生成→动线→落盘(I/O 层)
│ ├── serve.mjs # 零依赖本地预览服务器
│ ├── preview.ps1 # 一键预览逻辑(PowerShell,由 启动预览.cmd 调用)
│ └── lib/
│ ├── mystery.mjs # 案件生成内核(纯逻辑·可种子复现·题材包 THEMES)
│ ├── amap.mjs # 高德 Web 服务客户端(节流/重试/不回显 Key)
│ ├── render.mjs # 把案件数据 + JS Key 注入卷宗模板
│ ├── tts.mjs # 云端情感 TTS(Azure SSML 合成开场旁白,仅服务端构建期)
│ ├── config.mjs # Key 解析与状态(只暴露 ✅/❌,绝不打印明文)
│ └── geo.mjs # 纯几何工具(haversine 等)
├── assets/
│ ├── mystery.html # 卷宗地图模板(暗色美学 + 线索解锁交互 + 电影开场)
│ └── narrator.png # 侦探旁白立绘
├── input/
│ ├── sample-mystery.json # 样例输入(武康路·安福路民国谍战案)
│ └── sample-modern.json # 样例输入(陆家嘴现代追凶案)
├── output/ # 产物(gitignored):case.json + index.html + audio/
├── artifacts/ # 截图等临时产物(gitignored)
├── docs/screenshots/ # README 用四态截图(入库)
├── .env.example # 环境变量模板(复制为 .env 填真值)
└── package.json
cp .env.example .env编辑 .env,填入你的高德 Key(申请:https://console.amap.com/dev/key/app):
AMAP_WEB_SERVICE_KEY(必填)— 服务平台选「Web 服务」。用于服务端选点与路线。AMAP_JS_API_KEY(选填)— 服务平台选「Web 端(JS API)」。交互地图渲染需要。AMAP_JS_SECURITY_CODE(选填)— 与 JS API Key 配套的安全密钥。
云端情感 TTS(选填,让开场旁白真有怒吐/叹息语气)——申请:https://portal.azure.com 创建「语音服务(Speech)」资源:
AZURE_SPEECH_KEY— Azure 语音服务密钥。服务端密钥,仅构建期使用,绝不写进产物/日志。AZURE_SPEECH_REGION— 资源区域,如eastasia(不填默认eastasia)。AZURE_SPEECH_VOICE— 朗读音色,不填默认zh-CN-YunxiNeural(多情感男声侦探腔)。
不配 Azure 也能跑:开场旁白自动降级为浏览器本地朗读(见「沉浸式开场 · 语音旁白」)。
Key 也可放在
~/.openclaw/openclaw.json的skills.entries["amap-citymystery"].env下(OpenClaw Skill 形态)。
npm start # 或 npm run mystery(同一命令的别名)
# 等价于:node scripts/mystery.mjs --input input/sample-mystery.json --output-dir output控制台会打印 Key 状态、勘景数量、案情摘要与真相剧透;产物写入 output/case.json 与 output/index.html。
自定义街区:复制 input/sample-mystery.json 改参数,再 node scripts/mystery.mjs --input <你的输入>.json --output-dir output。
想跑现代追凶题材:
npm run mystery:modern(用input/sample-modern.json,上海陆家嘴实时路况案)。两个题材共用同一套生成/预览流程。
Windows 一键(推荐):双击项目根目录的 启动预览.cmd —— 自动起本地服务器并打开浏览器,省去手敲命令。
或手动(全平台):
npm run serve
# 打开 http://localhost:8791/
⚠️ 别直接双击 /file://打开index.html——高德会因域名非法触发INVALID_USER_DOMAIN(这正是「双击网页黑屏/一堆报错」的根源)。请用上面任一方式经localhost访问,并在高德 JS Key 的域名白名单里加localhost。未配置 JS Key 时页面自动降级为「卷宗未就绪」占位,右侧案件档案仍可查阅。
{
"theme": { "key", "label", "kicker" },
"area": "武康路·安福路",
"title": "此城有案 · 武康路·安福路疑案",
"timeText":"案发于一个雷雨交加的午夜",
"victim": { "name", "role" },
"suspects":[ { "id", "name", "role", "motive", "alibi" } ],
"scenes": [ { "order", "kind", "role", "icon", "name", "type", "address",
"lng", "lat", "rating", "isCrimeScene?",
"clue": { "title", "text", "unlocks" } } ],
"solution":{ "culpritId", "culpritName", "culpritRole", "method", "motive", "evidence", "reveal" },
"center": { "label", "lng", "lat" },
"route": { "legs":[ { "fromOrder","toOrder","polyline","durationMinutes","distanceMeters" } ],
"totalMinutes", "totalMeters" },
"difficulty": 3,
"seed": 1937,
"spanMeters": 0,
"assumptions": [ /* 虚构声明 / Skill 接管说明 / 评分透传声明 */ ],
"meta": { "apiCalls", "generatedAt", "poolSize" }
}现代追凶(
theme:"modern")的case.json还会多出:顶层window(案发窗口分钟)与theme.mode:"drive";每个suspect增加place{name,lng,lat}、driveMinutes、distanceMeters、traffic{label,congestedRatio}、cleared、drivePolyline。
theme: "modern" 把「实时路况下驾车赶不赶得到」做成真实的不在场证据 —— 这是本 Skill 与地图耦合最深的玩法:
- 每名嫌疑人落在一个真实据点(中心四周环形撒点搜到的小区/公寓);系统用高德
/v5/direction/driving(show_fields=cost,polyline,tmcs)算出实时路况感知的驾车耗时 + 逐段拥堵到案发现场。 - 线索锁定案发窗口 W 分钟;驾车耗时 > W = 洗清(赶不到),≤ W = 无法排除。
- 真凶在「赶得到」的人里,再由现场物证 / 动机锁定。距离更近 ≠ 更快——一路畅通的远点可能反超堵在路上的近点,这正是实时路况带来的反直觉破绽。
高德「交通态势」矩形 API 已于 2020 年底下线、新 Key 不可用;故改用驾车接口自带的实时耗时 +
tmcs逐段路况作为路况源(可用且更贴合具体路线)。
真机样例(npm run mystery:modern,上海陆家嘴,seed 2026,案发窗口 14 分钟):
| 嫌疑人 | 角色 | 据点 | 实时驾车 | 路况 | 裁定 |
|---|---|---|---|---|---|
| 赵薇 | 贴身助理 | 斜桥公寓 | 12 min | 全程畅通 | |
| 高岚 | 公司法务总监 | 永年路24弄 | 14 min | 全程畅通 | |
| 孙骁 | 私人健身教练 | 杨高南路1677弄 | 16 min | 全程畅通 | ✅ 已洗清 |
| 王珂 | 前妻 | 河间路50弄 | 19 min | 全程畅通 | ✅ 已洗清 |
16/19 分钟的两人被实时路况「洗清」(赶不到);剩下 12/14 分钟两人,再由现场物证与动机锁定真凶。地图上会画出每个嫌疑人据点 → 现场的实时驾车路线(有作案时间=红、已洗清=灰),点嫌疑人即高亮其路线。
运行:npm run mystery:modern → npm run serve → 打开 http://localhost:8791/ 。
题材即世界观,集中在 lib/mystery.mjs 的 THEMES。现内置 noir1930 民国谍战(步行探案)与 modern 现代追凶(实时路况驾车不在场),各含姓名池、身份/动机/手法/物证模板、场景类型与图标。
扩展新题材:照 noir1930 的结构新增一个 key(如 wuxia 武侠、scifi 近未来),无需改编排器或模板:
export const THEMES = {
noir1930: { /* ... */ },
wuxia: {
key: "wuxia", label: "江湖疑案", kicker: "某年 · 某镇",
surnames: [...], given: [...],
victimRoles: [...], suspectRoles: [...],
motives: [...], redHerrings: [...],
methods: ["在『{scene}』...被一招..."], evidences: [...],
sceneKinds: { meet:{label,icon}, archive:{...}, lodging:{...}, scene:{...} },
},
};本仓库是「技能提供能力、宿主大模型提供创意」的范例:
- 技能负责确定性、可复现的硬骨头:高德选点 + 真实动线 + 卷宗渲染 + 结构化提示词契约。
- 「现写案情」这件最需要创造力的事,由宿主大模型按
case.json的结构接管,可生成远比内置模板丰富的剧本。 - 内置
generateCase()是一个可离线复现的程序化底座:没有大模型也能跑出一桩自洽的 demo 案件(同seed必复现)。
- 🔑 Key 不入库不入产物:
.env已 gitignore;Web 服务 Key 仅服务端使用,绝不写进 HTML/日志/异常;仅前端 JS API Key 按高德设计注入页面。 - 🎭 纯虚构声明:案情、人物、线索均为程序化生成的虚构剧情;真实地点仅作剧本舞台,与现实场所无任何影射(已写入
case.json.assumptions)。 - 📊 数据不编造:地点评分等字段直接透传高德,缺失则标注,不臆造。
- 赛事:高德「智图创想」Skill 创作大赛
- 评分:创新性 + 场景可行性,完整 demo 加分
- 定位:剧本杀 × CityWalk × LBS × AI 的实景推理体验,区别于「工具型」地图应用
ISC




{ "area": "武康路·安福路", // 街区名(用于标题) "center": { "name": "上海武康大楼", "city": "上海" }, // 或 { "name":"...", "coord":[lng,lat] } "radius": 1100, // 周边搜索半径(米),默认 1200 "theme": "noir1930", // 题材 key(见「题材系统」) "suspects": 3, // 嫌疑人数 3–4 "seed": 1937, // 可选;同 seed 复现同一案件 "language": "zh-CN" // 预留 }