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
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ jobs:
run: pnpm test:e2e:install

- name: Build extension
# 正式版构建默认屏蔽 agent 入口;e2e 需覆盖 agent 用例,故强制开启
run: pnpm build
env:
SC_ENABLE_AGENT: "true"

- name: Run E2E tests
run: pnpm test:e2e
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "1.4.0-beta.4",
"version": "1.4.0",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",
Expand Down
10 changes: 9 additions & 1 deletion rspack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ZipExecutionPlugin } from "./rspack-plugins/ZipExecutionPlugin";
import { readFileSync } from "fs";
import { v4 as uuidv4 } from "uuid";
import { toChromeVersion } from "./scripts/version.js";
import { resolveAgentEnabled, applyAgentManifest } from "./scripts/build-config.js";

const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));

Expand All @@ -12,6 +13,9 @@ const dirname = path.resolve();
const isDev = process.env.NODE_ENV === "development";
const isBeta = version.includes("-");
const isReactTools = process.env.REACT_DEVTOOLS === "true";
// agent 功能仅在开发版与 beta 版本提供,正式版本屏蔽入口并移除 debugger 权限。
// 可用环境变量 SC_ENABLE_AGENT 强制覆盖(如 e2e 在正式版上强制开启)。
const enableAgent = resolveAgentEnabled({ isDev, isBeta, envValue: process.env.SC_ENABLE_AGENT });

