Skip to content

Commit 3ac78bd

Browse files
committed
feat: 优化 artifact 同步配置和 GitHub API 地址处理
- 定时 artifact 同步现在只要有 gistToken 就会执行,同步资格判断提取为独立方法。 - 新增 githubApiUrl 支持:自定义 API 地址时按自定义地址访问 Gist/Avatar,不再叠加 githubProxy;默认地址下仍可走 githubProxy。 - 更新 settings 处理逻辑:只有与 artifact store 相关的字段变更(gistToken、githubProxy、githubApiUrl、defaultProxy)才刷新 artifact store,并取消与此流程耦合的 avatar 更新。 - Gist 请求错误返回更稳健:4xx/5xx 时优先输出可读错误字段,JSON 无法解析时回退原始响应文本,避免解析异常导致误报。 - 新增/补齐测试:cron 同步资格检测、settings 刷新条件、GitHub API base URL 生成的行为全部有覆盖。
1 parent ad5a0e3 commit 3ac78bd

9 files changed

Lines changed: 231 additions & 24 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.22.20",
3+
"version": "2.22.21",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function hasCronArtifactSyncCredentials(settings = {}) {
2+
return Boolean(settings?.gistToken);
3+
}

backend/src/products/cron-sync-artifacts.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import $ from '@/core/app';
99
import { produceArtifact } from '@/restful/sync';
1010
import { syncToGist } from '@/restful/artifacts';
1111
import { findByName } from '@/utils/database';
12+
import { hasCronArtifactSyncCredentials } from '@/products/cron-sync-artifacts-eligibility';
1213

