Skip to content

valueUpdate 相关代码改进#809

Merged
CodFrm merged 28 commits intoscriptscat:mainfrom
cyfung1031:pr-GM_addValueChangeListener-1
Oct 10, 2025
Merged

valueUpdate 相关代码改进#809
CodFrm merged 28 commits intoscriptscat:mainfrom
cyfung1031:pr-GM_addValueChangeListener-1

Conversation

@cyfung1031
Copy link
Copy Markdown
Collaborator

@cyfung1031 cyfung1031 commented Oct 6, 2025

概述

close #807 close #641

1) GM异步API setValue/setValues/deleteValue/deleteValues 等待数据处理
2)修正 listValues 次序错误问题 (TM有自动排序) (确认了TM没排序,不用改)
3)代码改善,高效简单不出错为主

变更内容

GM异步API setValue/setValues/deleteValue/deleteValues 等待数据处理

  1. 把 valueChangeListener 改良一下,让execute速度更快 (抽出成独立 ListenerManager, 在多valueChangeListener时可以更直接针对该key进行execute. 这有利于大量 setValue 操作的脚本。 )
  2. ValueUpdateData 改良一下,由单一条目改为多条目,在setValues 时不用进行大量事件通知 ( content <-> service_worker )
  3. 这些事件传递中加入 id, 因此 GM_setValue发出后,能等待并接收回应。
  4. 改良了 service_worker 的 setValue 和 setValues。 setValues可判别无改变。
  5. 加了 encodeMessage decodeMessage 使 null 跟 undefined 可以正常传播 (sendMessage放物件/阵列的问题)
  6. 加了 stackAsyncTask,使简单的异步stack操作不用碰到 cacheInstance.tx,加快效能 (cacheInstance.tx 需要存取 storage.session. 但setValue 操作不需要搞得这么烦。全部都在同一个service_worker做的,所以 service_worker 内的次序一致就可以)

截图

其他

不懂 this.mq.emit<TScriptValueUpdate>("valueUpdate", { script }); 为何没更新也需要。估计是用来回传。先跟现行代码一样处理。 (messageQueue事件 subscribe 不影响 GM API)

@cyfung1031 cyfung1031 marked this pull request as draft October 6, 2025 11:49
@cyfung1031 cyfung1031 changed the title GM异步API setValue/setValues/deleteValue/deleteValues 等待数据处理 valueUpdate 相關代碼改進 Oct 6, 2025
@cyfung1031 cyfung1031 changed the title valueUpdate 相關代碼改進 valueUpdate 相关代码改进 Oct 6, 2025
@cyfung1031
Copy link
Copy Markdown
Collaborator Author

用这个来验收

// ==UserScript==
// @name         GM Batch API Tester + Inspector (A↔B)
// @namespace    https://bbs.tampermonkey.net.cn/
// @version      1.3.0
// @description  仅用 GM.setValues/GM.deleteValues 驱动批次变化;A 页用 GM_addValueChangeListener 验证。加初始化按钮与页面试调工具(list/get/set/delete...)。另增 setValue/deleteValue 单键测试按钮;使用 Shadow DOM 并固定字号避免页面样式影响。
// @author       you
// @match        https://wilson.lovestoblog.com/demo/A.html*
// @match        https://wilson.lovestoblog.com/demo/B.html*
// @grant        GM_addValueChangeListener
// @grant        GM.getValues
// @grant        GM.setValues
// @grant        GM.deleteValues
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// ==/UserScript==

