没有匹配的BOSS,请更换关键词或清空搜索。
📋
暂无BOSS记录
请先创建或加入房间,再去「隐藏专区」页记录击杀吧
▶
🔒 一大陆专区 · 快捷记录(点击开始计时)
▶
📝 预设BOSS管理
🔒 隐藏专区 · 快捷记录(点击开始计时)
▶
📝 预设BOSS管理
🌐 跨服专区 · 快捷记录(点击开始计时)
▶
📝 跨服预设管理
🛡️ 房间管理后台
管理员可删除房间及房间内全部数据。
🌐 全局预设(同步所有房间)
全局保存会更新所有现有房间;新建房间也使用这份列表。
房间预设
⏰ 提醒设置
后台通知需要启用通知,并在当前房间订阅。iPhone 请从主屏幕图标打开。
💾 数据管理
🔒 管理员
🔧 Supabase 配置
配置步骤(只需配置一次):
1. 打开 supabase.com 注册免费账号
2. 点击 "New Project" 创建项目(区域选离你近的,如 Singapore)
3. 进入项目 → 左侧 "SQL Editor" → 新建查询,粘贴下面的SQL执行:
5. 填到上面输入框,点保存即可
把这个HTML文件发给队友,他们使用同样的URL和Key,并使用同一个房间名称和密码,即可同步。
1. 打开 supabase.com 注册免费账号
2. 点击 "New Project" 创建项目(区域选离你近的,如 Singapore)
3. 进入项目 → 左侧 "SQL Editor" → 新建查询,粘贴下面的SQL执行:
-- 神途BOSS计时器21版:完整安装/补齐SQL
-- 全选执行即可;无需先执行19、20版升级。保留现有房间、BOSS和预设数据。
-- 将BOSS访问升级为房间密码会话验证,所有成员请使用21版页面。
begin;
-- 神途BOSS计时器:在对应 Supabase 项目的 SQL Editor 中执行全部内容。
-- 保留旧 BOSS 数据,旧记录 room_id 为 NULL,新房间不会显示这些公共记录。
-- 沿用原版匿名公开读写模式;房间 ID 用于分享和分组,不是身份认证或权限隔离。
create table if not exists public.rooms (
id text primary key check (id ~ '^[A-F0-9]{16}$'),
created_at timestamptz not null default now()
);
create table if not exists public.bosses (
id text primary key,
name text not null,
interval int not null,
kill_time bigint not null,
refresh_time bigint not null,
remark text default '',
created_at bigint default (extract(epoch from now()) * 1000)::bigint
);
alter table public.bosses add column if not exists remark text default '';
alter table public.bosses add column if not exists room_id text references public.rooms(id);
create index if not exists bosses_room_refresh_idx on public.bosses(room_id, refresh_time);
alter table public.rooms enable row level security;
alter table public.bosses enable row level security;
drop policy if exists timer_rooms_read on public.rooms;
create policy timer_rooms_read on public.rooms for select to anon, authenticated using (true);
drop policy if exists timer_rooms_create on public.rooms;
create policy timer_rooms_create on public.rooms for insert to anon, authenticated with check (true);
drop policy if exists timer_bosses_access on public.bosses;
create policy timer_bosses_access on public.bosses for all to anon, authenticated using (true) with check (true);
grant usage on schema public to anon, authenticated;
grant select, insert on public.rooms to anon, authenticated;
grant select, insert, update, delete on public.bosses to anon, authenticated;
create table if not exists public.presence (
id text primary key,
last_seen bigint not null,
user_agent text
);
alter table public.presence enable row level security;
drop policy if exists timer_presence_access on public.presence;
create policy timer_presence_access on public.presence for all to anon, authenticated using (true) with check (true);
grant select, insert, update, delete on public.presence to anon, authenticated;
create or replace function public.get_online_count(p_threshold bigint)
returns json language sql security definer set search_path = '' as $$
select json_build_object('count', count(*)) from public.presence where last_seen >= p_threshold;
$$;
grant execute on function public.get_online_count(bigint) to anon, authenticated;
-- 在之前已配置房间表的同一项目执行。本升级保留房间和BOSS数据。
-- 升级后旧18版无法直接读写BOSS,请所有队友使用19版。
create schema if not exists extensions;
create extension if not exists pgcrypto with schema extensions;
-- Supabase 的 pgcrypto 一般安装在 extensions;兼容原先安装在其他 schema 的情况。
do $$ begin
if exists (select 1 from pg_extension e join pg_namespace n on n.oid=e.extnamespace where e.extname='pgcrypto' and n.nspname <> 'extensions') then
alter extension pgcrypto set schema extensions;
end if;
end $$;
alter table public.rooms add column if not exists name text;
alter table public.rooms add column if not exists password_hash text;
update public.rooms set name = '房间 ' || id where name is null;
create table if not exists public.timer_room_sessions (
token_hash text primary key,
room_id text not null references public.rooms(id) on delete cascade,
expires_at timestamptz not null default (now() + interval '24 hours')
);
create index if not exists timer_room_sessions_expiry on public.timer_room_sessions(expires_at);
alter table public.timer_room_sessions enable row level security;
revoke all on public.timer_room_sessions from public, anon, authenticated;
revoke all on public.rooms from public, anon, authenticated;
-- 移除旧版公开策略,防止不带密码绕过页面直接访问。
do $$ declare p record; begin
for p in select policyname, tablename from pg_policies where schemaname='public' and tablename in ('rooms','bosses','timer_room_sessions') loop
execute format('drop policy %I on public.%I', p.policyname, p.tablename);
end loop;
end $$;
create or replace function public.timer_has_room_access(p_room text)
returns boolean language sql stable security definer set search_path = pg_catalog, public, extensions as $$
select exists (select 1 from public.timer_room_sessions s
where s.room_id = p_room and s.expires_at > now()
and s.token_hash = encode(sha256(convert_to(coalesce(nullif(current_setting('request.headers',true),'')::jsonb->>'x-room-token',''),'UTF8')),'hex'));
$$;
revoke all on function public.timer_has_room_access(text) from public;
grant execute on function public.timer_has_room_access(text) to anon, authenticated;
alter table public.bosses enable row level security;
create policy timer_password_room_access on public.bosses for all to anon, authenticated
using (public.timer_has_room_access(room_id)) with check (public.timer_has_room_access(room_id));
grant select, insert, update, delete on public.bosses to anon, authenticated;
create or replace function public.timer_create_room(p_name text,p_password text)
returns jsonb language plpgsql security definer set search_path = pg_catalog, public, extensions as $$
declare v_id text; v_token text;
begin
if p_name is null or char_length(btrim(p_name)) not between 1 and 40 then raise exception '请填写1~40字房间名称'; end if;
if p_password is null or char_length(p_password) not between 6 and 64 or btrim(p_password)='' or octet_length(p_password)>72 then raise exception '密码须为6~64位,UTF-8最多72字节'; end if;
v_id := upper(encode(extensions.gen_random_bytes(8),'hex'));
v_token := encode(extensions.gen_random_bytes(32),'hex');
insert into public.rooms(id,name,password_hash) values(v_id,btrim(p_name),extensions.crypt(p_password,extensions.gen_salt('bf',10)));
delete from public.timer_room_sessions where expires_at < now();
insert into public.timer_room_sessions(token_hash,room_id) values(encode(sha256(convert_to(v_token,'UTF8')),'hex'),v_id);
return jsonb_build_object('id',v_id,'name',btrim(p_name),'token',v_token);
end $$;
create or replace function public.timer_join_room(p_id text,p_password text)
returns jsonb language plpgsql security definer set search_path = pg_catalog, public, extensions as $$
declare r public.rooms%rowtype; v_token text;
begin
if p_password is null or octet_length(p_password)>72 then raise exception '房间ID或密码错误,旧房间需先由管理员设置密码'; end if;
select * into r from public.rooms where id=upper(btrim(p_id));
if not found or r.password_hash is null then raise exception '房间ID或密码错误,旧房间需先由管理员设置密码'; end if;
if extensions.crypt(p_password,r.password_hash) <> r.password_hash then raise exception '房间ID或密码错误'; end if;
v_token := encode(extensions.gen_random_bytes(32),'hex');
delete from public.timer_room_sessions where expires_at < now();
insert into public.timer_room_sessions(token_hash,room_id) values(encode(sha256(convert_to(v_token,'UTF8')),'hex'),r.id);
return jsonb_build_object('id',r.id,'name',r.name,'token',v_token);
end $$;
create or replace function public.timer_leave_room()
returns void language sql security definer set search_path = pg_catalog, public as $$
delete from public.timer_room_sessions where token_hash = encode(sha256(convert_to(coalesce(nullif(current_setting('request.headers',true),'')::jsonb->>'x-room-token',''),'UTF8')),'hex');
$$;
revoke all on function public.timer_create_room(text,text), public.timer_join_room(text,text), public.timer_leave_room() from public;
grant execute on function public.timer_create_room(text,text), public.timer_join_room(text,text), public.timer_leave_room() to anon, authenticated;
-- 旧房间没有密码,因此升级后默认无法加入;保留记录。
-- 如需继续使用旧房间,请修改以下示例的ID、名称和密码,取消注释后执行:
-- update public.rooms set name='我的战队', password_hash=extensions.crypt('请换成你自己的密码',extensions.gen_salt('bf',10)) where id='你的16位房间ID';
-- 修改已有密码后如需让已登录成员退出,可执行:
-- delete from public.timer_room_sessions where room_id='你的16位房间ID';
-- 先执行19版「Supabase房间名称密码升级.sql」,再执行本文件。
-- 预设按房间共享,本服/跨服分别存储;空列表保留,不会被默认预设复活。
create table if not exists public.timer_room_presets (
room_id text not null references public.rooms(id) on delete cascade,
kind text not null check(kind in ('local','cross')),
items jsonb not null default '[]'::jsonb check(jsonb_typeof(items)='array'),
version bigint not null default 1,
primary key(room_id,kind)
);
alter table public.timer_room_presets enable row level security;
revoke all on public.timer_room_presets from public,anon,authenticated;
create or replace function public.timer_validate_presets(p_items jsonb)
returns void language plpgsql set search_path=pg_catalog as $$
declare item jsonb; n numeric;
begin
if p_items is null or jsonb_typeof(p_items)<>'array' then raise exception '预设必须为数组'; end if;
if jsonb_array_length(p_items)>500 then raise exception '每类预设最多500个'; end if;
for item in select value from jsonb_array_elements(p_items) loop
if jsonb_typeof(item)<>'object' or jsonb_typeof(item->'name') is distinct from 'string' or char_length(btrim(item->>'name')) not between 1 and 80 then raise exception 'BOSS名称须为1~80字'; end if;
if jsonb_typeof(item->'interval') is distinct from 'number' then raise exception '刷新间隔必须为正整数'; end if;
n := (item->>'interval')::numeric;
if n <> trunc(n) or n not between 1 and 525600 then raise exception '刷新间隔须为1~525600的整数'; end if;
end loop;
end $$;
revoke all on function public.timer_validate_presets(jsonb) from public,anon,authenticated;
create or replace function public.timer_load_presets(p_room text,p_local jsonb,p_cross jsonb)
returns jsonb language plpgsql security definer set search_path=pg_catalog,public as $$
begin
if not public.timer_has_room_access(p_room) then raise exception '房间会话已失效,请重新加入'; end if;
-- 只在房间尚未初始化时使用第一个加入者的现有预设。
if not exists(select 1 from public.timer_room_presets where room_id=p_room and kind='local') then
perform public.timer_validate_presets(p_local);
insert into public.timer_room_presets(room_id,kind,items) values(p_room,'local',p_local) on conflict do nothing;
end if;
if not exists(select 1 from public.timer_room_presets where room_id=p_room and kind='cross') then
perform public.timer_validate_presets(p_cross);
insert into public.timer_room_presets(room_id,kind,items) values(p_room,'cross',p_cross) on conflict do nothing;
end if;
return (select jsonb_agg(jsonb_build_object('kind',kind,'items',items,'version',version) order by kind) from public.timer_room_presets where room_id=p_room);
end $$;
create or replace function public.timer_save_presets(p_room text,p_kind text,p_items jsonb,p_version bigint)
returns jsonb language plpgsql security definer set search_path=pg_catalog,public as $$
declare v_version bigint;
begin
if not public.timer_has_room_access(p_room) then raise exception '房间会话已失效,请重新加入'; end if;
if p_kind is null or p_kind not in ('local','cross') then raise exception '预设类别错误'; end if;
perform public.timer_validate_presets(p_items);
update public.timer_room_presets set items=p_items,version=version+1
where room_id=p_room and kind=p_kind and version=p_version returning version into v_version;
if not found then raise exception '其他成员已修改预设,请重新编辑'; end if;
return jsonb_build_object('version',v_version);
end $$;
revoke all on function public.timer_load_presets(text,jsonb,jsonb),public.timer_save_presets(text,text,jsonb,bigint) from public;
grant execute on function public.timer_load_presets(text,jsonb,jsonb),public.timer_save_presets(text,text,jsonb,bigint) to anon,authenticated;
-- 在19版密码SQL和20版预设SQL均执行后,执行本升级。
alter table public.timer_room_presets drop constraint if exists timer_room_presets_kind_check;
alter table public.timer_room_presets add constraint timer_room_presets_kind_check check(kind in ('local','cross','mainland'));
create or replace function public.timer_load_presets_v21(p_room text,p_local jsonb,p_cross jsonb,p_mainland jsonb)
returns jsonb language plpgsql security definer set search_path=pg_catalog,public as $$
begin
perform public.timer_load_presets(p_room,p_local,p_cross);
if not exists(select 1 from public.timer_room_presets where room_id=p_room and kind='mainland') then
perform public.timer_validate_presets(p_mainland);
insert into public.timer_room_presets(room_id,kind,items) values(p_room,'mainland',p_mainland) on conflict do nothing;
end if;
return (select jsonb_agg(jsonb_build_object('kind',kind,'items',items,'version',version) order by kind) from public.timer_room_presets where room_id=p_room);
end $$;
revoke all on function public.timer_load_presets_v21(text,jsonb,jsonb,jsonb) from public;
grant execute on function public.timer_load_presets_v21(text,jsonb,jsonb,jsonb) to anon,authenticated;
create or replace function public.timer_save_presets(p_room text,p_kind text,p_items jsonb,p_version bigint)
returns jsonb language plpgsql security definer set search_path=pg_catalog,public as $$
declare v_version bigint;
begin
if not public.timer_has_room_access(p_room) then raise exception '房间会话已失效,请重新加入'; end if;
if p_kind is null or p_kind not in ('local','cross','mainland') then raise exception '预设类别错误'; end if;
perform public.timer_validate_presets(p_items);
update public.timer_room_presets set items=p_items,version=version+1
where room_id=p_room and kind=p_kind and version=p_version returning version into v_version;
if not found then raise exception '其他成员已修改预设,请重新编辑'; end if;
return jsonb_build_object('version',v_version);
end $$;
revoke all on function public.timer_load_presets(text,jsonb,jsonb),public.timer_save_presets(text,text,jsonb,bigint) from public;
grant execute on function public.timer_load_presets(text,jsonb,jsonb),public.timer_save_presets(text,text,jsonb,bigint) to anon,authenticated;
create or replace function public.timer_create_room(p_name text,p_password text)
returns jsonb language plpgsql security definer set search_path = pg_catalog, public, extensions as $$
declare v_id text; v_token text;
begin
if p_name is null or char_length(btrim(p_name)) not between 1 and 40 then raise exception '请填写1~40字房间名称'; end if;
if p_password is null or char_length(p_password) not between 6 and 64 or btrim(p_password)='' or octet_length(p_password)>72 then raise exception '密码须为6~64位,UTF-8最多72字节'; end if;
perform pg_advisory_xact_lock(hashtextextended(btrim(p_name),0));
if exists(select 1 from public.rooms where name=btrim(p_name)) then raise exception '房间名称已存在,请换一个名称'; end if;
v_id := upper(encode(extensions.gen_random_bytes(8),'hex'));
v_token := encode(extensions.gen_random_bytes(32),'hex');
insert into public.rooms(id,name,password_hash) values(v_id,btrim(p_name),extensions.crypt(p_password,extensions.gen_salt('bf',10)));
delete from public.timer_room_sessions where expires_at < now();
insert into public.timer_room_sessions(token_hash,room_id) values(encode(sha256(convert_to(v_token,'UTF8')),'hex'),v_id);
return jsonb_build_object('id',v_id,'name',btrim(p_name),'token',v_token);
end $$;
create or replace function public.timer_join_room_by_name(p_name text,p_password text)
returns jsonb language plpgsql security definer set search_path=pg_catalog,public as $$
declare ids text[];
begin
select array_agg(id) into ids from public.rooms where name=btrim(p_name);
if coalesce(cardinality(ids),0)=0 then raise exception '房间名称或密码错误'; end if;
if cardinality(ids)>1 then raise exception '该名称存在重名房间,请管理员在rooms表中修改为不同名称'; end if;
return public.timer_join_room(ids[1],p_password);
end $$;
revoke all on function public.timer_join_room_by_name(text,text) from public;
grant execute on function public.timer_join_room_by_name(text,text) to anon,authenticated;
notify pgrst,'reload schema';
commit;
-- 执行成功后,下列项目均应显示 true。
select
to_regclass('public.rooms') is not null as rooms_ready,
to_regclass('public.bosses') is not null as bosses_ready,
to_regclass('public.timer_room_presets') is not null as presets_ready,
to_regclass('public.timer_room_sessions') is not null as sessions_ready,
to_regprocedure('public.timer_join_room_by_name(text,text)') is not null as name_join_ready,
to_regprocedure('public.timer_load_presets_v21(text,jsonb,jsonb,jsonb)') is not null as mainland_ready;
4. 左侧 "Project Settings" → "API" → 复制 Project URL 和 anon public key5. 填到上面输入框,点保存即可
把这个HTML文件发给队友,他们使用同样的URL和Key,并使用同一个房间名称和密码,即可同步。