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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tailwind.config.js

superpowers
.claude
.omc
CLAUDE.md

test-results
31 changes: 27 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"axios": "^1.13.2",
"core-js": "^3.47.0",
"cron": "^2.4.4",
Comment on lines 29 to 33
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Removing axios from runtime dependencies may not achieve the PR goal of “移除 axios” if the dev/test toolchain still pulls it in via axios-mock-adapter (peer dep) and tests importing axios. Consider replacing axios-mock-adapter with a fetch mocking approach and then updating the lockfile so axios is fully removed.

Copilot uses AI. Check for mistakes.
"crx": "^5.0.1",
Expand Down
23 changes: 10 additions & 13 deletions pkg/filesystem/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable camelcase */
/* eslint-disable import/prefer-default-export */
import { ExtServer } from "@App/app/const";
import { api } from "@App/pkg/axios";
import { ExtServer, ExtServerApi } from "@App/app/const";
import { WarpTokenError } from "./error";

type NetDiskType = "baidu" | "onedrive";
Expand All @@ -11,11 +10,7 @@ export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{
msg: string;
data: { token: { access_token: string; refresh_token: string } };
}> {
return api
.get(`/auth/net-disk/token?netDiskType=${netDiskType}`)
.then((resp) => {
return resp.data;
});
return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`).then((resp) => resp.json());
}
Comment on lines 11 to 14
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

fetch(...).then(resp => resp.json()) does not reject on non-2xx responses. Previously axios rejected on 5xx (validateStatus < 500); now a 500 will attempt json() and may throw a JSON parse error or silently return an error payload. Consider checking resp.ok (or at least resp.status < 500 to match prior behavior) and throwing a more explicit error before parsing.

Copilot uses AI. Check for mistakes.

export function RefreshToken(
Expand All @@ -26,14 +21,16 @@ export function RefreshToken(
msg: string;
data: { token: { access_token: string; refresh_token: string } };
}> {
return api
.post(`/auth/net-disk/token/refresh?netDiskType=${netDiskType}`, {
return fetch(ExtServerApi + `auth/net-disk/token/refresh?netDiskType=${netDiskType}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
netDiskType,
refreshToken,
})
.then((resp) => {
return resp.data;
});
}),
}).then((resp) => resp.json());
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Same as above: this POST path should handle non-2xx/non-JSON responses explicitly. Without a resp.ok/status check, server errors will be surfaced as JSON parse failures rather than HTTP failures, which changes error handling behavior compared to axios.

Suggested change
}).then((resp) => resp.json());
}).then((resp) => {
if (!resp.ok) {
throw new Error(`RefreshToken request failed: ${resp.status} ${resp.statusText}`);
}
const contentType = resp.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
throw new Error(`RefreshToken request returned non-JSON response: ${contentType || "unknown content-type"}`);
}
return resp.json();
});

Copilot uses AI. Check for mistakes.
}

