valueUpdate 相关代码改进#809
Merged
CodFrm merged 28 commits intoscriptscat:mainfrom Oct 10, 2025
Merged
Conversation
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);
})();
|
Member
|
valueUpdate 目前只用于 early script 的处理,没更新不推送没影响 |
Member
|
github出问题了?我将commit push上来了,怎么没更新? https://github.com/cyfung1031/scriptcat/commits/pr-GM_addValueChangeListener-1/ |
Collaborator
Author
CodFrm
reviewed
Oct 10, 2025
| type TStackFn<T> = (...args: any[]) => Promise<T>; | ||
|
|
||
| // 链表节点类型,包含任务、Promise 的 resolve/reject、以及下一个节点 | ||
| type TNode<T> = { |
Collaborator
Author
There was a problem hiding this comment.
有测过速度。用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 之类的代码当然简单直接会较好。
Contributor
There was a problem hiding this comment.
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";
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

概述
close #807 close #641
1) GM异步API setValue/setValues/deleteValue/deleteValues 等待数据处理
2)
修正 listValues 次序错误问题 (TM有自动排序)(确认了TM没排序,不用改)3)代码改善,高效简单不出错为主
变更内容
GM异步API setValue/setValues/deleteValue/deleteValues 等待数据处理
cacheInstance.tx,加快效能 (cacheInstance.tx 需要存取 storage.session. 但setValue 操作不需要搞得这么烦。全部都在同一个service_worker做的,所以 service_worker 内的次序一致就可以)截图
其他
不懂
this.mq.emit<TScriptValueUpdate>("valueUpdate", { script });为何没更新也需要。估计是用来回传。先跟现行代码一样处理。 (messageQueue事件 subscribe 不影响 GM API)