(function () {
  'use strict';

  const isA = location.href.includes('/demo/A.html');
  const isB = location.href.includes('/demo/B.html');

  // 测试键集合
  const KEYS = ['a','b','c','d','e'];

  // ---- UI ----
  // 使用 Shadow DOM 将测试面板样式与页面样式隔离,并固定字号,避免页面本来的设定影响测试工具内的文字大小
  const host = document.createElement('div');
  host.id = 'vt-host';
  const root = host.attachShadow({ mode: 'open' });

  const css = `
  :host{all:initial} /* 隔离宿主默认样式(不影响页面其他元素) */
  .vt-panel{position:fixed;right:12px;bottom:12px;width:420px;max-height:86vh;overflow:auto;
    z-index:2147483647;background:#0f172a;color:#e5e7eb;border:1px solid #334155;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.35);
    font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
    font-size:13px; line-height:1.5}
  .vt-head{display:flex;gap:8px;align-items:center;padding:10px 12px;border-bottom:1px solid #334155;position:sticky;top:0;background:#0f172a}
  .vt-badge{font-size:12px;padding:2px 8px;border-radius:999px;background:#1e293b;border:1px solid #475569}
  .vt-body{padding:10px 12px}
  .vt-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
  .vt-btn{cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid #475569;background:#111827;color:#e5e7eb;font-size:13px}
  .vt-btn:hover{background:#0b1220}
  .vt-log{font-size:12px;background:#0b1220;border:1px solid #1f2937;border-radius:8px;padding:8px;max-height:220px;overflow:auto;white-space:nowrap;}
  .vt-sec{border:1px solid #1f2937; border-radius:10px; padding:8px; background:#0b1220;}
  .vt-sec h4{margin:0 0 6px 0; font-size:12px; color:#cbd5e1;}
  .muted{color:#9ca3af}.pass{color:#4ade80}.fail{color:#f87171}
  .vt-input{width:100%; box-sizing:border-box; background:#0f172a; color:#e5e7eb; border:1px solid #334155; border-radius:8px; padding:6px; font-family:inherit; font-size:13px;}
  .vt-sel{background:#0f172a; color:#e5e7eb; border:1px solid #334155; border-radius:8px; padding:6px; font-size:13px;}
  .vt-row-vertical{flex-wrap:nowrap; flex-direction:column; align-items: flex-start;}
  code{font-size:12px}
  .vt-sublog{display:block;margin-left:2em;}
  `;
  const container = document.createElement('div');
  container.className = 'vt-panel';
  container.innerHTML = `
    <style>${css}</style>
    <div class="vt-head">
      <div><strong>GM Batch API Tester</strong></div>
      <div class="vt-badge">${isA ? 'A:监听验证' : isB ? 'B:触发操作' : 'Unknown'}</div>
      <div class="muted" style="margin-left:auto;">for keys: a,b,c,d,e</div>
    </div>
    <div class="vt-body">
      <div class="vt-row">
        <button class="vt-btn" id="openA">开 A</button>
        <button class="vt-btn" id="openB">开 B</button>
        <button class="vt-btn" id="initAll">初始化(清空键+清空事件)</button>
      </div>

      <div class="vt-row vt-row-vertical" ${isB ? '' : 'style="display:none"'} >
        <!-- B 端:批次 API 原有步骤 -->
        <button class="vt-btn" id="step1">(B)①setValues({b:5,d:6,e:9})</button>
        <button class="vt-btn" id="step2">(B)②deleteValues(['b','d'])</button>
        <button class="vt-btn" id="step3">(B)③setValues({a:1,b:2,c:3})</button>
        <button class="vt-btn" id="auto">(B)自动:①→②→③</button>
        <!-- B 端:新增单键 API 测试 -->
        <button class="vt-btn" id="stepS">(B)S:setValue('b', 7)</button>
        <button class="vt-btn" id="stepD">(B)D:deleteValue('b')</button>
      </div>

      <div class="vt-row vt-row-vertical" ${isA ? '' : 'style="display:none"'} >
        <!-- A 端:原有验证 -->
        <button class="vt-btn" id="verify123">(A)验证①②③:应为 {a:1,b:2,c:3,e:9}</button>
        <button class="vt-btn" id="verify31">(A)验证③①:应为 {a:1,b:5,c:3,d:6,e:9}</button>
        <button class="vt-btn" id="verify312">(A)验证③①②:应为 {a:1,c:3,e:9}</button>
        <!-- A 端:新增单键 API 验证 -->
        <button class="vt-btn" id="verifyS">(A)验证S:应为 {b:7}</button>
        <button class="vt-btn" id="verifySD">(A)验证S→D:应为 {}</button>
        <button class="vt-btn" id="clearLog">(A)清空事件记录</button>
      </div>

      <div class="vt-sec" style="margin-bottom:8px;">
        <h4>事件 Log(A 端透过 GM_addValueChangeListener 生成)</h4>
        <div class="vt-log" id="log"></div>
      </div>

      <div class="vt-sec">
        <h4>Inspector:直接执行 GM 指令(试调工具)</h4>
        <div class="vt-row">
          <select class="vt-sel" id="cmdSel">
            <option>listValues</option>
            <option>getValue</option>
            <option>setValue</option>
            <option>deleteValue</option>
            <option>getValues</option>
            <option>setValues</option>
            <option>deleteValues</option>
          </select>
          <button class="vt-btn" id="runCmd">执行</button>
        </div>
        <div class="vt-row">
          <input class="vt-input" id="cmdKey" placeholder="key(或多键:用 JSON/阵列;setValues 请直接填 JSON 物件)"/>
        </div>
        <div class="vt-row">
          <input class="vt-input" id="cmdVal" placeholder="value/default(JSON;getValue 可为 default,setValue/ setValues 的值)"/>
        </div>
        <div class="vt-log" id="cmdOut" style="max-height:180px;"></div>
        <div class="muted" style="margin-top:6px;">
          范例:setValue → key: <code>a</code>;value: <code>1</code><br/>
          setValues → key: <code>{"a":1,"b":2}</code>(整个物件放在「key」栏);deleteValues → key: <code>["a","b"]</code><br/>
          value 栏对 setValues 可留空;getValue 的 value 栏可填预设值(JSON)。
        </div>
      </div>
    </div>
  `;
  root.appendChild(container);
  document.documentElement.appendChild(host);

  const $ = (sel) => root.querySelector(sel);
  const logEl = $('#log');
  const cmdOut = $('#cmdOut');
  const ts = () => new Date().toLocaleTimeString();
  const log = (html) => { const d=document.createElement('div'); d.innerHTML=html; logEl.prepend(d); };
  const out = (html) => { const d=document.createElement('div'); d.innerHTML=html; cmdOut.prepend(d); };
  const j = (v) => { try { return JSON.stringify(v); } catch { return String(v); } };
  const fmt = (v) => `<code>${j(v)}</code>`;

  // ---- A 端:只用 GM_addValueChangeListener 收集事件 ----
  const events = []; // {t, name, oldV, newV, remote}
  if (isA && typeof GM_addValueChangeListener === 'function') {
    for (const key of KEYS) {
      GM_addValueChangeListener(key, (name, oldV, newV, remote) => {
        events.push({ t: Date.now(), name, oldV, newV, remote });
        log(`<span class="muted">${ts()}</span> <strong>${name}</strong> : old=${fmt(oldV)} → new=${fmt(newV)} (remote=${remote})`);
      });
    }
  }

  function buildStateFromEvents(list) {
    const state = {};
    for (const ev of list) {
      if (ev.newV === undefined) delete state[ev.name];
      else state[ev.name] = ev.newV;
    }
    return state;
  }

  function sortRecordByKey(a) {
    return Object.fromEntries(Object.entries(a).sort((a, b) => `${a}`.localeCompare(`${b}`)))
  }

  const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
  async function verifyExpected(expected) {
    const finalState = buildStateFromEvents(events);
    const expectedSorted = sortRecordByKey(expected);
    const finalStateSorted = sortRecordByKey(finalState);
    const gmStateSorted = await GM.getValues(Object.keys(expectedSorted));
    const ok = deepEqual(finalStateSorted, expectedSorted) && deepEqual(gmStateSorted, expectedSorted);
    log(`${ok ? '<span class="pass">PASS</span>' : '<span class="fail">FAIL</span>'} <div class="vt-sublog">期望事件记录 ${fmt(expectedSorted)}</div><div class="vt-sublog">实际事件记录 ${fmt(finalStateSorted)}</div><div class="vt-sublog">实际狀態列表 ${fmt(gmStateSorted)}</div>`);
  }

  // ---- 测试步骤(B 端仅用批次 API)----
  async function step1() {
    if (!GM?.setValues) return alert('GM.setValues 不可用');
    await GM.setValues({ b:5, d:6, e:9 });
    log(`<span class="muted">${ts()}</span> (B)setValues({b:5,d:6,e:9}) 已送出`);
  }
  async function step2() {
    if (!GM?.deleteValues) return alert('GM.deleteValues 不可用');
    await GM.deleteValues(['b','d']);
    log(`<span class="muted">${ts()}</span> (B)deleteValues(['b','d']) 已送出`);
  }
  async function step3() {
    if (!GM?.setValues) return alert('GM.setValues 不可用');
    await GM.setValues({ a:1, b:2, c:3 });
    log(`<span class="muted">${ts()}</span> (B)setValues({a:1,b:2,c:3}) 已送出`);
  }
  async function autoRun() {
    await step1();
    await new Promise(r => setTimeout(r, 150));
    await step2();
    await new Promise(r => setTimeout(r, 150));
    await step3();
  }

  // ---- 新增:单键 API 测试(B 端)----
  async function stepS() {
    if (!GM?.setValue) return alert('GM.setValue 不可用');
    await GM.setValue('b', 7);
    log(`<span class="muted">${ts()}</span> (B)setValue('b', 7) 已送出`);
  }
  async function stepD() {
    if (!GM?.deleteValue) return alert('GM.deleteValue 不可用');
    await GM.deleteValue('b');
    log(`<span class="muted">${ts()}</span> (B)deleteValue('b') 已送出`);
  }

  // ---- 初始化(清空键 + 清空事件)----
  async function initAll() {
    try {
      if (GM?.deleteValues) {
        await GM.deleteValues(KEYS);
      } else if (GM?.deleteValue) {
        for (const k of KEYS) await GM.deleteValue(k);
      }
      if (isA) {
        events.length = 0;
        logEl.innerHTML = '';
      }
      out(`<span class="muted">${ts()}</span> 初始化完成:已删除 ${fmt(KEYS)} 并清空 A 端事件`);
      log(`<span class="muted">${ts()}</span> 初始化完成(清空 a-e 与事件)`);
    } catch (e) {
      out(`<span class="fail">初始化失败</span>:${fmt(String(e))}`);
    }
  }

  // ---- Inspector:页面试调工具 ----
  async function runInspector() {
    const cmd = $('#cmdSel').value;
    const kIn = $('#cmdKey').value.trim();
    const vIn = $('#cmdVal').value.trim();

    function tryParse(s, fallback) {
      if (!s) return fallback;
      try { return JSON.parse(s); } catch { return s; } // 容忍非 JSON 字串
    }

    try {
      if (cmd === 'listValues') {
        if (!GM?.listValues) throw new Error('GM.listValues 不可用');
        const keys = await GM.listValues();
        out(`<span class="muted">${ts()}</span> listValues → ${fmt(keys)}`);
      }

      else if (cmd === 'getValue') {
        if (!GM?.getValue) throw new Error('GM.getValue 不可用');
        const def = tryParse(vIn, undefined);
        const val = await GM.getValue(kIn, def);
        out(`<span class="muted">${ts()}</span> getValue(${fmt(kIn)}, ${fmt(def)}) → ${fmt(val)}`);
      }

      else if (cmd === 'setValue') {
        if (!GM?.setValue) throw new Error('GM.setValue 不可用');
        const val = tryParse(vIn, vIn);
        await GM.setValue(kIn, val);
        out(`<span class="muted">${ts()}</span> setValue(${fmt(kIn)}, ${fmt(val)}) → done`);
      }

      else if (cmd === 'deleteValue') {
        if (!GM?.deleteValue) throw new Error('GM.deleteValue 不可用');
        await GM.deleteValue(kIn);
        out(`<span class="muted">${ts()}</span> deleteValue(${fmt(kIn)}) → done`);
      }

      else if (cmd === 'getValues') {
        if (!GM?.getValues) throw new Error('GM.getValues 不可用');
        const arr = tryParse(kIn, null);
        if (!Array.isArray(arr)) throw new Error('请在「key」栏填 JSON 阵列,如 ["a","b"]');
        const val = await GM.getValues(arr);
        out(`<span class="muted">${ts()}</span> getValues(${fmt(arr)}) → ${fmt(val)}`);
      }

      else if (cmd === 'setValues') {
        if (!GM?.setValues) throw new Error('GM.setValues 不可用');
        const obj = tryParse(kIn, null);
        if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw new Error('请在「key」栏填 JSON 物件,如 {"a":1,"b":2}');
        await GM.setValues(obj);
        out(`<span class="muted">${ts()}</span> setValues(${fmt(obj)}) → done`);
      }

      else if (cmd === 'deleteValues') {
        if (!GM?.deleteValues) throw new Error('GM.deleteValues 不可用');
        const arr = tryParse(kIn, null);
        if (!Array.isArray(arr)) throw new Error('请在「key」栏填 JSON 阵列,如 ["a","b"]');
        await GM.deleteValues(arr);
        out(`<span class="muted">${ts()}</span> deleteValues(${fmt(arr)}) → done`);
      }
    } catch (e) {
      out(`<span class="fail">${cmd} 失败</span>:${fmt(String(e))}`);
    }
  }

  // ---- 绑定 UI ----
  $('#openA')?.addEventListener('click', () => window.open('https://wilson.lovestoblog.com/demo/A.html','_blank'));
  $('#openB')?.addEventListener('click', () => window.open('https://wilson.lovestoblog.com/demo/B.html','_blank'));
  $('#initAll')?.addEventListener('click', initAll);

  if (isB) {
    $('#step1')?.addEventListener('click', step1);
    $('#step2')?.addEventListener('click', step2);
    $('#step3')?.addEventListener('click', step3);
    $('#auto')?.addEventListener('click', autoRun);
    // 新增单键 API
    $('#stepS')?.addEventListener('click', stepS);
    $('#stepD')?.addEventListener('click', stepD);
  }

  if (isA) {
    $('#verify123')?.addEventListener('click', () => verifyExpected({ a:1, b:2, c:3, e:9 }));
    $('#verify31')?.addEventListener('click', () => verifyExpected({ a:1, b:5, c:3, d:6, e:9 }));
    $('#verify312')?.addEventListener('click', () => verifyExpected({ a:1, c:3, e:9 }));
    // 新增单键 API 验证(假定先执行 S 再执行 D)
    $('#verifyS')?.addEventListener('click', () => verifyExpected({ b:7 }));
    $('#verifySD')?.addEventListener('click', () => verifyExpected({}));
    $('#clearLog')?.addEventListener('click', () => { events.length = 0; logEl.innerHTML = ''; });
  }

  $('#runCmd')?.addEventListener('click', runInspector);
})();