1314
!(async function () {
1415
let arg;
@@ -36,7 +37,7 @@ import { findByName } from '@/utils/database';
3637
} else {
3738
const settings = $.read(SETTINGS_KEY);
3839
// if GitHub token is not configured
39-
if (!settings.githubUser || !settings.gistToken) return;
40+
if (!hasCronArtifactSyncCredentials(settings)) return;
4041

4142
const artifacts = $.read(ARTIFACTS_KEY);
4243
if (!artifacts || artifacts.length === 0) return;

backend/src/restful/miscs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ function getEnv(req, res) {
133133
}
134134

135135
async function refresh(_, res) {
136-
// 1. get GitHub avatar and artifact store
137-
await updateAvatar();
136+
// 1. get artifact store
137+
// await updateAvatar();
138138
await updateArtifactStore();
139139

140140
// 2. clear resource cache

backend/src/restful/settings.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,31 @@ import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
22
import { success, failed } from './response';
33
import { InternalServerError } from '@/restful/errors';
44
import $ from '@/core/app';
5-
import Gist from '@/utils/gist';
5+
import Gist, { getGithubGistBaseURL } from '@/utils/gist';
66
import { clearLogSettingsCache } from '@/utils/debug-logs';
77

8+
const ARTIFACT_STORE_SETTING_KEYS = [
9+
'gistToken',
10+
'githubProxy',
11+
'githubApiUrl',
12+
'defaultProxy',
13+
];
14+
15+
export function shouldRefreshArtifactStoreForSettingsPatch(body = {}) {
16+
return ARTIFACT_STORE_SETTING_KEYS.some((key) =>
17+
Object.prototype.hasOwnProperty.call(body, key),
18+
);
19+
}
20+
21+
export function getGithubAvatarApiUrl({ username, githubApiUrl, githubProxy }) {
22+
const githubApiBaseURL = getGithubGistBaseURL({
23+
githubApiUrl,
24+
githubProxy,
25+
});
26+
27+
return `${githubApiBaseURL}/users/${encodeURIComponent(username)}`;
28+
}
29+
830
export default function register($app) {
931
const settings = $.read(SETTINGS_KEY);
1032
if (!settings) $.write({}, SETTINGS_KEY);
@@ -19,7 +41,7 @@ async function getSettings(req, res) {
1941
$.write(settings, SETTINGS_KEY);
2042
}
2143

22-
if (!settings.avatarUrl) await updateAvatar();
44+
// await updateAvatar();
2345
if (!settings.artifactStore) await updateArtifactStore();
2446

2547
success(res, settings);
@@ -70,13 +92,8 @@ async function updateSettings(req, res) {
7092
}
7193
$.write(newSettings, SETTINGS_KEY);
7294
clearLogSettingsCache();
73-
if (
74-
req.body.githubUser ||
75-
req.body.gistToken ||
76-
req.body.githubProxy ||
77-
req.body.defaultProxy
78-
) {
79-
await updateAvatar();
95+
if (shouldRefreshArtifactStoreForSettingsPatch(req.body)) {
96+
// await updateAvatar();
8097
await updateArtifactStore();
8198
}
8299
success(res, newSettings);
@@ -95,7 +112,12 @@ async function updateSettings(req, res) {
95112

96113
export async function updateAvatar() {
97114
const settings = $.read(SETTINGS_KEY);
98-
const { githubUser: username, syncPlatform, githubProxy } = settings;
115+
const {
116+
githubUser: username,
117+
syncPlatform,
118+
githubProxy,
119+
githubApiUrl,
120+
} = settings;
99121
if (username) {
100122
if (syncPlatform === 'gitlab') {
101123
try {
@@ -126,11 +148,11 @@ export async function updateAvatar() {
126148
try {
127149
const data = await $.http
128150
.get({
129-
url: `${
130-
githubProxy ? `${githubProxy}/` : ''
131-
}https://api.github.com/users/${encodeURIComponent(
151+
url: getGithubAvatarApiUrl({
132152
username,
133-
)}`,
153+
githubApiUrl,
154+
githubProxy,
155+
}),
134156
headers: {
135157
'User-Agent':
136158
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { hasCronArtifactSyncCredentials } from '@/products/cron-sync-artifacts-eligibility';
5+
6+
describe('cron artifact sync credentials', function () {
7+
it('allows token-only Gist-compatible sync settings', function () {
8+
expect(
9+
hasCronArtifactSyncCredentials({
10+
gistToken: 'token',
11+
githubUser: '',
12+
}),
13+
).to.equal(true);
14+
});
15+
16+
it('skips sync when the token is missing', function () {
17+
expect(
18+
hasCronArtifactSyncCredentials({
19+
githubUser: 'xream',
20+
}),
21+
).to.equal(false);
22+
});
23+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import {
5+
getGithubAvatarApiUrl,
6+
shouldRefreshArtifactStoreForSettingsPatch,
7+
} from '@/restful/settings';
8+
9+
describe('settings routes', function () {
10+
describe('artifact store refresh detection', function () {
11+
it('refreshes when GitHub API URL changes', function () {
12+
expect(
13+
shouldRefreshArtifactStoreForSettingsPatch({
14+
githubApiUrl: 'https://litegist.example.com/api',
15+
}),
16+
).to.equal(true);
17+
});
18+
19+
it('refreshes when a Gist-affecting setting is cleared', function () {
20+
expect(
21+
shouldRefreshArtifactStoreForSettingsPatch({
22+
githubProxy: '',
23+
}),
24+
).to.equal(true);
25+
});
26+
27+
it('does not refresh for unrelated settings', function () {
28+
expect(
29+
shouldRefreshArtifactStoreForSettingsPatch({
30+
logsMaxCount: 100,
31+
}),
32+
).to.equal(false);
33+
});
34+
35+
it('does not refresh when only GitHub username changes', function () {
36+
expect(
37+
shouldRefreshArtifactStoreForSettingsPatch({
38+
githubUser: 'xream',
39+
}),
40+
).to.equal(false);
41+
});
42+
});
43+
44+
describe('GitHub avatar API URL', function () {
45+
it('uses the default GitHub users API when no custom API URL is set', function () {
46+
expect(
47+
getGithubAvatarApiUrl({
48+
username: 'xream',
49+
}),
50+
).to.equal('https://api.github.com/users/xream');
51+
});
52+
53+
it('applies GitHub proxy only to the default GitHub users API', function () {
54+
expect(
55+
getGithubAvatarApiUrl({
56+
username: 'xream',
57+
githubProxy: 'https://proxy.example.com/',
58+
}),
59+
).to.equal(
60+
'https://proxy.example.com/https://api.github.com/users/xream',
61+
);
62+
});
63+
64+
it('does not apply GitHub proxy to a custom GitHub users API', function () {
65+
expect(
66+
getGithubAvatarApiUrl({
67+
username: 'xream',
68+
githubApiUrl: 'https://litegist.example.com/api/',
69+
githubProxy: 'https://proxy.example.com/',
70+
}),
71+
).to.equal('https://litegist.example.com/api/users/xream');
72+
});
73+
});
74+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { getGithubGistBaseURL } from '@/utils/gist';
5+
6+
describe('Gist GitHub API URL', function () {
7+
it('uses the default GitHub API URL when unset', function () {
8+
expect(getGithubGistBaseURL()).to.equal('https://api.github.com');
9+
});
10+
11+
it('uses the default GitHub API URL when blank', function () {
12+
expect(
13+
getGithubGistBaseURL({
14+
githubApiUrl: ' ',
15+
}),
16+
).to.equal('https://api.github.com');
17+
});
18+
19+
it('applies GitHub proxy only to the default GitHub API URL', function () {
20+
expect(
21+
getGithubGistBaseURL({
22+
githubProxy: 'https://proxy.example.com/',
23+
}),
24+
).to.equal('https://proxy.example.com/https://api.github.com');
25+
});
26+
27+
it('does not apply GitHub proxy to a custom GitHub API URL', function () {
28+
expect(
29+
getGithubGistBaseURL({
30+
githubApiUrl: 'https://litegist.example.com/api/',
31+
githubProxy: 'https://proxy.example.com/',
32+
}),
33+
).to.equal('https://litegist.example.com/api');
34+
});
35+
});

backend/src/utils/gist.js

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,31 @@ import { getPolicyDescriptor } from '@/utils';
33
import $ from '@/core/app';
44
import { SETTINGS_KEY } from '@/constants';
55

6+
const DEFAULT_GITHUB_API_URL = 'https://api.github.com';
7+
8+
function normalizeApiUrl(url, fallback = DEFAULT_GITHUB_API_URL) {
9+
const normalizedUrl = String(url ?? '').trim() || fallback;
10+
11+
return normalizedUrl.replace(/\/+$/, '');
12+
}
13+
14+
export function getGithubGistBaseURL({ githubApiUrl, githubProxy } = {}) {
15+
const normalizedGithubApiUrl = normalizeApiUrl(githubApiUrl);
16+
const isCustomGithubApiUrl =
17+
normalizedGithubApiUrl !== DEFAULT_GITHUB_API_URL;
18+
const normalizedGithubProxy = String(githubProxy || '')
19+
.trim()
20+
.replace(/\/+$/, '');
21+
22+
if (isCustomGithubApiUrl) {
23+
return normalizedGithubApiUrl;
24+
}
25+
26+
return `${
27+
normalizedGithubProxy ? `${normalizedGithubProxy}/` : ''
28+
}${DEFAULT_GITHUB_API_URL}`;
29+
}
30+
631
/**
732
* Gist backup
833
*/
@@ -13,7 +38,12 @@ export default class Gist {
1338
defaultProxy,
1439
defaultTimeout: timeout,
1540
githubProxy,
41+
githubApiUrl,
1642
} = $.read(SETTINGS_KEY);
43+
const githubGistBaseURL = getGithubGistBaseURL({
44+
githubApiUrl,
45+
githubProxy,
46+
});
1747
let proxy = defaultProxy;
1848
if ($.env.isNode) {
1949
proxy =
@@ -50,9 +80,19 @@ export default class Gist {
5080
events: {
5181
onResponse: (resp) => {
5282
if (/^[45]/.test(String(resp.statusCode))) {
53-
const body = JSON.parse(resp.body);
83+
let body;
84+
try {
85+
body = JSON.parse(resp.body);
86+
} catch (e) {
87+
//
88+
}
5489
return Promise.reject(
55-
`ERROR: ${body.message?.error ?? body.message}`,
90+
`ERROR: ${
91+
body?.message?.error ??
92+
body?.error ??
93+
body?.message ??
94+
resp.body
95+
}`,
5696
);
5797
} else {
5898
return resp;
@@ -67,9 +107,7 @@ export default class Gist {
67107
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
68108
};
69109
this.http = HTTP({
70-
baseURL: `${
71-
githubProxy ? `${githubProxy}/` : ''
72-
}https://api.github.com`,
110+
baseURL: githubGistBaseURL,
73111
headers: {
74112
...this.headers,
75113
...(isStash && proxy
@@ -92,8 +130,19 @@ export default class Gist {
92130
events: {
93131
onResponse: (resp) => {
94132
if (/^[45]/.test(String(resp.statusCode))) {
133+
let body;
134+
try {
135+
body = JSON.parse(resp.body);
136+
} catch (e) {
137+
//
138+
}
95139
return Promise.reject(
96-
`ERROR: ${JSON.parse(resp.body).message}`,
140+
`ERROR: ${
141+
body?.message?.error ??
142+
body?.error ??
143+
body?.message ??
144+
resp.body
145+
}`,
97146
);
98147
} else {
99148
return resp;

0 commit comments

Comments
 (0)