fix find post
This commit is contained in:
parent
d0da1a1103
commit
b7125abeac
|
|
@ -1 +1 @@
|
|||
26382
|
||||
51557
|
||||
|
|
|
|||
|
|
@ -1,43 +1,248 @@
|
|||
2026/06/25 16:19:34 job scheduler started: holder=wangxinghuadeMac-mini.local-scheduler interval=1m0s
|
||||
2026/06/25 16:19:34 job reaper started: interval=30s
|
||||
2026/06/25 16:19:34 job worker started: id=wangxinghuadeMac-mini.local-go-worker type=go
|
||||
2026/06/25 17:30:42 job scheduler started: holder=wangxinghuadeMac-mini.local-scheduler interval=1m0s
|
||||
2026/06/25 17:30:42 job worker started: id=wangxinghuadeMac-mini.local-go-worker type=go
|
||||
2026/06/25 17:30:42 job reaper started: interval=30s
|
||||
Starting backend backend at 0.0.0.0:8890...
|
||||
{"@timestamp":"2026-06-25T16:19:35.055+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:60604 - curl/8.7.1","duration":"0.3ms","level":"info","span":"6415334576486240","trace":"950fc5b3ba616a4912d51241eb2b3a48"}
|
||||
{"@timestamp":"2026-06-25T16:19:35.063+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:60605 - curl/8.7.1","duration":"0.1ms","level":"info","span":"32b367b9757aa4ce","trace":"b8d6e2b9b9f6991f39ca52286514f8d0"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.414+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 401 - GET /api/v1/members/me - 127.0.0.1:60617 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.3ms","level":"info","span":"0a0ab6f5c14d5d26","trace":"a092f127556e39b4d17762ca139e6068"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.416+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 401 - GET /api/v1/members/me - 127.0.0.1:60618 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.1ms","level":"info","span":"79c81d67629142ba","trace":"d819499213c89d08de3b5b5d2f35b7e6"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.419+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/auth/refresh - 127.0.0.1:60619 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.2ms","level":"info","span":"028c5080eabb0c20","trace":"91f42acca9e2bd90c32b9bd5425f5243"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:60623 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"11.7ms","level":"info","span":"e505d50d87cf5e3f","trace":"a1dc33e9536bba807f03d824b68d11ae"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:60624 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"dc5a2ac116d2793f","trace":"bf0c219c6522b3f0026595075c847f2a"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.472+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8 - 127.0.0.1:60632 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"13.7ms","level":"info","span":"3986579b07eb7cc6","trace":"62e9f8ae8907e878c693ed5cc909bacd"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.480+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60633 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"21.1ms","level":"info","span":"b106b93beea0b78b","trace":"26cc88f8ff2121b4df8f38b43d7304c0"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.481+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8 - 127.0.0.1:60646 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.4ms","level":"info","span":"0ac7545e219b276a","trace":"9801452fce8d5260fd59282be8d94535"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.483+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60648 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"514548e176d35c49","trace":"3ea5c3db304edb00642910eb331cad11"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.485+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60630 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"27.3ms","level":"info","span":"846dd3bd919fabfc","trace":"bc5287a47b8d1e9aff648aa6f1801b30"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.487+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:60636 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"27.5ms","level":"info","span":"4274ae675b087aa5","trace":"c4e5d877a268cd28ed5560216c49fbb3"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.487+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60650 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.0ms","level":"info","span":"d95aa13b6583ddbd","trace":"1b963cd6f167ad24b1b5a7f425b1382e"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.489+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:60631 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"31.0ms","level":"info","span":"74793b560680c5b3","trace":"759db809c9ccaaee2681541c1ac9e4c0"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.489+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60654 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"7ecde433bfd09aa3","trace":"34f4e4ff4c1efc22ad6c2b71434e5327"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.489+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:60652 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.8ms","level":"info","span":"33138517c2669bfd","trace":"d7bb1ef130cd1fc4c0ea13d5857eb43a"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.490+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/ai-settings - 127.0.0.1:60635 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"31.3ms","level":"info","span":"832aa7d686494971","trace":"3a88a027e63eccbd06bec130f801209a"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.492+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60658 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.6ms","level":"info","span":"c27fd79c0ce6aa3b","trace":"2102a91a76c1610f1622df77402f0993"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.492+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/ai-settings - 127.0.0.1:60660 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"41eac6809cffa5fc","trace":"acd5544f67f670d2614da16744372bfc"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.492+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:60659 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.9ms","level":"info","span":"edc1fd6192317f35","trace":"0389ec83da5ac17ae72d4fc745389dc3"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.493+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60662 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.4ms","level":"info","span":"7eb2e6972f3e44c9","trace":"5af3f0bad4af7c49455e5756f60cdb07"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.527+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:60664 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.5ms","level":"info","span":"ade7a29f73a7c288","trace":"5ab83b6cd21393e1de22b5f87cf3da1b"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.533+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:60666 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.8ms","level":"info","span":"2cbde8636870326f","trace":"dfeeff52daffd47979d1bf27132ff1fe"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.554+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:60668 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"21.5ms","level":"info","span":"e99768c1e5a60f19","trace":"ed1142224257855380d4c7d82ef30d7d"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.562+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:60670 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.1ms","level":"info","span":"682d76eaf77ba854","trace":"2debafb3df297667243d29080aaf46f3"}
|
||||
{"@timestamp":"2026-06-25T16:19:37.567+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:60672 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.3ms","level":"info","span":"ede6f51d147113cc","trace":"ed9e3a6e7d3dce0809583ad7069428a6"}
|
||||
{"@timestamp":"2026-06-25T16:19:38.079+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node - slowcall(2059.6ms)","duration":"2059.6ms","level":"slow","span":"154f34f3c1f12155","trace":"f8ed314cbff91e799bccd0404c1e8d10"}
|
||||
{"@timestamp":"2026-06-25T16:19:38.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node","duration":"2059.6ms","level":"info","span":"154f34f3c1f12155","trace":"f8ed314cbff91e799bccd0404c1e8d10"}
|
||||
{"@timestamp":"2026-06-25T16:19:39.468+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60674 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.8ms","level":"info","span":"3aa1e079de1afc33","trace":"c83e8c4576b691e3c046fcf69afab8ba"}
|
||||
{"@timestamp":"2026-06-25T16:19:42.157+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60676 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"6.8ms","level":"info","span":"49f9c0136a1bb10e","trace":"1af1b6eda97a33a254a83ef3528a232f"}
|
||||
{"@timestamp":"2026-06-25T16:19:43.159+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node - slowcall(2065.1ms)","duration":"2065.1ms","level":"slow","span":"b0466c9c7c11a34b","trace":"6c3157a0083e8935e5d727cefa561016"}
|
||||
{"@timestamp":"2026-06-25T16:19:43.159+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node","duration":"2065.1ms","level":"info","span":"b0466c9c7c11a34b","trace":"6c3157a0083e8935e5d727cefa561016"}
|
||||
{"@timestamp":"2026-06-25T16:19:44.154+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60678 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.7ms","level":"info","span":"0b8ef70e28a0b93c","trace":"fa02be6dc695e628d454c08e32b04602"}
|
||||
{"@timestamp":"2026-06-25T16:19:46.152+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60682 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.1ms","level":"info","span":"1de3a57a2b185f05","trace":"113b14351258aa182b42f764d9fc3d78"}
|
||||
{"@timestamp":"2026-06-25T16:19:48.151+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60684 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.6ms","level":"info","span":"6bfcce92975c0546","trace":"154518eadb36033595c2b0608bbff0d1"}
|
||||
{"@timestamp":"2026-06-25T16:19:48.248+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node - slowcall(2084.2ms)","duration":"2084.2ms","level":"slow","span":"efaa36b9549d8d3d","trace":"fed1ba361d31f1dca61f215e56dcb2ca"}
|
||||
{"@timestamp":"2026-06-25T16:19:48.248+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:60606 - node","duration":"2084.2ms","level":"info","span":"efaa36b9549d8d3d","trace":"fed1ba361d31f1dca61f215e56dcb2ca"}
|
||||
{"@timestamp":"2026-06-25T16:19:50.152+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:60689 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.6ms","level":"info","span":"d34bb8e081297bc5","trace":"36e0c61a91be1e5075ac51dbe563ff7a"}
|
||||
{"@timestamp":"2026-06-25T17:30:42.961+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:55160 - curl/8.7.1","duration":"0.5ms","level":"info","span":"045ba34c000a4d69","trace":"162a6986337c530f2a88d15d9486f01b"}
|
||||
{"@timestamp":"2026-06-25T17:30:42.975+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:55161 - curl/8.7.1","duration":"0.1ms","level":"info","span":"53345b0a3c07f4f5","trace":"e3dfecd1de18e183f93bc45d19581b53"}
|
||||
{"@timestamp":"2026-06-25T17:30:45.843+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2044.6ms)","duration":"2044.6ms","level":"slow","span":"9aca8d10547dac0f","trace":"b886a38ebf0791d14e081ecb6d7ed106"}
|
||||
{"@timestamp":"2026-06-25T17:30:45.843+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2044.6ms","level":"info","span":"9aca8d10547dac0f","trace":"b886a38ebf0791d14e081ecb6d7ed106"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.602+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/auth/login - 127.0.0.1:55190 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"103.3ms","level":"info","span":"c9d9bb386da27ea9","trace":"836f3f3c7831c91ec30eb7b3fc8ca0c4"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.615+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55192 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.5ms","level":"info","span":"70e96ee134e675e3","trace":"9dc9ea0636288a4f404fd0e5811988d0"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.633+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/job/schedules?page=1&pageSize=100 - 127.0.0.1:55195 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"7.7ms","level":"info","span":"efb917d662253464","trace":"08f52016878848d8d8ca6f442d0ba7f3"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.636+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/job/schedules?page=1&pageSize=100 - 127.0.0.1:55208 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"0818d78ae2e89a7f","trace":"4dffd229432d026d33abea4323b5198c"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.636+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=50 - 127.0.0.1:55193 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"10.6ms","level":"info","span":"1046ad31eb987738","trace":"24c8e91f7fbe024108c2a3f9db90dde8"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.638+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55196 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"12.3ms","level":"info","span":"3aa47488bcdd1604","trace":"884c0060f20b5e484741198dc4ca3152"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.639+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=50 - 127.0.0.1:55210 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"7609b8075ef2eee7","trace":"18a169caf39dcc4345c12a3cdbaa2d9b"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.641+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55197 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"15.0ms","level":"info","span":"cfbcfeb0821e857b","trace":"0a479a0de3a42fe1ed30a5eacbe01ff3"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.641+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55212 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.6ms","level":"info","span":"c60cfa9cdee18f32","trace":"d4663d1888e3e42bc566969a35bc5184"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.643+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55201 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"16.8ms","level":"info","span":"87a95c55afefe871","trace":"d5c9aab073e7c0b07713a310013f8ac6"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.645+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55214 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.3ms","level":"info","span":"9b34d1828c68edff","trace":"fc03532d8b9e854a866115f0daf1bde0"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.648+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55216 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.6ms","level":"info","span":"85ec75935d353dd6","trace":"3e50d1982ee1a69fa8aea7329449225c"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.648+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55218 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"b215edceb1f23e97","trace":"a6cf1b0662541ef1a31543861d85cfd6"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.657+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55220 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.9ms","level":"info","span":"45df893595b47cc0","trace":"3951796ab3abbe515d7f773bddac0ba2"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.673+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55223 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"ee3bfef0ea16861c","trace":"5d69ba637d00496e147e94f4ee06fa25"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.675+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55224 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"0b01b7ac3436d854","trace":"16a644b25a2d990c0e17080b00c975ef"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.678+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55226 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.3ms","level":"info","span":"c2e07676a4032951","trace":"59b892589ae8dd18cf94b9d6d8fa4ad4"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.927+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2071.2ms)","duration":"2071.2ms","level":"slow","span":"bc8917b8a7e369d1","trace":"9ee3d32d8264e74da8337a6c7d73d37a"}
|
||||
{"@timestamp":"2026-06-25T17:30:50.927+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2071.2ms","level":"info","span":"bc8917b8a7e369d1","trace":"9ee3d32d8264e74da8337a6c7d73d37a"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.901+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55230 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.9ms","level":"info","span":"7ad2d1a8325fa704","trace":"3abd6632e7c80697176e734cf753a5af"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.902+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:55231 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.4ms","level":"info","span":"d058165b1246ef75","trace":"485546586d2d29478fbaf67de46cf79d"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.902+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8 - 127.0.0.1:55232 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.4ms","level":"info","span":"caab402214329961","trace":"8ce648cae59691a7cc0343b5a5b88458"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.903+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55234 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.0ms","level":"info","span":"af6416f906ac1a54","trace":"470ebfa460168763428cbe5e0efecefd"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.905+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:55237 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.9ms","level":"info","span":"102df3903a0141a5","trace":"f00e87c07bbf62c3ead92f4e8c252f8b"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.905+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8 - 127.0.0.1:55239 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.7ms","level":"info","span":"0092616cd44b40d3","trace":"790f9d14191133a97cc9e247b6814345"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.905+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55240 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.0ms","level":"info","span":"a309118e34ed5a0f","trace":"ea7eb9e26f977c5db6a4319983a2eca0"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.909+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55244 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.2ms","level":"info","span":"ebac4f23af901a17","trace":"dfae73fa78d7ed5d0dc789bf4607df80"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.911+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55247 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"e870871cf77b2547","trace":"f76091fcd5bfd6b5531737793226c789"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.913+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55250 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.0ms","level":"info","span":"af6699a87fe4ac3c","trace":"9563aacda92c6243ab744e13458879b8"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.914+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55248 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.3ms","level":"info","span":"12de4c48e88ed5b6","trace":"bf1fae11ed90857242f6c622f30f2ba8"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.914+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55243 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"6.9ms","level":"info","span":"0aacc80c57455da4","trace":"33d7d6b3b0001d61905ca344584ba5da"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.917+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55252 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.4ms","level":"info","span":"1b3b6aca6dc53382","trace":"ed1bf91d8e0328c40dc9f7d5b4f2192a"}
|
||||
{"@timestamp":"2026-06-25T17:30:51.920+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55254 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.8ms","level":"info","span":"88ee7b8163e1e59e","trace":"72706beab614f76decaad9832a35cacd"}
|
||||
{"@timestamp":"2026-06-25T17:30:52.631+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55257 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.5ms","level":"info","span":"f8f9c1b34a503b8c","trace":"d99aa9424ef6164a3245e984b25dcf13"}
|
||||
{"@timestamp":"2026-06-25T17:30:54.633+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55259 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.1ms","level":"info","span":"0ea6ec0bbb6a4bc6","trace":"57e282aa4117d5ac1606b786c7b138bc"}
|
||||
{"@timestamp":"2026-06-25T17:30:55.970+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2038.7ms)","duration":"2038.7ms","level":"slow","span":"08539fb389748e1a","trace":"c9fb51ce83c0be9e6489261fc5a428ed"}
|
||||
{"@timestamp":"2026-06-25T17:30:55.970+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2038.7ms","level":"info","span":"08539fb389748e1a","trace":"c9fb51ce83c0be9e6489261fc5a428ed"}
|
||||
{"@timestamp":"2026-06-25T17:30:56.630+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55262 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.1ms","level":"info","span":"8083aeacc63cca50","trace":"d6eade15023e75f4b280079447013bd7"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.926+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55272 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.5ms","level":"info","span":"aa89e894abd169f5","trace":"2d64a34f896f4e5151f42251dc070e42"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.929+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55274 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"1254fab35f182638","trace":"aebdfaaf3752c01fdc1976c3de194882"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.935+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55275 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"6ec5292bee49323e","trace":"33a347e6566102f39f02984c2dea7813"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.936+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55278 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"b2d34d812726e84a","trace":"eca7ad3c5e369376a172c8dfbff67e59"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.936+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55277 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.2ms","level":"info","span":"771c64fe2b4c0239","trace":"c3fa86ff681184f804898da637c9c45b"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.936+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55279 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.7ms","level":"info","span":"0a285bb192ade3a0","trace":"f91633612e1569268b5f436d1560f0d2"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.937+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55280 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.9ms","level":"info","span":"0d25b78969ddb628","trace":"736282ea74cf471d142e8f1eca4126f7"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.939+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55285 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"abb2e9e78a60b447","trace":"996f7cd66cfb407dea41143e80ebea55"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.940+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55287 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.4ms","level":"info","span":"cd8ce20de364a2ee","trace":"0dd73756ab2857d517af7c63d0a58d58"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.940+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55289 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.7ms","level":"info","span":"05e73404ad0815f6","trace":"a1d282ffeebb9512ec086b51c76ad0b9"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.941+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55291 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"020f06a8d967d281","trace":"fce59e05edd3d9220068d4dc90d95516"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.941+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55290 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.5ms","level":"info","span":"7809c4b92f9f91c9","trace":"3d29ed43e55f3533f9d39ebc0f90ae31"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.943+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55294 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.0ms","level":"info","span":"20688e0d89c3ddbe","trace":"7b00b63a2e64ae10aa0933913af5b251"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.944+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55295 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"91bb232409b87bb8","trace":"6b906e48b510477cd45ed33310c4d29c"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.946+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55296 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"ac581a6a34288c03","trace":"2a9b86259eac4d1347448f04e3c14f20"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.975+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55298 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.7ms","level":"info","span":"b82ee1ef135412db","trace":"c6a76cf7ea27a813a4a60d925d20742e"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.981+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55300 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.6ms","level":"info","span":"70c68dc7b205af38","trace":"dccb0fd715ebff61cbbaa992c3cb01a3"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.984+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55302 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"7ba4f89461bc3bc6","trace":"4071dc284ec847524b095aac1578e4b1"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.987+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55304 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.3ms","level":"info","span":"8a5325643dfa5d22","trace":"c3b6fd6bfed495c3306b8e1ab40e810c"}
|
||||
{"@timestamp":"2026-06-25T17:30:57.994+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55306 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.4ms","level":"info","span":"db3308545e2198d3","trace":"eca7614390d72379a2a20e00207d42e4"}
|
||||
{"@timestamp":"2026-06-25T17:30:59.946+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55309 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"6.2ms","level":"info","span":"3a12f6b9d681fed7","trace":"e3ac5f81f3a84be398d60bd89af93365"}
|
||||
{"@timestamp":"2026-06-25T17:31:01.046+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2070.2ms)","duration":"2070.2ms","level":"slow","span":"11b5f89014e104e1","trace":"eda812904869cf93bb175f8431f19278"}
|
||||
{"@timestamp":"2026-06-25T17:31:01.046+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2070.2ms","level":"info","span":"11b5f89014e104e1","trace":"eda812904869cf93bb175f8431f19278"}
|
||||
{"@timestamp":"2026-06-25T17:31:01.942+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55311 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.9ms","level":"info","span":"cd14d0819298e6f5","trace":"fb41d4703fe398a0e93d2e36542596b7"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.451+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55319 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.6ms","level":"info","span":"f590d82d383b8975","trace":"682f1840c98d1c7ce975fc7f009c638f"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.453+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55320 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"260d074990c5a8b6","trace":"3c7a7656ad9b70ae62fb4734a56112b9"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.459+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"d6ccd931430440be","trace":"26a6a1551b1be8dffa08f16a2bc5a544"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.460+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55324 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.6ms","level":"info","span":"299b095e60854722","trace":"2e19508624657ccca9e5f71fcc2fe1e7"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.460+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55323 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.2ms","level":"info","span":"eb4fe2790f17a9c6","trace":"8fcc1419576f5a783804f57878c34857"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.460+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55326 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.3ms","level":"info","span":"786557eaddeaf5d2","trace":"1f8eeda14829f13fe6498a94b02d10ee"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.461+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55325 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.6ms","level":"info","span":"def6394282635fc7","trace":"80b043112914c34d6f1442579ade5613"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.463+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55333 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"c36b05b6840c3656","trace":"074245184bb2b94c9f45d3740650e441"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.463+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55334 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.8ms","level":"info","span":"9ae2637e676f1999","trace":"7bbee3bc35d4387f67b7dcdced9ce6a6"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.464+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55335 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"cd5599243bac82b5","trace":"85d6616e236bce9a9da9ac3f13d3e710"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.464+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55336 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.7ms","level":"info","span":"d572a72809faad57","trace":"bf691da31b1c75406a4fcc15f4e121cc"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.465+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55338 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.6ms","level":"info","span":"7848a1a7397fbdea","trace":"0cda585155368929a9a43952c96a303f"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.465+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55337 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.9ms","level":"info","span":"86520d0e8cc8438e","trace":"797c4cbcacfed0981188100ecc5a1653"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.468+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55340 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"5c1fbc2e0ab14685","trace":"8ed837a6e5953763948bd8cae9905301"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.471+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55342 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.4ms","level":"info","span":"a7860be5759e30e6","trace":"cc4cd12c25607433a490cef57515e18f"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.501+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55344 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.7ms","level":"info","span":"804076bea5a1f541","trace":"c04e6a00f0d3c68e5b9e94ba6803d136"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.508+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55348 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"68023896a9bf8545","trace":"6a068ddf94de0313ecfffd2c83ffca99"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.509+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55346 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.0ms","level":"info","span":"0dc3c275a4672e17","trace":"51987cd7f911ce0a17caa223bb72577f"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.515+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55350 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.3ms","level":"info","span":"bf13884fa827bed1","trace":"8e4411e78f82926d11c26357d4f9789a"}
|
||||
{"@timestamp":"2026-06-25T17:31:02.522+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55352 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"81a01a3eba4a754b","trace":"a7d10ab7dc922150c3a3fe82713cd664"}
|
||||
{"@timestamp":"2026-06-25T17:31:04.468+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55354 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.6ms","level":"info","span":"3575c4ac7e2c1da7","trace":"50831760b2d840ce3e5083f6a0a4af37"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.701+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55361 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"2ca12b5ee6e1df92","trace":"a5003bcc30c894f9ac57d70d9fb87c5d"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.704+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55360 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"6.2ms","level":"info","span":"06a14815c58e5951","trace":"4c2a945ce414d9c257aa3952e8603dac"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.709+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55365 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.9ms","level":"info","span":"6cd5323379666c0d","trace":"cef5a557e47ba0a9090c155570507710"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.710+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55363 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.1ms","level":"info","span":"3398337a0bde5c87","trace":"0a3e7a18112c41f1ca97ab700d4962ab"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.715+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55359 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"17.8ms","level":"info","span":"cee65f36538d0eff","trace":"927e60670d29f47b89148d1750e05ef8"}
|
||||
{"@timestamp":"2026-06-25T17:31:05.723+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55367 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.7ms","level":"info","span":"fddf9358b7b6bb30","trace":"a96c5f8debc97727f81d5b89a3d1f98c"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.110+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2060.1ms)","duration":"2060.1ms","level":"slow","span":"da165bfebe449f26","trace":"c54a81f0c5673c639a774b12db9b0b88"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.110+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2060.1ms","level":"info","span":"da165bfebe449f26","trace":"c54a81f0c5673c639a774b12db9b0b88"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.464+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55380 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.8ms","level":"info","span":"2c9273fe735bd60c","trace":"73ba788e5c133779ff7fee945c1ec01b"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.787+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55388 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.0ms","level":"info","span":"b28a988bad4fa8c5","trace":"b4d5b47524a770309ddd50a8f0f95991"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.790+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55389 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.7ms","level":"info","span":"056038562bd58aa6","trace":"a639448cb9ae36e53a9bac43c0328413"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.798+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55392 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"62901cf042a6c15a","trace":"a7e5e615be32313dc32518099fcccabd"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.799+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55394 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.3ms","level":"info","span":"9cd703d25683b9fc","trace":"2a4afca6c0c5d93ac953b1cc5b64e594"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.799+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55393 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.0ms","level":"info","span":"305b9bbe16be35e9","trace":"f8f9be45343402570a145bfe11c13f2c"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.799+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55395 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.9ms","level":"info","span":"89f18bfb6eebfdb0","trace":"bd4f44b3d6daedb7763c0696997fbad8"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.800+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55391 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"88a87954dec11ce1","trace":"23f0a49a7258868c88f6586f92b31bf2"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.804+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55402 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.9ms","level":"info","span":"2e3f33a921286e13","trace":"8891e84abef9bd8062f5cc28358a0f00"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.805+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55403 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.1ms","level":"info","span":"e7a0017f02cc6a8f","trace":"53d0cb527d7ae27ea85a8b9d7eadc6c7"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.807+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55406 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.5ms","level":"info","span":"1e169418721e50fe","trace":"4691f8943301de04f39a19f058c492d4"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.807+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55405 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.5ms","level":"info","span":"fb87ead57d815315","trace":"113a2d9bcf5cb0910a9d1b10998133f3"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.807+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55404 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.7ms","level":"info","span":"a800b51bc9ccdd05","trace":"882c6acf043f066791ecb91d129f2c9a"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.810+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55407 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"a4b80e2720bb300c","trace":"f087b6816f30239a33c6c0930b2b4ec5"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.838+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55409 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.8ms","level":"info","span":"1f2009151093c23a","trace":"23feced1a871fd42af22c7ea21628b3b"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.838+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55411 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"8f376760945ea213","trace":"f615ff626e75098105d969f8c64ba337"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.844+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55413 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.6ms","level":"info","span":"cd4dccf233bb29d5","trace":"941f9f90bf3d44db6ad3a563727d50a9"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.851+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55415 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.6ms","level":"info","span":"d9a672ab54258f17","trace":"2b7177c3cad783a4304f23db9d291eef"}
|
||||
{"@timestamp":"2026-06-25T17:31:06.855+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/a76918ed-69a7-4f45-8ea2-c984d8a85409/connection - 127.0.0.1:55417 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.5ms","level":"info","span":"33be1ab5c1770cc8","trace":"6db71228a0988ff7e507196a6cab0a26"}
|
||||
{"@timestamp":"2026-06-25T17:31:08.801+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55420 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.4ms","level":"info","span":"b05cf50f858dd528","trace":"df0163829a024e24c95f36e2746a76ac"}
|
||||
{"@timestamp":"2026-06-25T17:31:09.479+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/auth/logout - 127.0.0.1:55422 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.6ms","level":"info","span":"6511056c48eb6664","trace":"cf3c0886fdf1265b3eecaa92634d266d"}
|
||||
{"@timestamp":"2026-06-25T17:31:11.207+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2094.3ms)","duration":"2094.3ms","level":"slow","span":"c69ffc592e5e910b","trace":"c4f65c6e7add84a1ebc61cd242e25883"}
|
||||
{"@timestamp":"2026-06-25T17:31:11.208+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2094.3ms","level":"info","span":"c69ffc592e5e910b","trace":"c4f65c6e7add84a1ebc61cd242e25883"}
|
||||
{"@timestamp":"2026-06-25T17:31:16.292+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2078.8ms)","duration":"2078.8ms","level":"slow","span":"654f6d8d44cdceba","trace":"80abdae74376b55c046de09eec4eaaf3"}
|
||||
{"@timestamp":"2026-06-25T17:31:16.292+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2078.8ms","level":"info","span":"654f6d8d44cdceba","trace":"80abdae74376b55c046de09eec4eaaf3"}
|
||||
{"@timestamp":"2026-06-25T17:31:21.398+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2098.0ms)","duration":"2098.0ms","level":"slow","span":"04c66ed148f5aea8","trace":"a2254c41f02b5ea7387120779b02cfbc"}
|
||||
{"@timestamp":"2026-06-25T17:31:21.398+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2098.0ms","level":"info","span":"04c66ed148f5aea8","trace":"a2254c41f02b5ea7387120779b02cfbc"}
|
||||
{"@timestamp":"2026-06-25T17:31:26.484+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2079.5ms)","duration":"2079.5ms","level":"slow","span":"2c5bcf866c9a0807","trace":"31fa976dc0be92e5ca985a3b8f7e4d12"}
|
||||
{"@timestamp":"2026-06-25T17:31:26.484+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2079.5ms","level":"info","span":"2c5bcf866c9a0807","trace":"31fa976dc0be92e5ca985a3b8f7e4d12"}
|
||||
{"@timestamp":"2026-06-25T17:31:31.588+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2097.6ms)","duration":"2097.6ms","level":"slow","span":"a95aa5c0c8e39189","trace":"39a5e176b834d5c1f8b59f687344f12f"}
|
||||
{"@timestamp":"2026-06-25T17:31:31.589+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2097.6ms","level":"info","span":"a95aa5c0c8e39189","trace":"39a5e176b834d5c1f8b59f687344f12f"}
|
||||
{"@timestamp":"2026-06-25T17:31:36.695+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2099.9ms)","duration":"2099.9ms","level":"slow","span":"c45c88f25aa37028","trace":"4644b074b0e7eb5eb03b22b914b40d44"}
|
||||
{"@timestamp":"2026-06-25T17:31:36.695+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2099.9ms","level":"info","span":"c45c88f25aa37028","trace":"4644b074b0e7eb5eb03b22b914b40d44"}
|
||||
{"@timestamp":"2026-06-25T17:31:41.784+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2085.0ms)","duration":"2085.0ms","level":"slow","span":"ad09cf5e01b779fb","trace":"cdd16a4dac610045e5fea7b1c3ddef3d"}
|
||||
{"@timestamp":"2026-06-25T17:31:41.785+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2085.0ms","level":"info","span":"ad09cf5e01b779fb","trace":"cdd16a4dac610045e5fea7b1c3ddef3d"}
|
||||
{"@timestamp":"2026-06-25T17:31:42.550+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.4Mi, TotalAlloc=10.8Mi, Sys=22.8Mi, NumGC=8","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:31:42.582+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 118, pass: 118, drop: 0","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:31:42.963+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 2.0/s, drops: 0, avg time: 214.8ms, med: 2.6ms, 90th: 2044.6ms, 99th: 2099.8ms, 99.9th: 2099.8ms","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:31:46.867+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2077.7ms)","duration":"2077.7ms","level":"slow","span":"6ab8d4ee8a0087bf","trace":"3cac9f4fd18a0d0301c09bc511a4191e"}
|
||||
{"@timestamp":"2026-06-25T17:31:46.867+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2077.7ms","level":"info","span":"6ab8d4ee8a0087bf","trace":"3cac9f4fd18a0d0301c09bc511a4191e"}
|
||||
{"@timestamp":"2026-06-25T17:31:51.957+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2087.0ms)","duration":"2087.0ms","level":"slow","span":"3ce71abc1ab25c02","trace":"3fdab0afb5165445531bb339273d225f"}
|
||||
{"@timestamp":"2026-06-25T17:31:51.957+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2087.0ms","level":"info","span":"3ce71abc1ab25c02","trace":"3fdab0afb5165445531bb339273d225f"}
|
||||
{"@timestamp":"2026-06-25T17:31:57.052+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2091.3ms)","duration":"2091.3ms","level":"slow","span":"27627a4eac5a334f","trace":"57fa85ee4d69821697646a59d5a39f13"}
|
||||
{"@timestamp":"2026-06-25T17:31:57.053+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2091.3ms","level":"info","span":"27627a4eac5a334f","trace":"57fa85ee4d69821697646a59d5a39f13"}
|
||||
{"@timestamp":"2026-06-25T17:32:02.155+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2098.2ms)","duration":"2098.2ms","level":"slow","span":"f5780756d4a5c68c","trace":"b7f04f30dc65c57b0bf550e066bee273"}
|
||||
{"@timestamp":"2026-06-25T17:32:02.156+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2098.2ms","level":"info","span":"f5780756d4a5c68c","trace":"b7f04f30dc65c57b0bf550e066bee273"}
|
||||
{"@timestamp":"2026-06-25T17:32:07.225+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2066.1ms)","duration":"2066.1ms","level":"slow","span":"663c65e67c709bef","trace":"7de4c2867d569f88ad80c275d9772991"}
|
||||
{"@timestamp":"2026-06-25T17:32:07.226+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2066.1ms","level":"info","span":"663c65e67c709bef","trace":"7de4c2867d569f88ad80c275d9772991"}
|
||||
{"@timestamp":"2026-06-25T17:32:12.242+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2013.0ms)","duration":"2013.0ms","level":"slow","span":"0d7b4a7560c09816","trace":"e8bfe2682b4e73a4b43263aaf038da2c"}
|
||||
{"@timestamp":"2026-06-25T17:32:12.242+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2013.0ms","level":"info","span":"0d7b4a7560c09816","trace":"e8bfe2682b4e73a4b43263aaf038da2c"}
|
||||
{"@timestamp":"2026-06-25T17:32:17.344+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2096.7ms)","duration":"2096.7ms","level":"slow","span":"ead4d4433e6a7b3b","trace":"a2fb614f0dd6d6dd23287e7c831cb5b9"}
|
||||
{"@timestamp":"2026-06-25T17:32:17.344+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2096.7ms","level":"info","span":"ead4d4433e6a7b3b","trace":"a2fb614f0dd6d6dd23287e7c831cb5b9"}
|
||||
{"@timestamp":"2026-06-25T17:32:22.429+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2080.3ms)","duration":"2080.3ms","level":"slow","span":"db06ee273e5a445d","trace":"09d612fbb44ab144261d8484c2be206e"}
|
||||
{"@timestamp":"2026-06-25T17:32:22.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2080.3ms","level":"info","span":"db06ee273e5a445d","trace":"09d612fbb44ab144261d8484c2be206e"}
|
||||
{"@timestamp":"2026-06-25T17:32:27.516+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2082.5ms)","duration":"2082.5ms","level":"slow","span":"12e7d0db835a4559","trace":"1683ffea836ab1cf3ccedede55f846ce"}
|
||||
{"@timestamp":"2026-06-25T17:32:27.517+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2082.5ms","level":"info","span":"12e7d0db835a4559","trace":"1683ffea836ab1cf3ccedede55f846ce"}
|
||||
{"@timestamp":"2026-06-25T17:32:32.529+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2008.3ms)","duration":"2008.3ms","level":"slow","span":"71633f5376ecfa93","trace":"c2114cba9ffe336425ff66da96f4c70d"}
|
||||
{"@timestamp":"2026-06-25T17:32:32.529+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2008.3ms","level":"info","span":"71633f5376ecfa93","trace":"c2114cba9ffe336425ff66da96f4c70d"}
|
||||
{"@timestamp":"2026-06-25T17:32:37.619+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2085.1ms)","duration":"2085.1ms","level":"slow","span":"ec573737c77d8639","trace":"e3d884c37a7c8bdb0eb7044a12d42399"}
|
||||
{"@timestamp":"2026-06-25T17:32:37.619+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2085.1ms","level":"info","span":"ec573737c77d8639","trace":"e3d884c37a7c8bdb0eb7044a12d42399"}
|
||||
{"@timestamp":"2026-06-25T17:32:42.551+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.6Mi, TotalAlloc=11.0Mi, Sys=22.8Mi, NumGC=8","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:32:42.582+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 12, pass: 11, drop: 0","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:32:42.709+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2084.8ms)","duration":"2084.8ms","level":"slow","span":"6dd76653010f793b","trace":"c261af1cf03adfe1ff293788c26738f8"}
|
||||
{"@timestamp":"2026-06-25T17:32:42.709+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2084.8ms","level":"info","span":"6dd76653010f793b","trace":"c261af1cf03adfe1ff293788c26738f8"}
|
||||
{"@timestamp":"2026-06-25T17:32:42.964+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 0.2/s, drops: 0, avg time: 2072.4ms, med: 2084.7ms, 90th: 2098.1ms, 99th: 2098.1ms, 99.9th: 2098.1ms","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:32:47.786+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2073.1ms)","duration":"2073.1ms","level":"slow","span":"c66a6d108d49e2ba","trace":"75cf868767690979b2a9b2752ba29e91"}
|
||||
{"@timestamp":"2026-06-25T17:32:47.786+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2073.1ms","level":"info","span":"c66a6d108d49e2ba","trace":"75cf868767690979b2a9b2752ba29e91"}
|
||||
{"@timestamp":"2026-06-25T17:32:52.890+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2100.5ms)","duration":"2100.5ms","level":"slow","span":"76d74728bd1fba11","trace":"5ff817f5c11917d5eebf4fadbcc52faa"}
|
||||
{"@timestamp":"2026-06-25T17:32:52.890+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2100.5ms","level":"info","span":"76d74728bd1fba11","trace":"5ff817f5c11917d5eebf4fadbcc52faa"}
|
||||
{"@timestamp":"2026-06-25T17:32:57.986+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2091.4ms)","duration":"2091.4ms","level":"slow","span":"ae40024330d59976","trace":"3bb8ad99579eaf978378032663ff25fe"}
|
||||
{"@timestamp":"2026-06-25T17:32:57.986+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2091.4ms","level":"info","span":"ae40024330d59976","trace":"3bb8ad99579eaf978378032663ff25fe"}
|
||||
{"@timestamp":"2026-06-25T17:33:03.021+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2031.2ms)","duration":"2031.2ms","level":"slow","span":"8f7609830dfbc72a","trace":"502c18fca2d055ca54d17fa40ba52a34"}
|
||||
{"@timestamp":"2026-06-25T17:33:03.022+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2031.2ms","level":"info","span":"8f7609830dfbc72a","trace":"502c18fca2d055ca54d17fa40ba52a34"}
|
||||
{"@timestamp":"2026-06-25T17:33:08.029+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2003.4ms)","duration":"2003.4ms","level":"slow","span":"adcb55997fe2d556","trace":"6020e9b6ef12d81c8e62b85392ed6f60"}
|
||||
{"@timestamp":"2026-06-25T17:33:08.029+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2003.4ms","level":"info","span":"adcb55997fe2d556","trace":"6020e9b6ef12d81c8e62b85392ed6f60"}
|
||||
{"@timestamp":"2026-06-25T17:33:13.034+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2002.2ms)","duration":"2002.2ms","level":"slow","span":"a768e89de63fcfb2","trace":"5ca4e3870e866ac9a06c700523adedf4"}
|
||||
{"@timestamp":"2026-06-25T17:33:13.034+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2002.2ms","level":"info","span":"a768e89de63fcfb2","trace":"5ca4e3870e866ac9a06c700523adedf4"}
|
||||
{"@timestamp":"2026-06-25T17:33:18.092+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2054.1ms)","duration":"2054.1ms","level":"slow","span":"80d74a43a2d943e2","trace":"cc8b48dde391f96686e22641aae76364"}
|
||||
{"@timestamp":"2026-06-25T17:33:18.092+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2054.1ms","level":"info","span":"80d74a43a2d943e2","trace":"cc8b48dde391f96686e22641aae76364"}
|
||||
{"@timestamp":"2026-06-25T17:33:23.106+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2010.1ms)","duration":"2010.1ms","level":"slow","span":"1e6c0bad83977082","trace":"e20cf6f7eb7a72a0e1fd541c70ec5a3e"}
|
||||
{"@timestamp":"2026-06-25T17:33:23.107+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2010.1ms","level":"info","span":"1e6c0bad83977082","trace":"e20cf6f7eb7a72a0e1fd541c70ec5a3e"}
|
||||
{"@timestamp":"2026-06-25T17:33:28.166+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2054.8ms)","duration":"2054.8ms","level":"slow","span":"dd8e290d37298772","trace":"c65be74d49540b646ca4f2a28bdfa4ff"}
|
||||
{"@timestamp":"2026-06-25T17:33:28.166+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2054.8ms","level":"info","span":"dd8e290d37298772","trace":"c65be74d49540b646ca4f2a28bdfa4ff"}
|
||||
{"@timestamp":"2026-06-25T17:33:33.206+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2036.3ms)","duration":"2036.3ms","level":"slow","span":"a3b150258e687634","trace":"3cda63e235415958488ed77ad31ef94f"}
|
||||
{"@timestamp":"2026-06-25T17:33:33.206+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2036.3ms","level":"info","span":"a3b150258e687634","trace":"3cda63e235415958488ed77ad31ef94f"}
|
||||
{"@timestamp":"2026-06-25T17:33:38.220+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2009.6ms)","duration":"2009.6ms","level":"slow","span":"50b2976cce40ffca","trace":"c78f86732e57bf74dd28919535aba491"}
|
||||
{"@timestamp":"2026-06-25T17:33:38.220+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2009.6ms","level":"info","span":"50b2976cce40ffca","trace":"c78f86732e57bf74dd28919535aba491"}
|
||||
{"@timestamp":"2026-06-25T17:33:42.551+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.2Mi, TotalAlloc=11.8Mi, Sys=22.8Mi, NumGC=9","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:33:42.583+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 12, pass: 12, drop: 0","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:33:42.964+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 0.2/s, drops: 0, avg time: 2042.3ms, med: 2054.0ms, 90th: 2100.4ms, 99th: 2100.4ms, 99.9th: 2100.4ms","level":"stat"}
|
||||
{"@timestamp":"2026-06-25T17:33:43.284+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2058.7ms)","duration":"2058.7ms","level":"slow","span":"d9af7376685e531b","trace":"8f8c36e4db268ccbc125037b980ca964"}
|
||||
{"@timestamp":"2026-06-25T17:33:43.284+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2058.7ms","level":"info","span":"d9af7376685e531b","trace":"8f8c36e4db268ccbc125037b980ca964"}
|
||||
{"@timestamp":"2026-06-25T17:33:48.319+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2031.8ms)","duration":"2031.8ms","level":"slow","span":"5286d37a79fc5cae","trace":"d0c98a2998a5b7a229bbbe6bf9572f0b"}
|
||||
{"@timestamp":"2026-06-25T17:33:48.319+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2031.8ms","level":"info","span":"5286d37a79fc5cae","trace":"d0c98a2998a5b7a229bbbe6bf9572f0b"}
|
||||
{"@timestamp":"2026-06-25T17:33:53.331+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2009.5ms)","duration":"2009.5ms","level":"slow","span":"628998740afc0158","trace":"eed13a92127ae10bd5b1248314445789"}
|
||||
{"@timestamp":"2026-06-25T17:33:53.332+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2009.5ms","level":"info","span":"628998740afc0158","trace":"eed13a92127ae10bd5b1248314445789"}
|
||||
{"@timestamp":"2026-06-25T17:33:58.410+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2075.2ms)","duration":"2075.2ms","level":"slow","span":"6f069a1ef9563546","trace":"be08467974b3d47491178c99318c3c5e"}
|
||||
{"@timestamp":"2026-06-25T17:33:58.411+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2075.2ms","level":"info","span":"6f069a1ef9563546","trace":"be08467974b3d47491178c99318c3c5e"}
|
||||
{"@timestamp":"2026-06-25T17:34:03.446+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2029.9ms)","duration":"2029.9ms","level":"slow","span":"f592b115b639fd49","trace":"508156e8fe0fdc6a2fff56f68bc313f3"}
|
||||
{"@timestamp":"2026-06-25T17:34:03.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2029.9ms","level":"info","span":"f592b115b639fd49","trace":"508156e8fe0fdc6a2fff56f68bc313f3"}
|
||||
{"@timestamp":"2026-06-25T17:34:08.498+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2048.2ms)","duration":"2048.2ms","level":"slow","span":"34eb427912169013","trace":"2a3caed7bf8273c9069e03bbb1aed951"}
|
||||
{"@timestamp":"2026-06-25T17:34:08.499+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2048.2ms","level":"info","span":"34eb427912169013","trace":"2a3caed7bf8273c9069e03bbb1aed951"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.226+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/auth/login - 127.0.0.1:55487 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"83.2ms","level":"info","span":"de0b846b0d0b03c4","trace":"c660d10bd6c44b171a644f0dc70b971a"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.236+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:55489 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"6c957466489aab06","trace":"1e8c4908971c451df4ee47cb9160791e"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.257+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/job/schedules?page=1&pageSize=100 - 127.0.0.1:55496 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.7ms","level":"info","span":"bad2dcc4e3bfb2f8","trace":"2e36e44a4834890ebea0623ca074b016"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.257+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55498 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"4a1922242b423c1b","trace":"7637c5d70cd66c2a5697c04b812d2d24"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.258+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55497 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.3ms","level":"info","span":"16aab11bd1b4c035","trace":"7c784b566db4eccfa21b9945240b6fc2"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.259+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=50 - 127.0.0.1:55495 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.9ms","level":"info","span":"70c19f25af237fab","trace":"32c92af096f8607fdb05fbf9c5a7c960"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.259+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55499 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.6ms","level":"info","span":"603161ac3d384acf","trace":"7a74b059192f4518f01e048714e357c5"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.261+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/job/schedules?page=1&pageSize=100 - 127.0.0.1:55504 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"177a9042c244ce1d","trace":"f743752bbb395d9e72700e8b6bb8ec64"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.261+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55506 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.4ms","level":"info","span":"693d09517c51272a","trace":"73ae172771c01b130d1f4b689ef92d6a"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.262+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:55509 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.9ms","level":"info","span":"dc5a6a542b42a416","trace":"27eda16b5954803e005e706333a12fb6"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.262+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=50 - 127.0.0.1:55508 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.0ms","level":"info","span":"fa57482c8b72b6f9","trace":"8ec65300c6fa37e09447c2d8e37035d2"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.263+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55511 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.6ms","level":"info","span":"466f6152cdfb187c","trace":"2693043b7f13a296f0c6542e470098e8"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.263+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55507 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.0ms","level":"info","span":"580d9d44dc13a16a","trace":"63db48c572bdcc3eeb9aa09b6590de09"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.280+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55513 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.2ms","level":"info","span":"9276cb12d5979c5b","trace":"62455819c00df613fcc8f3f1a287de16"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.287+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:55515 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.7ms","level":"info","span":"973d3515d269fbff","trace":"c3c8d27a51dc0f9aaad906f67f10739d"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.538+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2034.5ms)","duration":"2034.5ms","level":"slow","span":"b00c0db209e4a57c","trace":"e905c3b542cc69d904c58d440f2bdfbe"}
|
||||
{"@timestamp":"2026-06-25T17:34:13.538+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2034.5ms","level":"info","span":"b00c0db209e4a57c","trace":"e905c3b542cc69d904c58d440f2bdfbe"}
|
||||
{"@timestamp":"2026-06-25T17:34:15.264+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55517 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.6ms","level":"info","span":"15046a0c56108d0d","trace":"ed13c4023911ef36a7c3ab00b8eb601b"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.227+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55523 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.3ms","level":"info","span":"63bd15555e2ad4f6","trace":"20805b5a6a194019c762c96fdf77481c"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.230+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55522 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.9ms","level":"info","span":"193a63f058a5a407","trace":"0b3add8c84563b9d8a1cc76d7e923c75"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.236+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55527 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.9ms","level":"info","span":"0d6f0e38bcc544dd","trace":"6e531fba3a04700c3d41dafb0d85de00"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.237+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:55526 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.4ms","level":"info","span":"9ade351ba23e98b1","trace":"55f3065710880297345462d524e4d22a"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.243+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55521 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"18.0ms","level":"info","span":"f159c0c225117347","trace":"0637774e9aedef60f5f35dc1b95991fb"}
|
||||
{"@timestamp":"2026-06-25T17:34:16.250+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55529 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.6ms","level":"info","span":"1d41091e46c55919","trace":"bae55464a7aa3a4560a023e14bca3a99"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.022+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55535 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.5ms","level":"info","span":"c9295ac3ec07382e","trace":"18a8409ade0c78bd800a280b03bc8bda"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.022+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55533 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.9ms","level":"info","span":"40baa4e6a7825420","trace":"215a3fefe6bd654286ac03130b39eee8"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.022+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55534 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.9ms","level":"info","span":"89b56c79ceb6d6ca","trace":"ed516227a2db16352b36d3bfe686f560"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.026+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8 - 127.0.0.1:55539 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.3ms","level":"info","span":"008a0d02d1227d76","trace":"2053d1fa6f5e0b35954fc3443a2136c5"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.027+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/personas/d539b02a-ea8e-4d6e-8ebb-3d94a1f31cb8/copy-missions - 127.0.0.1:55541 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.3ms","level":"info","span":"7268f09956de0acf","trace":"4e62ffe935e050a7a0a95ffbf10f6d97"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.027+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55540 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.6ms","level":"info","span":"803b24364aedcfc6","trace":"ebdb018aec2678b6c74733974e030e5f"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.031+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55543 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.8ms","level":"info","span":"adeca56146be8aaf","trace":"8f1cc00206ee54dd00b6834049923778"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.034+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8 - 127.0.0.1:55548 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.3ms","level":"info","span":"32049a924f5ae43d","trace":"504423223c86cf78724f872097d80d5a"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.035+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55551 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"0.9ms","level":"info","span":"55c16eef394ba771","trace":"6e9f7fd8bdab24c5a6ade3343550a2e6"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.037+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:55546 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.5ms","level":"info","span":"231f87d8841e869c","trace":"5eb008225ced180fc32ce5e585876b6e"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.037+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55553 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.1ms","level":"info","span":"58276bde115780a9","trace":"43b7b943cbc8fca865d4674204d21779"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.038+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/bf9e27eb-d529-4ef5-be11-20870194a4b8/connection - 127.0.0.1:55550 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.4ms","level":"info","span":"b4e924e714587574","trace":"fc5701e6ccaefe8f4e5d4f43395a79e4"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.042+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:55556 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.2ms","level":"info","span":"e7df1756a3e8db26","trace":"a9e55bf54acd83a7f916e9c3b40c9d00"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.048+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/51f4d954-b152-4127-bfb9-e47bde06d1f8/copy-missions - 127.0.0.1:55558 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"2.1ms","level":"info","span":"09ef1c864b6ca639","trace":"5bcfdf427812c467f8e1df7fd7ef459d"}
|
||||
{"@timestamp":"2026-06-25T17:34:17.261+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55560 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.0ms","level":"info","span":"126f4dbdeec6f9ef","trace":"3d743f615986dcbe18559139486ef4b3"}
|
||||
{"@timestamp":"2026-06-25T17:34:18.582+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2040.3ms)","duration":"2040.3ms","level":"slow","span":"2d9b54c5b0b86800","trace":"a0bb3d0b3f12e06400af2833e441739c"}
|
||||
{"@timestamp":"2026-06-25T17:34:18.582+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2040.3ms","level":"info","span":"2d9b54c5b0b86800","trace":"a0bb3d0b3f12e06400af2833e441739c"}
|
||||
{"@timestamp":"2026-06-25T17:34:20.207+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55562 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"5.2ms","level":"info","span":"c3b1673f60408246","trace":"4e2eaf72dd42b52b9613344d48ee9f5d"}
|
||||
{"@timestamp":"2026-06-25T17:34:22.204+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55564 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"4.9ms","level":"info","span":"8d6b9a718d300d64","trace":"7966693ed54d53352b4e60acbb2f7df9"}
|
||||
{"@timestamp":"2026-06-25T17:34:23.614+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node - slowcall(2028.4ms)","duration":"2028.4ms","level":"slow","span":"7d9ad1fced3ee7ce","trace":"53f8a448da91d69a57eabef7359056f7"}
|
||||
{"@timestamp":"2026-06-25T17:34:23.614+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:55162 - node","duration":"2028.4ms","level":"info","span":"7d9ad1fced3ee7ce","trace":"53f8a448da91d69a57eabef7359056f7"}
|
||||
{"@timestamp":"2026-06-25T17:34:24.203+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55566 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"3.5ms","level":"info","span":"91d37732e5e293da","trace":"de4e83de346a4a7ca03ad1afc9b0d2f1"}
|
||||
{"@timestamp":"2026-06-25T17:34:26.200+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55568 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36","duration":"1.8ms","level":"info","span":"c1ce92f78e053dce","trace":"aef13880fae066abcbc1c0c4f8c6c720"}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
> vite
|
||||
|
||||
|
||||
VITE v6.4.3 ready in 174 ms
|
||||
VITE v6.4.3 ready in 130 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: use --host to expose
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
> haixun-master@0.1.0 worker:style-8d
|
||||
> . scripts/playwright-env.sh && npx playwright install chromium && tsx haixun-backend/worker/style-8d-worker.ts
|
||||
|
||||
[8d-worker] started id=local-style-8d-node-26520 api=http://127.0.0.1:8890
|
||||
[8d-worker] started id=local-style-8d-node-51650 api=http://127.0.0.1:8890
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
26440
|
||||
51572
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
26441
|
||||
51573
|
||||
|
|
|
|||
|
|
@ -9,11 +9,17 @@ type (
|
|||
}
|
||||
|
||||
CopySimilarAccountData {
|
||||
Username string `json:"username"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Confidence string `json:"confidence,omitempty"`
|
||||
ProfileUrl string `json:"profile_url,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Confidence string `json:"confidence,omitempty"`
|
||||
ProfileUrl string `json:"profile_url,omitempty"`
|
||||
AuthorVerified bool `json:"author_verified,omitempty"`
|
||||
FollowerCount int `json:"follower_count,omitempty"`
|
||||
EngagementScore int `json:"engagement_score,omitempty"`
|
||||
LikeCount int `json:"like_count,omitempty"`
|
||||
ReplyCount int `json:"reply_count,omitempty"`
|
||||
PostCount int `json:"post_count,omitempty"`
|
||||
}
|
||||
|
||||
CopyMissionResearchMapData {
|
||||
|
|
|
|||
|
|
@ -242,4 +242,7 @@ service gateway {
|
|||
|
||||
@handler disableJobSchedule
|
||||
post /job/schedules/:id/disable (JobScheduleIDPath) returns (JobScheduleData)
|
||||
|
||||
@handler deleteJobSchedule
|
||||
delete /job/schedules/:id (JobScheduleIDPath)
|
||||
}
|
||||
|
|
@ -88,6 +88,8 @@ type (
|
|||
SearchTag string `json:"search_tag"`
|
||||
Permalink string `json:"permalink"`
|
||||
Author string `json:"author"`
|
||||
AuthorVerified bool `json:"author_verified,omitempty"`
|
||||
FollowerCount int `json:"follower_count,omitempty"`
|
||||
Text string `json:"text"`
|
||||
LikeCount int `json:"like_count"`
|
||||
ReplyCount int `json:"reply_count"`
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ func InspireCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||
data, err := l.InspireCopyMission(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ func StartCopyMissionCopyDraftJobHandler(svcCtx *svc.ServiceContext) http.Handle
|
|||
data, err := l.StartCopyMissionCopyDraftJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ func StartCopyMissionMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFu
|
|||
data, err := l.StartCopyMissionMatrixJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DeleteJobScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobScheduleIDPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewDeleteJobScheduleLogic(r.Context(), svcCtx)
|
||||
err := l.DeleteJobSchedule(&req)
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -226,16 +226,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:personaId/copy-missions",
|
||||
Handler: copy_mission.ListCopyMissionsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:personaId/copy-mission-inspiration",
|
||||
Handler: copy_mission.InspireCopyMissionHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:personaId/copy-missions",
|
||||
Handler: copy_mission.ListCopyMissionsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:personaId/copy-missions",
|
||||
|
|
@ -261,6 +261,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
Path: "/:personaId/copy-missions/:id/analyze-jobs",
|
||||
Handler: copy_mission.StartCopyMissionAnalyzeJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:personaId/copy-missions/:id/copy-draft-jobs",
|
||||
Handler: copy_mission.StartCopyMissionCopyDraftJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:personaId/copy-missions/:id/copy-drafts",
|
||||
|
|
@ -276,11 +281,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
Path: "/:personaId/copy-missions/:id/matrix-jobs",
|
||||
Handler: copy_mission.StartCopyMissionMatrixJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:personaId/copy-missions/:id/copy-draft-jobs",
|
||||
Handler: copy_mission.StartCopyMissionCopyDraftJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:personaId/copy-missions/:id/scan-jobs",
|
||||
|
|
@ -306,85 +306,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithPrefix("/api/v1/personas"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.ListJobSchedulesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.CreateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/schedules/:id",
|
||||
Handler: job.UpdateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/disable",
|
||||
Handler: job.DisableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/enable",
|
||||
Handler: job.EnableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates",
|
||||
Handler: job.ListJobTemplatesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.GetJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.UpsertJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs",
|
||||
Handler: job.CreateJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs",
|
||||
Handler: job.ListJobsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id",
|
||||
Handler: job.GetJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/cancel",
|
||||
Handler: job.CancelJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id/events",
|
||||
Handler: job.ListJobEventsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/retry",
|
||||
Handler: job.RetryJobHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.WorkerSecret},
|
||||
|
|
@ -444,6 +365,90 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithPrefix("/api/v1/internal"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.ListJobSchedulesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.CreateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/schedules/:id",
|
||||
Handler: job.UpdateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/job/schedules/:id",
|
||||
Handler: job.DeleteJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/disable",
|
||||
Handler: job.DisableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/enable",
|
||||
Handler: job.EnableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates",
|
||||
Handler: job.ListJobTemplatesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.GetJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.UpsertJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs",
|
||||
Handler: job.CreateJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs",
|
||||
Handler: job.ListJobsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id",
|
||||
Handler: job.GetJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/cancel",
|
||||
Handler: job.CancelJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id/events",
|
||||
Handler: job.ListJobEventsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/retry",
|
||||
Handler: job.RetryJobHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
|
|
|
|||
|
|
@ -52,4 +52,4 @@ func FormatViralSamples(posts []ViralPostSample) string {
|
|||
}
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package permmatch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MethodAllowed reports whether method is covered by a pipe-separated methods string.
|
||||
func MethodAllowed(methods, method string) bool {
|
||||
method = strings.ToUpper(strings.TrimSpace(method))
|
||||
if method == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range strings.Split(methods, "|") {
|
||||
if strings.ToUpper(strings.TrimSpace(item)) == method {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PathAllowed reports whether requestPath matches pattern (exact or trailing * wildcard).
|
||||
func PathAllowed(pattern, requestPath string) bool {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
requestPath = strings.TrimSpace(requestPath)
|
||||
if pattern == "" || requestPath == "" {
|
||||
return false
|
||||
}
|
||||
if pattern == requestPath {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
prefix := strings.TrimRight(strings.TrimSuffix(pattern, "*"), "/")
|
||||
if requestPath == prefix {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(requestPath, prefix+"/")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RequestAllowed checks member permission map against an HTTP request.
|
||||
func RequestAllowed(permissions map[string]string, method, requestPath string) bool {
|
||||
if len(permissions) == 0 {
|
||||
return false
|
||||
}
|
||||
for pattern, methods := range permissions {
|
||||
if PathAllowed(pattern, requestPath) && MethodAllowed(methods, method) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package permmatch
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMethodAllowed(t *testing.T) {
|
||||
if !MethodAllowed("GET|POST", "get") {
|
||||
t.Fatal("expected GET")
|
||||
}
|
||||
if MethodAllowed("GET|POST", "PATCH") {
|
||||
t.Fatal("expected no PATCH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathAllowed(t *testing.T) {
|
||||
if !PathAllowed("/api/v1/jobs/*", "/api/v1/jobs/abc/cancel") {
|
||||
t.Fatal("expected wildcard match")
|
||||
}
|
||||
if PathAllowed("/api/v1/jobs/*", "/api/v1/job/schedules") {
|
||||
t.Fatal("expected no match")
|
||||
}
|
||||
if !PathAllowed("/api/v1/members/me", "/api/v1/members/me") {
|
||||
t.Fatal("expected exact match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestAllowed(t *testing.T) {
|
||||
perms := map[string]string{
|
||||
"/api/v1/members/me": "GET|PATCH",
|
||||
"/api/v1/jobs/*": "GET|POST",
|
||||
}
|
||||
if !RequestAllowed(perms, "GET", "/api/v1/members/me") {
|
||||
t.Fatal("expected member me")
|
||||
}
|
||||
if RequestAllowed(perms, "DELETE", "/api/v1/members/me") {
|
||||
t.Fatal("expected deny delete")
|
||||
}
|
||||
if !RequestAllowed(perms, "POST", "/api/v1/jobs/x/cancel") {
|
||||
t.Fatal("expected job cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathAllowedListRoot(t *testing.T) {
|
||||
for _, path := range []string{"/api/v1/personas", "/api/v1/jobs", "/api/v1/brands"} {
|
||||
pattern := path + "/*"
|
||||
if !PathAllowed(pattern, path) {
|
||||
t.Fatalf("expected list root match for %s", path)
|
||||
}
|
||||
}
|
||||
if PathAllowed("/api/v1/jobs/*", "/api/v1/job/schedules") {
|
||||
t.Fatal("expected schedules path to stay blocked")
|
||||
}
|
||||
}
|
||||
|
|
@ -22,14 +22,14 @@ type execCrawlerInput struct {
|
|||
}
|
||||
|
||||
type execCrawlerPost struct {
|
||||
Text string `json:"text"`
|
||||
Permalink string `json:"permalink"`
|
||||
ExternalID string `json:"externalId"`
|
||||
AuthorName string `json:"authorName"`
|
||||
LikeCount int `json:"likeCount"`
|
||||
ReplyCount int `json:"replyCount"`
|
||||
AuthorVerified bool `json:"authorVerified"`
|
||||
FollowerCount int `json:"followerCount"`
|
||||
Text string `json:"text"`
|
||||
Permalink string `json:"permalink"`
|
||||
ExternalID string `json:"externalId"`
|
||||
AuthorName string `json:"authorName"`
|
||||
LikeCount int `json:"likeCount"`
|
||||
ReplyCount int `json:"replyCount"`
|
||||
AuthorVerified bool `json:"authorVerified"`
|
||||
FollowerCount int `json:"followerCount"`
|
||||
}
|
||||
|
||||
type execCrawlerOutput struct {
|
||||
|
|
|
|||
|
|
@ -106,4 +106,4 @@ func Discover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, Discove
|
|||
}
|
||||
|
||||
return nil, "", fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,4 +110,4 @@ func runCrawlerDiscover(ctx context.Context, req DiscoverRequest) ([]DiscoverPos
|
|||
return nil, fmt.Errorf("crawler keyword is empty")
|
||||
}
|
||||
return req.Crawler(ctx, req.Member, keyword, req.Limit)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import "testing"
|
|||
|
||||
func TestShouldTryCrawlerFirst_mixedWithBrowser(t *testing.T) {
|
||||
m := MemberContext{
|
||||
AllowsCrawler: true,
|
||||
BrowserConnected: true,
|
||||
SearchSourceMode: SearchSourceMixed,
|
||||
AllowsThreadsAPI: true,
|
||||
ApiConnected: true,
|
||||
AllowsCrawler: true,
|
||||
BrowserConnected: true,
|
||||
SearchSourceMode: SearchSourceMixed,
|
||||
AllowsThreadsAPI: true,
|
||||
ApiConnected: true,
|
||||
}
|
||||
if !ShouldTryCrawlerFirst(m) {
|
||||
t.Fatal("mixed + browser should try crawler first to save API")
|
||||
|
|
@ -38,4 +38,4 @@ func TestBuildMemberContextFormalModeKeepsCrawlerMode(t *testing.T) {
|
|||
if !ctx.AllowsThreadsAPI {
|
||||
t.Fatal("threads_crawler should still allow API fallback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ func EffectiveExpandStrategy(research ResearchSettings) libkg.ExpandStrategy {
|
|||
|
||||
func WebSearchAvailable(research ResearchSettings) bool {
|
||||
return !MissingWebSearchKey(research)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,4 @@ func TestWebSearchAvailable(t *testing.T) {
|
|||
if !WebSearchAvailable(ResearchSettings{WebSearchProvider: "brave", BraveAPIKey: "k"}) {
|
||||
t.Fatal("expected available with brave key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,4 +101,4 @@ func ResolvePersonaBlock(personaText, styleProfileJSON, brief string) string {
|
|||
parts = append(parts, block)
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,4 @@ func TestHasReady8DFromAnalysisOnly(t *testing.T) {
|
|||
if !HasReady8D("", raw) {
|
||||
t.Fatal("expected ready from analysis summary")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ const ViralSoftWarn = 300
|
|||
type LengthBand string
|
||||
|
||||
const (
|
||||
BandEmpty LengthBand = "empty"
|
||||
BandTooShort LengthBand = "too_short"
|
||||
BandSweet LengthBand = "sweet"
|
||||
BandLong LengthBand = "long"
|
||||
BandOverSoft LengthBand = "over_soft"
|
||||
BandOverHard LengthBand = "over_hard"
|
||||
BandEmpty LengthBand = "empty"
|
||||
BandTooShort LengthBand = "too_short"
|
||||
BandSweet LengthBand = "sweet"
|
||||
BandLong LengthBand = "long"
|
||||
BandOverSoft LengthBand = "over_soft"
|
||||
BandOverHard LengthBand = "over_hard"
|
||||
)
|
||||
|
||||
func RuneLen(text string) int {
|
||||
|
|
@ -97,4 +97,4 @@ func PublishHint(text string) string {
|
|||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,4 +46,4 @@ func TestPublishBand(t *testing.T) {
|
|||
t.Fatalf("len %d: got %s want %s", c.len, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,4 +230,4 @@ func sortByEngagement(items []placement.ScanCandidate) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ func TestCountMissionQuality(t *testing.T) {
|
|||
if got := countMissionQuality(merged); got != 1 {
|
||||
t.Fatalf("expected 1 quality post, got %d", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,4 +69,4 @@ func TestRunDiscover_missionRelaxedFallbackWithoutVerified(t *testing.T) {
|
|||
if out[0].AuthorVerified {
|
||||
t.Fatal("verified should remain false when API omits it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,4 +138,4 @@ func pickRawMessage(root map[string]json.RawMessage, keys ...string) json.RawMes
|
|||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ import (
|
|||
)
|
||||
|
||||
type MissionInspireInput struct {
|
||||
PersonaDisplayName string
|
||||
PersonaBrief string
|
||||
PersonaBlock string
|
||||
StyleBenchmark string
|
||||
PersonaAudience string
|
||||
PersonaContentGoal string
|
||||
PersonaQuestions []string
|
||||
PersonaPillars []string
|
||||
RecentMissionLabels []string
|
||||
RecentSeedQueries []string
|
||||
TrendSnippets []MissionInspireTrendSnippet
|
||||
WebSearchProvider string
|
||||
LLMOnly bool
|
||||
PersonaDisplayName string
|
||||
PersonaBrief string
|
||||
PersonaBlock string
|
||||
StyleBenchmark string
|
||||
PersonaAudience string
|
||||
PersonaContentGoal string
|
||||
PersonaQuestions []string
|
||||
PersonaPillars []string
|
||||
RecentMissionLabels []string
|
||||
RecentSeedQueries []string
|
||||
TrendSnippets []MissionInspireTrendSnippet
|
||||
WebSearchProvider string
|
||||
LLMOnly bool
|
||||
}
|
||||
|
||||
type MissionInspireTrendSnippet struct {
|
||||
|
|
@ -30,11 +30,11 @@ type MissionInspireTrendSnippet struct {
|
|||
}
|
||||
|
||||
type MissionInspireOutput struct {
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
TrendReason string
|
||||
TrendKeywords []string
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
TrendReason string
|
||||
TrendKeywords []string
|
||||
}
|
||||
|
||||
func BuildMissionInspireSystemPrompt() string {
|
||||
|
|
@ -175,4 +175,4 @@ func ParseMissionInspireOutput(raw string) (MissionInspireOutput, error) {
|
|||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,4 +63,4 @@ func CollectMissionInspireTrends(
|
|||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ func TestParseMissionInspireOutput(t *testing.T) {
|
|||
func TestBuildMissionInspireUserPromptLLMOnly(t *testing.T) {
|
||||
prompt := BuildMissionInspireUserPrompt(MissionInspireInput{
|
||||
PersonaDisplayName: "測試人設",
|
||||
PersonaBrief: "職場焦慮",
|
||||
LLMOnly: true,
|
||||
PersonaBrief: "職場焦慮",
|
||||
LLMOnly: true,
|
||||
})
|
||||
if !strings.Contains(prompt, "未設定 Web Search API key") {
|
||||
t.Fatalf("expected llm-only hint in prompt: %s", prompt)
|
||||
|
|
@ -31,4 +31,4 @@ func TestBuildMissionInspireUserPromptLLMOnly(t *testing.T) {
|
|||
if !strings.Contains(prompt, "無外部趨勢結果") {
|
||||
t.Fatalf("expected empty trend fallback in prompt: %s", prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,4 +52,4 @@ func TestParseMissionResearchMapOutput_StringSuggestedTags(t *testing.T) {
|
|||
if len(out.SuggestedTags) != 4 {
|
||||
t.Fatalf("expected 4 tags, got %#v", out.SuggestedTags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import "haixun-backend/internal/library/placement"
|
|||
|
||||
// Mission-quality gates for copy-mission viral patrol (stricter than generic patrol).
|
||||
const (
|
||||
MissionQualityMinLikes = 18
|
||||
MissionQualityMinEngagement = 50
|
||||
MissionVerifiedMinLikes = 10
|
||||
MissionVerifiedMinEngagement = 35
|
||||
MissionQualityMinLikes = 18
|
||||
MissionQualityMinEngagement = 50
|
||||
MissionVerifiedMinLikes = 10
|
||||
MissionVerifiedMinEngagement = 35
|
||||
MissionInfluencerMinFollowers = 5000
|
||||
)
|
||||
|
||||
|
|
@ -36,4 +36,4 @@ func MergeAuthorSignals(prev, next placement.ScanCandidate) placement.ScanCandid
|
|||
prev.FollowerCount = next.FollowerCount
|
||||
}
|
||||
return prev
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ func TestPassesMissionQualityCandidate_unverifiedStricter(t *testing.T) {
|
|||
if !PassesMissionQualityCandidate("轉職面試技巧分享心得", 25, 4, 65, false, 0, nil) {
|
||||
t.Fatal("unverified author should pass with strong engagement")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,4 +198,4 @@ func formatReferenceReason(item referenceAuthorAgg) string {
|
|||
return fmt.Sprintf("標籤「%s」高互動作者", item.sampleSearchTag)
|
||||
}
|
||||
return "本次海巡高互動作者"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,4 +21,4 @@ func TestBuildReferenceAccountsFromScan_relaxedFallback(t *testing.T) {
|
|||
if got[0].AuthorVerified {
|
||||
t.Fatal("verified must stay false when not provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,4 +55,4 @@ func TestBuildReferenceAccountsFromScan_requiresTopicMatch(t *testing.T) {
|
|||
if len(got) != 0 {
|
||||
t.Fatalf("expected no accounts for off-topic post, got %d", len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ type CopyResearchMap struct {
|
|||
}
|
||||
|
||||
type CopyResearchMapInput struct {
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
Persona string
|
||||
StyleBenchmark string
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
Persona string
|
||||
StyleBenchmark string
|
||||
PersonaAudienceSummary string
|
||||
PersonaContentGoal string
|
||||
PersonaQuestions []string
|
||||
PersonaPillars []string
|
||||
PersonaContentGoal string
|
||||
PersonaQuestions []string
|
||||
PersonaPillars []string
|
||||
}
|
||||
|
||||
var copyMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/library/authctx"
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func RequireAdmin(ctx context.Context, svcCtx *svc.ServiceContext) error {
|
||||
actor, ok := authctx.ActorFromContext(ctx)
|
||||
if !ok {
|
||||
return app.For(code.Auth).AuthUnauthorized("missing actor")
|
||||
}
|
||||
member, err := svcCtx.Member.GetByUID(ctx, actor.TenantID, actor.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, role := range member.Roles {
|
||||
if role == "admin" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return app.For(code.Auth).AuthForbidden("admin role required")
|
||||
}
|
||||
|
|
@ -151,4 +151,4 @@ func (l *InspireCopyMissionLogic) InspireCopyMission(req *types.PersonaCopyMissi
|
|||
WebSearchUsed: webSearchUsed,
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,4 +74,4 @@ func (l *StartCopyMissionCopyDraftJobLogic) StartCopyMissionCopyDraftJob(
|
|||
Status: string(run.Status),
|
||||
Message: "深仿寫已在背景執行",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ func (l *StartCopyMissionMatrixJobLogic) StartCopyMissionMatrixJob(
|
|||
|
||||
payload := map[string]any{
|
||||
"persona_id": personaID,
|
||||
"copy_mission_id": missionID,
|
||||
"count": count,
|
||||
"copy_mission_id": missionID,
|
||||
"count": count,
|
||||
}
|
||||
run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{
|
||||
TemplateType: "generate-copy-matrix",
|
||||
|
|
@ -80,4 +80,4 @@ func (l *StartCopyMissionMatrixJobLogic) StartCopyMissionMatrixJob(
|
|||
Status: string(run.Status),
|
||||
Message: "內容矩陣產出已在背景執行",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"haixun-backend/internal/library/authctx"
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
"haixun-backend/internal/logic/authz"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func actorFrom(ctx context.Context) (tenantID, uid string, err error) {
|
||||
|
|
@ -15,3 +17,7 @@ func actorFrom(ctx context.Context) (tenantID, uid string, err error) {
|
|||
}
|
||||
return actor.TenantID, actor.UID, nil
|
||||
}
|
||||
|
||||
func requireAdmin(ctx context.Context, svcCtx *svc.ServiceContext) error {
|
||||
return authz.RequireAdmin(ctx, svcCtx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type DeleteJobScheduleLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDeleteJobScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteJobScheduleLogic {
|
||||
return &DeleteJobScheduleLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DeleteJobScheduleLogic) DeleteJobSchedule(req *types.JobScheduleIDPath) error {
|
||||
return l.svcCtx.Job.DeleteSchedule(l.ctx, req.ID)
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@ func NewGetJobTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Ge
|
|||
}
|
||||
|
||||
func (l *GetJobTemplateLogic) GetJobTemplate(req *types.JobTemplatePath) (*types.JobTemplateData, error) {
|
||||
if err := requireAdmin(l.ctx, l.svcCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template, err := l.svcCtx.Job.GetTemplate(l.ctx, req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ func NewListJobTemplatesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *
|
|||
}
|
||||
|
||||
func (l *ListJobTemplatesLogic) ListJobTemplates() (*types.JobTemplateListData, error) {
|
||||
if err := requireAdmin(l.ctx, l.svcCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templates, err := l.svcCtx.Job.ListTemplates(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ func NewUpsertJobTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
|||
}
|
||||
|
||||
func (l *UpsertJobTemplateLogic) UpsertJobTemplate(req *types.UpsertJobTemplateReq) (*types.JobTemplateData, error) {
|
||||
if err := requireAdmin(l.ctx, l.svcCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template, err := l.svcCtx.Job.UpsertTemplate(l.ctx, domusecase.UpsertTemplateRequest{
|
||||
Type: req.Type,
|
||||
Version: req.Version,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package permission
|
|||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/logic/authz"
|
||||
domusecase "haixun-backend/internal/model/permission/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
|
@ -18,6 +19,9 @@ func NewGetPermissionCatalogLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
|||
}
|
||||
|
||||
func (l *GetPermissionCatalogLogic) GetPermissionCatalog(req *types.PermissionCatalogQuery) (*types.PermissionCatalogData, error) {
|
||||
if err := authz.RequireAdmin(l.ctx, l.svcCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, list, err := l.svcCtx.Permission.Catalog(l.ctx, domusecase.CatalogRequest{
|
||||
Status: req.Status,
|
||||
Type: req.Type,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/library/authctx"
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
"haixun-backend/internal/library/permmatch"
|
||||
memberdomain "haixun-backend/internal/model/member/domain/usecase"
|
||||
permissiondomain "haixun-backend/internal/model/permission/domain/usecase"
|
||||
"haixun-backend/internal/response"
|
||||
)
|
||||
|
||||
// PermissionRBACMiddleware enforces catalog permissions for authenticated members.
|
||||
// Mount after AuthJWT or MemberAuth so actor is present in context.
|
||||
type PermissionRBACMiddleware struct {
|
||||
members memberdomain.UseCase
|
||||
permissions permissiondomain.UseCase
|
||||
}
|
||||
|
||||
func NewPermissionRBACMiddleware(
|
||||
members memberdomain.UseCase,
|
||||
permissions permissiondomain.UseCase,
|
||||
) *PermissionRBACMiddleware {
|
||||
return &PermissionRBACMiddleware{members: members, permissions: permissions}
|
||||
}
|
||||
|
||||
func (m *PermissionRBACMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
actor, ok := authctx.ActorFromContext(r.Context())
|
||||
if !ok {
|
||||
response.Write(r.Context(), w, nil, app.For(code.Auth).AuthUnauthorized("missing actor"))
|
||||
return
|
||||
}
|
||||
if m.members == nil || m.permissions == nil {
|
||||
response.Write(r.Context(), w, nil, app.For(code.Permission).SysNotImplemented("permission rbac is not configured"))
|
||||
return
|
||||
}
|
||||
|
||||
member, err := m.members.GetByUID(r.Context(), actor.TenantID, actor.UID)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
me, err := m.permissions.Me(r.Context(), member, false)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
if !permmatch.RequestAllowed(me.Permissions, r.Method, r.URL.Path) {
|
||||
response.Write(r.Context(), w, nil, app.For(code.Permission).AuthForbidden("permission denied"))
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
|
@ -53,13 +53,13 @@ type MissionSummary struct {
|
|||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
TenantID string
|
||||
OwnerUID string
|
||||
PersonaID string
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
InitialResearchMap *entity.ResearchMap
|
||||
TenantID string
|
||||
OwnerUID string
|
||||
PersonaID string
|
||||
Label string
|
||||
SeedQuery string
|
||||
Brief string
|
||||
InitialResearchMap *entity.ResearchMap
|
||||
InitialSelectedTags []string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type ScheduleRepository interface {
|
|||
Create(ctx context.Context, schedule *entity.Schedule) (*entity.Schedule, error)
|
||||
Update(ctx context.Context, schedule *entity.Schedule) (*entity.Schedule, error)
|
||||
FindByID(ctx context.Context, id string) (*entity.Schedule, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context, filter ScheduleListFilter, offset, limit int64) ([]*entity.Schedule, int64, error)
|
||||
FindDue(ctx context.Context, now int64, limit int64) ([]*entity.Schedule, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ type UseCase interface {
|
|||
UpdateSchedule(ctx context.Context, req UpdateScheduleRequest) (*entity.Schedule, error)
|
||||
EnableSchedule(ctx context.Context, scheduleID string) (*entity.Schedule, error)
|
||||
DisableSchedule(ctx context.Context, scheduleID string) (*entity.Schedule, error)
|
||||
DeleteSchedule(ctx context.Context, scheduleID string) error
|
||||
|
||||
RunSchedulerTick(ctx context.Context, holder string) (int, error)
|
||||
RunMaintenance(ctx context.Context) (MaintenanceResult, error)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,24 @@ func (r *mongoScheduleRepository) Update(ctx context.Context, schedule *entity.S
|
|||
return &out, nil
|
||||
}
|
||||
|
||||
func (r *mongoScheduleRepository) Delete(ctx context.Context, id string) error {
|
||||
if r.collection == nil {
|
||||
return app.For(code.Job).DBUnavailable("Mongo is not configured")
|
||||
}
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return app.For(code.Job).InputInvalidFormat("invalid schedule id")
|
||||
}
|
||||
res, err := r.collection.DeleteOne(ctx, bson.M{"_id": objectID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.DeletedCount == 0 {
|
||||
return app.For(code.Job).ResNotFound("job schedule not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mongoScheduleRepository) FindByID(ctx context.Context, id string) (*entity.Schedule, error) {
|
||||
if r.collection == nil {
|
||||
return nil, app.For(code.Job).DBUnavailable("Mongo is not configured")
|
||||
|
|
|
|||
|
|
@ -125,6 +125,16 @@ func (u *jobUseCase) DisableSchedule(ctx context.Context, scheduleID string) (*e
|
|||
return u.schedules.Update(ctx, schedule)
|
||||
}
|
||||
|
||||
func (u *jobUseCase) DeleteSchedule(ctx context.Context, scheduleID string) error {
|
||||
if strings.TrimSpace(scheduleID) == "" {
|
||||
return app.For(code.Job).InputMissingRequired("schedule id is required")
|
||||
}
|
||||
if _, err := u.schedules.FindByID(ctx, scheduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.schedules.Delete(ctx, scheduleID)
|
||||
}
|
||||
|
||||
func (u *jobUseCase) RunSchedulerTick(ctx context.Context, holder string) (int, error) {
|
||||
ok, err := u.queue.TrySchedulerLock(ctx, holder, 55)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,21 @@ func (m *memoryScheduleRepo) FindByID(_ context.Context, id string) (*entity.Sch
|
|||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (m *memoryScheduleRepo) Delete(_ context.Context, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, item := range m.schedules {
|
||||
if item.ID == objectID {
|
||||
m.schedules = append(m.schedules[:i], m.schedules[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *memoryScheduleRepo) List(_ context.Context, _ domrepo.ScheduleListFilter, _, _ int64) ([]*entity.Schedule, int64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package usecase
|
|||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
memberentity "haixun-backend/internal/model/member/domain/entity"
|
||||
"haixun-backend/internal/model/permission/domain/entity"
|
||||
|
|
@ -111,9 +112,14 @@ func (u *permissionUseCase) Me(ctx context.Context, member *memberentity.Member,
|
|||
Permissions: map[string]string{},
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.HTTPPath != "" && node.HTTPMethods != "" {
|
||||
result.Permissions[node.HTTPPath] = node.HTTPMethods
|
||||
if node.HTTPPath == "" || node.HTTPMethods == "" {
|
||||
continue
|
||||
}
|
||||
if existing, ok := result.Permissions[node.HTTPPath]; ok {
|
||||
result.Permissions[node.HTTPPath] = mergeHTTPMethods(existing, node.HTTPMethods)
|
||||
continue
|
||||
}
|
||||
result.Permissions[node.HTTPPath] = node.HTTPMethods
|
||||
}
|
||||
if includeTree {
|
||||
result.Tree = buildTree(nodes)
|
||||
|
|
@ -121,6 +127,25 @@ func (u *permissionUseCase) Me(ctx context.Context, member *memberentity.Member,
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func mergeHTTPMethods(existing, next string) string {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, 8)
|
||||
for _, block := range []string{existing, next} {
|
||||
for _, item := range strings.Split(block, "|") {
|
||||
method := strings.ToUpper(strings.TrimSpace(item))
|
||||
if method == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[method]; ok {
|
||||
continue
|
||||
}
|
||||
seen[method] = struct{}{}
|
||||
out = append(out, method)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, "|")
|
||||
}
|
||||
|
||||
func hasRole(roles []string, role string) bool {
|
||||
for _, item := range roles {
|
||||
if item == role {
|
||||
|
|
@ -146,10 +171,20 @@ func filterPermissionsByName(items []*entity.Permission, names []string) []*enti
|
|||
|
||||
func defaultUserPermissionNames() []string {
|
||||
return []string{
|
||||
"auth.logout",
|
||||
"member.me.read",
|
||||
"member.me.update",
|
||||
"member.placement_settings.read",
|
||||
"member.placement_settings.update",
|
||||
"permission.me.read",
|
||||
"setting.manage",
|
||||
"ai.use",
|
||||
"brand.manage",
|
||||
"persona.manage",
|
||||
"placement_topic.manage",
|
||||
"threads_account.manage",
|
||||
"job.manage",
|
||||
"job.schedule.manage",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,14 +231,28 @@ func buildTree(items []domusecase.PermissionNode) []domusecase.PermissionNode {
|
|||
}
|
||||
|
||||
func defaultPermissions() []*entity.Permission {
|
||||
allMethods := "GET|POST|PUT|PATCH|DELETE"
|
||||
return []*entity.Permission{
|
||||
{Name: "auth.logout", HTTPMethods: "POST", HTTPPath: "/api/v1/auth/logout", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
|
||||
{Name: "member.me.read", HTTPMethods: "GET", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
{Name: "member.me.update", HTTPMethods: "PATCH", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
{Name: "member.placement_settings.read", HTTPMethods: "GET", HTTPPath: "/api/v1/members/me/placement-settings", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
{Name: "member.placement_settings.update", HTTPMethods: "PATCH", HTTPPath: "/api/v1/members/me/placement-settings", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
|
||||
{Name: "permission.catalog.read", HTTPMethods: "GET", HTTPPath: "/api/v1/permissions/catalog", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "permission.me.read", HTTPMethods: "GET", HTTPPath: "/api/v1/permissions/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
|
||||
{Name: "setting.manage", HTTPMethods: "GET|PUT|DELETE", HTTPPath: "/api/v1/settings/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "ai.use", HTTPMethods: "GET|POST", HTTPPath: "/api/v1/ai/*", Status: entity.StatusOpen, Type: entity.TypeFrontendUser},
|
||||
{Name: "job.manage", HTTPMethods: "GET|POST|PUT", HTTPPath: "/api/v1/jobs/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
|
||||
{Name: "brand.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/brands/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "persona.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/personas/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "placement_topic.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/placement/topics/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "threads_account.manage", HTTPMethods: "GET|POST|PUT|PATCH", HTTPPath: "/api/v1/threads-accounts/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
|
||||
{Name: "job.manage", HTTPMethods: "GET|POST", HTTPPath: "/api/v1/jobs/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "job.schedule.manage", HTTPMethods: "GET|POST|PUT|DELETE", HTTPPath: "/api/v1/job/schedules/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
{Name: "job.template.manage", HTTPMethods: "GET|PUT", HTTPPath: "/api/v1/job/templates/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package usecase
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMergeHTTPMethods(t *testing.T) {
|
||||
got := mergeHTTPMethods("GET", "PATCH")
|
||||
if got != "GET|PATCH" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
got = mergeHTTPMethods("GET|POST", "POST|PATCH")
|
||||
if got != "GET|POST|PATCH" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +135,9 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := permissionUseCase.EnsureDefaultRolePermissions(ctx, "default"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jobTemplateRepository := jobrepo.NewMongoTemplateRepository(mongoClient.Database())
|
||||
jobRunRepository := jobrepo.NewMongoRunRepository(mongoClient.Database())
|
||||
|
|
|
|||
|
|
@ -230,18 +230,6 @@ type CopyMissionData struct {
|
|||
UpdateAt int64 `json:"update_at"`
|
||||
}
|
||||
|
||||
type CopyMissionPath struct {
|
||||
PersonaID string `path:"personaId" validate:"required"`
|
||||
ID string `path:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type CopyMissionInspirationSourceData struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Snippet string `json:"snippet,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type CopyMissionInspirationData struct {
|
||||
Label string `json:"label"`
|
||||
SeedQuery string `json:"seed_query"`
|
||||
|
|
@ -253,6 +241,18 @@ type CopyMissionInspirationData struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type CopyMissionInspirationSourceData struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Snippet string `json:"snippet,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type CopyMissionPath struct {
|
||||
PersonaID string `path:"personaId" validate:"required"`
|
||||
ID string `path:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type CopyMissionResearchMapData struct {
|
||||
AudienceSummary string `json:"audience_summary,omitempty"`
|
||||
ContentGoal string `json:"content_goal,omitempty"`
|
||||
|
|
@ -1084,19 +1084,19 @@ type StartCopyMissionAnalyzeJobData struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StartCopyMissionScanJobData struct {
|
||||
type StartCopyMissionCopyDraftJobData struct {
|
||||
JobID string `json:"job_id"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StartCopyMissionMatrixJobReq struct {
|
||||
Count int `json:"count,optional"`
|
||||
type StartCopyMissionCopyDraftJobHandlerReq struct {
|
||||
CopyMissionPath
|
||||
StartCopyMissionCopyDraftJobReq
|
||||
}
|
||||
|
||||
type StartCopyMissionMatrixJobHandlerReq struct {
|
||||
CopyMissionPath
|
||||
StartCopyMissionMatrixJobReq
|
||||
type StartCopyMissionCopyDraftJobReq struct {
|
||||
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||
}
|
||||
|
||||
type StartCopyMissionMatrixJobData struct {
|
||||
|
|
@ -1105,16 +1105,16 @@ type StartCopyMissionMatrixJobData struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StartCopyMissionCopyDraftJobReq struct {
|
||||
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||
}
|
||||
|
||||
type StartCopyMissionCopyDraftJobHandlerReq struct {
|
||||
type StartCopyMissionMatrixJobHandlerReq struct {
|
||||
CopyMissionPath
|
||||
StartCopyMissionCopyDraftJobReq
|
||||
StartCopyMissionMatrixJobReq
|
||||
}
|
||||
|
||||
type StartCopyMissionCopyDraftJobData struct {
|
||||
type StartCopyMissionMatrixJobReq struct {
|
||||
Count int `json:"count,optional"`
|
||||
}
|
||||
|
||||
type StartCopyMissionScanJobData struct {
|
||||
JobID string `json:"job_id"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
|
|
|
|||
|
|
@ -207,4 +207,4 @@ func runGenerateCopyDraft(ctx context.Context, step StepContext, deps GenerateCo
|
|||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,4 +243,4 @@ func intField(payload map[string]any, key string) int {
|
|||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
libviral "haixun-backend/internal/library/viral"
|
||||
domai "haixun-backend/internal/model/ai/domain/usecase"
|
||||
aiusecase "haixun-backend/internal/model/ai/usecase"
|
||||
missionentity "haixun-backend/internal/model/copy_mission/domain/entity"
|
||||
copydraftusecase "haixun-backend/internal/model/copy_draft/domain/usecase"
|
||||
missionentity "haixun-backend/internal/model/copy_mission/domain/entity"
|
||||
missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase"
|
||||
jobdom "haixun-backend/internal/model/job/domain/usecase"
|
||||
personaentity "haixun-backend/internal/model/persona/domain/entity"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { AuthProvider } from './auth/AuthContext'
|
||||
import { ThemeProvider } from './theme/ThemeContext'
|
||||
import { AdminRoute } from './components/AdminRoute'
|
||||
import { Layout } from './components/Layout'
|
||||
import { ProtectedRoute } from './components/ProtectedRoute'
|
||||
import { AiPage } from './pages/AiPage'
|
||||
import { DashboardPage } from './pages/DashboardPage'
|
||||
import { JobDetailPage } from './pages/JobDetailPage'
|
||||
import { JobSchedulesPage } from './pages/JobSchedulesPage'
|
||||
import { JobTemplatesPage } from './pages/JobTemplatesPage'
|
||||
import { JobsPage } from './pages/JobsPage'
|
||||
import { LoginPage } from './pages/LoginPage'
|
||||
import { EasterEggsPage } from './pages/EasterEggsPage'
|
||||
import { PermissionsPage } from './pages/PermissionsPage'
|
||||
import { LegacyBrandRouteRedirect } from './components/LegacyBrandRouteRedirect'
|
||||
import { BrandDetailPage } from './pages/BrandDetailPage'
|
||||
|
|
@ -59,12 +60,14 @@ export default function App() {
|
|||
<Route path="/personas/:id/matrix" element={<LegacyBrandRouteRedirect to="/matrix" />} />
|
||||
<Route path="/personas/:id" element={<PersonaDetailPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/permissions" element={<PermissionsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/ai" element={<AiPage />} />
|
||||
<Route path="/jobs" element={<JobsPage />} />
|
||||
<Route path="/jobs/:id" element={<JobDetailPage />} />
|
||||
<Route path="/job-templates" element={<JobTemplatesPage />} />
|
||||
<Route element={<AdminRoute />}>
|
||||
<Route path="/job-templates" element={<JobTemplatesPage />} />
|
||||
<Route path="/permissions" element={<PermissionsPage />} />
|
||||
<Route path="/easter-eggs" element={<EasterEggsPage />} />
|
||||
</Route>
|
||||
<Route path="/job-schedules" element={<JobSchedulesPage />} />
|
||||
<Route path="/threads/:id" element={<ThreadsAccountWorkspace />}>
|
||||
<Route index element={<Navigate to="publish" replace />} />
|
||||
|
|
|
|||
|
|
@ -221,22 +221,3 @@ export async function streamIslanderChat(
|
|||
await consumeAIEventStream(res, onDelta, onDone, onError)
|
||||
}
|
||||
|
||||
export async function streamAIChat(
|
||||
body: Record<string, unknown>,
|
||||
onDelta: (text: string) => void,
|
||||
onDone: (finishReason?: string) => void,
|
||||
onError: (msg: string) => void,
|
||||
) {
|
||||
const memberToken = storage.getAccessToken()
|
||||
const providerToken = storage.getAiProviderToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (memberToken) headers['X-Member-Authorization'] = `Bearer ${memberToken}`
|
||||
if (providerToken) headers.Authorization = `Bearer ${providerToken}`
|
||||
|
||||
const res = await fetch('/api/v1/ai/chat/stream', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
await consumeAIEventStream(res, onDelta, onDone, onError)
|
||||
}
|
||||
|
|
@ -3,73 +3,95 @@ import type { AcAppKey } from '../lib/acAssets'
|
|||
const stroke = {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
strokeWidth: 1.75,
|
||||
strokeWidth: 1.85,
|
||||
strokeLinecap: 'round' as const,
|
||||
strokeLinejoin: 'round' as const,
|
||||
}
|
||||
|
||||
const paths: Record<AcAppKey, React.ReactNode> = {
|
||||
home: <path {...stroke} d="M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1v-9.5Z" />,
|
||||
home: (
|
||||
<>
|
||||
<path {...stroke} d="M4.5 10.8 12 5.2l7.5 5.6V19a1.2 1.2 0 0 1-1.2 1.2H14.8v-5.4H9.2v5.4H5.7A1.2 1.2 0 0 1 4.5 19v-8.2Z" />
|
||||
<path {...stroke} d="M10 19.2v-4.6h4v4.6" />
|
||||
</>
|
||||
),
|
||||
persona: (
|
||||
<>
|
||||
<circle {...stroke} cx="12" cy="8" r="3.5" />
|
||||
<path {...stroke} d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
<path {...stroke} d="M17 5l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z" />
|
||||
<circle {...stroke} cx="12" cy="8.2" r="3.2" />
|
||||
<path {...stroke} d="M5.5 19.5c0-3.4 2.9-5.8 6.5-5.8s6.5 2.4 6.5 5.8" />
|
||||
<path {...stroke} d="M17.2 6.2 18.6 8l2.2.4-1.6 1.4-.4 2.2-1.8-1.1-1.8 1.1.4-2.2-1.6-1.4 2.2-.4 1.4-1.8Z" />
|
||||
</>
|
||||
),
|
||||
jobs: (
|
||||
<>
|
||||
<path {...stroke} d="M7.5 5.5h9a1.5 1.5 0 0 1 1.5 1.5v11.8a1.5 1.5 0 0 1-1.5 1.5h-9a1.5 1.5 0 0 1-1.5-1.5V7a1.5 1.5 0 0 1 1.5-1.5Z" />
|
||||
<path {...stroke} d="M9.2 9.8h7.6M9.2 12.6h5.4M9.2 15.4h6.8" />
|
||||
<circle fill="currentColor" stroke="none" cx="17.4" cy="12.6" r="1.1" />
|
||||
</>
|
||||
),
|
||||
jobs: <path {...stroke} d="M6 4h12a1 1 0 0 1 1 1v14l-7-3.5L5 19V5a1 1 0 0 1 1-1Z" />,
|
||||
schedule: (
|
||||
<>
|
||||
<path {...stroke} d="M8 2v4M16 2v4M4 8h16M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z" />
|
||||
<path {...stroke} d="M8 12h4M8 16h8" />
|
||||
</>
|
||||
),
|
||||
ai: (
|
||||
<>
|
||||
<rect {...stroke} x="5" y="7" width="14" height="10" rx="2" />
|
||||
<path {...stroke} d="M9 11h.01M15 11h.01M10 15h4" />
|
||||
<path {...stroke} d="M12 4v2" />
|
||||
<path {...stroke} d="M8 3.8v2.4M16 3.8v2.4M5.8 8.6h12.4M7.2 6.2h9.6a1.8 1.8 0 0 1 1.8 1.8v10.2a1.8 1.8 0 0 1-1.8 1.8H7.2a1.8 1.8 0 0 1-1.8-1.8V8a1.8 1.8 0 0 1 1.8-1.8Z" />
|
||||
<circle fill="currentColor" stroke="none" cx="12" cy="14.8" r="1.65" />
|
||||
<path {...stroke} d="M9.2 11.8h2.2" />
|
||||
</>
|
||||
),
|
||||
template: (
|
||||
<>
|
||||
<path {...stroke} d="M8 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z" />
|
||||
<path {...stroke} d="M10 9h8M10 13h8M10 17h5" />
|
||||
<rect {...stroke} x="6.8" y="5.5" width="10.4" height="13" rx="1.6" />
|
||||
<path {...stroke} d="M9.4 9.2h7.2M9.4 12h7.2M9.4 14.8h4.6" />
|
||||
<path {...stroke} d="M15.8 5.5V4.3a1.2 1.2 0 0 1 1.2-1.2h2.8a1.2 1.2 0 0 1 1.2 1.2v12.4a1.2 1.2 0 0 1-1.2 1.2h-2.8a1.2 1.2 0 0 1-1.2-1.2v-1" />
|
||||
</>
|
||||
),
|
||||
settings: (
|
||||
<>
|
||||
<circle {...stroke} cx="12" cy="12" r="3" />
|
||||
<path
|
||||
{...stroke}
|
||||
d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4l1.4-1.4M17 7l1.4-1.4"
|
||||
/>
|
||||
<path {...stroke} d="M6.5 9.2v5.6M12 7v10M17.5 10.4v3.2" />
|
||||
<circle {...stroke} cx="6.5" cy="9.2" r="2" />
|
||||
<circle {...stroke} cx="12" cy="7" r="2" />
|
||||
<circle {...stroke} cx="17.5" cy="10.4" r="2" />
|
||||
</>
|
||||
),
|
||||
profile: (
|
||||
<>
|
||||
<circle {...stroke} cx="12" cy="8" r="3.5" />
|
||||
<path {...stroke} d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
<circle {...stroke} cx="12" cy="12" r="8.2" />
|
||||
<circle {...stroke} cx="12" cy="9.4" r="2.8" />
|
||||
<path {...stroke} d="M6.8 17.8c1.2-2.6 3.2-3.8 5.2-3.8s4 1.2 5.2 3.8" />
|
||||
</>
|
||||
),
|
||||
permissions: (
|
||||
<>
|
||||
<path {...stroke} d="M8 11V8a4 4 0 1 1 8 0v3" />
|
||||
<rect {...stroke} x="6" y="11" width="12" height="9" rx="2" />
|
||||
<path {...stroke} d="M12 4.2 17.2 6.4v5.1c0 3.1-2.2 5.2-5.2 6.3-3-.9-5.2-3.2-5.2-6.3V6.4L12 4.2Z" />
|
||||
<path {...stroke} d="m9.4 12.2 1.8 1.8 3.6-3.8" />
|
||||
</>
|
||||
),
|
||||
easter: (
|
||||
<>
|
||||
<path
|
||||
{...stroke}
|
||||
d="M12 5.2c-3.8 0-6.8 2.4-6.8 5.8 0 2.6 1.6 4.8 4 5.8"
|
||||
/>
|
||||
<path
|
||||
{...stroke}
|
||||
d="M12 5.2c3.8 0 6.8 2.4 6.8 5.8 0 2.6-1.6 4.8-4 5.8"
|
||||
/>
|
||||
<path {...stroke} d="M12 16.8v2.2M9.4 19.8h5.2" />
|
||||
<path {...stroke} d="M10.2 9.8c1.4 1 3.2 1 4.8 0" opacity="0.65" />
|
||||
</>
|
||||
),
|
||||
threads: (
|
||||
<>
|
||||
<circle {...stroke} cx="12" cy="8" r="3.5" />
|
||||
<path {...stroke} d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
<path {...stroke} d="M16 6.5c2 0 3.5 1.2 3.5 2.8S18 12 16 12" />
|
||||
<circle {...stroke} cx="9" cy="9.2" r="2.6" />
|
||||
<circle {...stroke} cx="15.8" cy="14.8" r="2.6" />
|
||||
<path {...stroke} d="M11.2 10.8c2.2 1.2 2.8 2.2 2.8 3.2" />
|
||||
<path {...stroke} d="M5.5 19.5c0-3.2 2.4-5.2 5.2-5.2" opacity="0.55" />
|
||||
</>
|
||||
),
|
||||
more: (
|
||||
<>
|
||||
<circle fill="currentColor" cx="7" cy="12" r="1.5" stroke="none" />
|
||||
<circle fill="currentColor" cx="12" cy="12" r="1.5" stroke="none" />
|
||||
<circle fill="currentColor" cx="17" cy="12" r="1.5" stroke="none" />
|
||||
<rect fill="currentColor" stroke="none" x="5.5" y="5.5" width="5.2" height="5.2" rx="1.3" />
|
||||
<rect fill="currentColor" stroke="none" x="13.3" y="5.5" width="5.2" height="5.2" rx="1.3" />
|
||||
<rect fill="currentColor" stroke="none" x="5.5" y="13.3" width="5.2" height="5.2" rx="1.3" />
|
||||
<rect fill="currentColor" stroke="none" x="13.3" y="13.3" width="5.2" height="5.2" rx="1.3" />
|
||||
</>
|
||||
),
|
||||
}
|
||||
|
|
@ -84,7 +106,7 @@ export function AcIcon({
|
|||
className?: string
|
||||
}) {
|
||||
const px = size === 'sm' ? 'h-9 w-9' : size === 'lg' ? 'h-12 w-12' : 'h-10 w-10'
|
||||
const iconPx = size === 'sm' ? 'h-4 w-4' : size === 'lg' ? 'h-6 w-6' : 'h-5 w-5'
|
||||
const iconPx = size === 'sm' ? 'h-[18px] w-[18px]' : size === 'lg' ? 'h-6 w-6' : 'h-5 w-5'
|
||||
return (
|
||||
<span className={`ac-app-icon-svg ${px} ${className}`}>
|
||||
<svg aria-hidden className={iconPx} viewBox="0 0 24 24">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import {
|
|||
DEFAULT_AI_CREDENTIALS,
|
||||
getApiKeyStatus,
|
||||
isMaskedKey,
|
||||
normalizeProviderSettings,
|
||||
normalizeSupportedProvider,
|
||||
PROVIDER_KEY_LABELS,
|
||||
PROVIDER_OPTIONS,
|
||||
PROVIDER_ORDER,
|
||||
|
|
@ -47,7 +49,15 @@ export function AccountAiSettings({ accountId, compact }: AccountAiSettingsProps
|
|||
setError('')
|
||||
try {
|
||||
const data = await api.get<ThreadsAccountAiSettingsData>(aiSettingsPath(accountId), { auth: true })
|
||||
setSettings(data)
|
||||
setSettings({
|
||||
...data,
|
||||
...normalizeProviderSettings({
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
research_provider: data.research_provider,
|
||||
research_model: data.research_model,
|
||||
}),
|
||||
})
|
||||
setConfigured(parseConfigured(data.api_keys_configured))
|
||||
setKeyInputs(data.api_keys ?? {})
|
||||
} catch (e) {
|
||||
|
|
@ -68,23 +78,37 @@ export function AccountAiSettings({ accountId, compact }: AccountAiSettingsProps
|
|||
setMessage('')
|
||||
try {
|
||||
const apiKeys: ProviderApiKeys = {}
|
||||
for (const [provider, value] of Object.entries(keyInputs) as [ProviderId, string][]) {
|
||||
const trimmed = value?.trim()
|
||||
for (const provider of PROVIDER_ORDER) {
|
||||
const trimmed = keyInputs[provider]?.trim()
|
||||
if (!trimmed || isMaskedKey(trimmed)) continue
|
||||
apiKeys[provider] = trimmed
|
||||
}
|
||||
const normalized = normalizeProviderSettings({
|
||||
provider: settings.provider,
|
||||
model: settings.model,
|
||||
research_provider: settings.research_provider,
|
||||
research_model: settings.research_model,
|
||||
})
|
||||
const data = await api.put<ThreadsAccountAiSettingsData>(
|
||||
aiSettingsPath(accountId),
|
||||
{
|
||||
provider: settings.provider,
|
||||
model: settings.model,
|
||||
research_provider: settings.research_provider,
|
||||
research_model: settings.research_model,
|
||||
provider: normalized.provider,
|
||||
model: normalized.model,
|
||||
research_provider: normalized.research_provider,
|
||||
research_model: normalized.research_model,
|
||||
api_keys: apiKeys,
|
||||
},
|
||||
{ auth: true },
|
||||
)
|
||||
setSettings(data)
|
||||
setSettings({
|
||||
...data,
|
||||
...normalizeProviderSettings({
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
research_provider: data.research_provider,
|
||||
research_model: data.research_model,
|
||||
}),
|
||||
})
|
||||
setConfigured(parseConfigured(data.api_keys_configured))
|
||||
setKeyInputs(data.api_keys ?? {})
|
||||
setMessage('AI 設定已儲存')
|
||||
|
|
@ -95,9 +119,8 @@ export function AccountAiSettings({ accountId, compact }: AccountAiSettingsProps
|
|||
}
|
||||
}
|
||||
|
||||
const provider = (settings?.provider as ProviderId) || DEFAULT_AI_CREDENTIALS.provider
|
||||
const researchProvider =
|
||||
(settings?.research_provider as ProviderId) || provider
|
||||
const provider = normalizeSupportedProvider(settings?.provider) || DEFAULT_AI_CREDENTIALS.provider
|
||||
const researchProvider = normalizeSupportedProvider(settings?.research_provider ?? provider)
|
||||
const providerOption = PROVIDER_OPTIONS.find((item) => item.value === provider)
|
||||
const researchOption = PROVIDER_OPTIONS.find((item) => item.value === researchProvider)
|
||||
const currentProviderConfigured = configured[provider]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { Navigate, Outlet } from 'react-router-dom'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { isAdminMember } from '../lib/memberRole'
|
||||
|
||||
export function AdminRoute() {
|
||||
const { member, loading } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-[12rem] items-center justify-center text-muted">
|
||||
載入中…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAdminMember(member)) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
return <Outlet />
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { AuthTicketIcon } from './AuthDecor'
|
||||
|
||||
type Props = {
|
||||
variant?: 'header' | 'sidebar'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AppBrandLink({ variant = 'header', className = '' }: Props) {
|
||||
const isSidebar = variant === 'sidebar'
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/"
|
||||
className={`ac-brand-link ${isSidebar ? 'ac-sidebar-brand' : 'ac-app-header-brand'} ${className}`.trim()}
|
||||
aria-label="回到總覽"
|
||||
>
|
||||
<AuthTicketIcon
|
||||
className={isSidebar ? 'ac-sidebar-brand-icon' : 'ac-app-header-icon'}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="display-en text-[10px] font-semibold tracking-[0.16em] text-accent uppercase">
|
||||
Haixun Patrol
|
||||
</p>
|
||||
<p
|
||||
className={
|
||||
isSidebar
|
||||
? 'truncate text-base font-bold leading-snug text-ink'
|
||||
: 'truncate text-lg font-bold leading-snug text-ink'
|
||||
}
|
||||
>
|
||||
巡樓管理台
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { useMemo } from 'react'
|
||||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { isAdminMember } from '../lib/memberRole'
|
||||
import { navGroupsForOnboarding, onboardingGlowClass, shouldGlowNav } from '../lib/onboarding'
|
||||
import { coreWorkflowNavGroup, type AcAppKey } from '../lib/acAssets'
|
||||
import { getActiveTopicId } from '../lib/brandContext'
|
||||
|
|
@ -53,14 +55,16 @@ function SidebarNavItem({
|
|||
|
||||
export function AppSidebar() {
|
||||
const { pathname } = useLocation()
|
||||
const { member } = useAuth()
|
||||
const { isComplete, nextStep } = useOnboarding()
|
||||
const isAdmin = isAdminMember(member)
|
||||
const groups = useMemo(() => {
|
||||
const base = navGroupsForOnboarding(isComplete)
|
||||
const base = navGroupsForOnboarding(isComplete, isAdmin)
|
||||
if (!isComplete) return base
|
||||
const flowGroup = coreWorkflowNavGroup()
|
||||
if (base.length === 0) return [flowGroup]
|
||||
return [flowGroup, ...base]
|
||||
}, [isComplete])
|
||||
}, [isComplete, isAdmin])
|
||||
|
||||
return (
|
||||
<aside className="ac-sidebar hidden lg:flex" aria-label="側欄導覽">
|
||||
|
|
|
|||
|
|
@ -58,23 +58,59 @@ export function SceneDecor() {
|
|||
/** @deprecated 使用 SceneDecor */
|
||||
export const AuthSceneDecor = SceneDecor
|
||||
|
||||
/** 品牌徽章:巡檢視窗 + 燈塔信標,幾何俐落、適合小尺寸 */
|
||||
export function AuthTicketIcon({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<span className={`auth-ticket-icon ${className}`.trim()}>
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M5 11.5 12 6l7 5.5V19a1.5 1.5 0 0 1-1.5 1.5H14v-5.5h-4V20.5H6.5A1.5 1.5 0 0 1 5 19v-7.5Z"
|
||||
<svg viewBox="0 0 40 40" fill="none" aria-hidden className="auth-ticket-icon__svg">
|
||||
<defs>
|
||||
<linearGradient id="hx-mark-body" x1="8" y1="10" x2="30" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--hx-brand)" />
|
||||
<stop offset="1" stopColor="var(--hx-brand-hover)" />
|
||||
</linearGradient>
|
||||
<linearGradient id="hx-mark-beam" x1="22" y1="11" x2="34" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--hx-brand)" stopOpacity="0.85" />
|
||||
<stop offset="1" stopColor="var(--hx-brand)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
x="7.5"
|
||||
y="7.5"
|
||||
width="25"
|
||||
height="25"
|
||||
rx="7.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.35"
|
||||
opacity="0.28"
|
||||
/>
|
||||
<path
|
||||
d="M17 4.5c1.2.8 2 2.2 2 3.8 0 2.8-2.2 4.2-4 5.2"
|
||||
d="M11 13.5h5.2M23.8 13.5h5.2M11 26.5h5.2M23.8 26.5h5.2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeWidth="1.45"
|
||||
strokeLinecap="round"
|
||||
opacity="0.42"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M14.2 28.4h11.6a1.8 1.8 0 0 0 1.8-1.8V18.2H14.2v8.4Z"
|
||||
fill="url(#hx-mark-body)"
|
||||
/>
|
||||
<path d="M14.2 18.2 20 12.4l5.8 5.8H14.2Z" fill="currentColor" opacity="0.92" />
|
||||
<path d="M22.2 12.8 28 18.6" stroke="var(--hx-surface)" strokeWidth="0.9" strokeLinecap="round" opacity="0.55" />
|
||||
|
||||
<path d="M22 18.2v8.4a1.35 1.35 0 0 1-2.7 0v-8.4" fill="var(--hx-surface)" opacity="0.96" />
|
||||
|
||||
<circle cx="20" cy="10.6" r="2.15" fill="var(--hx-surface)" />
|
||||
<circle cx="20" cy="10.6" r="1.05" fill="currentColor" />
|
||||
<path
|
||||
d="M21.4 10.6h11.5"
|
||||
stroke="url(#hx-mark-beam)"
|
||||
strokeWidth="2.4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="18.5" cy="5.5" r="1.25" fill="currentColor" />
|
||||
|
||||
<ellipse cx="20" cy="31.8" rx="10.5" ry="1.7" fill="currentColor" opacity="0.1" />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { Button, Card } from './ui'
|
|||
|
||||
export function CopyFlowPipeline() {
|
||||
return (
|
||||
<section className="ac-persona-pipeline" aria-label="拷貝忍者工作流入口">
|
||||
<div className="mb-4 space-y-1">
|
||||
<section className="ac-persona-pipeline flex h-full flex-col" aria-label="拷貝忍者工作流入口">
|
||||
<div className="mb-4 flex-1 space-y-1 lg:min-h-[8.5rem]">
|
||||
<p className="display-en text-[10px] font-bold tracking-[0.16em] text-brand uppercase">
|
||||
拷貝忍者
|
||||
</p>
|
||||
|
|
@ -17,7 +17,7 @@ export function CopyFlowPipeline() {
|
|||
,建立拷貝任務產生研究地圖、勾選標籤海巡,再產出可編輯的內容矩陣或單篇深度仿寫。
|
||||
</p>
|
||||
</div>
|
||||
<Card className="flex flex-wrap items-center justify-between gap-4 p-5">
|
||||
<Card className="flex min-h-[5.5rem] flex-wrap items-center justify-between gap-4 p-5">
|
||||
<div>
|
||||
<p className="font-bold text-ink">拷貝忍者</p>
|
||||
<p className="mt-1 text-sm text-ink-secondary">拷貝任務、爆款海巡與草稿編輯</p>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ export type CopyMissionScanScheduleData = {
|
|||
last_run_at?: number
|
||||
}
|
||||
|
||||
const CRON_PRESETS: Array<{ label: string; value: string }> = [
|
||||
{ label: '每天 09:00', value: '0 9 * * *' },
|
||||
{ label: '每週一 09:00', value: '0 9 * * 1' },
|
||||
{ label: '每 6 小時', value: '0 */6 * * *' },
|
||||
{ label: '每 12 小時', value: '0 */12 * * *' },
|
||||
]
|
||||
import { CRON_PRESETS } from '../lib/scheduleCatalog'
|
||||
|
||||
type Props = {
|
||||
personaId: string
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|||
import { Link } from 'react-router-dom'
|
||||
import { api } from '../api/client'
|
||||
import { isTerminalJobStatus, jobStatusBadgeClass, jobStatusLabel } from '../lib/jobStatus'
|
||||
import { jobTemplateLabel } from '../lib/jobTemplate'
|
||||
import { ANALYZE_COPY_MISSION_PIPELINE_STEPS, VIRAL_SCAN_PIPELINE_STEPS } from '../lib/copyFlow'
|
||||
import { EXPAND_GRAPH_PIPELINE_STEPS, PLACEMENT_SCAN_PIPELINE_STEPS } from '../lib/knowledgeGraph'
|
||||
import { setPlacementHandoff } from '../lib/islander/handoffStore'
|
||||
|
|
@ -57,13 +58,7 @@ function recentEnough(job: JobData) {
|
|||
}
|
||||
|
||||
function shortJobTitle(job: JobData) {
|
||||
if (job.template_type === 'style-8d') return '8D 風格分析'
|
||||
if (job.template_type === 'expand-graph') return '知識圖譜擴展'
|
||||
if (job.template_type === 'placement-scan') return '雙軌海巡'
|
||||
if (job.template_type === 'scan-viral') return '爆款掃描'
|
||||
if (job.template_type === 'analyze-copy-mission') return '拷貝研究地圖'
|
||||
if (job.template_type === 'demo_long_task') return 'Demo 任務'
|
||||
return job.template_type
|
||||
return jobTemplateLabel(job.template_type)
|
||||
}
|
||||
|
||||
function phaseHint(job: JobData) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Outlet } from 'react-router-dom'
|
||||
import { AuthTicketIcon, SceneDecor } from './AuthDecor'
|
||||
import { SceneDecor } from './AuthDecor'
|
||||
import { AppBrandLink } from './AppBrandLink'
|
||||
import { AppSidebar } from './AppSidebar'
|
||||
import { MobileBottomNav } from './MobileBottomNav'
|
||||
import { AccountSwitcher } from './AccountSwitcher'
|
||||
import { MemberMenu } from './MemberMenu'
|
||||
import { JobMonitor } from './JobMonitor'
|
||||
import { IslanderCompanion } from './islander/IslanderCompanion'
|
||||
import { useIslanderUnlock } from '../hooks/useIslanderUnlock'
|
||||
import { IslanderPageProvider } from '../lib/islander'
|
||||
import { OnboardingBanner } from './OnboardingBanner'
|
||||
import { OnboardingRouteGuard } from './OnboardingRouteGuard'
|
||||
|
|
@ -13,6 +15,8 @@ import { OnboardingProvider } from '../onboarding/OnboardingContext'
|
|||
import { ThreadsAccountProvider } from '../threads/ThreadsAccountContext'
|
||||
|
||||
export function Layout() {
|
||||
const islanderUnlocked = useIslanderUnlock()
|
||||
|
||||
return (
|
||||
<ThreadsAccountProvider>
|
||||
<OnboardingProvider>
|
||||
|
|
@ -24,16 +28,8 @@ export function Layout() {
|
|||
|
||||
<div className="ac-workspace-column flex min-w-0 flex-1 flex-col">
|
||||
<header className="ac-app-chrome">
|
||||
<div className="ac-app-header-brand lg:hidden">
|
||||
<AuthTicketIcon className="ac-app-header-icon" />
|
||||
<div className="min-w-0">
|
||||
<p className="display-en text-[10px] font-semibold tracking-[0.16em] text-accent uppercase">
|
||||
Haixun Patrol
|
||||
</p>
|
||||
<h1 className="truncate text-lg font-bold leading-snug text-ink">巡樓管理台</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ac-app-chrome-spacer hidden min-w-0 flex-1 lg:block" aria-hidden />
|
||||
<AppBrandLink />
|
||||
<div className="ac-app-chrome-spacer min-w-0 flex-1" aria-hidden />
|
||||
<div className="ac-app-header-actions flex shrink-0 items-center gap-2">
|
||||
<AccountSwitcher />
|
||||
<MemberMenu />
|
||||
|
|
@ -55,7 +51,7 @@ export function Layout() {
|
|||
|
||||
<MobileBottomNav />
|
||||
<JobMonitor />
|
||||
<IslanderCompanion />
|
||||
{islanderUnlocked ? <IslanderCompanion /> : null}
|
||||
</div>
|
||||
</OnboardingProvider>
|
||||
</ThreadsAccountProvider>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { formatMemberRoles, memberAvatarGlyph } from '../lib/memberRole'
|
||||
import { ThemeToggle } from './ThemeToggle'
|
||||
|
||||
function memberInitial(email?: string, displayName?: string) {
|
||||
const source = displayName?.trim() || email?.trim() || '?'
|
||||
return source.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
export function MemberMenu() {
|
||||
const navigate = useNavigate()
|
||||
const { member, logout } = useAuth()
|
||||
|
|
@ -17,8 +13,9 @@ export function MemberMenu() {
|
|||
|
||||
const email = member?.email ?? ''
|
||||
const displayName = member?.display_name?.trim()
|
||||
const roleLabel = member?.roles?.join(', ') || 'member'
|
||||
const shortLabel = displayName || email.split('@')[0] || '會員'
|
||||
const roleLabel = formatMemberRoles(member?.roles)
|
||||
const avatarGlyph = memberAvatarGlyph(member)
|
||||
const shortLabel = displayName || '會員'
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
|
@ -58,7 +55,7 @@ export function MemberMenu() {
|
|||
aria-label={`會員:${email || shortLabel}`}
|
||||
>
|
||||
<span className="ac-member-avatar" aria-hidden>
|
||||
{memberInitial(email, displayName)}
|
||||
{avatarGlyph}
|
||||
</span>
|
||||
<span className="ac-member-trigger-text hidden min-w-0 max-w-[6.5rem] truncate sm:inline">
|
||||
{shortLabel}
|
||||
|
|
@ -73,7 +70,7 @@ export function MemberMenu() {
|
|||
<p className="ac-account-menu-title">已登入</p>
|
||||
<div className="ac-member-menu-body">
|
||||
<div className="ac-member-menu-hero">
|
||||
<span className="ac-member-avatar ac-member-avatar--lg">{memberInitial(email, displayName)}</span>
|
||||
<span className="ac-member-avatar ac-member-avatar--lg">{avatarGlyph}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
{displayName ? (
|
||||
<p className="truncate text-sm font-bold text-ink">{displayName}</p>
|
||||
|
|
@ -88,7 +85,7 @@ export function MemberMenu() {
|
|||
className="ac-member-menu-link"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
會員 Profile
|
||||
會員資料
|
||||
</Link>
|
||||
<div className="ac-member-menu-theme">
|
||||
<span className="text-sm font-semibold text-ink-secondary">外觀</span>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { NavLink, useLocation, useNavigate } from 'react-router-dom'
|
|||
import { PLACEMENT_FLOW, isPlacementFlowPath, placementFlowPath, topicIdFromSearch } from '../lib/placementFlow'
|
||||
import { getActiveTopicId } from '../lib/brandContext'
|
||||
import { onboardingGlowClass, onboardingNavApps, shouldGlowNav } from '../lib/onboarding'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { isAdminMember } from '../lib/memberRole'
|
||||
import { useOnboarding } from '../onboarding/OnboardingContext'
|
||||
import { AcIcon } from './AcIcon'
|
||||
|
||||
|
|
@ -21,12 +23,14 @@ function isMoreActive(pathname: string, routes: { to: string }[]) {
|
|||
}
|
||||
|
||||
export function MobileBottomNav() {
|
||||
const { member } = useAuth()
|
||||
const { isComplete, nextStep } = useOnboarding()
|
||||
const [moreOpen, setMoreOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const isAdmin = isAdminMember(member)
|
||||
const mobileTabs = isComplete ? fullMobileTabs : onboardingMobileTabs
|
||||
const navApps = onboardingNavApps(isComplete)
|
||||
const navApps = onboardingNavApps(isComplete, isAdmin)
|
||||
const moreRoutes = navApps.filter((n) => !mobileTabs.some((t) => t.to === n.to))
|
||||
const activeTopicId = topicIdFromSearch(location.search) || getActiveTopicId()
|
||||
const resolveTo = (to: string) =>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,7 @@ export type BrandScanScheduleData = {
|
|||
last_run_at?: number
|
||||
}
|
||||
|
||||
const CRON_PRESETS: Array<{ label: string; value: string }> = [
|
||||
{ label: '每天 09:00', value: '0 9 * * *' },
|
||||
{ label: '每週一 09:00', value: '0 9 * * 1' },
|
||||
{ label: '每 6 小時', value: '0 */6 * * *' },
|
||||
{ label: '每 12 小時', value: '0 */12 * * *' },
|
||||
]
|
||||
import { CRON_PRESETS } from '../lib/scheduleCatalog'
|
||||
|
||||
type Props = {
|
||||
brandId: string
|
||||
|
|
|
|||
|
|
@ -1,43 +1,27 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { PLACEMENT_FLOW } from '../lib/placementFlow'
|
||||
import { Button, Card } from './ui'
|
||||
|
||||
type Props = {
|
||||
highlight?: 'research' | 'outreach'
|
||||
}
|
||||
|
||||
export function PlacementFlowPipeline({ highlight }: Props) {
|
||||
export function PlacementFlowPipeline() {
|
||||
return (
|
||||
<section className="ac-persona-pipeline" aria-label="找 TA 工作流入口">
|
||||
<div className="mb-4 space-y-1">
|
||||
<section className="ac-persona-pipeline flex h-full flex-col" aria-label="找 TA 工作流入口">
|
||||
<div className="mb-4 flex-1 space-y-1 lg:min-h-[8.5rem]">
|
||||
<p className="display-en text-[10px] font-bold tracking-[0.16em] text-brand uppercase">
|
||||
找 TA
|
||||
</p>
|
||||
<h2 className="text-lg font-black text-ink">① 研究 → ② 找 TA</h2>
|
||||
<p className="max-w-2xl text-sm leading-relaxed text-ink-secondary">
|
||||
在
|
||||
<Link to="/placement/topics" className="mx-1 font-semibold text-brand hover:underline">
|
||||
找 TA 主題
|
||||
</Link>
|
||||
新增任務並產生研究地圖;人設只在「找 TA 留言」時選用語氣。
|
||||
在找 TA 主題新增任務並產生研究地圖;人設只在「找 TA 留言」時選用語氣。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{PLACEMENT_FLOW.map((step) => {
|
||||
const isHighlight = highlight === step.key
|
||||
return (
|
||||
<Link
|
||||
key={step.key}
|
||||
to={step.path}
|
||||
className={`ac-persona-pipeline__card ${isHighlight ? 'ac-persona-pipeline__card--highlight' : ''}`}
|
||||
>
|
||||
<span className="ac-persona-pipeline__step">{step.step}</span>
|
||||
<span className="ac-persona-pipeline__title">{step.label}</span>
|
||||
<span className="ac-persona-pipeline__desc">{step.desc}</span>
|
||||
<span className="ac-persona-pipeline__cta">進入 →</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Card className="flex min-h-[5.5rem] flex-wrap items-center justify-between gap-4 p-5">
|
||||
<div>
|
||||
<p className="font-bold text-ink">找 TA</p>
|
||||
<p className="mt-1 text-sm text-ink-secondary">主題列表、研究地圖與獲客留言</p>
|
||||
</div>
|
||||
<Link to="/placement/topics">
|
||||
<Button>進入找 TA</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { Select } from './ui'
|
||||
import {
|
||||
HOUR_OPTIONS,
|
||||
INTERVAL_HOUR_OPTIONS,
|
||||
MINUTE_OPTIONS,
|
||||
WEEKDAY_OPTIONS,
|
||||
type ScheduleFrequency,
|
||||
type ScheduleRepeatMode,
|
||||
} from '../lib/scheduleCron'
|
||||
|
||||
const REPEAT_OPTIONS: Array<{ value: ScheduleRepeatMode; label: string }> = [
|
||||
{ value: 'daily', label: '每天' },
|
||||
{ value: 'weekly', label: '每週' },
|
||||
{ value: 'interval', label: '固定間隔' },
|
||||
]
|
||||
|
||||
type Props = {
|
||||
value: ScheduleFrequency
|
||||
onChange: (next: ScheduleFrequency) => void
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
function timeSelectClass(compact?: boolean) {
|
||||
return compact ? 'px-2 py-1.5 text-sm' : ''
|
||||
}
|
||||
|
||||
export function ScheduleFrequencyPicker({ value, onChange, compact = false }: Props) {
|
||||
const update = (patch: Partial<ScheduleFrequency>) => onChange({ ...value, ...patch })
|
||||
|
||||
return (
|
||||
<div className={compact ? 'flex flex-wrap items-center gap-2' : 'space-y-3'}>
|
||||
<label className={compact ? 'flex items-center gap-2 text-sm text-ink' : 'block text-base'}>
|
||||
{!compact ? <span className="mb-2 block font-bold text-ink">重複</span> : null}
|
||||
<Select
|
||||
value={value.mode}
|
||||
onChange={(e) => update({ mode: e.target.value as ScheduleRepeatMode })}
|
||||
className={timeSelectClass(compact)}
|
||||
>
|
||||
{REPEAT_OPTIONS.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
{value.mode === 'weekly' ? (
|
||||
<label className={compact ? 'flex items-center gap-2 text-sm text-ink' : 'block text-base'}>
|
||||
{!compact ? <span className="mb-2 block font-bold text-ink">星期</span> : null}
|
||||
<Select
|
||||
value={String(value.weekday)}
|
||||
onChange={(e) => update({ weekday: Number.parseInt(e.target.value, 10) })}
|
||||
className={timeSelectClass(compact)}
|
||||
>
|
||||
{WEEKDAY_OPTIONS.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{value.mode === 'interval' ? (
|
||||
<label className={compact ? 'flex items-center gap-2 text-sm text-ink' : 'block text-base'}>
|
||||
{!compact ? <span className="mb-2 block font-bold text-ink">間隔</span> : null}
|
||||
<Select
|
||||
value={String(value.intervalHours)}
|
||||
onChange={(e) => update({ intervalHours: Number.parseInt(e.target.value, 10) })}
|
||||
className={timeSelectClass(compact)}
|
||||
>
|
||||
{INTERVAL_HOUR_OPTIONS.map((hours) => (
|
||||
<option key={hours} value={hours}>
|
||||
每 {hours} 小時
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
) : (
|
||||
<div className={compact ? 'flex items-center gap-2 text-sm text-ink' : 'block text-base'}>
|
||||
{!compact ? <span className="mb-2 block font-bold text-ink">時間(台北)</span> : null}
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={String(value.hour)}
|
||||
onChange={(e) => update({ hour: Number.parseInt(e.target.value, 10) })}
|
||||
className={timeSelectClass(compact)}
|
||||
aria-label="小時"
|
||||
>
|
||||
{HOUR_OPTIONS.map((hour) => (
|
||||
<option key={hour} value={hour}>
|
||||
{String(hour).padStart(2, '0')}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<span className="text-muted">:</span>
|
||||
<Select
|
||||
value={String(value.minute)}
|
||||
onChange={(e) => update({ minute: Number.parseInt(e.target.value, 10) })}
|
||||
className={timeSelectClass(compact)}
|
||||
aria-label="分鐘"
|
||||
>
|
||||
{MINUTE_OPTIONS.map((minute) => (
|
||||
<option key={minute} value={minute}>
|
||||
{String(minute).padStart(2, '0')}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -198,15 +198,23 @@ export function Notice({
|
|||
)
|
||||
}
|
||||
|
||||
export function CopyableId({ label, value }: { label: string; value: string }) {
|
||||
export function CopyableId({
|
||||
label,
|
||||
value,
|
||||
className = '',
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
className?: string
|
||||
}) {
|
||||
const copy = async () => {
|
||||
if (!value) return
|
||||
await navigator.clipboard.writeText(value)
|
||||
}
|
||||
return (
|
||||
<div className="ac-slot px-4 py-3">
|
||||
<div className={`ac-slot px-5 py-5 ${className}`.trim()}>
|
||||
<div className="text-sm font-bold text-ink-secondary">{label}</div>
|
||||
<div className="mt-1 flex items-start justify-between gap-2">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<code className="break-all text-base font-en text-ink">{value || '—'}</code>
|
||||
{value ? (
|
||||
<button type="button" onClick={copy} className="ac-btn-secondary shrink-0 px-3 py-1 text-sm">
|
||||
|
|
@ -357,4 +365,25 @@ export function ProgressBar({ value, className = '' }: { value: number; classNam
|
|||
<div className="ac-progress__fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HoverTip({
|
||||
label,
|
||||
children,
|
||||
placement = 'top',
|
||||
className = '',
|
||||
}: {
|
||||
label: string
|
||||
children: ReactNode
|
||||
placement?: 'top' | 'bottom'
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<span className={`ac-hover-tip ac-hover-tip--${placement} ${className}`.trim()}>
|
||||
{children}
|
||||
<span className="ac-hover-tip__panel" role="tooltip">
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
appendIslanderUnlockKey,
|
||||
ISLANDER_UNLOCK_EVENT,
|
||||
readIslanderUnlocked,
|
||||
toggleIslanderUnlocked,
|
||||
} from '../lib/islander/unlock'
|
||||
|
||||
function shouldIgnoreKeydown(event: KeyboardEvent): boolean {
|
||||
if (event.isComposing) return true
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) return true
|
||||
|
||||
const el = event.target
|
||||
if (!(el instanceof HTMLElement)) return false
|
||||
if (el.isContentEditable) return true
|
||||
if (el instanceof HTMLInputElement && el.type === 'password') return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function useIslanderUnlock() {
|
||||
const [unlocked, setUnlocked] = useState(readIslanderUnlocked)
|
||||
|
||||
useEffect(() => {
|
||||
const onUnlockChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ unlocked?: boolean }>).detail
|
||||
setUnlocked(typeof detail?.unlocked === 'boolean' ? detail.unlocked : readIslanderUnlocked())
|
||||
}
|
||||
|
||||
window.addEventListener(ISLANDER_UNLOCK_EVENT, onUnlockChange)
|
||||
|
||||
return () => window.removeEventListener(ISLANDER_UNLOCK_EVENT, onUnlockChange)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let buffer = ''
|
||||
let toggling = false
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (shouldIgnoreKeydown(event)) return
|
||||
|
||||
const { buffer: nextBuffer, matched } = appendIslanderUnlockKey(buffer, event.key)
|
||||
buffer = nextBuffer
|
||||
if (!matched || toggling) return
|
||||
|
||||
toggling = true
|
||||
const next = toggleIslanderUnlocked()
|
||||
setUnlocked(next)
|
||||
buffer = ''
|
||||
window.setTimeout(() => {
|
||||
toggling = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', onKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', onKeyDown, true)
|
||||
}, [])
|
||||
|
||||
return unlocked
|
||||
}
|
||||
|
|
@ -2474,6 +2474,16 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
.ac-brand-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
.ac-brand-link:hover {
|
||||
color: var(--hx-brand);
|
||||
}
|
||||
|
||||
.ac-sidebar-brand {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -2483,12 +2493,30 @@ th {
|
|||
padding: 1rem 1rem 0.9rem;
|
||||
}
|
||||
|
||||
.ac-brand-link.ac-app-header-brand {
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.15rem 0.35rem 0.15rem 0;
|
||||
}
|
||||
|
||||
.ac-brand-link.ac-app-header-brand:hover {
|
||||
background: var(--hx-brand-soft);
|
||||
}
|
||||
|
||||
.ac-brand-link.ac-sidebar-brand:hover {
|
||||
background: color-mix(in srgb, var(--hx-brand-soft) 55%, transparent 45%);
|
||||
}
|
||||
|
||||
.ac-sidebar-brand-icon.auth-ticket-icon {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ac-sidebar-brand-icon.auth-ticket-icon .auth-ticket-icon__svg {
|
||||
height: 1.55rem;
|
||||
width: 1.55rem;
|
||||
}
|
||||
|
||||
.ac-sidebar-nav {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
|
@ -2863,15 +2891,30 @@ th {
|
|||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--hx-line);
|
||||
border-radius: 1rem;
|
||||
background: var(--hx-brand-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--hx-brand) 34%, var(--hx-line) 66%);
|
||||
border-radius: 1.2rem;
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 90% at 18% 12%,
|
||||
color-mix(in srgb, var(--hx-brand-soft) 88%, var(--hx-surface) 12%),
|
||||
transparent 58%
|
||||
),
|
||||
linear-gradient(
|
||||
165deg,
|
||||
color-mix(in srgb, var(--hx-surface) 90%, var(--hx-brand-soft) 10%),
|
||||
color-mix(in srgb, var(--hx-brand-soft) 42%, var(--hx-surface) 58%)
|
||||
);
|
||||
color: var(--hx-brand);
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, var(--hx-surface) 88%, transparent 12%) inset,
|
||||
0 0 0 3px color-mix(in srgb, var(--hx-brand) 7%, transparent 93%),
|
||||
0 8px 20px color-mix(in srgb, var(--hx-brand) 14%, transparent 86%);
|
||||
}
|
||||
|
||||
.auth-ticket-icon svg {
|
||||
height: 1.65rem;
|
||||
width: 1.65rem;
|
||||
.auth-ticket-icon__svg {
|
||||
height: 2.05rem;
|
||||
width: 2.05rem;
|
||||
filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--hx-brand-shadow) 10%, transparent 90%));
|
||||
}
|
||||
|
||||
.auth-shell-title.ac-title-bar {
|
||||
|
|
@ -2947,11 +2990,21 @@ th {
|
|||
width: 2.75rem;
|
||||
}
|
||||
|
||||
.ac-app-header-icon.auth-ticket-icon .auth-ticket-icon__svg {
|
||||
height: 1.75rem;
|
||||
width: 1.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.ac-app-header-icon.auth-ticket-icon {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.ac-app-header-icon.auth-ticket-icon .auth-ticket-icon__svg {
|
||||
height: 1.9rem;
|
||||
width: 1.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-app-main-panel {
|
||||
|
|
@ -3171,17 +3224,36 @@ th {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.75rem;
|
||||
border: 2px solid var(--hx-line);
|
||||
background: var(--hx-surface);
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid color-mix(in srgb, var(--hx-line) 82%, var(--hx-brand) 18%);
|
||||
background:
|
||||
radial-gradient(
|
||||
95% 85% at 22% 14%,
|
||||
color-mix(in srgb, var(--hx-surface) 92%, var(--hx-brand-soft) 8%),
|
||||
var(--hx-surface) 72%
|
||||
);
|
||||
color: var(--hx-ink-secondary);
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, var(--hx-surface) 90%, transparent 10%) inset;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
color 0.16s ease,
|
||||
background 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.ac-app-tile--active .ac-app-icon-svg,
|
||||
.ac-app-tile:hover .ac-app-icon-svg {
|
||||
border-color: var(--hx-brand);
|
||||
border-color: color-mix(in srgb, var(--hx-brand) 48%, var(--hx-line) 52%);
|
||||
color: var(--hx-brand);
|
||||
background: var(--hx-surface);
|
||||
background:
|
||||
radial-gradient(
|
||||
95% 85% at 22% 14%,
|
||||
color-mix(in srgb, var(--hx-brand-soft) 72%, var(--hx-surface) 28%),
|
||||
var(--hx-surface) 70%
|
||||
);
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, var(--hx-surface) 90%, transparent 10%) inset,
|
||||
0 0 0 2px color-mix(in srgb, var(--hx-brand) 10%, transparent 90%);
|
||||
}
|
||||
|
||||
.ac-btn-primary {
|
||||
|
|
@ -4950,6 +5022,209 @@ th {
|
|||
color: var(--hx-muted);
|
||||
}
|
||||
|
||||
.ac-job-history-row {
|
||||
padding: 1.15rem 1.35rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.ac-job-history-row {
|
||||
padding: 1.25rem 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-schedule-row {
|
||||
padding: 0.7rem 0.9rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.ac-schedule-row {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-schedule-row__main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.ac-schedule-row__info {
|
||||
min-width: 0;
|
||||
flex: 1 1 12rem;
|
||||
}
|
||||
|
||||
.ac-schedule-row__controls {
|
||||
flex: 2 1 16rem;
|
||||
}
|
||||
|
||||
.ac-schedule-row__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ac-job-history-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem 1rem;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
'meta'
|
||||
'status'
|
||||
'progress'
|
||||
'summary';
|
||||
}
|
||||
|
||||
.ac-job-history-grid__meta {
|
||||
grid-area: meta;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
color: var(--hx-ink);
|
||||
}
|
||||
|
||||
.ac-job-history-grid__time {
|
||||
margin-top: 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--hx-muted);
|
||||
}
|
||||
|
||||
.ac-job-history-grid__status {
|
||||
grid-area: status;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__status-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__percent {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 800;
|
||||
color: var(--hx-ink-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__progress {
|
||||
grid-area: progress;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__summary {
|
||||
grid-area: summary;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.55;
|
||||
color: var(--hx-ink-secondary);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.ac-job-history-grid {
|
||||
gap: 0.65rem 1.5rem;
|
||||
grid-template-columns: minmax(8.5rem, 30%) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
'meta status'
|
||||
'meta progress'
|
||||
'summary summary';
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__meta {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.ac-job-history-grid__title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-hover-tip {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ac-job-error-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 2px solid color-mix(in srgb, var(--hx-danger) 35%, var(--hx-line) 65%);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--hx-danger-soft);
|
||||
padding: 0.2rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
color: var(--hx-danger);
|
||||
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
.ac-job-error-btn:hover,
|
||||
.ac-job-error-btn:focus-visible {
|
||||
border-color: var(--hx-danger);
|
||||
background: color-mix(in srgb, var(--hx-danger-soft) 70%, var(--hx-surface) 30%);
|
||||
color: var(--hx-danger);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ac-hover-tip__panel {
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(22rem, calc(100vw - 2rem));
|
||||
transform: translateX(-50%) translateY(0.35rem);
|
||||
border: 2px solid var(--hx-line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--hx-surface);
|
||||
padding: 0.65rem 0.85rem;
|
||||
box-shadow: var(--hx-shadow-card);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
color: var(--hx-danger);
|
||||
white-space: pre-wrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s ease;
|
||||
}
|
||||
|
||||
.ac-hover-tip--top .ac-hover-tip__panel {
|
||||
bottom: calc(100% + 0.35rem);
|
||||
transform: translateX(-50%) translateY(-0.35rem);
|
||||
}
|
||||
|
||||
.ac-hover-tip--bottom .ac-hover-tip__panel {
|
||||
top: calc(100% + 0.35rem);
|
||||
}
|
||||
|
||||
.ac-hover-tip:hover .ac-hover-tip__panel,
|
||||
.ac-hover-tip:focus-visible .ac-hover-tip__panel {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ac-hover-tip--top:hover .ac-hover-tip__panel,
|
||||
.ac-hover-tip--top:focus-visible .ac-hover-tip__panel {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.ac-hover-tip--bottom:hover .ac-hover-tip__panel,
|
||||
.ac-hover-tip--bottom:focus-visible .ac-hover-tip__panel {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.ac-table-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -3,20 +3,21 @@ export type AcAppKey =
|
|||
| 'persona'
|
||||
| 'jobs'
|
||||
| 'schedule'
|
||||
| 'ai'
|
||||
| 'template'
|
||||
| 'settings'
|
||||
| 'profile'
|
||||
| 'permissions'
|
||||
| 'easter'
|
||||
| 'threads'
|
||||
| 'more'
|
||||
|
||||
type NavItem = {
|
||||
export type NavItem = {
|
||||
to: string
|
||||
label: string
|
||||
icon: AcAppKey
|
||||
end?: boolean
|
||||
matchPrefix?: string
|
||||
adminOnly?: boolean
|
||||
}
|
||||
|
||||
/** 外層雙軌:流程 A 拷貝忍者 + 流程 B 找 TA(主題列表 + 子步驟) */
|
||||
|
|
@ -45,11 +46,11 @@ export const navGroups: { label: string; items: NavItem[] }[] = [
|
|||
{
|
||||
label: '系統',
|
||||
items: [
|
||||
{ to: '/ai', label: 'AI', icon: 'ai' },
|
||||
{ to: '/job-templates', label: '模板', icon: 'template' },
|
||||
{ to: '/job-templates', label: '模板', icon: 'template', adminOnly: true },
|
||||
{ to: '/settings', label: '設定', icon: 'settings' },
|
||||
{ to: '/profile', label: '會員', icon: 'profile' },
|
||||
{ to: '/permissions', label: '權限', icon: 'permissions' },
|
||||
{ to: '/permissions', label: '權限', icon: 'permissions', adminOnly: true },
|
||||
{ to: '/easter-eggs', label: '彩蛋手冊', icon: 'easter', adminOnly: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
export type ProviderId = 'xai' | 'openai' | 'anthropic' | 'google' | 'opencode-go'
|
||||
export type ProviderId = 'xai' | 'opencode-go'
|
||||
|
||||
export type ProviderApiKeys = Partial<Record<ProviderId, string>>
|
||||
|
||||
export const AI_CREDENTIALS_KEY = 'ai.credentials'
|
||||
|
||||
export const SUPPORTED_PROVIDERS: ProviderId[] = ['opencode-go', 'xai']
|
||||
|
||||
export const PROVIDER_KEY_LABELS: Record<
|
||||
ProviderId,
|
||||
{ label: string; hint: string; docsUrl?: string }
|
||||
|
|
@ -14,18 +16,9 @@ export const PROVIDER_KEY_LABELS: Record<
|
|||
docsUrl: 'https://opencode.ai/docs/go/',
|
||||
},
|
||||
xai: { label: 'Grok (xAI)', hint: 'xAI Console API key' },
|
||||
openai: { label: 'OpenAI', hint: 'platform.openai.com API key' },
|
||||
anthropic: { label: 'Anthropic', hint: 'console.anthropic.com API key' },
|
||||
google: { label: 'Google Gemini', hint: 'Google AI Studio API key' },
|
||||
}
|
||||
|
||||
export const PROVIDER_ORDER: ProviderId[] = [
|
||||
'opencode-go',
|
||||
'xai',
|
||||
'openai',
|
||||
'anthropic',
|
||||
'google',
|
||||
]
|
||||
export const PROVIDER_ORDER: ProviderId[] = [...SUPPORTED_PROVIDERS]
|
||||
|
||||
export const PROVIDER_OPTIONS = [
|
||||
{
|
||||
|
|
@ -41,13 +34,6 @@ export const PROVIDER_OPTIONS = [
|
|||
],
|
||||
},
|
||||
{ value: 'xai' as const, label: 'Grok (xAI)', models: ['grok-3', 'grok-3-fast', 'grok-2-1212'] },
|
||||
{ value: 'openai' as const, label: 'OpenAI', models: ['gpt-4o', 'gpt-4o-mini'] },
|
||||
{
|
||||
value: 'anthropic' as const,
|
||||
label: 'Anthropic',
|
||||
models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'],
|
||||
},
|
||||
{ value: 'google' as const, label: 'Google', models: ['gemini-2.0-flash', 'gemini-1.5-pro'] },
|
||||
] as const
|
||||
|
||||
export type AccountAiCredentials = {
|
||||
|
|
@ -66,6 +52,48 @@ export const DEFAULT_AI_CREDENTIALS: AccountAiCredentials = {
|
|||
api_keys: {},
|
||||
}
|
||||
|
||||
export function isSupportedProvider(value: string | undefined | null): value is ProviderId {
|
||||
return value === 'xai' || value === 'opencode-go'
|
||||
}
|
||||
|
||||
export function normalizeSupportedProvider(value: string | undefined | null): ProviderId {
|
||||
return isSupportedProvider(value) ? value : DEFAULT_AI_CREDENTIALS.provider
|
||||
}
|
||||
|
||||
function modelsForProvider(provider: ProviderId): readonly string[] {
|
||||
return PROVIDER_OPTIONS.find((item) => item.value === provider)?.models ?? []
|
||||
}
|
||||
|
||||
function normalizeModel(provider: ProviderId, model: string | undefined, fallback: string): string {
|
||||
const models = modelsForProvider(provider)
|
||||
if (model && models.includes(model)) return model
|
||||
return models[0] ?? fallback
|
||||
}
|
||||
|
||||
export type ProviderSettingsInput = {
|
||||
provider?: string
|
||||
model?: string
|
||||
research_provider?: string
|
||||
research_model?: string
|
||||
}
|
||||
|
||||
export function normalizeProviderSettings(
|
||||
settings: ProviderSettingsInput,
|
||||
): Pick<AccountAiCredentials, 'provider' | 'model' | 'research_provider' | 'research_model'> {
|
||||
const provider = normalizeSupportedProvider(settings.provider)
|
||||
const researchProvider = normalizeSupportedProvider(settings.research_provider ?? provider)
|
||||
return {
|
||||
provider,
|
||||
model: normalizeModel(provider, settings.model, DEFAULT_AI_CREDENTIALS.model),
|
||||
research_provider: researchProvider,
|
||||
research_model: normalizeModel(
|
||||
researchProvider,
|
||||
settings.research_model,
|
||||
DEFAULT_AI_CREDENTIALS.research_model ?? DEFAULT_AI_CREDENTIALS.model,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function maskApiKey(key: string | undefined | null): string | null {
|
||||
if (!key) return null
|
||||
if (key.length <= 4) return '••••'
|
||||
|
|
@ -91,7 +119,8 @@ export function mergeProviderApiKeys(
|
|||
incoming: ProviderApiKeys,
|
||||
): ProviderApiKeys {
|
||||
const merged = { ...existing }
|
||||
for (const [provider, value] of Object.entries(incoming) as [ProviderId, string][]) {
|
||||
for (const provider of PROVIDER_ORDER) {
|
||||
const value = incoming[provider]
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed || isMaskedKey(trimmed)) continue
|
||||
merged[provider] = trimmed
|
||||
|
|
@ -113,29 +142,28 @@ export function parseAccountAiCredentials(raw: Record<string, unknown> | undefin
|
|||
raw.api_keys && typeof raw.api_keys === 'object'
|
||||
? (raw.api_keys as ProviderApiKeys)
|
||||
: {}
|
||||
const provider = (raw.provider as ProviderId) || DEFAULT_AI_CREDENTIALS.provider
|
||||
const researchProvider = (raw.research_provider as ProviderId) || provider
|
||||
return {
|
||||
provider,
|
||||
const normalized = normalizeProviderSettings({
|
||||
provider: typeof raw.provider === 'string' ? raw.provider : DEFAULT_AI_CREDENTIALS.provider,
|
||||
model: typeof raw.model === 'string' ? raw.model : DEFAULT_AI_CREDENTIALS.model,
|
||||
research_provider: researchProvider,
|
||||
research_provider:
|
||||
typeof raw.research_provider === 'string'
|
||||
? raw.research_provider
|
||||
: DEFAULT_AI_CREDENTIALS.research_provider,
|
||||
research_model:
|
||||
typeof raw.research_model === 'string'
|
||||
? raw.research_model
|
||||
: DEFAULT_AI_CREDENTIALS.research_model,
|
||||
api_keys: apiKeys,
|
||||
}
|
||||
})
|
||||
return { ...normalized, api_keys: apiKeys }
|
||||
}
|
||||
|
||||
export function buildAccountAiCredentialsPayload(
|
||||
current: AccountAiCredentials,
|
||||
keyInputs: ProviderApiKeys,
|
||||
): AccountAiCredentials {
|
||||
const normalized = normalizeProviderSettings(current)
|
||||
return {
|
||||
provider: current.provider,
|
||||
model: current.model,
|
||||
research_provider: current.research_provider,
|
||||
research_model: current.research_model,
|
||||
...normalized,
|
||||
api_keys: mergeProviderApiKeys(current.api_keys, keyInputs),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
export type EasterEggKind = 'keyboard' | 'admin-page' | 'hidden-route'
|
||||
|
||||
export type EasterEggEntry = {
|
||||
id: string
|
||||
title: string
|
||||
/** 鍵盤彩蛋的連續輸入密碼;頁面類彩蛋可為 null */
|
||||
password: string | null
|
||||
kind: EasterEggKind
|
||||
summary: string
|
||||
howTo: string[]
|
||||
notes?: string
|
||||
}
|
||||
|
||||
/** 島民鍵盤密碼 — unlock.ts 與本目錄共用 */
|
||||
export const ISLANDER_UNLOCK_CODE = 'abababc'
|
||||
|
||||
/**
|
||||
* 巡樓彩蛋登錄簿(前端)。
|
||||
* 新增彩蛋時請同步更新此檔,管理員可在「彩蛋手冊」頁查閱。
|
||||
*/
|
||||
export const easterEggCatalog: EasterEggEntry[] = [
|
||||
{
|
||||
id: 'islander-guide',
|
||||
title: '島民嚮導',
|
||||
password: ISLANDER_UNLOCK_CODE,
|
||||
kind: 'keyboard',
|
||||
summary: '預設隱藏右下角浮動島民。連續輸入密碼可開關,狀態僅存於目前分頁的 session。',
|
||||
howTo: [
|
||||
'登入後,在任意已登入頁面(不需點特定區域)',
|
||||
'鍵盤連續輸入密碼(英文小寫,不需 Enter)',
|
||||
'右下角出現島民浮動按鈕;再輸入一次相同密碼可隱藏',
|
||||
'重新整理分頁後,若 session 仍有效會維持顯示狀態',
|
||||
],
|
||||
notes: '密碼欄位與 IME 組字中不會觸發;一般搜尋框內輸入亦可解鎖。',
|
||||
},
|
||||
{
|
||||
id: 'easter-eggs-handbook',
|
||||
title: '彩蛋手冊(本頁)',
|
||||
password: null,
|
||||
kind: 'admin-page',
|
||||
summary: '記錄所有已知彩蛋密碼與操作說明,僅管理員可從側欄「系統」區進入。',
|
||||
howTo: [
|
||||
'以 admin 角色登入',
|
||||
'桌面:左側 PATROL PAD → 系統 → 彩蛋手冊',
|
||||
'手機:底部「更多」→ 彩蛋手冊',
|
||||
],
|
||||
notes: '路由:/easter-eggs。非 admin 會被導回總覽。',
|
||||
},
|
||||
]
|
||||
|
||||
export function easterEggKindLabel(kind: EasterEggKind): string {
|
||||
switch (kind) {
|
||||
case 'keyboard':
|
||||
return '鍵盤密碼'
|
||||
case 'admin-page':
|
||||
return '管理員頁面'
|
||||
case 'hidden-route':
|
||||
return '隱藏路由'
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,8 @@ export const routeGuides: RouteGuide[] = [
|
|||
{
|
||||
pattern: /^\/$/,
|
||||
title: '總覽',
|
||||
purpose: '查看巡樓工作台捷徑與近期狀態',
|
||||
hints: ['建立經營帳號與連線請到設定', '背景任務進度可看左下「任」或任務列表'],
|
||||
purpose: '查看工作台概況、核心工作流與快速入口',
|
||||
hints: ['概況區顯示進行中任務與排程', '建立經營帳號與連線請到設定', '背景任務進度可看左下「任」或任務列表'],
|
||||
suggestions: ['這頁要怎麼用?', '帶我去設定頁', '任務列表在哪?'],
|
||||
},
|
||||
{
|
||||
|
|
@ -111,10 +111,10 @@ export const routeGuides: RouteGuide[] = [
|
|||
},
|
||||
{
|
||||
pattern: /^\/jobs$/,
|
||||
title: '任務列表',
|
||||
purpose: '查看所有背景任務狀態',
|
||||
hints: ['8D 風格分析會出現在這裡', '進行中任務也可看左下「任」'],
|
||||
suggestions: ['8D 任務失敗怎麼辦?', '幫我刷新任務列表', '任務進度去哪看?'],
|
||||
title: '背景任務',
|
||||
purpose: '查看歷史背景任務執行紀錄',
|
||||
hints: ['列表直接顯示狀態與進度', '進行中任務也可看左下「任」'],
|
||||
suggestions: ['幫我刷新任務列表', '任務進度去哪看?', '8D 任務失敗怎麼辦?'],
|
||||
},
|
||||
{
|
||||
pattern: /^\/jobs\/[^/]+$/,
|
||||
|
|
@ -125,10 +125,10 @@ export const routeGuides: RouteGuide[] = [
|
|||
},
|
||||
{
|
||||
pattern: /^\/job-schedules$/,
|
||||
title: '排程',
|
||||
purpose: '管理定時觸發的背景任務',
|
||||
hints: ['需先有任務模板', '注意 cron 時區僅用於解讀排程'],
|
||||
suggestions: ['怎麼建立排程?', '這頁要怎麼用?'],
|
||||
title: '定時排程',
|
||||
purpose: '管理找 TA 與拷貝忍者的自動海巡排程',
|
||||
hints: ['選類型與對象後儲存', '勾選節點或標籤請在對應工作流頁完成'],
|
||||
suggestions: ['怎麼建立排程?', '找 TA 排程要怎麼設?'],
|
||||
},
|
||||
{
|
||||
pattern: /^\/job-templates$/,
|
||||
|
|
@ -151,13 +151,6 @@ export const routeGuides: RouteGuide[] = [
|
|||
hints: ['開發模式可用 extension 同步 session', '正式發文前確認連線就緒'],
|
||||
suggestions: ['怎麼同步 Chrome Session?', '開發模式怎麼開?', '連線失敗怎麼辦?'],
|
||||
},
|
||||
{
|
||||
pattern: /^\/ai$/,
|
||||
title: 'AI 實驗台',
|
||||
purpose: '直接測試 provider 與模型(進階)',
|
||||
hints: ['日常建議用設定頁的 AI 設定', '島民聊天會自動讀設定頁的 key'],
|
||||
suggestions: ['這頁要怎麼用?', 'AI key 要去哪裡設?'],
|
||||
},
|
||||
{
|
||||
pattern: /^\/profile$/,
|
||||
title: '會員資料',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { ISLANDER_UNLOCK_CODE } from '../easterEggCatalog'
|
||||
|
||||
export { ISLANDER_UNLOCK_CODE }
|
||||
export const ISLANDER_UNLOCK_KEY = 'haixun.islander.unlocked'
|
||||
export const ISLANDER_UNLOCK_EVENT = 'haixun.islander-unlock-changed'
|
||||
|
||||
export function readIslanderUnlocked(): boolean {
|
||||
try {
|
||||
return sessionStorage.getItem(ISLANDER_UNLOCK_KEY) === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function writeIslanderUnlocked(unlocked: boolean) {
|
||||
try {
|
||||
sessionStorage.setItem(ISLANDER_UNLOCK_KEY, unlocked ? '1' : '0')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ISLANDER_UNLOCK_EVENT, { detail: { unlocked } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function isTypingTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
if (target.isContentEditable) return true
|
||||
const tag = target.tagName
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
|
||||
}
|
||||
|
||||
export function toggleIslanderUnlocked(): boolean {
|
||||
const next = !readIslanderUnlocked()
|
||||
writeIslanderUnlocked(next)
|
||||
return next
|
||||
}
|
||||
|
||||
/** Append one key to the rolling buffer; returns true when the unlock code matches. */
|
||||
export function appendIslanderUnlockKey(buffer: string, key: string): { buffer: string; matched: boolean } {
|
||||
const code = ISLANDER_UNLOCK_CODE
|
||||
|
||||
if (key === 'Backspace') {
|
||||
return { buffer: buffer.slice(0, -1), matched: false }
|
||||
}
|
||||
|
||||
if (key.length !== 1) {
|
||||
return { buffer, matched: false }
|
||||
}
|
||||
|
||||
const next = (buffer + key.toLowerCase()).slice(-code.length)
|
||||
return { buffer: next, matched: next === code }
|
||||
}
|
||||
|
|
@ -1,5 +1,19 @@
|
|||
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled', 'expired'])
|
||||
|
||||
type JobErrorSource = {
|
||||
error?: string
|
||||
progress?: {
|
||||
steps?: Array<{ status: string; message?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
/** 任務失敗時的錯誤訊息(優先 failed step,其次 job.error)。 */
|
||||
export function jobErrorMessage(job: JobErrorSource): string | null {
|
||||
const failedStep = job.progress?.steps?.find((step) => step.status === 'failed')
|
||||
const message = failedStep?.message?.trim() || job.error?.trim()
|
||||
return message || null
|
||||
}
|
||||
|
||||
export function isTerminalJobStatus(status: string): boolean {
|
||||
return TERMINAL.has(status)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
const JOB_TEMPLATE_LABELS: Record<string, string> = {
|
||||
'style-8d': '8D 風格分析',
|
||||
'expand-graph': '知識圖譜擴展',
|
||||
'placement-scan': '雙軌海巡',
|
||||
'scan-viral': '爆款掃描',
|
||||
'analyze-copy-mission': '拷貝研究地圖',
|
||||
'generate-copy-matrix': '內容矩陣產生',
|
||||
'generate-copy-draft': '深度仿寫草稿',
|
||||
demo_long_task: 'Demo 任務',
|
||||
}
|
||||
|
||||
export function jobTemplateLabel(templateType: string): string {
|
||||
return JOB_TEMPLATE_LABELS[templateType] ?? templateType
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import type { NavItem } from './acAssets'
|
||||
import type { MemberMeData } from '../types/api'
|
||||
|
||||
export type NavGroup = {
|
||||
label: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
admin: '管理員',
|
||||
member: '一般會員',
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
active: '使用中',
|
||||
inactive: '已停用',
|
||||
}
|
||||
|
||||
const ORIGIN_LABELS: Record<string, string> = {
|
||||
native: '本站註冊',
|
||||
email: '電子郵件',
|
||||
}
|
||||
|
||||
export function formatMemberRoles(roles: string[] | undefined): string {
|
||||
if (!roles?.length) return ROLE_LABELS.member
|
||||
return roles.map((role) => ROLE_LABELS[role] ?? role).join('、')
|
||||
}
|
||||
|
||||
export function formatMemberStatus(status: string | undefined): string {
|
||||
if (!status) return '—'
|
||||
return STATUS_LABELS[status] ?? status
|
||||
}
|
||||
|
||||
export function formatMemberOrigin(origin: string | undefined): string {
|
||||
if (!origin) return '—'
|
||||
return ORIGIN_LABELS[origin] ?? origin
|
||||
}
|
||||
|
||||
const CJK_CHAR_RE = /\p{Script=Han}/u
|
||||
|
||||
/** 頂欄圓形頭像內文字:優先顯示名稱首字,否則依角色顯示中文。 */
|
||||
export function memberAvatarGlyph(
|
||||
member: Pick<MemberMeData, 'display_name' | 'roles'> | null | undefined,
|
||||
): string {
|
||||
const displayName = member?.display_name?.trim()
|
||||
if (displayName) {
|
||||
const cjk = displayName.match(CJK_CHAR_RE)
|
||||
if (cjk) return cjk[0]
|
||||
}
|
||||
const roles = member?.roles ?? []
|
||||
if (roles.includes('admin')) return '管'
|
||||
return '員'
|
||||
}
|
||||
|
||||
export function isAdminMember(member: MemberMeData | null | undefined): boolean {
|
||||
const roles = member?.roles ?? []
|
||||
if (roles.length === 0) return false
|
||||
return roles.includes('admin')
|
||||
}
|
||||
|
||||
export function filterAdminOnlyNavGroups(groups: NavGroup[], isAdmin: boolean): NavGroup[] {
|
||||
return groups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => !item.adminOnly || isAdmin),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ThreadsAccountConnectionData } from '../types/api'
|
||||
import { navGroups, type AcAppKey } from './acAssets'
|
||||
import { navGroups } from './acAssets'
|
||||
import { filterAdminOnlyNavGroups, type NavGroup } from './memberRole'
|
||||
|
||||
export type OnboardingStep = 'account' | 'connection' | 'persona'
|
||||
|
||||
|
|
@ -12,15 +13,7 @@ export type OnboardingSnapshot = {
|
|||
nextStep: OnboardingStep | null
|
||||
}
|
||||
|
||||
type NavItem = {
|
||||
to: string
|
||||
label: string
|
||||
icon: AcAppKey
|
||||
end?: boolean
|
||||
matchPrefix?: string
|
||||
}
|
||||
|
||||
export const onboardingNavGroups: { label: string; items: NavItem[] }[] = [
|
||||
export const onboardingNavGroups: NavGroup[] = [
|
||||
{
|
||||
label: '入門設定',
|
||||
items: [
|
||||
|
|
@ -67,11 +60,13 @@ export function buildOnboardingSnapshot(input: {
|
|||
}
|
||||
}
|
||||
|
||||
export function navGroupsForOnboarding(complete: boolean) {
|
||||
return complete ? navGroups : onboardingNavGroups
|
||||
export function navGroupsForOnboarding(complete: boolean, isAdmin = false): NavGroup[] {
|
||||
const groups: NavGroup[] = complete ? navGroups : onboardingNavGroups
|
||||
return filterAdminOnlyNavGroups(groups, isAdmin)
|
||||
}
|
||||
|
||||
export function isOnboardingAllowedPath(pathname: string) {
|
||||
if (pathname === '/') return true
|
||||
if (pathname === '/settings') return true
|
||||
if (pathname === '/personas' || pathname.startsWith('/personas/')) return true
|
||||
if (pathname === '/brands' || pathname.startsWith('/brands/')) return true
|
||||
|
|
@ -88,8 +83,8 @@ export function onboardingRedirectPath(pathname: string, complete: boolean) {
|
|||
return '/settings'
|
||||
}
|
||||
|
||||
export function onboardingNavApps(complete: boolean) {
|
||||
return navGroupsForOnboarding(complete).flatMap((group) => group.items)
|
||||
export function onboardingNavApps(complete: boolean, isAdmin = false) {
|
||||
return navGroupsForOnboarding(complete, isAdmin).flatMap((group) => group.items)
|
||||
}
|
||||
|
||||
export function onboardingGlowClass(active: boolean) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import type { JobScheduleData } from '../types/api'
|
||||
import type { CopyMissionData } from '../types/copyMission'
|
||||
import type { PlacementTopicData } from '../types/placementTopic'
|
||||
import { describeCron } from './scheduleCron'
|
||||
import { jobTemplateLabel } from './jobTemplate'
|
||||
|
||||
export const CRON_PRESETS: Array<{ label: string; value: string }> = [
|
||||
{ label: '每天 09:00', value: '0 9 * * *' },
|
||||
{ label: '每週一 09:00', value: '0 9 * * 1' },
|
||||
{ label: '每 6 小時', value: '0 */6 * * *' },
|
||||
{ label: '每 12 小時', value: '0 */12 * * *' },
|
||||
]
|
||||
|
||||
export const SCHEDULE_TIMEZONE = 'Asia/Taipei'
|
||||
|
||||
export type ScheduleKind = 'placement_topic_scan' | 'copy_mission_scan'
|
||||
|
||||
export type ScheduleKindMeta = {
|
||||
kind: ScheduleKind
|
||||
label: string
|
||||
description: string
|
||||
templateType: 'placement-scan' | 'scan-viral'
|
||||
configureHint: string
|
||||
}
|
||||
|
||||
export const SCHEDULE_KINDS: ScheduleKindMeta[] = [
|
||||
{
|
||||
kind: 'placement_topic_scan',
|
||||
label: '找 TA · 定期雙軌海巡',
|
||||
description: '依主題研究地圖勾選的節點,定期重跑海巡找貼文;不會自動發文,留言仍須手動 po。',
|
||||
templateType: 'placement-scan',
|
||||
configureHint: '請先在主題研究地圖勾選節點並儲存,再啟用排程。',
|
||||
},
|
||||
{
|
||||
kind: 'copy_mission_scan',
|
||||
label: '拷貝忍者 · 定期爆款海巡',
|
||||
description: '依拷貝任務內勾選的標籤,定期重跑爆款海巡;不會自動發文,仍須手動 po。',
|
||||
templateType: 'scan-viral',
|
||||
configureHint: '請先在拷貝任務勾選搜尋標籤並儲存,再啟用排程。',
|
||||
},
|
||||
]
|
||||
|
||||
const KIND_BY_TEMPLATE = new Map(
|
||||
SCHEDULE_KINDS.map((item) => [item.templateType, item] as const),
|
||||
)
|
||||
|
||||
export function isManagedScheduleTemplate(templateType: string): boolean {
|
||||
return templateType === 'placement-scan' || templateType === 'scan-viral'
|
||||
}
|
||||
|
||||
export function filterManagedSchedules<T extends { template_type: string }>(schedules: T[]): T[] {
|
||||
return schedules.filter((item) => isManagedScheduleTemplate(item.template_type))
|
||||
}
|
||||
|
||||
export function scheduleKindMeta(templateType: string): ScheduleKindMeta | null {
|
||||
return KIND_BY_TEMPLATE.get(templateType as ScheduleKindMeta['templateType']) ?? null
|
||||
}
|
||||
|
||||
export function cronPresetLabel(cron: string): string {
|
||||
return CRON_PRESETS.find((preset) => preset.value === cron)?.label ?? describeCron(cron)
|
||||
}
|
||||
|
||||
type BrandLike = { id: string; display_name?: string; seed_query?: string }
|
||||
|
||||
export function resolvePlacementScanTarget(
|
||||
schedule: JobScheduleData,
|
||||
topics: PlacementTopicData[],
|
||||
brands: BrandLike[] = [],
|
||||
): { label: string; href: string; source: string } {
|
||||
const topic = topics.find((item) => item.id === schedule.scope_id)
|
||||
if (topic) {
|
||||
const name = topic.topic_name?.trim() || topic.seed_query?.trim() || '未命名主題'
|
||||
return {
|
||||
label: name,
|
||||
href: `/placement/topics/${encodeURIComponent(topic.id)}/research-map`,
|
||||
source: '找 TA 主題',
|
||||
}
|
||||
}
|
||||
const brand = brands.find((item) => item.id === schedule.scope_id)
|
||||
if (brand) {
|
||||
const name = brand.display_name?.trim() || brand.seed_query?.trim() || '未命名品牌'
|
||||
return {
|
||||
label: name,
|
||||
href: '/research',
|
||||
source: '研究地圖',
|
||||
}
|
||||
}
|
||||
if (schedule.scope === 'brand') {
|
||||
return {
|
||||
label: `品牌 ${schedule.scope_id.slice(0, 8)}…`,
|
||||
href: '/research',
|
||||
source: '研究地圖',
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: `主題 ${schedule.scope_id.slice(0, 8)}…`,
|
||||
href: '/placement/topics',
|
||||
source: '找 TA',
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 resolvePlacementScanTarget */
|
||||
export function resolvePlacementTopicTarget(
|
||||
schedule: JobScheduleData,
|
||||
topics: PlacementTopicData[],
|
||||
): { label: string; href: string } {
|
||||
const { label, href } = resolvePlacementScanTarget(schedule, topics)
|
||||
return { label, href }
|
||||
}
|
||||
|
||||
export function resolveCopyMissionTarget(
|
||||
schedule: JobScheduleData,
|
||||
missions: CopyMissionData[],
|
||||
): { label: string; href: string; personaId: string } {
|
||||
const mission = missions.find((item) => item.id === schedule.scope_id)
|
||||
if (!mission) {
|
||||
return {
|
||||
label: `任務 ${schedule.scope_id.slice(0, 8)}…`,
|
||||
href: '/matrix',
|
||||
personaId: '',
|
||||
}
|
||||
}
|
||||
const name = mission.label?.trim() || mission.seed_query?.trim() || '未命名任務'
|
||||
return {
|
||||
label: name,
|
||||
href: `/matrix/missions/${encodeURIComponent(mission.id)}`,
|
||||
personaId: mission.persona_id,
|
||||
}
|
||||
}
|
||||
|
||||
export function scheduleDisplayTitle(templateType: string): string {
|
||||
return scheduleKindMeta(templateType)?.label ?? jobTemplateLabel(templateType)
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
export type ScheduleRepeatMode = 'daily' | 'weekly' | 'interval'
|
||||
|
||||
export type ScheduleFrequency = {
|
||||
mode: ScheduleRepeatMode
|
||||
hour: number
|
||||
minute: number
|
||||
weekday: number
|
||||
intervalHours: number
|
||||
}
|
||||
|
||||
export const WEEKDAY_OPTIONS: Array<{ value: number; label: string }> = [
|
||||
{ value: 1, label: '週一' },
|
||||
{ value: 2, label: '週二' },
|
||||
{ value: 3, label: '週三' },
|
||||
{ value: 4, label: '週四' },
|
||||
{ value: 5, label: '週五' },
|
||||
{ value: 6, label: '週六' },
|
||||
{ value: 0, label: '週日' },
|
||||
]
|
||||
|
||||
export const INTERVAL_HOUR_OPTIONS = [1, 2, 3, 4, 6, 8, 12, 24] as const
|
||||
|
||||
export const HOUR_OPTIONS = Array.from({ length: 24 }, (_, hour) => hour)
|
||||
export const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, minute) => minute)
|
||||
|
||||
export function defaultScheduleFrequency(): ScheduleFrequency {
|
||||
return {
|
||||
mode: 'daily',
|
||||
hour: 9,
|
||||
minute: 0,
|
||||
weekday: 1,
|
||||
intervalHours: 6,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCronFromFrequency(freq: ScheduleFrequency): string {
|
||||
switch (freq.mode) {
|
||||
case 'daily':
|
||||
return `${freq.minute} ${freq.hour} * * *`
|
||||
case 'weekly':
|
||||
return `${freq.minute} ${freq.hour} * * ${freq.weekday}`
|
||||
case 'interval':
|
||||
return `0 */${freq.intervalHours} * * *`
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCronToFrequency(cron: string): ScheduleFrequency | null {
|
||||
const parts = cron.trim().split(/\s+/)
|
||||
if (parts.length !== 5) return null
|
||||
|
||||
const [minute, hour, dom, month, dow] = parts
|
||||
|
||||
if (
|
||||
minute === '0' &&
|
||||
dom === '*' &&
|
||||
month === '*' &&
|
||||
dow === '*' &&
|
||||
/^\*\/(\d+)$/.test(hour)
|
||||
) {
|
||||
const intervalHours = Number.parseInt(hour.slice(2), 10)
|
||||
if (intervalHours >= 1 && intervalHours <= 24) {
|
||||
return {
|
||||
mode: 'interval',
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
weekday: 1,
|
||||
intervalHours,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
dom === '*' &&
|
||||
month === '*' &&
|
||||
dow === '*' &&
|
||||
/^\d+$/.test(minute) &&
|
||||
/^\d+$/.test(hour)
|
||||
) {
|
||||
return {
|
||||
mode: 'daily',
|
||||
hour: Number.parseInt(hour, 10),
|
||||
minute: Number.parseInt(minute, 10),
|
||||
weekday: 1,
|
||||
intervalHours: 6,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
dom === '*' &&
|
||||
month === '*' &&
|
||||
/^\d+$/.test(minute) &&
|
||||
/^\d+$/.test(hour) &&
|
||||
/^\d+$/.test(dow)
|
||||
) {
|
||||
return {
|
||||
mode: 'weekly',
|
||||
hour: Number.parseInt(hour, 10),
|
||||
minute: Number.parseInt(minute, 10),
|
||||
weekday: Number.parseInt(dow, 10),
|
||||
intervalHours: 6,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function padTimePart(value: number): string {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
export function describeCron(cron: string): string {
|
||||
const freq = parseCronToFrequency(cron)
|
||||
if (!freq) return cron
|
||||
|
||||
const time = `${padTimePart(freq.hour)}:${padTimePart(freq.minute)}`
|
||||
switch (freq.mode) {
|
||||
case 'daily':
|
||||
return `每天 ${time}`
|
||||
case 'weekly': {
|
||||
const weekday = WEEKDAY_OPTIONS.find((item) => item.value === freq.weekday)?.label ?? `週${freq.weekday}`
|
||||
return `每${weekday} ${time}`
|
||||
}
|
||||
case 'interval':
|
||||
return `每 ${freq.intervalHours} 小時`
|
||||
}
|
||||
}
|
||||
|
||||
export function frequencyFromCron(cron: string): ScheduleFrequency {
|
||||
return parseCronToFrequency(cron) ?? defaultScheduleFrequency()
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { api, streamAIChat } from '../api/client'
|
||||
import { ApiError } from '../api/client'
|
||||
import { storage } from '../lib/storage'
|
||||
import { Button, Card, ErrorText, Field, Input, PageTitle, Textarea } from '../components/ui'
|
||||
import type { AIProviderOption } from '../types/api'
|
||||
|
||||
export function AiPage() {
|
||||
const [providers, setProviders] = useState<AIProviderOption[]>([])
|
||||
const [provider, setProvider] = useState('opencode-go')
|
||||
const [model, setModel] = useState('')
|
||||
const [models, setModels] = useState<string[]>([])
|
||||
const [providerToken, setProviderToken] = useState(() => storage.getAiProviderToken())
|
||||
const [prompt, setPrompt] = useState('用一句話介紹巡樓系統')
|
||||
const [reply, setReply] = useState('')
|
||||
const [streaming, setStreaming] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<{ providers: AIProviderOption[] }>('/api/v1/ai/providers')
|
||||
.then((d) => {
|
||||
setProviders(d.providers)
|
||||
if (d.providers[0]) setProvider(d.providers[0].id)
|
||||
})
|
||||
.catch((e: ApiError) => setError(e.message))
|
||||
}, [])
|
||||
|
||||
const loadModels = async () => {
|
||||
setError('')
|
||||
storage.setAiProviderToken(providerToken)
|
||||
try {
|
||||
const data = await api.post<{ models: string[]; error?: string }>(
|
||||
`/api/v1/ai/providers/${provider}/models`,
|
||||
{ provider },
|
||||
{ memberAuth: true, providerToken },
|
||||
)
|
||||
setModels(data.models)
|
||||
if (data.models[0]) setModel(data.models[0])
|
||||
if (data.error) setError(data.error)
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '拉 models 失敗')
|
||||
}
|
||||
}
|
||||
|
||||
const chatOnce = async () => {
|
||||
setError('')
|
||||
setReply('')
|
||||
storage.setAiProviderToken(providerToken)
|
||||
try {
|
||||
const data = await api.post<{ text: string }>(
|
||||
'/api/v1/ai/chat',
|
||||
{
|
||||
provider,
|
||||
model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
},
|
||||
{ memberAuth: true, providerToken },
|
||||
)
|
||||
setReply(data.text)
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : 'chat 失敗')
|
||||
}
|
||||
}
|
||||
|
||||
const chatStream = async () => {
|
||||
setError('')
|
||||
setReply('')
|
||||
setStreaming(true)
|
||||
storage.setAiProviderToken(providerToken)
|
||||
await streamAIChat(
|
||||
{ provider, model, messages: [{ role: 'user', content: prompt }] },
|
||||
(t) => setReply((r) => r + t),
|
||||
() => setStreaming(false),
|
||||
(msg) => {
|
||||
setError(msg)
|
||||
setStreaming(false)
|
||||
},
|
||||
)
|
||||
setStreaming(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle
|
||||
title="AI"
|
||||
subtitle="Provider token 放 Authorization;會員 JWT 放 X-Member-Authorization"
|
||||
/>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="space-y-4">
|
||||
<Field label="Provider API Token">
|
||||
<Input
|
||||
type="password"
|
||||
value={providerToken}
|
||||
onChange={(e) => setProviderToken(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Provider">
|
||||
<select
|
||||
className="w-full rounded-[var(--radius-md)] border border-line bg-surface px-4 py-3 text-[15px] text-ink outline-none focus:border-brand focus:ring-4 focus:ring-brand-soft"
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
>
|
||||
{providers.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.label} {p.streams ? '(stream)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={loadModels}>
|
||||
從 Provider 載入模型清單
|
||||
</Button>
|
||||
<select
|
||||
className="flex-1 rounded-[var(--radius-md)] border border-line bg-surface px-4 py-3 text-[15px] text-ink outline-none focus:border-brand focus:ring-4 focus:ring-brand-soft"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
>
|
||||
{models.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Field label="Prompt">
|
||||
<Textarea rows={4} value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
||||
</Field>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={chatOnce} disabled={!model || streaming}>
|
||||
送出對話(一次完整回覆)
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={chatStream} disabled={!model || streaming}>
|
||||
{streaming ? '串流回覆中…' : '送出對話(SSE 串流回覆)'}
|
||||
</Button>
|
||||
</div>
|
||||
<ErrorText message={error} />
|
||||
</Card>
|
||||
<Card>
|
||||
<h2 className="font-semibold text-ink">回應</h2>
|
||||
<pre className="mt-3 whitespace-pre-wrap text-[15px] leading-relaxed text-ink">{reply || '—'}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,98 +1,181 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { api } from '../api/client'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { CopyFlowPipeline } from '../components/CopyFlowPipeline'
|
||||
import { PlacementFlowPipeline } from '../components/PlacementFlowPipeline'
|
||||
import { Badge, Card, PageTitle, QuickLinkCard, StatCard } from '../components/ui'
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
PageTitle,
|
||||
ProgressBar,
|
||||
QuickLinkCard,
|
||||
StatCard,
|
||||
StatusBadge,
|
||||
} from '../components/ui'
|
||||
import { isActiveJobStatus, isTerminalJobStatus, jobStatusBadgeClass, jobStatusLabel } from '../lib/jobStatus'
|
||||
import { jobTemplateLabel } from '../lib/jobTemplate'
|
||||
import { filterManagedSchedules } from '../lib/scheduleCatalog'
|
||||
import type { JobData, JobScheduleData, Pagination } from '../types/api'
|
||||
|
||||
type DashboardStats = {
|
||||
activeJobs: number
|
||||
scheduleTotal: number
|
||||
enabledSchedules: number
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const { member, tenantId, uid } = useAuth()
|
||||
const [health, setHealth] = useState('')
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<{ pong: string }>('/api/v1/health')
|
||||
.then((d) => setHealth(d.pong))
|
||||
.catch(() => setHealth('unreachable'))
|
||||
const { member } = useAuth()
|
||||
const [jobs, setJobs] = useState<JobData[]>([])
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [statsLoading, setStatsLoading] = useState(true)
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const [jobsRes, schedulesRes] = await Promise.all([
|
||||
api.get<{ list: JobData[]; pagination: Pagination }>('/api/v1/jobs', {
|
||||
auth: true,
|
||||
query: { page: 1, pageSize: 50 },
|
||||
}),
|
||||
api.get<{ list: JobScheduleData[]; pagination: Pagination }>('/api/v1/job/schedules', {
|
||||
auth: true,
|
||||
query: { page: 1, pageSize: 100 },
|
||||
}),
|
||||
])
|
||||
const list = jobsRes.list ?? []
|
||||
const activeJobs = list.filter((j) => isActiveJobStatus(j.status))
|
||||
const managedSchedules = filterManagedSchedules(schedulesRes.list ?? [])
|
||||
setJobs(activeJobs)
|
||||
setStats({
|
||||
activeJobs: activeJobs.length,
|
||||
scheduleTotal: managedSchedules.length,
|
||||
enabledSchedules: managedSchedules.filter((s) => s.enabled).length,
|
||||
})
|
||||
} catch {
|
||||
setJobs([])
|
||||
setStats(null)
|
||||
} finally {
|
||||
setStatsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const healthy = health === 'pong'
|
||||
useEffect(() => {
|
||||
loadStats().catch(() => undefined)
|
||||
}, [loadStats])
|
||||
|
||||
useEffect(() => {
|
||||
const hasActive = jobs.some((j) => !isTerminalJobStatus(j.status))
|
||||
if (!hasActive) return
|
||||
const timer = window.setInterval(() => loadStats().catch(() => undefined), 3000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [jobs, loadStats])
|
||||
|
||||
const displayName =
|
||||
member?.display_name?.trim() || member?.email?.split('@')[0] || '島民'
|
||||
const activeJobs = useMemo(() => jobs.slice(0, 5), [jobs])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="ac-bulletin mb-10">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="ac-mark" aria-hidden />
|
||||
<p className="display-en text-xs font-semibold tracking-[0.16em] text-accent uppercase">Overview</p>
|
||||
<p className="display-en text-xs font-semibold tracking-[0.16em] text-accent uppercase">
|
||||
Overview
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="max-w-xl text-balance text-3xl font-bold tracking-tight text-ink md:text-4xl">
|
||||
歡迎回來,
|
||||
<span className="text-brand">{member?.email?.split('@')[0] || '使用者'}</span>
|
||||
歡迎回來,<span className="text-brand">{displayName}</span>
|
||||
</h1>
|
||||
<p className="mt-3 max-w-lg text-base leading-relaxed text-ink-secondary">
|
||||
兩條平行工作流:拷貝忍者學風格仿寫爆款;找 TA 找痛點、推產品獲客。
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<Badge tone="brand">巡檢</Badge>
|
||||
<Badge tone="sky">任務</Badge>
|
||||
<Badge tone={healthy ? 'success' : 'warning'}>{healthy ? 'API 正常' : 'API 異常'}</Badge>
|
||||
</div>
|
||||
{stats && stats.activeJobs > 0 ? (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<Badge tone="sky">{stats.activeJobs} 個任務進行中</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<PageTitle title="系統狀態" subtitle="連線與登入資訊" />
|
||||
{stats ? (
|
||||
<>
|
||||
<PageTitle title="概況" />
|
||||
<div className="mb-10 grid gap-4 sm:grid-cols-2">
|
||||
<StatCard
|
||||
label="進行中任務"
|
||||
value={stats.activeJobs}
|
||||
hint={stats.activeJobs > 0 ? '背景任務執行或排隊中' : '目前沒有進行中的任務'}
|
||||
tone={stats.activeJobs > 0 ? 'brand' : 'default'}
|
||||
/>
|
||||
<StatCard
|
||||
label="定時排程"
|
||||
value={stats.enabledSchedules > 0 ? `${stats.enabledSchedules} 啟用` : stats.scheduleTotal}
|
||||
hint={
|
||||
stats.scheduleTotal > 0
|
||||
? stats.enabledSchedules > 0
|
||||
? `共 ${stats.scheduleTotal} 筆海巡排程`
|
||||
: `共 ${stats.scheduleTotal} 筆,尚未啟用`
|
||||
: '尚未建立海巡排程'
|
||||
}
|
||||
tone={stats.enabledSchedules > 0 ? 'sky' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : statsLoading ? (
|
||||
<p className="mb-10 text-sm text-muted">載入概況中…</p>
|
||||
) : null}
|
||||
|
||||
<div className="mb-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatCard
|
||||
label="API 連線"
|
||||
value={healthy ? '正常' : health || '…'}
|
||||
hint="GET /api/v1/health"
|
||||
tone={healthy ? 'brand' : 'sky'}
|
||||
/>
|
||||
<StatCard label="租戶" value={tenantId} hint="Tenant ID" />
|
||||
<StatCard label="角色" value={member?.roles?.join(', ') || 'user'} hint={member?.email} />
|
||||
</div>
|
||||
{activeJobs.length > 0 ? (
|
||||
<section className="mb-10">
|
||||
<PageTitle title="進行中任務" />
|
||||
<Card className="divide-y divide-line p-0">
|
||||
<ul>
|
||||
{activeJobs.map((job) => {
|
||||
const summary = job.progress?.summary?.trim()
|
||||
return (
|
||||
<li key={job.id}>
|
||||
<Link
|
||||
to={`/jobs/${job.id}`}
|
||||
className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 transition hover:bg-surface-muted"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-semibold text-ink">
|
||||
{jobTemplateLabel(job.template_type)}
|
||||
</p>
|
||||
{summary ? (
|
||||
<p className="mt-0.5 truncate text-sm text-muted">{summary}</p>
|
||||
) : null}
|
||||
<div className="mt-2 max-w-xs">
|
||||
<ProgressBar value={job.progress?.percentage ?? 0} />
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge className={jobStatusBadgeClass(job.status)}>
|
||||
{jobStatusLabel(job.status)}
|
||||
</StatusBadge>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<div className="px-5 py-3 text-right">
|
||||
<Link to="/jobs" className="text-sm font-semibold text-brand hover:underline">
|
||||
查看全部任務 →
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<PageTitle title="核心工作流" subtitle="拷貝忍者與找 TA 可並行使用" />
|
||||
<PageTitle title="核心工作流" />
|
||||
|
||||
<div className="mb-10 grid gap-8 lg:grid-cols-2">
|
||||
<div className="mb-10 grid items-stretch gap-8 lg:grid-cols-2">
|
||||
<CopyFlowPipeline />
|
||||
<PlacementFlowPipeline />
|
||||
</div>
|
||||
<p className="-mt-4 mb-10 text-sm text-ink-secondary">
|
||||
<Link to="/brands" className="font-semibold text-brand hover:underline">
|
||||
品牌庫
|
||||
</Link>
|
||||
{'(找 TA 用)· '}
|
||||
<Link to="/personas" className="font-semibold text-brand hover:underline">
|
||||
人設庫
|
||||
</Link>
|
||||
{'(8D 對標與留言語氣)'}
|
||||
</p>
|
||||
|
||||
<PageTitle title="快速入口" subtitle="常用功能" />
|
||||
|
||||
<div className="mb-10 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<QuickLinkCard to="/jobs" icon="jobs" title="背景任務" desc="建立、追蹤、取消或重試巡檢任務。" />
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<QuickLinkCard to="/jobs" icon="jobs" title="背景任務" desc="建立、追蹤、取消或重試任務。" />
|
||||
<QuickLinkCard to="/job-schedules" icon="schedule" title="定時排程" desc="以 cron 自動觸發背景任務。" />
|
||||
<QuickLinkCard to="/ai" icon="ai" title="AI 助手" desc="串流對話,輔助紀錄與分析。" />
|
||||
<QuickLinkCard to="/settings" icon="settings" title="系統設定" desc="管理 key-value 設定項目。" />
|
||||
<QuickLinkCard to="/profile" icon="profile" title="個人資料" desc="更新顯示名稱與偏好。" />
|
||||
<QuickLinkCard to="/job-templates" icon="template" title="任務模板" desc="查看可用的背景任務模板。" />
|
||||
<QuickLinkCard to="/settings" icon="settings" title="系統設定" desc="連線、AI 與帳號偏好。" />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="ac-title-bar -mx-1 -mt-1 mb-4">Session</div>
|
||||
<dl className="grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div className="ac-slot px-4 py-3">
|
||||
<dt className="text-xs font-semibold text-ink-secondary">UID</dt>
|
||||
<dd className="mt-1 break-all font-mono text-sm text-ink">{member?.uid || uid}</dd>
|
||||
</div>
|
||||
<div className="ac-slot px-4 py-3">
|
||||
<dt className="text-xs font-semibold text-ink-secondary">Email</dt>
|
||||
<dd className="mt-1 text-ink">{member?.email}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { Badge, Card, CopyableId, Notice, PageTitle } from '../components/ui'
|
||||
import {
|
||||
easterEggCatalog,
|
||||
easterEggKindLabel,
|
||||
type EasterEggEntry,
|
||||
} from '../lib/easterEggCatalog'
|
||||
|
||||
function EasterEggCard({ entry }: { entry: EasterEggEntry }) {
|
||||
const kindTone =
|
||||
entry.kind === 'keyboard' ? 'brand' : entry.kind === 'admin-page' ? 'sky' : 'neutral'
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-ink">{entry.title}</h2>
|
||||
<p className="mt-2 max-w-prose text-base leading-relaxed text-ink-secondary">{entry.summary}</p>
|
||||
</div>
|
||||
<Badge tone={kindTone}>{easterEggKindLabel(entry.kind)}</Badge>
|
||||
</div>
|
||||
|
||||
{entry.password ? (
|
||||
<CopyableId label="密碼(連續輸入)" value={entry.password} />
|
||||
) : (
|
||||
<div className="ac-slot px-5 py-5">
|
||||
<div className="text-sm font-bold text-ink-secondary">密碼</div>
|
||||
<p className="mt-3 text-base text-muted">無 — 透過管理員導覽進入</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-ink-secondary">操作步驟</h3>
|
||||
<ol className="mt-3 list-decimal space-y-2 pl-5 text-base leading-relaxed text-ink">
|
||||
{entry.howTo.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{entry.notes ? (
|
||||
<p className="rounded-[var(--radius-md)] border border-line bg-surface-muted px-4 py-3 text-sm leading-relaxed text-ink-secondary">
|
||||
<span className="font-bold text-ink">備註:</span>
|
||||
{entry.notes}
|
||||
</p>
|
||||
) : null}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function EasterEggsPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<PageTitle
|
||||
title="彩蛋手冊"
|
||||
subtitle="管理員專用備忘 — 密碼與功能說明都記在這裡,免得下次忘記。"
|
||||
/>
|
||||
|
||||
<Notice
|
||||
tone="info"
|
||||
title="僅管理員可見"
|
||||
message="此頁不對一般會員開放。新增或修改彩蛋時,請同步更新 lib/easterEggCatalog.ts。"
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-5">
|
||||
{easterEggCatalog.map((entry) => (
|
||||
<EasterEggCard key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,146 +1,412 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../api/client'
|
||||
import { ApiError } from '../api/client'
|
||||
import { useAuth } from '../auth/AuthContext'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { api, ApiError } from '../api/client'
|
||||
import { ScheduleFrequencyPicker } from '../components/ScheduleFrequencyPicker'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ErrorText,
|
||||
Field,
|
||||
Input,
|
||||
Notice,
|
||||
PageTitle,
|
||||
Select,
|
||||
StatusBadge,
|
||||
Table,
|
||||
TableAction,
|
||||
TablePanel,
|
||||
TableShell,
|
||||
TableEmpty,
|
||||
} from '../components/ui'
|
||||
import type { JobScheduleData, Pagination } from '../types/api'
|
||||
import { formatUnixNano } from '../lib/formatDate'
|
||||
import {
|
||||
buildCronFromFrequency,
|
||||
defaultScheduleFrequency,
|
||||
describeCron,
|
||||
frequencyFromCron,
|
||||
type ScheduleFrequency,
|
||||
} from '../lib/scheduleCron'
|
||||
import { jobTemplateLabel } from '../lib/jobTemplate'
|
||||
import {
|
||||
SCHEDULE_KINDS,
|
||||
SCHEDULE_TIMEZONE,
|
||||
filterManagedSchedules,
|
||||
isManagedScheduleTemplate,
|
||||
resolveCopyMissionTarget,
|
||||
resolvePlacementScanTarget,
|
||||
scheduleDisplayTitle,
|
||||
type ScheduleKind,
|
||||
} from '../lib/scheduleCatalog'
|
||||
import type { CopyMissionData } from '../types/copyMission'
|
||||
import type { JobScheduleData, Pagination, PersonaData } from '../types/api'
|
||||
import type { BrandData } from '../types/brand'
|
||||
import type { PlacementTopicData } from '../types/placementTopic'
|
||||
|
||||
type CopyMissionOption = CopyMissionData & { persona_id: string }
|
||||
|
||||
export function JobSchedulesPage() {
|
||||
const { uid } = useAuth()
|
||||
const [schedules, setSchedules] = useState<JobScheduleData[]>([])
|
||||
const [otherSchedules, setOtherSchedules] = useState<JobScheduleData[]>([])
|
||||
const [topics, setTopics] = useState<PlacementTopicData[]>([])
|
||||
const [brands, setBrands] = useState<BrandData[]>([])
|
||||
const [missions, setMissions] = useState<CopyMissionOption[]>([])
|
||||
const [pagination, setPagination] = useState<Pagination | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [deletingId, setDeletingId] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [form, setForm] = useState({
|
||||
template_type: 'placement-scan',
|
||||
scope: 'persona',
|
||||
scope_id: '',
|
||||
cron: '0 9 * * *',
|
||||
timezone: 'Asia/Taipei',
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const load = async () => {
|
||||
const [kind, setKind] = useState<ScheduleKind>('placement_topic_scan')
|
||||
const [topicId, setTopicId] = useState('')
|
||||
const [missionKey, setMissionKey] = useState('')
|
||||
const [frequency, setFrequency] = useState<ScheduleFrequency>(defaultScheduleFrequency())
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
|
||||
const cron = useMemo(() => buildCronFromFrequency(frequency), [frequency])
|
||||
|
||||
const loadCatalog = useCallback(async () => {
|
||||
const [topicsRes, personasRes, brandsRes] = await Promise.all([
|
||||
api.get<{ list: PlacementTopicData[] }>('/api/v1/placement/topics/', { auth: true }),
|
||||
api.get<{ list: PersonaData[] }>('/api/v1/personas', { auth: true }),
|
||||
api.get<{ list: BrandData[] }>('/api/v1/brands', { auth: true }),
|
||||
])
|
||||
const topicList = topicsRes.list ?? []
|
||||
const personaList = personasRes.list ?? []
|
||||
const missionGroups = await Promise.all(
|
||||
personaList.map(async (persona) => {
|
||||
try {
|
||||
const data = await api.get<{ list: CopyMissionData[] }>(
|
||||
`/api/v1/personas/${encodeURIComponent(persona.id)}/copy-missions`,
|
||||
{ auth: true },
|
||||
)
|
||||
return (data.list ?? []).map((mission) => ({ ...mission, persona_id: persona.id }))
|
||||
} catch {
|
||||
return [] as CopyMissionOption[]
|
||||
}
|
||||
}),
|
||||
)
|
||||
const flatMissions = missionGroups.flat()
|
||||
setTopics(topicList)
|
||||
setBrands(brandsRes.list ?? [])
|
||||
setMissions(flatMissions)
|
||||
setTopicId((prev) => prev || topicList[0]?.id || '')
|
||||
setMissionKey((prev) => {
|
||||
if (prev) return prev
|
||||
const first = flatMissions[0]
|
||||
return first ? `${first.persona_id}:${first.id}` : ''
|
||||
})
|
||||
}, [])
|
||||
|
||||
const loadSchedules = useCallback(async () => {
|
||||
setError('')
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await api.get<{ list: JobScheduleData[]; pagination: Pagination }>(
|
||||
'/api/v1/job/schedules',
|
||||
{ auth: true, query: { page: 1, pageSize: 20 } },
|
||||
{ auth: true, query: { page: 1, pageSize: 50 } },
|
||||
)
|
||||
setSchedules(data.list)
|
||||
setPagination(data.pagination)
|
||||
const all = data.list ?? []
|
||||
setSchedules(filterManagedSchedules(all))
|
||||
setOtherSchedules(all.filter((item) => !isManagedScheduleTemplate(item.template_type)))
|
||||
setPagination(data.pagination ?? null)
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '載入失敗')
|
||||
setError(e instanceof ApiError ? e.message : '載入排程失敗')
|
||||
setSchedules([])
|
||||
setOtherSchedules([])
|
||||
setPagination(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) setForm((f) => ({ ...f, scope_id: uid }))
|
||||
load()
|
||||
}, [uid])
|
||||
Promise.all([loadCatalog(), loadSchedules()]).catch(() => undefined)
|
||||
}, [loadCatalog, loadSchedules])
|
||||
|
||||
const create = async () => {
|
||||
const selectedKind = useMemo(
|
||||
() => SCHEDULE_KINDS.find((item) => item.kind === kind) ?? SCHEDULE_KINDS[0],
|
||||
[kind],
|
||||
)
|
||||
|
||||
const selectedMission = useMemo(() => {
|
||||
const [personaId, missionId] = missionKey.split(':')
|
||||
if (!personaId || !missionId) return null
|
||||
return missions.find((item) => item.persona_id === personaId && item.id === missionId) ?? null
|
||||
}, [missionKey, missions])
|
||||
|
||||
const createSchedule = async () => {
|
||||
setSaving(true)
|
||||
setError('')
|
||||
try {
|
||||
await api.post('/api/v1/job/schedules', form, { auth: true })
|
||||
await load()
|
||||
if (kind === 'placement_topic_scan') {
|
||||
if (!topicId) throw new ApiError(0, '請選擇找 TA 主題')
|
||||
await api.put(
|
||||
`/api/v1/placement/topics/${encodeURIComponent(topicId)}/scan-schedule`,
|
||||
{ cron, timezone: SCHEDULE_TIMEZONE, enabled },
|
||||
{ auth: true },
|
||||
)
|
||||
} else {
|
||||
if (!selectedMission) throw new ApiError(0, '請選擇拷貝任務')
|
||||
await api.put(
|
||||
`/api/v1/personas/${encodeURIComponent(selectedMission.persona_id)}/copy-missions/${encodeURIComponent(selectedMission.id)}/scan-schedule`,
|
||||
{ cron, timezone: SCHEDULE_TIMEZONE, enabled },
|
||||
{ auth: true },
|
||||
)
|
||||
}
|
||||
await loadSchedules()
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '建立失敗')
|
||||
setError(e instanceof ApiError ? e.message : '儲存排程失敗')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = async (id: string, enabled: boolean) => {
|
||||
const path = enabled ? 'disable' : 'enable'
|
||||
await api.post(`/api/v1/job/schedules/${id}/${path}`, {}, { auth: true })
|
||||
await load()
|
||||
const updateCron = async (schedule: JobScheduleData, nextCron: string) => {
|
||||
setError('')
|
||||
try {
|
||||
await api.put(
|
||||
`/api/v1/job/schedules/${encodeURIComponent(schedule.id)}`,
|
||||
{ cron: nextCron, timezone: schedule.timezone || SCHEDULE_TIMEZONE },
|
||||
{ auth: true },
|
||||
)
|
||||
await loadSchedules()
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '更新頻率失敗')
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = async (schedule: JobScheduleData) => {
|
||||
setError('')
|
||||
const path = schedule.enabled ? 'disable' : 'enable'
|
||||
try {
|
||||
await api.post(`/api/v1/job/schedules/${encodeURIComponent(schedule.id)}/${path}`, {}, { auth: true })
|
||||
await loadSchedules()
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '更新狀態失敗')
|
||||
}
|
||||
}
|
||||
|
||||
const removeSchedule = async (schedule: JobScheduleData) => {
|
||||
const target =
|
||||
schedule.template_type === 'placement-scan'
|
||||
? resolvePlacementScanTarget(schedule, topics, brands).label
|
||||
: resolveCopyMissionTarget(schedule, missions).label
|
||||
if (!window.confirm(`確定刪除「${target}」的排程?刪除後需重新設定,無法復原。`)) return
|
||||
|
||||
setDeletingId(schedule.id)
|
||||
setError('')
|
||||
try {
|
||||
await api.delete(`/api/v1/job/schedules/${encodeURIComponent(schedule.id)}`, { auth: true })
|
||||
await loadSchedules()
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : '刪除排程失敗')
|
||||
} finally {
|
||||
setDeletingId('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="Job 排程" subtitle="建立、列表、啟用/停用" />
|
||||
<Card className="mb-4 grid gap-3 md:grid-cols-2">
|
||||
<Field label="Template Type" hint="建議在海巡研究頁用人設專用排程;此處可手動建立">
|
||||
<Input
|
||||
value={form.template_type}
|
||||
onChange={(e) => setForm((f) => ({ ...f, template_type: e.target.value }))}
|
||||
placeholder="placement-scan"
|
||||
<PageTitle title="定時排程" subtitle="定期海巡掃貼文,發文仍須手動" />
|
||||
|
||||
<div className="mb-6 space-y-3">
|
||||
<Notice
|
||||
tone="warning"
|
||||
title="排程不會自動發文"
|
||||
message="定期任務只會依頻率重跑海巡、把新貼文收進候選列表,不會在 Threads 自動 po 文或回留言。掃到合適對象後,請到找 TA 留言或拷貝任務頁手動產草稿並發佈。"
|
||||
/>
|
||||
<Notice
|
||||
tone="info"
|
||||
title="目前支援兩種排程"
|
||||
message="找 TA 雙軌海巡與拷貝忍者爆款海巡。執行目標與 payload 由系統依主題/任務自動帶入,這裡只需選對象與頻率。"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="mb-8 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-ink">新增或更新排程</h2>
|
||||
<p className="mt-1 text-sm leading-relaxed text-ink-secondary">
|
||||
同一主題或拷貝任務重複儲存會更新既有排程,不會建立第二筆。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Field label="排程類型">
|
||||
<Select value={kind} onChange={(e) => setKind(e.target.value as ScheduleKind)}>
|
||||
{SCHEDULE_KINDS.map((item) => (
|
||||
<option key={item.kind} value={item.kind}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<p className="-mt-2 text-sm text-ink-secondary">{selectedKind.description}</p>
|
||||
|
||||
{kind === 'placement_topic_scan' ? (
|
||||
<Field label="找 TA 主題" hint={selectedKind.configureHint}>
|
||||
<Select value={topicId} onChange={(e) => setTopicId(e.target.value)}>
|
||||
{topics.length === 0 ? <option value="">尚無主題,請先到找 TA 建立</option> : null}
|
||||
{topics.map((topic) => {
|
||||
const name = topic.topic_name?.trim() || topic.seed_query?.trim() || topic.id
|
||||
return (
|
||||
<option key={topic.id} value={topic.id}>
|
||||
{name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</Field>
|
||||
) : (
|
||||
<Field label="拷貝任務" hint={selectedKind.configureHint}>
|
||||
<Select value={missionKey} onChange={(e) => setMissionKey(e.target.value)}>
|
||||
{missions.length === 0 ? (
|
||||
<option value="">尚無拷貝任務,請先到拷貝忍者建立</option>
|
||||
) : null}
|
||||
{missions.map((mission) => {
|
||||
const name = mission.label?.trim() || mission.seed_query?.trim() || mission.id
|
||||
return (
|
||||
<option key={`${mission.persona_id}:${mission.id}`} value={`${mission.persona_id}:${mission.id}`}>
|
||||
{name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<ScheduleFrequencyPicker value={frequency} onChange={setFrequency} />
|
||||
<p className="text-sm text-muted">預覽:{describeCron(cron)}(台北時間)</p>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-ink">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-line"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Scope">
|
||||
<Input
|
||||
value={form.scope}
|
||||
onChange={(e) => setForm((f) => ({ ...f, scope: e.target.value }))}
|
||||
placeholder="persona"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Cron">
|
||||
<Input value={form.cron} onChange={(e) => setForm((f) => ({ ...f, cron: e.target.value }))} />
|
||||
</Field>
|
||||
<Field label="Scope ID">
|
||||
<Input
|
||||
value={form.scope_id}
|
||||
onChange={(e) => setForm((f) => ({ ...f, scope_id: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Timezone">
|
||||
<Input
|
||||
value={form.timezone}
|
||||
onChange={(e) => setForm((f) => ({ ...f, timezone: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex flex-wrap gap-2 md:col-span-2">
|
||||
<Button onClick={create}>建立定時排程任務</Button>
|
||||
<Button variant="ghost" onClick={load}>
|
||||
重新載入排程列表
|
||||
建立後立即啟用
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => void createSchedule()} disabled={saving || (kind === 'placement_topic_scan' ? !topicId : !selectedMission)}>
|
||||
{saving ? '儲存中…' : '儲存排程'}
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => void loadSchedules()} disabled={loading}>
|
||||
重新載入列表
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ErrorText message={error} />
|
||||
</Card>
|
||||
<TablePanel title="島民服務處排程" count={pagination?.total}>
|
||||
<TableShell>
|
||||
<Table minWidth="40rem">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Cron</th>
|
||||
<th>模板</th>
|
||||
<th>狀態</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{schedules.map((s) => (
|
||||
<tr key={s.id}>
|
||||
<td className="font-mono text-xs">{s.id.slice(0, 8)}…</td>
|
||||
<td className="font-semibold">{s.cron}</td>
|
||||
<td>{s.template_type}</td>
|
||||
<td>
|
||||
<StatusBadge className={s.enabled ? 'ac-status-badge--success' : 'ac-status-badge--muted'}>
|
||||
{s.enabled ? '啟用中' : '已停用'}
|
||||
</StatusBadge>
|
||||
</td>
|
||||
<td>
|
||||
<TableAction variant={s.enabled ? 'danger' : 'link'} onClick={() => toggle(s.id, s.enabled)}>
|
||||
{s.enabled ? '停用' : '啟用'}
|
||||
</TableAction>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</TableShell>
|
||||
</TablePanel>
|
||||
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-sm text-ink-secondary">
|
||||
{pagination ? `共 ${schedules.length} 筆可管理排程` : loading ? '載入中…' : '—'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden p-0">
|
||||
{loading && schedules.length === 0 ? (
|
||||
<TableEmpty>載入排程中…</TableEmpty>
|
||||
) : schedules.length === 0 ? (
|
||||
<TableEmpty>
|
||||
尚無海巡排程。從上方選類型與對象後儲存;若曾在「研究地圖」頁設定品牌排程,也會出現在這裡。
|
||||
</TableEmpty>
|
||||
) : (
|
||||
<ul className="divide-y divide-line">
|
||||
{schedules.map((schedule) => {
|
||||
const target =
|
||||
schedule.template_type === 'placement-scan'
|
||||
? resolvePlacementScanTarget(schedule, topics, brands)
|
||||
: resolveCopyMissionTarget(schedule, missions)
|
||||
const nextRun = formatUnixNano(schedule.next_run_at)
|
||||
const rowFrequency = frequencyFromCron(schedule.cron)
|
||||
|
||||
return (
|
||||
<li key={schedule.id} className="ac-schedule-row">
|
||||
<div className="ac-schedule-row__main">
|
||||
<div className="ac-schedule-row__info">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-bold text-ink">
|
||||
{scheduleDisplayTitle(schedule.template_type)}
|
||||
</p>
|
||||
<StatusBadge
|
||||
className={schedule.enabled ? 'ac-status-badge--success' : 'ac-status-badge--muted'}
|
||||
>
|
||||
{schedule.enabled ? '啟用' : '停用'}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className="text-xs text-ink-secondary">
|
||||
{'source' in target ? (
|
||||
<span className="text-muted">{target.source} · </span>
|
||||
) : null}
|
||||
<Link to={target.href} className="font-semibold text-brand hover:underline">
|
||||
{target.label}
|
||||
</Link>
|
||||
{nextRun ? <span className="ml-2 text-muted">下次 {nextRun}</span> : null}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ac-schedule-row__controls">
|
||||
<ScheduleFrequencyPicker
|
||||
compact
|
||||
value={rowFrequency}
|
||||
onChange={(next) => {
|
||||
const nextCron = buildCronFromFrequency(next)
|
||||
if (nextCron !== schedule.cron) void updateCron(schedule, nextCron)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ac-schedule-row__actions">
|
||||
<Button
|
||||
variant={schedule.enabled ? 'ghost' : 'soft'}
|
||||
className="min-h-8 px-3 py-1 text-sm"
|
||||
onClick={() => void toggle(schedule)}
|
||||
>
|
||||
{schedule.enabled ? '停用' : '啟用'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
className="min-h-8 px-3 py-1 text-sm"
|
||||
disabled={deletingId === schedule.id}
|
||||
onClick={() => void removeSchedule(schedule)}
|
||||
>
|
||||
{deletingId === schedule.id ? '刪除中…' : '刪除'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{otherSchedules.length > 0 ? (
|
||||
<Card className="mt-6 space-y-4 p-5">
|
||||
<Notice
|
||||
tone="warning"
|
||||
title="另有其他類型排程"
|
||||
message="這些排程不會出現在上方海巡列表,但會被總覽舊版統計計入。若不需要可刪除。"
|
||||
/>
|
||||
<ul className="divide-y divide-line rounded-[var(--radius-lg)] border-2 border-line">
|
||||
{otherSchedules.map((schedule) => (
|
||||
<li key={schedule.id} className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold text-ink">{jobTemplateLabel(schedule.template_type)}</p>
|
||||
<p className="text-xs text-muted">
|
||||
{schedule.scope} / {schedule.scope_id.slice(0, 12)}… · {schedule.enabled ? '啟用' : '停用'}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
className="min-h-8 px-3 py-1 text-sm"
|
||||
disabled={deletingId === schedule.id}
|
||||
onClick={() => void removeSchedule(schedule)}
|
||||
>
|
||||
{deletingId === schedule.id ? '刪除中…' : '刪除'}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue