大肥鱼环游世界 · coopani-world-journey
让她真的出门走一趟,而不是演一段旅行给你看。
她从你设定的家出发,自己挑地方、自己挑交通方式,路上耗掉的是真实的时间; 到了之后按当地时间过日子——吃饭、看日出、回酒店、写日记。日记放在你指定的同步盘里, 换台电脑她还记得自己去过哪。
现在能做到什么
| 能做 | 说明 |
|---|---|
| 124 座城,每城都有真地标 | 中国全境 + 东亚、东南亚、欧洲、北美、拉美、大洋洲。每城六到九个具体去处,凑不出真名字的城市宁可不写 |
| 自己选目的地 | 按距离分近/中/远三档掷,没去过的优先,不会连着三次都去山城;够不着的地方不进候选 |
| 九种交通方式 | 步行、自行车、电瓶车、摩托车、自驾、长途汽车、火车、轮船、飞机 |
| 用时是真算的 | 同一段距离,步行与飞机差两个数量级;短途飞机只快一小时,因为两头各要三小时 |
| 不会出现「步行去巴黎」 | 每种方式有距离上限,船必须两端都有客运码头 |
| 到站按当地时间 | 时区靠 Intl 算,夏令时也对;日出日落用 NOAA 公式本地算 |
| 一天有作息 | 三个饭点固定;晚八点后没夜间活动就回酒店;有日出条件才排凌晨起床 |
| 酒店掷骰 | 六成普通连锁、四成景观房、不到百分之一是那种住一晚就后悔的小店 |
| 日记跟着人走 | 一个月一个 Markdown,可放同步盘;冲突副本按天合并,旧的一天一篇会自动并进来 |
| 半年上限 | 一次最多连续在外几个月(可调),到了自动回家,回家休息几天(可调) |
| 有些地方在作品里出现过 | 排到那儿的时候她会知道这是哪部作品的哪个场景 —— 但不知道剧情,也不会补清单外的地名。首批 10 处,札幌和里约故意空着 |
| 能跟作者说两句 | 面板底部「说两句」:报 bug、提想加的地点。默认就指向作者,装了直接能报 |
为什么不用高德
能用,但只当尺子,不当剧本。
- 距离、时区、日出日落、有没有码头 —— 全部本地算。没网也能走。
- 高德只在两处值得用:每天的天气、以及「这个地点是否真的存在」的抽查。
- 它给不了火车班次和节日,也写不出「当地是什么味道」。后者交给她自己写。
半年连续旅行天天调接口,个人开发者配额几天就烧完。配额一没,表现不是报错, 是她开始乱走 —— 所以底座必须内置。
天气:两个源,留空也有
| 情况 | 走谁 |
|---|---|
| 国内 + 填了高德密钥 | 高德(分昼夜、中文天气名准,但个人 key 有 QPS 限制) |
| 国外(任何情况) | Open-Meteo(高德不覆盖) |
| 国内 + 没填密钥 / key 失效 | Open-Meteo |
不填密钥不等于没有天气。 早先设置页写着「留空天气标为未知」,那是文案没跟上实现 —— 0.2.6 起两处统一:留空自动走 Open-Meteo,免密钥、按经纬度直查、覆盖全球, 只是日预报只有一个天气码,夜里沿用白天的值。风、温度、下不下雨这些够用。
key 失效也只是降级,不是故障:下一次查询就落到 Open-Meteo,旅行一步都不耽误。
装
- 需要 Coopani 0.2.17 以上。
- 在
data/extensions/package.json的dependencies里加一行:"coopani-world-journey": "link:<这个包的绝对路径>" pnpm installconfig.json的worlds.journey.enabled设true,或者从 World 页面打开- 重启 Core(改
.ts之后必须,不能用/api/worlds/restart)
打开这个世界:两个页签
她此刻 —— 她所在的城市、她那边的时间、身体感觉、今晚住哪、日出几点。 在路上时多一条进度:走了百分之几、还剩多久、当地几点到站。 下面是一条时间轴:刚过去的那个点(灰的)加接下来四件事,每一条都有时刻。
这是这个面板最值钱的地方。别处只能告诉你「配置好了没有」,这里告诉你她下一件事什么时候来。
照片 —— 她路上拍的。一天一张,全是空镜:窗外、街角、傍晚的城墙。画面里没有她。
她在家的时候:一个开关,一个按钮
她在家时,面板上多一块:
- 开关「出门等我点头」 —— 打开后她不会自己走,就在家等着你按。
- 按钮「让她出发」 —— 按一下她就动身,然后按钮锁住,直到她回家。
开关默认关。关着的时候她是自己过日子的:歇够了就走,不问。这是这个扩展的主张 —— 打开就能看见它自己在动。想自己掌控节奏的人把开关打开即可,两种人都照顾到了。
按钮锁着的时候会说清为什么:还在歇着(显示歇到哪天)、或者这会儿没找到能去的地方。
手按的那一下会跳过两个自动门禁:加载宽限、以及 7–21 点的出门窗口。 你明确说「现在走吧」,再回你一句「现在不是出门的点」就是抬杠。
第一次打开会有三句话
不是配置检查清单 —— 这个扩展不配置也能跑。引导讲的是三件最容易被误解的事:
- 上面那个钟是她那边的时间,跟你几点没关系
- 下一站她自己挑,你不用安排,也不用催
- 你能做的三件事:问她在哪、让她去某个地方、翻她的日记
点「知道了」收起。之后想再看,页脚有「再看一次引导」。
四条指示灯
World 列表上,这个扩展名字右边有四个小圆点 —— 那是它自报的链路状态,鼠标悬停看详情。
| 灯 | 绿 | 红 | 灰 |
|---|---|---|---|
| 天气 | 上次查到了(高德 / Open-Meteo) | 上次没查到 | 还没查过 |
| 日记 | 目录可写 | 写不进去(悬停看原因) | — |
| 照片 | 上一次拍成了 | 上一次没拍成(日志里搜「没拍成」) | 没配接口 / 还没拍过 |
| 听歌 | 在放《歌名》 | — | 没在放,或者没装那个扩展 |
「没启用」是灰的,不是红的 —— 没配生图接口不算故障,红点会让人以为坏了。 反过来,灯说的是「上一次发生了什么」,不是体检报告:没网的时候天气也会红, 那跟配置错了是两回事。
和「一起听歌」联动
装了 coopani-world-music 的话:
- 面板上多一行「在放 · 歌名 · 歌手」。这一行同时是联通性指示灯 —— 有歌,说明这边读到了那个扩展写出来的快照;一直是空,就是它换了路径、或者你没在放。
- 她带着你正在放的歌出门,出发和路上会提一句。
- 歌名里带地名就当场进「想去」(《成都》《回到拉萨》),不用等出发那一刻 —— 面板上会多一行「想去 · 成都」。下次挑目的地,那个地方权重大得多。
- 这边不抢话。 换歌时她不专门开口 —— 那是「一起听歌」自己的事(它换歌必说一句)。 这边的活儿是让她心里多一个想去的地方,等合适的时候自己提。
这是读文件实现的,不是接口。 宿主没有让两个 World 互查状态的通道,
所以这边只读 coopani-nowplaying.json,不碰那个扩展的进程。
代价是路径约定必须对齐 —— 2026-10-31 它把快照搬到了 <core>/data/tmp/,
这边还在读系统临时目录,联动静默失效了七个小时,用户是靠「好像没了(」发现的。
现在会从 World 目录逐层向上找,几个候选里取最新的那份。
设置项
设置页按「用户来这儿想干什么」分三组,不是按技术分类 —— 最常改的在最上面,一辈子不动一次的沉到底。
出发与节奏
| 项 | 默认 | 说明 |
|---|---|---|
| 从哪儿出发 | 空 | 填城市名(兰州 / 西安)。改它等于搬家:她在家就跟着搬过去,在外面就把回家的目标换成新家 |
| 时间倍速 | 1 | 1 = 真实速度。4 = 一天压缩成六小时。24 = 一天一小时 |
| 路上消息间隔 | 2 | 路上隔多久冒一句(服务区、过站、餐车)。0 = 一路安静,只在出发和到站开口。一趟最多三条 |
| 出门等你点头 | 关 | 打开后她不会自己走,只在面板上按「让她出发」。关着就自己按节奏走 |
| 回家后歇几天 | 3 | 0 = 当天就能再走 |
| 一次最多在外多久 | 6 | 到了强制回家 |
她的日子
| 项 | 默认 | 说明 |
|---|---|---|
| 让你跟着一块走 | 关 | 打开后她说「我们」,日记里会提到你。只是叙事,她并不知道你此刻在做什么 |
| 记花销 | 关 | 打开才累计交通、房费、三餐 |
| 预算 | 0 | 0 = 不限。见底时她会自己降级 |
| 日记保存位置 | 空 | 留空跟本机。填同步盘则多设备共用 |
可选接口
| 项 | 默认 | 说明 |
|---|---|---|
| 高德密钥 | 空 | 填了用高德(分昼夜、中文天气名准),还能查到真实的酒店名。留空自动用 Open-Meteo,一样有天气,只是酒店只说类别 |
| 生图接口 / 密钥 / 模型 | 空 | 留空就把景色写成文字。填好就一天一张空镜(同城同日不重复) |
反馈
| 项 | 默认 | 说明 |
|---|---|---|
| 反馈邮箱 | 作者的地址 | 「她此刻」底部那一栏「说两句」发给谁:两个按钮,一个报 bug、一个提想加的地点,点开是封写好的信,版本号和她当时在哪儿已经填进去了。默认就是作者,装了不用翻设置就能报;想换个人收就改这里,清空则整块不显示。它只在你点了之后才发,不会自己往外送东西 |
页面顶部还有两个徽章,不点任何东西就知道她现在在哪儿(「现在 在去天水的路上」)、去过几个地方、花了多少。
生图:她真的会拍一张
填好「生图接口」和「生图模型」,她出去逛的时候会顺手拍一张 —— 一天一张,同城同日不重复。
- 只拍风景,不拍自己。 没有角色参考图、没有双候选、没有一致性审核, 因为画面里根本没有她。代价是你看不到「她在照片里」,换来的是一致性问题当场消失。
- 图进宿主的媒体库(
data/media/,按内容哈希存,重复的只存一份), 事件里只带一个log:句柄,面板拿句柄换图。 - 失败静默。 接口挂了、超时、额度没了 —— 旅行一步都不停,只在日志里留一行。
- 留空就只写字不拍,相册是空的,别的照旧。
三个框各填什么
| 框 | 填什么 | 举例 |
|---|---|---|
| 生图服务地址 | 服务商的 API 根地址,填到 /v1 或 /api/v3 为止 | 硅基流动 https://api.siliconflow.cn/v1;火山方舟 https://ark.cn-beijing.volces.com/api/v3;OpenAI https://api.openai.com/v1 |
| 生图密钥 | 那串 API Key,服务商控制台里生成的 | 硅基流动 sk-...;方舟的 ARK_API_KEY;OpenAI 的 sk-... |
| 生图模型名 | 服务商文档里的图片模型 ID,各家叫法不同 | 硅基流动 Kwai-Kolors/Kolors(免费、中文提示词最顺)、Tongyi-MAI/Z-Image-Turbo;方舟 doubao-seedream-5-0-lite-260128;OpenAI gpt-image-1 |
想先试试又不想折腾? 硅基流动注册即可用,国内外都能直连,Kwai-Kolors/Kolors 那档是免费的,
画质够做旅行空镜。一天一张的用量,收费档也就几分钱。
别把 images/generations 填进地址。 那个路径程序自己会拼 —— 填进去会变成
.../images/generations/images/generations,然后 404。
填错了不会静默。 日志里会写明原因(HTTP 状态 + 服务端原话,比如
这张没拍成(黄昏的麦积山):服务返回 HTTP 401:Invalid API key)。
相册空着的时候,先去 core.log 里搜「没拍成」。
各家对尺寸和返回格式的写法不一样,程序会照错误文本换一次再试,最多试三次:
| 服务挑的东西 | 第二次怎么发 |
|---|---|
不认 size,只认 image_size(硅基流动、阿里百炼那一支) | 换成 image_size,尺寸值不变 |
不认 response_format(有的自建兼容层) | 去掉它,回头取 data[0].url 也能拿到图 |
尺寸值本身不收(OpenAI 要 1024x1024,方舟要 2K) | 什么尺寸都不带,用服务商的默认值 |
两种字段同时被挑的情况不追 —— 让它报错进日志,比连发五六次把限速打满强。
倍速到底影响什么
世界时刻 = 基准世界时刻 + (真实时刻 − 基准真实时刻) × 倍速
所有旅行判断都在世界时刻这条轴上:当地几点、到没到站、待了几天、日出日落。 倍速只决定这条轴跑多快,不改内容 —— 火车还是走那么久,还是那个城市,还是那个房型。
- 换挡时世界时间连续,不会因为 1x 切 24x 就跳过两周
- 巡检间隔跟着倍速自适应(1x→60 秒、24x→37.5 秒),保证一次巡检跨过的世界时间不超过 15 分钟
⚠ 高倍速下会跳点:巡检要等宿主把事件处理完,模型思考期间世界时间照走。 24x 通常只跳一两个作息节点;别长期挂 240x,那是压测用的极端值。 日常建议 1x 或 4x。
她怎么决定去哪
三个约束叠在一起。都不是写死的规则,是权重 —— 写死规则换一对城市就露馅。
一、方向:在外面逛不走回头路
她记着上一段往哪走(heading,一个方位角)。下一站顺着这个方向加权:
| 和上一段的夹角 | 权重 |
|---|---|
| ≤55°(顺着) | ×1.6 |
| 55~100°(侧向) | ×1.0 |
| 100~140°(侧后方) | ×0.5 |
| >140°(原路折返) | ×0.12 |
夹角超过 150° 直接从候选里剔掉 —— 但剔空了再放开,否则她到了漠河这种角落会出不来。
没有这一层的时候,选点是个纯随机游走:每一段单独看都「合理」,连起来是 九寨沟 → 南宁 → 银川 这种 Z 字。四个字:局部对,整体错。
二、节奏:远的走完,近的转几个
距离分四档:周边(≤300km)30%、近(300-800)28%、中(800-2500)32%、远(>2500)10%。
上一段超过 1400 公里,远档压到 2%、周边提到 55% —— 真实的长途旅行是「一段远的 + 在落脚点周围散开 + 再一段远的」,不是连着横穿。
为什么最细一档是 300 而不是 800:八百公里在中国已经是大跳了。 原来最细的档是 0-800,等于「到了新地方先在附近转转」这件事根本没有地方发生。
三、气候:她是从你家出发的鱼
城市表有经纬度,能算距离、时区、日出 —— 但算不出「她受不受得了」。 所以另有一层气候档案(八个档:干温 / 干冷 / 高原 / 严寒 / 温带 / 湿温 / 湿热 / 热带)。
以家为基准,代码里没有一个城市是写死的。 你改出发地,基准跟着变 —— 同一个南宁,对兰州来的和对南宁本地来的,是两种说法。
(提示词里那句「你从哪儿出来的鱼」也走同一个变量 {{journey.home}},
家那边的体感由城市数据现算成 {{journey.homeFeel}}。公开扩展不该带着作者的家乡。)
不合胃口的地方去得少,但不是不去(权重下限 0.42)。半年在外面本来就该见识 不一样的地方 —— 不习惯的表现应该是「去了以后难受、说得出来」,不是「永远不去」。
她随时说得出受不受得了:journey_now 里有一行「身体感觉」,
到达事件里也带一句。但那是事实,不是让她念的句子 ——
提示词里明写了「别播报气温,那是天气 App 的话」。
回家是走回去的,不是传送
决定回去之后:
- 离家 500 公里内才一趟车到家;还在外面就一站一站往回走
- 途中每一站都算真的到过 —— 有路上事件、写日记、有花费,住一晚就走
- 选点三级收敛:先找方向朝家且明显更近(≥25%)的,没有就放宽到 8%, 再没有才退回纯距离(死角兜底,宁可绕一点也不能回不了家)
- depart 文案单独一套:「往〈家〉的方向走。下一站某某,离家还有多少公里」
回程也是旅行。 以前是直接把位置改成家 —— 两千公里一步到, 中间的城市一个都不算去过。那段时间在她的日记里是空的, 而真实的旅行里,回程常常是想法最多的一段。
数据存在哪
| 数据 | 位置 | 能不能跟着人走 |
|---|---|---|
| 行程状态(现在在哪) | <dataDir>/state.json | 不能。两台机器共写会互相覆盖 |
| 日记 | 设置的目录,或 <dataDir>/diary | 能。按日期前缀合并冲突副本 |
这条分界不是随便划的。 放反了不会报错,只会出现「回家打开电脑,看到的是公司那台 三天前的位置」这种幻觉。音乐扩展踩过同一个坑。
⚠ 用网盘时把这个文件夹设成「始终保留在此设备上」。网盘的「按需文件」会把人文件留成 占位符,离线读不到,看起来和「记忆丢了」一模一样。
已知限制
- 没填高德密钥时,天气不分昼夜:夜里沿用当天白天那个预报值。所以「傍晚雨停了」 这种变化她感受不到。填个高德密钥就能补上(见上一节)。
- 照片只在本机:媒体库在
data/media/,不跟着同步盘走。换台电脑,日记还在,照片没了。 - 节日没做(万圣节、春运这些还没进作息表)——见 CHANGELOG 的下一步。
- 酒店名依赖高德。 店名从高德的地点库查(真名 + 地址),查不到就只说「县城宾馆」这种类别。 名单里会滤掉「暂停营业」和明显不是住处的地方(实测高德会把餐厅也归进住宿服务)。 它也会过时 —— 高德的库不是实时的,赶上刚关门的店只能认。
- 没有真实时刻表。 12306 不向第三方提供数据,航班同理,所以车次号是按线路和日期生成的 (同一天同一条线路固定)。它不会自相矛盾,但也不是真的能拿去买票的班次。 同理不做「余票」「晚点」—— 那些必须实时数据,编一个就等于把可信度搭进去。
- 目的地只有 90 多个,内置坐标精度到小数点后两位,够算距离,不够导航。
- 改倍速不影响已经在路上的那一段,下一段才生效。
- 到达后「城里的一处地方」是占位描述,不是具体景点。她要是编一个具体景点出来, 那是她没守住提示词里的第一条约束。
测试
# 152 项:距离、时长、时区、日出、作息表、酒店概率、选路权重、路上节点密度、照片、巡礼、跨海
node "<Coopanion>/resources/app/node_modules/tsx/dist/cli.mjs" --test test/journey.test.mjs
# 三十天压缩模拟:查状态机会不会卡死、会不会重复推送
node "<Coopanion>/resources/app/node_modules/tsx/dist/cli.mjs" test/sim.mts 30 24
# 装载自检:33 项
node "<Coopanion>/resources/app/node_modules/tsx/dist/cli.mjs" \
"<skill>/scripts/verify-world.mjs" .