Prepare internal extension distribution
This commit is contained in:
parent
4f1f80b79b
commit
37e29bd6b8
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,6 +1,9 @@
|
|||||||
.worktrees/
|
.worktrees/
|
||||||
.old-reference/
|
.old-reference/
|
||||||
|
.local/
|
||||||
dist/
|
dist/
|
||||||
|
dist-release/
|
||||||
|
release/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Local debug captures
|
# Local debug captures
|
||||||
|
|||||||
22
README.md
22
README.md
@ -10,6 +10,18 @@ npm test
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Release Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:release
|
||||||
|
npm run package:internal
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npm run build` outputs the development bundle to `dist/`
|
||||||
|
- `npm run build:release` outputs the internal distribution bundle to `dist-release/`
|
||||||
|
- `npm run package:internal` creates `release/star-chart-search-enhancer-internal.zip`
|
||||||
|
- The extension ID is fixed to `pkjopdibdnomhogjheclhnknmejccffg`
|
||||||
|
|
||||||
## Load The Extension
|
## Load The Extension
|
||||||
|
|
||||||
1. Run `npm run build`
|
1. Run `npm run build`
|
||||||
@ -37,7 +49,7 @@ Replace these before real sign-in testing:
|
|||||||
- `apiResource`
|
- `apiResource`
|
||||||
- Any extra scopes beyond `openid`, `profile`, and `offline_access`
|
- Any extra scopes beyond `openid`, `profile`, and `offline_access`
|
||||||
|
|
||||||
The popup dev panel is controlled by `enableDevAuthPanel`.
|
The popup dev panel is controlled by `enableDevAuthPanel` and is disabled by default.
|
||||||
|
|
||||||
## Popup Behavior
|
## Popup Behavior
|
||||||
|
|
||||||
@ -66,7 +78,7 @@ The popup dev panel is controlled by `enableDevAuthPanel`.
|
|||||||
6. Click `提交批次`
|
6. Click `提交批次`
|
||||||
7. Enter a batch name in the browser prompt
|
7. Enter a batch name in the browser prompt
|
||||||
8. Confirm the toolbar shows `批次提交成功`
|
8. Confirm the toolbar shows `批次提交成功`
|
||||||
9. Confirm the mock batch response accepts the payload and reports the submitted `batchId`
|
9. Confirm the mock batch response accepts the payload
|
||||||
|
|
||||||
## Market Auth Gate
|
## Market Auth Gate
|
||||||
|
|
||||||
@ -91,7 +103,6 @@ When the market page is opened without a valid auth state, the content script re
|
|||||||
"creatorName": "王少卿",
|
"creatorName": "王少卿",
|
||||||
"resource": "https://talent-search.intelligrow.cn",
|
"resource": "https://talent-search.intelligrow.cn",
|
||||||
"batchName": "自动验证批次",
|
"batchName": "自动验证批次",
|
||||||
"batchId": "p7pdhhtde8kj-<提交当时的 ISO 时间戳>",
|
|
||||||
"createdAt": "<提交当时的 ISO 时间戳>",
|
"createdAt": "<提交当时的 ISO 时间戳>",
|
||||||
"authors": [
|
"authors": [
|
||||||
{ "authorId": "7041184989643276324", "authorName": "旖旖小虎🐯" },
|
{ "authorId": "7041184989643276324", "authorName": "旖旖小虎🐯" },
|
||||||
@ -116,4 +127,7 @@ When the market page is opened without a valid auth state, the content script re
|
|||||||
{ "authorId": "7319160797236559882", "authorName": "郑皓文" }
|
{ "authorId": "7319160797236559882", "authorName": "郑皓文" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Internal distribution steps are documented in
|
||||||
|
`docs/internal-extension-distribution.md`.
|
||||||
|
|||||||
31
docs/internal-extension-distribution.md
Normal file
31
docs/internal-extension-distribution.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Internal Extension Distribution
|
||||||
|
|
||||||
|
## Fixed Extension ID
|
||||||
|
|
||||||
|
- Manifest key is fixed in the project.
|
||||||
|
- The unpacked extension ID is `pkjopdibdnomhogjheclhnknmejccffg`.
|
||||||
|
- Update Logto with:
|
||||||
|
- `https://pkjopdibdnomhogjheclhnknmejccffg.chromiumapp.org/callback`
|
||||||
|
- `https://pkjopdibdnomhogjheclhnknmejccffg.chromiumapp.org/`
|
||||||
|
- `chrome-extension://pkjopdibdnomhogjheclhnknmejccffg`
|
||||||
|
|
||||||
|
## Internal Package
|
||||||
|
|
||||||
|
1. Run `npm test`.
|
||||||
|
2. Run `npm run package:internal`.
|
||||||
|
3. Send `release/star-chart-search-enhancer-internal.zip` to coworkers.
|
||||||
|
|
||||||
|
## Coworker Install Steps
|
||||||
|
|
||||||
|
1. Unzip `star-chart-search-enhancer-internal.zip`.
|
||||||
|
2. Open `chrome://extensions`.
|
||||||
|
3. Enable developer mode.
|
||||||
|
4. Click `Load unpacked`.
|
||||||
|
5. Select the unzipped folder.
|
||||||
|
6. Confirm the extension ID is `pkjopdibdnomhogjheclhnknmejccffg`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep `.local/extension-key.pem` private and backed up internally.
|
||||||
|
- Do not commit or share the private key with people who only need to install the extension.
|
||||||
|
- If the batch submit backend changes away from `192.168.31.21:8083`, update `scripts/manifest.mjs` before packaging.
|
||||||
@ -5,7 +5,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
|
"build:release": "BUILD_TARGET=release node scripts/build.mjs",
|
||||||
"mock:protected-api": "node scripts/mock-protected-api.mjs",
|
"mock:protected-api": "node scripts/mock-protected-api.mjs",
|
||||||
|
"package:internal": "npm run build:release && node scripts/package-release.mjs",
|
||||||
|
"package:release": "npm run build:release && node scripts/package-release.mjs",
|
||||||
"test": "vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"test:watch": "vitest --passWithNoTests"
|
"test:watch": "vitest --passWithNoTests"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { cp, mkdir, rm } from "node:fs/promises";
|
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { build } from "tsup";
|
import { build } from "tsup";
|
||||||
|
import { createManifest } from "./manifest.mjs";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const projectRoot = path.resolve(__dirname, "..");
|
const projectRoot = path.resolve(__dirname, "..");
|
||||||
const distDir = path.join(projectRoot, "dist");
|
const buildTarget = process.env.BUILD_TARGET === "release" ? "release" : "development";
|
||||||
|
const distDir = path.join(
|
||||||
|
projectRoot,
|
||||||
|
buildTarget === "release" ? "dist-release" : "dist"
|
||||||
|
);
|
||||||
|
|
||||||
await rm(distDir, { recursive: true, force: true });
|
await rm(distDir, { recursive: true, force: true });
|
||||||
await mkdir(path.join(distDir, "content"), { recursive: true });
|
await mkdir(path.join(distDir, "content"), { recursive: true });
|
||||||
@ -68,11 +73,16 @@ await build({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await cp(
|
await writeFile(
|
||||||
path.join(projectRoot, "src/manifest.json"),
|
path.join(distDir, "manifest.json"),
|
||||||
path.join(distDir, "manifest.json")
|
`${JSON.stringify(createManifest({ target: buildTarget }), null, 2)}\n`
|
||||||
);
|
);
|
||||||
await cp(
|
await cp(
|
||||||
path.join(projectRoot, "src/popup/index.html"),
|
path.join(projectRoot, "src/popup/index.html"),
|
||||||
path.join(distDir, "popup/index.html")
|
path.join(distDir, "popup/index.html")
|
||||||
);
|
);
|
||||||
|
await cp(
|
||||||
|
path.join(projectRoot, "src/assets"),
|
||||||
|
path.join(distDir, "assets"),
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
|||||||
76
scripts/manifest.mjs
Normal file
76
scripts/manifest.mjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const sharedIcons = {
|
||||||
|
16: "assets/icons/icon-16.png",
|
||||||
|
32: "assets/icons/icon-32.png",
|
||||||
|
48: "assets/icons/icon-48.png",
|
||||||
|
128: "assets/icons/icon-128.png"
|
||||||
|
};
|
||||||
|
const extensionKey =
|
||||||
|
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB";
|
||||||
|
|
||||||
|
const sharedManifest = {
|
||||||
|
action: {
|
||||||
|
default_icon: {
|
||||||
|
16: sharedIcons[16],
|
||||||
|
32: sharedIcons[32]
|
||||||
|
},
|
||||||
|
default_popup: "popup/index.html"
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
service_worker: "background/index.js"
|
||||||
|
},
|
||||||
|
content_scripts: [
|
||||||
|
{
|
||||||
|
js: ["content/index.js"],
|
||||||
|
matches: [
|
||||||
|
"https://xingtu.cn/ad/creator/market*",
|
||||||
|
"https://*.xingtu.cn/ad/creator/market*"
|
||||||
|
],
|
||||||
|
run_at: "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
description: "Bootstraps the Xingtu creator market content script.",
|
||||||
|
icons: sharedIcons,
|
||||||
|
key: extensionKey,
|
||||||
|
manifest_version: 3,
|
||||||
|
name: "Star Chart Search Enhancer",
|
||||||
|
permissions: ["downloads", "identity", "storage"],
|
||||||
|
version: "0.2.0421.2",
|
||||||
|
web_accessible_resources: [
|
||||||
|
{
|
||||||
|
matches: [
|
||||||
|
"https://xingtu.cn/*",
|
||||||
|
"https://*.xingtu.cn/*"
|
||||||
|
],
|
||||||
|
resources: ["content/market-page-bridge.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const hostPermissionsByTarget = {
|
||||||
|
development: [
|
||||||
|
"http://*/*",
|
||||||
|
"https://login-api.intelligrow.cn/*",
|
||||||
|
"http://127.0.0.1:4319/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
release: [
|
||||||
|
"https://xingtu.cn/ad/creator/market*",
|
||||||
|
"https://*.xingtu.cn/ad/creator/market*",
|
||||||
|
"https://login-api.intelligrow.cn/*",
|
||||||
|
"https://talent-search.intelligrow.cn/*",
|
||||||
|
"http://192.168.31.21:8083/*"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createManifest(options = {}) {
|
||||||
|
const target = options.target ?? "development";
|
||||||
|
const hostPermissions = hostPermissionsByTarget[target];
|
||||||
|
if (!hostPermissions) {
|
||||||
|
throw new Error(`Unsupported manifest target: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sharedManifest,
|
||||||
|
host_permissions: hostPermissions
|
||||||
|
};
|
||||||
|
}
|
||||||
24
scripts/package-release.mjs
Normal file
24
scripts/package-release.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { mkdir, rm } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const projectRoot = path.resolve(__dirname, "..");
|
||||||
|
const sourceDir = path.join(projectRoot, "dist-release");
|
||||||
|
const releaseDir = path.join(projectRoot, "release");
|
||||||
|
const archivePath = path.join(
|
||||||
|
releaseDir,
|
||||||
|
"star-chart-search-enhancer-internal.zip"
|
||||||
|
);
|
||||||
|
|
||||||
|
await mkdir(releaseDir, { recursive: true });
|
||||||
|
await rm(archivePath, { force: true });
|
||||||
|
await execFileAsync("zip", ["-X", "-r", archivePath, "."], {
|
||||||
|
cwd: sourceDir
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Internal archive created at ${archivePath}`);
|
||||||
BIN
src/assets/icons/icon-128.png
Normal file
BIN
src/assets/icons/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icons/icon-16.png
Normal file
BIN
src/assets/icons/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 777 B |
BIN
src/assets/icons/icon-32.png
Normal file
BIN
src/assets/icons/icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/icons/icon-48.png
Normal file
BIN
src/assets/icons/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
13
src/assets/icons/icon-source.svg
Normal file
13
src/assets/icons/icon-source.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="16" y1="12" x2="114" y2="116" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#9F1239"/>
|
||||||
|
<stop offset="1" stop-color="#4C0519"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="8" y="8" width="112" height="112" rx="28" fill="url(#bg)"/>
|
||||||
|
<path d="M34 80L50 62L64 70L83 46" stroke="#FFF7ED" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"/>
|
||||||
|
<circle cx="86" cy="44" r="8" fill="#FFF7ED"/>
|
||||||
|
<path d="M79 80C79 71.7157 85.7157 65 94 65C102.284 65 109 71.7157 109 80C109 88.2843 102.284 95 94 95C85.7157 95 79 88.2843 79 80Z" stroke="#FFF7ED" stroke-width="8"/>
|
||||||
|
<path d="M104 91L114 101" stroke="#FFF7ED" stroke-linecap="round" stroke-width="8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 822 B |
@ -6,7 +6,6 @@ export interface BatchPayload {
|
|||||||
authorId: string;
|
authorId: string;
|
||||||
authorName: string;
|
authorName: string;
|
||||||
}>;
|
}>;
|
||||||
batchId: string;
|
|
||||||
batchName: string;
|
batchName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
creatorName: string;
|
creatorName: string;
|
||||||
@ -40,7 +39,6 @@ export function createBatchPayload(options: {
|
|||||||
authorId: record.authorId,
|
authorId: record.authorId,
|
||||||
authorName: record.authorName
|
authorName: record.authorName
|
||||||
})),
|
})),
|
||||||
batchId: `${logtoUserId}-${options.createdAt}`,
|
|
||||||
batchName,
|
batchName,
|
||||||
createdAt: options.createdAt,
|
createdAt: options.createdAt,
|
||||||
creatorName:
|
creatorName:
|
||||||
|
|||||||
@ -79,15 +79,12 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
options.submitBatch ??
|
options.submitBatch ??
|
||||||
((payload: BatchPayload) =>
|
((payload: BatchPayload) =>
|
||||||
readBatchSubmitAck(sendRuntimeMessage, payload));
|
readBatchSubmitAck(sendRuntimeMessage, payload));
|
||||||
|
let activeProgressLabel = "导出中";
|
||||||
|
let shouldShowDetailedProgress = true;
|
||||||
const exportRangeController = createExportRangeController({
|
const exportRangeController = createExportRangeController({
|
||||||
document: options.document,
|
document: options.document,
|
||||||
onProgress: ({ currentPage, totalPages }) => {
|
onProgress: ({ currentPage, totalPages }) => {
|
||||||
setToolbarExportStatus(
|
updateToolbarProgress(currentPage, totalPages);
|
||||||
toolbar,
|
|
||||||
totalPages
|
|
||||||
? `导出中 ${currentPage}/${totalPages} 页...`
|
|
||||||
: `导出中 第${currentPage}页...`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
prepareCurrentPageForExport: prepareCurrentPageForExport,
|
prepareCurrentPageForExport: prepareCurrentPageForExport,
|
||||||
readCurrentPageRecords: () => getVisibleOrderedRecords(),
|
readCurrentPageRecords: () => getVisibleOrderedRecords(),
|
||||||
@ -97,12 +94,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
const silentExportController = createSilentExportController({
|
const silentExportController = createSilentExportController({
|
||||||
document: options.document,
|
document: options.document,
|
||||||
onProgress: ({ currentPage, totalPages }) => {
|
onProgress: ({ currentPage, totalPages }) => {
|
||||||
setToolbarExportStatus(
|
updateToolbarProgress(currentPage, totalPages);
|
||||||
toolbar,
|
|
||||||
totalPages
|
|
||||||
? `导出中 ${currentPage}/${totalPages} 页...`
|
|
||||||
: `导出中 第${currentPage}页...`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let activeSort: MarketSortState | undefined;
|
let activeSort: MarketSortState | undefined;
|
||||||
@ -155,7 +147,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
const records = filterRecordsBySelection(
|
const records = filterRecordsBySelection(
|
||||||
await exportRecords(exportTarget.target)
|
await exportRecords(exportTarget.target, "导出中", {
|
||||||
|
showDetailedProgress: selectedAuthorIds.size === 0
|
||||||
|
})
|
||||||
);
|
);
|
||||||
options.onCsvReady?.(buildCsv(records));
|
options.onCsvReady?.(buildCsv(records));
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
@ -477,8 +471,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
async function exportRecords(
|
async function exportRecords(
|
||||||
target: MarketExportTarget,
|
target: MarketExportTarget,
|
||||||
inProgressLabel = "导出中"
|
inProgressLabel = "导出中",
|
||||||
|
progressOptions: {
|
||||||
|
showDetailedProgress?: boolean;
|
||||||
|
} = {}
|
||||||
): Promise<MarketRecord[]> {
|
): Promise<MarketRecord[]> {
|
||||||
|
activeProgressLabel = inProgressLabel;
|
||||||
|
shouldShowDetailedProgress = progressOptions.showDetailedProgress ?? true;
|
||||||
setToolbarExportStatus(toolbar, `${inProgressLabel}...`);
|
setToolbarExportStatus(toolbar, `${inProgressLabel}...`);
|
||||||
|
|
||||||
if (target.mode === "count" && target.pageCount <= 1) {
|
if (target.mode === "count" && target.pageCount <= 1) {
|
||||||
@ -499,6 +498,23 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
return exportRangeController.exportRecords(target);
|
return exportRangeController.exportRecords(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateToolbarProgress(
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number | undefined
|
||||||
|
): void {
|
||||||
|
if (!shouldShowDetailedProgress) {
|
||||||
|
setToolbarExportStatus(toolbar, `${activeProgressLabel}...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToolbarExportStatus(
|
||||||
|
toolbar,
|
||||||
|
totalPages
|
||||||
|
? `${activeProgressLabel} ${currentPage}/${totalPages} 页...`
|
||||||
|
: `${activeProgressLabel} 第${currentPage}页...`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function filterRecordsBySelection(records: MarketRecord[]): MarketRecord[] {
|
function filterRecordsBySelection(records: MarketRecord[]): MarketRecord[] {
|
||||||
if (selectedAuthorIds.size === 0) {
|
if (selectedAuthorIds.size === 0) {
|
||||||
return records;
|
return records;
|
||||||
|
|||||||
@ -3,7 +3,14 @@
|
|||||||
"name": "Star Chart Search Enhancer",
|
"name": "Star Chart Search Enhancer",
|
||||||
"version": "0.2.0421.2",
|
"version": "0.2.0421.2",
|
||||||
"description": "Bootstraps the Xingtu creator market content script.",
|
"description": "Bootstraps the Xingtu creator market content script.",
|
||||||
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB",
|
||||||
"permissions": ["downloads", "identity", "storage"],
|
"permissions": ["downloads", "identity", "storage"],
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icons/icon-16.png",
|
||||||
|
"32": "assets/icons/icon-32.png",
|
||||||
|
"48": "assets/icons/icon-48.png",
|
||||||
|
"128": "assets/icons/icon-128.png"
|
||||||
|
},
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"http://*/*",
|
"http://*/*",
|
||||||
"https://login-api.intelligrow.cn/*",
|
"https://login-api.intelligrow.cn/*",
|
||||||
@ -11,6 +18,10 @@
|
|||||||
"https://*/*"
|
"https://*/*"
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
|
"default_icon": {
|
||||||
|
"16": "assets/icons/icon-16.png",
|
||||||
|
"32": "assets/icons/icon-32.png"
|
||||||
|
},
|
||||||
"default_popup": "popup/index.html"
|
"default_popup": "popup/index.html"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export interface AuthConfig {
|
|||||||
const defaultAuthConfig: AuthConfig = {
|
const defaultAuthConfig: AuthConfig = {
|
||||||
apiResource: "https://talent-search.intelligrow.cn",
|
apiResource: "https://talent-search.intelligrow.cn",
|
||||||
appId: "i4jkllbvih0554r4n0fd3",
|
appId: "i4jkllbvih0554r4n0fd3",
|
||||||
enableDevAuthPanel: true,
|
enableDevAuthPanel: false,
|
||||||
logtoEndpoint: "https://login-api.intelligrow.cn",
|
logtoEndpoint: "https://login-api.intelligrow.cn",
|
||||||
scopes: ["openid", "profile", "offline_access", "talent-search:read"]
|
scopes: ["openid", "profile", "offline_access", "talent-search:read"]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ describe("auth-config", () => {
|
|||||||
expect(readAuthConfig()).toEqual({
|
expect(readAuthConfig()).toEqual({
|
||||||
apiResource: "https://talent-search.intelligrow.cn",
|
apiResource: "https://talent-search.intelligrow.cn",
|
||||||
appId: "i4jkllbvih0554r4n0fd3",
|
appId: "i4jkllbvih0554r4n0fd3",
|
||||||
enableDevAuthPanel: true,
|
enableDevAuthPanel: false,
|
||||||
logtoEndpoint: "https://login-api.intelligrow.cn",
|
logtoEndpoint: "https://login-api.intelligrow.cn",
|
||||||
scopes: [
|
scopes: [
|
||||||
"openid",
|
"openid",
|
||||||
|
|||||||
@ -164,7 +164,6 @@ describe("background-index", () => {
|
|||||||
{
|
{
|
||||||
payload: {
|
payload: {
|
||||||
authors: [{ authorId: "111", authorName: "达人A" }],
|
authors: [{ authorId: "111", authorName: "达人A" }],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
@ -182,9 +181,10 @@ describe("background-index", () => {
|
|||||||
|
|
||||||
expect(submitBatch).toHaveBeenCalledWith(
|
expect(submitBatch).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z"
|
batchName: "批次A"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
|
||||||
expect(sendResponse).toHaveBeenCalledWith({
|
expect(sendResponse).toHaveBeenCalledWith({
|
||||||
ok: true,
|
ok: true,
|
||||||
type: "batch:ack",
|
type: "batch:ack",
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
|
|||||||
import { createBatchPayload } from "../src/content/market/batch-payload";
|
import { createBatchPayload } from "../src/content/market/batch-payload";
|
||||||
|
|
||||||
describe("batch-payload", () => {
|
describe("batch-payload", () => {
|
||||||
test("builds a batch id from the user id and timestamp", () => {
|
test("builds the batch payload without a client-side batch id", () => {
|
||||||
const payload = createBatchPayload({
|
const payload = createBatchPayload({
|
||||||
authState: {
|
authState: {
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
@ -26,7 +26,6 @@ describe("batch-payload", () => {
|
|||||||
{ authorId: "111", authorName: "达人A" },
|
{ authorId: "111", authorName: "达人A" },
|
||||||
{ authorId: "222", authorName: "达人B" }
|
{ authorId: "222", authorName: "达人B" }
|
||||||
],
|
],
|
||||||
batchId: "p7pdhhtde8kj-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "618达人筛选第一批",
|
batchName: "618达人筛选第一批",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
|
|||||||
@ -36,7 +36,6 @@ describe("batch-submit-client", () => {
|
|||||||
|
|
||||||
await client.submitBatch({
|
await client.submitBatch({
|
||||||
authors: [{ authorId: "111", authorName: "达人A" }],
|
authors: [{ authorId: "111", authorName: "达人A" }],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
@ -49,7 +48,6 @@ describe("batch-submit-client", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
authors: [{ authorId: "111", authorName: "达人A" }],
|
authors: [{ authorId: "111", authorName: "达人A" }],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
@ -87,7 +85,6 @@ describe("batch-submit-client", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
client.submitBatch({
|
client.submitBatch({
|
||||||
authors: [],
|
authors: [],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
@ -115,7 +112,6 @@ describe("batch-submit-client", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
client.submitBatch({
|
client.submitBatch({
|
||||||
authors: [],
|
authors: [],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import manifest from "../src/manifest.json";
|
import manifest from "../src/manifest.json";
|
||||||
|
import { createManifest } from "../scripts/manifest.mjs";
|
||||||
|
|
||||||
describe("manifest", () => {
|
describe("manifest", () => {
|
||||||
test("injects the content script on the www Xingtu market page", () => {
|
test("injects the content script on the www Xingtu market page", () => {
|
||||||
@ -16,6 +17,9 @@ describe("manifest", () => {
|
|||||||
expect(manifest.permissions).toEqual(
|
expect(manifest.permissions).toEqual(
|
||||||
expect.arrayContaining(["downloads", "identity", "storage"])
|
expect.arrayContaining(["downloads", "identity", "storage"])
|
||||||
);
|
);
|
||||||
|
expect(manifest.key).toBe(
|
||||||
|
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB"
|
||||||
|
);
|
||||||
expect(manifest.host_permissions).toEqual(
|
expect(manifest.host_permissions).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"http://*/*",
|
"http://*/*",
|
||||||
@ -27,4 +31,38 @@ describe("manifest", () => {
|
|||||||
expect(manifest.background?.service_worker).toBe("background/index.js");
|
expect(manifest.background?.service_worker).toBe("background/index.js");
|
||||||
expect(manifest.action?.default_popup).toBe("popup/index.html");
|
expect(manifest.action?.default_popup).toBe("popup/index.html");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("builds a release manifest with narrowed host permissions", () => {
|
||||||
|
const releaseManifest = createManifest({ target: "release" });
|
||||||
|
|
||||||
|
expect(releaseManifest.permissions).toEqual(
|
||||||
|
expect.arrayContaining(["downloads", "identity", "storage"])
|
||||||
|
);
|
||||||
|
expect(releaseManifest.host_permissions).toEqual([
|
||||||
|
"https://xingtu.cn/ad/creator/market*",
|
||||||
|
"https://*.xingtu.cn/ad/creator/market*",
|
||||||
|
"https://login-api.intelligrow.cn/*",
|
||||||
|
"https://talent-search.intelligrow.cn/*",
|
||||||
|
"http://192.168.31.21:8083/*"
|
||||||
|
]);
|
||||||
|
expect(releaseManifest.host_permissions).not.toEqual(
|
||||||
|
expect.arrayContaining(["http://*/*", "https://*/*", "http://127.0.0.1:4319/*"])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds a release manifest with extension icons", () => {
|
||||||
|
const releaseManifest = createManifest({ target: "release" });
|
||||||
|
|
||||||
|
expect(releaseManifest.icons).toEqual({
|
||||||
|
"16": "assets/icons/icon-16.png",
|
||||||
|
"32": "assets/icons/icon-32.png",
|
||||||
|
"48": "assets/icons/icon-48.png",
|
||||||
|
"128": "assets/icons/icon-128.png"
|
||||||
|
});
|
||||||
|
expect(releaseManifest.key).toBe(manifest.key);
|
||||||
|
expect(releaseManifest.action?.default_icon).toEqual({
|
||||||
|
"16": "assets/icons/icon-16.png",
|
||||||
|
"32": "assets/icons/icon-32.png"
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1535,6 +1535,117 @@ describe("market-content-entry", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"selected export keeps a generic loading status while exporting the default paged range",
|
||||||
|
async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }],
|
||||||
|
[{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }],
|
||||||
|
[{ authorId: "555", authorName: "达人 E", price21To60s: "¥55,000" }],
|
||||||
|
[{ authorId: "666", authorName: "达人 F", price21To60s: "¥66,000" }]
|
||||||
|
];
|
||||||
|
const secondPageDeferred = createDeferred<{
|
||||||
|
json(): Promise<unknown>;
|
||||||
|
ok: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
const buildCsv = vi.fn(() => "csv-output");
|
||||||
|
const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}")) as { page?: number };
|
||||||
|
const pageNumber = body.page ?? 1;
|
||||||
|
const response = {
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[pageNumber - 1] ?? []),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pageNumber === 2) {
|
||||||
|
return secondPageDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & {
|
||||||
|
fetch?: typeof fetchMock;
|
||||||
|
}
|
||||||
|
).fetch = fetchMock;
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({
|
||||||
|
page: 1
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildCsv,
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady: vi.fn(),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
|
||||||
|
click('[data-plugin-export="button"]');
|
||||||
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
if (
|
||||||
|
fetchMock.mock.calls.some(([, init]) => {
|
||||||
|
const body = JSON.parse(
|
||||||
|
String((init as RequestInit | undefined)?.body ?? "{}")
|
||||||
|
) as { page?: number };
|
||||||
|
return body.page === 2;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||||
|
).toBe("导出中...");
|
||||||
|
|
||||||
|
secondPageDeferred.resolve({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[1]),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForMockCall(buildCsv, 120, 50);
|
||||||
|
|
||||||
|
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
||||||
|
"111"
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
15000
|
||||||
|
);
|
||||||
|
|
||||||
test("selected export falls back to all creators in the current range when no selection matches", async () => {
|
test("selected export falls back to all creators in the current range when no selection matches", async () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
[
|
[
|
||||||
@ -1610,11 +1721,11 @@ describe("market-content-entry", () => {
|
|||||||
expect(promptBatchName).toHaveBeenCalledTimes(1);
|
expect(promptBatchName).toHaveBeenCalledTimes(1);
|
||||||
expect(submitBatch).toHaveBeenCalledWith(
|
expect(submitBatch).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
batchId: expect.stringContaining("p7pdhhtde8kj-"),
|
|
||||||
batchName: "618达人筛选第一批",
|
batchName: "618达人筛选第一批",
|
||||||
logtoUserId: "p7pdhhtde8kj"
|
logtoUserId: "p7pdhhtde8kj"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selected batch submit uses only creators selected in the current range", async () => {
|
test("selected batch submit uses only creators selected in the current range", async () => {
|
||||||
|
|||||||
@ -53,7 +53,6 @@ describe("mock-protected-api", () => {
|
|||||||
const response = await fetch(`${server.baseUrl}/api/mock/batches`, {
|
const response = await fetch(`${server.baseUrl}/api/mock/batches`, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
authors: [{ authorId: "111", authorName: "达人A" }],
|
authors: [{ authorId: "111", authorName: "达人A" }],
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
|
||||||
batchName: "批次A",
|
batchName: "批次A",
|
||||||
createdAt: "2026-04-22T12:30:00.000Z",
|
createdAt: "2026-04-22T12:30:00.000Z",
|
||||||
creatorName: "王少卿",
|
creatorName: "王少卿",
|
||||||
@ -71,7 +70,7 @@ describe("mock-protected-api", () => {
|
|||||||
await expect(response.json()).resolves.toEqual(
|
await expect(response.json()).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
acceptedCount: 1,
|
acceptedCount: 1,
|
||||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
batchId: null,
|
||||||
ok: true,
|
ok: true,
|
||||||
source: "mock-batch-submit"
|
source: "mock-batch-submit"
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user