@cyfung1031 cyfung1031 marked this pull request as ready for review October 6, 2025 23:54
@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Oct 9, 2025

valueUpdate 目前只用于 early script 的处理,没更新不推送没影响

@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Oct 9, 2025

github出问题了?我将commit push上来了,怎么没更新?

https://github.com/cyfung1031/scriptcat/commits/pr-GM_addValueChangeListener-1/

@cyfung1031
Copy link
Copy Markdown
Collaborator Author

单元测试 (GM_setValues, GM_deleteValue, GM_deleteValues)
1be624e
close #810

type TStackFn<T> = (...args: any[]) => Promise<T>;

// 链表节点类型,包含任务、Promise 的 resolve/reject、以及下一个节点
type TNode<T> = {
Copy link
Copy Markdown
Member

@CodFrm CodFrm Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接用数组不是更好理解一些?性能也好一些

看到了 #815 在这里面处理吧

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有测过速度。用shift很慢
然后想了一下这个不用 index 存取的用 Linked list 最快
所以就改了。
这东西只是内部处理用。不用太在意。

测试如下 (結果是Macebook Pro Brave)

a=[1]
while( a.length<100000 ){
  a = a.concat(a);
}
t0=performance.now();
while( 0<a.length ){
  a.shift();
}
t1=performance.now();
t1-t0;

850 ~ 900ms

a=[1]
while( a.length<100000 ){
  a = a.concat(a);
}
t0=performance.now();
while( 0<a.length ){
  a.pop();
}
t1=performance.now();
t1-t0;

0.8 ~ 1.6 ms


做成 库 的话,这些能改善的都做吧
如果是混在 Runtime 之类的代码当然简单直接会较好。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我去,shift慢着这么多的么

QQ_1760105935978

@CodFrm CodFrm requested a review from Copilot October 10, 2025 14:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

这个PR主要改进了ScriptCat扩展中GM异步API的value存储和更新机制,提升了性能和数据处理能力。

核心改进:

  • 实现GM异步API的等待响应机制,使setValue/setValues/deleteValue/deleteValues操作能够等待数据处理完成
  • 引入批量处理和事件优化,减少大量setValue操作时的性能开销
  • 添加消息编码解码机制以确保null/undefined值的正确传输

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/pkg/utils/message_value.ts 新增消息编码解码工具,用于处理null/undefined值的序列化传输
src/pkg/utils/message_value.test.ts 消息编码解码工具的单元测试
src/pkg/utils/async_queue.ts 新增异步队列管理工具,用于优化并发操作性能
src/pkg/utils/async_queue.test.ts 异步队列工具的单元测试
src/app/service/service_worker/value.ts 重构value服务,支持批量操作和异步等待机制
src/app/service/service_worker/value.test.ts value服务的单元测试
src/app/service/service_worker/gm_api.ts 更新GM API处理器,支持新的异步机制和编码消息
src/app/service/sandbox/runtime.ts 更新运行时以处理新的编码消息格式
src/app/service/content/types.ts 重构value更新数据结构,支持批量条目和编码传输
src/app/service/content/script_executor.ts 更新脚本执行器以处理新的value更新格式
src/app/service/content/listener_manager.ts 新增监听器管理器,优化value变更监听性能
src/app/service/content/listener_manager.test.ts 监听器管理器的单元测试
src/app/service/content/inject.ts 更新注入运行时以处理新的编码消息类型
src/app/service/content/gm_api.ts 重构GM API实现,支持异步等待和批量操作
src/app/service/content/gm_api.test.ts GM API的扩展单元测试
src/app/service/content/exec_script.ts 更新执行脚本以处理新的value更新类型
src/app/service/content/create_context.ts 更新上下文创建以使用新的监听器管理器
src/app/repo/value.ts 添加GM key-value类型定义
Comments suppressed due to low confidence (1)

src/app/service/content/gm_api.ts:1

  • 使用了宽松相等比较(==)而不是严格相等比较(===)。在第72行中正确使用了===,这里应该保持一致。
import type { Message, MessageConnect } from "@Packages/message/types";

Comment thread src/app/service/content/gm_api.test.ts Outdated
Comment thread src/pkg/utils/async_queue.ts Outdated
Comment thread src/pkg/utils/async_queue.test.ts Outdated
Comment thread src/pkg/utils/async_queue.test.ts Outdated
Comment thread src/pkg/utils/async_queue.test.ts Outdated
CodFrm and others added 5 commits October 10, 2025 22:04
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@CodFrm CodFrm merged commit 066da8d into scriptscat:main Oct 10, 2025
1 of 2 checks passed
@cyfung1031 cyfung1031 deleted the pr-GM_addValueChangeListener-1 branch October 17, 2025 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GM.setValue 要等候 service_worker 的 valueChange 回传 [BUG] GM_addValueChangeListener经常监听不到

3 participants