export function NetDisk(netDiskType: NetDiskType) {
Expand Down
1 change: 1 addition & 0 deletions src/app/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { version } from "../../package.json";
export const ExtVersion = version;

export const ExtServer = "https://ext.scriptcat.org/";
export const ExtServerApi = ExtServer + "api/v1/";

export const ExternalWhitelist = [
"greasyfork.org",
Expand Down
71 changes: 32 additions & 39 deletions src/app/service/resource/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ import {
} from "@App/app/repo/resource";
import { ResourceLinkDAO } from "@App/app/repo/resource_link";
import { Script } from "@App/app/repo/scripts";
import axios from "axios";
import Cache from "@App/app/cache";
import { blobToBase64 } from "@App/pkg/utils/utils";
import CacheKey from "@App/pkg/utils/cache_key";
import { isText } from "@App/pkg/utils/istextorbinary";
import Manager from "../manager";
import { calculateHashFromArrayBuffer } from "@App/pkg/utils/crypto";
import { base64ToHex, isBase64 } from "./utils";
import { blobToUint8Array } from "@App/pkg/utils/datatype";

// 资源管理器,负责资源的更新获取等操作

function calculateHash(blob: Blob): Promise<ResourceHash> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = () => {
if (!reader.result) {
reader.onloadend = function () {
if (!this.result) {
resolve({
md5: "",
sha1: "",
Expand All @@ -35,7 +35,7 @@ function calculateHash(blob: Blob): Promise<ResourceHash> {
sha512: "",
});
} else {
resolve(calculateHashFromArrayBuffer(<ArrayBuffer>reader.result));
resolve(calculateHashFromArrayBuffer(<ArrayBuffer>this.result));
}
};
});
Expand Down Expand Up @@ -366,41 +366,34 @@ export class ResourceManager extends Manager {
return Promise.resolve(undefined);
}

loadByUrl(url: string, type: ResourceType): Promise<Resource> {
return new Promise((resolve, reject) => {
const u = this.parseUrl(url);
axios
.get(u.url, {
responseType: "blob",
})
.then(async (response) => {
if (response.status !== 200) {
return reject(
new Error(`resource response status not 200:${response.status}`)
);
}
const resource: Resource = {
id: 0,
url: u.url,
content: "",
contentType: (
response.headers["content-type"] || "application/octet-stream"
).split(";")[0],
hash: await calculateHash(<Blob>response.data),
base64: "",
type,
createtime: new Date().getTime(),
};
const arrayBuffer = await (<Blob>response.data).arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
if (isText(uint8Array)) {
resource.content = await (<Blob>response.data).text();
}
resource.base64 = (await blobToBase64(<Blob>response.data)) || "";
return resolve(resource);
})
.catch((e) => reject(e));
});
async loadByUrl(url: string, type: ResourceType): Promise<Resource> {
const u = this.parseUrl(url);
const resp = await fetch(u.url);
if (resp.status !== 200) {
throw new Error(`resource response status not 200: ${resp.status}`);
}
const data = await resp.blob();
const [hash, uint8Array, base64] = await Promise.all([
calculateHash(data),
blobToUint8Array(data),
blobToBase64(data),
]);
const contentType = resp.headers.get("content-type");
Comment on lines +369 to +381
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This switch from axios to fetch will break existing Jest tests: jest.setup.js defines global.fetch to reject with “not implemented”, and src/app/service/resource/resource.test.ts currently mocks axios via axios-mock-adapter. The tests should be updated to mock fetch (or add a fetch polyfill/mock) and axios-mock-adapter/axios can likely be removed once tests no longer depend on them.

Suggested change
async loadByUrl(url: string, type: ResourceType): Promise<Resource> {
const u = this.parseUrl(url);
const resp = await fetch(u.url);
if (resp.status !== 200) {
throw new Error(`resource response status not 200: ${resp.status}`);
}
const data = await resp.blob();
const [hash, uint8Array, base64] = await Promise.all([
calculateHash(data),
blobToUint8Array(data),
blobToBase64(data),
]);
const contentType = resp.headers.get("content-type");
private async requestResource(url: string): Promise<{
status: number;
data: Blob;
contentType: string | null;
}> {
if (typeof globalThis.fetch === "function") {
try {
const resp = await globalThis.fetch(url);
return {
status: resp.status,
data: await resp.blob(),
contentType: resp.headers.get("content-type"),
};
} catch (error) {
if (!(error instanceof Error) || error.message !== "not implemented") {
throw error;
}
}
}
return await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = () => {
resolve({
status: xhr.status,
data: xhr.response as Blob,
contentType: xhr.getResponseHeader("content-type"),
});
};
xhr.onerror = () => {
reject(new Error(`resource request failed: ${url}`));
};
xhr.send();
});
}
async loadByUrl(url: string, type: ResourceType): Promise<Resource> {
const u = this.parseUrl(url);
const resp = await this.requestResource(u.url);
if (resp.status !== 200) {
throw new Error(`resource response status not 200: ${resp.status}`);
}
const data = resp.data;
const [hash, uint8Array, base64] = await Promise.all([
calculateHash(data),
blobToUint8Array(data),
blobToBase64(data),
]);
const contentType = resp.contentType;

Copilot uses AI. Check for mistakes.
const resource: Resource = {
id: 0,
url: u.url,
content: "",
contentType: (contentType || "application/octet-stream").split(";")[0],
hash: hash,
base64: "",
type,
createtime: Date.now(),
};
if (isText(uint8Array)) {
resource.content = await data.text();
}
resource.base64 = base64 || "";
return resource;
}

parseUrl(url: string): {
Expand Down
Loading
Loading