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
34 changes: 21 additions & 13 deletions src/app/service/content/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface IGM_Base {
emitEvent(event: string, eventId: string, data: any): void;
}

const integrity = {}; // 僅防止非法实例化
const integrity = {}; // 仅防止非法实例化

// GM_Base 定义内部用变量和函数。均使用@protected
// 暂不考虑 Object.getOwnPropertyNames(GM_Base.prototype) 和 ts-morph 脚本生成
Expand Down Expand Up @@ -1026,17 +1026,25 @@ export default class GMApi extends GM_Base {
}

@GMContext.API({ depend: ["GM_closeInTab"], alias: "GM.openInTab" })
public GM_openInTab(url: string, options?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab {
let option: GMTypes.OpenTabOptions = {};
if (arguments.length === 1) {
option.active = true;
} else if (typeof options === "boolean") {
option.active = !options;
} else {
option = <GMTypes.OpenTabOptions>options;
public GM_openInTab(url: string, param?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab {
let option = {} as GMTypes.OpenTabOptions;
if (typeof param === "boolean") {
option.active = !param; // Greasemonkey 3.x loadInBackground
} else if (param) {
option = { ...param } as GMTypes.OpenTabOptions;
}
if (typeof option.active !== "boolean" && typeof option.loadInBackground === "boolean") {
// TM 同时兼容 active 和 loadInBackground ( active 优先 )
option.active = !option.loadInBackground;
} else if (option.active === undefined) {
option.active = true; // TM 预设 active: false;VM 预设 active: true;旧SC 预设 active: true;GM 依从 浏览器
}
if (option.insert === undefined) {
option.insert = true; // TM 预设 insert: true;VM 预设 insert: true;旧SC 无此设计 (false)
}
if (option.active === undefined) {
option.active = true;
if (option.setParent === undefined) {
option.setParent = true; // TM 预设 setParent: false; 旧SC 预设 setParent: true;
// SC 预设 setParent: true 以避免不可预计的问题
}
let tabid: any;

Expand Down Expand Up @@ -1213,8 +1221,8 @@ export default class GMApi extends GM_Base {
}
}

// GM_Base 對象中解構出 createGMBase 函数並導出(可供其他模塊使用
// GM_Base 对象中解构出 createGMBase 函数并导出(可供其他模块使用
export const { createGMBase } = GM_Base;

// GMApi 對象中解構出內部函數,用於後續本地使用,不導出
// GMApi 对象中解构出内部函数,用于后续本地使用,不导出
const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_xmlhttpRequest } = GMApi;
91 changes: 65 additions & 26 deletions src/app/service/service_worker/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,38 +809,77 @@ export default class GMApi {

@PermissionVerify.API({})
async GM_openInTab(request: Request, sender: IGetSender) {
const url = request.params[0];
const options = request.params[1] || {};
if (options.useOpen === true) {
// 发送给offscreen页面处理
const ok = await sendMessage(this.msgSender, "offscreen/gmApi/openInTab", { url });
if (ok) {
// 由于window.open强制在前台打开标签,因此获取状态为{ active:true }的标签即为新标签
const tab = await getCurrentTab();
await cacheInstance.set(`GM_openInTab:${tab.id}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
const url = request.params[0] as string;
const options = (request.params[1] || {}) as GMTypes.OpenTabOptions &
Required<Pick<GMTypes.OpenTabOptions, "active">>;
const getNewTabId = async () => {
if (options.useOpen === true) {
// 发送给offscreen页面处理 (使用window.open)
const ok = await sendMessage(this.msgSender, "offscreen/gmApi/openInTab", { url });
if (ok) {
// 由于window.open强制在前台打开标签,因此获取状态为{ active:true }的标签即为新标签
const tab = await getCurrentTab();
return tab.id;
} else {
// 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭
// 似乎在Firefox中禁止在background页面使用window.open(),强制返回null
return false;
}
} else {
// 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭
// 似乎在Firefox中禁止在background页面使用window.open(),强制返回null
return false;
const { tabId, windowId } = sender.getExtMessageSender();
const active = options.active;
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

在处理active选项时缺少默认值处理。根据内容脚本中的逻辑,当active未定义时应该默认为true,但这里直接使用可能导致undefined值传递给Chrome API。

Suggested change
const active = options.active;
const active = options.active ?? true;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

content 的 GM_openInTab API 有处理default值问题。TypeScript这里要加 !
默认值处理 只在 content / service_worker 其中一边做,否则会很混乱

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

const currentTab = await chrome.tabs.get(tabId);
let newTabIndex = -1;
if (options.incognito && !currentTab.incognito) {
// incognito: "split" 在 normal 里不会看到 incognito
// 只能创建新 incognito window
// pinned 无效
// insert 不重要
await chrome.windows.create({
url,
incognito: true,
focused: active,
});
return 0;
}
if ((typeof options.insert === "number" || options.insert === true) && currentTab && currentTab.index >= 0) {
// insert 为 boolean 时,插入至当前Tab下一格 (TM行为)
// insert 为 number 时,插入至相对位置 (SC独自)
const insert = +options.insert;
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

当options.insert为boolean类型时,使用+操作符转换会得到0或1,但对于true应该表示插入到当前标签后一格。应该使用条件判断:const insert = options.insert === true ? 1 : +options.insert;

Suggested change
const insert = +options.insert;
const insert = options.insert === true ? 1 : +options.insert;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

JavaScript +options.insert 就是 1

newTabIndex = currentTab.index + insert;
if (newTabIndex < 0) newTabIndex = 0;
Comment on lines +845 to +850
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

条件判断逻辑复杂且容易出错。建议重构为更清晰的结构,先检查options.insert的类型,然后分别处理boolean和number的情况。

Suggested change
if ((typeof options.insert === "number" || options.insert === true) && currentTab && currentTab.index >= 0) {
// insert 为 boolean 时,插入至当前Tab下一格 (TM行为)
// insert 为 number 时,插入至相对位置 (SC独自)
const insert = +options.insert;
newTabIndex = currentTab.index + insert;
if (newTabIndex < 0) newTabIndex = 0;
if (currentTab && currentTab.index >= 0) {
let insert: number | undefined;
if (typeof options.insert === "number") {
// insert 为 number 时,插入至相对位置 (SC独自)
insert = options.insert;
} else if (options.insert === true) {
// insert 为 boolean 时,插入至当前Tab下一格 (TM行为)
insert = 1;
}
if (typeof insert === "number") {
newTabIndex = currentTab.index + insert;
if (newTabIndex < 0) newTabIndex = 0;
}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

不需要冗长代码。原代码足够清晰

}
const createProperties = {
url,
active: active,
} as chrome.tabs.CreateProperties;
if (options.setParent) {
// SC 预设 setParent: true 以避免不可预计的问题
createProperties.openerTabId = tabId === -1 ? undefined : tabId;
createProperties.windowId = windowId === -1 ? undefined : windowId;
}
if (options.pinned) {
// VM/FM行为
createProperties.pinned = true;
} else if (newTabIndex >= 0) {
// insert option; pinned 情况下无效
createProperties.index = newTabIndex;
}
const tab = await chrome.tabs.create(createProperties);
return tab.id;
}
} else {
const { tabId, windowId } = sender.getExtMessageSender();
const tab = await chrome.tabs.create({
url,
active: options.active,
openerTabId: tabId === -1 ? undefined : tabId,
windowId: windowId === -1 ? undefined : windowId,
});
await cacheInstance.set(`GM_openInTab:${tab.id}`, {
};
const tabId = await getNewTabId();
if (tabId) {
// 有 tab 创建的话
await cacheInstance.set(`GM_openInTab:${tabId}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
return tabId;
}
// 创建失败时返回 0
return 0;
}

@PermissionVerify.API({
Expand Down
77 changes: 75 additions & 2 deletions src/template/scriptcat.d.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,83 @@ declare namespace GMTypes {
) => unknown;

interface OpenTabOptions {
/**
* 决定新标签页是否在打开时获得焦点。
*
* - `true` → 新标签页会立即切换到前台。
* - `false` → 新标签页在后台打开,不会打断当前页面的焦点。
*
* 默认值:true
*/
active?: boolean;
insert?: boolean;

/**
* 决定新标签页插入位置。
*
* - 如果是 `boolean`:
* - `true` → 插入在当前标签页之后。
* - `false` → 插入到窗口末尾。
* - 如果是 `number`:
* - `0` → 插入到当前标签前一格。
* - `1` → 插入到当前标签后一格。
*
* 默认值:true
*/
insert?: boolean | number;

/**
* 决定是否设置父标签页(即 `openerTabId`)。
*
* - `true` → 浏览器能追踪由哪个标签打开的子标签,
* 有助于某些扩展(如标签树管理器)识别父子关系。
*
* 默认值:true
*/
setParent?: boolean;
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178

/**
* 是否在隐私窗口(无痕模式)中打开标签页。
*
* 注意:ScriptCat 的 manifest.json 配置了 `"incognito": "split"`,
* 在 normal window 中执行时,tabId/windowId 将不可用,
* 只能执行「打开新标签页」动作。
*
* 默认值:false
*/
incognito?: boolean;

/**
* 历史兼容字段,仅 TM 支持。
* 语义与 `active` **相反**:
*
* - `true` → 等价于 `active = false`(后台加载)。
* - `false` → 等价于 `active = true`(前台加载)。
*
* ⚠️ 不推荐使用:与 `active` 功能重复且容易混淆。
*
* 默认值:false
* @deprecated 请使用 `active` 替代
*/
loadInBackground?: boolean;

/**
* 是否将新标签页固定(pin)在浏览器标签栏左侧。
*
* - `true` → 新标签页为固定状态。
* - `false` → 普通标签页。
*
* 默认值:false
*/
pinned?: boolean;

/**
* 使用 `window.open` 打开新窗口,而不是浏览器 API。
* 可以打开某些特殊的链接
*
* 相关:Issue #178
* 默认值:false
*/
useOpen?: boolean;
}

interface XHRResponse {
Expand Down
77 changes: 75 additions & 2 deletions src/types/scriptcat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,83 @@ declare namespace GMTypes {
) => unknown;

interface OpenTabOptions {
/**
* 决定新标签页是否在打开时获得焦点。
*
* - `true` → 新标签页会立即切换到前台。
* - `false` → 新标签页在后台打开,不会打断当前页面的焦点。
*
* 默认值:true
*/
active?: boolean;
insert?: boolean;

/**
* 决定新标签页插入位置。
*
* - 如果是 `boolean`:
* - `true` → 插入在当前标签页之后。
* - `false` → 插入到窗口末尾。
* - 如果是 `number`:
* - `0` → 插入到当前标签前一格。
* - `1` → 插入到当前标签后一格。
*
* 默认值:true
*/
insert?: boolean | number;

/**
* 决定是否设置父标签页(即 `openerTabId`)。
*
* - `true` → 浏览器能追踪由哪个标签打开的子标签,
* 有助于某些扩展(如标签树管理器)识别父子关系。
*
* 默认值:true
*/
setParent?: boolean;
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178

/**
* 是否在隐私窗口(无痕模式)中打开标签页。
*
* 注意:ScriptCat 的 manifest.json 配置了 `"incognito": "split"`,
* 在 normal window 中执行时,tabId/windowId 将不可用,
* 只能执行「打开新标签页」动作。
*
* 默认值:false
*/
incognito?: boolean;

/**
* 历史兼容字段,仅 TM 支持。
* 语义与 `active` **相反**:
*
* - `true` → 等价于 `active = false`(后台加载)。
* - `false` → 等价于 `active = true`(前台加载)。
*
* ⚠️ 不推荐使用:与 `active` 功能重复且容易混淆。
*
* 默认值:false
* @deprecated 请使用 `active` 替代
*/
loadInBackground?: boolean;

/**
* 是否将新标签页固定(pin)在浏览器标签栏左侧。
*
* - `true` → 新标签页为固定状态。
* - `false` → 普通标签页。
*
* 默认值:false
*/
pinned?: boolean;

/**
* 使用 `window.open` 打开新窗口,而不是浏览器 API。
* 可以打开某些特殊的链接
*
* 相关:Issue #178
* 默认值:false
*/
useOpen?: boolean;
}

interface XHRResponse {
Expand Down
Loading