423 lines
15 KiB
JavaScript
423 lines
15 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// src/popup/view.ts
|
|
function renderLoggedOut(root, error) {
|
|
root.innerHTML = `
|
|
<section data-popup-state="logged-out">
|
|
<h1>Star Chart Search Enhancer</h1>
|
|
<p>\u767B\u5F55\u540E\u624D\u80FD\u4F7F\u7528\u661F\u56FE\u589E\u5F3A\u529F\u80FD</p>
|
|
${error ? `<p data-popup-error="true">${error}</p>` : ""}
|
|
<button type="button" data-popup-sign-in="button">\u767B\u5F55 Logto</button>
|
|
</section>
|
|
`;
|
|
}
|
|
function renderLoggedIn(root, authState) {
|
|
const userInfo = authState.userInfo;
|
|
root.innerHTML = `
|
|
<section data-popup-state="logged-in">
|
|
<h1>Star Chart Search Enhancer</h1>
|
|
<p>\u5DF2\u767B\u5F55</p>
|
|
<p>${userInfo?.name ?? userInfo?.username ?? "\u672A\u77E5\u7528\u6237"}</p>
|
|
<p>${userInfo?.email ?? ""}</p>
|
|
<section data-popup-update="root">
|
|
<h2>\u7248\u672C\u66F4\u65B0</h2>
|
|
<p data-popup-update-status="text">\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...</p>
|
|
</section>
|
|
<button type="button" data-popup-sign-out="button">\u9000\u51FA\u767B\u5F55</button>
|
|
</section>
|
|
`;
|
|
}
|
|
function renderUpdateStatus(root, options) {
|
|
const container = root.querySelector('[data-popup-update="root"]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
if (options.status === "checking") {
|
|
container.innerHTML = `
|
|
<h2>\u7248\u672C\u66F4\u65B0</h2>
|
|
<p data-popup-update-status="text">\u5F53\u524D\u7248\u672C\uFF1A${options.currentVersion}</p>
|
|
<p>\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...</p>
|
|
`;
|
|
return;
|
|
}
|
|
if (options.status === "error") {
|
|
container.innerHTML = `
|
|
<h2>\u7248\u672C\u66F4\u65B0</h2>
|
|
<p data-popup-update-status="text">\u5F53\u524D\u7248\u672C\uFF1A${options.currentVersion}</p>
|
|
<p>\u6682\u65F6\u65E0\u6CD5\u68C0\u67E5\u66F4\u65B0</p>
|
|
<p>\u5982\u679C\u9700\u8981\u65B0\u7248\uFF0C\u8BF7\u8054\u7CFB\u7EF4\u62A4\u540C\u4E8B\u83B7\u53D6\u66F4\u65B0\u5305\u3002</p>
|
|
`;
|
|
return;
|
|
}
|
|
if (options.status === "latest" || !options.manifest) {
|
|
container.innerHTML = `
|
|
<h2>\u7248\u672C\u66F4\u65B0</h2>
|
|
<p data-popup-update-status="text">\u5F53\u524D\u7248\u672C\uFF1A${options.currentVersion}</p>
|
|
<p>\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C</p>
|
|
`;
|
|
return;
|
|
}
|
|
container.innerHTML = `
|
|
<h2>\u7248\u672C\u66F4\u65B0</h2>
|
|
<p data-popup-update-status="text">\u5F53\u524D\u7248\u672C\uFF1A${options.currentVersion}</p>
|
|
<p>\u53D1\u73B0\u65B0\u7248\u672C\uFF1A${options.manifest.latestVersion}</p>
|
|
${renderReleaseNotes(options.manifest.releaseNotes)}
|
|
<button type="button" data-popup-download-update="button">\u4E0B\u8F7D\u66F4\u65B0\u5305</button>
|
|
<button type="button" data-popup-download-guide="button">\u4E0B\u8F7D\u4F7F\u7528\u8BF4\u660E</button>
|
|
<p data-popup-update-download-status="text">\u4E0B\u8F7D\u540E\u8BF7\u89E3\u538B\u65B0\u7248 zip\uFF0C\u5E76\u5728 chrome://extensions \u91CC\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u3002</p>
|
|
`;
|
|
}
|
|
function setUpdateDownloadStatus(root, value) {
|
|
const output = root.querySelector('[data-popup-update-download-status="text"]');
|
|
if (!output) {
|
|
return;
|
|
}
|
|
output.textContent = value;
|
|
}
|
|
function renderReleaseNotes(releaseNotes) {
|
|
if (releaseNotes.length === 0) {
|
|
return "";
|
|
}
|
|
return `
|
|
<ul>
|
|
${releaseNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join("")}
|
|
</ul>
|
|
`;
|
|
}
|
|
function escapeHtml(value) {
|
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
function renderDevPanel(root, authState) {
|
|
const panel = root.ownerDocument.createElement("section");
|
|
panel.dataset.popupDevPanel = "root";
|
|
panel.innerHTML = `
|
|
<h2>dev auth panel</h2>
|
|
<p>resource: ${authState.resource ?? ""}</p>
|
|
<p>scopes: ${(authState.scopes ?? []).join(", ")}</p>
|
|
<p>token: ${authState.tokenAvailable ? "available" : "missing"}</p>
|
|
<p>expires: ${authState.accessTokenExpiresAt ?? "unknown"}</p>
|
|
<p>error: ${authState.lastError ?? ""}</p>
|
|
<button type="button" data-popup-test-protected-api="button">\u6D4B\u8BD5\u53D7\u4FDD\u62A4\u63A5\u53E3</button>
|
|
<pre data-popup-protected-api-result="output"></pre>
|
|
`;
|
|
root.appendChild(panel);
|
|
}
|
|
function setProtectedApiResult(root, value) {
|
|
const output = root.querySelector(
|
|
'[data-popup-protected-api-result="output"]'
|
|
);
|
|
if (!output) {
|
|
return;
|
|
}
|
|
output.textContent = value;
|
|
}
|
|
|
|
// src/shared/auth-config.ts
|
|
var defaultAuthConfig = {
|
|
apiResource: "https://talent-search.intelligrow.cn",
|
|
appId: "i4jkllbvih0554r4n0fd3",
|
|
enableDevAuthPanel: false,
|
|
logtoEndpoint: "https://login-api.intelligrow.cn",
|
|
scopes: ["openid", "profile", "offline_access", "talent-search:read"]
|
|
};
|
|
function readAuthConfig(overrides = {}) {
|
|
const nextConfig = {
|
|
...defaultAuthConfig,
|
|
...overrides
|
|
};
|
|
if (!nextConfig.logtoEndpoint.trim()) {
|
|
throw new Error("auth config logtoEndpoint is required");
|
|
}
|
|
if (!nextConfig.appId.trim()) {
|
|
throw new Error("auth config appId is required");
|
|
}
|
|
if (!nextConfig.apiResource.trim()) {
|
|
throw new Error("auth config apiResource is required");
|
|
}
|
|
return nextConfig;
|
|
}
|
|
|
|
// src/shared/auth-messages.ts
|
|
function isAuthResponseMessage(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = value;
|
|
if (candidate.ok === false) {
|
|
return candidate.type === "auth:error" && typeof candidate.error === "string";
|
|
}
|
|
if (candidate.ok !== true || typeof candidate.type !== "string") {
|
|
return false;
|
|
}
|
|
if (candidate.type === "auth:ack") {
|
|
return true;
|
|
}
|
|
if (candidate.type === "auth:token") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string"
|
|
);
|
|
}
|
|
if (candidate.type === "auth:state") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean"
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// src/shared/protected-api-client.ts
|
|
function createProtectedApiClient(options) {
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
return {
|
|
async loadProtectedMockData() {
|
|
const token = await readAccessToken(options.sendMessage);
|
|
const response = await fetchImpl(
|
|
new URL("/api/mock/protected", options.baseUrl).toString(),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
},
|
|
method: "GET"
|
|
}
|
|
);
|
|
if (response.status === 401 || response.status === 403) {
|
|
throw new Error("protected api unauthorized");
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`protected api request failed: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
};
|
|
}
|
|
async function readAccessToken(sendMessage) {
|
|
const response = await sendMessage({ type: "auth:get-access-token" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim()) {
|
|
throw new Error("protected api token unavailable");
|
|
}
|
|
return response.value.accessToken;
|
|
}
|
|
|
|
// src/shared/update-check.ts
|
|
function compareExtensionVersions(left, right) {
|
|
const leftParts = parseVersionParts(left);
|
|
const rightParts = parseVersionParts(right);
|
|
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
for (let index = 0; index < maxLength; index += 1) {
|
|
const leftValue = leftParts[index] ?? 0;
|
|
const rightValue = rightParts[index] ?? 0;
|
|
if (leftValue !== rightValue) {
|
|
return leftValue - rightValue;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
function parseUpdateManifest(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return null;
|
|
}
|
|
const candidate = value;
|
|
if (!isVersionString(candidate.latestVersion) || !isVersionString(candidate.minSupportedVersion) || !isHttpsUrl(candidate.zipUrl) || !isHttpsUrl(candidate.guideUrl) || typeof candidate.publishedAt !== "string" || !Array.isArray(candidate.releaseNotes) || !candidate.releaseNotes.every((note) => typeof note === "string")) {
|
|
return null;
|
|
}
|
|
return {
|
|
guideUrl: candidate.guideUrl,
|
|
latestVersion: candidate.latestVersion,
|
|
minSupportedVersion: candidate.minSupportedVersion,
|
|
publishedAt: candidate.publishedAt,
|
|
releaseNotes: candidate.releaseNotes,
|
|
zipUrl: candidate.zipUrl
|
|
};
|
|
}
|
|
async function fetchUpdateManifest(manifestUrl, fetchImpl = fetch) {
|
|
const response = await fetchImpl(manifestUrl, {
|
|
cache: "no-store"
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`update manifest request failed: ${response.status}`);
|
|
}
|
|
const manifest = parseUpdateManifest(await response.json());
|
|
if (!manifest) {
|
|
throw new Error("update manifest is invalid");
|
|
}
|
|
return manifest;
|
|
}
|
|
function parseVersionParts(value) {
|
|
return value.split(".").map((part) => {
|
|
const parsed = Number.parseInt(part, 10);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
});
|
|
}
|
|
function isVersionString(value) {
|
|
return typeof value === "string" && /^\d+(?:\.\d+)*$/.test(value);
|
|
}
|
|
function isHttpsUrl(value) {
|
|
if (typeof value !== "string") {
|
|
return false;
|
|
}
|
|
try {
|
|
return new URL(value).protocol === "https:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// src/shared/update-config.ts
|
|
var UPDATE_MANIFEST_URL = "https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json";
|
|
|
|
// src/popup/index.ts
|
|
async function bootPopup(options = {}) {
|
|
const currentDocument = options.document ?? document;
|
|
const popupConfig = readAuthConfig(options.config);
|
|
const currentVersion = options.currentVersion ?? readCurrentVersion();
|
|
const root = currentDocument.querySelector("#app");
|
|
const HTMLElementCtor = currentDocument.defaultView?.HTMLElement;
|
|
if (!root || HTMLElementCtor && !(root instanceof HTMLElementCtor)) {
|
|
throw new Error("popup root #app is required");
|
|
}
|
|
const sendMessage = options.sendMessage ?? ((message) => Promise.resolve(
|
|
globalThis.chrome?.runtime?.sendMessage?.(message)
|
|
));
|
|
const fetchProtectedApi = options.fetchProtectedApi ?? createProtectedApiClient({
|
|
baseUrl: "http://127.0.0.1:4319",
|
|
sendMessage
|
|
}).loadProtectedMockData;
|
|
const fetchUpdateManifest2 = options.fetchUpdateManifest ?? (() => fetchUpdateManifest(
|
|
options.updateManifestUrl ?? UPDATE_MANIFEST_URL
|
|
));
|
|
await renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi, {
|
|
currentVersion,
|
|
fetchUpdateManifest: fetchUpdateManifest2
|
|
});
|
|
}
|
|
async function renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi, updateOptions) {
|
|
const response = await sendMessage({ type: "auth:get-state" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
|
|
renderLoggedOut(root, "\u8BA4\u8BC1\u72B6\u6001\u8BFB\u53D6\u5931\u8D25");
|
|
return;
|
|
}
|
|
if (!response.value.isAuthenticated) {
|
|
renderLoggedOut(root, response.value.lastError);
|
|
root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, {
|
|
actionMessage: { type: "auth:sign-in" },
|
|
fetchProtectedApi,
|
|
updateOptions
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
renderLoggedIn(root, response.value);
|
|
void runUpdateCheck(root, sendMessage, updateOptions);
|
|
root.querySelector('[data-popup-sign-out="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, {
|
|
actionMessage: { type: "auth:sign-out" },
|
|
fetchProtectedApi,
|
|
updateOptions
|
|
});
|
|
});
|
|
if (popupConfig.enableDevAuthPanel) {
|
|
renderDevPanel(root, response.value);
|
|
root.querySelector('[data-popup-test-protected-api="button"]')?.addEventListener("click", () => {
|
|
void runProtectedApiProbe(root, fetchProtectedApi);
|
|
});
|
|
}
|
|
}
|
|
async function runAuthAction(root, popupConfig, sendMessage, options) {
|
|
const response = await sendMessage(options.actionMessage);
|
|
if (isActionError(response)) {
|
|
renderLoggedOut(root, response.error);
|
|
root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, options);
|
|
});
|
|
return;
|
|
}
|
|
await renderCurrentAuthState(
|
|
root,
|
|
popupConfig,
|
|
sendMessage,
|
|
options.fetchProtectedApi,
|
|
options.updateOptions
|
|
);
|
|
}
|
|
function isActionError(response) {
|
|
return isAuthResponseMessage(response) && !response.ok && response.type === "auth:error";
|
|
}
|
|
async function runUpdateCheck(root, sendMessage, options) {
|
|
renderUpdateStatus(root, {
|
|
currentVersion: options.currentVersion,
|
|
status: "checking"
|
|
});
|
|
try {
|
|
const manifest = await options.fetchUpdateManifest();
|
|
if (compareExtensionVersions(manifest.latestVersion, options.currentVersion) <= 0) {
|
|
renderUpdateStatus(root, {
|
|
currentVersion: options.currentVersion,
|
|
status: "latest"
|
|
});
|
|
return;
|
|
}
|
|
renderUpdateStatus(root, {
|
|
currentVersion: options.currentVersion,
|
|
manifest,
|
|
status: "available"
|
|
});
|
|
bindUpdateDownloadButtons(root, sendMessage, manifest);
|
|
} catch {
|
|
renderUpdateStatus(root, {
|
|
currentVersion: options.currentVersion,
|
|
status: "error"
|
|
});
|
|
}
|
|
}
|
|
function bindUpdateDownloadButtons(root, sendMessage, manifest) {
|
|
root.querySelector('[data-popup-download-update="button"]')?.addEventListener("click", () => {
|
|
void downloadUpdateAsset(root, sendMessage, {
|
|
filename: "star-chart-search-enhancer-internal.zip",
|
|
url: manifest.zipUrl
|
|
});
|
|
});
|
|
root.querySelector('[data-popup-download-guide="button"]')?.addEventListener("click", () => {
|
|
void downloadUpdateAsset(root, sendMessage, {
|
|
filename: "\u661F\u56FE\u589E\u5F3A\u63D2\u4EF6-\u8D85\u7B80\u5355\u5B89\u88C5\u4F7F\u7528\u6307\u5357.pdf",
|
|
url: manifest.guideUrl
|
|
});
|
|
});
|
|
}
|
|
async function downloadUpdateAsset(root, sendMessage, options) {
|
|
setUpdateDownloadStatus(root, "\u6B63\u5728\u4E0B\u8F7D...");
|
|
try {
|
|
await sendMessage({
|
|
filename: options.filename,
|
|
type: "update:download",
|
|
url: options.url
|
|
});
|
|
setUpdateDownloadStatus(root, "\u5DF2\u89E6\u53D1\u4E0B\u8F7D\u3002\u4E0B\u8F7D\u540E\u8BF7\u89E3\u538B\u65B0\u7248 zip\uFF0C\u5E76\u5728 chrome://extensions \u91CC\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u3002");
|
|
} catch (error) {
|
|
setUpdateDownloadStatus(
|
|
root,
|
|
error instanceof Error ? error.message : "\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
|
|
);
|
|
}
|
|
}
|
|
function readCurrentVersion() {
|
|
const runtime = globalThis.chrome?.runtime;
|
|
return runtime?.getManifest?.().version ?? "0.0.0";
|
|
}
|
|
async function runProtectedApiProbe(root, fetchProtectedApi) {
|
|
setProtectedApiResult(root, "\u8BF7\u6C42\u4E2D...");
|
|
try {
|
|
const result = await fetchProtectedApi();
|
|
setProtectedApiResult(root, JSON.stringify(result, null, 2));
|
|
} catch (error) {
|
|
setProtectedApiResult(
|
|
root,
|
|
error instanceof Error ? error.message : String(error)
|
|
);
|
|
}
|
|
}
|
|
if (typeof document !== "undefined") {
|
|
void bootPopup();
|
|
}
|
|
})();
|