// Target browsers, see: https://github.com/browserslist/browserslist
// 依照 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts#browser_compatibility
Expand Down Expand Up @@ -132,6 +136,7 @@ export default {
new rspack.DefinePlugin({
"process.env.VI_TESTING": "'false'",
"process.env.SC_RANDOM_KEY": `'${uuidv4()}'`,
"process.env.SC_ENABLE_AGENT": `'${enableAgent}'`,
}),
new rspack.CopyRspackPlugin({
patterns: [
Expand All @@ -140,7 +145,10 @@ export default {
to: `${dist}/ext`,
// 将manifest.json内版本号替换为package.json中版本号
transform(content: Buffer) {
const manifest = JSON.parse(content.toString()) as chrome.runtime.ManifestV3;
const manifest = applyAgentManifest(
JSON.parse(content.toString()) as chrome.runtime.ManifestV3,
enableAgent
);
manifest.version = toChromeVersion(version);
if (isDev || isBeta) {
manifest.name = "__MSG_scriptcat_beta__";
Expand Down
35 changes: 35 additions & 0 deletions scripts/build-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* agent 功能仅在开发版与 beta 预发布版本中提供,正式版本屏蔽。
* @param {{ isDev: boolean, isBeta: boolean }} env
* @returns {boolean}
*/
export function isAgentEnabled({ isDev, isBeta }) {
return Boolean(isDev || isBeta);
}

/**
* 解析 agent 开关:环境变量 SC_ENABLE_AGENT 优先(用于 e2e/CI 或手动强制覆盖),
* 未设置时按版本派生(dev/beta 启用,正式版屏蔽)。
* @param {{ isDev: boolean, isBeta: boolean, envValue: string | undefined }} param
* @returns {boolean}
*/
export function resolveAgentEnabled({ isDev, isBeta, envValue }) {
if (envValue !== undefined) return envValue === "true";
return isAgentEnabled({ isDev, isBeta });
}

/**
* 正式版本不提供 agent,移除仅 agent 使用的 debugger 权限;
* 其余权限保持不变。启用 agent 时原样返回,禁用时返回新对象,不修改入参。
* @template {{ permissions?: string[] }} T
* @param {T} manifest
* @param {boolean} agentEnabled
* @returns {T}
*/
export function applyAgentManifest(manifest, agentEnabled) {
if (agentEnabled) return manifest;
return {
...manifest,
permissions: (manifest.permissions || []).filter((permission) => permission !== "debugger"),
};
}
67 changes: 67 additions & 0 deletions scripts/build-config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect } from "vitest";
import { isAgentEnabled, resolveAgentEnabled, applyAgentManifest } from "./build-config.js";

describe("构建配置 - agent 开关", () => {
describe("isAgentEnabled", () => {
it("开发版本应启用 agent", () => {
expect(isAgentEnabled({ isDev: true, isBeta: false })).toBe(true);
});

it("beta 版本应启用 agent", () => {
expect(isAgentEnabled({ isDev: false, isBeta: true })).toBe(true);
});

it("正式版本应禁用 agent", () => {
expect(isAgentEnabled({ isDev: false, isBeta: false })).toBe(false);
});
});

describe("resolveAgentEnabled - 环境变量覆盖", () => {
it("未设置环境变量时按版本派生(正式版禁用)", () => {
expect(resolveAgentEnabled({ isDev: false, isBeta: false, envValue: undefined })).toBe(false);
});

it("未设置环境变量时按版本派生(beta 启用)", () => {
expect(resolveAgentEnabled({ isDev: false, isBeta: true, envValue: undefined })).toBe(true);
});

it("环境变量 'true' 可在正式版强制启用 agent(用于 e2e)", () => {
expect(resolveAgentEnabled({ isDev: false, isBeta: false, envValue: "true" })).toBe(true);
});

it("环境变量 'false' 可在 beta 强制禁用 agent", () => {
expect(resolveAgentEnabled({ isDev: false, isBeta: true, envValue: "false" })).toBe(false);
});
});

describe("applyAgentManifest", () => {
const makeManifest = () => ({
permissions: ["tabs", "debugger", "storage"],
optional_permissions: ["background", "userScripts"],
});

it("启用 agent 时原样返回 manifest 且保留 debugger 权限", () => {
const manifest = makeManifest();
const result = applyAgentManifest(manifest, true);
expect(result).toBe(manifest);
expect(result.permissions).toContain("debugger");
});

it("禁用 agent 时移除 debugger 权限", () => {
const result = applyAgentManifest(makeManifest(), false);
expect(result.permissions).not.toContain("debugger");
expect(result.permissions).toEqual(["tabs", "storage"]);
});

it("禁用 agent 时不改动其它权限", () => {
const result = applyAgentManifest(makeManifest(), false);
expect(result.optional_permissions).toEqual(["background", "userScripts"]);
});

it("禁用 agent 时不修改入参 manifest", () => {
const manifest = makeManifest();
applyAgentManifest(manifest, false);
expect(manifest.permissions).toContain("debugger");
});
});
});
12 changes: 10 additions & 2 deletions scripts/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import manifest from "../src/manifest.json" with { type: "json" };
import packageInfo from "../package.json" with { type: "json" };
import semver from "semver";
import { toChromeVersion } from "./version.js";
import { resolveAgentEnabled, applyAgentManifest } from "./build-config.js";

// ============================================================================

Expand All @@ -28,6 +29,13 @@ const addZipFile = async (zip, path, content) => {

// 判断是否为beta版本
const version = semver.parse(packageInfo.version);
// agent 功能仅在 beta 版本提供,正式版本屏蔽入口并移除 debugger 权限
// (与 rspack 构建一致,支持环境变量 SC_ENABLE_AGENT 覆盖)
const agentEnabled = resolveAgentEnabled({
isDev: false,
isBeta: version.prerelease.length > 0,
envValue: process.env.SC_ENABLE_AGENT,
});
manifest.version = toChromeVersion(packageInfo.version);
if (version.prerelease.length) {
manifest.name = `__MSG_scriptcat_beta__`;
Expand Down Expand Up @@ -58,8 +66,8 @@ execSync("pnpm run build", { stdio: "inherit" });
// 处理firefox和chrome的zip压缩包

// 浅拷贝防止后续修改
const firefoxManifest = { ...manifest, background: { ...manifest.background } };
const chromeManifest = { ...manifest, background: { ...manifest.background } };
const firefoxManifest = applyAgentManifest({ ...manifest, background: { ...manifest.background } }, agentEnabled);
const chromeManifest = applyAgentManifest({ ...manifest, background: { ...manifest.background } }, agentEnabled);

chromeManifest.optional_permissions = chromeManifest.optional_permissions.filter((val) => val !== "userScripts");
delete chromeManifest.background.scripts;
Expand Down
4 changes: 4 additions & 0 deletions src/app/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { version } from "../../package.json";

export const ExtVersion = version;

// agent 功能仅在开发版与 beta 版本提供,正式版本屏蔽相关入口。
// 由打包变量 process.env.SC_ENABLE_AGENT 注入(见 rspack.config.ts / scripts/build-config.js)。
export const EnableAgent = process.env.SC_ENABLE_AGENT === "true";
export const Discord = "https://discord.gg/JF76nHCCM7";
export const DocumentationSite = "https://docs.scriptcat.org";

Expand Down
56 changes: 35 additions & 21 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { CompiledResourceDAO } from "@App/app/repo/resource";
import { initRegularUpdateCheck } from "./regular_updatecheck";
import { parseSkillScriptMetadata } from "@App/pkg/utils/skill_script";
import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage";
import { EnableAgent } from "@App/app/const";

export type TCheckScriptUpdateOption = Partial<
{ checkType: "user"; noUpdateCheck?: number } | ({ checkType: "system" } & Record<string, any>)
Expand Down Expand Up @@ -98,8 +99,11 @@ export class ScriptService {
}
// 处理url, 实现安装脚本
let targetUrl: string;
// 判断是否为 file:///*/*.user.js 或 file:///*/*.skill.js
if (req.url.startsWith("file://") && (req.url.endsWith(".user.js") || req.url.endsWith(".skill.js"))) {
// 判断是否为 file:///*/*.user.js 或 file:///*/*.skill.js(skill 仅 agent 启用时)
if (
req.url.startsWith("file://") &&
(req.url.endsWith(".user.js") || (EnableAgent && req.url.endsWith(".skill.js")))
) {
targetUrl = req.url;
} else {
const reqUrl = new URL(req.url);
Expand Down Expand Up @@ -166,8 +170,13 @@ export class ScriptService {
{ schemes: ["http", "https"], hostEquals: "docs.scriptcat.org", pathPrefix: "/en/docs/script_installation/" },
{ schemes: ["http", "https"], hostEquals: "www.tampermonkey.net", pathPrefix: "/script_installation.php" },
{ schemes: ["file"], pathSuffix: ".user.js" },
{ schemes: ["file"], pathSuffix: ".skill.js" },
{ schemes: ["file"], pathSuffix: ".cat.md" },
// Skill 安装入口仅在 agent 启用时拦截(正式版屏蔽)
...(EnableAgent
? [
{ schemes: ["file"], pathSuffix: ".skill.js" },
{ schemes: ["file"], pathSuffix: ".cat.md" },
]
: []),
],
}
);
Expand Down Expand Up @@ -252,21 +261,26 @@ export class ScriptService {
isUrlFilterCaseSensitive: false,
requestDomains: ["bitbucket.org"], // Chrome 101+
},
// SkillScript (.skill.js) 安装检测
{
regexFilter: "^([^?#]+?\\.skill\\.js)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod],
isUrlFilterCaseSensitive: false,
excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"],
},
// Skill 包 (.cat.md) 安装检测
{
regexFilter: "^([^?#]+?\\.cat\\.md)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod],
isUrlFilterCaseSensitive: false,
},
// Skill 安装入口仅在 agent 启用时拦截(正式版屏蔽)
...(EnableAgent
? [
// SkillScript (.skill.js) 安装检测
{
regexFilter: "^([^?#]+?\\.skill\\.js)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod],
isUrlFilterCaseSensitive: false,
excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"],
},
// Skill 包 (.cat.md) 安装检测
{
regexFilter: "^([^?#]+?\\.cat\\.md)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod],
isUrlFilterCaseSensitive: false,
},
]
: []),
];
const installPageURL = chrome.runtime.getURL("src/install.html");
const rules = conditions.map((condition, idx) => {
Expand Down Expand Up @@ -948,8 +962,8 @@ export class ScriptService {
logger?.error("prepare script failed", Logger.E(e));
}
}
// 检测是否为 SkillScript
const skillScriptMeta = parseSkillScriptMetadata(code);
// 检测是否为 SkillScript(仅 agent 启用时,正式版不识别 skill 安装)
const skillScriptMeta = EnableAgent ? parseSkillScriptMetadata(code) : null;
if (skillScriptMeta) {
const si = await createTempCodeEntry(false, uuid, code, url, upsertBy, {} as SCMetadata, options);
si[1].skillScript = true;
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_scriptcat__",
"version": "1.4.0.1500",
"version": "1.4.0",
"author": "CodFrm",
"description": "__MSG_scriptcat_description__",
"options_ui": {
Expand Down
13 changes: 8 additions & 5 deletions src/pages/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import "./index.css";
import { arcoLocale } from "@App/locales/arco";
import { prepareScriptByCode, parseMetadata } from "@App/pkg/utils/script";
import { parseSkillScriptMetadata } from "@App/pkg/utils/skill_script";
import { EnableAgent } from "@App/app/const";
import { saveHandle } from "@App/pkg/utils/filehandle-db";
import { makeBlobURL, openInCurrentTab } from "@App/pkg/utils/utils";
import ScrollBoundary from "@App/pages/components/layout/ScrollBoundary";
Expand Down Expand Up @@ -221,8 +222,8 @@ const MainLayout: React.FC<{
Promise.all(
acceptedFiles.map(async (aFile) => {
try {
// ZIP 文件走 Skill 安装流程
if (aFile.name.endsWith(".zip")) {
// ZIP 文件走 Skill 安装流程(仅 agent 启用时)
if (EnableAgent && aFile.name.endsWith(".zip")) {
await handleZipSkillInstall(aFile);
stat.success++;
return;
Expand Down Expand Up @@ -261,9 +262,11 @@ const MainLayout: React.FC<{
// 先尝试 UserScript 解析
const metadata = parseMetadata(code);
if (metadata) return prepareScriptByCode(code, `file:///*resp-check*/${file.name}`);
// 再尝试 SkillScript 解析
const skillScriptMeta = parseSkillScriptMetadata(code);
if (skillScriptMeta) return { script: {} as any };
// 再尝试 SkillScript 解析(仅 agent 启用时)
if (EnableAgent) {
const skillScriptMeta = parseSkillScriptMetadata(code);
if (skillScriptMeta) return { script: {} as any };
}
throw new Error("not a valid UserScript or SkillScript");
}),
simpleDigestMessage(`f=${file.name}\ns=${file.size},m=${file.lastModified}`),
Expand Down
Loading
Loading