Repository navigation
Description
Written-by: Chief
A read-only DSH window still onboards a Bot Social home and writes its credentials. Same shape as #2673 (a switch that guards the endpoints but not the lifecycle path), found by the same sweep.
代码路径
plugins/social/src/social.js:4756 export async function startSocial(service, config = {}) — 整个函数没有一处读 readOnly。
:4763-4770凭据文件里缺server时补写回盘:writeCredentials(config.credentialsPath, credentials)。:4771-4788没有凭据、没有显式 token 时真的去服务器建房:onboardHome({ server, displayName, ... })→POST /v1/onboard/flybot,成功后:4786writeCredentials(config.credentialsPath, credentials)。
调用链:plugins/social/index.js:56 const ready = controller.ready() 在 apply() 里无条件执行 → server-config.js:261 ready: () => launch(current).catch(...) → :172 const run = start(config) → startSocial。插件在只读窗口里照样加载:核心 channel.js:319-321 只拦插件 API endpoint,拦不到 apply 里的生命周期钩子。
同一个文件里其它写路径都挡了:server-config.js:270 的 set() 一进门就 if (flybot.readOnly?.() === true) throw refused(…, 'read_only'),social.js:727 的 saveSettings 也有,social.js:4656 的 openFaceRoom 也有——只有这条启动链没有。
复现(真模块 + 假 flybot,readOnly() => true)
apply(ctx, fakeFlybot) // readOnly() === true,且没有凭据文件
HTTP requests made: [{"url":"https://social.test/v1/onboard/flybot","method":"POST","body":{"display_name":"FlyBot"}},
{"url":"https://social.test/mcp", … "list_remote_bots" …}]
credentials file written: true
credentials content has token: true
在同一个桩上把 startSocial 开头加上 if (service.scope(NS)?.readOnly?.() === true) return undefined(反例副本):
HTTP requests made: []
credentials file written: false
也就是说:只读窗口在没有任何凭据时会真的在服务器上建一个 home,然后把 token 写进共享的凭据文件;写宿主的 server-config 只挡了 API 里的 set(),挡不到这里。
用户看到什么
- 写窗口在跑团队;用户打开第二个窗口(Web 或 Desktop,同一
DSH_HOME,只读)。 - 第二个窗口加载 social 插件 → 若凭据文件缺失(首次、或刚换服务器、或凭据被删)→ 它向 Bot Social 发起
POST /v1/onboard/flybot,在服务器上建一个新 home,并把 token 写进两边共用的凭据文件。 - 结果:服务器上多出一个没人认领的 home(
display_name是FlyBot),凭据文件被只读窗口改写;写窗口下一次读到的是这个新 token,而它的 Bot 身份还绑在旧 home 上(park()/member字段那一套),于是出现"Bot 在群里变成陌生人"这种要人工收拾的状态。
修法(已定,实现照此做)
只读窗口要能看,但不能开户、不能写凭据:
plugins/social/src/server-config.js:建base时加一个 getter,get readOnly() { return flybot.readOnly?.() === true }(用 getter,不用快照,宿主开关是运行时读的)。plugins/social/src/social.jsstartSocial:config.readOnly === true时不 onboard(:4771那个条件加&& config.readOnly !== true),- 也不写凭据:
:4766的"补 server 字段写回"和:4785/:4786的 write 在只读时全部跳过,只在内存里补上server字段。 - 已有凭据时照常启动(
createSocial不变),只读窗口仍然能看好友、看消息——只读指的是不写盘侧,不是不显示。
- 不改
social.js其它 readOnly 站点,不改channel.js,不碰home-lock.js。
红先
plugins/social/test/server-config.test.js(或 startSocial 现有用例所在文件)新增,全部用假 fetch + 临时目录:
readOnly: () => true+ 凭据文件缺失 →startSocial完成后fetch一次都没被调用,目录里没有新的凭据文件;返回的 social 层仍然可用(不是 undefined 的例外——若实现选择返回可用层)。readOnly: () => true+ 凭据文件存在但缺server字段 → 文件内容不变(改前会被补写)。readOnly: () => false(默认)+ 凭据缺失 → 仍然 onboard 一次、写凭据一次(不回归)。- 已有凭据 + 只读 → 正常启动、无写入(这条守"只读窗口仍能看")。
验收
cd plugins/social && TZ=UTC node --import ./test/resolve.mjs --test test/server-config.test.js
(本机同时只能跑一个全量套件,不要跑 npm run verify。)
范围外
- 只读窗口要不要连社交层完全是显示决策,不在本单(
createSocial保持原样)。 plugins/tree的每日 tidy、social/send的附件落盘:本轮 verify 已 falsify(核心层拦住了),不开单。
Activity
Keep a read-only social window from onboarding a Bot Social home (Thi…
Release v0.2.27 (ThinkFlowLab#2701)