From 37e29bd6b891ba2ae13c6a9eacc9c59c3b150667 Mon Sep 17 00:00:00 2001 From: admin123 Date: Wed, 29 Apr 2026 16:58:35 +0800 Subject: [PATCH] Prepare internal extension distribution --- .gitignore | 3 + README.md | 22 ++++- docs/internal-extension-distribution.md | 31 +++++++ package.json | 3 + scripts/build.mjs | 20 +++-- scripts/manifest.mjs | 76 ++++++++++++++++ scripts/package-release.mjs | 24 +++++ src/assets/icons/icon-128.png | Bin 0 -> 15279 bytes src/assets/icons/icon-16.png | Bin 0 -> 777 bytes src/assets/icons/icon-32.png | Bin 0 -> 1859 bytes src/assets/icons/icon-48.png | Bin 0 -> 3302 bytes src/assets/icons/icon-source.svg | 13 +++ src/content/market/batch-payload.ts | 2 - src/content/market/index.ts | 44 ++++++--- src/manifest.json | 11 +++ src/shared/auth-config.ts | 2 +- tests/auth-config.test.ts | 2 +- tests/background-index.test.ts | 4 +- tests/batch-payload.test.ts | 3 +- tests/batch-submit-client.test.ts | 4 - tests/manifest.test.ts | 38 ++++++++ tests/market-content-entry.test.ts | 113 +++++++++++++++++++++++- tests/mock-protected-api.test.ts | 3 +- 23 files changed, 380 insertions(+), 38 deletions(-) create mode 100644 docs/internal-extension-distribution.md create mode 100644 scripts/manifest.mjs create mode 100644 scripts/package-release.mjs create mode 100644 src/assets/icons/icon-128.png create mode 100644 src/assets/icons/icon-16.png create mode 100644 src/assets/icons/icon-32.png create mode 100644 src/assets/icons/icon-48.png create mode 100644 src/assets/icons/icon-source.svg diff --git a/.gitignore b/.gitignore index 62c1b09..65ceac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .worktrees/ .old-reference/ +.local/ dist/ +dist-release/ +release/ node_modules/ # Local debug captures diff --git a/README.md b/README.md index 0b86272..512e9e4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ npm test 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 1. Run `npm run build` @@ -37,7 +49,7 @@ Replace these before real sign-in testing: - `apiResource` - 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 @@ -66,7 +78,7 @@ The popup dev panel is controlled by `enableDevAuthPanel`. 6. Click `提交批次` 7. Enter a batch name in the browser prompt 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 @@ -91,7 +103,6 @@ When the market page is opened without a valid auth state, the content script re "creatorName": "王少卿", "resource": "https://talent-search.intelligrow.cn", "batchName": "自动验证批次", - "batchId": "p7pdhhtde8kj-<提交当时的 ISO 时间戳>", "createdAt": "<提交当时的 ISO 时间戳>", "authors": [ { "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": "郑皓文" } ] } -``` \ No newline at end of file +``` + +Internal distribution steps are documented in +`docs/internal-extension-distribution.md`. diff --git a/docs/internal-extension-distribution.md b/docs/internal-extension-distribution.md new file mode 100644 index 0000000..6d1cf4f --- /dev/null +++ b/docs/internal-extension-distribution.md @@ -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. diff --git a/package.json b/package.json index 8b95f4d..527b6db 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "private": true, "scripts": { "build": "node scripts/build.mjs", + "build:release": "BUILD_TARGET=release node scripts/build.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:watch": "vitest --passWithNoTests" }, diff --git a/scripts/build.mjs b/scripts/build.mjs index d6179b1..c6d01af 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -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 { fileURLToPath } from "node:url"; import { build } from "tsup"; +import { createManifest } from "./manifest.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); 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 mkdir(path.join(distDir, "content"), { recursive: true }); @@ -68,11 +73,16 @@ await build({ } }); -await cp( - path.join(projectRoot, "src/manifest.json"), - path.join(distDir, "manifest.json") +await writeFile( + path.join(distDir, "manifest.json"), + `${JSON.stringify(createManifest({ target: buildTarget }), null, 2)}\n` ); await cp( path.join(projectRoot, "src/popup/index.html"), path.join(distDir, "popup/index.html") ); +await cp( + path.join(projectRoot, "src/assets"), + path.join(distDir, "assets"), + { recursive: true } +); diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs new file mode 100644 index 0000000..1de02bf --- /dev/null +++ b/scripts/manifest.mjs @@ -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 + }; +} diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs new file mode 100644 index 0000000..96ff8fd --- /dev/null +++ b/scripts/package-release.mjs @@ -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}`); diff --git a/src/assets/icons/icon-128.png b/src/assets/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc212df24b0bd77fc9a1ad8f74a78fe74955b48 GIT binary patch literal 15279 zcmZv@Wl$YV4={Rg_u}qQT#8$9ic4`nxVyW%ySo>6cXx+U+?|8N0q*nuxp(e--;Zpv z+05)rvYX6ilSm~6DHKEkL;wJQA|oxX@*fWPAHc)@Hx3CIP5p-;oK>Vm0kyM4Xa7ya zO*LiALiQ2L%5C003100f71sL;R0d0Qvu}1{6U3fAs$ZrQ@L8 z0RVn{8S$TL9uQZ)p53{>U7v&yWI_T8jFiL&g2;;0Bgl%ykfdKN?ywq)R8BA$(&~;> zmDIviT2e`ea7|M>K7PVT3`dD!D}6Je3&uQVg@&LE1SEwV`pwm^`aO4aJm-01MfpEm zd(U+}>kl>MwLJOl+~@7ll*+YEAj+v5HbN$2XVmnYz|b(TY6brPS)b)<1i$(f(D45iOF$2x{1D>Q(g$v|@2j5OMg%$@Xxv$dQro(+E=|@UGX)xBkk)tN(zGF? zJlfGu<@bV*;(PWt+tTmT7w$K#ie7h!FNfK^H&am5|2j|3r??lRcNJ1ubI?Smg?@vB zS7XLjPeNa6#64<`wc2778Mrn|w9I50qo;-(duWL{3tvI}JTt|zOR^a{P~(NwwlJiJ zETjx6f|k6Pb5wi+d3;+UHS~Hy!cpiuq&&(hjr`bI;kFdKC!h6LaBIrb=X(?>d2*!U zVu+seDZWJ9#71$dRZ#7BD?A($8_H6BMMv;^Do^M;`^VJ%&1h`!`SowCwAHW*4+urT zI)?VE9wD6|(b2&**~6skfsE^tg0{n>+t5=>$o#C)bd03uGz}*Ak%Qmr*XuMye*50# z4|N^BD8B|E5#zENR7`%!{>S2HWuOp^Pv*^8cX*wxsZM}SKU?2l+#i^+}}7L?2P4Wg0YEDD$5A~ROt`+=a0v! z+w-*Xx*qouKkxC+(BCpG$84W)In0FK#x<0Y^sj##o}3_Hopd^`p0PzY<%4V#c9t8v zJ9Da(+GQw3UR=}H#2v_eJ;U4N51e8`Az?ovXR`%!L-DMQqybS6*5wqeb)VYu_CdHPlvA~PmRi$fOfQxrx`imx#q%rT^zajkkwgP^y^!zCeabM8r+A42DOWazLxmjV^qsR$f@Xniy)WgA5{qS z>_`nnt7Qw={D{yEs~c97Kb4S_4_KjILL}eF7Due3^B5!J9gip!XP9n$@B(9Tir?2o zx$rQ#&?|3wUdX9tJ)g?Xfr8oc!K^{uxY8v+A+IV!AQs*lL#( z=>!2v(!k=@z?K)wqp`S(JIU#ln@!h6)vectr#qXI%zhxPeb%zz*Ow_7OX>CD@p9*N zliO78+h0-{r817i20GdRV$ke{3*}p|FlLeg|JFNeY*;c`dC$2Fk1!AtqHSa|+rVe0 z{I!=$1K$eeZu<#)*-bY_cH0^dQNOeWVxiE9WH?AW+GT`l-X298&D!-^q(HS|j2Yg2S6I>xoE+Cb4iw9WTo+ zer$17SW;rI{i!1(V&?n}MGLiEg%m<0dz7dH)OwrMFyu+*A&&w_hv>jf%;LGeQ8xF@ z2ySmcYeynKhPMBO5pFA}4E@h&DCr|&-rf2Xh3$|`oS07C-4_0sCP%};a%5)u;M(gh zh=l^~BrYZSj}xf4!V)u-@i%BrX46=qW3HBFVHs*&@kc$9>oOmskK0R(9PPei4Ai?v z#XP2}LSgp)ko4_0;C#gu%bhfMoPoIJ8KVY!5R5Mag107w=TI7M(X$%L5N(e0-*78G zyD)feqDGr*G4mPoYlaZ=3b&tVHn{i9FT2{emWvaaKIC`8{Jt?8?$-KjEQ%#Dl?R*2 zb`jQuuuZW<C>Fgg#pNXH)l>`A9FDz>+h%2BpDO^h+H&ery*BBR&-Jfc9{Pc_4O zYT%CF`%UTXXb0e7%o+G-JDxTA-lGj7FoFIAJrVHs4!#1YiZqSV=UfElZtz zk?ezAC^gOT9$8T6Pm28LJH%pp9pF3_^uRZwNutAtmR4nar0~KR}Wx};q83x{Z z+A#09SY?F7jD00k)3_sY;1q9o;02smxOzSz<3Whs>U0aq4u{d``B)2t*Od1=M-~r_ zj2sWH7!DnL65*KLL<&dDD}&A(tm&qxLcDIy!y0vCC^}v2m28&}Wyd`lv&f}R0=7eE zhL-q9c&^tXB4D&oer;L^_$yQfWImTBl=J(h2HQlm!7@gWjW0I=iHO=ZiA|v7t#s8I z%c%IX$6buA9%TyvMkU4EOf6BPYk;sb8Lq@ZhNl13g1gX!J<6))7~H_OXF3rxkzw<2 zW5YN#VhKzV=^q!`9TKh&Str^>kN80gWH)0kh#i{^6|nuC$j%CVHkYkuycACOW9-54 zlc>W~i1#L1o7>;fa{`ngRG4UNKYn{fSe_B zbS=wgDaExK$&*}w6*;!^jHu+;a#b3@#Wcnoin zYQO4sh;1*~JadZ>{uPvGW2Fr3_GL(OvG{~gZ24D3aUfkW z3U|iCKDp5gkSK$wg^A@Ir1%*$lSSh{mdpEQAQFr?av2kZ{r5)9~(!|pc->fGTRk!{1!JbSm)xEaAy>q@@!+^$aw*GZV>l(=j1fZWmq;&|Ta=1G?P{Ws2a1=d zNhB|?+lOmCslon3pwF9zwlM(ci;=R~a3=B5{uB+0bUF`E*|%%&*n~{Xg9>re>3>Fena+v^6!;&Nx@2N_iYrP?(pSlI zF z&4&&U?rFAQY7;5?l_0Pfjuyu>LUzjv5C-b}c?kCsyegk~>=}cXae|PHM+N zGx9h^%;P3tk3D;a6OZpll3X=Kp(Gu}ovGH*ZmZ^_W6jB_;Nc80S z1!v6z5t=TA^3arRp-Sbs0!(hd0Vzag%*Q+n6T1c_-_O}#-b_VTr`baZs-Pci@QFWU z`eJa)7R&X{L&V59?V#h52w@XUWz|+SB;ynTHG0~yt7uyQc)r|O1xP5?vptzj@df9? zX;t<~j!d@(Fk2PM{jZNGH~^4ey$!9CXWwus3^(T}WBW3=+(8wU?eO@t+eU_0 zyPu9J_UZ&oj-Dp(S&|n2*tT9W?YE|H+Qe<6v$}2JTQynyEU0GpKJokL+__hkB+I+a zgejna;-`dVeNRoU$Rm`cOk=pcMm}tw801sbDl;dg+c061ukXLB<`4FS2#6eRD-7|^ zm@$eob_HNUO*W03Wya@smvxyqHnz>xaD44flihQS20s6|_Lr9d^z$frg;lpk@ozi* zlL99H01rlx*5?nFrtvI(MK-5QDZus}cN7%GBa>XUbhTUGFueBGozF!1rUHvJb89yl~na=o@CR4K!H zTY%s~^uzqa*cI&FrlX2uS`MQ1dq6>qM5Ip6E3d@P#wpJ19lgB`J4Bs(xlx6WP?_g7 z9P{VriC-yrf*IJiGZ@=2^uD9L>c261MEL$8$OZ9r%rR?5HA*^qn~1Tl>)5DNRlj7q z6P5}!ZDlv1NlQz9I6iP{d&tB;oi~Ekxp-(?UqL{bFgkA)?iAOc8X**cB_i}w7b!TZ z%=X&BQ-bU)%Y?_>CKkT3!%HHq-%((gjZ4DeqA4|YidJZ^xc#A8;8eoQ@A7U03YNK- zXutl9xoIjd{OeB+|ED3W!j>WUPLe}ogxUbMM45AB>?fYFSe&|<;!lu5U*g*UA=!Q> z-eH_HxvT<4*S<4`<-WWa>9oZ#vsc_jkBF(Qg*~JP10eDAr2Sg2;9Rk%hA8<{0BJE7 z2%^bnjIxAwJ5bdBM?(egOB_$mj+%ep=6j<B1O(IdVyO|S}+*F z#c}x+BKQv~fDdtgIuOXH{IYI9@w$-W2114R6r)ijtWakQX_Ee)eh4sPN8i45|EyEa zzqjPqK3E!PZPzk!8M%i)Ru)bY^>FLJ;4r{QP}r^yNcF!GqYXG7t@J~WUxl>;sIu{5 zVpbqVLcQyHm8brTxw&R<}LYBF%1Hc}5-G%2U%lVXy2ae)K-3^oboTd{t z&XM|At&nm=60y*T#!Bn?tlK_~Ua1J|`8mHriuYYbCKK_E*R)v=@Hv^}1C(s(sq-!e zb34$ipzI+L5=3h7r#7x?>@JlAl60ZN1hdVEP67yJ5TDhJuB8V)Xtdr%wbcB4H%-*`{nnrRO#)Zp|X@c1vz*y1gQaE4nV5q}D9g_oqDW2FnD z*-3`fZ52w@Q56)#9mxHdmt^U)E2;d(E6nmM?~(aiI?s2uX{mP3#wGyo z1q0XdnS$MuV;sktx8KH1Gi6uMl3S4N-Upi3PYfO{c3MI7cpsI-yBTgG6T^4SoH$_@ z5Qa!myn@3nELj}FEBlZr1{W^*sas6pN|+3xoSOX;d9v^*r(6y}xD)4FNu2K+U0%JR zh#e&Dw>zffKJ(E*HeCHDP;veJn)qtWq1h#>`(#Sx5izZQVa>c2bo41nK7r}+*hn}>7@MY&+T6MyiJm_4^$U2ypn}6}&ICU8%c)(R)qXqOKjW4*><;p$L{f}Cm>$U3381K{g-eW0@tL$qe;S1CeDFhhQBu5UJ7FQ#tF zAv#mdQ=DXEC?GCm^2iY{m1z4#RtcdlIwSkMa{Pi4`8~S=hc@azkd~d+2EHFaZAKjX z%iLF{>$2`RAk4u*YkOXyUJk+VCE1(d5O^T2q%aLdK_cj`XCeApz~ul1AMmr79=*UB z%VR(kpI?O|RtM48uIPdGXH`=tRA{eg z#Dg(S!Ao_6la8a@a?;;re|lB?{p#SmFTXCA?bB4Z9hJ03(Sqq_bR~Y7rH!Cttw?o? zjy@}@k%H_&<=Sp~$~jUqeL=OXcphkEjePAM#CPc5`9^=nEi$@D-b2E8(uLm_3HcJJ z8DR#SOZ8Zdld4;ex3?=BUJok=8p|nVDbw^o4hi`cs5dE&L#3CgdZ|wS?eo+0{2V>- zpNaSXQ!LP9g2018}$y0 zckT?08Q&7H(*K`csDh-;Mv654II#L^c1~uM%W)~i9@mXbkS$>4O&tC%)9L06f$=-L z*e?uNxCvGtpagLs8-M%<@>&r1=2pU^1~ynE^oI$GsmsZuL!CD6kv(t@ zE9IQdj(%c$U#sYUO*noy#^yS7zueQN@YlaoAt`NSV^JhPW$`)|;G?F%1&3*53$G*+ zfXZ3^d;hx+DPL=Av(Q7uZo=9HJh)I~BwBN_2+($vR7QeLZ1ix}D1t5YD*A`ipmHYe z!d0s{AYr5EBP&a4`Gqe{Jg3IkN{#}Va3r!3+20a&IK5POdGmag{hfS;`$&!%ub7&FaaKsIOTim+;nW}0Ni z3{9@_d85-Rkv~;1nGwpq?asHIrrdGAvWYbND!%Ayq3vwp+j~3h0gqyyiM7+f17A)I z_(#|h=19lgNh=?xoMffcq|@1hN+zEP5oPj&qpJc{GPtnlRL!4qH5~OxfDqCmT>+j2CU9EoicbYOj{v>o#_Ce>~rF!}^)*Z&pb-^V1!N-A}LUHoMzImYdnv@sp{WlYjXs z#p?Z21fF!PDuyyC!f!cKT38nrIa`9O$YQHOczmNoKm{l{u?^QBVQ$M@9#8yhrx{h$^M0%`e~jn-_(@r4 zK}7FQ{3DW=t!ee2c8S3iME^Q|-=q|ti?WoG%EQWueXu)Oc3*68Ovu?dakVVZSxaPT z4wZ>5X?q2IWdE`#r<-`0qWi{;kN(@+#!>ExPo><5u|o5Q-!>TZLD{Qr*ZWfb-FVdV zJ!fdr`f0;<%Ry1_(}dUU87*b!pI^N5tg%!cV~o`RBzYaK%`2_x{aRnE4QNwi-+C`Q z#r0O%sdrElxh51%K$oQZ>e^{-g*#1$`Te)&F1I_~*J1jw4@LdgMW`y%1~i+Iv@{wvD7n3x6e@-rZ8IcI) zUbk8T<=GQ!L`$WlTDt3b`gR;T)26)J=jgbBZ~so|OYK^39@reZ9>1f~bMJ%l|2UNe zxQbdWt!nmM-g0}VU+p}mAHn?R{pH72gjt_!LHA94dbK?FI;M9A{m71>wqT89QetPx*X;#bdMh0hyVYn;tt#2VDLUT@Rt2X-)GP&}Xy)-G#z#izn_?CgIG- z9m^E``vG3s=rE}kuPw1 zB%xN=q+}G~+jC@y$Wz*9G(D|3dhDBH@2g!<UrQ zpA%IAb&%QLnOFhDG9-c{5KIxSiw;vWc3>d<9$pz`&b>qZU^4r=Yt+UuA_l!gP69# z%&)iu)ZBUBkO1}le(%)hJO@qBm23Xs@SDu>;3F4bJ@d)+t?G507$>53i0@>@MF>gZ z{caAGKJ%sIGRh{xUDi9Is}RzJnwC9S`fc{_pIGV+1U7UPB%)ZnC6lse6_=R3Ae+Lpj(#yq2TuHy=K%ynv2$;)F|B@TKrR+D`{y zj)nz=wMjZ|+DwNuY?Yq`_OYtLSO56$obE2{KTP)01$?NTd#7XF-{C%XmmP&q?xBhe zj3__eq%_jJ?K@ipp5||0I#xU9SXjJPrY6<>dx4%;LS5;ZFK-(jn}Xb1DseAO&xTDb zO}ur#7)c1?YfQIkbMC^s#&iz?zmg8^s{3w{_$p)3UWS42@zqhtbVGF}3roVSL5v;+ zLhd!(as&C=bIkVquSlko%Yn_`QFr8cZSxgVckY<+191hr3v}=8(^KH=JF0D(AJa@9 zTgJXn4T6NCxXl%YxxMqeC_c+q>FRlgEYi_#boASH^eu&~`W`Arj>2hkR296x$dBG` zK1&2##srTlB^38Bj9&}_?>x@$XeWc+w{+~@2Ii(r%MA)7wxDz1dN}pGgI%Yv*45ng zotuoSSMf~kMd|o9{RMk&qx0f@npej8Wct89x)2oi0!@GarPX#sc&aqj#X%%wlSR^? zDuzEAG+27>w9!6EqV*Yi9`k<{ZUx%^C5WDzqnYwMZ?-f|kNJScx+;V_H4x&J6n zXJm5zJU=ki|CavxyBANW>y#*K9C%5l5Fnm(xomBU_;}a(}0`IVd18aoMjEwTD zo81na#pF2joC@kj`Iws?_Y4_2@W|gK)$h^J0z+>zidK6U4djmWV z5~4?hFq-UN$+>Kp7Vw#ib@4D)U_@anPx>nFYn}Tz3;8WU?YF95YS-;C(K}-zy7+GR zpVV>rMDA{{lNV168u{LL?bm3`;0KMK7idFvFOxvo4WqA~v6dHujVq|5?XSUosGx^N zJ8e26!X3Vt;+d{b?xWLx+d3qfUrJGumutg>^7bBcI@Jn15o)-?K(**ZI4EwBq;&qM zLgE+%EScQo{d;t*&_^S)dEtidc?!!f{brF;k#X(oy_A@%9Y0aR zP|nHmaEw%2>61A7=;fbq2#bxrJq0C^zt5%C5w5xK-{@N1T}9*|HhM7V>R$r^%e%lm z=?BxNqLdMjzY``PE={#^hZ*ktw&}L5kKxPytB+-vp!y1Q-7|wG^y*T>k^d@n*#NF2 z?1qRa*~k25f2`IyDb24iAU#+_fR(qB1x&gxMF@Mz#lxk=|Q72-*x>U7Vc_g95_mj|H~$H^$#vxHtvn@rSpvc1#gvC zlEvdSAKAzLYs}z_JO95Fq7y-&)UllqK4n|T$(18X9v}0^X2&0k(7Wh<^Ws1H$0O=F zW)PfmPYmtq1MS=Jw3hnX;`+6jDWl~AWJ8g3rb*QaAAFW5*20La3v=NQ~wl4_VTWm6b z)7?h3LydL}rv@WYR)(dH@7YiPb@0fq?gfsUD#9sGQvNN3#smyuPm6&G+?S|g(ZdWx z?JQsFUpn6dq|Pf-_2hVt=V+SRijFE`SswGWA3`2x{{c$KzYoa5?%U(E2C4L2pM4Dj zc+*`*<3g>Q12_&ADugCAZ}<4ug$V{Hw-gjsx1nIQcG z8|R9QAv{M~{V!(i^??eFx{3{T9|Avh-HIH8**<8IvOr@`kjfL{@^`P;K7GamEt&+JtABlm z`PYc=``t2yopF@)`Fu8DzgwW zpIt(k(W&ZLtzK4DUEYjm%lrNc7|J+DNy}H8(6?qNXHEyV4Hdx(neoKh{!N86@|A|= zQZ&zukt*45!Fu#7k^Vm=0Q-WcD4+KYRsc436~k(v1dY4_F(*9ByQEeKPFZjH_Lrd- zYmAB#0nnU1Xxuzxs3@EiBW|!?%5NfE$ycaWGRkCinN2Yx6zzjqUh*=(=~!+rNm%S@ z#a}$$Z*P|&Ajy1#4SfK9&|*>I?%nY>{Hqd4t0&gFL4;A^L-0Qaed*k+&Wj-#Go~M==Zv02WTjbiBk=9+ImYlRE_FRxI`hR_Ug^uFVnWDqC21 z50uA^5(@SuiQhGeGJIQKkAl(bsi9?g=;pwME0PF|Mc};}{%3lh5k|~x3KrVcjJ>&P z!cWg0kF7|1Wi-WbW_~}1iZ_b>+A`X)LLJ}yfUu{vz6r?R;2@Qn?Inn3#cPMS#F`M_ z`^|tED}t@jn8P-Kmt2_T>*u%cNsrgk_fX+e9FE``3lut!Ek!gF4EsSdclhKaP0Ua_ zn5;>v@jexBI*{IK>;29CCT@jm5RU&LL1VNCYRt*yvnu;xHB=?X3Q{TYOIE+5LqLjO zAZU)sYMP4qpzO1lS4_wQs-hnr1EIq~x{zKWE;S?SLKF_|ZxuZA(3b4o-dKZq{I83~ zgE{(i;yLc0yCvZt=Rp)suw=2ekc* zUEmMDDh)@Ces?)X>N#c~ht|9sq;Xg&JCMd+wg|8V?&St`d`KPNfIZD#gs>d~Qy`j? zNYMl%q9?zY0|LRSNM;&IdlKzVPYL@E)!9;vkw(^qV>}-}VzDXgXE$~ptFsqPhVw|zgv|eBF7n#RqG3WD^b3&; z%hxi|nN|zC&oq7r+1rgU#MpP8!@knO?g01NtDg~>eS;i(5O)=pySOj42l1TAyKjB6 zao3FwWH~27lzL7m;P~|sj`xd|`0&?S|DcJ%FXcX3W`6qp^{m&`(#Jo~8hYE}<6!#jUCfg(Kf9Nhi=JLMEYcV|Xto-K`q?;W28B@4T?|#nSd(Tf}R$YEJeS`5X zOWe|xBvp~3*_5vh^mRrBga>H)?q4rmVKVRK&wB=hUC(Q6lzm|bAqZPj|!GWtyz@p>+d5(bs zErpn+mDtV%%m4kk;7}sa6qx*i?i}xr9`w|9pOC?seSD2;XQvi|Q>J5|9fVdN3CVH% zW}rWcE_YE(5u#TL^cgz?hs^N6qAD2&kXeGDl4L9~pMY zEic1lZMVSnX;Ty*mVMiN{sN-gSeDp}x)hd|MVYtBEVdX~kQT$}I1h&1F!LgTGVR{p zMR(^mGj%%-{Am?_4I|t)Kj-PjNOa|9G8EzsWd-w3w@#u;b5Rg(N(}E@Hc8bw=r|cT zI+tC0NZ~2TCW5sf)I8DM*yc}(hU{m4wUdYAaWJlZ;g}Ek%cO;sSrN;T>HW31??gIj7wVb?8XT2xsN#APvxR|TWZ|eDk z0B_SU&XZ+-yH$SXsxFgTv@3|o!Zr}JLjan`=l(KBME5tG@B!JTx-?;HLR`3t>$Pnr z3zsvZF`fAeCZu3FMV*E4IMZF?DvMeCEfgWV;?86erPLgfD~spla|9~+EG~D9ELy0? zAcSnzT@WKNL0UIn71y%ti zT82MRI-4RI)sDU&L9XOMLy1tec!b@L?(#&-3sA6!9bFpZ^&FJPsN3m#ylW&HbM$gI zcV}rZHq>*jv9_0K5~jcYon!Mp!aeb=o8I^$h4oYV#+bLDkiDDTjyZjt8X(SLzlMrm z^9I}B=jqMjyh@oC($I)}`DkdwE2;RmqUhwIDIuyCR{jJnz>7K(uQ2s@^d65Hxe%M& zI{~y)*eYcRCyH}>(vK73_>##O_PDqDL@^KalQ7vjI;n2Uufe``&sIY2fkIp#CN7Bk z>FNHGwF85xmD$O;T zGI359`9mM_URK~DGL;E0f=K19P>dKMu>B?CMEAUeFe{_oCh{JA9_Z6J8$@4qgas$)hv7REdDI{7r)0BXb?m3zgQI=ncy zZMwat&}*;_)TS^-kU|%P!4UOgfEY~{7Ii}7Di#SvJbaPlNdvR=Mr$)}vDAm#QV%j@ zb?Ga1fBn>CqZ<_Ku9U*U2_;~_A8V_)lTVoq&!M}T5_aI)v}yuHad+3+MEO%i8SEaq zzj)xzjix(;k;r;$`##PIC*_5Es=j)B12Ek=Ui@I^9tD=*T4+i1)Pz3D+|%G3BnY&( z|6T``KF7TxsFw)vUG|#pRO-}zWW%gKTbm=KI(UfcvrK$1k_`mnROx}QVHsQWU1s_C z8!Nry5T=_JAgXFNrS0>ZOA~nl%-hFMNF&`Zzf}*=mN8X09!x16TiWbz{JIMzeHLcr zcJhy5Mf+lok3xg8CGnySRC1d1*n$rCxwk&pgzf{+#!sg2e7MIe=sA7W+~46k;hUHE z?F#n49{E6m=lvfr&^w>lCO)4(d}VBgTuP@yl||itm!>+n76wU}ED~5TY9Xq_2Nex4 zO&ew5CCqbu^J-I?ve>d(Fjj9#6+2mg##%n2kE8Sv_9lGYB9k=rmulb~ZVUu<@% z%cc(+9;9chKuDZ8yG+waZ(e0KzC)q-ffLpE!bVw*r|e;LFw$1a7zZjg`>7Gpgr|;(1#|8y+@4SdnO|cO@B^fNkvl$jY8f zJlKNnC;VX07>?npMY;VOy2Jue2}3bGQh|UU+MW4JNR)t_(R?L0V(_+>I5 zp}OEwtb$}*&p!J+VI=ZY>zfz)k7#M#EP{N#xD^EcFcL9xVj@s!W)xAwbZGYQT*K+I zNw8xq#Cufs!08B^tD03q?06-KQoVUH(_8oc?@LK3elD*wsZYsMW24#(Ca#|SlK#^t zgMSqa1Jc<6qyoO)vs&d2Gp9c)P<-$+0Q?Hsu27#J|DgIkJJll)v9-fFlcgngH%d9C zd-8*;wZ6^vemC21n6L8Kh3lTeh|Yu-O)kW4C`Y+XJ3xd2~z&XLcPg zPEE0qf*-Kl!TZNO_Vm$U7AekKN)&7$)rfTC+@FuhVuudC;R~_vwm*$+9Cs)d^^Iu? z9GS~cIw0ke+_J*a%Kvp3cIJ5h1U4Kl#SBTY@x}$#|9Fm&GCq=IV-8VHJuini(nH?- z?tgA#pL;tvt85sy8s~ao{#>STX7ByJKHfmUYmCTpPA?yGS2?Y`qYvsg=f7x>I`Bf2 zj;Q1?Ckh>$yVunM!tY}aCyMi!W6Yak(1RfV#yS7Mm`Je_7qWt$1Gg9xz)WwY#vb6t%uooM_<-#D?IZr-5#SaA!O=pGqi!8Qqy)C04 z?s&PT!XOW&5zQYn87#g_#g2O7J*xul{2EUKOZgQIzGn69EriE_D#!i3IMFuJ$nnkA zW52n!;GA0(NH>0_Nfi}BtBS(diO!xsI zFrLg2IJCrc@oe7M4T@$=@s?xg9lNM_1m9nFe+v3m+?U)J`bSg_@}TqiM{HE(j*cO|kInQbd3I07`8 zW9IN)RU=k41Q0VA?|5s>k*+$;1Z~MujZQRtrCB`}(4IF>dR{ilb< z9;J29OgSbeR$NDA>OGHbj-y)mhWd$(Gwvp4)fbOqlkQEVTp@i{TdsXkQ9@9XZB4mn zrNyAh-pb+_2OmH@y7o@B!JWezTd*E^3KcqF5riDKNj#?22FKet@2x4+ctO+)nPd%#au0 z;AzajaqJ=66&RH|)sGtR*Hcy2{sakkhxCh4W?i8^+5zi}H8=ysK%)@e?j2+VB^bb_2 z`lOx8VW8%iaa$eWV)LHQq*rf|J#vvFbj7P`PM-K?Us);LQF_*Y*ksH$O(9J*GSLvl zBFsV%t_2%ZN_=E>wGX_MxC?9AhQTgnw98lA4RK`ld-M7k1o1JLBUs6=A|0B4AdSp* zwuC*zGgzQ#=(RLVwG|mZ^|_YDs1Mw^VeauZ%Buaf!={6sGWuN5()z-HE{IwbOK&F1 zOoh2pTRtsOqK{Om%8|6T-dcFgkEHE==4f$e4M9H^jNn9B;einzuu{7Lm{>`7)Hxl8 z!VUk#D`{iue2KxMX$cR`ZyY_?0R`pX^7naaf)(|fJwDg{bH)R(Eat|s3u4`ml3<*7 zy01rR1zSAW&e#9KcAhOuq4ZE_<1ou>$VJg9M>kC=661878Y!db`QTQ;%&YV_a4Zih zQDE5qnUZk%7Tuv|K}w?!&s+=UmwS^Ypg6Y$#?BcRX`zaLpI`UR$)2QG9dg`Ig(Zmx z-hbXVQj6VWoMsDz;4LRDkaeXllGd+Ina|)6@S)G0kV={PcsUHDlqOB_(cL}CJ?%Gj z8xbK}EKSrb5qsAd`=j?TbN_tY?6o#LE82*@l0@y1 z)UHP8DEsdIE0GouoD0%r$28epVb!^+F2@U3U_8k?spD$r6W(42NF6i zRJyVWVha6;BzC;#P?XSuUS?`LP5M%)G+A(0W+a6H~;_u literal 0 HcmV?d00001 diff --git a/src/assets/icons/icon-16.png b/src/assets/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..b2d62c6b63dc42f64a5c68d4ead0d02c463b1c9d GIT binary patch literal 777 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 literal 0 HcmV?d00001 diff --git a/src/assets/icons/icon-48.png b/src/assets/icons/icon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..ed5f38f2c26de7401bf08cb7def8fc1ec0d9b0a7 GIT binary patch literal 3302 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 literal 0 HcmV?d00001 diff --git a/src/assets/icons/icon-source.svg b/src/assets/icons/icon-source.svg new file mode 100644 index 0000000..4c81685 --- /dev/null +++ b/src/assets/icons/icon-source.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/content/market/batch-payload.ts b/src/content/market/batch-payload.ts index fbff457..a6268c6 100644 --- a/src/content/market/batch-payload.ts +++ b/src/content/market/batch-payload.ts @@ -6,7 +6,6 @@ export interface BatchPayload { authorId: string; authorName: string; }>; - batchId: string; batchName: string; createdAt: string; creatorName: string; @@ -40,7 +39,6 @@ export function createBatchPayload(options: { authorId: record.authorId, authorName: record.authorName })), - batchId: `${logtoUserId}-${options.createdAt}`, batchName, createdAt: options.createdAt, creatorName: diff --git a/src/content/market/index.ts b/src/content/market/index.ts index 390f717..ae6d8ce 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -79,15 +79,12 @@ export function createMarketController(options: CreateMarketControllerOptions) { options.submitBatch ?? ((payload: BatchPayload) => readBatchSubmitAck(sendRuntimeMessage, payload)); + let activeProgressLabel = "导出中"; + let shouldShowDetailedProgress = true; const exportRangeController = createExportRangeController({ document: options.document, onProgress: ({ currentPage, totalPages }) => { - setToolbarExportStatus( - toolbar, - totalPages - ? `导出中 ${currentPage}/${totalPages} 页...` - : `导出中 第${currentPage}页...` - ); + updateToolbarProgress(currentPage, totalPages); }, prepareCurrentPageForExport: prepareCurrentPageForExport, readCurrentPageRecords: () => getVisibleOrderedRecords(), @@ -97,12 +94,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { const silentExportController = createSilentExportController({ document: options.document, onProgress: ({ currentPage, totalPages }) => { - setToolbarExportStatus( - toolbar, - totalPages - ? `导出中 ${currentPage}/${totalPages} 页...` - : `导出中 第${currentPage}页...` - ); + updateToolbarProgress(currentPage, totalPages); } }); let activeSort: MarketSortState | undefined; @@ -155,7 +147,9 @@ export function createMarketController(options: CreateMarketControllerOptions) { setToolbarBusyState(toolbar, true); try { const records = filterRecordsBySelection( - await exportRecords(exportTarget.target) + await exportRecords(exportTarget.target, "导出中", { + showDetailedProgress: selectedAuthorIds.size === 0 + }) ); options.onCsvReady?.(buildCsv(records)); setToolbarExportStatus(toolbar, ""); @@ -477,8 +471,13 @@ export function createMarketController(options: CreateMarketControllerOptions) { async function exportRecords( target: MarketExportTarget, - inProgressLabel = "导出中" + inProgressLabel = "导出中", + progressOptions: { + showDetailedProgress?: boolean; + } = {} ): Promise { + activeProgressLabel = inProgressLabel; + shouldShowDetailedProgress = progressOptions.showDetailedProgress ?? true; setToolbarExportStatus(toolbar, `${inProgressLabel}...`); if (target.mode === "count" && target.pageCount <= 1) { @@ -499,6 +498,23 @@ export function createMarketController(options: CreateMarketControllerOptions) { 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[] { if (selectedAuthorIds.size === 0) { return records; diff --git a/src/manifest.json b/src/manifest.json index bb53cd5..d0aafba 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,7 +3,14 @@ "name": "Star Chart Search Enhancer", "version": "0.2.0421.2", "description": "Bootstraps the Xingtu creator market content script.", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB", "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": [ "http://*/*", "https://login-api.intelligrow.cn/*", @@ -11,6 +18,10 @@ "https://*/*" ], "action": { + "default_icon": { + "16": "assets/icons/icon-16.png", + "32": "assets/icons/icon-32.png" + }, "default_popup": "popup/index.html" }, "background": { diff --git a/src/shared/auth-config.ts b/src/shared/auth-config.ts index 99fcd5c..3390c2c 100644 --- a/src/shared/auth-config.ts +++ b/src/shared/auth-config.ts @@ -9,7 +9,7 @@ export interface AuthConfig { const defaultAuthConfig: AuthConfig = { apiResource: "https://talent-search.intelligrow.cn", appId: "i4jkllbvih0554r4n0fd3", - enableDevAuthPanel: true, + enableDevAuthPanel: false, logtoEndpoint: "https://login-api.intelligrow.cn", scopes: ["openid", "profile", "offline_access", "talent-search:read"] }; diff --git a/tests/auth-config.test.ts b/tests/auth-config.test.ts index 2fca477..4219109 100644 --- a/tests/auth-config.test.ts +++ b/tests/auth-config.test.ts @@ -7,7 +7,7 @@ describe("auth-config", () => { expect(readAuthConfig()).toEqual({ apiResource: "https://talent-search.intelligrow.cn", appId: "i4jkllbvih0554r4n0fd3", - enableDevAuthPanel: true, + enableDevAuthPanel: false, logtoEndpoint: "https://login-api.intelligrow.cn", scopes: [ "openid", diff --git a/tests/background-index.test.ts b/tests/background-index.test.ts index 5d166f9..64ab09f 100644 --- a/tests/background-index.test.ts +++ b/tests/background-index.test.ts @@ -164,7 +164,6 @@ describe("background-index", () => { { payload: { authors: [{ authorId: "111", authorName: "达人A" }], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", @@ -182,9 +181,10 @@ describe("background-index", () => { expect(submitBatch).toHaveBeenCalledWith( expect.objectContaining({ - batchId: "批次A-2026-04-22T12:30:00.000Z" + batchName: "批次A" }) ); + expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId"); expect(sendResponse).toHaveBeenCalledWith({ ok: true, type: "batch:ack", diff --git a/tests/batch-payload.test.ts b/tests/batch-payload.test.ts index b93d8a0..22ab26b 100644 --- a/tests/batch-payload.test.ts +++ b/tests/batch-payload.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest"; import { createBatchPayload } from "../src/content/market/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({ authState: { isAuthenticated: true, @@ -26,7 +26,6 @@ describe("batch-payload", () => { { authorId: "111", authorName: "达人A" }, { authorId: "222", authorName: "达人B" } ], - batchId: "p7pdhhtde8kj-2026-04-22T12:30:00.000Z", batchName: "618达人筛选第一批", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", diff --git a/tests/batch-submit-client.test.ts b/tests/batch-submit-client.test.ts index ddc8c7e..fbef987 100644 --- a/tests/batch-submit-client.test.ts +++ b/tests/batch-submit-client.test.ts @@ -36,7 +36,6 @@ describe("batch-submit-client", () => { await client.submitBatch({ authors: [{ authorId: "111", authorName: "达人A" }], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", @@ -49,7 +48,6 @@ describe("batch-submit-client", () => { expect.objectContaining({ body: JSON.stringify({ authors: [{ authorId: "111", authorName: "达人A" }], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", @@ -87,7 +85,6 @@ describe("batch-submit-client", () => { await expect( client.submitBatch({ authors: [], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", @@ -115,7 +112,6 @@ describe("batch-submit-client", () => { await expect( client.submitBatch({ authors: [], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 97c0bac..80f8694 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vitest"; import manifest from "../src/manifest.json"; +import { createManifest } from "../scripts/manifest.mjs"; describe("manifest", () => { test("injects the content script on the www Xingtu market page", () => { @@ -16,6 +17,9 @@ describe("manifest", () => { expect(manifest.permissions).toEqual( 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.arrayContaining([ "http://*/*", @@ -27,4 +31,38 @@ describe("manifest", () => { expect(manifest.background?.service_worker).toBe("background/index.js"); 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" + }); + }); }); diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index 126bc81..b4278c2 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -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; + 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 () => { const pages = [ [ @@ -1610,11 +1721,11 @@ describe("market-content-entry", () => { expect(promptBatchName).toHaveBeenCalledTimes(1); expect(submitBatch).toHaveBeenCalledWith( expect.objectContaining({ - batchId: expect.stringContaining("p7pdhhtde8kj-"), batchName: "618达人筛选第一批", logtoUserId: "p7pdhhtde8kj" }) ); + expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId"); }); test("selected batch submit uses only creators selected in the current range", async () => { diff --git a/tests/mock-protected-api.test.ts b/tests/mock-protected-api.test.ts index 8081dfa..0037081 100644 --- a/tests/mock-protected-api.test.ts +++ b/tests/mock-protected-api.test.ts @@ -53,7 +53,6 @@ describe("mock-protected-api", () => { const response = await fetch(`${server.baseUrl}/api/mock/batches`, { body: JSON.stringify({ authors: [{ authorId: "111", authorName: "达人A" }], - batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", @@ -71,7 +70,7 @@ describe("mock-protected-api", () => { await expect(response.json()).resolves.toEqual( expect.objectContaining({ acceptedCount: 1, - batchId: "批次A-2026-04-22T12:30:00.000Z", + batchId: null, ok: true, source: "mock-batch-submit" })