⚔️ 神途 BOSS 计时器

注册手机号账户后使用。已有账户可直接登录。

手机号用于账户注册,不发送短信。请妥善保管密码。

⚔️ 神途 BOSS 计时器 多人同步

未连接
计时
一大陆专区
隐藏专区
跨服专区
活动
设置

🏠 房间

已创建的房间
正在加载房间…

点击房间名称,输入密码即可进入。

点击上方房间并输入密码加入;创建新房间请填写名称和密码。

📋

暂无BOSS记录

请先创建或加入房间,再去「隐藏专区」页记录击杀吧

▶

🔒 一大陆专区 · 快捷记录(点击开始计时)

▶

📝 预设BOSS管理

▶

🔒 隐藏专区 · 快捷记录(点击开始计时)

▶

📝 预设BOSS管理

🌐 跨服专区 · 快捷记录(点击开始计时)

▶

📝 跨服预设管理

🛡️ 房间管理后台

管理员可删除房间及房间内全部数据。

⏰ 提醒设置

后台通知需要启用通知,并在当前房间订阅。iPhone 请从主屏幕图标打开。

💾 数据管理

🔒 管理员

🔧 Supabase 配置

配置步骤(只需配置一次):
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 key
5. 填到上面输入框,点保存即可

把这个HTML文件发给队友,他们使用同样的URL和Key,并使用同一个房间名称和密码,即可同步。

请输入管理密码

👁
⚠️
BOSS 即将刷新!