Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions scripts/dev-webui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { bilibiliPanelDecls } from '../src/worlds/bilibili/world.ts';
import { BUILTIN_OVERLAY_STYLES, normalizeOverlayDesign } from '../src/worlds/bilibili/overlay/model.ts';
import { OverlayAssetStore } from '../src/worlds/bilibili/overlay/assets.ts';
import { BilibiliOverlayServer, OverlayEditorConflictError } from '../src/worlds/bilibili/overlay/server.ts';
import type { AgentAnnouncementState, BilibiliOverlayDesign } from '../src/worlds/bilibili/overlay/types.ts';
import type { AgentAnnouncementState, BilibiliOverlayDesign, VideoQueueState } from '../src/worlds/bilibili/overlay/types.ts';
import { PromptRevisionConflict, WebApp, type ExtensionComment, type ExtensionDescribe, type ExtensionInfo, type ExtensionRating, type ExtensionSearchHit, type MarketTag, type OwnedStoragePart, type ToolOwner, type WebAppMarketDeps } from '../src/web/server.ts';
import { EXTENSION_API_VERSIONS, parseExtensionManifest } from '../src/extensions/manifest.ts';
import { PREVIOUS_CONTRACT_VERSIONS } from '../src/extensions/previous-contract.ts';
Expand Down Expand Up @@ -539,6 +539,17 @@ devBilibiliDesign.components.push(
styleId: 'builtin:white',
text: '今日目标:完成 Overlay 联调',
},
{
id: 'dev-video-queue',
name: '视频排队',
kind: 'video-queue',
styleId: 'builtin:translucent',
axis: 'vertical',
gap: 8,
showIndex: true,
emptyText: '暂无排队视频',
hideWhenEmpty: false,
},
);
devBilibiliDesign.placements.push(
{
Expand All @@ -553,13 +564,26 @@ devBilibiliDesign.placements.push(
id: 'placement-dev-fixed-notice', componentId: 'dev-fixed-notice',
x: 120, y: 220, width: 760, height: 120, z: 18, visible: true, locked: false,
},
{
id: 'placement-dev-video-queue', componentId: 'dev-video-queue',
x: 120, y: 690, width: 700, height: 190, z: 16, visible: true, locked: false,
},
);
let devBilibiliAnnouncement: AgentAnnouncementState = {
schemaVersion: 1,
text: '正在测试新版直播 Overlay',
revision: 1,
updatedAt: new Date().toISOString(),
};
const devBilibiliVideoQueue: VideoQueueState = {
schemaVersion: 1,
items: [
{ bvid: 'BV1Ab4y1C7dE', note: '新番预告 · 观众点播', addedAt: new Date().toISOString() },
{ bvid: 'BV1Qx411m7Ks', note: '', addedAt: new Date().toISOString() },
],
revision: 1,
updatedAt: new Date().toISOString(),
};
const devBilibiliAssets = new OverlayAssetStore(join(tmpData, 'bilibili-overlay-assets'));
const devBilibiliOverlay: BilibiliOverlayServer = new BilibiliOverlayServer({
preferredPort: 0,
Expand All @@ -569,6 +593,7 @@ const devBilibiliOverlay: BilibiliOverlayServer = new BilibiliOverlayServer({
designRevision: devBilibiliDesignRevision,
builtinStyles: structuredClone(BUILTIN_OVERLAY_STYLES),
agentAnnouncement: { ...devBilibiliAnnouncement },
videoQueue: structuredClone(devBilibiliVideoQueue),
}),
editor: {
state: () => devBilibiliState(),
Expand All @@ -595,7 +620,9 @@ function devBilibiliState(): Record<string, unknown> {
designRevision: devBilibiliDesignRevision,
builtinStyles: structuredClone(BUILTIN_OVERLAY_STYLES),
agentAnnouncement: { ...devBilibiliAnnouncement },
videoQueue: structuredClone(devBilibiliVideoQueue),
maxAnnouncementChars: BILIBILI_DEFAULTS.overlay.agentNoticeMaxChars,
maxQueuedVideos: BILIBILI_DEFAULTS.overlay.maxQueuedVideos,
assets: devBilibiliAssets.list(DEV_BILIBILI_OVERLAY_URL ? devBilibiliOverlay.baseUrl : null),
url: DEV_BILIBILI_OVERLAY_URL,
streamUp: !MINIMAL,
Expand Down Expand Up @@ -641,6 +668,14 @@ const devBilibiliOverlayPanel = {
},
};

let devBilibiliLoginStartedAt: number | null = null;
function devBilibiliLoginState(): Record<string, unknown> {
const base = { url: null, uid: null, error: null, configured: true, selfUid: 87654321 };
if (devBilibiliLoginStartedAt === null) return { ...base, phase: 'idle' };
if (Date.now() - devBilibiliLoginStartedAt < 4000) return { ...base, phase: 'waiting', url: 'about:blank#dev-bilibili-login' };
return { ...base, phase: 'done', uid: 87654321 };
}

/** 按 World id、面板局部 id、方法名组织假数据,返回最终接口响应形状。 */
const devPanels: Record<string, Record<string, Record<string, (...args: never[]) => unknown>>> = {
qq: {
Expand Down Expand Up @@ -681,7 +716,18 @@ const devPanels: Record<string, Record<string, Record<string, (...args: never[])
},
} as never,
bilibili: {

login: {
// dev:开始后 4 秒视为已在浏览器确认,不连 B 站。
state: () => devBilibiliLoginState(),
start: () => {
devBilibiliLoginStartedAt = Date.now();
return devBilibiliLoginState();
},
cancel: () => {
devBilibiliLoginStartedAt = null;
return devBilibiliLoginState();
},
},
log: {
state: () => ({
status: {
Expand Down Expand Up @@ -1025,7 +1071,7 @@ const devFakeWorlds: World[] = MINIMAL ? [] : [

const devFakeWorldTools: Record<string, string[]> = {
minecraft: ['mc_do', 'mc_queue', 'mc_check', 'mc_stop', 'mc_escape'],
bilibili: ['bilibili_set_announcement'],
bilibili: ['bilibili_set_announcement', 'bilibili_update_video_queue'],
};

/** QQ 事件面板(dev):按会话分组读假事件库 */
Expand Down
5 changes: 4 additions & 1 deletion src/worlds/bilibili/ENV_PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
观众看到的画面比现场晚 5 到 8 秒,注意下延迟
你可以用 `bilibili_set_announcement` 更新直播画面上的写字板,最多 {{bilibili.agentAnnouncementLimit}} 字
当前写字板内容:{{bilibili.agentAnnouncement | (暂无内容)}}
可以把你当前的目标/直播内容,或者别的你想写的更新在写字板。更新之前要通知外界。
可以把你当前的目标/直播内容,或者别的你想写的更新在写字板。更新之前要通知外界。
你还可以用 `bilibili_update_video_queue` 维护一份视频排队列表:加入、移除、调整顺序或清空,最多 {{bilibili.videoQueueLimit}} 个。Overlay 里放了视频排队组件时,这份列表会显示在直播画面上
当前排队:
{{bilibili.videoQueue | (空)}}
24 changes: 19 additions & 5 deletions src/worlds/bilibili/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# worlds/bilibili

B 站直播间协议只读接入。观众的弹幕、礼物、醒目留言、上舰、进场与人流读数从这里进事件流;
同一 World 提供本机透明 Overlay。唯一工具 `bilibili_set_announcement` 只写本机 Agent 公告栏,
不会向 B 站账号发送弹幕或执行平台操作。
同一 World 提供本机透明 Overlay。两个工具只改本机 Overlay:`bilibili_set_announcement` 写 Agent 公告栏,
`bilibili_update_video_queue` 改视频排队列表;都不会向 B 站账号发送弹幕或执行平台操作。

“重要观众”指符合 UID 资格、在限流时优先分配上下文预算的观众。资格、拥挤状态、事件分组与人物档案召回由 [`audience-admission.ts`](audience-admission.ts) 管理;阈值和预算在 [`config.ts`](config.ts)。资格变化不删除 Memory 中的人物档案。具体条件见「重要观众与实时限流」。

Expand Down Expand Up @@ -46,7 +46,13 @@ B 站直播间协议只读接入。观众的弹幕、礼物、醒目留言、上

## 登录凭证与观众身份

`worlds.bilibili.sessdata` 保存浏览器登录后的 SESSDATA cookie,位于部署的 `config.json`,不进入版本控制。匿名连接仍可接收弹幕;已观测到匿名响应将观众 uid 置为 0,并对昵称脱敏。`uid=0` 时不生成 `senderKey`,昵称不作为稳定身份键。
`worlds.bilibili.sessdata` 保存登录 cookie 的 SESSDATA,位于部署的 `config.json`,不进入版本控制。匿名连接仍可接收弹幕;已观测到匿名响应将观众 uid 置为 0,并对昵称脱敏。`uid=0` 时不生成 `senderKey`,昵称不作为稳定身份键。

WebUI 的 `login` 面板经 [`login.ts`](login.ts) 申请 B 站网页扫码登录的确认页链接;在已登录 B 站的浏览器里确认后,
登录的是这个浏览器当前的账号。
World 每 2 秒轮询登录状态,成功后取响应 `Set-Cookie` 里的 SESSDATA 原样(含 `%2C`)经 `onSessdata` 写回配置,
再只重建直播间长连;Overlay、归并缓冲与开播基线不动,所以不会补报开播。链接约 180 秒后由 B 站作废,
写回配置失败时这次登录记为失败、仍用旧凭证。

已有弹幕且最近至多 20 条均缺少有效 uid 时,WebUI 显示「近期弹幕缺少观众 uid」。

Expand Down Expand Up @@ -123,13 +129,15 @@ World 在 `127.0.0.1` 启动独立页面:`GET /overlay` 是 OBS browser source
- 横向或纵向弹幕机,准入可选弹幕、礼物、弹幕+礼物;用户名与内容可分别限制显示字数,礼物包含免费礼物、付费礼物、SC 与舰队事件。
- 横向或纵向滚动公告、固定公告、Agent 公告;多行滚动公告可设置每行停留时间和行间切换时长,滚动内容在运动方向的两端渐隐。
- HTTPS/HTTP 外部图片或上传图片,支持 contain/cover/fill。
- 视频排队:显示 Bot 维护的 BV 号列表,默认纵向堆叠,可改横向;可显示序号,队列为空时显示占位文字或隐藏组件。
序号与 BV 号用样式的用户名文字,Bot 写的备注用正文文字,放不下时以省略号截断。

组件可带独立标题。标题占据上、右、下或左侧边带,支持起点、居中、终点对齐,并拥有独立的
字体、字号、字重、RGBA 颜色与描边。Agent 公告更新时按 Unicode 字符逐字显示并保留输入光标;
弹幕机 Mock 会持续混合注入弹幕和礼物,组件工作区的 Mock 测试只写入预览运行器,不改设计、
真实公告或直播事件流。

内置样式为天蓝色、海军蓝、白色与极简,全部默认尖角。自定义样式可分别配置用户名和正文的
内置样式为半透明黑、天蓝色、海军蓝、白色与极简,全部默认尖角;半透明黑是新组件、缺省设计与找不到样式时的默认面板。自定义样式可分别配置用户名和正文的
字体、字号、字重、RGBA 颜色与描边。Nine-slice 将源图四条切线与输出边框宽度分开保存;
编辑器可在源图上拖动切线,并用可缩放的实际组件预览验证拉伸、平铺和中心填充。

Expand All @@ -144,14 +152,20 @@ WebP 与 GIF,单文件上限 8 MiB。Agent 公告单独原子保存到
正在滚动的弹幕。公告内容在下一次主会话前缀重建时通过 `ENV_PROMPT.md` 占位符同步; World 不会
为了每次公告写入强制重建前缀。

视频排队列表属于 World,所有排队组件显示同一份,原子保存到 `data/bilibili-overlay/video-queue.json`。
`bilibili_update_video_queue` 的 `add` 按位置插入(从 1 数起,缺省或超出末尾排到最后),`remove` 与 `move`
作用于第一个相同的 BV 号,`clear` 清空;BV 号可以写在视频链接里。队列达到 `worlds.bilibili.overlay.maxQueuedVideos`
时 `add` 被拒绝,调低上限不删除已有条目。每次修改以独立 SSE 事件 `video-queue` 更新组件,回执列出整队;
`ENV_PROMPT.md` 的 `bilibili.videoQueue` 占位符同公告一样在前缀重建时同步。

## WebUI

WebUI 提供直播间事件诊断和独立 Overlay 编辑器入口。样式、组件与布局编辑共享草稿、撤销历史和保存操作。草稿预览复用 OBS 渲染器,支持拖动、八向缩放、网格、画布与组件吸附、层级、可见性和锁定;编辑控件与选择框不进入 `/overlay` 输出。

从主 Web 页面打开编辑器时,语义调色板通过 URL fragment 传递。编辑器内置浅色和深色默认调色板,主 Web 停止后仍可通过独立 URL 打开。画布背景的浅深设置只保存在本机,不写入设计数据。

配置项包括直播间号、登录凭证、礼物即时投递门槛、相同消息归并窗与容量上限,以及 Overlay
启用、端口和 Agent 公告字数上限。
启用、端口、Agent 公告字数上限和视频排队上限。

日志面板每 2 秒轮询 `log.state`。`total` 是累计记录数,包括已超出 `RECENT_CAP` 而从缓冲移除的记录;客户端按差值追加新行,保留滚动位置。

Expand Down
2 changes: 1 addition & 1 deletion src/worlds/bilibili/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { Logger, ShutdownExternalCheck } from '../../core/types.ts';
import { text, type BilibiliText } from './strings.ts';
import { encodePacket, OP, parseFrame } from './wire.ts';

const UA =
export const UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36';

const HEARTBEAT_MS = 30_000;
Expand Down
9 changes: 9 additions & 0 deletions src/worlds/bilibili/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ export function bilibiliConfigGroup(language: Language): ConfigGroup {
'x-hot': true,
description: t.noticeMaxDescription,
},
'worlds.bilibili.overlay.maxQueuedVideos': {
type: 'integer',
title: t.queueMaxTitle,
minimum: 1,
maximum: 100,
'x-suffix': t.suffixItems,
'x-hot': true,
description: t.queueMaxDescription,
},
},
},
};
Expand Down
12 changes: 11 additions & 1 deletion src/worlds/bilibili/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const BILIBILI: WorldDefinition<BilibiliConfigSection> = {
return new BilibiliWorld({
roomId: cfg.roomId,
sessdata: cfg.sessdata,
onSessdata: (sessdata) => ctx.persist({ sessdata }),
giftFlushYuan: () => cfg.giftFlushYuan,
coalesceWindowMs: () => cfg.coalesceWindowMs,
coalesceMaxItems: () => cfg.coalesceMaxItems,
Expand All @@ -27,17 +28,26 @@ export const BILIBILI: WorldDefinition<BilibiliConfigSection> = {
overlay: cfg.overlay,
agentNoticeMaxChars: () => cfg.overlay.agentNoticeMaxChars,
agentNoticeFile: join(ctx.dataDir, 'bilibili-overlay', 'agent-notice.json'),
maxQueuedVideos: () => cfg.overlay.maxQueuedVideos,
videoQueueFile: join(ctx.dataDir, 'bilibili-overlay', 'video-queue.json'),
overlayAssetDir: join(ctx.dataDir, 'bilibili-overlay', 'assets'),
onOverlayConfig: (overlay) => ctx.persist({ overlay: { design: overlay.design } }),
timezone: ctx.timezone,
});
},
describe: {
tools: describeTools(bilibiliToolDecls(BILIBILI_DEFAULTS.overlay.agentNoticeMaxChars), {
tools: describeTools(bilibiliToolDecls(
BILIBILI_DEFAULTS.overlay.agentNoticeMaxChars,
BILIBILI_DEFAULTS.overlay.maxQueuedVideos,
), {
bilibili_set_announcement: {
zh: '改写直播画面叠加层上的公告板文字。',
en: 'Rewrites the announcement board text on the stream overlay.',
},
bilibili_update_video_queue: {
zh: '改写直播画面上的视频排队列表:加入、移除、调整顺序或清空。',
en: 'Edits the video queue list on the stream overlay: add, remove, reorder or clear.',
},
}),
events: [
{
Expand Down
Loading
Loading