diff --git a/haixun-backend/.run/api.pid b/haixun-backend/.run/api.pid index 6c9738c..63de6e4 100644 --- a/haixun-backend/.run/api.pid +++ b/haixun-backend/.run/api.pid @@ -1 +1 @@ -91942 +26382 diff --git a/haixun-backend/.run/logs/api.log b/haixun-backend/.run/logs/api.log index f14a5f6..3ee7cff 100644 --- a/haixun-backend/.run/logs/api.log +++ b/haixun-backend/.run/logs/api.log @@ -1,1193 +1,43 @@ -2026/06/25 01:20:54 job scheduler started: holder=wangxinghuadeMacBook-Pro-204.local-scheduler interval=1m0s -2026/06/25 01:20:54 job reaper started: interval=30s +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 Starting backend backend at 0.0.0.0:8890... -2026/06/25 01:20:54 job worker started: id=wangxinghuadeMacBook-Pro-204.local-go-worker type=go -{"@timestamp":"2026-06-25T01:20:55.248+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:53661 - curl/8.7.1","duration":"0.3ms","level":"info","span":"a4d3b3d156ad94b9","trace":"7a0632c42e4d2b37e2f410638e0e7647"} -{"@timestamp":"2026-06-25T01:20:55.258+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:53662 - curl/8.7.1","duration":"0.0ms","level":"info","span":"a245c1d544129fd0","trace":"f40b1f64e462fbb2b69ce1934b9d8398"} -{"@timestamp":"2026-06-25T01:20:57.147+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:53675 - 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":"a55a365496cf0b85","trace":"7401279116cf13132500bce89976c782"} -{"@timestamp":"2026-06-25T01:20:57.155+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:53677 - 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":"e3db07787ae4cca1","trace":"a99a603f60f88c9f355d625effc6c089"} -{"@timestamp":"2026-06-25T01:20:57.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53688 - 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":"9.8ms","level":"info","span":"7a1c7477bc55a3df","trace":"97d152e5963d2ae2e782a9d95aef7e51"} -{"@timestamp":"2026-06-25T01:20:57.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53682 - 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.1ms","level":"info","span":"08cf8a6e008539af","trace":"ce5351c5d22f4e66b0c1c558b5f1ec64"} -{"@timestamp":"2026-06-25T01:20:57.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/ai-settings - 127.0.0.1:53687 - 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.8ms","level":"info","span":"a61acab895d84aec","trace":"812e50217c27027bf80b4a990de3560e"} -{"@timestamp":"2026-06-25T01:20:57.178+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53683 - 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.2ms","level":"info","span":"2ec15d085ae41063","trace":"a0efb9cd5001b022876d4aea564c84d3"} -{"@timestamp":"2026-06-25T01:20:57.178+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53681 - 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.4ms","level":"info","span":"e7af7057aa37f547","trace":"8eaf7c0a3efe4c0feb4ce1739658d4ff"} -{"@timestamp":"2026-06-25T01:20:57.179+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53678 - 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.3ms","level":"info","span":"bd90b8a622c17473","trace":"0effc6689f23ad8ba4350bb14d7bdd93"} -{"@timestamp":"2026-06-25T01:20:57.209+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53703 - 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":"bbcdf6b3528a44f9","trace":"986f0378b0cc647c618b0395f060d556"} -{"@timestamp":"2026-06-25T01:20:57.211+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53704 - 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":"a3632f3947d64095","trace":"e3a259439f4b85a3814d2777901c0e7f"} -{"@timestamp":"2026-06-25T01:20:57.211+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53707 - 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":"215ce7566790c638","trace":"6bf6dd0f1e9517973c5fcbffe2da3719"} -{"@timestamp":"2026-06-25T01:20:57.212+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/ai-settings - 127.0.0.1:53705 - 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":"eeb08e87ac8f85d0","trace":"4de4bc190a1a1f107da65f8e9f25ea7e"} -{"@timestamp":"2026-06-25T01:20:57.212+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:53702 - 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":"3b60d92a37dae7de","trace":"255739b7f4860e6cd0d8360acda495e3"} -{"@timestamp":"2026-06-25T01:20:57.213+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53709 - 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":"f27a6454b8dced3f","trace":"ed30bfe516f08c04d73d2a34a297f455"} -{"@timestamp":"2026-06-25T01:20:57.213+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53706 - 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":"e1069b774d9da609","trace":"23fc2baf3b0235e594b4494498004db1"} -{"@timestamp":"2026-06-25T01:20:57.214+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53714 - 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":"6461ce4c5ce6b1ad","trace":"4cb2a7392803632e782dc5eed4418ea2"} -{"@timestamp":"2026-06-25T01:20:57.216+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53716 - 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":"5efbfdfae391656d","trace":"f8433b634df2e4d51dffdc6eea9a229d"} -{"@timestamp":"2026-06-25T01:20:57.216+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53712 - 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":"a0de87fd6f2be21b","trace":"23b2212dcd34548ced4b29c2dc0a7a68"} -{"@timestamp":"2026-06-25T01:20:57.218+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:53717 - 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":"7127203304f527e0","trace":"7c35fef3ad6c9c38bf8db82dc0d66598"} -{"@timestamp":"2026-06-25T01:20:57.219+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53722 - 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":"bf36bf26aacd831f","trace":"9fdb14d179e32bf0ecb0e5abd77c4e2c"} -{"@timestamp":"2026-06-25T01:20:57.219+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53720 - 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":"e7bcd8ac3e949f57","trace":"01a74bcb1c813d13ba07f2d642dfa128"} -{"@timestamp":"2026-06-25T01:20:57.221+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53723 - 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.8ms","level":"info","span":"e383edf5de1a7931","trace":"5f14e058933dc99a9ae737bf8395e870"} -{"@timestamp":"2026-06-25T01:20:57.224+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53725 - 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":"3bb9396d741406d8","trace":"d64fac9d126c69303a2342bd8a878212"} -{"@timestamp":"2026-06-25T01:20:57.231+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53728 - 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":"34d4595fa4107f66","trace":"98be0147f138b2986eb4793a58dbb89d"} -{"@timestamp":"2026-06-25T01:20:57.231+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53729 - 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":"f60b52cff53cb79f","trace":"030fec208b5317e01e7cac7f332425a4"} -{"@timestamp":"2026-06-25T01:20:57.238+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53732 - 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":"0c4f22f15d35f4ee","trace":"0cea1b7b98a582ff815fa04999e3f5b3"} -{"@timestamp":"2026-06-25T01:20:57.239+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53733 - 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":"d5e77d646bc21ec4","trace":"4207d40ca1166d395c27afb226be40bf"} -{"@timestamp":"2026-06-25T01:20:57.254+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53737 - 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":"b41fb8aa0853fefb","trace":"900b8a254c9e82a74bdad10b55fa3aa4"} -{"@timestamp":"2026-06-25T01:20:58.150+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2094.8ms)","duration":"2094.8ms","level":"slow","span":"c2dd033cd024aaa9","trace":"29288dbff87de8c2a0fd9e34fd414829"} -{"@timestamp":"2026-06-25T01:20:58.150+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2094.8ms","level":"info","span":"c2dd033cd024aaa9","trace":"29288dbff87de8c2a0fd9e34fd414829"} -{"@timestamp":"2026-06-25T01:20:59.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53741 - 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.2ms","level":"info","span":"70472691d2f5de91","trace":"0be13f76a5e93f62553cb5bdfd5b9dc4"} -{"@timestamp":"2026-06-25T01:20:59.804+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53744 - 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":"578860342feff289","trace":"9b4a363b15b38da1255e12eadffaf84b"} -{"@timestamp":"2026-06-25T01:20:59.806+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20 - 127.0.0.1:53743 - 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.5ms","level":"info","span":"a1a2690b17dca70c","trace":"94d26bb64cd6647415ec142056e7504e"} -{"@timestamp":"2026-06-25T01:20:59.812+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53747 - 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":"453c6b47f1c74ee6","trace":"6ea68f28b7cb661a19d632de3d7c48eb"} -{"@timestamp":"2026-06-25T01:20:59.813+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20 - 127.0.0.1:53748 - 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":"bd4265e8a907863a","trace":"d607c6f79f0e157625a0de49c1150018"} -{"@timestamp":"2026-06-25T01:21:01.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53750 - 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":"f938d03e33b6eb8a","trace":"ce0543c7c01b64985b6c3704f156c11b"} -{"@timestamp":"2026-06-25T01:21:03.165+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2004.2ms)","duration":"2004.2ms","level":"slow","span":"b764faa0af87d896","trace":"b8bf476760f5aa0065decc5f577f6637"} -{"@timestamp":"2026-06-25T01:21:03.165+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2004.2ms","level":"info","span":"b764faa0af87d896","trace":"b8bf476760f5aa0065decc5f577f6637"} -{"@timestamp":"2026-06-25T01:21:03.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53752 - 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.0ms","level":"info","span":"b20cb511f8fec371","trace":"34134a3a87a02c8342863dfac357b9dc"} -{"@timestamp":"2026-06-25T01:21:05.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53754 - 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":"9687b3db2dd38a69","trace":"6cc7b9698921c3feb6f832da8afe116b"} -{"@timestamp":"2026-06-25T01:21:07.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53756 - 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":"011607fea61a800d","trace":"acd21292b1b9dd8909784816c697a877"} -{"@timestamp":"2026-06-25T01:21:08.206+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2035.0ms)","duration":"2035.0ms","level":"slow","span":"bd05afa7ecfd41bb","trace":"17aa88308da57fb005a6701a26195fb2"} -{"@timestamp":"2026-06-25T01:21:08.206+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2035.0ms","level":"info","span":"bd05afa7ecfd41bb","trace":"17aa88308da57fb005a6701a26195fb2"} -{"@timestamp":"2026-06-25T01:21:09.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53758 - 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.2ms","level":"info","span":"2674428f27ee8d56","trace":"4a64de49c020270f70716cd8fc2c36fa"} -{"@timestamp":"2026-06-25T01:21:11.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53760 - 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":"a06c5739aee2112a","trace":"610e9bc2d678aa24bf4af9f1450769cf"} -{"@timestamp":"2026-06-25T01:21:13.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53762 - 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":"d6ef2f441d442cca","trace":"508573461d9111bd31ce5134625af236"} -{"@timestamp":"2026-06-25T01:21:13.274+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2063.5ms)","duration":"2063.5ms","level":"slow","span":"bbd1158e0c561aa7","trace":"b598175acb46a594a32e3c8c0facd56c"} -{"@timestamp":"2026-06-25T01:21:13.274+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2063.5ms","level":"info","span":"bbd1158e0c561aa7","trace":"b598175acb46a594a32e3c8c0facd56c"} -{"@timestamp":"2026-06-25T01:21:15.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53764 - 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.8ms","level":"info","span":"1ec70e69b2ee0ed0","trace":"428c5bd10e7d3a009b49f8fa20949c0e"} -{"@timestamp":"2026-06-25T01:21:17.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53766 - 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.2ms","level":"info","span":"0dd1e9bc5db7d4db","trace":"93d665e659e0b82c53f83b044cddf0f8"} -{"@timestamp":"2026-06-25T01:21:18.318+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2038.4ms)","duration":"2038.4ms","level":"slow","span":"834beea6b9f9c46c","trace":"2e05fdc9adb64faaa466d1edc0251937"} -{"@timestamp":"2026-06-25T01:21:18.318+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2038.4ms","level":"info","span":"834beea6b9f9c46c","trace":"2e05fdc9adb64faaa466d1edc0251937"} -{"@timestamp":"2026-06-25T01:21:19.166+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53768 - 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":"681357363489c30e","trace":"cfcba78914d7522e2a8a8642596ab8c2"} -{"@timestamp":"2026-06-25T01:21:19.980+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3bcd46a6c5019550b87619 - 127.0.0.1:53772 - 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":"4c4a7c9e8450d26b","trace":"2c95aa2b3e2df680a4c4da24c67303b0"} -{"@timestamp":"2026-06-25T01:21:19.981+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53774 - 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":"f921fe8846b8d27a","trace":"a9ae70877241116723e789d2c775aedd"} -{"@timestamp":"2026-06-25T01:21:19.985+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3bcd46a6c5019550b87619/events?limit=50 - 127.0.0.1:53773 - 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":"8.2ms","level":"info","span":"559c6a147a7bc595","trace":"05ffd09b28310e714391b2094febc886"} -{"@timestamp":"2026-06-25T01:21:19.987+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3bcd46a6c5019550b87619 - 127.0.0.1:53776 - 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":"9f246cc65d398723","trace":"11d67a8c3c76030c559d14eddde69a09"} -{"@timestamp":"2026-06-25T01:21:19.988+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53778 - 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":"e43bf24748aa8d8b","trace":"32936829aa8dac2310ab75ff14b7fe56"} -{"@timestamp":"2026-06-25T01:21:19.989+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3bcd46a6c5019550b87619/events?limit=50 - 127.0.0.1:53780 - 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":"7cd86a38cd2c5be4","trace":"dbd755c2f93b0e57bc0c512964bcaf37"} -{"@timestamp":"2026-06-25T01:21:21.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53783 - 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.9ms","level":"info","span":"b0af34e47cd6a3a8","trace":"f0dbd4b3922cb5d6d8f43c781fe5f0c0"} -{"@timestamp":"2026-06-25T01:21:23.109+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53786 - 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":"f3b3ad63af76aad2","trace":"f9ba43bc653607a706360acc57bb67a3"} -{"@timestamp":"2026-06-25T01:21:23.112+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20 - 127.0.0.1:53785 - 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":"6a47e9821f999e43","trace":"77c99b0ac10226eed658d8cd12f193a7"} -{"@timestamp":"2026-06-25T01:21:23.117+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53788 - 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":"9c7d279e172359d1","trace":"70ef2af392bc5670652efd4c1c63b54b"} -{"@timestamp":"2026-06-25T01:21:23.119+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20 - 127.0.0.1:53790 - 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":"3013c385661e6fd3","trace":"e73cbb3791313f0fb327fb53cbdb27cb"} -{"@timestamp":"2026-06-25T01:21:23.167+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53792 - 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":"709a3f772bdc77c9","trace":"da372bfa5b362c71c5137ff12b7e9bba"} -{"@timestamp":"2026-06-25T01:21:23.334+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2011.1ms)","duration":"2011.1ms","level":"slow","span":"1eeebe7c89df9707","trace":"3aabb5753b0d50195a3c472e300509e8"} -{"@timestamp":"2026-06-25T01:21:23.334+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2011.1ms","level":"info","span":"1eeebe7c89df9707","trace":"3aabb5753b0d50195a3c472e300509e8"} -{"@timestamp":"2026-06-25T01:21:25.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53794 - 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":"1b72efa36a10bd57","trace":"41eb19cf9304628fed4d2e3e29d4d489"} -{"@timestamp":"2026-06-25T01:21:27.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53796 - 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":"0936f09f887e2a66","trace":"7fe27ff04e73db7799974e71a6168d39"} -{"@timestamp":"2026-06-25T01:21:28.364+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.7ms)","duration":"2026.7ms","level":"slow","span":"6afb6600b1666bb5","trace":"cbb3c52a2ce6a06f8ab3ec81eaae6a94"} -{"@timestamp":"2026-06-25T01:21:28.365+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.7ms","level":"info","span":"6afb6600b1666bb5","trace":"cbb3c52a2ce6a06f8ab3ec81eaae6a94"} -{"@timestamp":"2026-06-25T01:21:29.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53798 - 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.3ms","level":"info","span":"c9f7440525073e7b","trace":"3e131b365648bf6ada5066cf2610c8f2"} -{"@timestamp":"2026-06-25T01:21:29.622+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:53801 - 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":"43592513f67f62d7","trace":"76156f3f5352f4f70145840bcb0a752f"} -{"@timestamp":"2026-06-25T01:21:29.623+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53802 - 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":"d6a6aa0baacbb8de","trace":"38e46e91a199f3a6505e1de767c5b6f9"} -{"@timestamp":"2026-06-25T01:21:29.625+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/health - 127.0.0.1:53804 - 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":"0e60548a7440d967","trace":"00107f8711761521c7e73b34f81af6e0"} -{"@timestamp":"2026-06-25T01:21:29.633+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53807 - 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":"b6966e3783dd1582","trace":"30d3f60dc83124addf2ca59e240668e8"} -{"@timestamp":"2026-06-25T01:21:31.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53808 - 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":"43143d196d9f9378","trace":"b5a570e38a85b09dd40118408278e647"} -{"@timestamp":"2026-06-25T01:21:31.288+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:53813 - 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":"3cc16f3bf846f439","trace":"6996105f1395ce7e468c147059273dcd"} -{"@timestamp":"2026-06-25T01:21:31.288+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53814 - 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":"e2f5d9ec156a5954","trace":"2304f6bb3e6d4b24879930783c7cddad"} -{"@timestamp":"2026-06-25T01:21:31.291+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53812 - 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.5ms","level":"info","span":"b3d342a998b325d7","trace":"86362eb5bcc92d0e06f6199ce29e7017"} -{"@timestamp":"2026-06-25T01:21:31.293+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:53816 - 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":"ebb830b1c00ec516","trace":"6d1520def0b9ce424b135d07a4d79d78"} -{"@timestamp":"2026-06-25T01:21:31.296+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53818 - 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":"eaf1e6621152f5c4","trace":"eb1d3cb35f77c26d2a2f9823201a04bf"} -{"@timestamp":"2026-06-25T01:21:31.297+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53820 - 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":"cbbff0ba0798a368","trace":"4d1a242313373b1ebabfc55f58d97118"} -{"@timestamp":"2026-06-25T01:21:33.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53822 - 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.3ms","level":"info","span":"9675bd0ced7103b4","trace":"9bd468a9550509ab36f5f9824fee866e"} -{"@timestamp":"2026-06-25T01:21:33.374+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2004.0ms)","duration":"2004.0ms","level":"slow","span":"3d6e848bd8396828","trace":"e573be0b4ada604e552371ac81e4ee44"} -{"@timestamp":"2026-06-25T01:21:33.374+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2004.0ms","level":"info","span":"3d6e848bd8396828","trace":"e573be0b4ada604e552371ac81e4ee44"} -{"@timestamp":"2026-06-25T01:21:33.587+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53830 - 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":"dd12ccfd47c81b3f","trace":"ce2d4d5330db14a20c99005b593ac06a"} -{"@timestamp":"2026-06-25T01:21:33.588+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53828 - 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":"d2ed984e4af46d37","trace":"10ec971e01247ababa6c5a06228e603e"} -{"@timestamp":"2026-06-25T01:21:33.588+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53827 - 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":"d5f7a24977ebcd53","trace":"2d043afe3204e7d5e09a6460c1a5ca9f"} -{"@timestamp":"2026-06-25T01:21:33.590+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53829 - 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.4ms","level":"info","span":"4a13aaa929914ccc","trace":"f0e1db7433f97589c05831b1ee8385be"} -{"@timestamp":"2026-06-25T01:21:33.595+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53834 - 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":"045d114ef065b22e","trace":"c62d9e7007ac824c1249e881c2a32f6f"} -{"@timestamp":"2026-06-25T01:21:33.597+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53840 - 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":"6679e10e8fab4668","trace":"d1240837ad7b531ce2ec5546a576f824"} -{"@timestamp":"2026-06-25T01:21:33.597+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53838 - 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":"215006c8f5099133","trace":"292335c167f9373c0deebdc87368c0da"} -{"@timestamp":"2026-06-25T01:21:33.597+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:53835 - 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":"33502537800cebce","trace":"abe92ce8f348fbe9c857957a0e86d3dd"} -{"@timestamp":"2026-06-25T01:21:33.598+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53839 - 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":"466afa9586e11260","trace":"e4fcb97446d914d82cb33dc2367a6d30"} -{"@timestamp":"2026-06-25T01:21:33.602+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53843 - 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":"943d94308193a96e","trace":"cd6a355ba2bd3f8dedbf3360928aa5f0"} -{"@timestamp":"2026-06-25T01:21:33.602+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53844 - 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":"fdea2634e6d2fabe","trace":"2f3a83992b0cea2d79efe4731ddc3fb3"} -{"@timestamp":"2026-06-25T01:21:33.605+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53846 - 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":"a74c224dfb6e617d","trace":"60f4f6f0628e55d83d018306e97315f2"} -{"@timestamp":"2026-06-25T01:21:35.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53848 - 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":"10862dc8ea4c4205","trace":"9fa1e5d5c55e7228e6647a66008dfae3"} -{"@timestamp":"2026-06-25T01:21:37.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53850 - 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":"363008e47f0ae08d","trace":"9eb846c67736cc36d962d09bb7bf6d67"} -{"@timestamp":"2026-06-25T01:21:37.873+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph/expand - 127.0.0.1:53852 - 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":"20.5ms","level":"info","span":"4888e4bdd49c3e10","trace":"9ff49f514f384b66b49b29522c4de693"} -{"@timestamp":"2026-06-25T01:21:37.879+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:53854 - 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":"6280082b4c40f0d3","trace":"addf9ae02e5e03b2c4c6ffed575e9b29"} -{"@timestamp":"2026-06-25T01:21:37.883+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:53856 - 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":"8d9cba661bb38145","trace":"3ec99629d6704d28b33f11e741f09a8d"} -{"@timestamp":"2026-06-25T01:21:37.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53858 - 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":"651cbcc09d3fad91","trace":"bffe6aa5d4b77391b67d25d4995bc5b4"} -{"@timestamp":"2026-06-25T01:21:37.889+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53860 - 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":"d4769de557e4660b","trace":"687bee8e4c0f16cd1c4e551f5792a279"} -{"@timestamp":"2026-06-25T01:21:37.890+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53862 - 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":"5625ce9be8f05de3","trace":"17d88f8d66621eda36733bf5ce01df2b"} -{"@timestamp":"2026-06-25T01:21:37.897+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53865 - 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":"cd6bf923b03d5f92","trace":"5a9fc531fc8dd85cb69b9d0e94fecd3a"} -{"@timestamp":"2026-06-25T01:21:37.898+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53866 - 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":"d8df8e40a0dd10bd","trace":"a0a208574cfb876a364ce76a01a9fb9f"} -{"@timestamp":"2026-06-25T01:21:37.901+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53868 - 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":"e00e3f2541f5e89f","trace":"443490151a46457a04ecd35917ffdca0"} -{"@timestamp":"2026-06-25T01:21:38.408+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.0ms)","duration":"2027.0ms","level":"slow","span":"756d9bc1869b6bc4","trace":"302dc25297d4859598fe5caacd418029"} -{"@timestamp":"2026-06-25T01:21:38.409+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.0ms","level":"info","span":"756d9bc1869b6bc4","trace":"302dc25297d4859598fe5caacd418029"} -{"@timestamp":"2026-06-25T01:21:39.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53871 - 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":"67d339352d99e1d0","trace":"63d63bf09bde9415df0670e35a0fa590"} -{"@timestamp":"2026-06-25T01:21:40.888+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:53873 - 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":"bd9176bd8a2e14fc","trace":"b6ac7c99b5b4806227abe14856abe82c"} -{"@timestamp":"2026-06-25T01:21:40.897+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53875 - 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":"846240105c53efdc","trace":"07041b90f3c0959e0c07dd7cd03a0c57"} -{"@timestamp":"2026-06-25T01:21:40.903+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53877 - 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":"2479793e5b700d6d","trace":"33a38d170fb9cc70cd0e5ac268236661"} -{"@timestamp":"2026-06-25T01:21:40.910+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53879 - 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":"23bd7263a0de67bd","trace":"c0c62f92dd97f6890bd75d5b371d1150"} -{"@timestamp":"2026-06-25T01:21:41.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53882 - 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":"bd1191d36bf1a679","trace":"5e490958499e2c9fb8f8f6e71c64cf24"} -{"@timestamp":"2026-06-25T01:21:43.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53884 - 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":"bd5f059974d6a582","trace":"27db8200838aba50691647bf65cb5c29"} -{"@timestamp":"2026-06-25T01:21:43.418+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2004.5ms)","duration":"2004.5ms","level":"slow","span":"d35d3530bc1c1b86","trace":"4e4c1bf2e1704b5e95a0e17181a5c95a"} -{"@timestamp":"2026-06-25T01:21:43.418+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2004.5ms","level":"info","span":"d35d3530bc1c1b86","trace":"4e4c1bf2e1704b5e95a0e17181a5c95a"} -{"@timestamp":"2026-06-25T01:21:43.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:53886 - 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":"eecd5a74b2edbc91","trace":"c1558802c2b933cb9e9af96be1b4e88f"} -{"@timestamp":"2026-06-25T01:21:43.890+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:53888 - 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":"34c1a36bf59b83e2","trace":"5a332a80ecd533200d4cb75ad2d478f8"} -{"@timestamp":"2026-06-25T01:21:43.896+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53890 - 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":"6b834c3cc537bfee","trace":"8db672443b0b7046728185da84176384"} -{"@timestamp":"2026-06-25T01:21:43.902+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:53892 - 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":"bbee3eb35a471bc9","trace":"c7fb3bc54ddaec0d1fbab86755631f15"} -{"@timestamp":"2026-06-25T01:21:45.167+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53894 - 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":"51aa1fb3e14e2554","trace":"466168551dd5d14148fedca50c95a99f"} -{"@timestamp":"2026-06-25T01:21:45.760+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:53901 - 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":"876461f80294b49f","trace":"c73a5a0034b89390f47b5e77d408e06c"} -{"@timestamp":"2026-06-25T01:21:45.760+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53899 - 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":"38e1061e9635686c","trace":"3d5b26e995a1c5039bb6dc2c12f75cbd"} -{"@timestamp":"2026-06-25T01:21:45.760+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:53902 - 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":"d0ba95b13a08c907","trace":"7a642cc668230305e3278ae03d58d830"} -{"@timestamp":"2026-06-25T01:21:45.760+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53900 - 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":"1190aead2dd4cc82","trace":"b099146c670c94ce22b52978e4b97895"} -{"@timestamp":"2026-06-25T01:21:45.767+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53907 - 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":"6464d233e6f2e921","trace":"02e2925195d3d5d526362db6f236a117"} -{"@timestamp":"2026-06-25T01:21:45.768+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53905 - 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":"201ef0276bd3c910","trace":"e7b2b7abe5976c1d2f9bda7fe30ed82a"} -{"@timestamp":"2026-06-25T01:21:45.768+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:53908 - 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":"2dc26b94264e9e9c","trace":"c3fc2e7c5efa65e7cc3b19f1cb3b408b"} -{"@timestamp":"2026-06-25T01:21:45.775+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53912 - 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":"9579b1a711b4c709","trace":"f9f38b1cc575c98cc9cbd497c321349c"} -{"@timestamp":"2026-06-25T01:21:45.778+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53911 - 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":"de3aaa84adbd3f43","trace":"e354a22980ecc30c771ffb28e493e5a7"} -{"@timestamp":"2026-06-25T01:21:45.786+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53914 - 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":"3701320ac69f3aa4","trace":"d49d7b056399b6955692152d96ebd513"} -{"@timestamp":"2026-06-25T01:21:46.816+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph/expand - 127.0.0.1:53916 - 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":"8.1ms","level":"info","span":"ba14f141fe65b1b5","trace":"8f36c66eebba64ea1483f7be7ea8fb5f"} -{"@timestamp":"2026-06-25T01:21:46.824+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53918 - 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":"ccdba9ad9fa14136","trace":"532a0600607c134251ffd5ba4aee49b5"} -{"@timestamp":"2026-06-25T01:21:46.828+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53921 - 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":"a2b7e15ef606bb82","trace":"657189dd3990755032a2e0f84e8364eb"} -{"@timestamp":"2026-06-25T01:21:46.829+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53922 - 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":"371b7f7516705e55","trace":"ab2c2ad84a987c59b0f0b91a0ace0359"} -{"@timestamp":"2026-06-25T01:21:46.836+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53926 - 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":"10819dc7508922d6","trace":"fce7e0915648bcbe9311ec3380c71ea5"} -{"@timestamp":"2026-06-25T01:21:46.837+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53925 - 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":"687f61d6e87be3f2","trace":"ae0d9c20317cf99c9097d80cac86766c"} -{"@timestamp":"2026-06-25T01:21:46.844+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53930 - 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":"d7304d6679bc6019","trace":"732dbfda6a3261c4f7fbebcc7eda352c"} -{"@timestamp":"2026-06-25T01:21:46.844+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53928 - 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.2ms","level":"info","span":"68020b8926c5cc1f","trace":"81ab7aa2f0085a1652d623b894e31939"} -{"@timestamp":"2026-06-25T01:21:46.849+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53932 - 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":"bb1bbe0d92645a1d","trace":"5c66037c0a85682607cbb962703e3b19"} -{"@timestamp":"2026-06-25T01:21:47.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53934 - 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":"71e51a9c9c70cf34","trace":"130fe23b92e98d82366bf0f4f6df67c7"} -{"@timestamp":"2026-06-25T01:21:48.461+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2037.8ms)","duration":"2037.8ms","level":"slow","span":"23d879bccae2b2be","trace":"40a5cc023036ffd84c540cf71ece84d4"} -{"@timestamp":"2026-06-25T01:21:48.461+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2037.8ms","level":"info","span":"23d879bccae2b2be","trace":"40a5cc023036ffd84c540cf71ece84d4"} -{"@timestamp":"2026-06-25T01:21:49.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53936 - 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":"9418351697a59885","trace":"085ce1f15e29d6cdfbe6e1c64da6fe9d"} -{"@timestamp":"2026-06-25T01:21:49.829+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53938 - 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":"b88a1386e6bf9df3","trace":"24441e5b3cb19d6044e0a12cd7659610"} -{"@timestamp":"2026-06-25T01:21:49.836+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53940 - 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":"ad1d334358173cc3","trace":"762f6c1dabd37929d200073287ebc119"} -{"@timestamp":"2026-06-25T01:21:49.842+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53942 - 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":"bf1d2cdeb1b31639","trace":"6f5139dc5eaac0af86eafaf668a16f7a"} -{"@timestamp":"2026-06-25T01:21:49.850+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53944 - 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.3ms","level":"info","span":"e20c6ef1ea6b02c3","trace":"10b6bedf145ab3313d095667440855da"} -{"@timestamp":"2026-06-25T01:21:51.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53946 - 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":"33fc3978490bda9c","trace":"69ca12098cd62d7b242aa006c77a59ea"} -{"@timestamp":"2026-06-25T01:21:52.829+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53948 - 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":"e98f615632b5e71f","trace":"45abea716afac9ad974022a5a4104afc"} -{"@timestamp":"2026-06-25T01:21:52.837+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53950 - 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":"e8c61bb289e1717f","trace":"676b747a5df8d411bec8a45c77707f4d"} -{"@timestamp":"2026-06-25T01:21:52.842+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53952 - 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":"28f11c5ca6c26247","trace":"26ba376b82c81db92b7e088c96ebf1b9"} -{"@timestamp":"2026-06-25T01:21:52.848+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53954 - 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":"8eba05d8ef215590","trace":"8c96b2311114546c3a5a02ba49de69ac"} -{"@timestamp":"2026-06-25T01:21:53.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53956 - 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":"f951ec3ae81368a7","trace":"5bffe0b4ae7af8fc84719b0a0a65c05d"} -{"@timestamp":"2026-06-25T01:21:53.497+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2031.0ms)","duration":"2031.0ms","level":"slow","span":"27a52f815564a0d2","trace":"fa878a3756493553c8d88d97556d0edb"} -{"@timestamp":"2026-06-25T01:21:53.497+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2031.0ms","level":"info","span":"27a52f815564a0d2","trace":"fa878a3756493553c8d88d97556d0edb"} -{"@timestamp":"2026-06-25T01:21:54.661+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.6Mi, TotalAlloc=25.6Mi, Sys=23.1Mi, NumGC=17","level":"stat"} -{"@timestamp":"2026-06-25T01:21:54.720+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 150, pass: 150, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:21:55.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53958 - 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":"f5ffeef8cf59b64b","trace":"79737e37bca4ddcdd0411ed086831f06"} -{"@timestamp":"2026-06-25T01:21:55.249+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 2.5/s, drops: 0, avg time: 164.7ms, med: 3.1ms, 90th: 16.3ms, 99th: 2094.7ms, 99.9th: 2094.7ms","level":"stat"} -{"@timestamp":"2026-06-25T01:21:55.830+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53960 - 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":"318b7b86bce1d9a0","trace":"3c7ed6647cb00b8d99b1133554c93a7f"} -{"@timestamp":"2026-06-25T01:21:55.836+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53962 - 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":"9afc1f82638468e3","trace":"c3f51cace85c52a4507e5061eee89f84"} -{"@timestamp":"2026-06-25T01:21:55.842+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53964 - 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":"5a4ce8ad60648579","trace":"443f5c50cac2ee4b409e73013fa71c82"} -{"@timestamp":"2026-06-25T01:21:55.848+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53966 - 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":"6539b28e9e87fa02","trace":"cb21d690fe427d66dffc37d8fa81ce99"} -{"@timestamp":"2026-06-25T01:21:57.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53968 - 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":"b0eeefbdf0aad970","trace":"79c3405b9a2a8b99711c956565a9e9e9"} -{"@timestamp":"2026-06-25T01:21:58.513+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2013.1ms)","duration":"2013.1ms","level":"slow","span":"3d0529c1a401834e","trace":"18e99694736adf898c5c72e1114ef580"} -{"@timestamp":"2026-06-25T01:21:58.513+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2013.1ms","level":"info","span":"3d0529c1a401834e","trace":"18e99694736adf898c5c72e1114ef580"} -{"@timestamp":"2026-06-25T01:21:58.830+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53970 - 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":"1f71a09d07c232cf","trace":"8d39b74e2fa1bbf94899f9500c78d83d"} -{"@timestamp":"2026-06-25T01:21:58.837+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53972 - 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":"80b7775b11390b8d","trace":"8bdd44b343d26e07c4b7cfbc171c2b18"} -{"@timestamp":"2026-06-25T01:21:58.843+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53974 - 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":"a62b1d731106ccba","trace":"281f35aa26925db323fb449895e41750"} -{"@timestamp":"2026-06-25T01:21:58.849+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53976 - 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":"098b9f8ade9f1cf1","trace":"5683789895bac377ff6ec62e86f48189"} -{"@timestamp":"2026-06-25T01:21:59.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53978 - 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":"02e102e17b956528","trace":"6d4e5ebfa5f065704e56294d8bcaa5db"} -{"@timestamp":"2026-06-25T01:22:01.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53980 - 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":"fa1b963ff98b2892","trace":"e4729cf9deddb9734fd99d39a48271c9"} -{"@timestamp":"2026-06-25T01:22:01.830+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53982 - 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":"663eb83ce99145c5","trace":"901f4863fac2b794e8b336264af9675a"} -{"@timestamp":"2026-06-25T01:22:01.838+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53984 - 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":"7511e9535adf33a5","trace":"8751d79fdc88527c264d5462bef42830"} -{"@timestamp":"2026-06-25T01:22:01.844+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53986 - 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":"a75dc9ceabf780a3","trace":"8a991cb9192edd1bfef3fbde6b8d51f5"} -{"@timestamp":"2026-06-25T01:22:01.852+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53988 - 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":"29ef49b4d4e9387d","trace":"636288a9d139eb5339a80a13b5062dbb"} -{"@timestamp":"2026-06-25T01:22:03.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:53990 - 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":"7f3773dbb52bea10","trace":"a546b52c27d4da47a9bfbd9f78722c0b"} -{"@timestamp":"2026-06-25T01:22:03.550+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2032.8ms)","duration":"2032.8ms","level":"slow","span":"4c06e3718f8a7c06","trace":"041ad82a219c0fece32d5695be2a0aa1"} -{"@timestamp":"2026-06-25T01:22:03.550+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2032.8ms","level":"info","span":"4c06e3718f8a7c06","trace":"041ad82a219c0fece32d5695be2a0aa1"} -{"@timestamp":"2026-06-25T01:22:04.830+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:53992 - 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":"5f1748fb657e6ec5","trace":"906ef1829ad7431a22f65ad74066207a"} -{"@timestamp":"2026-06-25T01:22:04.838+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:53994 - 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":"8441ff46f86cc86f","trace":"99c2c055bec6b104c2efea96e839019b"} -{"@timestamp":"2026-06-25T01:22:04.843+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:53996 - 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":"bad9c13a38bcb391","trace":"ae6db2f00eca52df54dec16142d17bb1"} -{"@timestamp":"2026-06-25T01:22:04.849+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:53998 - 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":"ba8827718034566b","trace":"47e522b984b6582579f31b0dde001970"} -{"@timestamp":"2026-06-25T01:22:04.906+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54005 - 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":"9d2e22946d8b32d3","trace":"0c709c3cfb5cf071eb76b3ba8f586c02"} -{"@timestamp":"2026-06-25T01:22:04.906+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54003 - 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":"cde3d33f5453346a","trace":"83415c70e5debcd719fe4a409e866554"} -{"@timestamp":"2026-06-25T01:22:04.906+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54006 - 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":"6e7110275740edfa","trace":"8e931f3ef12794e61b9bffc825d3c830"} -{"@timestamp":"2026-06-25T01:22:04.908+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54004 - 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":"42b3dd0f1ea44af0","trace":"2d8d8b81b47050576b940d3f17e7f586"} -{"@timestamp":"2026-06-25T01:22:04.910+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54008 - 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":"460f798889ca3e4b","trace":"448bed73b4c9dab8d39c5b2c16105435"} -{"@timestamp":"2026-06-25T01:22:04.911+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54011 - 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":"1391d40de55adfbb","trace":"371ae0b8bfa91761cf725d7fd2f82eec"} -{"@timestamp":"2026-06-25T01:22:04.912+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54012 - 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":"01526b6a9eeefee0","trace":"1e7f47e4519bf7bc393de3f618644d5f"} -{"@timestamp":"2026-06-25T01:22:04.916+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54016 - 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":"226b8c32e16e2cc2","trace":"1cb0f6f592f5fa6938f39ba4d9f65922"} -{"@timestamp":"2026-06-25T01:22:04.916+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54017 - 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":"2ceb325473024f03","trace":"2047a9f43d2e335a0b2f80129c5e5961"} -{"@timestamp":"2026-06-25T01:22:04.918+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54018 - 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":"c84da6d2ec673b79","trace":"edf569c8119bc3856b3376ab8e5e3822"} -{"@timestamp":"2026-06-25T01:22:04.922+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54022 - 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":"118568402381e760","trace":"4e3e37acd477c0b539eaf6f2f196c2a2"} -{"@timestamp":"2026-06-25T01:22:04.923+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54021 - 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":"d6239f0c363f4dd4","trace":"5f620994091b9f2d1e2a7222697ff837"} -{"@timestamp":"2026-06-25T01:22:04.929+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54024 - 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":"d7495150c9ef821d","trace":"5a82381079382e6db4fd2ad5186b488c"} -{"@timestamp":"2026-06-25T01:22:04.936+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54026 - 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.2ms","level":"info","span":"b13332d971624917","trace":"6a5eaa205c887e86dd381ca6ecfcbf8a"} -{"@timestamp":"2026-06-25T01:22:05.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54028 - 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":"18aaae510eb3dfae","trace":"fbf193bb08fd9e4e5b61c1044a675c3f"} -{"@timestamp":"2026-06-25T01:22:07.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54030 - 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":"a3d6bfd8249de8d9","trace":"d68f5720dc7ff1218ef25e6f8697d923"} -{"@timestamp":"2026-06-25T01:22:07.921+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54032 - 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":"5a2663b2c30127aa","trace":"99c514dbe868a65801b77de393aba235"} -{"@timestamp":"2026-06-25T01:22:07.929+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54034 - 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":"d476b35041e05a24","trace":"ab94a322e56643dc6f823fd46445a8ab"} -{"@timestamp":"2026-06-25T01:22:07.936+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54036 - 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":"e40b87874d81bdb3","trace":"6b1d680d64239223b3ef28fc315592a7"} -{"@timestamp":"2026-06-25T01:22:07.942+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54038 - 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":"cf93a02262d7d638","trace":"552250b0e4968d4c58dfcac09b66bdc5"} -{"@timestamp":"2026-06-25T01:22:08.578+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.6ms)","duration":"2026.6ms","level":"slow","span":"2c44b822c96379a0","trace":"b683b270d405d73129433bd7e83462f4"} -{"@timestamp":"2026-06-25T01:22:08.578+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.6ms","level":"info","span":"2c44b822c96379a0","trace":"b683b270d405d73129433bd7e83462f4"} -{"@timestamp":"2026-06-25T01:22:09.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54040 - 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":"512e40b22ea5ea0c","trace":"ba720b44fd7974f887237aef6b6186bc"} -{"@timestamp":"2026-06-25T01:22:10.322+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54045 - 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":"c90834a5d2416d2a","trace":"fadb197690ec490f7b2cfe8309f8d3c0"} -{"@timestamp":"2026-06-25T01:22:10.323+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54047 - 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":"3673676507786088","trace":"64d5704d5aaaaf1db7241d72a45207d3"} -{"@timestamp":"2026-06-25T01:22:10.324+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54048 - 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":"d3cb2754eb97851c","trace":"22a5ade2ad2602aec86d3be6018d592c"} -{"@timestamp":"2026-06-25T01:22:10.324+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54046 - 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":"fd8a4a64ee21ea4c","trace":"192958b4ced1a2b4d897e0a63dfa95cb"} -{"@timestamp":"2026-06-25T01:22:10.331+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54052 - 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":"8d138340d0419982","trace":"5c601e20d4fcedc4771b00f56af77e1c"} -{"@timestamp":"2026-06-25T01:22:10.333+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54056 - 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":"0f69359867dafbd2","trace":"6703069ed4b383cd940c5628b074a945"} -{"@timestamp":"2026-06-25T01:22:10.333+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54054 - 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":"5276d582c8b4f09f","trace":"ba3994c3676b67e9b88a0fa3924ca6ea"} -{"@timestamp":"2026-06-25T01:22:10.333+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54053 - 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":"de76e2a4be16ebb2","trace":"7f29bb52295214fcaa816220b4a08435"} -{"@timestamp":"2026-06-25T01:22:10.338+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54061 - 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":"9882271745bfc980","trace":"85fe37d0b370fbf51ac900593d73671e"} -{"@timestamp":"2026-06-25T01:22:10.338+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54060 - 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":"f2d6fdd94b671df6","trace":"13b2afcd8231c4df5ada48a088f6779c"} -{"@timestamp":"2026-06-25T01:22:10.338+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54062 - 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":"518854858a14ca4a","trace":"6be081df9ccc4118783307b2fd60edc9"} -{"@timestamp":"2026-06-25T01:22:10.346+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54066 - 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":"da7a8ea3656d3d12","trace":"189d02a9f3d072a67fc4969cee07b9ca"} -{"@timestamp":"2026-06-25T01:22:10.348+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54065 - 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":"64a2eaad9a6a9b6b","trace":"8158346af8762aa43a8e317551616281"} -{"@timestamp":"2026-06-25T01:22:10.353+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54068 - 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":"ee7647860de63730","trace":"7f03a0be1ea148fa76467eb323ae11f1"} -{"@timestamp":"2026-06-25T01:22:11.166+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54070 - 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":"bf2a80a39b8c2cda","trace":"916a8db5a4968b3150bf99b4c1971db6"} -{"@timestamp":"2026-06-25T01:22:13.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54072 - 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.0ms","level":"info","span":"f8e15c9cc3ea64df","trace":"7a28c199f6ba249e35984b484e9cd1f6"} -{"@timestamp":"2026-06-25T01:22:13.335+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54074 - 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":"ff9dd4bb3066c519","trace":"7dae14727e167b7df58676c7c5a408fc"} -{"@timestamp":"2026-06-25T01:22:13.340+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54076 - 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":"e23107eaa9099e2a","trace":"4ca4f3c72e7e9abb8102a61de87955e8"} -{"@timestamp":"2026-06-25T01:22:13.346+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54078 - 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":"48b8133b71947ae2","trace":"6df2bdd7aada54cc9a66f55dc9bd7634"} -{"@timestamp":"2026-06-25T01:22:13.357+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54080 - 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":"43b688bf966025cb","trace":"8620e0c814bdcc03b48fecb6b6237115"} -{"@timestamp":"2026-06-25T01:22:13.593+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2011.8ms)","duration":"2011.8ms","level":"slow","span":"788160abccab48a3","trace":"ae7b4385be799e2f5907bd08f19a7ca4"} -{"@timestamp":"2026-06-25T01:22:13.593+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2011.8ms","level":"info","span":"788160abccab48a3","trace":"ae7b4385be799e2f5907bd08f19a7ca4"} -{"@timestamp":"2026-06-25T01:22:15.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54082 - 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":"7a78c345c5bb55c1","trace":"3c262a29a8c43a7e1a7505c53ee51b25"} -{"@timestamp":"2026-06-25T01:22:16.334+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54084 - 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":"0e9ff70dd1c2cb3d","trace":"d4079f7f10f56cf09520420d8bf54d67"} -{"@timestamp":"2026-06-25T01:22:16.339+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54086 - 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":"bb1f7143af447bc4","trace":"a28e996038b05772a23e8edfbb5e1dcd"} -{"@timestamp":"2026-06-25T01:22:16.343+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54088 - 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":"299391ed62a11760","trace":"edeb90b5af5631df9983f752989a20ac"} -{"@timestamp":"2026-06-25T01:22:16.347+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54090 - 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":"20da48b5e36e9298","trace":"57684022ae97925742f28d510882cad8"} -{"@timestamp":"2026-06-25T01:22:17.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54092 - 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":"01e9f8bb1a4bbf50","trace":"b0a6ecb74e726bf4c99fbf3ca0641d5c"} -{"@timestamp":"2026-06-25T01:22:18.634+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2037.5ms)","duration":"2037.5ms","level":"slow","span":"227ab12ae8bf8757","trace":"14b08e80fb3ac164b2cf0cee3f26e5e5"} -{"@timestamp":"2026-06-25T01:22:18.634+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2037.5ms","level":"info","span":"227ab12ae8bf8757","trace":"14b08e80fb3ac164b2cf0cee3f26e5e5"} -{"@timestamp":"2026-06-25T01:22:19.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54094 - 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":"96a8fe64b2e741d7","trace":"f4e292224c68676efcd4c14106fb3097"} -{"@timestamp":"2026-06-25T01:22:19.337+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54096 - 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":"e26dbd58a182a75c","trace":"a8bc9c5237bc91764fd4e7265261f75e"} -{"@timestamp":"2026-06-25T01:22:19.345+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54098 - 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":"64824b47b532c992","trace":"adb57b0af6a3dca2bcb44e6db933fd57"} -{"@timestamp":"2026-06-25T01:22:19.350+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54100 - 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":"7ca07ed9fe5142ef","trace":"a473281ffa93243bf4453c1024cbe9de"} -{"@timestamp":"2026-06-25T01:22:19.359+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54102 - 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":"46ff7b8db2d8bc0c","trace":"8c342141172fe71e72c12081e628bb8f"} -{"@timestamp":"2026-06-25T01:22:21.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54104 - 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":"853651198f727849","trace":"c607b91a7a92dd9c2b4869a22ba47b24"} -{"@timestamp":"2026-06-25T01:22:22.337+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54106 - 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":"d5baa06ef8533727","trace":"bc5063eeacc993a123ffa34559cb023b"} -{"@timestamp":"2026-06-25T01:22:22.348+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54108 - 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":"ffbbd05423afddd7","trace":"3c250bcf5577f53497d023d1ceb7865e"} -{"@timestamp":"2026-06-25T01:22:22.354+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54110 - 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":"76d82835fbbb789b","trace":"a12b3944aa99c0a853dc8eb4adcc9ff4"} -{"@timestamp":"2026-06-25T01:22:22.361+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54112 - 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":"e286403327fa3c83","trace":"8263dd135bafae0f22c1a29ddca45ebb"} -{"@timestamp":"2026-06-25T01:22:23.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54114 - 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.2ms","level":"info","span":"d1b58231e7686924","trace":"a57283b0f709a7b5520ccbe5b52a0821"} -{"@timestamp":"2026-06-25T01:22:23.651+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2013.9ms)","duration":"2013.9ms","level":"slow","span":"517859ba2c2486a3","trace":"0745b97aec329e0ab90f2272e62f1d28"} -{"@timestamp":"2026-06-25T01:22:23.651+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2013.9ms","level":"info","span":"517859ba2c2486a3","trace":"0745b97aec329e0ab90f2272e62f1d28"} -{"@timestamp":"2026-06-25T01:22:25.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54116 - 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":"6aaba283adaa320f","trace":"64e9aaad58b68363f3ff197dcf953f0d"} -{"@timestamp":"2026-06-25T01:22:25.336+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54118 - 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":"dcaa02dd046b70f3","trace":"608e20a9654f1fb895f6f3fa76302cf5"} -{"@timestamp":"2026-06-25T01:22:25.342+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54120 - 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":"29e0306b70862a1d","trace":"6b6a40366b6bfa75e105cb040f4f3241"} -{"@timestamp":"2026-06-25T01:22:25.347+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54122 - 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":"ea65bde917c3d192","trace":"88b7f9d253bc49bfc62a830c8dd66850"} -{"@timestamp":"2026-06-25T01:22:25.354+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54124 - 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":"3de932d531ef5377","trace":"ce1fd0c0de7386d29ce28690f7f738f2"} -{"@timestamp":"2026-06-25T01:22:27.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54126 - 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":"0707f544de6adaba","trace":"aef40051f5d1c1a3508d94681650d74e"} -{"@timestamp":"2026-06-25T01:22:28.336+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54128 - 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":"53e160113c986a0d","trace":"8e1d1b2473c03cd6932fd747fdcea891"} -{"@timestamp":"2026-06-25T01:22:28.344+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54130 - 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":"12125b3fc7dc9b26","trace":"9494a8d96060e90b71bdc2f559a21658"} -{"@timestamp":"2026-06-25T01:22:28.349+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54132 - 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":"801af9019c48fc88","trace":"2fad3bb938bbac5a36ba816d9d10d0fc"} -{"@timestamp":"2026-06-25T01:22:28.355+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54134 - 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":"93f35fc43987e7b6","trace":"248d462d86cac968b839b277c69b6a41"} -{"@timestamp":"2026-06-25T01:22:28.688+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2033.3ms)","duration":"2033.3ms","level":"slow","span":"d879319594d0800b","trace":"38071aa0c3158e6b1a8dd80cde45c541"} -{"@timestamp":"2026-06-25T01:22:28.689+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2033.3ms","level":"info","span":"d879319594d0800b","trace":"38071aa0c3158e6b1a8dd80cde45c541"} -{"@timestamp":"2026-06-25T01:22:29.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54136 - 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":"43970926c45f9412","trace":"d2fceeea14636bbd7e90b6e62b79be59"} -{"@timestamp":"2026-06-25T01:22:31.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54138 - 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":"2f6cc3950b5e24bf","trace":"103ae6ab14f5ec4d9c29e179885d830d"} -{"@timestamp":"2026-06-25T01:22:31.336+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54140 - 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":"b63497d7f0b0b9b5","trace":"417345e955b6902fd547ddcd6b6c8976"} -{"@timestamp":"2026-06-25T01:22:31.345+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54142 - 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":"d9732901704ab8e7","trace":"6ee10d6f1bdd438d254ef17f1a51640a"} -{"@timestamp":"2026-06-25T01:22:31.351+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54144 - 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":"3aefb17e054b2649","trace":"c6542df998143053c045d30cdd2197eb"} -{"@timestamp":"2026-06-25T01:22:31.356+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54146 - 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":"9304af5ea36e8fd2","trace":"18e565388dba1e06fe48d36834f00d25"} -{"@timestamp":"2026-06-25T01:22:33.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54148 - 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":"2f872de2bf01c4e0","trace":"14365eda39c5cb8f51fe6eec76548410"} -{"@timestamp":"2026-06-25T01:22:33.715+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2022.9ms)","duration":"2022.9ms","level":"slow","span":"ba472916907ce6de","trace":"478922fede092a88cb7284dd3bfa4221"} -{"@timestamp":"2026-06-25T01:22:33.715+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2022.9ms","level":"info","span":"ba472916907ce6de","trace":"478922fede092a88cb7284dd3bfa4221"} -{"@timestamp":"2026-06-25T01:22:34.336+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54150 - 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":"abeafde1e29b7ebf","trace":"3b00d5b37b7f39d84cdc5333eb8aacff"} -{"@timestamp":"2026-06-25T01:22:34.344+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54152 - 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":"d2c8ef38fc2891d3","trace":"03e53ce4de120bcfbf654ac8ec6edf1c"} -{"@timestamp":"2026-06-25T01:22:34.350+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54154 - 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":"f983e971d6c72ad6","trace":"bca81380904b8c4b5e130518279adecb"} -{"@timestamp":"2026-06-25T01:22:34.357+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54156 - 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":"bab5b062f28e4c9d","trace":"508930500e87392de55923ee2afff001"} -{"@timestamp":"2026-06-25T01:22:35.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54158 - 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":"bd19d2eae941b1a7","trace":"c66d6b691730108e4b8f793f61014a7f"} -{"@timestamp":"2026-06-25T01:22:37.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54160 - 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":"b186060a2c7b2227","trace":"bf6ad226498afd360d99cbe1c985fce0"} -{"@timestamp":"2026-06-25T01:22:37.334+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54162 - 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":"656e2523e9b3ea1d","trace":"6e46403acf600daf37459d5b8411a71c"} -{"@timestamp":"2026-06-25T01:22:37.339+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54164 - 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":"403de42e521f3a09","trace":"622195bb462b6bc3376f2a82e149ff32"} -{"@timestamp":"2026-06-25T01:22:37.344+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54166 - 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":"1859fcda3fb6bfca","trace":"f99b459e4060310e60acfaf307c13c0f"} -{"@timestamp":"2026-06-25T01:22:37.348+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54168 - 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":"a79e895a46bd18fb","trace":"1dc817facd6f1d65a14a2d79c78cd752"} -{"@timestamp":"2026-06-25T01:22:38.742+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.2ms)","duration":"2024.2ms","level":"slow","span":"6e41093d9f23fb84","trace":"74d3d8f4d8805876db2bc3cf9fed63a2"} -{"@timestamp":"2026-06-25T01:22:38.742+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.2ms","level":"info","span":"6e41093d9f23fb84","trace":"74d3d8f4d8805876db2bc3cf9fed63a2"} -{"@timestamp":"2026-06-25T01:22:39.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54170 - 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":"8ef08a561e0568f0","trace":"650fd2d1df8e65ec217e0307b2b997d7"} -{"@timestamp":"2026-06-25T01:22:40.339+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54172 - 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":"e228f99c2d1fe204","trace":"98e91022586248f0c01f99a5c2dba1a0"} -{"@timestamp":"2026-06-25T01:22:40.347+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54174 - 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":"36b05afb39bd0424","trace":"d0f03ecd00884b02d82741f226fd8af6"} -{"@timestamp":"2026-06-25T01:22:40.355+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54176 - 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":"81ff6cfeccec8fef","trace":"029bcbdf25c582821cf6234c6a6214eb"} -{"@timestamp":"2026-06-25T01:22:40.364+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54178 - 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":"a92561153191a543","trace":"6ffe106c6d9e8215ffa53d9f03e96694"} -{"@timestamp":"2026-06-25T01:22:41.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54181 - 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":"bd18908028ca77da","trace":"bcad1056fe390ae0bbfbb020acf4eb2b"} -{"@timestamp":"2026-06-25T01:22:43.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54183 - 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":"b69838c95462e996","trace":"7dc8f0d0a6e1df2f723b2277d3e9003c"} -{"@timestamp":"2026-06-25T01:22:43.337+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54185 - 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":"9c6dc405a5c03878","trace":"ef77a03647ec23d24565e961c52e9655"} -{"@timestamp":"2026-06-25T01:22:43.346+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54187 - 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":"69a7124b5365c2fc","trace":"f7b8b8028a5c2d9f56f3f13f8c2cfd94"} -{"@timestamp":"2026-06-25T01:22:43.353+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54189 - 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":"ddbdd08a9240f385","trace":"71bd82749da8783d99ea22f8150786de"} -{"@timestamp":"2026-06-25T01:22:43.362+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54191 - 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":"4f45c4f78f07021e","trace":"2949222c89e1da135867b2434041df06"} -{"@timestamp":"2026-06-25T01:22:43.771+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.9ms)","duration":"2025.9ms","level":"slow","span":"849e3d6c07122e93","trace":"46de8cab0b20e1a0ad8cb3117c11e4d8"} -{"@timestamp":"2026-06-25T01:22:43.771+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.9ms","level":"info","span":"849e3d6c07122e93","trace":"46de8cab0b20e1a0ad8cb3117c11e4d8"} -{"@timestamp":"2026-06-25T01:22:44.208+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54195 - 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":"d003087cc2ea9e25","trace":"30344a34e06d35554d9dd8426d2f8a01"} -{"@timestamp":"2026-06-25T01:22:44.210+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54196 - 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":"9beb8bcd1f9e02d7","trace":"519685c7be55b9ab28deef88a6191b2a"} -{"@timestamp":"2026-06-25T01:22:44.211+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54197 - 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":"c99332b892b4d9bb","trace":"ada097724ecb17157eb98cb7ac9a2b7b"} -{"@timestamp":"2026-06-25T01:22:44.214+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54201 - 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":"946d710ee9b06676","trace":"2a7379ce3c42c9e78bb48938984baf0c"} -{"@timestamp":"2026-06-25T01:22:44.216+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54200 - 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":"da74d2a690a8dea1","trace":"3ea75444b69e7b23f1e690927002b197"} -{"@timestamp":"2026-06-25T01:22:44.220+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54207 - 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":"55bc32f38057490a","trace":"d650a4a3866a5d88750ae128e3ed3092"} -{"@timestamp":"2026-06-25T01:22:44.221+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54208 - 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":"41a658160b9d8885","trace":"a634fcca8a76d764f3f4259f8132a4d1"} -{"@timestamp":"2026-06-25T01:22:44.221+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54206 - 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":"de6c5ff9ee4e0d6f","trace":"dd0cd52c0e5ffa8fcb5c434923527efb"} -{"@timestamp":"2026-06-25T01:22:44.226+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:54209 - 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":"8.0ms","level":"info","span":"a857276bd9ca82a2","trace":"a0c7782df3ca604812605ce86f016892"} -{"@timestamp":"2026-06-25T01:22:45.178+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54211 - 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.9ms","level":"info","span":"c1bfba84a4de9726","trace":"4b90be5ab9ab1f9141a3a59152b8cfe7"} -{"@timestamp":"2026-06-25T01:22:47.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54214 - 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":"934a14aa2f0008f6","trace":"cca58ea6ae947ba9c7b09c54a59dfbed"} -{"@timestamp":"2026-06-25T01:22:48.784+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2009.2ms)","duration":"2009.2ms","level":"slow","span":"5e731c54d1041792","trace":"3de5e809fa9a3b25d9234447dfbeb748"} -{"@timestamp":"2026-06-25T01:22:48.785+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2009.2ms","level":"info","span":"5e731c54d1041792","trace":"3de5e809fa9a3b25d9234447dfbeb748"} -{"@timestamp":"2026-06-25T01:22:49.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54216 - 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":"0b325319ee501c16","trace":"415a0a4a9adf6ff3f51e3e95f80d8f96"} -{"@timestamp":"2026-06-25T01:22:51.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54218 - 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":"20f65a99261a3ab9","trace":"99173c1b1b687aaca16df783051b78f3"} -{"@timestamp":"2026-06-25T01:22:52.990+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54227 - 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":"bb3bb956726852e7","trace":"fc9c50cb8fd821d784ab74bfd80985b5"} -{"@timestamp":"2026-06-25T01:22:52.991+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54226 - 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":"dd18aa1bdb62391c","trace":"ba32b9efa60cd60dbcaef14dc18cd9e3"} -{"@timestamp":"2026-06-25T01:22:52.992+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54224 - 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":"b9aeb66d31f5029d","trace":"753ec3016a56d6ecabf375e9cd1a7d6b"} -{"@timestamp":"2026-06-25T01:22:52.992+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54225 - 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":"920e82ca8d361bc0","trace":"cbec7e50cf72bdb4734fe886afaabf16"} -{"@timestamp":"2026-06-25T01:22:52.997+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54232 - 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":"bf691bcd61927a6b","trace":"c0bc71b7bbb58a11a4da355ba209967a"} -{"@timestamp":"2026-06-25T01:22:52.997+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54231 - 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":"7426e471ed6f844a","trace":"f7ccff98e54fdd0d08b8c123332acab4"} -{"@timestamp":"2026-06-25T01:22:52.997+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54234 - 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":"15b1d39f7e9cb262","trace":"467591730a0365283da0de89c45ed3c5"} -{"@timestamp":"2026-06-25T01:22:52.997+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54237 - 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":"3bbb4267cfe7b250","trace":"14514e36fe28d386c115f0fc5f5ed454"} -{"@timestamp":"2026-06-25T01:22:52.998+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54236 - 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":"7fa48680ca784a2c","trace":"94f9342becb57fff68b82390570997f8"} -{"@timestamp":"2026-06-25T01:22:53.005+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54240 - 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":"4ecbf2f8f618b209","trace":"6bf042e24f65d2e586284ae3e86bea95"} -{"@timestamp":"2026-06-25T01:22:53.006+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54242 - 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":"7f289bfd0f7922c4","trace":"e3b8302c80e2d1ca75c287eee2827228"} -{"@timestamp":"2026-06-25T01:22:53.007+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54243 - 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":"d0d35369c657158a","trace":"de95df810094304d3d55f7c714d8c27d"} -{"@timestamp":"2026-06-25T01:22:53.013+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54246 - 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":"8a9fe26f60a7856f","trace":"7828cc1e642795e1460aadad79c62a32"} -{"@timestamp":"2026-06-25T01:22:53.013+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54247 - 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":"d2e233ebd522a1a5","trace":"73dea37fa2e812c96c5fcaacf1473ba0"} -{"@timestamp":"2026-06-25T01:22:53.022+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54249 - 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":"e9ed1cc1dc86c0b3","trace":"fb8f3a2acb495a90e2cfd8d40c8cddd6"} -{"@timestamp":"2026-06-25T01:22:53.028+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54251 - 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":"f35bd25bf1d3a3e6","trace":"f785f26f06f51fbc4fae2f93d10b902a"} -{"@timestamp":"2026-06-25T01:22:53.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54253 - 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":"9ef0956ba8b47a17","trace":"ca14cf557cc6bc0190010a498d82170b"} -{"@timestamp":"2026-06-25T01:22:53.827+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2038.9ms)","duration":"2038.9ms","level":"slow","span":"3280f5ca5240b5b4","trace":"7ad9fec0e00c026928ce1d49c5e9f6e0"} -{"@timestamp":"2026-06-25T01:22:53.827+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2038.9ms","level":"info","span":"3280f5ca5240b5b4","trace":"7ad9fec0e00c026928ce1d49c5e9f6e0"} -{"@timestamp":"2026-06-25T01:22:54.662+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.5Mi, TotalAlloc=47.8Mi, Sys=23.6Mi, NumGC=28","level":"stat"} -{"@timestamp":"2026-06-25T01:22:54.720+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 159, pass: 159, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:22:55.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54255 - 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":"bdcb6d0ea07c1d07","trace":"65d07f824f4ea44a15ea510431322a33"} -{"@timestamp":"2026-06-25T01:22:55.250+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 2.7/s, drops: 0, avg time: 155.4ms, med: 2.7ms, 90th: 5.8ms, 99th: 2038.8ms, 99.9th: 2038.8ms","level":"stat"} -{"@timestamp":"2026-06-25T01:22:56.009+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54257 - 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":"ca1b50f33e46bdb9","trace":"f29f2d3a5d598391783480e487b7ee5a"} -{"@timestamp":"2026-06-25T01:22:56.016+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54259 - 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.8ms","level":"info","span":"64a146b522e76d20","trace":"a6a57c495a766759d2222b476772bbbb"} -{"@timestamp":"2026-06-25T01:22:56.021+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54261 - 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":"8fb444a81528cc82","trace":"fc01e16ca70a27d89c1f31e788f775b8"} -{"@timestamp":"2026-06-25T01:22:56.030+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54263 - 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":"8b3eccb516391aad","trace":"dafe03e02069404bad47fee05cacc1e0"} -{"@timestamp":"2026-06-25T01:22:56.410+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54271 - 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":"d1a6f086ffa1d602","trace":"f941347b6cdd14a309036c77b8b10849"} -{"@timestamp":"2026-06-25T01:22:56.410+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54270 - 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":"7bf8c07ef1e0357f","trace":"7c183f958b68a67a0b47bda597475902"} -{"@timestamp":"2026-06-25T01:22:56.410+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54268 - 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":"b01b93735220d4c2","trace":"29def497669a0a5f38b99cd75d6004cc"} -{"@timestamp":"2026-06-25T01:22:56.411+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54269 - 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":"2cb5c19a65b65d2e","trace":"3322d9b0879e3640799e6a8f7180b55e"} -{"@timestamp":"2026-06-25T01:22:56.415+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54273 - 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":"9a8fde0a523886b3","trace":"eef25dc6b2c6faaa1544d412b93a343e"} -{"@timestamp":"2026-06-25T01:22:56.416+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54276 - 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":"e1fcac310779d71c","trace":"1b9746b6d74d4cf11a6fde8756987f8d"} -{"@timestamp":"2026-06-25T01:22:56.417+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54277 - 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":"df344d2f490baf2a","trace":"5042d7f04e511706f555bf34d7478607"} -{"@timestamp":"2026-06-25T01:22:56.421+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54281 - 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":"f842c9a241923a34","trace":"2a8ff0a0872d983c5abd1c6c0230289d"} -{"@timestamp":"2026-06-25T01:22:56.422+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54282 - 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":"909269c2b61bc158","trace":"011df3e740138c1c8ff8515e3d0f1dde"} -{"@timestamp":"2026-06-25T01:22:56.422+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54283 - 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":"fa237f546d363ca8","trace":"dd2961ce1c7450e72319086b391397ae"} -{"@timestamp":"2026-06-25T01:22:56.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54287 - 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.8ms","level":"info","span":"a97da5193b5bdfd3","trace":"813e0e79cd7b55e75f9a1e3c4131953c"} -{"@timestamp":"2026-06-25T01:22:56.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54286 - 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":"476f1ea326702a1b","trace":"39a727d57b52c59ed32480a4d4927415"} -{"@timestamp":"2026-06-25T01:22:56.434+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54289 - 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":"97dffd0030d56104","trace":"2012e50c2e6dd43b948d975f6a9e77d0"} -{"@timestamp":"2026-06-25T01:22:56.443+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54291 - 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":"793ae944ae60898b","trace":"d15aed06953fb10c3a94d7292eaa4261"} -{"@timestamp":"2026-06-25T01:22:57.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54293 - 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":"d9c7f5b89693d728","trace":"eeb47537fd4c672b187a7fc0aba6cf66"} -{"@timestamp":"2026-06-25T01:22:58.834+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2004.6ms)","duration":"2004.6ms","level":"slow","span":"603efa9663006fff","trace":"93194c41d10527c2c1308804e60d7fa9"} -{"@timestamp":"2026-06-25T01:22:58.835+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2004.6ms","level":"info","span":"603efa9663006fff","trace":"93194c41d10527c2c1308804e60d7fa9"} -{"@timestamp":"2026-06-25T01:22:59.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54295 - 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.5ms","level":"info","span":"67c08475687f6a83","trace":"29184c397161ae293bf21c784c65f034"} -{"@timestamp":"2026-06-25T01:22:59.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54297 - 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":"2388379198c80fc6","trace":"c29e8a0a810c68106a8e5bdb468a5415"} -{"@timestamp":"2026-06-25T01:22:59.428+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54299 - 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":"b79bdf62867091d4","trace":"759cc6d28eb58b99d1b572025df82c0a"} -{"@timestamp":"2026-06-25T01:22:59.433+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54301 - 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":"e985a26b3d2a8325","trace":"4ce8afb7af1e3bba209efe5467298a25"} -{"@timestamp":"2026-06-25T01:22:59.440+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54303 - 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":"225c557faf1291f2","trace":"4de16ca2d9dba067e4467bdcec8a37dc"} -{"@timestamp":"2026-06-25T01:23:01.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54305 - 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":"c0933708a79d8469","trace":"34d6a5302223ef40d0b3efcbe5a8efe7"} -{"@timestamp":"2026-06-25T01:23:02.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54307 - 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":"f774f4b6baee2d68","trace":"147a71a39786bc7d2db0d1b8bdd29148"} -{"@timestamp":"2026-06-25T01:23:02.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54309 - 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":"afdbd5db61d40b41","trace":"80e68dbfc0ef6f23892041138f3c7219"} -{"@timestamp":"2026-06-25T01:23:02.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54311 - 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":"34cd42dfc9f6c235","trace":"54a7423ce7418028b558163d5c9f0731"} -{"@timestamp":"2026-06-25T01:23:02.443+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54313 - 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":"e78c40b2d29c39b3","trace":"2bd0fba1b01d7dc172e9378210e15178"} -{"@timestamp":"2026-06-25T01:23:03.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54315 - 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":"7bfbf9fe6754557d","trace":"4b2d7bcc74a1de1c030ed71fda3417ca"} -{"@timestamp":"2026-06-25T01:23:03.881+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2043.8ms)","duration":"2043.8ms","level":"slow","span":"d9d67b2bfa4a33b1","trace":"a02d375c44055e56370d6d1b60c0ba68"} -{"@timestamp":"2026-06-25T01:23:03.882+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2043.8ms","level":"info","span":"d9d67b2bfa4a33b1","trace":"a02d375c44055e56370d6d1b60c0ba68"} -{"@timestamp":"2026-06-25T01:23:05.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54317 - 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":"ccbaaf29b48bed93","trace":"34b2d3b8e9dbbb92b90a7ba681f8fd21"} -{"@timestamp":"2026-06-25T01:23:05.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54319 - 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":"bdbf57973ec85c22","trace":"51858d247a563df574face3b952b576f"} -{"@timestamp":"2026-06-25T01:23:05.428+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54321 - 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":"cafbc029433ad65a","trace":"ee90db4f35a98c92f2c20250d0cd0193"} -{"@timestamp":"2026-06-25T01:23:05.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54323 - 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":"f48f59cb3dfe1bab","trace":"fba84d67e6beab69373416dca4e062c4"} -{"@timestamp":"2026-06-25T01:23:05.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54325 - 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":"6a9ee44883b18046","trace":"302914b45e3b7826431b9388f6226bf0"} -{"@timestamp":"2026-06-25T01:23:07.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54327 - 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":"afcb452fd8bad1b2","trace":"65d2b91c5323b5d4dec1dc8e4ed1c0eb"} -{"@timestamp":"2026-06-25T01:23:08.421+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54329 - 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":"1dcade25ab8a1412","trace":"37858425a2eb4a024bda084d9e8344d5"} -{"@timestamp":"2026-06-25T01:23:08.426+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54331 - 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":"325a8fdf5f35f4e1","trace":"6634620865d10a79f0a97e730a1660ac"} -{"@timestamp":"2026-06-25T01:23:08.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54333 - 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":"ca8dfec8754dbba4","trace":"f6bf81184ddf35751029e3baccbdebe9"} -{"@timestamp":"2026-06-25T01:23:08.434+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54335 - 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":"a80ea857137b1233","trace":"0eb00af327f2634a7e5464542b0f6ab7"} -{"@timestamp":"2026-06-25T01:23:08.918+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2033.1ms)","duration":"2033.1ms","level":"slow","span":"365021250c7befe4","trace":"eddab4e75249bde8b41ceb2454ed5d03"} -{"@timestamp":"2026-06-25T01:23:08.918+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2033.1ms","level":"info","span":"365021250c7befe4","trace":"eddab4e75249bde8b41ceb2454ed5d03"} -{"@timestamp":"2026-06-25T01:23:09.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54337 - 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":"3ffbb5676c4b43a1","trace":"6017ef50b04ac6ecc42f9ab2ff4f5fed"} -{"@timestamp":"2026-06-25T01:23:11.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54339 - 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":"f2c88316310208d7","trace":"e336ca79a0b15e4460e5e7ae91d80cc6"} -{"@timestamp":"2026-06-25T01:23:11.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54341 - 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":"3536bd5deebf6e37","trace":"355a12f6bf4587f51ea0fdefbfc57e58"} -{"@timestamp":"2026-06-25T01:23:11.431+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54343 - 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.8ms","level":"info","span":"934b864bd8d3434c","trace":"23d1c2872531a24602fe3473fe1a827d"} -{"@timestamp":"2026-06-25T01:23:11.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54345 - 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":"b8a7324b889a1a55","trace":"7df5f263251df7ca6d406f5e8f781bab"} -{"@timestamp":"2026-06-25T01:23:11.443+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54347 - 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":"02c45d811f8b17cd","trace":"a010d04f0d5958ce4597fc9fb09e3507"} -{"@timestamp":"2026-06-25T01:23:13.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54349 - 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":"ab4ff2db4eff26d9","trace":"60f0f47996bab279a2f0d002ca472123"} -{"@timestamp":"2026-06-25T01:23:13.942+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2020.3ms)","duration":"2020.3ms","level":"slow","span":"c91c925c71688a2c","trace":"bc79ad16633d168d189eeb0860681a76"} -{"@timestamp":"2026-06-25T01:23:13.942+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2020.3ms","level":"info","span":"c91c925c71688a2c","trace":"bc79ad16633d168d189eeb0860681a76"} -{"@timestamp":"2026-06-25T01:23:14.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54351 - 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":"aee6b1769f1d11b2","trace":"88d876cff783845423aaf8dfa3580da0"} -{"@timestamp":"2026-06-25T01:23:14.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54353 - 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":"dd7bedd7ba2678f8","trace":"bd9dcb5b62f7330dda13f7192425da1e"} -{"@timestamp":"2026-06-25T01:23:14.435+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54355 - 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":"bfadfebc4f49c73c","trace":"274b878d0e8d1ad442102b9dfd350766"} -{"@timestamp":"2026-06-25T01:23:14.440+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54357 - 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":"0691d7a11c6efbf1","trace":"0b219115eeaccedff4b377cca1d2ecfc"} -{"@timestamp":"2026-06-25T01:23:15.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54359 - 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":"84eb9e71cee8f4ba","trace":"0e136c22713c987f8950ae14a26bd7ad"} -{"@timestamp":"2026-06-25T01:23:17.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54361 - 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":"dbe8cc11473fabee","trace":"fddd0cf4999fb104cc1d0b4fe7aa3d6f"} -{"@timestamp":"2026-06-25T01:23:17.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54363 - 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":"05dfc04d30b1d34f","trace":"5a48e848c0e054d726832c6fd46c3bea"} -{"@timestamp":"2026-06-25T01:23:17.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54365 - 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":"bf2027a2efc72d5f","trace":"43a9b2601bf3b99d550609174d4d6b2b"} -{"@timestamp":"2026-06-25T01:23:17.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54367 - 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":"9fcd54812f6440ff","trace":"214da655cb5c8aae42b378f1dad51f38"} -{"@timestamp":"2026-06-25T01:23:17.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54369 - 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":"fa1430aa08049296","trace":"06d8bbd02df45aa6787deecc2f2115fc"} -{"@timestamp":"2026-06-25T01:23:18.969+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2023.5ms)","duration":"2023.5ms","level":"slow","span":"098ce0b97360c2c1","trace":"437b21aad8ab67b3e04877034e1ebe16"} -{"@timestamp":"2026-06-25T01:23:18.969+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2023.5ms","level":"info","span":"098ce0b97360c2c1","trace":"437b21aad8ab67b3e04877034e1ebe16"} -{"@timestamp":"2026-06-25T01:23:19.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54371 - 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":"ad4858031890f6e0","trace":"0070c5e7900d5803be319a2f790c959f"} -{"@timestamp":"2026-06-25T01:23:20.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54373 - 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":"feb67af170c42ea6","trace":"7901b0075f8ee834c25a626edd58ba4f"} -{"@timestamp":"2026-06-25T01:23:20.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54375 - 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":"70873ef63d294fe5","trace":"fb552332da338e8d366140f8d4dd6e03"} -{"@timestamp":"2026-06-25T01:23:20.434+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54377 - 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":"f8d2640da4c8a58d","trace":"33976486ec32dcbde981c4ad4e547451"} -{"@timestamp":"2026-06-25T01:23:20.442+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54379 - 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":"d9feca3ef55a533d","trace":"d41d84514a9eb062eb5b8ded64ee413f"} -{"@timestamp":"2026-06-25T01:23:21.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54381 - 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":"aa6feaeca7233f28","trace":"5db5845d5cbfccd365c64ec4b22e0ccb"} -{"@timestamp":"2026-06-25T01:23:23.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54383 - 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.2ms","level":"info","span":"965d9f33c13a61bd","trace":"588b935da76bac03721b8d20a1faa0bd"} -{"@timestamp":"2026-06-25T01:23:23.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54385 - 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":"8c35b94d3f1e4ca4","trace":"e803af0f2c5786ff9a9d4118883b47e2"} -{"@timestamp":"2026-06-25T01:23:23.431+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54387 - 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.8ms","level":"info","span":"d7c8aea7f9168a3f","trace":"1ed0e1203c9d064ac10dc2f771ef7c4a"} -{"@timestamp":"2026-06-25T01:23:23.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54389 - 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":"5e310a5519bb6107","trace":"43f3ce64620ff5c0401331172b2025cc"} -{"@timestamp":"2026-06-25T01:23:23.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54391 - 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":"a930c14ed42f4ce2","trace":"a078204942bbf3fa778ef7f7cc66ebda"} -{"@timestamp":"2026-06-25T01:23:23.980+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2008.8ms)","duration":"2008.8ms","level":"slow","span":"e487a1c13847b459","trace":"5299c358223b317c9a8c2a8e3f45ae3a"} -{"@timestamp":"2026-06-25T01:23:23.980+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2008.8ms","level":"info","span":"e487a1c13847b459","trace":"5299c358223b317c9a8c2a8e3f45ae3a"} -{"@timestamp":"2026-06-25T01:23:25.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54393 - 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.2ms","level":"info","span":"a97dfa308c7836aa","trace":"75c54a7b6f1e03fd456233f74374772c"} -{"@timestamp":"2026-06-25T01:23:26.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54395 - 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":"8129a112d067761a","trace":"7149b091065a411addaa3154d6c4530d"} -{"@timestamp":"2026-06-25T01:23:26.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54397 - 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":"3cb2998698bb487c","trace":"006a3f9a41826d751f6d80753b824b15"} -{"@timestamp":"2026-06-25T01:23:26.433+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54399 - 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":"b947d7dc7189acd8","trace":"88dd0e15914ea4918a2b63aedc948146"} -{"@timestamp":"2026-06-25T01:23:26.438+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54401 - 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":"c77864abea67357a","trace":"a484714e1c0dc6c28ecb03f4fcc8b336"} -{"@timestamp":"2026-06-25T01:23:27.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54403 - 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":"8d5f7b6160116370","trace":"4ecc887b7f1dd864d7d1f5e463a533fc"} -{"@timestamp":"2026-06-25T01:23:28.993+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2009.0ms)","duration":"2009.0ms","level":"slow","span":"9b2dd83115b4f73d","trace":"c9cfb245a4a849384962951644e3abfa"} -{"@timestamp":"2026-06-25T01:23:28.993+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2009.0ms","level":"info","span":"9b2dd83115b4f73d","trace":"c9cfb245a4a849384962951644e3abfa"} -{"@timestamp":"2026-06-25T01:23:29.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54405 - 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":"362925bb45986c17","trace":"4d9839244a6696b96de11b184537de72"} -{"@timestamp":"2026-06-25T01:23:29.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54407 - 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":"144796a928d6abd2","trace":"01b80769c08a755bb8f83702df82a54b"} -{"@timestamp":"2026-06-25T01:23:29.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54409 - 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":"036f0f2ae0cf1a8e","trace":"123b0787d569a5c6bce62a1bfe589245"} -{"@timestamp":"2026-06-25T01:23:29.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54411 - 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":"89e74f7fac070245","trace":"7a70dffcdf14c11d0b2994942d5fb880"} -{"@timestamp":"2026-06-25T01:23:29.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54413 - 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":"e5fab4fcab55bf86","trace":"dc0c1e0be1ed0e842764d9f395198d99"} -{"@timestamp":"2026-06-25T01:23:31.179+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54415 - 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.2ms","level":"info","span":"ed5d0efeca774420","trace":"0bab9028fd0859acd75902e933ce191a"} -{"@timestamp":"2026-06-25T01:23:32.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54417 - 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":"7d6830897834b1a2","trace":"dce8c247bb2f795bf490d95e26c6d900"} -{"@timestamp":"2026-06-25T01:23:32.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54419 - 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":"9ed8d9470d3151b4","trace":"c68e2e4350ceff4cf176d1c6b577806d"} -{"@timestamp":"2026-06-25T01:23:32.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54421 - 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":"26874886924a249c","trace":"a6b3176a69d26f46a349040a4db5255b"} -{"@timestamp":"2026-06-25T01:23:32.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54423 - 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":"7bd555fe6b2d9e36","trace":"94c4289f825f598ab8187729b98c95f8"} -{"@timestamp":"2026-06-25T01:23:33.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54425 - 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":"81c55067bb051aed","trace":"55ff13b91913826d35267705887a8cda"} -{"@timestamp":"2026-06-25T01:23:34.035+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2039.0ms)","duration":"2039.0ms","level":"slow","span":"cb7ea0fb7bde19a8","trace":"63c3dff8d3b6e063fe32798df3de84b8"} -{"@timestamp":"2026-06-25T01:23:34.035+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2039.0ms","level":"info","span":"cb7ea0fb7bde19a8","trace":"63c3dff8d3b6e063fe32798df3de84b8"} -{"@timestamp":"2026-06-25T01:23:35.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54428 - 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.2ms","level":"info","span":"85a475fa2bc4f015","trace":"abacde613291421f5f739aa946607323"} -{"@timestamp":"2026-06-25T01:23:35.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54430 - 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":"ce5478490808c01c","trace":"140dd9f5d38b442e8e906447f110443c"} -{"@timestamp":"2026-06-25T01:23:35.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54432 - 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":"366a437f8663ae0c","trace":"a7e443fa4b0fcf861ad161a4fd0a789c"} -{"@timestamp":"2026-06-25T01:23:35.438+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54434 - 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":"823e7feef9dd30c0","trace":"d69ea42319640335eab61052474833ee"} -{"@timestamp":"2026-06-25T01:23:35.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54436 - 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":"5421fb71741f16f9","trace":"37947a9198ef2a0ddc0c4451a6e25696"} -{"@timestamp":"2026-06-25T01:23:37.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54438 - 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":"0cc3119996a9e5b3","trace":"5121ece4ea79009a704995254785f5c8"} -{"@timestamp":"2026-06-25T01:23:38.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54440 - 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":"596d91c13f8f0aa6","trace":"8d9fdb29794be415d0a6360b1d472821"} -{"@timestamp":"2026-06-25T01:23:38.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54442 - 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":"bd978811e672023b","trace":"11014eca3a4c1eca3d8a9d63aec4f07d"} -{"@timestamp":"2026-06-25T01:23:38.438+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54444 - 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":"f5196352a2e86453","trace":"dad6eb07a685c03affd261a0ed606a8c"} -{"@timestamp":"2026-06-25T01:23:38.448+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54446 - 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":"d6494ddcb8a4d254","trace":"ad5ee387d2ef74083e38623125fa5579"} -{"@timestamp":"2026-06-25T01:23:39.043+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2005.3ms)","duration":"2005.3ms","level":"slow","span":"1375f3cda18adaf5","trace":"57c429f1dcd4205672916787d13db774"} -{"@timestamp":"2026-06-25T01:23:39.043+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2005.3ms","level":"info","span":"1375f3cda18adaf5","trace":"57c429f1dcd4205672916787d13db774"} -{"@timestamp":"2026-06-25T01:23:39.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54448 - 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":"17cf10538e5fc7fa","trace":"3ae7462dc0a2471dd236857a17a04167"} -{"@timestamp":"2026-06-25T01:23:41.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54450 - 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":"7e21c201ef751909","trace":"d07783df7b43a40a52fe322aaa114008"} -{"@timestamp":"2026-06-25T01:23:41.427+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54452 - 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":"6caf8cc95bee6c81","trace":"a03b3b308198dc5fa9180e81da2aa94d"} -{"@timestamp":"2026-06-25T01:23:41.433+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54454 - 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":"276639f28404f409","trace":"270abfd772555e85e00e4493fb0bae45"} -{"@timestamp":"2026-06-25T01:23:41.439+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54456 - 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":"f64943dc855e938f","trace":"2cef07908d67ea707f2669f63c21912b"} -{"@timestamp":"2026-06-25T01:23:41.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54458 - 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":"614e83112fb959dd","trace":"13a55a77c37af170a3d60ea13ec004d4"} -{"@timestamp":"2026-06-25T01:23:43.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54460 - 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.9ms","level":"info","span":"af54d2e2dd57dbb5","trace":"3406cd15ed4f85285630b9e8c1f28d04"} -{"@timestamp":"2026-06-25T01:23:44.089+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2044.3ms)","duration":"2044.3ms","level":"slow","span":"858ddecfe0a5f99e","trace":"f6cb16b9bab2d8d098b16f9592935562"} -{"@timestamp":"2026-06-25T01:23:44.090+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2044.3ms","level":"info","span":"858ddecfe0a5f99e","trace":"f6cb16b9bab2d8d098b16f9592935562"} -{"@timestamp":"2026-06-25T01:23:44.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54462 - 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":"c62018da1a777c5c","trace":"939f33c82584ec952268636603b42fbb"} -{"@timestamp":"2026-06-25T01:23:44.431+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54464 - 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":"ea1ee73ecf50f286","trace":"b1a13884166bd2287dd627f5ae889bac"} -{"@timestamp":"2026-06-25T01:23:44.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54466 - 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":"b57cd3a954b7c75b","trace":"d5128ab6791688b7495c5324b45eb87e"} -{"@timestamp":"2026-06-25T01:23:44.442+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54468 - 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":"9fcae8f51269d7fc","trace":"f3574feac9b87a85bf2e131bcc8617ba"} -{"@timestamp":"2026-06-25T01:23:45.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54470 - 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":"e58d5b7c4a2d336d","trace":"d0016a039286cdae2b0a9674319b9751"} -{"@timestamp":"2026-06-25T01:23:47.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54472 - 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":"aa1729514aee70da","trace":"0bf573b4094ea6ced14378ab2306a345"} -{"@timestamp":"2026-06-25T01:23:47.424+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54474 - 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":"551c508ee79af5d6","trace":"ac4fd6c08123df4752aa982f68500ca8"} -{"@timestamp":"2026-06-25T01:23:47.431+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54476 - 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":"d0b09f7ac1f576d4","trace":"8893a968f09971fee9e0ddce216b367b"} -{"@timestamp":"2026-06-25T01:23:47.436+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54478 - 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":"afb6dd25d446e6fc","trace":"091fbaa171676904664aa5c980a62fbb"} -{"@timestamp":"2026-06-25T01:23:47.445+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54480 - 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.1ms","level":"info","span":"34c119a082c21556","trace":"13683968f348c9b3dd67bd5d43fea13f"} -{"@timestamp":"2026-06-25T01:23:49.120+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.8ms)","duration":"2025.8ms","level":"slow","span":"bdf40b05a2ba1f05","trace":"03b27981e6f61766a3e16adf87cc05fd"} -{"@timestamp":"2026-06-25T01:23:49.120+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.8ms","level":"info","span":"bdf40b05a2ba1f05","trace":"03b27981e6f61766a3e16adf87cc05fd"} -{"@timestamp":"2026-06-25T01:23:49.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54482 - 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":"02c3fd8923350c8b","trace":"03575b12d0ef38cc46147a71636a3bc2"} -{"@timestamp":"2026-06-25T01:23:50.426+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54484 - 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":"5cccab70a879846b","trace":"bb3838c2281b4d2bf6687054f032d626"} -{"@timestamp":"2026-06-25T01:23:50.434+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54486 - 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":"5080a0f71b8c88b3","trace":"3dc45d2b2e968845c408ed67ffcf386d"} -{"@timestamp":"2026-06-25T01:23:50.439+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54488 - 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":"9761a52e5a96ed68","trace":"a5a846d70f5bfeebc161779ba3e339e0"} -{"@timestamp":"2026-06-25T01:23:50.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54490 - 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":"f181d61363844968","trace":"c52ec9619f6435b6de3fe4537be4da9c"} -{"@timestamp":"2026-06-25T01:23:51.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54492 - 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":"9512858860ff4002","trace":"be6ae5f0d804680f7394c6bce69bde63"} -{"@timestamp":"2026-06-25T01:23:53.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54494 - 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":"e78e28f45ec40ae3","trace":"f9c99e0fe66183dc5d528749f32e9ac5"} -{"@timestamp":"2026-06-25T01:23:53.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54496 - 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":"f072faeb5179caf2","trace":"7416a8bb52db0fcb4b53c1cc96d83e95"} -{"@timestamp":"2026-06-25T01:23:53.429+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54498 - 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":"22167d99e338d9aa","trace":"0e5d29f6f6b9ad42680f56bd2d2ab2bf"} -{"@timestamp":"2026-06-25T01:23:53.435+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54500 - 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":"c1733ee749fcfed6","trace":"00c3860e585fc8079dbfdce8ee8918ac"} -{"@timestamp":"2026-06-25T01:23:53.441+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54502 - 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":"09a3d2cb37d23c8e","trace":"b847bde09fec845293afc95d6022ef99"} -{"@timestamp":"2026-06-25T01:23:54.125+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2003.5ms)","duration":"2003.5ms","level":"slow","span":"2335b77ae80499dc","trace":"bc4d2af52361a134e0c9cb9bea6b765f"} -{"@timestamp":"2026-06-25T01:23:54.125+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2003.5ms","level":"info","span":"2335b77ae80499dc","trace":"bc4d2af52361a134e0c9cb9bea6b765f"} -{"@timestamp":"2026-06-25T01:23:54.662+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.9Mi, TotalAlloc=62.1Mi, Sys=23.6Mi, NumGC=34","level":"stat"} -{"@timestamp":"2026-06-25T01:23:54.721+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 136, pass: 136, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:23:55.168+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54504 - 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":"66a3a945aa34a06f","trace":"74b0af6d4bfd72076eba67f2345f1d58"} -{"@timestamp":"2026-06-25T01:23:55.250+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 2.3/s, drops: 0, avg time: 181.1ms, med: 2.9ms, 90th: 10.0ms, 99th: 2044.2ms, 99.9th: 2044.2ms","level":"stat"} -{"@timestamp":"2026-06-25T01:23:56.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54506 - 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":"72b619fbb4e29571","trace":"38e7015f8a2bb48ba1df5fb1ffc5aa64"} -{"@timestamp":"2026-06-25T01:23:56.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54508 - 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":"74a56150617da19e","trace":"61c7fcf7003ca9e5c8880cf85f57e346"} -{"@timestamp":"2026-06-25T01:23:56.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54510 - 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":"8237ff585fc0af83","trace":"5d734d6118470ae67f6edae3c24f6027"} -{"@timestamp":"2026-06-25T01:23:56.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54512 - 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.2ms","level":"info","span":"895ced0326df102c","trace":"9986df3d1d120d9f5099dd113f014922"} -{"@timestamp":"2026-06-25T01:23:57.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54514 - 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":"042f8fa258ab6880","trace":"f5e585e8239f5828b4869dfa431ec3fd"} -{"@timestamp":"2026-06-25T01:23:59.166+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2038.9ms)","duration":"2038.9ms","level":"slow","span":"2d583ef2f9ff66b1","trace":"4a67697b0c6724c39f9ae618c6052e9c"} -{"@timestamp":"2026-06-25T01:23:59.166+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2038.9ms","level":"info","span":"2d583ef2f9ff66b1","trace":"4a67697b0c6724c39f9ae618c6052e9c"} -{"@timestamp":"2026-06-25T01:23:59.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54516 - 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":"df8dc52f197b75b5","trace":"2e0cecfb8032482686f1ed3a880ce8ff"} -{"@timestamp":"2026-06-25T01:23:59.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54518 - 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":"5c72779e2a1e9538","trace":"e08bb7ff844330d9cad25823086ee7da"} -{"@timestamp":"2026-06-25T01:23:59.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54520 - 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":"98348af4ae9ea888","trace":"7f702c1e124f71a5dc8b93166cbc14be"} -{"@timestamp":"2026-06-25T01:23:59.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54522 - 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":"471f3dd66bcf00ee","trace":"a61544d41b33a7b17e8deb93514ca6ef"} -{"@timestamp":"2026-06-25T01:23:59.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54524 - 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":"304ef08e775f883f","trace":"323b8efc60900af11349f8811200d888"} -{"@timestamp":"2026-06-25T01:24:01.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54526 - 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.2ms","level":"info","span":"319ecdf4e1c12479","trace":"828c6b0773c2692692dbd338b6af2a03"} -{"@timestamp":"2026-06-25T01:24:02.426+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54528 - 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":"fe8d7c8b50754ea7","trace":"916a2c7627af1c91ed6a900484840f67"} -{"@timestamp":"2026-06-25T01:24:02.434+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54530 - 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":"6237dd6e9087a10a","trace":"0c18692cdfcd4ceabd2e5820472f924f"} -{"@timestamp":"2026-06-25T01:24:02.440+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54532 - 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":"c40f0bbb25005ec9","trace":"18682c6dba07c1ae2da29b1fb60b72e7"} -{"@timestamp":"2026-06-25T01:24:02.448+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54534 - 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":"0bc9a06926ccc5fa","trace":"98d1dea34d1897be791cb575a3d10808"} -{"@timestamp":"2026-06-25T01:24:03.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54536 - 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":"ad0c3a2aa3a7dbdd","trace":"3043888bc0a486c9c102f279c19e9a45"} -{"@timestamp":"2026-06-25T01:24:04.201+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2033.0ms)","duration":"2033.0ms","level":"slow","span":"9827cb187a95b4d8","trace":"2e5501b913ea7cad6f2d76c27f4ca507"} -{"@timestamp":"2026-06-25T01:24:04.201+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2033.0ms","level":"info","span":"9827cb187a95b4d8","trace":"2e5501b913ea7cad6f2d76c27f4ca507"} -{"@timestamp":"2026-06-25T01:24:05.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54538 - 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":"920e0523ae121ef4","trace":"8870dc2c7e71c7e4a2d354d0bf156928"} -{"@timestamp":"2026-06-25T01:24:05.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54540 - 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":"57d3e4552c9d7527","trace":"3cfef9646b01a2fcd268c36cbb1bb8fb"} -{"@timestamp":"2026-06-25T01:24:05.432+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54542 - 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.8ms","level":"info","span":"731a47e46792e2a6","trace":"63c2cf45cce5f4aba58ea5d650418a17"} -{"@timestamp":"2026-06-25T01:24:05.437+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54544 - 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":"78924477a6ecdcc8","trace":"41fcc518b09cae33f4784a690db1f31c"} -{"@timestamp":"2026-06-25T01:24:05.443+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54546 - 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":"b0e4c599739c78fe","trace":"847ecdfa899443c9e18c7908b1fa63c0"} -{"@timestamp":"2026-06-25T01:24:06.482+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c12214a12013022054a39 - 127.0.0.1:54552 - 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":"9be5816aa650bdf1","trace":"a1d54afe26857f0c36e16937e62da4da"} -{"@timestamp":"2026-06-25T01:24:06.483+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54551 - 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":"afcb32cc3024f088","trace":"6ec1d1e2f5026f4b14efb04898121b0d"} -{"@timestamp":"2026-06-25T01:24:06.482+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54550 - 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":"37422a368b095ada","trace":"160b5507e51551711a84258b9dc557cb"} -{"@timestamp":"2026-06-25T01:24:06.487+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54554 - 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":"f0c2c28ec0172f0f","trace":"14349f2f4e3424361e1b559d9584dc97"} -{"@timestamp":"2026-06-25T01:24:06.493+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54559 - 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":"87dd74fcba7016a2","trace":"7354519f16743514f9f359e262a44e72"} -{"@timestamp":"2026-06-25T01:24:06.493+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54560 - 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":"70a5d9d0a8d7488a","trace":"cf1f69268d979c4a23f29c8adf83e5de"} -{"@timestamp":"2026-06-25T01:24:06.494+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54557 - 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.5ms","level":"info","span":"245423c486ae8d8f","trace":"ce22e7fdf17795e07c17e2a1f34db844"} -{"@timestamp":"2026-06-25T01:24:06.496+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54562 - 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":"bb5f9d5a37aadc48","trace":"bb96e268f905d06c5d77b6ee079c29fa"} -{"@timestamp":"2026-06-25T01:24:06.501+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54568 - 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":"dc969204d07fbfc5","trace":"80b47fe6a3005070a34f0a951f343719"} -{"@timestamp":"2026-06-25T01:24:06.501+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54566 - 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":"247d0e43c7f664a3","trace":"e1444f69a829664f4db7b1c2179a0eb0"} -{"@timestamp":"2026-06-25T01:24:06.501+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54565 - 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":"3dfe02fe964843a7","trace":"5f8dde411483e19e0562116bdb7906b1"} -{"@timestamp":"2026-06-25T01:24:06.505+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54571 - 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":"41f972d6c81fe2cd","trace":"0de39f2c3726ef56d485db8912397457"} -{"@timestamp":"2026-06-25T01:24:06.506+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54572 - 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":"319e58fc82e214bb","trace":"a2ef5185abf91f11b79c0819db17fd7f"} -{"@timestamp":"2026-06-25T01:24:06.513+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54574 - 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":"4cb044667c184200","trace":"95eae32bfa2ec3948ebdc685b20b35e7"} -{"@timestamp":"2026-06-25T01:24:07.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54576 - 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":"e3c2e3f04c20b54e","trace":"189639818a3e6ddf3458ca0499fda1f1"} -{"@timestamp":"2026-06-25T01:24:09.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54578 - 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.3ms","level":"info","span":"a94214afcfabe964","trace":"b72bb9cc60f462f36bd38090ac3b8fed"} -{"@timestamp":"2026-06-25T01:24:09.243+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2037.8ms)","duration":"2037.8ms","level":"slow","span":"0f37551e6bc05abe","trace":"445ccde37f4a31ee10c8f7379b912837"} -{"@timestamp":"2026-06-25T01:24:09.243+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2037.8ms","level":"info","span":"0f37551e6bc05abe","trace":"445ccde37f4a31ee10c8f7379b912837"} -{"@timestamp":"2026-06-25T01:24:09.270+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54585 - 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":"dd5086312e2381de","trace":"4b0e2d4e8711ea7acec5f0ccd141680d"} -{"@timestamp":"2026-06-25T01:24:09.271+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54583 - 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":"c7375d05feca89f9","trace":"ce7522c7ca823c278994d84be161f261"} -{"@timestamp":"2026-06-25T01:24:09.272+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54586 - 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":"586adebdf3f93d9c","trace":"69912728f29cd3339baeb8c7906eab03"} -{"@timestamp":"2026-06-25T01:24:09.273+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54584 - 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":"1ef40c2dc85f9c2f","trace":"1336f0d31cb4efd7b273a6f4039eb046"} -{"@timestamp":"2026-06-25T01:24:09.276+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54588 - 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":"e1d9080702340358","trace":"28110a2127498a99b83e093e706752d3"} -{"@timestamp":"2026-06-25T01:24:09.279+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54591 - 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":"a29c2239f9125bd3","trace":"37c741a35b5718c8f6c0ca824a609bde"} -{"@timestamp":"2026-06-25T01:24:09.279+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54592 - 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":"bd0f3d948adabe9b","trace":"9594c2c611f22a2c59dcf00c78d6d362"} -{"@timestamp":"2026-06-25T01:24:09.282+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54595 - 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":"e9c125802139fe07","trace":"66de49648fe0e17cc4582376496a9c93"} -{"@timestamp":"2026-06-25T01:24:09.284+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54596 - 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":"c054866ccaa3ac28","trace":"b67ebb4b1bae442c721956fad67facd9"} -{"@timestamp":"2026-06-25T01:24:09.288+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54598 - 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":"79d5bf5fa22e982f","trace":"01211d08d8f2ce78ccd745c388d243d4"} -{"@timestamp":"2026-06-25T01:24:11.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54600 - 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":"97a0d41f8765feb7","trace":"cc291820e1809bb8a0a7383a76c319a7"} -{"@timestamp":"2026-06-25T01:24:13.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54602 - 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":"4253b98c1d3303de","trace":"5193852dc118598e869d99a1ca95bf3b"} -{"@timestamp":"2026-06-25T01:24:14.269+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2023.7ms)","duration":"2023.7ms","level":"slow","span":"f6e6c4da96337c2e","trace":"2a94d5d2fcfc963e6c2e484439cb50b2"} -{"@timestamp":"2026-06-25T01:24:14.269+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2023.7ms","level":"info","span":"f6e6c4da96337c2e","trace":"2a94d5d2fcfc963e6c2e484439cb50b2"} -{"@timestamp":"2026-06-25T01:24:15.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54604 - 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.1ms","level":"info","span":"aaa74b24758feb59","trace":"0a0fbe31a845e7ed6a120a811797ec13"} -{"@timestamp":"2026-06-25T01:24:17.172+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54606 - 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":"b4c7396ca4f25e92","trace":"0a4dbff36171d6d57402e9e81033b2cb"} -{"@timestamp":"2026-06-25T01:24:19.171+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54608 - 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":"dee101bc840d845c","trace":"83e4a4112dfd361c08787439b3bd8d46"} -{"@timestamp":"2026-06-25T01:24:19.301+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2029.2ms)","duration":"2029.2ms","level":"slow","span":"b47a37442713064c","trace":"d8f28fe9c7095774f3a5075efcaf4808"} -{"@timestamp":"2026-06-25T01:24:19.301+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2029.2ms","level":"info","span":"b47a37442713064c","trace":"d8f28fe9c7095774f3a5075efcaf4808"} -{"@timestamp":"2026-06-25T01:24:21.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54610 - 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":"e321967717b88cab","trace":"aa48079395c919bf5a7bcb66b05c254d"} -{"@timestamp":"2026-06-25T01:24:23.057+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:54618 - 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":"abca418c1d31af9a","trace":"baa63418c7e8bfc68caa269a6dbd0a50"} -{"@timestamp":"2026-06-25T01:24:23.059+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/members/me - 127.0.0.1:54619 - 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":"185349dcafaef97a","trace":"098429a7d2949e4cb873f6c9edde13d2"} -{"@timestamp":"2026-06-25T01:24:23.069+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/ai-settings - 127.0.0.1:54626 - 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":"445cc448c878c66e","trace":"6ff2aaed4809974f1ec26c4f23d3864b"} -{"@timestamp":"2026-06-25T01:24:23.069+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54627 - 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":"3e1ca507f8df69cb","trace":"5af233e92ec4622302ea53718efbfef2"} -{"@timestamp":"2026-06-25T01:24:23.069+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54622 - 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.8ms","level":"info","span":"64f948b9b4771061","trace":"c73a49ae102452122893dfa5c9b76401"} -{"@timestamp":"2026-06-25T01:24:23.070+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54620 - 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":"29db748dc30e4e3c","trace":"0a94f7d305765e6c1e47f7be326c6afb"} -{"@timestamp":"2026-06-25T01:24:23.071+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54625 - 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":"9d6004d0fbf76e7b","trace":"700ecc2e239b5de1b521445a9d52cfb5"} -{"@timestamp":"2026-06-25T01:24:23.071+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54624 - 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":"44a32714d3a62ee9","trace":"b52ab7d022e6990b6bc018c04adefd93"} -{"@timestamp":"2026-06-25T01:24:23.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:54631 - 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":"a11199a446477529","trace":"7d1a9f9ac67fc76019359790506c4b75"} -{"@timestamp":"2026-06-25T01:24:23.086+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54638 - 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":"5ffd2ee243ffaca1","trace":"a2cb42e61f2121188a5a8770d34908cd"} -{"@timestamp":"2026-06-25T01:24:23.087+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54637 - 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":"c1f5b2e53660c0cc","trace":"706d4d8d8ff95cc0a329185d9f17f017"} -{"@timestamp":"2026-06-25T01:24:23.087+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/ai-settings - 127.0.0.1:54636 - 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":"0c55a0224d4334f2","trace":"1a85ce61d66dad184ad9207a31423ecf"} -{"@timestamp":"2026-06-25T01:24:23.088+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54640 - 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":"17a6f01d60088935","trace":"4712ea61685305732f8896371f1b36d3"} -{"@timestamp":"2026-06-25T01:24:23.088+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54639 - 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.5ms","level":"info","span":"eb6ac9958b37684b","trace":"47004ebe23603bdfc7708563ab1ad5f7"} -{"@timestamp":"2026-06-25T01:24:23.088+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54641 - 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.1ms","level":"info","span":"e3cc7d19b73aacac","trace":"750f2476868bba6e4349fc9a065a4910"} -{"@timestamp":"2026-06-25T01:24:23.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54648 - 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":"85657baa724e93a8","trace":"e6d8ce8053141c25839dee2d00a00ecb"} -{"@timestamp":"2026-06-25T01:24:23.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54651 - 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":"38287f8434080414","trace":"6a0d12033e77e7399a8102dd84f4f923"} -{"@timestamp":"2026-06-25T01:24:23.103+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54650 - 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":"877b9eb12a0606b8","trace":"6a51657b91987ece7fb4cdf9147e8b63"} -{"@timestamp":"2026-06-25T01:24:23.103+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts - 127.0.0.1:54649 - 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":"f0f3f1b6e3f07c3e","trace":"75af5d7228733e7d05edae54c99ac010"} -{"@timestamp":"2026-06-25T01:24:23.106+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54653 - 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":"d2b12ad05a8c4673","trace":"9ecdc4ee7b707b1b78880787c3ee40f9"} -{"@timestamp":"2026-06-25T01:24:23.106+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54652 - 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":"b3e5bd6c227f825d","trace":"f6b638140a42e3c68b87e722f7907bd2"} -{"@timestamp":"2026-06-25T01:24:23.107+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54655 - 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":"9c1ac1df615470dd","trace":"92973684ea4c50bdb864d5bc2f34f5a5"} -{"@timestamp":"2026-06-25T01:24:23.109+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54657 - 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":"1f80bba7266a1b74","trace":"9a722cc25f48187f1e52fadff0abb4bd"} -{"@timestamp":"2026-06-25T01:24:23.110+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54659 - 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":"70aacb1bdb82ec0b","trace":"50d1a34468ad06b8ac0f82427a7c4185"} -{"@timestamp":"2026-06-25T01:24:23.114+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54663 - 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":"1e128ab4be373eb1","trace":"3eecc149f47014eda66b588a333c3bb4"} -{"@timestamp":"2026-06-25T01:24:23.118+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54665 - 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":"d2dec4044979288e","trace":"2781310d9863af6373ef87405afa583b"} -{"@timestamp":"2026-06-25T01:24:23.118+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54664 - 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":"d3c5e3d906c573f7","trace":"cd4630b556dee6f344032294b2650155"} -{"@timestamp":"2026-06-25T01:24:23.126+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54667 - 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":"e4ec3542a11e9939","trace":"c823959a02a8456683e4cf166b357a5f"} -{"@timestamp":"2026-06-25T01:24:24.306+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2002.4ms)","duration":"2002.4ms","level":"slow","span":"b9c824b3fd28e648","trace":"60247fa1654dbe66890d3e2ce1f50536"} -{"@timestamp":"2026-06-25T01:24:24.306+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2002.4ms","level":"info","span":"b9c824b3fd28e648","trace":"60247fa1654dbe66890d3e2ce1f50536"} -{"@timestamp":"2026-06-25T01:24:25.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54671 - 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.3ms","level":"info","span":"eebf69989a497920","trace":"0a7cf4caf50c0cffb8799c5e91935301"} -{"@timestamp":"2026-06-25T01:24:27.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54672 - 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.2ms","level":"info","span":"c0fc7b4e7e2f4b99","trace":"d8100c5f203adb8ffef7341efd3701c1"} -{"@timestamp":"2026-06-25T01:24:29.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54673 - 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.2ms","level":"info","span":"023524051fdb8e1c","trace":"16b5fabf163bc646db082d6368e8a79c"} -{"@timestamp":"2026-06-25T01:24:29.356+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2045.7ms)","duration":"2045.7ms","level":"slow","span":"4290c0bd112a7c2b","trace":"e28ba4219cb348a0ed826d686eb18c68"} -{"@timestamp":"2026-06-25T01:24:29.356+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2045.7ms","level":"info","span":"4290c0bd112a7c2b","trace":"e28ba4219cb348a0ed826d686eb18c68"} -{"@timestamp":"2026-06-25T01:24:31.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54675 - 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":"78ac0806c5cbac96","trace":"083429ab29c1bde31770e5b2c4eec04f"} -{"@timestamp":"2026-06-25T01:24:33.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54677 - 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":"3646a8cca5061136","trace":"1c4ea24fe0715e319b569f478f72cce6"} -{"@timestamp":"2026-06-25T01:24:34.383+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.4ms)","duration":"2024.4ms","level":"slow","span":"0ada5cd925bcaefe","trace":"06ff9af51b9d8b1c7ceec2727558319f"} -{"@timestamp":"2026-06-25T01:24:34.383+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.4ms","level":"info","span":"0ada5cd925bcaefe","trace":"06ff9af51b9d8b1c7ceec2727558319f"} -{"@timestamp":"2026-06-25T01:24:35.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54679 - 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":"ceeb1ab9c410f8f4","trace":"27806bcebca0dc7c0b207fbb249f401f"} -{"@timestamp":"2026-06-25T01:24:37.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54681 - 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":"cee53425bab32c75","trace":"dea6404721609493eeff6e9dccff9b84"} -{"@timestamp":"2026-06-25T01:24:39.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54683 - 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.2ms","level":"info","span":"86e5b85e2b3b4564","trace":"bd35f4e24873f114c01a731aab29fc46"} -{"@timestamp":"2026-06-25T01:24:39.419+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2031.0ms)","duration":"2031.0ms","level":"slow","span":"f10c775e1655b2b6","trace":"0ec4880ff47944a3482abad4e0676608"} -{"@timestamp":"2026-06-25T01:24:39.419+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2031.0ms","level":"info","span":"f10c775e1655b2b6","trace":"0ec4880ff47944a3482abad4e0676608"} -{"@timestamp":"2026-06-25T01:24:41.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54685 - 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":"6dd54d3812e93b7e","trace":"9552aecc0903c74313d140b961059f7a"} -{"@timestamp":"2026-06-25T01:24:43.072+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54687 - 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":"278539d73e9dfb21","trace":"dde87f03a2a50a2cb89cea70a92ff3ac"} -{"@timestamp":"2026-06-25T01:24:44.446+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.2ms)","duration":"2025.2ms","level":"slow","span":"acd4248d33255d79","trace":"571eb5a36478dcacaa0bd8580c4a949e"} -{"@timestamp":"2026-06-25T01:24:44.446+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.2ms","level":"info","span":"acd4248d33255d79","trace":"571eb5a36478dcacaa0bd8580c4a949e"} -{"@timestamp":"2026-06-25T01:24:45.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54689 - 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":"b66cc7de15732d22","trace":"d86bb6b306bf790241872a1edeb79ea4"} -{"@timestamp":"2026-06-25T01:24:47.072+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54691 - 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":"07501c15c05975ac","trace":"8c7fe06ccb91b40b8a3e176269a81f2b"} -{"@timestamp":"2026-06-25T01:24:49.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54693 - 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":"2449b5a176f0d064","trace":"d02a49481a7a9b9700f4269392cde15b"} -{"@timestamp":"2026-06-25T01:24:49.477+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.6ms)","duration":"2026.6ms","level":"slow","span":"617f2b4d0687db0a","trace":"1a26fb62ade7d88f45e8672538d3e603"} -{"@timestamp":"2026-06-25T01:24:49.477+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.6ms","level":"info","span":"617f2b4d0687db0a","trace":"1a26fb62ade7d88f45e8672538d3e603"} -{"@timestamp":"2026-06-25T01:24:51.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54695 - 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":"370fb78c2a625f6d","trace":"87658d09607e0d2a73b8cbe64891cf83"} -{"@timestamp":"2026-06-25T01:24:53.072+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54697 - 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":"932a9ee1b22a6f4c","trace":"a4b6bf779f02e3d4b60e20149cf27db5"} -{"@timestamp":"2026-06-25T01:24:54.506+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.4ms)","duration":"2026.4ms","level":"slow","span":"74dfd0568539a922","trace":"008934f90ac9cacc80ac0f85c57d6342"} -{"@timestamp":"2026-06-25T01:24:54.506+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.4ms","level":"info","span":"74dfd0568539a922","trace":"008934f90ac9cacc80ac0f85c57d6342"} -{"@timestamp":"2026-06-25T01:24:54.663+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=4.6Mi, TotalAlloc=80.3Mi, Sys=23.9Mi, NumGC=43","level":"stat"} -{"@timestamp":"2026-06-25T01:24:54.722+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 109, pass: 109, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:24:55.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54699 - 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":"ea13957784446e62","trace":"31b797e701ab3154b1dac20835c9d0db"} -{"@timestamp":"2026-06-25T01:24:55.251+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 1.8/s, drops: 0, avg time: 226.2ms, med: 3.6ms, 90th: 2024.3ms, 99th: 2045.6ms, 99.9th: 2045.6ms","level":"stat"} -{"@timestamp":"2026-06-25T01:24:57.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54701 - 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":"d1c6f09ac708b311","trace":"1aee23a3dc071aa221745dd36588cf69"} -{"@timestamp":"2026-06-25T01:24:59.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54704 - 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":"26dc9e6177a56d99","trace":"1ffa6e8a893208c4fd9c9bd860cbe228"} -{"@timestamp":"2026-06-25T01:24:59.523+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2013.4ms)","duration":"2013.4ms","level":"slow","span":"4b759fb3b6a2d966","trace":"d10e7a9bf9d06568630f76c4ca7d4300"} -{"@timestamp":"2026-06-25T01:24:59.523+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2013.4ms","level":"info","span":"4b759fb3b6a2d966","trace":"d10e7a9bf9d06568630f76c4ca7d4300"} -{"@timestamp":"2026-06-25T01:25:01.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54705 - 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":"9.8ms","level":"info","span":"fe56af52a749872d","trace":"5bd092f9ba4a4d2bc7cf5089eed592fd"} -{"@timestamp":"2026-06-25T01:25:03.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54707 - 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":"cafbb56c48f34003","trace":"8c7b023382b61234f4ea64c017af338e"} -{"@timestamp":"2026-06-25T01:25:04.569+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2042.9ms)","duration":"2042.9ms","level":"slow","span":"affd5d417b081236","trace":"8f4d88ed989928d157d3170197eae532"} -{"@timestamp":"2026-06-25T01:25:04.569+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2042.9ms","level":"info","span":"affd5d417b081236","trace":"8f4d88ed989928d157d3170197eae532"} -{"@timestamp":"2026-06-25T01:25:05.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54709 - 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":"178da8f45f4f0dc5","trace":"7f7ede25926c9e349b9e5eb5d9582f0d"} -{"@timestamp":"2026-06-25T01:25:07.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54711 - 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":"a3baaa2b513d46ab","trace":"6737cf629d3b418eca2111ae6c18c56b"} -{"@timestamp":"2026-06-25T01:25:09.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54713 - 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.2ms","level":"info","span":"71721438d1d47c17","trace":"448014aaa6b985b3c8df49ce6e375339"} -{"@timestamp":"2026-06-25T01:25:09.602+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2029.9ms)","duration":"2029.9ms","level":"slow","span":"71099618d1c9a959","trace":"72ca6ed897e2735138035ecd95f0d08f"} -{"@timestamp":"2026-06-25T01:25:09.602+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2029.9ms","level":"info","span":"71099618d1c9a959","trace":"72ca6ed897e2735138035ecd95f0d08f"} -{"@timestamp":"2026-06-25T01:25:11.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54715 - 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":"ace93f2f2d278fbe","trace":"ac25151e8aabfa6f05a37ad39fe52d82"} -{"@timestamp":"2026-06-25T01:25:13.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54717 - 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":"0abbad02734a8c31","trace":"b0f4c08c193064f658e4febd4400d47e"} -{"@timestamp":"2026-06-25T01:25:14.637+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2032.2ms)","duration":"2032.2ms","level":"slow","span":"308b01f5e5048caa","trace":"0ea21b93a7960a1170a66793e1cddcbf"} -{"@timestamp":"2026-06-25T01:25:14.637+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2032.2ms","level":"info","span":"308b01f5e5048caa","trace":"0ea21b93a7960a1170a66793e1cddcbf"} -{"@timestamp":"2026-06-25T01:25:15.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54719 - 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":"045e51175819cc07","trace":"363e650b65b273730af412493bc61548"} -{"@timestamp":"2026-06-25T01:25:17.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54722 - 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.0ms","level":"info","span":"5c1e68bdf2b26f64","trace":"c6b97f98d76b8ecdc596b62bfc1c1fcd"} -{"@timestamp":"2026-06-25T01:25:19.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54724 - 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.5ms","level":"info","span":"144cae6171d7335b","trace":"4f45f2295d7bf363ba2d628da788324e"} -{"@timestamp":"2026-06-25T01:25:19.649+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2006.9ms)","duration":"2006.9ms","level":"slow","span":"0c45126a81f89711","trace":"111c7977ee273d87a0c5ed2c4f01b1c9"} -{"@timestamp":"2026-06-25T01:25:19.649+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2006.9ms","level":"info","span":"0c45126a81f89711","trace":"111c7977ee273d87a0c5ed2c4f01b1c9"} -{"@timestamp":"2026-06-25T01:25:21.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54726 - 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":"772f5f44dc8dd283","trace":"21cd1996e1b44fcc9364f863ce53b5f3"} -{"@timestamp":"2026-06-25T01:25:23.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54728 - 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.0ms","level":"info","span":"d9e88678ee4957e6","trace":"39b9bdb6788090a0b8849f4331a00686"} -{"@timestamp":"2026-06-25T01:25:24.667+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2014.1ms)","duration":"2014.1ms","level":"slow","span":"98a8a929ac0fa4b6","trace":"df2e21d992999c426efdd9232232d6cf"} -{"@timestamp":"2026-06-25T01:25:24.668+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2014.1ms","level":"info","span":"98a8a929ac0fa4b6","trace":"df2e21d992999c426efdd9232232d6cf"} -{"@timestamp":"2026-06-25T01:25:25.313+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54730 - 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.5ms","level":"info","span":"2a9588110a14ed51","trace":"7f22ac4b19ff03f330b4f07b8dbef5a5"} -{"@timestamp":"2026-06-25T01:25:27.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54732 - 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":"0174abd644a46fbd","trace":"4afec8a4066a7597ecc596582cb42faa"} -{"@timestamp":"2026-06-25T01:25:29.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54734 - 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.3ms","level":"info","span":"073a07fe43531b08","trace":"392e65f334429b9bdd71a5945cbc6aa0"} -{"@timestamp":"2026-06-25T01:25:29.718+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2046.6ms)","duration":"2046.6ms","level":"slow","span":"eeabb4099febd8aa","trace":"e2eef75035b984662d68249b3dc9e376"} -{"@timestamp":"2026-06-25T01:25:29.718+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2046.6ms","level":"info","span":"eeabb4099febd8aa","trace":"e2eef75035b984662d68249b3dc9e376"} -{"@timestamp":"2026-06-25T01:25:31.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54736 - 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":"2741b9b2d891db8e","trace":"495f802b5006262c15814aec9473d496"} -{"@timestamp":"2026-06-25T01:25:33.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54738 - 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":"207366a5463e24c8","trace":"ba1b6bd5a51c94f8a8c5d48b5af75cc7"} -{"@timestamp":"2026-06-25T01:25:34.740+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2020.0ms)","duration":"2020.0ms","level":"slow","span":"16148e3d048338e4","trace":"ef9495ee94e45cf7e698df3e9ca5f24b"} -{"@timestamp":"2026-06-25T01:25:34.740+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2020.0ms","level":"info","span":"16148e3d048338e4","trace":"ef9495ee94e45cf7e698df3e9ca5f24b"} -{"@timestamp":"2026-06-25T01:25:35.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54740 - 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":"e53dfdd848a02209","trace":"941b1ef7b7d955ad681c8de5b1816722"} -{"@timestamp":"2026-06-25T01:25:37.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54742 - 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.3ms","level":"info","span":"1202e0e43bb843ba","trace":"88a10555803eb1e656802e280ad4f393"} -{"@timestamp":"2026-06-25T01:25:39.307+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54744 - 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":"f83c1cc66240d792","trace":"a37e5e97aee724cc20b31d85b880d8c0"} -{"@timestamp":"2026-06-25T01:25:39.773+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2029.7ms)","duration":"2029.7ms","level":"slow","span":"3d8939c25a34e6a2","trace":"cd75c05b879d25e4d500c041798c2b11"} -{"@timestamp":"2026-06-25T01:25:39.773+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2029.7ms","level":"info","span":"3d8939c25a34e6a2","trace":"cd75c05b879d25e4d500c041798c2b11"} -{"@timestamp":"2026-06-25T01:25:41.310+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54746 - 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":"5d0497170ff51504","trace":"6948ebd9c3fe4b19edb92545d4400000"} -{"@timestamp":"2026-06-25T01:25:43.312+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54749 - 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.3ms","level":"info","span":"8c89c6abac4354b8","trace":"39f1a617829f4d82c3864352dbbe2561"} -{"@timestamp":"2026-06-25T01:25:44.806+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2029.3ms)","duration":"2029.3ms","level":"slow","span":"5ba21ba2e591a132","trace":"fb845e0d96e3460ef14433fa196b83ef"} -{"@timestamp":"2026-06-25T01:25:44.806+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2029.3ms","level":"info","span":"5ba21ba2e591a132","trace":"fb845e0d96e3460ef14433fa196b83ef"} -{"@timestamp":"2026-06-25T01:25:45.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54751 - 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.4ms","level":"info","span":"b7ace985383cf4cf","trace":"5c1e318959753b5e9b03a35c985cdc1f"} -{"@timestamp":"2026-06-25T01:25:47.310+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54753 - 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.3ms","level":"info","span":"6b813796e54ff3f0","trace":"13c51e2e66d083b9ab654c35d853e3c4"} -{"@timestamp":"2026-06-25T01:25:49.314+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54755 - 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":"221dbc8881b6d632","trace":"e424e811db7a36e326f7052b0a14ddbd"} -{"@timestamp":"2026-06-25T01:25:49.824+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2012.5ms)","duration":"2012.5ms","level":"slow","span":"594bb83e17f182c9","trace":"16a7366a0046ee8b79967f4df2dd0e9c"} -{"@timestamp":"2026-06-25T01:25:49.824+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2012.5ms","level":"info","span":"594bb83e17f182c9","trace":"16a7366a0046ee8b79967f4df2dd0e9c"} -{"@timestamp":"2026-06-25T01:25:51.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54757 - 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":"d23a1163360907b7","trace":"3e37cc8b9ccc0d7e7142bd54c84f54c3"} -{"@timestamp":"2026-06-25T01:25:53.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54759 - 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":"f7383063fda6761d","trace":"32444ca7a77d7fab66320ca056fded33"} -{"@timestamp":"2026-06-25T01:25:54.663+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=5.1Mi, TotalAlloc=87.9Mi, Sys=23.9Mi, NumGC=46","level":"stat"} -{"@timestamp":"2026-06-25T01:25:54.722+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 42, pass: 41, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:25:54.840+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2012.9ms)","duration":"2012.9ms","level":"slow","span":"b374f3c8ebe1c497","trace":"5a4884f019b1246bda803091d62ffe1e"} -{"@timestamp":"2026-06-25T01:25:54.840+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2012.9ms","level":"info","span":"b374f3c8ebe1c497","trace":"5a4884f019b1246bda803091d62ffe1e"} -{"@timestamp":"2026-06-25T01:25:55.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54761 - 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":"e385c4099de9279d","trace":"ea9a152ec0cb47516ed2b851a0eb617a"} -{"@timestamp":"2026-06-25T01:25:55.252+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 0.7/s, drops: 0, avg time: 581.9ms, med: 5.3ms, 90th: 2029.9ms, 99th: 2046.6ms, 99.9th: 2046.6ms","level":"stat"} -{"@timestamp":"2026-06-25T01:25:57.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54763 - 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":"746fef4623e61829","trace":"26f2661d1304c3bb9c16cb26d03e409c"} -{"@timestamp":"2026-06-25T01:25:59.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54765 - 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.6ms","level":"info","span":"9395a16bee369cff","trace":"261902dfe2fa7bd800ebfdee9d6bff15"} -{"@timestamp":"2026-06-25T01:25:59.869+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.0ms)","duration":"2026.0ms","level":"slow","span":"04fd12f5421937a3","trace":"27cb3dec3023239dce464e19f04a28d5"} -{"@timestamp":"2026-06-25T01:25:59.869+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.0ms","level":"info","span":"04fd12f5421937a3","trace":"27cb3dec3023239dce464e19f04a28d5"} -{"@timestamp":"2026-06-25T01:26:01.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54767 - 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":"5c48910fb10dcef7","trace":"f99fcf38242536588ef8809a43413111"} -{"@timestamp":"2026-06-25T01:26:03.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54769 - 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":"c7b1cd266df9a271","trace":"0fc1516468551c01015ee03d21d23e82"} -{"@timestamp":"2026-06-25T01:26:04.884+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2011.0ms)","duration":"2011.0ms","level":"slow","span":"d01af86442d38547","trace":"73846753513a2778ab3b123715319d22"} -{"@timestamp":"2026-06-25T01:26:04.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2011.0ms","level":"info","span":"d01af86442d38547","trace":"73846753513a2778ab3b123715319d22"} -{"@timestamp":"2026-06-25T01:26:05.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54771 - 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.2ms","level":"info","span":"081ea165be2df4be","trace":"7ed3deeef78baa05d7aa8eca7c35af8d"} -{"@timestamp":"2026-06-25T01:26:07.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54773 - 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":"ca82e387751dd597","trace":"f4ead4701262c9cb4b614e8d5e0a2aa3"} -{"@timestamp":"2026-06-25T01:26:09.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54775 - 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":"1a82deafd6ff1913","trace":"3f820b06f576c97b62138afa83d82da7"} -{"@timestamp":"2026-06-25T01:26:09.932+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2044.9ms)","duration":"2044.9ms","level":"slow","span":"dde09b139e9aab29","trace":"8d127c37a349477980fc13d3376a7935"} -{"@timestamp":"2026-06-25T01:26:09.932+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2044.9ms","level":"info","span":"dde09b139e9aab29","trace":"8d127c37a349477980fc13d3376a7935"} -{"@timestamp":"2026-06-25T01:26:11.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54777 - 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":"4c74f50727c8ad23","trace":"2983739a14a556917ea58804393cdf86"} -{"@timestamp":"2026-06-25T01:26:12.141+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54783 - 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":"4334d095edd2987f","trace":"5925eb0d77d1440c922aee8036d76163"} -{"@timestamp":"2026-06-25T01:26:12.142+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54781 - 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.8ms","level":"info","span":"a77000d296e8338d","trace":"e48b61341eb3ecd9ae3165804c6db26d"} -{"@timestamp":"2026-06-25T01:26:12.142+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54782 - 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":"866476707e68b65a","trace":"676cf26ca42efde7c5671c7c9581af6f"} -{"@timestamp":"2026-06-25T01:26:12.147+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54785 - 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":"4af166ae18eedf07","trace":"7a427992bc81561f09bb6db9cd18c19e"} -{"@timestamp":"2026-06-25T01:26:12.151+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54787 - 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":"e9444b9504288b41","trace":"500ddbc860a8f56c32fa4a8e85980943"} -{"@timestamp":"2026-06-25T01:26:12.153+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54789 - 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":"f0a3c0c5683526eb","trace":"2d4206d86a635cecab2afa11e51c0412"} -{"@timestamp":"2026-06-25T01:26:12.157+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54791 - 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":"2896a372c6ab2437","trace":"60fa4cc699a025f6b25a8e6283c67dbd"} -{"@timestamp":"2026-06-25T01:26:12.157+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54793 - 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":"188fd29bc326461c","trace":"eb2bd353a2656daa9f3351d8e662c7f4"} -{"@timestamp":"2026-06-25T01:26:12.162+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54795 - 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":"934ce0e03ba5e8b0","trace":"0ae9b092ddcbfda36904cfea7679877e"} -{"@timestamp":"2026-06-25T01:26:12.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54797 - 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":"c482834491eae086","trace":"4cdf7c63fda6b1fb8866584c15a0c5a0"} -{"@timestamp":"2026-06-25T01:26:13.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54799 - 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":"3f8ee4393ac0b5e4","trace":"49aea4b868c2a0e48810dcfbc33e60d9"} -{"@timestamp":"2026-06-25T01:26:14.960+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.0ms)","duration":"2025.0ms","level":"slow","span":"e392c2da5db0aa65","trace":"d136891d14847a1aa5a82ce78845d75a"} -{"@timestamp":"2026-06-25T01:26:14.960+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.0ms","level":"info","span":"e392c2da5db0aa65","trace":"d136891d14847a1aa5a82ce78845d75a"} -{"@timestamp":"2026-06-25T01:26:15.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54801 - 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":"5202bb5f47de8fec","trace":"ca7f7eabad987e96f3b778a1f9e67ef7"} -{"@timestamp":"2026-06-25T01:26:15.156+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54803 - 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":"5d92b2c6447c7585","trace":"d627e589e03b705338c8d483118c97fd"} -{"@timestamp":"2026-06-25T01:26:15.166+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54805 - 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":"8d5a86f2b89b4960","trace":"b82e858fd5b1056414e5187e47e715db"} -{"@timestamp":"2026-06-25T01:26:15.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54807 - 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":"b7208323fe7b26a9","trace":"89ab01ad8886723a715a3e8bec442940"} -{"@timestamp":"2026-06-25T01:26:15.183+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54809 - 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.2ms","level":"info","span":"652948c6c83d2a4c","trace":"42a2805efc9fb616bb16c11ee19bd53a"} -{"@timestamp":"2026-06-25T01:26:17.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54811 - 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":"10023ba1fe69ec67","trace":"aa35cc59abace5b6baa4fb0d0920c93d"} -{"@timestamp":"2026-06-25T01:26:18.156+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54813 - 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":"1b0ec50984ae0bf0","trace":"b374854733ab0bdb2f6679fc8bf327ba"} -{"@timestamp":"2026-06-25T01:26:18.164+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54815 - 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":"2300923c89313bae","trace":"ef0c48ef9043ca5f78d09b5d54b8cadd"} -{"@timestamp":"2026-06-25T01:26:18.170+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54817 - 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":"2a28d6dfa9b41276","trace":"68963124cf6736a9a5b13ef4c16317b8"} -{"@timestamp":"2026-06-25T01:26:18.179+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54819 - 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":"40ea7bd71c374808","trace":"a9071f4acd669f54228074cf13d9d26e"} -{"@timestamp":"2026-06-25T01:26:19.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54821 - 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":"47cd2d95bd796b29","trace":"f14a5775afe1d2a8b49318bd78cb2915"} -{"@timestamp":"2026-06-25T01:26:19.981+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2016.9ms)","duration":"2016.9ms","level":"slow","span":"8e3f10f3c29fe151","trace":"2084f7886d10bf34248a701cae3a2321"} -{"@timestamp":"2026-06-25T01:26:19.981+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2016.9ms","level":"info","span":"8e3f10f3c29fe151","trace":"2084f7886d10bf34248a701cae3a2321"} -{"@timestamp":"2026-06-25T01:26:21.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54823 - 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":"a60797dcd84ea3b7","trace":"35b59a09527f751b904fe470cdfa6e4a"} -{"@timestamp":"2026-06-25T01:26:21.156+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54825 - 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":"40c835f139a9b339","trace":"fc524f2aefdd459e1bb22aabab814c09"} -{"@timestamp":"2026-06-25T01:26:21.163+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54827 - 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":"f15441aafcfb6345","trace":"1a4594a0d0439e543a2753587d241245"} -{"@timestamp":"2026-06-25T01:26:21.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54829 - 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":"1612c3c4e8dffd17","trace":"ed8b286741e3534e7460c83dcb325e76"} -{"@timestamp":"2026-06-25T01:26:21.177+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54831 - 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":"616c1806be58394c","trace":"e8072ff6f36aaf67068d1d47a75deb2e"} -{"@timestamp":"2026-06-25T01:26:23.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54833 - 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":"354dfca3544ed1b5","trace":"da68f11a424d05c005ddf442693d3901"} -{"@timestamp":"2026-06-25T01:26:24.157+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54835 - 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":"830dfc22203900ba","trace":"9cafd2758e334b46bbd75d307eb50794"} -{"@timestamp":"2026-06-25T01:26:24.164+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54837 - 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":"c48b32cc9641dc76","trace":"e59503a03af33251c5776f3a6c02a219"} -{"@timestamp":"2026-06-25T01:26:24.169+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54839 - 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":"5f6171a25eefa0e8","trace":"7ce725b685ad7ede714ac8b41fcddad0"} -{"@timestamp":"2026-06-25T01:26:24.177+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54841 - 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":"c6c6ab7c37a0eead","trace":"154eac667d306c45b77d9414aa01c72a"} -{"@timestamp":"2026-06-25T01:26:25.011+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.4ms)","duration":"2027.4ms","level":"slow","span":"1ff53098b9ddb268","trace":"43a8f0c9f7c1f672c65c90cfc5b4c8ed"} -{"@timestamp":"2026-06-25T01:26:25.011+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.4ms","level":"info","span":"1ff53098b9ddb268","trace":"43a8f0c9f7c1f672c65c90cfc5b4c8ed"} -{"@timestamp":"2026-06-25T01:26:25.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54843 - 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":"9787c4ed9ded45ab","trace":"2ac18f64369d7566b53ba676a0fa9ace"} -{"@timestamp":"2026-06-25T01:26:27.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54845 - 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":"428db0ce22a05ce2","trace":"858bca345003d61fa608b11b3e8ac89f"} -{"@timestamp":"2026-06-25T01:26:27.155+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54847 - 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":"62cc776d307eade8","trace":"6bff516ddfb716f0b221bca62f40b972"} -{"@timestamp":"2026-06-25T01:26:27.160+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54849 - 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":"c0f8ce4366efffb2","trace":"a47d9e36d5c1d581fc1b790b90297007"} -{"@timestamp":"2026-06-25T01:26:27.164+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54851 - 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":"4b14d7977ea497fa","trace":"be6d7f16e3c9817906c2f12392244451"} -{"@timestamp":"2026-06-25T01:26:27.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54853 - 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":"931b7e59d6341e82","trace":"7cb41e86c43ccd836a87f8c150b2356a"} -{"@timestamp":"2026-06-25T01:26:28.101+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54859 - 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":"3fa17356b1093daa","trace":"bcd5e1f82749c24960a755cbb996ff6f"} -{"@timestamp":"2026-06-25T01:26:28.101+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:54857 - 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.8ms","level":"info","span":"8a167e803f7a1248","trace":"d4bb4f36ffa241a2258f81639a8b5f07"} -{"@timestamp":"2026-06-25T01:26:28.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54858 - 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":"0d927e6c6a451ab7","trace":"06c8833779428b147a82367818b207c5"} -{"@timestamp":"2026-06-25T01:26:28.105+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:54863 - 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":"b3791616dbc74a95","trace":"8794ba049bcfd5681407a162feab5262"} -{"@timestamp":"2026-06-25T01:26:28.107+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54864 - 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":"d3e9491bfd9a3b74","trace":"cdfed00fe21a09bb878cdaffb4326086"} -{"@timestamp":"2026-06-25T01:26:28.108+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54865 - 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":"aeed2115ad3c8e50","trace":"5bd2bfdc279ecf90e4d5c8bbd604ec58"} -{"@timestamp":"2026-06-25T01:26:29.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54867 - 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":"08d4e7c7bd54e6ec","trace":"69170dac9ad3aa51e44e264f398ef119"} -{"@timestamp":"2026-06-25T01:26:30.041+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.0ms)","duration":"2027.0ms","level":"slow","span":"b3693ab1b5fd9ecf","trace":"88cb5f9b4c6d01815486d8475004e9ed"} -{"@timestamp":"2026-06-25T01:26:30.041+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.0ms","level":"info","span":"b3693ab1b5fd9ecf","trace":"88cb5f9b4c6d01815486d8475004e9ed"} -{"@timestamp":"2026-06-25T01:26:31.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54869 - 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":"85a6eca71864bbbf","trace":"2d327edcf51a9ddd02898e7a75ebe3ad"} -{"@timestamp":"2026-06-25T01:26:33.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54871 - 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":"7ce29ed94ad90f29","trace":"3ce0d6789ecec5ead547709e178e9a24"} -{"@timestamp":"2026-06-25T01:26:34.115+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54879 - 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":"d78793e4a223af5f","trace":"e5fc9d2c9bbe154f2969791404241cc0"} -{"@timestamp":"2026-06-25T01:26:34.116+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54877 - 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":"522d2e0cf8c06dc3","trace":"1419e827dbdb115261776d3e75bf59bd"} -{"@timestamp":"2026-06-25T01:26:34.117+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54878 - 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":"8eaf9794ed80923f","trace":"da44106b4ec816483168acc930cf4417"} -{"@timestamp":"2026-06-25T01:26:34.117+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54873 - 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.3ms","level":"info","span":"0d4f98890de1185e","trace":"99f7d4d5267d8a83f3c295ce87d1e15b"} -{"@timestamp":"2026-06-25T01:26:34.122+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54881 - 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":"c57c8bbb14ed2bbd","trace":"fad377696048c52c61ec350129ea4054"} -{"@timestamp":"2026-06-25T01:26:34.123+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54887 - 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":"88b67774a13a082f","trace":"988fd6fb68f29c303832c6c7bca672a6"} -{"@timestamp":"2026-06-25T01:26:34.124+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54889 - 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":"fcd3257bae3471f6","trace":"de7a191d6280b8a451bd6c77c59e18f8"} -{"@timestamp":"2026-06-25T01:26:34.124+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54888 - 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":"c488f18aa64fb6c6","trace":"e15b0b248caa38e63bd4e0eecf119a8b"} -{"@timestamp":"2026-06-25T01:26:34.124+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54886 - 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":"55c82a4a30793c6e","trace":"e26893e5ce5fd2e2b7ea8b530da9d3d1"} -{"@timestamp":"2026-06-25T01:26:34.126+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54891 - 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":"27dc3da3640e9469","trace":"d638f05a23ee11e2d8426229496ca6e7"} -{"@timestamp":"2026-06-25T01:26:34.128+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54894 - 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":"3a202c26c60fc577","trace":"01b31322b872a44dfefbee08c63c5bb3"} -{"@timestamp":"2026-06-25T01:26:34.129+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54895 - 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":"fea86661e70087d3","trace":"9e35ccad292a4a9e772a576deb594991"} -{"@timestamp":"2026-06-25T01:26:34.133+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54898 - 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":"0b46615c2cafac09","trace":"1f074c271cb5b0f0958860c7795f0482"} -{"@timestamp":"2026-06-25T01:26:34.135+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54899 - 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":"ef1b90eed0abd0d8","trace":"72897ee06f35446ef3b10a6e437d7a74"} -{"@timestamp":"2026-06-25T01:26:34.140+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54901 - 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":"f6428e6c5816dee2","trace":"385a25ac02fb1b4ab25ff73326fe6282"} -{"@timestamp":"2026-06-25T01:26:34.145+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54903 - 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":"d657817958658471","trace":"605d33dc2aae4c50c7b600974741b3a9"} -{"@timestamp":"2026-06-25T01:26:35.068+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2022.2ms)","duration":"2022.2ms","level":"slow","span":"dfb1c9a802a80e2a","trace":"3e4884ae41169a264bbe9a4c6cf61f46"} -{"@timestamp":"2026-06-25T01:26:35.068+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2022.2ms","level":"info","span":"dfb1c9a802a80e2a","trace":"3e4884ae41169a264bbe9a4c6cf61f46"} -{"@timestamp":"2026-06-25T01:26:35.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54906 - 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":"0511c55e28384b87","trace":"b0c3605e1d0ac0ca732e1ca76a3f3413"} -{"@timestamp":"2026-06-25T01:26:36.816+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54913 - 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":"e6ad1a26e1ba6512","trace":"3781d3427d2c13c9b1a9e362704fbc87"} -{"@timestamp":"2026-06-25T01:26:36.817+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54911 - 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":"96f2b36a8fc78f2d","trace":"70d5738f32a20d4b4564490ccf285cd9"} -{"@timestamp":"2026-06-25T01:26:36.817+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54914 - 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":"d14a74e7b13e3038","trace":"552be34ad4c6da7507ec8a7c747e5499"} -{"@timestamp":"2026-06-25T01:26:36.819+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54912 - 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":"21b6591a3fa3eb06","trace":"d9260c7f34f0de5b71069a4a7733536f"} -{"@timestamp":"2026-06-25T01:26:36.825+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54918 - 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":"f70a2d8d081a29e7","trace":"e7725b94d202708e80817280ff663d14"} -{"@timestamp":"2026-06-25T01:26:36.826+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54920 - 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":"bcedc35b3e797a44","trace":"45984ea6dc8122a76a59d9b4f7e19d08"} -{"@timestamp":"2026-06-25T01:26:36.826+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54919 - 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":"238ab93818c83686","trace":"a69ae455ea056b87785ad19393102f89"} -{"@timestamp":"2026-06-25T01:26:36.829+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54922 - 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":"0da61218d60a88ce","trace":"dc582156de2f1929ef01cf6cdc0704f8"} -{"@timestamp":"2026-06-25T01:26:36.833+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54924 - 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":"836fff3d307ef372","trace":"38db0d80188a592ff3c3a67d4075e1a3"} -{"@timestamp":"2026-06-25T01:26:36.836+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54926 - 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":"3811fb18755189ed","trace":"74fa0850075142776394d11a5ab765f8"} -{"@timestamp":"2026-06-25T01:26:37.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54928 - 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.2ms","level":"info","span":"1fa6d745c7bf80dd","trace":"203e2ff00c026de67f60c83f88f801e2"} -{"@timestamp":"2026-06-25T01:26:38.458+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54934 - 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":"b71aa455269d667e","trace":"f56b9ad57128e30a95e9ca8463e9ebc5"} -{"@timestamp":"2026-06-25T01:26:38.458+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:54932 - 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":"182a2a1cd93cecfb","trace":"d6766ec96add33e987785ad2d9445658"} -{"@timestamp":"2026-06-25T01:26:38.460+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54933 - 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":"dce71d01ca42b066","trace":"e4873e9c7a970bf809b1873968104a8e"} -{"@timestamp":"2026-06-25T01:26:38.462+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:54937 - 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":"5ac7cd1d49934c6c","trace":"ecbbfbd225993c4605efbe1c852a591d"} -{"@timestamp":"2026-06-25T01:26:38.463+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54938 - 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":"c7acc3c88268f55f","trace":"f69cdd2703243311bc4a20f0a5f93469"} -{"@timestamp":"2026-06-25T01:26:38.464+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54940 - 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":"3cb61f207f24e54b","trace":"fc0372a73c0449e304e7bbc2462b9134"} -{"@timestamp":"2026-06-25T01:26:39.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54942 - 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":"bcc838cee95dabf2","trace":"e098ead0c42f481e0202245a5e653740"} -{"@timestamp":"2026-06-25T01:26:40.098+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.3ms)","duration":"2025.3ms","level":"slow","span":"b736c96877c55f8c","trace":"1ad472f75afb763ebf1e9403385760ed"} -{"@timestamp":"2026-06-25T01:26:40.098+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.3ms","level":"info","span":"b736c96877c55f8c","trace":"1ad472f75afb763ebf1e9403385760ed"} -{"@timestamp":"2026-06-25T01:26:41.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54944 - 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":"d768cec63c3d33f1","trace":"3916ebc437aadece2ba851c10894cb21"} -{"@timestamp":"2026-06-25T01:26:43.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54946 - 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":"010f838f2eed6838","trace":"b13e24e42019b5e0a706e27f18e126e0"} -{"@timestamp":"2026-06-25T01:26:45.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54948 - 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":"e305274c4aa99cf9","trace":"4f464d28baf171be5c64ed07baca6256"} -{"@timestamp":"2026-06-25T01:26:45.112+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2010.9ms)","duration":"2010.9ms","level":"slow","span":"bf94ac5a7884d505","trace":"51a296e6b7ffcd2110c8c70c0573000a"} -{"@timestamp":"2026-06-25T01:26:45.112+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2010.9ms","level":"info","span":"bf94ac5a7884d505","trace":"51a296e6b7ffcd2110c8c70c0573000a"} -{"@timestamp":"2026-06-25T01:26:47.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54950 - 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":"4b1db25fc0a45b3c","trace":"6fe3ae0beabc6e393c55e149bb14e0d0"} -{"@timestamp":"2026-06-25T01:26:48.519+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54958 - 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":"096833392b9ab710","trace":"184cdfac6431ea16bae7b080ac66e461"} -{"@timestamp":"2026-06-25T01:26:48.520+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54956 - 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":"481d79bd7f186b2b","trace":"cdd02223c9138d07de3b88d33756dd37"} -{"@timestamp":"2026-06-25T01:26:48.521+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54955 - 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.3ms","level":"info","span":"a4d45eb356cdc6b7","trace":"42b64b0cd6914b0975fe7ac0220d4fc9"} -{"@timestamp":"2026-06-25T01:26:48.522+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54957 - 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":"f2ebd966540c21d5","trace":"f45b900d5e35837da86618c31d1bf85e"} -{"@timestamp":"2026-06-25T01:26:48.524+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54961 - 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":"28a359bd5fca126e","trace":"96ae086f841792a22cb75e1b2a158eee"} -{"@timestamp":"2026-06-25T01:26:48.527+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:54965 - 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":"611bb5741dd024ec","trace":"174743bca87e9801020a0da642be55f1"} -{"@timestamp":"2026-06-25T01:26:48.527+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54967 - 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.8ms","level":"info","span":"67033703a94072bb","trace":"3f061857fe1960708e59c479cb7dab23"} -{"@timestamp":"2026-06-25T01:26:48.527+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54966 - 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":"d10302946c7f22ce","trace":"c7c3de85c56241d363052c107295a8c1"} -{"@timestamp":"2026-06-25T01:26:48.528+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:54968 - 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":"4c4535187a6df22d","trace":"b97e4b08a4c77b4ce93451b995d99b44"} -{"@timestamp":"2026-06-25T01:26:48.530+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54970 - 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":"765b06cecdf34adb","trace":"b4f0cd842a4a4f225d2b8df297686136"} -{"@timestamp":"2026-06-25T01:26:48.532+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54972 - 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":"bb91baaf9a57c897","trace":"e8c6fc711c0bfdeea7845f46b7116989"} -{"@timestamp":"2026-06-25T01:26:48.540+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:54974 - 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":"e196d5f448bd2fc9","trace":"dd4060a5416344060f11f9660c1c22c2"} -{"@timestamp":"2026-06-25T01:26:49.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54976 - 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":"dba0134ae9181a21","trace":"435b8ca6cf3b06ae38591ae2ae2833ae"} -{"@timestamp":"2026-06-25T01:26:50.162+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2046.3ms)","duration":"2046.3ms","level":"slow","span":"013974931e28e396","trace":"dcca3a4aa0a13f41593e4e2b9f6c1634"} -{"@timestamp":"2026-06-25T01:26:50.162+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2046.3ms","level":"info","span":"013974931e28e396","trace":"dcca3a4aa0a13f41593e4e2b9f6c1634"} -{"@timestamp":"2026-06-25T01:26:51.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:54978 - 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.3ms","level":"info","span":"a739d05044d523b5","trace":"f7ce3c66277a13a22e3fb3aa52737019"} -{"@timestamp":"2026-06-25T01:26:52.490+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54981 - 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":"8.1ms","level":"info","span":"8f2710a08e9c78d7","trace":"8b399549adcba0bf9db35c5da0c8be0f"} -{"@timestamp":"2026-06-25T01:26:52.492+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:54985 - 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":"33a15bc35805fae7","trace":"d1d3e8a9845a61c9e0eaa3c3eb9db538"} -{"@timestamp":"2026-06-25T01:26:52.493+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54984 - 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":"88bf065c06cad0fd","trace":"92b74f3183848efc713cb83898d7071b"} -{"@timestamp":"2026-06-25T01:26:52.499+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54988 - 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":"9ef4acb15149a677","trace":"4c6cf406c5fb0dc8b0444dda0de0214c"} -{"@timestamp":"2026-06-25T01:26:52.500+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:54989 - 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":"2eab9b3e0caa19b7","trace":"2e2d7060e5ea8cb5a0f2e6095c87a078"} -{"@timestamp":"2026-06-25T01:26:52.502+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:54991 - 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":"c8193ccb687e2ef3","trace":"ae5d07ce99e4d326a584739fa802edb4"} -{"@timestamp":"2026-06-25T01:26:52.503+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54993 - 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":"5f3c385b2aa98b12","trace":"fbeb325fa42ba3a27356880ff9faf7be"} -{"@timestamp":"2026-06-25T01:26:52.510+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:54995 - 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":"38f2b989da14bca1","trace":"b5835f20fe3c74a2b37c8a885eefda78"} -{"@timestamp":"2026-06-25T01:26:52.514+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:54997 - 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":"a408daa6962c63e0","trace":"2a8987f43dce5ebc8ff1554dbe3a9a0a"} -{"@timestamp":"2026-06-25T01:26:52.519+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:54999 - 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":"a8a586f1d695db71","trace":"43482510ad840417d801ac47e01691c4"} -{"@timestamp":"2026-06-25T01:26:53.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55001 - 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.3ms","level":"info","span":"71e0950e168800e2","trace":"c893318b8c1dee9888142cce143786a5"} -{"@timestamp":"2026-06-25T01:26:54.664+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.7Mi, TotalAlloc=105.0Mi, Sys=23.9Mi, NumGC=55","level":"stat"} -{"@timestamp":"2026-06-25T01:26:54.723+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 132, pass: 132, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:26:55.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55003 - 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":"4f97aa03fa2be3b3","trace":"eb7f27debb95e8a045f111a21b671358"} -{"@timestamp":"2026-06-25T01:26:55.174+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2008.7ms)","duration":"2008.7ms","level":"slow","span":"1042cf79de43743b","trace":"ccc14edb6c5ecbe6abd78ba5372ac85e"} -{"@timestamp":"2026-06-25T01:26:55.174+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2008.7ms","level":"info","span":"1042cf79de43743b","trace":"ccc14edb6c5ecbe6abd78ba5372ac85e"} -{"@timestamp":"2026-06-25T01:26:55.253+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 2.2/s, drops: 0, avg time: 186.8ms, med: 3.2ms, 90th: 8.0ms, 99th: 2046.2ms, 99.9th: 2046.2ms","level":"stat"} -{"@timestamp":"2026-06-25T01:26:55.507+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:55005 - 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.8ms","level":"info","span":"fec315d0f9c6dbce","trace":"f3d8d084fdf3b67e36bb5dc482644d38"} -{"@timestamp":"2026-06-25T01:26:55.515+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55007 - 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":"8b7523c62a7573cf","trace":"b1251a5a03ff096dea348f5f8eae3a39"} -{"@timestamp":"2026-06-25T01:26:55.521+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55009 - 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":"a36e4b0eba0f59e8","trace":"f38682517e001b71f5243174117273d7"} -{"@timestamp":"2026-06-25T01:26:55.531+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 404 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:55011 - 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.9ms","level":"info","span":"fd8bcb8b6a9d71c7","trace":"e450663c55e4cc27349fe0c718803f57"} -{"@timestamp":"2026-06-25T01:26:55.898+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55019 - 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":"140c034a26519b6b","trace":"6e248c69fea6a3fb18e9e4fdc57c25fc"} -{"@timestamp":"2026-06-25T01:26:55.899+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c122a4a12013022054a3c - 127.0.0.1:55018 - 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":"652dd9535c18e614","trace":"7cb59cc17b8ab330a53f9bd4557dd713"} -{"@timestamp":"2026-06-25T01:26:55.900+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55016 - 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":"ff1b8581cdbf5d61","trace":"18727178474c3219e27925518c141311"} -{"@timestamp":"2026-06-25T01:26:55.900+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55017 - 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":"fbe5b8c6ff463652","trace":"f3f9f922bde9b8dbad47b798739c3f3e"} -{"@timestamp":"2026-06-25T01:26:55.906+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55025 - 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":"627890d1a8dbc039","trace":"0cd8d5e6f03760bd7068fb5b877c415e"} -{"@timestamp":"2026-06-25T01:26:55.907+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55024 - 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":"eded0546c3253ae3","trace":"c119cb4a1cb1b27de66447615de8826b"} -{"@timestamp":"2026-06-25T01:26:55.908+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55023 - 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":"d64eaaf782e2198c","trace":"bf1af6771e666498dae0927d24d36e71"} -{"@timestamp":"2026-06-25T01:26:55.911+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55029 - 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":"7e81dd6200e81cae","trace":"d1b1be89690077d8e0f6eb418fdcfa2d"} -{"@timestamp":"2026-06-25T01:26:55.913+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55028 - 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":"a8593171f3e21b0b","trace":"6e0899f6a9793d81a0d08c6baead4ee5"} -{"@timestamp":"2026-06-25T01:26:55.919+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55031 - 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":"7f4a16dbed4276b8","trace":"7f58ef45edcec2881f9d376854322413"} -{"@timestamp":"2026-06-25T01:26:57.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55033 - 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":"93a8f4613dab31ed","trace":"2788b61fb7c356e298038b09b7902dd7"} -{"@timestamp":"2026-06-25T01:26:58.195+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55037 - 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":"d7da59b909f54904","trace":"1cc29172b43884c93b353e43c27734ae"} -{"@timestamp":"2026-06-25T01:26:58.197+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55038 - 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":"286842f4672b99aa","trace":"3a8c803c3f4a1217843c0b5866044c00"} -{"@timestamp":"2026-06-25T01:26:58.197+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55039 - 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":"44266274ea399de9","trace":"38f20a498988b201109250947c05c88f"} -{"@timestamp":"2026-06-25T01:26:58.201+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55043 - 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":"a68b5117b780dbd0","trace":"9d83f66202889c76a8d1075d7262bcb6"} -{"@timestamp":"2026-06-25T01:26:58.202+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55042 - 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":"8704a7d14f83b90c","trace":"cebbbcb7c87573f037e0a810c2187f09"} -{"@timestamp":"2026-06-25T01:26:58.206+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55049 - 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":"8d2510a32d9f39ed","trace":"e19026a910c2118f481fe68644b87325"} -{"@timestamp":"2026-06-25T01:26:58.206+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55048 - 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":"056555d353f6ad1b","trace":"7fce37e715ee4cd7629a239f33e7a974"} -{"@timestamp":"2026-06-25T01:26:58.208+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55050 - 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":"2fa975f7464e82e0","trace":"6ee0e925ab1d810f3992101d2a3ac867"} -{"@timestamp":"2026-06-25T01:26:58.209+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55051 - 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":"042720aed160ee21","trace":"03556c0c2281de64b37cb46704fd8c68"} -{"@timestamp":"2026-06-25T01:26:59.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55053 - 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.3ms","level":"info","span":"d42188dfb7eb3ba0","trace":"1dda221e4f983b7ba9bbd47cb11bac44"} -{"@timestamp":"2026-06-25T01:26:59.652+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55061 - 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":"48f4b5e3d15907d6","trace":"ea5466e208f98bb5ae02e0132f46fe93"} -{"@timestamp":"2026-06-25T01:26:59.653+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55059 - 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":"6c9bd0eb54e5dc20","trace":"366506eb6a84192b3ddadf7224e0fa44"} -{"@timestamp":"2026-06-25T01:26:59.654+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55060 - 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":"722293c22de3dea9","trace":"f338a16416f1bdf3e99a036e062cd3cb"} -{"@timestamp":"2026-06-25T01:26:59.654+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55058 - 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":"6467982408041e8f","trace":"613ac5ac34e8eb71f35eeafdf9e02d1b"} -{"@timestamp":"2026-06-25T01:26:59.657+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55065 - 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":"4dc888d19f23162a","trace":"71787e8e4673613c5b83426e9f1794bc"} -{"@timestamp":"2026-06-25T01:26:59.659+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55067 - 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":"056e96b2e7ab46c3","trace":"f5121d7ad3d4d78231ca67e0663a0cdb"} -{"@timestamp":"2026-06-25T01:26:59.660+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55069 - 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":"ab21df23c7c5bb5e","trace":"3b40f6bfc2c4b11cef028ec85ba273d2"} -{"@timestamp":"2026-06-25T01:26:59.660+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55068 - 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":"5946e3f84785e741","trace":"fd7412f1a7dd704856daeb354def061e"} -{"@timestamp":"2026-06-25T01:26:59.662+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55071 - 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":"587e5a7547281e05","trace":"eb42fde2bb61070707b11564be4e84dc"} -{"@timestamp":"2026-06-25T01:26:59.664+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55073 - 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":"ca6922342c3fc194","trace":"06caf13a149adb54b4035d0905a239bc"} -{"@timestamp":"2026-06-25T01:26:59.669+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55075 - 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":"5b95d9cdfceebab7","trace":"f6df6ee78c6343d1f1c2c473d0ce3199"} -{"@timestamp":"2026-06-25T01:26:59.673+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55077 - 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":"5de69cf587a52274","trace":"4c2e398b40be6d746f961c18ad214afd"} -{"@timestamp":"2026-06-25T01:27:00.219+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2041.0ms)","duration":"2041.0ms","level":"slow","span":"e79da07c62452c44","trace":"96e5f5e9765a842972e49f13ab70bdb6"} -{"@timestamp":"2026-06-25T01:27:00.219+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2041.0ms","level":"info","span":"e79da07c62452c44","trace":"96e5f5e9765a842972e49f13ab70bdb6"} -{"@timestamp":"2026-06-25T01:27:01.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55079 - 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":"98de7892915c8eb2","trace":"30dc04a72668b2a35f82ab13f55831fe"} -{"@timestamp":"2026-06-25T01:27:03.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55081 - 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":"12c814231b4bda0a","trace":"0fabaa6ec16033a6c242fff612516ae5"} -{"@timestamp":"2026-06-25T01:27:05.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55083 - 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":"4b11d78bfa410111","trace":"1b09bc2f758892c4981598e3f12ecd38"} -{"@timestamp":"2026-06-25T01:27:05.223+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2001.4ms)","duration":"2001.4ms","level":"slow","span":"29bcf22db164c16b","trace":"423ff7c15631d34e0d5113df67f60fd3"} -{"@timestamp":"2026-06-25T01:27:05.223+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2001.4ms","level":"info","span":"29bcf22db164c16b","trace":"423ff7c15631d34e0d5113df67f60fd3"} -{"@timestamp":"2026-06-25T01:27:07.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55085 - 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":"daae4619622828ca","trace":"68e281ec00643f0153f881fad38696ea"} -{"@timestamp":"2026-06-25T01:27:09.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55087 - 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":"7e552d000ee6cc2c","trace":"680a8bbb27f4340ffe319b3bb9954b5e"} -{"@timestamp":"2026-06-25T01:27:10.279+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2054.0ms)","duration":"2054.0ms","level":"slow","span":"d0d460338730b732","trace":"86f060d1d1009bd97234505533a369e3"} -{"@timestamp":"2026-06-25T01:27:10.279+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2054.0ms","level":"info","span":"d0d460338730b732","trace":"86f060d1d1009bd97234505533a369e3"} -{"@timestamp":"2026-06-25T01:27:11.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55089 - 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":"345f40b4766b041a","trace":"84e2b4ad48a438ff71bb364a4bb58a20"} -{"@timestamp":"2026-06-25T01:27:13.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55091 - 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":"ebefc14a9f21e862","trace":"2b4b259c516863df65071538779b3d62"} -{"@timestamp":"2026-06-25T01:27:15.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55093 - 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.6ms","level":"info","span":"b3f22e490f7e2fde","trace":"05dc1fe8194b0537671188af87ac7982"} -{"@timestamp":"2026-06-25T01:27:15.312+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2029.3ms)","duration":"2029.3ms","level":"slow","span":"54b779c284a31744","trace":"4b78da0a73ae1c5341cfb13a2cd15cb1"} -{"@timestamp":"2026-06-25T01:27:15.312+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2029.3ms","level":"info","span":"54b779c284a31744","trace":"4b78da0a73ae1c5341cfb13a2cd15cb1"} -{"@timestamp":"2026-06-25T01:27:16.460+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - PATCH /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55095 - 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.7ms","level":"info","span":"681d42b256b491e2","trace":"bf3b916cc65144e60c58968abbea36ec"} -{"@timestamp":"2026-06-25T01:27:17.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55097 - 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":"6576bdb7e3db32df","trace":"abfcc80b2fcbabff0628aa2ae7bdcbad"} -{"@timestamp":"2026-06-25T01:27:18.973+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-jobs - 127.0.0.1:55099 - 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.8ms","level":"info","span":"59168fb919c1264a","trace":"fd421574dd2896ca04bff46cb541a16c"} -{"@timestamp":"2026-06-25T01:27:18.979+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55101 - 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":"06000465262ba289","trace":"9608d39e44f212ec4ddfaf6aa0fd2147"} -{"@timestamp":"2026-06-25T01:27:18.982+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55103 - 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":"03aa8f3ce5597dc2","trace":"65c755425cd2ae1199dbd00913ef0322"} -{"@timestamp":"2026-06-25T01:27:19.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55105 - 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":"d22b215a3911df67","trace":"79414691b50c04487d580cdd105a11f8"} -{"@timestamp":"2026-06-25T01:27:20.341+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.1ms)","duration":"2026.1ms","level":"slow","span":"5843684107fe6e4d","trace":"c65f1d9a03d75cf622890a4f6ed8718b"} -{"@timestamp":"2026-06-25T01:27:20.342+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.1ms","level":"info","span":"5843684107fe6e4d","trace":"c65f1d9a03d75cf622890a4f6ed8718b"} -{"@timestamp":"2026-06-25T01:27:21.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55118 - 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":"1cb0df84e9288b60","trace":"0b950c0e9a0215f641e26a901a047e48"} -{"@timestamp":"2026-06-25T01:27:21.984+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55120 - 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":"b6c9201fef1669b8","trace":"44c5871c82321d268a1f257c46a8efc6"} -{"@timestamp":"2026-06-25T01:27:22.088+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55126 - 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":"c1344def5c807474","trace":"611487a6460ccb5c7069fdf3cef753c3"} -{"@timestamp":"2026-06-25T01:27:22.089+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55125 - 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":"a4624865c8785e6d","trace":"84a652eb27654328230b4fda38f0e600"} -{"@timestamp":"2026-06-25T01:27:22.089+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55124 - 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":"2722ba95f82ea6fc","trace":"6466cb2dccccc2a627479f0b505bc329"} -{"@timestamp":"2026-06-25T01:27:22.092+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55128 - 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":"02bd28eb0fb85783","trace":"767efb9bb106a8a831e142747fd74ab8"} -{"@timestamp":"2026-06-25T01:27:22.095+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55130 - 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":"842508c07dc39acc","trace":"16eeece42efbd69b51b9285c56cea166"} -{"@timestamp":"2026-06-25T01:27:22.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:55132 - 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":"35793414ff072d55","trace":"bb4a728a640bef283c5ae3eb2faffaa4"} -{"@timestamp":"2026-06-25T01:27:23.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55150 - 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":"d91ff3af1c425321","trace":"2d0350bdaeff8ddf393ff917a8a642ca"} -{"@timestamp":"2026-06-25T01:27:25.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55152 - 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":"9378dc6fa6fe6f47","trace":"c684e0db90849d215475f970bcacfa62"} -{"@timestamp":"2026-06-25T01:27:25.371+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.8ms)","duration":"2027.8ms","level":"slow","span":"6f01e9aabea48c0f","trace":"0a67c12b8c30558d9c9cb041642fc1bc"} -{"@timestamp":"2026-06-25T01:27:25.371+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.8ms","level":"info","span":"6f01e9aabea48c0f","trace":"0a67c12b8c30558d9c9cb041642fc1bc"} -{"@timestamp":"2026-06-25T01:27:27.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55154 - 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":"908fbf67bdc8bc27","trace":"19a60b458127d12f0c99b06a983b16f3"} -{"@timestamp":"2026-06-25T01:27:29.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55160 - 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":"69529977c2514426","trace":"d67aebd0279674a3f4938b2d890b73e4"} -{"@timestamp":"2026-06-25T01:27:30.398+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.7ms)","duration":"2024.7ms","level":"slow","span":"cb3aac3fbaa8a770","trace":"2183b81683b5b30656fa137f6bcae450"} -{"@timestamp":"2026-06-25T01:27:30.398+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.7ms","level":"info","span":"cb3aac3fbaa8a770","trace":"2183b81683b5b30656fa137f6bcae450"} -{"@timestamp":"2026-06-25T01:27:31.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55162 - 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":"08feba6bdd4551ea","trace":"ca0509936ff7fda358c19669c7ed5e44"} -{"@timestamp":"2026-06-25T01:27:33.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55164 - 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":"9dbeee1c08d2a1cb","trace":"526fb55d3429ffa0e50c994a3506fcd7"} -{"@timestamp":"2026-06-25T01:27:35.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55166 - 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":"c06f2239de720c9f","trace":"a23244c554a36b45f3f2ff25b794327e"} -{"@timestamp":"2026-06-25T01:27:35.425+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.6ms)","duration":"2024.6ms","level":"slow","span":"269888ecf16be09d","trace":"2db6412961e9547f30f7e1299c0f8fd1"} -{"@timestamp":"2026-06-25T01:27:35.425+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.6ms","level":"info","span":"269888ecf16be09d","trace":"2db6412961e9547f30f7e1299c0f8fd1"} -{"@timestamp":"2026-06-25T01:27:37.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55168 - 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.3ms","level":"info","span":"aa15344ec1215c71","trace":"e4aa0c56784888f726de134b316adc4d"} -{"@timestamp":"2026-06-25T01:27:38.245+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55174 - 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":"71558b1e5a77cac3","trace":"f19b54280622087205ffc172f32f2993"} -{"@timestamp":"2026-06-25T01:27:38.247+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55172 - 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":"02401e728e88c5e3","trace":"4309c24382cdb5afe3090c1029efd36b"} -{"@timestamp":"2026-06-25T01:27:38.247+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55173 - 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":"797903433e759bbe","trace":"c29464465b550da8893bc0547cc245e4"} -{"@timestamp":"2026-06-25T01:27:38.256+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55178 - 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":"ee17522a8974f8a1","trace":"f902ed72dcc23922405f9712e85b7f1d"} -{"@timestamp":"2026-06-25T01:27:38.258+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55177 - 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":"ee67c8376d05d78d","trace":"4d34ad567495c5af96f1f952138814bd"} -{"@timestamp":"2026-06-25T01:27:38.265+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55181 - 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":"6fc0619adbe2bb8d","trace":"e492791d6e9fb773dfa51fde37a9c920"} -{"@timestamp":"2026-06-25T01:27:38.268+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55182 - 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":"90c219b4f1541aa9","trace":"ec8bb2ddd82916d1133f7855201e5140"} -{"@timestamp":"2026-06-25T01:27:39.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55184 - 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.2ms","level":"info","span":"e8878a0a111af9df","trace":"e4cd5341e7803a50da3fec32db3cf976"} -{"@timestamp":"2026-06-25T01:27:40.444+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2014.4ms)","duration":"2014.4ms","level":"slow","span":"955791968ec92398","trace":"5e4bb9a512441b42c385350d68d60cf7"} -{"@timestamp":"2026-06-25T01:27:40.444+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2014.4ms","level":"info","span":"955791968ec92398","trace":"5e4bb9a512441b42c385350d68d60cf7"} -{"@timestamp":"2026-06-25T01:27:41.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55186 - 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":"285b8ed5ef7634e7","trace":"910d59bb0a077cae838b0eea0605bfde"} -{"@timestamp":"2026-06-25T01:27:41.267+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55188 - 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":"12ac5e2f3f5dd8cb","trace":"93fa4064663700f2577f239016fc0636"} -{"@timestamp":"2026-06-25T01:27:43.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.1ms","level":"info","span":"b6d10e772e087f76","trace":"51bf66b79767491acc08271b9304a9b5"} -{"@timestamp":"2026-06-25T01:27:44.266+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 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":"0.9ms","level":"info","span":"338fd64e7d49bfc2","trace":"9a78132873a96409a06f6ff21c858e63"} -{"@timestamp":"2026-06-25T01:27:45.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.5ms","level":"info","span":"a8344862dabeac11","trace":"dbc348829e08196ccc77908bda59b0e7"} -{"@timestamp":"2026-06-25T01:27:45.477+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2028.4ms)","duration":"2028.4ms","level":"slow","span":"ae79851a8984d1da","trace":"f5378328f36547b073942bba4864cac6"} -{"@timestamp":"2026-06-25T01:27:45.477+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2028.4ms","level":"info","span":"ae79851a8984d1da","trace":"f5378328f36547b073942bba4864cac6"} -{"@timestamp":"2026-06-25T01:27:47.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55205 - 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":"335fbd55f674218f","trace":"5011aa33e3be8ecebd474f3df06a3a35"} -{"@timestamp":"2026-06-25T01:27:47.239+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55211 - 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":"f831d39d9de5287c","trace":"a270cd59114b3154a0836bb1035b3847"} -{"@timestamp":"2026-06-25T01:27:47.241+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55209 - 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":"8056102eee14c626","trace":"7eb72c3e1185821ec0e6ac9959bc056c"} -{"@timestamp":"2026-06-25T01:27:47.241+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 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":"4.1ms","level":"info","span":"fa18ec2e31876046","trace":"7fa367a69f03cb953ac9fca9e8a5f8e0"} -{"@timestamp":"2026-06-25T01:27:47.245+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55213 - 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":"ecb5e2459eea1871","trace":"ec2f8655c70299692af07db6b6c11607"} -{"@timestamp":"2026-06-25T01:27:47.246+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55215 - 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":"13dd868bcf026ed3","trace":"975e812a87a07f6917a73e5469be6860"} -{"@timestamp":"2026-06-25T01:27:47.253+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 127.0.0.1:55217 - 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":"f61358bda3e84ba2","trace":"2fbbf7e0fdf8eaa45ec44002b79ab66f"} -{"@timestamp":"2026-06-25T01:27:48.564+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-jobs - 127.0.0.1:55219 - 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":"8.8ms","level":"info","span":"c346cd46f6b26a4a","trace":"612550cc3bc73286c887036b9d37752e"} -{"@timestamp":"2026-06-25T01:27:48.571+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13944a12013022054a44 - 127.0.0.1:55221 - 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":"f3350476de4d7fe5","trace":"2864e6a311a752494d0779a54e73a39c"} -{"@timestamp":"2026-06-25T01:27:48.574+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13944a12013022054a44 - 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.0ms","level":"info","span":"934579935ab71c7d","trace":"7b0e25f69935f931a0cb99edf8f38287"} -{"@timestamp":"2026-06-25T01:27:49.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55225 - 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":"f2d92227d8ec5692","trace":"51ab33eeeb4444dbcd69daf458acc733"} -{"@timestamp":"2026-06-25T01:27:50.497+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2019.6ms)","duration":"2019.6ms","level":"slow","span":"e0beb2c33967ffae","trace":"be9323c69f986d29e9967a4073e9ae21"} -{"@timestamp":"2026-06-25T01:27:50.497+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2019.6ms","level":"info","span":"e0beb2c33967ffae","trace":"be9323c69f986d29e9967a4073e9ae21"} -{"@timestamp":"2026-06-25T01:27:51.090+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55241 - 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":"ad1afa43968ba141","trace":"066cefe916f005ff395acc0f0a59e5ef"} -{"@timestamp":"2026-06-25T01:27:51.090+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 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":"2.3ms","level":"info","span":"e84a52219004d6ae","trace":"a802af2ff7a15f71ff8a684566b18b45"} -{"@timestamp":"2026-06-25T01:27:51.091+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55242 - 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":"f42af23660f8cf8f","trace":"cc1d7f11a9273cae7804fdf7af27af0e"} -{"@timestamp":"2026-06-25T01:27:51.091+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.6ms","level":"info","span":"66bd0ff5387eaa74","trace":"b4b38515c8553f96e0972f089fed1fcf"} -{"@timestamp":"2026-06-25T01:27:51.096+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55246 - 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":"abe59c342d1cd34b","trace":"1e28467e8f6f31fe8de3e2a2508a8555"} -{"@timestamp":"2026-06-25T01:27:51.097+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 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":"3.6ms","level":"info","span":"5d1d47b4ca08cdf9","trace":"041c53fb5898133286a633c583f4ae8d"} -{"@timestamp":"2026-06-25T01:27:51.100+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55249 - 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":"af7331a1829996c6","trace":"c39c65db01fa48fda58685c3b1667bcc"} -{"@timestamp":"2026-06-25T01:27:51.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 127.0.0.1:55251 - 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":"35d1bb747f59fdc8","trace":"60f42c176ed96e5a46175b3f987c95cb"} -{"@timestamp":"2026-06-25T01:27:53.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"5.1ms","level":"info","span":"2aab296b69c503c4","trace":"461db3af82e2846410ba394ced46f939"} -{"@timestamp":"2026-06-25T01:27:54.104+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55256 - 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":"c9d76d3399853582","trace":"62889cd30bd41a714e4010fcc6e4483e"} -{"@timestamp":"2026-06-25T01:27:54.665+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=3.8Mi, TotalAlloc=123.2Mi, Sys=23.9Mi, NumGC=64","level":"stat"} -{"@timestamp":"2026-06-25T01:27:54.724+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 114, pass: 114, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:27:55.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55258 - 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.2ms","level":"info","span":"1453abe6ab378043","trace":"97cd9ee0d4d57d2b289be3d3d8f4eabe"} -{"@timestamp":"2026-06-25T01:27:55.253+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 1.9/s, drops: 0, avg time: 200.3ms, med: 3.5ms, 90th: 2001.3ms, 99th: 2053.9ms, 99.9th: 2053.9ms","level":"stat"} -{"@timestamp":"2026-06-25T01:27:55.531+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2031.2ms)","duration":"2031.2ms","level":"slow","span":"00ce4756a953409e","trace":"094ddd0467a7991abdbf1b61c04ae757"} -{"@timestamp":"2026-06-25T01:27:55.531+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2031.2ms","level":"info","span":"00ce4756a953409e","trace":"094ddd0467a7991abdbf1b61c04ae757"} -{"@timestamp":"2026-06-25T01:27:57.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55260 - 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":"707af18b0e7d8fbc","trace":"1faef3944aeccef5ae92a70defdaab78"} -{"@timestamp":"2026-06-25T01:27:57.103+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 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":"1.9ms","level":"info","span":"6a33707eeee920a7","trace":"323e6410c6c56603505d080d2e91a690"} -{"@timestamp":"2026-06-25T01:27:59.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55264 - 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.2ms","level":"info","span":"c32b83640b17b2f3","trace":"8cf3509d7c6deb6fc87537eace6be0d6"} -{"@timestamp":"2026-06-25T01:27:59.419+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/jobs/6a3c13944a12013022054a44/cancel - 127.0.0.1:55266 - 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.2ms","level":"info","span":"5ee58eed86f9b587","trace":"ebacc6087be9703c2c778e806554ee6a"} -{"@timestamp":"2026-06-25T01:27:59.430+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55268 - 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.2ms","level":"info","span":"d0f569bd92ba4f94","trace":"ba9d2cdffe930f5d668c4d72b8c5648b"} -{"@timestamp":"2026-06-25T01:28:00.105+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55270 - 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":"8bc775d6e4be7345","trace":"2432e6b31d2c88daac88b6112c3bcb1f"} -{"@timestamp":"2026-06-25T01:28:00.562+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2028.6ms)","duration":"2028.6ms","level":"slow","span":"26365f66391ca9e0","trace":"89a3eee9eb9319143a2b8b69667f5665"} -{"@timestamp":"2026-06-25T01:28:00.562+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2028.6ms","level":"info","span":"26365f66391ca9e0","trace":"89a3eee9eb9319143a2b8b69667f5665"} -{"@timestamp":"2026-06-25T01:28:01.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"6.0ms","level":"info","span":"567108905210ad23","trace":"1f0a6d700f3c6e8f7fca3d05af448ee2"} -{"@timestamp":"2026-06-25T01:28:03.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.2ms","level":"info","span":"725714606df3ef50","trace":"d1ba54f93299e5f6f8f878595c4ec8e9"} -{"@timestamp":"2026-06-25T01:28:03.102+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13764a12013022054a41 - 127.0.0.1:55276 - 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":"b46113a96d8d7482","trace":"17b1b2667f94911d2528ea87c7fe731b"} -{"@timestamp":"2026-06-25T01:28:05.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.0ms","level":"info","span":"24bc8e05d3f2a6f5","trace":"42a71fbf44bdc241aa8518c077d34287"} -{"@timestamp":"2026-06-25T01:28:05.120+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":"2.0ms","level":"info","span":"6306963471e6f1cc","trace":"d6e8bbff13dadd67802b6c68068893c1"} -{"@timestamp":"2026-06-25T01:28:05.120+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55284 - 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":"d562b73a39f6243d","trace":"23ddda6af74d498e6836359afaa35c14"} -{"@timestamp":"2026-06-25T01:28:05.120+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55283 - 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":"e7fbc0ccc85bf62c","trace":"34ee5e831a7252ca4b3351396c5f969e"} -{"@timestamp":"2026-06-25T01:28:05.126+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 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.0ms","level":"info","span":"e84024bd0f75be59","trace":"0f51c5cb13b6b2d0fd3512b56fc5eff5"} -{"@timestamp":"2026-06-25T01:28:05.126+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55288 - 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":"10c0165c58b4192f","trace":"907dfc167f09efd8abc233c9349149e9"} -{"@timestamp":"2026-06-25T01:28:05.131+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/knowledge-graph - 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":"2.2ms","level":"info","span":"4bd161a184d4437f","trace":"0f50e7bc7e4e0d7bead29ae0b2662f9b"} -{"@timestamp":"2026-06-25T01:28:05.600+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2035.4ms)","duration":"2035.4ms","level":"slow","span":"f5b9be0d036137bd","trace":"a34bff19f468683e20a12e2a8d376cb8"} -{"@timestamp":"2026-06-25T01:28:05.600+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2035.4ms","level":"info","span":"f5b9be0d036137bd","trace":"a34bff19f468683e20a12e2a8d376cb8"} -{"@timestamp":"2026-06-25T01:28:07.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55293 - 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":"00e144570ae8f6d5","trace":"03d1c5d6977f9c723f7ebbe82d03519a"} -{"@timestamp":"2026-06-25T01:28:09.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.2ms","level":"info","span":"397fa2b025799906","trace":"4e44041c3f0b734521f3d0b674d779de"} -{"@timestamp":"2026-06-25T01:28:10.630+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.3ms)","duration":"2027.3ms","level":"slow","span":"6bd9b15b10df8f8c","trace":"ade845ae7d3ad15f2ff728cca8e2fe46"} -{"@timestamp":"2026-06-25T01:28:10.630+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.3ms","level":"info","span":"6bd9b15b10df8f8c","trace":"ade845ae7d3ad15f2ff728cca8e2fe46"} -{"@timestamp":"2026-06-25T01:28:11.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55305 - 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.2ms","level":"info","span":"772cb47148dcb5c2","trace":"462d1b404a1dfa3f12a1ba015067600d"} -{"@timestamp":"2026-06-25T01:28:13.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55307 - 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":"b23f534b9b94ebe9","trace":"bf85648dfbd84393ee9ea37d2cc3b7a4"} -{"@timestamp":"2026-06-25T01:28:15.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.4ms","level":"info","span":"6c0906ccba69c589","trace":"439577ea3035520a2d4f7ec3b309e670"} -{"@timestamp":"2026-06-25T01:28:15.657+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.0ms)","duration":"2024.0ms","level":"slow","span":"bee357f6e89be313","trace":"abfbf2b4715aa3eab2a89749e942fdba"} -{"@timestamp":"2026-06-25T01:28:15.657+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.0ms","level":"info","span":"bee357f6e89be313","trace":"abfbf2b4715aa3eab2a89749e942fdba"} -{"@timestamp":"2026-06-25T01:28:17.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55322 - 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":"a2638e640d959f20","trace":"893362778c6198af5a521650b2c67dab"} -{"@timestamp":"2026-06-25T01:28:19.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.4ms","level":"info","span":"57b491f2c3590347","trace":"4d38115dff4ddc35cee43530a8fd7c4a"} -{"@timestamp":"2026-06-25T01:28:20.677+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2016.1ms)","duration":"2016.1ms","level":"slow","span":"9b55627f24f13b7e","trace":"d0e9afcac9795cfe7a52de1a9df99ecf"} -{"@timestamp":"2026-06-25T01:28:20.677+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2016.1ms","level":"info","span":"9b55627f24f13b7e","trace":"d0e9afcac9795cfe7a52de1a9df99ecf"} -{"@timestamp":"2026-06-25T01:28:20.791+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - PATCH /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 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":"4.3ms","level":"info","span":"7fae40832b6e4d21","trace":"192560c875dd6acd080da73985e1f74c"} -{"@timestamp":"2026-06-25T01:28:21.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55328 - 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":"430c86e7cd773de4","trace":"a6c3831a80bfd47d97361623628bba64"} -{"@timestamp":"2026-06-25T01:28:23.074+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55332 - 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":"64e1ea1314cc0ebd","trace":"8d62808813e2d524e290b0a024a5f0ec"} -{"@timestamp":"2026-06-25T01:28:23.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-jobs - 127.0.0.1:55330 - 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.1ms","level":"info","span":"8898945af691cacf","trace":"31332e936b7a74b8ee1e586e035bb5d5"} -{"@timestamp":"2026-06-25T01:28:23.082+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 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.9ms","level":"info","span":"b0cad92d8249b4e1","trace":"ad4a135393b262609aec376e0b75ba19"} -{"@timestamp":"2026-06-25T01:28:23.086+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 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":"0.8ms","level":"info","span":"d99b1baa01feaff2","trace":"9effb0e563d0e4024e1cccaad076f283"} -{"@timestamp":"2026-06-25T01:28:25.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.9ms","level":"info","span":"390acce38a1ccc3b","trace":"d067f8c3551386a993f2b5107ce392e0"} -{"@timestamp":"2026-06-25T01:28:25.693+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2013.4ms)","duration":"2013.4ms","level":"slow","span":"4b37eed9f5344750","trace":"f382621ad6ce0d3a73e8949bf26285b6"} -{"@timestamp":"2026-06-25T01:28:25.693+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2013.4ms","level":"info","span":"4b37eed9f5344750","trace":"f382621ad6ce0d3a73e8949bf26285b6"} -{"@timestamp":"2026-06-25T01:28:26.090+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 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.3ms","level":"info","span":"385a3293dc556eab","trace":"58d91f13fa34d653072f6a580674f7a1"} -{"@timestamp":"2026-06-25T01:28:27.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"6.7ms","level":"info","span":"ee8946daa6d4979b","trace":"c4661b5829c5ea2ac6cc5890a5d8210b"} -{"@timestamp":"2026-06-25T01:28:29.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55369 - 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":"0c16beab81b391de","trace":"c71872964197afaa86dc208ff3b805c9"} -{"@timestamp":"2026-06-25T01:28:29.088+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 127.0.0.1:55371 - 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":"f7eae6db8a157af6","trace":"eea9f939d960653cb860051f3d8686d1"} -{"@timestamp":"2026-06-25T01:28:30.702+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2006.3ms)","duration":"2006.3ms","level":"slow","span":"9136b12e0ac05114","trace":"22d6852c1ded06aa1531520a2ffeb89a"} -{"@timestamp":"2026-06-25T01:28:30.702+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2006.3ms","level":"info","span":"9136b12e0ac05114","trace":"22d6852c1ded06aa1531520a2ffeb89a"} -{"@timestamp":"2026-06-25T01:28:31.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55374 - 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":"8db643bb9625422a","trace":"7723576cab7cea9a41207c9b2e08a246"} -{"@timestamp":"2026-06-25T01:28:31.810+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55382 - 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":"4739b811dab38f32","trace":"adb3d15e90b8846ab4b16cbebe6c6795"} -{"@timestamp":"2026-06-25T01:28:31.810+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 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":"2.3ms","level":"info","span":"56364a5de97927ac","trace":"eb8936d2b5eef5b228ef2386873c596b"} -{"@timestamp":"2026-06-25T01:28:31.811+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55381 - 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":"57145aab986ef008","trace":"09005a941944fed9e9919c02ef663cf9"} -{"@timestamp":"2026-06-25T01:28:31.817+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55386 - 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":"c9ef78746d85672a","trace":"8d4b03b5e0a9673d3aaf551667900a46"} -{"@timestamp":"2026-06-25T01:28:31.818+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55385 - 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":"156e6f93d9b028dc","trace":"8008a5341be62b6caf091f3b29739e55"} -{"@timestamp":"2026-06-25T01:28:31.822+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/knowledge-graph - 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.7ms","level":"info","span":"0d9b3be83864fff2","trace":"d6559680c392a3d4c828c5eabb26c580"} -{"@timestamp":"2026-06-25T01:28:33.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55390 - 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":"7c2fe2faca2368f4","trace":"4e7868f47cdc329c05caefa8abd2c017"} -{"@timestamp":"2026-06-25T01:28:35.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.3ms","level":"info","span":"09d08b76d78e8ecf","trace":"409a239c4e6caac91a2b894cd4d4af52"} -{"@timestamp":"2026-06-25T01:28:35.326+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 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":"1.7ms","level":"info","span":"daee769ec34fe49c","trace":"56b647662d8abaa0a8d06bf79570a359"} -{"@timestamp":"2026-06-25T01:28:35.328+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55396 - 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":"70b6c94e29a0b56d","trace":"06f9ee7bae2d10189bef61d2cb6a14ab"} -{"@timestamp":"2026-06-25T01:28:35.329+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55398 - 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":"c4b11c205bb55246","trace":"7b78263d6b8fc2bb41365fd65edfbabc"} -{"@timestamp":"2026-06-25T01:28:35.331+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55400 - 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":"3c772d3a472516e6","trace":"e79d1f8de5734ec044774163531b4a7c"} -{"@timestamp":"2026-06-25T01:28:35.331+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 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":"0.9ms","level":"info","span":"c6724d1f8af74cec","trace":"2e3ee9e1274acc7c5aede64d5e55143d"} -{"@timestamp":"2026-06-25T01:28:35.337+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 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":"2.3ms","level":"info","span":"768c7a49e0acc74b","trace":"3a4b2923cade60d60131ac7e831a8a76"} -{"@timestamp":"2026-06-25T01:28:35.341+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55408 - 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":"e8054ccbd40b695c","trace":"f9611cb9dc03a9f4f8c3aa789d24167d"} -{"@timestamp":"2026-06-25T01:28:35.342+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 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":"2.6ms","level":"info","span":"ae0baf79a961c29d","trace":"25963a8b4f761557ee628ff332e6b619"} -{"@timestamp":"2026-06-25T01:28:35.347+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55410 - 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.0ms","level":"info","span":"f42efb4df46fff05","trace":"a940f20db6a565ff2fc42fddce405d7d"} -{"@timestamp":"2026-06-25T01:28:35.752+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2047.6ms)","duration":"2047.6ms","level":"slow","span":"2700f5da57c083a7","trace":"60e30118a320f1fb29b949c4c44b5302"} -{"@timestamp":"2026-06-25T01:28:35.752+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2047.6ms","level":"info","span":"2700f5da57c083a7","trace":"60e30118a320f1fb29b949c4c44b5302"} -{"@timestamp":"2026-06-25T01:28:37.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55412 - 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":"04f30a1daf95f117","trace":"61464ae84ca324da4e6b5acab82da81d"} -{"@timestamp":"2026-06-25T01:28:39.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55414 - 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":"eb725d4a9c6754cc","trace":"ff2b82de787205dfb39daeb58a6709d1"} -{"@timestamp":"2026-06-25T01:28:40.775+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2019.5ms)","duration":"2019.5ms","level":"slow","span":"190822cd72c9e8d2","trace":"890b16b899d5bedec332635aa8a5fdf7"} -{"@timestamp":"2026-06-25T01:28:40.775+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2019.5ms","level":"info","span":"190822cd72c9e8d2","trace":"890b16b899d5bedec332635aa8a5fdf7"} -{"@timestamp":"2026-06-25T01:28:41.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55416 - 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.5ms","level":"info","span":"5d4e9050972c4da4","trace":"4a6e19446eab15973de97e972b8213af"} -{"@timestamp":"2026-06-25T01:28:43.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55419 - 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":"9d7119356d7dbec0","trace":"56b590b791782dcdb3b77b8512f9f862"} -{"@timestamp":"2026-06-25T01:28:43.926+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 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":"2.2ms","level":"info","span":"02307ab7d0ae783f","trace":"b37e3e5e30d86b606dad323b7f35fd90"} -{"@timestamp":"2026-06-25T01:28:43.927+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55423 - 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":"c484314dd70c563e","trace":"30f366ca240f8024f304a1fd3de3a6c7"} -{"@timestamp":"2026-06-25T01:28:43.933+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55425 - 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":"aaeacecfb6054670","trace":"f92202990fddb81f99bd3cef161e1146"} -{"@timestamp":"2026-06-25T01:28:45.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55427 - 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.6ms","level":"info","span":"19941dac7162ff13","trace":"ac3a3496c2e3965115132d7b8967bef1"} -{"@timestamp":"2026-06-25T01:28:45.807+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2030.1ms)","duration":"2030.1ms","level":"slow","span":"e5c864e543b341f9","trace":"d66c2cd1c4219c0ea7f99a0fc29057ef"} -{"@timestamp":"2026-06-25T01:28:45.807+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2030.1ms","level":"info","span":"e5c864e543b341f9","trace":"d66c2cd1c4219c0ea7f99a0fc29057ef"} -{"@timestamp":"2026-06-25T01:28:46.937+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 127.0.0.1:55430 - 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":"16145dd0f5bb89e5","trace":"7079ad4ce92a99a75b9776fbcc250d80"} -{"@timestamp":"2026-06-25T01:28:46.945+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55432 - 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":"991ccd3967b4e0a6","trace":"9f275f5c29aaab4ce32c022382fa818d"} -{"@timestamp":"2026-06-25T01:28:47.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55440 - 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":"8740e95eded657c0","trace":"6fa4bc1bd5a1065e638399747d303487"} -{"@timestamp":"2026-06-25T01:28:49.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55443 - 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":"d9d5709f4a613d62","trace":"bff3cb4a5d2546ecf4d1710f054573af"} -{"@timestamp":"2026-06-25T01:28:49.937+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs/6a3c13b74a12013022054a48 - 127.0.0.1:55453 - 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":"4b31306045854c2d","trace":"222cf15605ffcbaca99c6b6a9b8ca541"} -{"@timestamp":"2026-06-25T01:28:49.943+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55455 - 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":"605c4f7d82875c33","trace":"68b8a4dc8ce608ed334908a12d58f62d"} -{"@timestamp":"2026-06-25T01:28:50.833+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.1ms)","duration":"2024.1ms","level":"slow","span":"fbc75392cbf1d866","trace":"89409d58db9fc5bddab71cc664547605"} -{"@timestamp":"2026-06-25T01:28:50.833+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.1ms","level":"info","span":"fbc75392cbf1d866","trace":"89409d58db9fc5bddab71cc664547605"} -{"@timestamp":"2026-06-25T01:28:51.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55457 - 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":"5f307cff35581c80","trace":"7843e9766a9e1b57b0e059345cf72e90"} -{"@timestamp":"2026-06-25T01:28:52.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55463 - 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":"5ccb6fa4a5d8eaf8","trace":"dfff956a679ff6981ce71bea7a42bb53"} -{"@timestamp":"2026-06-25T01:28:52.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55461 - 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":"83867e7552e07054","trace":"2da7e1bccebf984d01df173999e0f254"} -{"@timestamp":"2026-06-25T01:28:52.886+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55462 - 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":"3e8c7808bace2ee9","trace":"96e1996f17ebc2829175c719d5f5c07e"} -{"@timestamp":"2026-06-25T01:28:53.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55465 - 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":"3333104e08034ed7","trace":"0741ff38da2f191c3580103f7e9dac51"} -{"@timestamp":"2026-06-25T01:28:54.666+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=4.0Mi, TotalAlloc=138.5Mi, Sys=23.9Mi, NumGC=71","level":"stat"} -{"@timestamp":"2026-06-25T01:28:54.723+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 84, pass: 84, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:28:55.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55472 - 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":"1232445367635314","trace":"480e4f9d3b0359abbcf187aa98e96e64"} -{"@timestamp":"2026-06-25T01:28:55.254+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 1.4/s, drops: 0, avg time: 292.2ms, med: 3.8ms, 90th: 2024.0ms, 99th: 2047.5ms, 99.9th: 2047.5ms","level":"stat"} -{"@timestamp":"2026-06-25T01:28:55.842+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2005.5ms)","duration":"2005.5ms","level":"slow","span":"fdd7a1233f3bf381","trace":"145a2523361fdb99269746e12c6bf90b"} -{"@timestamp":"2026-06-25T01:28:55.842+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2005.5ms","level":"info","span":"fdd7a1233f3bf381","trace":"145a2523361fdb99269746e12c6bf90b"} -{"@timestamp":"2026-06-25T01:28:57.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55474 - 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":"5773d92dcfb579e5","trace":"a82e02df6e754de284b1dc7841944c98"} -{"@timestamp":"2026-06-25T01:28:59.311+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55476 - 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":"2358205347b757e7","trace":"ab415a1b51b06335d6bb0c0778b1205e"} -{"@timestamp":"2026-06-25T01:29:00.893+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2048.0ms)","duration":"2048.0ms","level":"slow","span":"719c198533b5dd01","trace":"7525f21561763c0cd7ae68f5db9d7fe7"} -{"@timestamp":"2026-06-25T01:29:00.893+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2048.0ms","level":"info","span":"719c198533b5dd01","trace":"7525f21561763c0cd7ae68f5db9d7fe7"} -{"@timestamp":"2026-06-25T01:29:01.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55478 - 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":"f586880774480961","trace":"1996337f6c1782add2bd1b4270a16bc3"} -{"@timestamp":"2026-06-25T01:29:03.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55480 - 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":"6663808c8d70423e","trace":"c84bc28caaa803a0d30608578b6e63d1"} -{"@timestamp":"2026-06-25T01:29:05.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55482 - 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.2ms","level":"info","span":"bad1a5ad3480ad2a","trace":"a60bf918385878e2af78a0d660ad2108"} -{"@timestamp":"2026-06-25T01:29:05.543+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/outreach-drafts/generate - 127.0.0.1:55469 - 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 - slowcall(10807.2ms)","duration":"10807.2ms","level":"slow","span":"636d94cc0028bc07","trace":"82d46dc5cee25e60a12b3613742456a7"} -{"@timestamp":"2026-06-25T01:29:05.543+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/outreach-drafts/generate - 127.0.0.1:55469 - 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":"10807.2ms","level":"info","span":"636d94cc0028bc07","trace":"82d46dc5cee25e60a12b3613742456a7"} -{"@timestamp":"2026-06-25T01:29:05.923+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.3ms)","duration":"2027.3ms","level":"slow","span":"ec8b5a15a9172687","trace":"30648f9322d73c0fe4fbe7e2d35b9d3c"} -{"@timestamp":"2026-06-25T01:29:05.923+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.3ms","level":"info","span":"ec8b5a15a9172687","trace":"30648f9322d73c0fe4fbe7e2d35b9d3c"} -{"@timestamp":"2026-06-25T01:29:07.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55485 - 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":"7b04f4c9bdafc187","trace":"dd3b8dd2419f9773b16ecfa86f2573d4"} -{"@timestamp":"2026-06-25T01:29:09.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55488 - 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.3ms","level":"info","span":"58018515bcd684f2","trace":"602aad171e30e9ace5e804a1d68b779c"} -{"@timestamp":"2026-06-25T01:29:10.389+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - PATCH /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts/e5b4be3f-92b4-45d2-9209-f95d8aee6f77 - 127.0.0.1:55491 - 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.0ms","level":"info","span":"2371edf2d97996b9","trace":"856f092b510caec1dc055fe67c75d182"} -{"@timestamp":"2026-06-25T01:29:10.947+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2021.0ms)","duration":"2021.0ms","level":"slow","span":"992833801ee9b17e","trace":"01dee08416da34c542f1b7b35490e4b7"} -{"@timestamp":"2026-06-25T01:29:10.947+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2021.0ms","level":"info","span":"992833801ee9b17e","trace":"01dee08416da34c542f1b7b35490e4b7"} -{"@timestamp":"2026-06-25T01:29:11.073+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"2.6ms","level":"info","span":"6e400262475500d2","trace":"940f2a82b697d359897a97faaa0b4916"} -{"@timestamp":"2026-06-25T01:29:13.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55502 - 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":"ad1e55f2f4e39a5d","trace":"148523b049ebfc52d868529a3802ce87"} -{"@timestamp":"2026-06-25T01:29:15.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.3ms","level":"info","span":"dd5fb118d96f34db","trace":"8b1fab17ca8baaaeaaa21ed552605b80"} -{"@timestamp":"2026-06-25T01:29:15.972+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2021.9ms)","duration":"2021.9ms","level":"slow","span":"a410189f6efd9ef0","trace":"83c1b21f3e6534dc35f95a67f1fb77e7"} -{"@timestamp":"2026-06-25T01:29:15.972+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2021.9ms","level":"info","span":"a410189f6efd9ef0","trace":"83c1b21f3e6534dc35f95a67f1fb77e7"} -{"@timestamp":"2026-06-25T01:29:17.075+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.2ms","level":"info","span":"2a7926e71282e534","trace":"aa1de5ad62ba9ca7f311474291462890"} -{"@timestamp":"2026-06-25T01:29:19.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.8ms","level":"info","span":"e1057d867dbe358d","trace":"50b328d7317905e5e5158e9a8bfe3a16"} -{"@timestamp":"2026-06-25T01:29:21.002+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2026.6ms)","duration":"2026.6ms","level":"slow","span":"83fc6b746bd5ef7c","trace":"15ebdd540e877529d013f3f3efaa1bf1"} -{"@timestamp":"2026-06-25T01:29:21.002+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2026.6ms","level":"info","span":"83fc6b746bd5ef7c","trace":"15ebdd540e877529d013f3f3efaa1bf1"} -{"@timestamp":"2026-06-25T01:29:21.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.2ms","level":"info","span":"68a1a62363036fee","trace":"b3d988e00354ec91326547fb7a4b0a53"} -{"@timestamp":"2026-06-25T01:29:23.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.8ms","level":"info","span":"6cad8863ed92f05b","trace":"ba99cc6f395bf9fecd9ee1524db7b856"} -{"@timestamp":"2026-06-25T01:29:25.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.6ms","level":"info","span":"1be2316081636685","trace":"1c650afeca70d0362fd4b374466f21b7"} -{"@timestamp":"2026-06-25T01:29:26.013+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2007.9ms)","duration":"2007.9ms","level":"slow","span":"d48a790f93be26a3","trace":"637e3234b37d0fe264e15c8f8f73a766"} -{"@timestamp":"2026-06-25T01:29:26.013+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2007.9ms","level":"info","span":"d48a790f93be26a3","trace":"637e3234b37d0fe264e15c8f8f73a766"} -{"@timestamp":"2026-06-25T01:29:27.079+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":"6.3ms","level":"info","span":"83de1da3dd78b3d5","trace":"179e3a33b9a43e8cc38f245f95bcdc64"} -{"@timestamp":"2026-06-25T01:29:29.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55519 - 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":"9519832dc513dad6","trace":"50416e87270ccda6ebebeee2afb47f6f"} -{"@timestamp":"2026-06-25T01:29:31.065+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2048.7ms)","duration":"2048.7ms","level":"slow","span":"e4b779c0734be99b","trace":"392b1c398f40f1b7adb23e62b898d630"} -{"@timestamp":"2026-06-25T01:29:31.065+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2048.7ms","level":"info","span":"e4b779c0734be99b","trace":"392b1c398f40f1b7adb23e62b898d630"} -{"@timestamp":"2026-06-25T01:29:31.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.7ms","level":"info","span":"13d53f8da45ac8f9","trace":"81a3e6dd652884a5ebd529a7ccf6a0ea"} -{"@timestamp":"2026-06-25T01:29:33.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.9ms","level":"info","span":"cdc645c2fb71dec9","trace":"d4b57c448d0a386a1852259c7b966926"} -{"@timestamp":"2026-06-25T01:29:35.080+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"5.1ms","level":"info","span":"2fc8909386f115a5","trace":"fe77a69982214f859b21d2ab2897b0bf"} -{"@timestamp":"2026-06-25T01:29:36.092+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.0ms)","duration":"2024.0ms","level":"slow","span":"eb1903a5823bb7df","trace":"caf7a99ce1806bf4b0e68518d36fccd5"} -{"@timestamp":"2026-06-25T01:29:36.092+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.0ms","level":"info","span":"eb1903a5823bb7df","trace":"caf7a99ce1806bf4b0e68518d36fccd5"} -{"@timestamp":"2026-06-25T01:29:37.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"f35007be548da702","trace":"c75bfb0ffd888da82d11630084b52842"} -{"@timestamp":"2026-06-25T01:29:39.081+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55531 - 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.5ms","level":"info","span":"45782989eeef142e","trace":"40b35391577009d0d4d0a27f89817df9"} -{"@timestamp":"2026-06-25T01:29:40.402+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":"2.4ms","level":"info","span":"f888d6ac29ec0393","trace":"56be57fb1a7edd9c9bdfb576deaa8a26"} -{"@timestamp":"2026-06-25T01:29:40.406+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 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.7ms","level":"info","span":"1045c262f1e8bb65","trace":"f3cb4b758f6ea7957e2e87169d626ed8"} -{"@timestamp":"2026-06-25T01:29:40.409+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55537 - 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":"f215eab8e54e6543","trace":"442c2400c79fcb210c008eec43ace26c"} -{"@timestamp":"2026-06-25T01:29:40.411+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 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":"3.1ms","level":"info","span":"e90d36cc9bd9b1aa","trace":"c149e4c4a20804151170fbaf91c09e67"} -{"@timestamp":"2026-06-25T01:29:41.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"5.3ms","level":"info","span":"b35df46f83eee1ce","trace":"d8e001fa356e12505d6b0236319285ae"} -{"@timestamp":"2026-06-25T01:29:41.121+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.5ms)","duration":"2025.5ms","level":"slow","span":"ad21582d2a105668","trace":"790061d9123c944a4680728cc00ea8e6"} -{"@timestamp":"2026-06-25T01:29:41.121+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.5ms","level":"info","span":"ad21582d2a105668","trace":"790061d9123c944a4680728cc00ea8e6"} -{"@timestamp":"2026-06-25T01:29:41.935+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/c84e9133-6c97-4238-bc82-95509489ed67 - 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":"4.0ms","level":"info","span":"00fef661a9c65248","trace":"eba82cd92781d0995e6d683854609cf0"} -{"@timestamp":"2026-06-25T01:29:41.935+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 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.1ms","level":"info","span":"030672099577f591","trace":"26e33cba07199cefe1e9f21c280f0ad8"} -{"@timestamp":"2026-06-25T01:29:41.939+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=persona&scope_id=c84e9133-6c97-4238-bc82-95509489ed67 - 127.0.0.1:55549 - 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.8ms","level":"info","span":"779393dda7e0ea28","trace":"fefd16760d83052271ca149662bfff00"} -{"@timestamp":"2026-06-25T01:29:41.940+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas/c84e9133-6c97-4238-bc82-95509489ed67 - 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":"1.4ms","level":"info","span":"6f295a23a6a92f4a","trace":"326b0454753941a08aca2d96e1bdf821"} -{"@timestamp":"2026-06-25T01:29:41.942+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 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.7ms","level":"info","span":"424214708c4fde58","trace":"7df2ed0be473809b7a7427327ca79991"} -{"@timestamp":"2026-06-25T01:29:41.943+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=persona&scope_id=c84e9133-6c97-4238-bc82-95509489ed67 - 127.0.0.1:55552 - 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":"facbf9f7a6a2dcf5","trace":"d6da859bfd02610497abedbc9b588bd1"} -{"@timestamp":"2026-06-25T01:29:43.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"3.6ms","level":"info","span":"a071198ccaafa0b6","trace":"ebf90f6dfe4c47d1bc7e977b65cc2322"} -{"@timestamp":"2026-06-25T01:29:45.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 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":"4.9ms","level":"info","span":"a9d8f5ecf1776965","trace":"72a2f2df19317adaff970f88f9260a6d"} -{"@timestamp":"2026-06-25T01:29:46.153+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2028.0ms)","duration":"2028.0ms","level":"slow","span":"fdb6142bdc50c88a","trace":"c80f49a427e486c421494f513140543b"} -{"@timestamp":"2026-06-25T01:29:46.153+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2028.0ms","level":"info","span":"fdb6142bdc50c88a","trace":"c80f49a427e486c421494f513140543b"} -{"@timestamp":"2026-06-25T01:29:47.076+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":"4.5ms","level":"info","span":"49bab55a6dcac042","trace":"f88c7a4e86260f8ef5cc98d6ce4f67eb"} -{"@timestamp":"2026-06-25T01:29:49.075+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":"2.6ms","level":"info","span":"5c908f0407850c86","trace":"60374db73b3c41e559bee2b4cd3e79ee"} -{"@timestamp":"2026-06-25T01:29:51.075+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.4ms","level":"info","span":"1587a01667d44efb","trace":"6194d208797ea53b181247e06f4bec2a"} -{"@timestamp":"2026-06-25T01:29:51.173+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2018.2ms)","duration":"2018.2ms","level":"slow","span":"e2278b9fdf2b0696","trace":"5c29dc677229b4c25575e1f11e1a3446"} -{"@timestamp":"2026-06-25T01:29:51.173+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2018.2ms","level":"info","span":"e2278b9fdf2b0696","trace":"5c29dc677229b4c25575e1f11e1a3446"} -{"@timestamp":"2026-06-25T01:29:53.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55567 - 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":"1c50bb52ca5242e2","trace":"b81404a07e75261bf27d166c21e8fbc9"} -{"@timestamp":"2026-06-25T01:29:54.666+08:00","caller":"stat/usage.go:82","content":"CPU: 0m, MEMORY: Alloc=4.4Mi, TotalAlloc=150.3Mi, Sys=23.9Mi, NumGC=76","level":"stat"} -{"@timestamp":"2026-06-25T01:29:54.725+08:00","caller":"load/sheddingstat.go:61","content":"(api) shedding_stat [1m], cpu: 0, total: 54, pass: 54, drop: 0","level":"stat"} -{"@timestamp":"2026-06-25T01:29:55.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55569 - 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":"e4e4a083c4d6784a","trace":"628f23aa7a8ec570fd72a9e8569ac601"} -{"@timestamp":"2026-06-25T01:29:55.255+08:00","caller":"stat/metrics.go:210","content":"(haixun-backend) - qps: 0.9/s, drops: 0, avg time: 653.1ms, med: 4.6ms, 90th: 2027.2ms, 99th: 10807.2ms, 99.9th: 10807.2ms","level":"stat"} -{"@timestamp":"2026-06-25T01:29:55.605+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55575 - 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":"ef8f319c18fd535f","trace":"2f1d5f1d740373119b94586fe96ddace"} -{"@timestamp":"2026-06-25T01:29:55.605+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55574 - 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":"d9bb3dd8c17e5119","trace":"a720792369639ffca0004db6e2f2358a"} -{"@timestamp":"2026-06-25T01:29:55.606+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55573 - 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.2ms","level":"info","span":"acf44dd417f487f5","trace":"d4d2fcf42939e763bb9283b571eb8c7d"} -{"@timestamp":"2026-06-25T01:29:55.609+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55577 - 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":"e8704641aa72e48a","trace":"f1b08e55fc718ddfb7b8be6c1cfc5a14"} -{"@timestamp":"2026-06-25T01:29:55.611+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55580 - 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":"8b075cc0d59e0b36","trace":"2ba8b877dae59325959da9337a3c28ca"} -{"@timestamp":"2026-06-25T01:29:55.612+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55581 - 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":"d6ff77f31209b06f","trace":"8b632aa49f814e15c26ff5b60a854d20"} -{"@timestamp":"2026-06-25T01:29:56.207+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2030.5ms)","duration":"2030.5ms","level":"slow","span":"e0dc28ff3c876705","trace":"ce4eb1c7dfde19e2d16d3be0d27ee02b"} -{"@timestamp":"2026-06-25T01:29:56.207+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2030.5ms","level":"info","span":"e0dc28ff3c876705","trace":"ce4eb1c7dfde19e2d16d3be0d27ee02b"} -{"@timestamp":"2026-06-25T01:29:57.080+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55583 - 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":"adbc93ded58d63c1","trace":"446811c5d6d18cb3422d74e5d2dede27"} -{"@timestamp":"2026-06-25T01:29:57.554+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55586 - 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":"75d4b5c16efa2991","trace":"a2f95a7b356a33010b286b78c0a9cfc0"} -{"@timestamp":"2026-06-25T01:29:57.555+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55587 - 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":"6a4f52af19913eec","trace":"451609d376e4113d8054f7bce1537c29"} -{"@timestamp":"2026-06-25T01:29:57.558+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55590 - 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":"012bf49db2a896cd","trace":"e4320b979eb0ce4033b5d8e5f65c43b8"} -{"@timestamp":"2026-06-25T01:29:57.560+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55593 - 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":"2f641ef8a1c7afda","trace":"de7e3af01e565863aee9b4987d921819"} -{"@timestamp":"2026-06-25T01:29:57.560+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55591 - 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":"7046422113d6aeed","trace":"a4d0e95782777daadc254fda955c6cb3"} -{"@timestamp":"2026-06-25T01:29:57.564+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/cd608c78-342d-4c83-85d6-53556ee985ff - 127.0.0.1:55597 - 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":"ee0dd6aec434d1b4","trace":"0d6c334f4e00e65dc4d0642176d2496a"} -{"@timestamp":"2026-06-25T01:29:57.567+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55598 - 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.2ms","level":"info","span":"07f0aaabf083a722","trace":"069d9b9522f536fd256da9d6a6fd2bfe"} -{"@timestamp":"2026-06-25T01:29:57.567+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55599 - 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":"28468c2302a07c87","trace":"467538c6b2d28fa0acb5d66e62321a3a"} -{"@timestamp":"2026-06-25T01:29:57.568+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55601 - 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":"28ffa91a7b3c6016","trace":"c96cd70e8859c4899511da1a2fc9d1f9"} -{"@timestamp":"2026-06-25T01:29:59.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55603 - 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":"9871ad152dbac16f","trace":"61f0c6318384345cf43086f7fa961e28"} -{"@timestamp":"2026-06-25T01:30:01.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55605 - 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":"24e48f56cc4cde10","trace":"c5874495fe95302bf289467f3144d970"} -{"@timestamp":"2026-06-25T01:30:01.234+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2023.9ms)","duration":"2023.9ms","level":"slow","span":"ff9c0e6084a6d25b","trace":"c30dda68e6b68c7c85d0e3b0365088a0"} -{"@timestamp":"2026-06-25T01:30:01.235+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2023.9ms","level":"info","span":"ff9c0e6084a6d25b","trace":"c30dda68e6b68c7c85d0e3b0365088a0"} -{"@timestamp":"2026-06-25T01:30:03.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55607 - 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.5ms","level":"info","span":"aecd72b10537381a","trace":"274664e24cf692f899a6efb30ed34144"} -{"@timestamp":"2026-06-25T01:30:05.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55609 - 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":"5190826fa8666bdd","trace":"ba4ba57222b94c4770ce68e5daa1934d"} -{"@timestamp":"2026-06-25T01:30:06.257+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2019.1ms)","duration":"2019.1ms","level":"slow","span":"60a947ee2ac59b8a","trace":"50af971a0b7af502f0951c9134da1248"} -{"@timestamp":"2026-06-25T01:30:06.257+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2019.1ms","level":"info","span":"60a947ee2ac59b8a","trace":"50af971a0b7af502f0951c9134da1248"} -{"@timestamp":"2026-06-25T01:30:07.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55611 - 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":"62dff78cf3528334","trace":"1785adf8bb6017acb5619f340fa2d2b5"} -{"@timestamp":"2026-06-25T01:30:09.080+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55613 - 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":"88e8690366d14c91","trace":"2f7264ce69e71866b9d8d50b6eeb0e8a"} -{"@timestamp":"2026-06-25T01:30:10.651+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55619 - 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":"f458d3706b748a81","trace":"43912cc7a914ee9609bac5febd18d295"} -{"@timestamp":"2026-06-25T01:30:10.652+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55617 - 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":"95e2e2b496b362d9","trace":"11b221991af0dc5c9f34989a15b4fbca"} -{"@timestamp":"2026-06-25T01:30:10.655+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55618 - 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":"8.5ms","level":"info","span":"62f4b8919c25cfdb","trace":"6ed955cbd75aa9560d12bc4158acc06f"} -{"@timestamp":"2026-06-25T01:30:11.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55621 - 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":"3108354bf4f9c077","trace":"236f61cb8696d7779b53dfffc4e75a90"} -{"@timestamp":"2026-06-25T01:30:11.285+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.5ms)","duration":"2024.5ms","level":"slow","span":"7061fbd2583dae20","trace":"e16e45130f0bb34f21ee2ab8e041f525"} -{"@timestamp":"2026-06-25T01:30:11.285+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.5ms","level":"info","span":"7061fbd2583dae20","trace":"e16e45130f0bb34f21ee2ab8e041f525"} -{"@timestamp":"2026-06-25T01:30:13.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55623 - 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":"a427aa5e4ee2a5a2","trace":"fdd824f7c2c9855f06995744af070fc8"} -{"@timestamp":"2026-06-25T01:30:15.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55625 - 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":"b93a79bf0b6a2bb7","trace":"b5267fd1ecf24098610d70701393c8b5"} -{"@timestamp":"2026-06-25T01:30:15.882+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0 - 127.0.0.1:55628 - 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.1ms","level":"info","span":"9956367f0c43eb7c","trace":"47ecfe2657f6d4ffbde0bbdcdca97b55"} -{"@timestamp":"2026-06-25T01:30:15.884+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/b36ce9c3-e7b8-40b5-a27c-6b1c9fd078d0/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55629 - 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":"9.0ms","level":"info","span":"083c4673401a0d48","trace":"726473c588230203cf4b33ba435f9158"} -{"@timestamp":"2026-06-25T01:30:15.885+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55631 - 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":"a91656d24eb31e40","trace":"34309d9c3ce1ade95e3ae118ef590397"} -{"@timestamp":"2026-06-25T01:30:16.315+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.1ms)","duration":"2027.1ms","level":"slow","span":"91aad7fcc993a4a9","trace":"4dc4441fd1626555f88792dc8d6f1254"} -{"@timestamp":"2026-06-25T01:30:16.315+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.1ms","level":"info","span":"91aad7fcc993a4a9","trace":"4dc4441fd1626555f88792dc8d6f1254"} -{"@timestamp":"2026-06-25T01:30:17.076+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55633 - 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.2ms","level":"info","span":"64d7c2479b5272e9","trace":"bafed0073f679a1e8d433a57359fc559"} -{"@timestamp":"2026-06-25T01:30:18.273+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=20&scope=placement_topic&scope_id=c6516f8a-c330-4360-8f0f-a5eaa5cfb573 - 127.0.0.1:55637 - 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":"c709f9986257141d","trace":"ac0deeb390de43f21b42d5d737453d6c"} -{"@timestamp":"2026-06-25T01:30:18.274+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55639 - 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":"433803f5d1a88a58","trace":"fc2ec749ec1deceb6b2afd2a29d9c6ff"} -{"@timestamp":"2026-06-25T01:30:18.276+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/c6516f8a-c330-4360-8f0f-a5eaa5cfb573/scan-posts?recent_7d=true&product_fit_min=70&limit=100 - 127.0.0.1:55638 - 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.4ms","level":"info","span":"4df35923cb6cdfbe","trace":"a4c16e1949dc563ee23b33ea755d9edd"} -{"@timestamp":"2026-06-25T01:30:19.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55641 - 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.3ms","level":"info","span":"3a239a2ff04259c5","trace":"ac71482c9718737c1404859cf189382f"} -{"@timestamp":"2026-06-25T01:30:21.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55643 - 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":"830be4c120d5c39c","trace":"49a878414a4714f673eb4364d99a5a01"} -{"@timestamp":"2026-06-25T01:30:21.342+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2023.8ms)","duration":"2023.8ms","level":"slow","span":"b0154b3a0a9998cb","trace":"b58da5550c3ead0b1468fc760acd8132"} -{"@timestamp":"2026-06-25T01:30:21.342+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2023.8ms","level":"info","span":"b0154b3a0a9998cb","trace":"b58da5550c3ead0b1468fc760acd8132"} -{"@timestamp":"2026-06-25T01:30:23.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55645 - 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":"c1a17f91eb804082","trace":"f2d335e2e3a4fd15e9ee5c496cb4a0f5"} -{"@timestamp":"2026-06-25T01:30:25.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55647 - 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.3ms","level":"info","span":"6559d66c117f4c82","trace":"9d4838b8dd38aaf062e312cb30ee5218"} -{"@timestamp":"2026-06-25T01:30:26.367+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2021.0ms)","duration":"2021.0ms","level":"slow","span":"1e87bcc0668cde9b","trace":"07dea0b6b396daf43898ebf95555d13e"} -{"@timestamp":"2026-06-25T01:30:26.367+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2021.0ms","level":"info","span":"1e87bcc0668cde9b","trace":"07dea0b6b396daf43898ebf95555d13e"} -{"@timestamp":"2026-06-25T01:30:26.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55652 - 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":"6089a5bb0d62f211","trace":"e4005dba27c892da3c082e2b989fa0f1"} -{"@timestamp":"2026-06-25T01:30:26.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/personas - 127.0.0.1:55653 - 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":"4e7c55dfd03d2a1f","trace":"052e4ae36cdaffb98172070c1883f1e4"} -{"@timestamp":"2026-06-25T01:30:26.423+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55651 - 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":"b85737af31debd40","trace":"f96b2d61ff277ff07b52ac19159b2986"} -{"@timestamp":"2026-06-25T01:30:26.427+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/brands/ - 127.0.0.1:55656 - 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":"b9ee94cc4dab1938","trace":"24cb25f9d5a04bacff56886beb2d838b"} -{"@timestamp":"2026-06-25T01:30:26.428+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/threads-accounts/edc2117d-e76e-4623-a4f8-d5edee8490ab/connection - 127.0.0.1:55659 - 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":"c89243b611a09892","trace":"6f8009d8571beea6f823468c0b616f92"} -{"@timestamp":"2026-06-25T01:30:26.428+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/placement/topics/ - 127.0.0.1:55658 - 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":"e311f24d4ef418e5","trace":"12c8aa94d5d0cdeb1c3e440ba3400da6"} -{"@timestamp":"2026-06-25T01:30:27.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55661 - 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":"42ced925f54c01e2","trace":"e123d10e3899e1e7e3ce60164c463618"} -{"@timestamp":"2026-06-25T01:30:29.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55663 - 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":"76e04a233fa95009","trace":"02008f88f6b4c4f13cbfb810c3ce8168"} -{"@timestamp":"2026-06-25T01:30:31.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55665 - 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":"61b659997da739b5","trace":"231696cf864bdfdaf2ae67b604ec7048"} -{"@timestamp":"2026-06-25T01:30:31.396+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2025.9ms)","duration":"2025.9ms","level":"slow","span":"0c7fff98baa4ead5","trace":"1f4979560161731655aef8300cd86d27"} -{"@timestamp":"2026-06-25T01:30:31.396+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2025.9ms","level":"info","span":"0c7fff98baa4ead5","trace":"1f4979560161731655aef8300cd86d27"} -{"@timestamp":"2026-06-25T01:30:33.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55667 - 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":"51e45021469c1dec","trace":"50e1fced62c475e1988ec998a6856ab7"} -{"@timestamp":"2026-06-25T01:30:35.079+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55671 - 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.3ms","level":"info","span":"9a61ba57939e323d","trace":"50a5504f581579e2ea6807aad84231ca"} -{"@timestamp":"2026-06-25T01:30:36.428+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2027.2ms)","duration":"2027.2ms","level":"slow","span":"4e9f62b9118ea418","trace":"6a72506b71a79000ccf190ee6d5a696f"} -{"@timestamp":"2026-06-25T01:30:36.428+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2027.2ms","level":"info","span":"4e9f62b9118ea418","trace":"6a72506b71a79000ccf190ee6d5a696f"} -{"@timestamp":"2026-06-25T01:30:37.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55673 - 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":"05c3d4cbd9ed7b2b","trace":"56207506bd25217f81872453dcfe4a55"} -{"@timestamp":"2026-06-25T01:30:39.077+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55675 - 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":"a985f4311fcd0e17","trace":"6c04bae2c6c9316bcef79ffaa2786a6d"} -{"@timestamp":"2026-06-25T01:30:41.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55677 - 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.2ms","level":"info","span":"ee9604f6539fa9fc","trace":"c24d72a739de4536a9d85689cc910679"} -{"@timestamp":"2026-06-25T01:30:41.456+08:00","caller":"handler/loghandler.go:151","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node - slowcall(2024.9ms)","duration":"2024.9ms","level":"slow","span":"399b2c7b1014fec1","trace":"35cd43b8a2b8f50a85d403acea0bcda4"} -{"@timestamp":"2026-06-25T01:30:41.456+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - POST /api/v1/internal/workers/jobs/claim - 127.0.0.1:53665 - node","duration":"2024.9ms","level":"info","span":"399b2c7b1014fec1","trace":"35cd43b8a2b8f50a85d403acea0bcda4"} -{"@timestamp":"2026-06-25T01:30:43.078+08:00","caller":"handler/loghandler.go:167","content":"[HTTP] 200 - GET /api/v1/jobs?page=1&pageSize=12 - 127.0.0.1:55679 - 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":"9bc0dd8d64faeb40","trace":"59a7ab847510c28cf8e87433239becd5"} +{"@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"} diff --git a/haixun-backend/.run/logs/web.log b/haixun-backend/.run/logs/web.log index adc6bb8..b3777aa 100644 --- a/haixun-backend/.run/logs/web.log +++ b/haixun-backend/.run/logs/web.log @@ -3,7 +3,7 @@ > vite - VITE v6.4.3 ready in 111 ms + VITE v6.4.3 ready in 174 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose diff --git a/haixun-backend/.run/logs/worker.log b/haixun-backend/.run/logs/worker.log index e19de1a..c39ed1d 100644 --- a/haixun-backend/.run/logs/worker.log +++ b/haixun-backend/.run/logs/worker.log @@ -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-92403 api=http://127.0.0.1:8890 +[8d-worker] started id=local-style-8d-node-26520 api=http://127.0.0.1:8890 diff --git a/haixun-backend/.run/web.pid b/haixun-backend/.run/web.pid index b445406..e117843 100644 --- a/haixun-backend/.run/web.pid +++ b/haixun-backend/.run/web.pid @@ -1 +1 @@ -92316 +26440 diff --git a/haixun-backend/.run/worker.pid b/haixun-backend/.run/worker.pid index 41a2ebc..273f917 100644 --- a/haixun-backend/.run/worker.pid +++ b/haixun-backend/.run/worker.pid @@ -1 +1 @@ -92317 +26441 diff --git a/haixun-backend/generate/api/brand.api b/haixun-backend/generate/api/brand.api index ebbca9d..b4102de 100644 --- a/haixun-backend/generate/api/brand.api +++ b/haixun-backend/generate/api/brand.api @@ -231,6 +231,7 @@ type ( GenerateOutreachDraftsReq { ScanPostID string `json:"scan_post_id" validate:"required"` + TopicID string `json:"topic_id,optional"` Count int `json:"count,optional"` VoicePersonaID string `json:"voice_persona_id,optional"` ProductID string `json:"product_id,optional"` diff --git a/haixun-backend/generate/api/copy_mission.api b/haixun-backend/generate/api/copy_mission.api new file mode 100644 index 0000000..e4646a2 --- /dev/null +++ b/haixun-backend/generate/api/copy_mission.api @@ -0,0 +1,250 @@ +syntax = "v1" + +type ( + CopySuggestedTagData { + Tag string `json:"tag"` + Reason string `json:"reason,omitempty"` + SearchIntent string `json:"search_intent,omitempty"` + SearchType string `json:"search_type,omitempty"` + } + + 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"` + } + + CopyMissionResearchMapData { + AudienceSummary string `json:"audience_summary,omitempty"` + ContentGoal string `json:"content_goal,omitempty"` + Questions []string `json:"questions,omitempty"` + Pillars []string `json:"pillars,omitempty"` + Exclusions []string `json:"exclusions,omitempty"` + SuggestedTags []CopySuggestedTagData `json:"suggested_tags,omitempty"` + SimilarAccounts []CopySimilarAccountData `json:"similar_accounts,omitempty"` + BenchmarkNotes string `json:"benchmark_notes,omitempty"` + } + + CopyMissionData { + ID string `json:"id"` + PersonaID string `json:"persona_id"` + Label string `json:"label,omitempty"` + SeedQuery string `json:"seed_query,omitempty"` + Brief string `json:"brief,omitempty"` + ResearchMap CopyMissionResearchMapData `json:"research_map,omitempty"` + SelectedTags []string `json:"selected_tags,omitempty"` + LastScanJobID string `json:"last_scan_job_id,omitempty"` + Status string `json:"status,omitempty"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + } + + ListCopyMissionsData { + List []CopyMissionData `json:"list"` + } + + CreateCopyMissionReq { + Label string `json:"label" validate:"required"` + SeedQuery string `json:"seed_query" validate:"required"` + Brief string `json:"brief" validate:"required"` + } + + UpdateCopyMissionReq { + Label *string `json:"label,optional"` + SeedQuery *string `json:"seed_query,optional"` + Brief *string `json:"brief,optional"` + AudienceSummary *string `json:"audience_summary,optional"` + ContentGoal *string `json:"content_goal,optional"` + Questions []string `json:"questions,optional"` + Pillars []string `json:"pillars,optional"` + Exclusions []string `json:"exclusions,optional"` + BenchmarkNotes *string `json:"benchmark_notes,optional"` + SelectedTags []string `json:"selected_tags,optional"` + Status *string `json:"status,optional"` + } + + CopyMissionScanScheduleData { + ID string `json:"id,omitempty"` + PersonaID string `json:"persona_id"` + MissionID string `json:"mission_id"` + Cron string `json:"cron"` + Timezone string `json:"timezone"` + Enabled bool `json:"enabled"` + NextRunAt int64 `json:"next_run_at,omitempty"` + LastRunAt int64 `json:"last_run_at,omitempty"` + } + + UpsertCopyMissionScanScheduleReq { + Cron string `json:"cron,optional"` + Timezone string `json:"timezone,optional"` + Enabled bool `json:"enabled"` + } + + UpsertCopyMissionScanScheduleHandlerReq { + CopyMissionPath + UpsertCopyMissionScanScheduleReq + } + + CopyMissionPath { + PersonaID string `path:"personaId" validate:"required"` + ID string `path:"id" validate:"required"` + } + + PersonaCopyMissionsPath { + PersonaID string `path:"personaId" validate:"required"` + } + + CreateCopyMissionHandlerReq { + PersonaCopyMissionsPath + CreateCopyMissionReq + } + + UpdateCopyMissionHandlerReq { + CopyMissionPath + UpdateCopyMissionReq + } + + StartCopyMissionAnalyzeJobData { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` + } + + StartCopyMissionScanJobData { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` + } + + StartCopyMissionMatrixJobReq { + Count int `json:"count,optional"` + } + + StartCopyMissionMatrixJobHandlerReq { + CopyMissionPath + StartCopyMissionMatrixJobReq + } + + StartCopyMissionMatrixJobData { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` + } + + StartCopyMissionCopyDraftJobReq { + ScanPostID string `json:"scan_post_id" validate:"required"` + } + + StartCopyMissionCopyDraftJobHandlerReq { + CopyMissionPath + StartCopyMissionCopyDraftJobReq + } + + StartCopyMissionCopyDraftJobData { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` + } + + ListCopyMissionScanPostsReq { + Limit int `form:"limit,optional"` + } + + ListCopyMissionScanPostsHandlerReq { + CopyMissionPath + ListCopyMissionScanPostsReq + } + + GenerateCopyMissionMatrixReq { + Count int `json:"count,optional"` + } + + GenerateCopyMissionMatrixHandlerReq { + CopyMissionPath + GenerateCopyMissionMatrixReq + } + + GenerateCopyMissionMatrixData { + Drafts []CopyDraftData `json:"drafts"` + Message string `json:"message"` + } + + ListCopyMissionCopyDraftsData { + List []CopyDraftData `json:"list"` + Total int `json:"total"` + } + + CopyMissionInspirationSourceData { + Query string `json:"query,omitempty"` + Title string `json:"title,omitempty"` + Snippet string `json:"snippet,omitempty"` + URL string `json:"url,omitempty"` + } + + CopyMissionInspirationData { + Label string `json:"label"` + SeedQuery string `json:"seed_query"` + Brief string `json:"brief"` + TrendReason string `json:"trend_reason,omitempty"` + TrendKeywords []string `json:"trend_keywords,omitempty"` + Sources []CopyMissionInspirationSourceData `json:"sources,omitempty"` + WebSearchUsed bool `json:"web_search_used"` + Message string `json:"message"` + } +) + +@server( + group: copy_mission + prefix: /api/v1/personas + middleware: AuthJWT + tags: "CopyMission" + summary: "Copy ninja missions (Flow A). Requires Bearer JWT." +) +service gateway { + @handler listCopyMissions + get /:personaId/copy-missions (PersonaCopyMissionsPath) returns (ListCopyMissionsData) + + @handler inspireCopyMission + post /:personaId/copy-mission-inspiration (PersonaCopyMissionsPath) returns (CopyMissionInspirationData) + + @handler createCopyMission + post /:personaId/copy-missions (CreateCopyMissionHandlerReq) returns (CopyMissionData) + + @handler getCopyMission + get /:personaId/copy-missions/:id (CopyMissionPath) returns (CopyMissionData) + + @handler updateCopyMission + patch /:personaId/copy-missions/:id (UpdateCopyMissionHandlerReq) returns (CopyMissionData) + + @handler deleteCopyMission + delete /:personaId/copy-missions/:id (CopyMissionPath) + + @handler startCopyMissionAnalyzeJob + post /:personaId/copy-missions/:id/analyze-jobs (CopyMissionPath) returns (StartCopyMissionAnalyzeJobData) + + @handler startCopyMissionScanJob + post /:personaId/copy-missions/:id/scan-jobs (CopyMissionPath) returns (StartCopyMissionScanJobData) + + @handler listCopyMissionScanPosts + get /:personaId/copy-missions/:id/scan-posts (ListCopyMissionScanPostsHandlerReq) returns (ListPersonaViralScanPostsData) + + @handler generateCopyMissionMatrix + post /:personaId/copy-missions/:id/matrix-drafts (GenerateCopyMissionMatrixHandlerReq) returns (GenerateCopyMissionMatrixData) + + @handler startCopyMissionMatrixJob + post /:personaId/copy-missions/:id/matrix-jobs (StartCopyMissionMatrixJobHandlerReq) returns (StartCopyMissionMatrixJobData) + + @handler startCopyMissionCopyDraftJob + post /:personaId/copy-missions/:id/copy-draft-jobs (StartCopyMissionCopyDraftJobHandlerReq) returns (StartCopyMissionCopyDraftJobData) + + @handler listCopyMissionCopyDrafts + get /:personaId/copy-missions/:id/copy-drafts (CopyMissionPath) returns (ListCopyMissionCopyDraftsData) + + @handler getCopyMissionScanSchedule + get /:personaId/copy-missions/:id/scan-schedule (CopyMissionPath) returns (CopyMissionScanScheduleData) + + @handler upsertCopyMissionScanSchedule + put /:personaId/copy-missions/:id/scan-schedule (UpsertCopyMissionScanScheduleHandlerReq) returns (CopyMissionScanScheduleData) +} \ No newline at end of file diff --git a/haixun-backend/generate/api/gateway.api b/haixun-backend/generate/api/gateway.api index 9429e51..6a50145 100644 --- a/haixun-backend/generate/api/gateway.api +++ b/haixun-backend/generate/api/gateway.api @@ -22,6 +22,7 @@ import ( "permission.api" "threads_account.api" "persona.api" + "copy_mission.api" "brand.api" "placement_topic.api" "worker_internal.api" diff --git a/haixun-backend/generate/api/member.api b/haixun-backend/generate/api/member.api index e795a4b..6fd1a58 100644 --- a/haixun-backend/generate/api/member.api +++ b/haixun-backend/generate/api/member.api @@ -30,18 +30,25 @@ type ( } MemberPlacementSettingsData { - BraveAPIKey string `json:"brave_api_key,omitempty"` - BraveAPIKeyConfigured bool `json:"brave_api_key_configured"` - BraveCountry string `json:"brave_country"` - BraveSearchLang string `json:"brave_search_lang"` - ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid + WebSearchProvider string `json:"web_search_provider"` // brave | exa + BraveAPIKey string `json:"brave_api_key,omitempty"` + BraveAPIKeyConfigured bool `json:"brave_api_key_configured"` + ExaAPIKey string `json:"exa_api_key,omitempty"` + ExaAPIKeyConfigured bool `json:"exa_api_key_configured"` + BraveCountry string `json:"brave_country"` + BraveSearchLang string `json:"brave_search_lang"` + ExaUserLocation string `json:"exa_user_location"` + ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid } UpdateMemberPlacementSettingsReq { - BraveAPIKey *string `json:"brave_api_key,optional"` - BraveCountry *string `json:"brave_country,optional"` - BraveSearchLang *string `json:"brave_search_lang,optional"` - ExpandStrategy *string `json:"expand_strategy,optional"` + WebSearchProvider *string `json:"web_search_provider,optional"` + BraveAPIKey *string `json:"brave_api_key,optional"` + ExaAPIKey *string `json:"exa_api_key,optional"` + BraveCountry *string `json:"brave_country,optional"` + BraveSearchLang *string `json:"brave_search_lang,optional"` + ExaUserLocation *string `json:"exa_user_location,optional"` + ExpandStrategy *string `json:"expand_strategy,optional"` } ) diff --git a/haixun-backend/generate/api/persona.api b/haixun-backend/generate/api/persona.api index 3bffc18..44a25ef 100644 --- a/haixun-backend/generate/api/persona.api +++ b/haixun-backend/generate/api/persona.api @@ -39,6 +39,7 @@ type ( UpdatePersonaReq { DisplayName *string `json:"display_name,optional"` Persona *string `json:"persona,optional"` + Brief *string `json:"brief,optional"` StyleProfile *string `json:"style_profile,optional"` StyleBenchmark *string `json:"style_benchmark,optional"` } @@ -83,17 +84,18 @@ type ( } ViralScanPostData { - ID string `json:"id"` - SearchTag string `json:"search_tag"` - Permalink string `json:"permalink"` - Author string `json:"author"` - Text string `json:"text"` - LikeCount int `json:"like_count"` - ReplyCount int `json:"reply_count"` - EngagementScore int `json:"engagement_score"` - Source string `json:"source"` - ScanJobID string `json:"scan_job_id"` - CreateAt int64 `json:"create_at"` + ID string `json:"id"` + SearchTag string `json:"search_tag"` + Permalink string `json:"permalink"` + Author string `json:"author"` + Text string `json:"text"` + LikeCount int `json:"like_count"` + ReplyCount int `json:"reply_count"` + EngagementScore int `json:"engagement_score"` + Source string `json:"source"` + ScanJobID string `json:"scan_job_id"` + Replies []ScanReplyData `json:"replies,omitempty"` + CreateAt int64 `json:"create_at"` } ListPersonaViralScanPostsData { @@ -107,18 +109,23 @@ type ( } CopyDraftData { - ID string `json:"id"` - PersonaID string `json:"persona_id"` - ScanPostID string `json:"scan_post_id,omitempty"` - DraftType string `json:"draft_type"` + ID string `json:"id"` + PersonaID string `json:"persona_id"` + CopyMissionID string `json:"copy_mission_id,omitempty"` + ScanPostID string `json:"scan_post_id,omitempty"` + DraftType string `json:"draft_type"` + SortOrder int `json:"sort_order,omitempty"` Text string `json:"text"` Angle string `json:"angle,omitempty"` Hook string `json:"hook,omitempty"` Rationale string `json:"rationale,omitempty"` ReferenceNotes string `json:"reference_notes,omitempty"` Sources []string `json:"sources,omitempty"` - Status string `json:"status,omitempty"` - CreateAt int64 `json:"create_at"` + Status string `json:"status,omitempty"` + PublishedMediaID string `json:"published_media_id,omitempty"` + PublishedPermalink string `json:"published_permalink,omitempty"` + PublishedAt int64 `json:"published_at,omitempty"` + CreateAt int64 `json:"create_at"` } ListPersonaCopyDraftsData { @@ -139,6 +146,41 @@ type ( Draft CopyDraftData `json:"draft"` Message string `json:"message"` } + + CopyDraftPath { + ID string `path:"id" validate:"required"` + DraftID string `path:"draftId" validate:"required"` + } + + UpdateCopyDraftReq { + Text *string `json:"text,optional"` + Hook *string `json:"hook,optional"` + Angle *string `json:"angle,optional"` + Status *string `json:"status,optional"` + } + + UpdateCopyDraftHandlerReq { + CopyDraftPath + UpdateCopyDraftReq + } + + PublishCopyDraftReq { + Text string `json:"text,optional"` + Confirm bool `json:"confirm"` + } + + PublishCopyDraftHandlerReq { + CopyDraftPath + PublishCopyDraftReq + } + + PublishCopyDraftData { + DraftID string `json:"draft_id"` + MediaID string `json:"media_id"` + Permalink string `json:"permalink,omitempty"` + Status string `json:"status"` + Message string `json:"message"` + } ) @server( @@ -178,4 +220,10 @@ service gateway { @handler generatePersonaCopyDraft post /:id/copy-drafts/generate (GeneratePersonaCopyDraftHandlerReq) returns (GeneratePersonaCopyDraftData) + + @handler updatePersonaCopyDraft + patch /:id/copy-drafts/:draftId (UpdateCopyDraftHandlerReq) returns (CopyDraftData) + + @handler publishPersonaCopyDraft + post /:id/copy-drafts/:draftId/publish (PublishCopyDraftHandlerReq) returns (PublishCopyDraftData) } \ No newline at end of file diff --git a/haixun-backend/internal/handler/brand/create_brand_product_handler.go b/haixun-backend/internal/handler/brand/create_brand_product_handler.go index 9975349..74f50f5 100644 --- a/haixun-backend/internal/handler/brand/create_brand_product_handler.go +++ b/haixun-backend/internal/handler/brand/create_brand_product_handler.go @@ -6,11 +6,12 @@ package brand import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/brand" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func CreateBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/brand/delete_brand_product_handler.go b/haixun-backend/internal/handler/brand/delete_brand_product_handler.go index fedc915..aa96e88 100644 --- a/haixun-backend/internal/handler/brand/delete_brand_product_handler.go +++ b/haixun-backend/internal/handler/brand/delete_brand_product_handler.go @@ -6,11 +6,12 @@ package brand import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/brand" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func DeleteBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/brand/list_brand_products_handler.go b/haixun-backend/internal/handler/brand/list_brand_products_handler.go index 382613e..8d6a49a 100644 --- a/haixun-backend/internal/handler/brand/list_brand_products_handler.go +++ b/haixun-backend/internal/handler/brand/list_brand_products_handler.go @@ -6,11 +6,12 @@ package brand import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/brand" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func ListBrandProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/brand/update_brand_product_handler.go b/haixun-backend/internal/handler/brand/update_brand_product_handler.go index 0c0136a..42b5be9 100644 --- a/haixun-backend/internal/handler/brand/update_brand_product_handler.go +++ b/haixun-backend/internal/handler/brand/update_brand_product_handler.go @@ -6,11 +6,12 @@ package brand import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/brand" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func UpdateBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/copy_mission/create_copy_mission_handler.go b/haixun-backend/internal/handler/copy_mission/create_copy_mission_handler.go new file mode 100644 index 0000000..b97e098 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/create_copy_mission_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func CreateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateCopyMissionHandlerReq + 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 := copy_mission.NewCreateCopyMissionLogic(r.Context(), svcCtx) + data, err := l.CreateCopyMission(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/delete_copy_mission_handler.go b/haixun-backend/internal/handler/copy_mission/delete_copy_mission_handler.go new file mode 100644 index 0000000..cedeb8d --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/delete_copy_mission_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func DeleteCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewDeleteCopyMissionLogic(r.Context(), svcCtx) + err := l.DeleteCopyMission(&req) + response.Write(r.Context(), w, nil, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go b/haixun-backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go new file mode 100644 index 0000000..db5e4bb --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GenerateCopyMissionMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GenerateCopyMissionMatrixHandlerReq + 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 := copy_mission.NewGenerateCopyMissionMatrixLogic(r.Context(), svcCtx) + data, err := l.GenerateCopyMissionMatrix(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/get_copy_mission_handler.go b/haixun-backend/internal/handler/copy_mission/get_copy_mission_handler.go new file mode 100644 index 0000000..6034abd --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/get_copy_mission_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewGetCopyMissionLogic(r.Context(), svcCtx) + data, err := l.GetCopyMission(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go b/haixun-backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go new file mode 100644 index 0000000..9737c28 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewGetCopyMissionScanScheduleLogic(r.Context(), svcCtx) + data, err := l.GetCopyMissionScanSchedule(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/inspire_copy_mission_handler.go b/haixun-backend/internal/handler/copy_mission/inspire_copy_mission_handler.go new file mode 100644 index 0000000..addb22e --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/inspire_copy_mission_handler.go @@ -0,0 +1,30 @@ +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func InspireCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PersonaCopyMissionsPath + 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 := copy_mission.NewInspireCopyMissionLogic(r.Context(), svcCtx) + data, err := l.InspireCopyMission(&req) + response.Write(r.Context(), w, data, err) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go b/haixun-backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go new file mode 100644 index 0000000..ff4fe54 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ListCopyMissionCopyDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewListCopyMissionCopyDraftsLogic(r.Context(), svcCtx) + data, err := l.ListCopyMissionCopyDrafts(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go b/haixun-backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go new file mode 100644 index 0000000..1f8705b --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ListCopyMissionScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ListCopyMissionScanPostsHandlerReq + 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 := copy_mission.NewListCopyMissionScanPostsLogic(r.Context(), svcCtx) + data, err := l.ListCopyMissionScanPosts(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/list_copy_missions_handler.go b/haixun-backend/internal/handler/copy_mission/list_copy_missions_handler.go new file mode 100644 index 0000000..3d82016 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/list_copy_missions_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ListCopyMissionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PersonaCopyMissionsPath + 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 := copy_mission.NewListCopyMissionsLogic(r.Context(), svcCtx) + data, err := l.ListCopyMissions(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go b/haixun-backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go new file mode 100644 index 0000000..8b550a5 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func StartCopyMissionAnalyzeJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewStartCopyMissionAnalyzeJobLogic(r.Context(), svcCtx) + data, err := l.StartCopyMissionAnalyzeJob(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go b/haixun-backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go new file mode 100644 index 0000000..266d08d --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go @@ -0,0 +1,30 @@ +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func StartCopyMissionCopyDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.StartCopyMissionCopyDraftJobHandlerReq + 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 := copy_mission.NewStartCopyMissionCopyDraftJobLogic(r.Context(), svcCtx) + data, err := l.StartCopyMissionCopyDraftJob(&req) + response.Write(r.Context(), w, data, err) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go b/haixun-backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go new file mode 100644 index 0000000..6b31493 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go @@ -0,0 +1,30 @@ +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func StartCopyMissionMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.StartCopyMissionMatrixJobHandlerReq + 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 := copy_mission.NewStartCopyMissionMatrixJobLogic(r.Context(), svcCtx) + data, err := l.StartCopyMissionMatrixJob(&req) + response.Write(r.Context(), w, data, err) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go b/haixun-backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go new file mode 100644 index 0000000..4309303 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func StartCopyMissionScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CopyMissionPath + 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 := copy_mission.NewStartCopyMissionScanJobLogic(r.Context(), svcCtx) + data, err := l.StartCopyMissionScanJob(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/update_copy_mission_handler.go b/haixun-backend/internal/handler/copy_mission/update_copy_mission_handler.go new file mode 100644 index 0000000..580a256 --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/update_copy_mission_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UpdateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateCopyMissionHandlerReq + 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 := copy_mission.NewUpdateCopyMissionLogic(r.Context(), svcCtx) + data, err := l.UpdateCopyMission(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go b/haixun-backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go new file mode 100644 index 0000000..4beed7c --- /dev/null +++ b/haixun-backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package copy_mission + +import ( + "net/http" + + "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UpsertCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpsertCopyMissionScanScheduleHandlerReq + 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 := copy_mission.NewUpsertCopyMissionScanScheduleLogic(r.Context(), svcCtx) + data, err := l.UpsertCopyMissionScanSchedule(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/persona/publish_persona_copy_draft_handler.go b/haixun-backend/internal/handler/persona/publish_persona_copy_draft_handler.go new file mode 100644 index 0000000..2f242e4 --- /dev/null +++ b/haixun-backend/internal/handler/persona/publish_persona_copy_draft_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package persona + +import ( + "net/http" + + "haixun-backend/internal/logic/persona" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func PublishPersonaCopyDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PublishCopyDraftHandlerReq + 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 := persona.NewPublishPersonaCopyDraftLogic(r.Context(), svcCtx) + data, err := l.PublishPersonaCopyDraft(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/persona/update_persona_copy_draft_handler.go b/haixun-backend/internal/handler/persona/update_persona_copy_draft_handler.go new file mode 100644 index 0000000..9302cb8 --- /dev/null +++ b/haixun-backend/internal/handler/persona/update_persona_copy_draft_handler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package persona + +import ( + "net/http" + + "haixun-backend/internal/logic/persona" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UpdatePersonaCopyDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateCopyDraftHandlerReq + 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 := persona.NewUpdatePersonaCopyDraftLogic(r.Context(), svcCtx) + data, err := l.UpdatePersonaCopyDraft(&req) + response.Write(r.Context(), w, data, err) + } +} diff --git a/haixun-backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go b/haixun-backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go index a21cd8e..87673c7 100644 --- a/haixun-backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go +++ b/haixun-backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func BatchDeletePlacementTopicScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/create_placement_topic_handler.go b/haixun-backend/internal/handler/placement_topic/create_placement_topic_handler.go index 1ee8428..6589fa5 100644 --- a/haixun-backend/internal/handler/placement_topic/create_placement_topic_handler.go +++ b/haixun-backend/internal/handler/placement_topic/create_placement_topic_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func CreatePlacementTopicHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/delete_placement_topic_handler.go b/haixun-backend/internal/handler/placement_topic/delete_placement_topic_handler.go index d157030..7c7827a 100644 --- a/haixun-backend/internal/handler/placement_topic/delete_placement_topic_handler.go +++ b/haixun-backend/internal/handler/placement_topic/delete_placement_topic_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func DeletePlacementTopicHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go b/haixun-backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go index ca67242..ad8d898 100644 --- a/haixun-backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go +++ b/haixun-backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func DeletePlacementTopicScanPostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go b/haixun-backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go index 3ab53b7..2eb2d29 100644 --- a/haixun-backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go +++ b/haixun-backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func ExpandPlacementTopicGraphHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go b/haixun-backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go index b01d389..c645174 100644 --- a/haixun-backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go +++ b/haixun-backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GeneratePlacementTopicContentMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go b/haixun-backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go index 32fcaa1..d980af6 100644 --- a/haixun-backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go +++ b/haixun-backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GeneratePlacementTopicOutreachDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go b/haixun-backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go index a0790df..1939e9c 100644 --- a/haixun-backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go +++ b/haixun-backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetPlacementTopicContentMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go b/haixun-backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go index c9aac44..ae7db4a 100644 --- a/haixun-backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go +++ b/haixun-backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetPlacementTopicGraphHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/get_placement_topic_handler.go b/haixun-backend/internal/handler/placement_topic/get_placement_topic_handler.go index c8bb3de..bfcc7a6 100644 --- a/haixun-backend/internal/handler/placement_topic/get_placement_topic_handler.go +++ b/haixun-backend/internal/handler/placement_topic/get_placement_topic_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetPlacementTopicHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go b/haixun-backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go index 2441b38..7783c54 100644 --- a/haixun-backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go +++ b/haixun-backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetPlacementTopicScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go b/haixun-backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go index 040db9b..f294c9c 100644 --- a/haixun-backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go +++ b/haixun-backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func ListPlacementTopicScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go b/haixun-backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go index ff84179..e649322 100644 --- a/haixun-backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go +++ b/haixun-backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func PatchPlacementTopicGraphNodesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go b/haixun-backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go index 5c20f60..c816159 100644 --- a/haixun-backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go +++ b/haixun-backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func PatchPlacementTopicScanPostOutreachHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go b/haixun-backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go index 05d691a..c925cb8 100644 --- a/haixun-backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go +++ b/haixun-backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func PublishPlacementTopicOutreachDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go b/haixun-backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go index 6ec3868..2254b3c 100644 --- a/haixun-backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go +++ b/haixun-backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func StartPlacementTopicScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/update_placement_topic_handler.go b/haixun-backend/internal/handler/placement_topic/update_placement_topic_handler.go index f8b1f8b..641c30e 100644 --- a/haixun-backend/internal/handler/placement_topic/update_placement_topic_handler.go +++ b/haixun-backend/internal/handler/placement_topic/update_placement_topic_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func UpdatePlacementTopicHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go b/haixun-backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go index 1563d31..6963740 100644 --- a/haixun-backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go +++ b/haixun-backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go @@ -6,11 +6,12 @@ package placement_topic import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/placement_topic" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" ) func UpsertPlacementTopicScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { diff --git a/haixun-backend/internal/handler/routes.go b/haixun-backend/internal/handler/routes.go index be2b4bd..2313285 100644 --- a/haixun-backend/internal/handler/routes.go +++ b/haixun-backend/internal/handler/routes.go @@ -9,6 +9,7 @@ import ( ai "haixun-backend/internal/handler/ai" auth "haixun-backend/internal/handler/auth" brand "haixun-backend/internal/handler/brand" + copy_mission "haixun-backend/internal/handler/copy_mission" job "haixun-backend/internal/handler/job" member "haixun-backend/internal/handler/member" normal "haixun-backend/internal/handler/normal" @@ -223,61 +224,86 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( rest.WithMiddlewares( - []rest.Middleware{serverCtx.WorkerSecret}, + []rest.Middleware{serverCtx.AuthJWT}, []rest.Route{ { - Method: http.MethodPost, - Path: "/workers/jobs/:id/analyze-style8d", - Handler: job.AnalyzeStyle8DFromWorkerHandler(serverCtx), + Method: http.MethodGet, + Path: "/:personaId/copy-missions", + Handler: copy_mission.ListCopyMissionsHandler(serverCtx), }, { Method: http.MethodPost, - Path: "/workers/jobs/:id/cancel-ack", - Handler: job.AckWorkerJobCancelHandler(serverCtx), + Path: "/:personaId/copy-mission-inspiration", + Handler: copy_mission.InspireCopyMissionHandler(serverCtx), }, { Method: http.MethodPost, - Path: "/workers/jobs/:id/cancel-check", - Handler: job.CheckWorkerJobCancelHandler(serverCtx), + Path: "/:personaId/copy-missions", + Handler: copy_mission.CreateCopyMissionHandler(serverCtx), }, { - Method: http.MethodPost, - Path: "/workers/jobs/:id/complete", - Handler: job.CompleteWorkerJobHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/workers/jobs/:id/fail", - Handler: job.FailWorkerJobHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/workers/jobs/:id/heartbeat", - Handler: job.RefreshWorkerJobLockHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/workers/jobs/:id/progress", - Handler: job.UpdateWorkerJobProgressHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/workers/jobs/claim", - Handler: job.ClaimWorkerJobHandler(serverCtx), + Method: http.MethodGet, + Path: "/:personaId/copy-missions/:id", + Handler: copy_mission.GetCopyMissionHandler(serverCtx), }, { Method: http.MethodPatch, - Path: "/workers/personas/:id/style-profile", - Handler: job.StorePersonaStyleProfileFromWorkerHandler(serverCtx), + Path: "/:personaId/copy-missions/:id", + Handler: copy_mission.UpdateCopyMissionHandler(serverCtx), + }, + { + Method: http.MethodDelete, + Path: "/:personaId/copy-missions/:id", + Handler: copy_mission.DeleteCopyMissionHandler(serverCtx), }, { Method: http.MethodPost, - Path: "/workers/threads-accounts/:id/session", - Handler: job.GetWorkerThreadsAccountSessionHandler(serverCtx), + Path: "/:personaId/copy-missions/:id/analyze-jobs", + Handler: copy_mission.StartCopyMissionAnalyzeJobHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/:personaId/copy-missions/:id/copy-drafts", + Handler: copy_mission.ListCopyMissionCopyDraftsHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/:personaId/copy-missions/:id/matrix-drafts", + Handler: copy_mission.GenerateCopyMissionMatrixHandler(serverCtx), + }, + { + Method: http.MethodPost, + 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", + Handler: copy_mission.StartCopyMissionScanJobHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/:personaId/copy-missions/:id/scan-posts", + Handler: copy_mission.ListCopyMissionScanPostsHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/:personaId/copy-missions/:id/scan-schedule", + Handler: copy_mission.GetCopyMissionScanScheduleHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/:personaId/copy-missions/:id/scan-schedule", + Handler: copy_mission.UpsertCopyMissionScanScheduleHandler(serverCtx), }, }..., ), - rest.WithPrefix("/api/v1/internal"), + rest.WithPrefix("/api/v1/personas"), ) server.AddRoutes( @@ -359,6 +385,65 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1"), ) + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.WorkerSecret}, + []rest.Route{ + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/analyze-style8d", + Handler: job.AnalyzeStyle8DFromWorkerHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/cancel-ack", + Handler: job.AckWorkerJobCancelHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/cancel-check", + Handler: job.CheckWorkerJobCancelHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/complete", + Handler: job.CompleteWorkerJobHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/fail", + Handler: job.FailWorkerJobHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/heartbeat", + Handler: job.RefreshWorkerJobLockHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/:id/progress", + Handler: job.UpdateWorkerJobProgressHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/jobs/claim", + Handler: job.ClaimWorkerJobHandler(serverCtx), + }, + { + Method: http.MethodPatch, + Path: "/workers/personas/:id/style-profile", + Handler: job.StorePersonaStyleProfileFromWorkerHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/workers/threads-accounts/:id/session", + Handler: job.GetWorkerThreadsAccountSessionHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1/internal"), + ) + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.AuthJWT}, @@ -452,6 +537,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/:id/copy-drafts", Handler: persona.ListPersonaCopyDraftsHandler(serverCtx), }, + { + Method: http.MethodPatch, + Path: "/:id/copy-drafts/:draftId", + Handler: persona.UpdatePersonaCopyDraftHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/:id/copy-drafts/:draftId/publish", + Handler: persona.PublishPersonaCopyDraftHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/:id/copy-drafts/generate", diff --git a/haixun-backend/internal/library/exa/client.go b/haixun-backend/internal/library/exa/client.go new file mode 100644 index 0000000..c2a6eb7 --- /dev/null +++ b/haixun-backend/internal/library/exa/client.go @@ -0,0 +1,191 @@ +package exa + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +const defaultBaseURL = "https://api.exa.ai/search" + +type Mode string + +const ( + ModeKnowledgeExpand Mode = "knowledge_expand" + ModeThreadsDiscover Mode = "threads_discover" +) + +type SearchResult struct { + Title string + Snippet string + URL string + PublishedDate string + Author string + HighlightScore float64 +} + +type SearchResponse struct { + Results []SearchResult + Query string + Status string // success | unavailable +} + +type Client struct { + apiKey string + baseURL string + http *http.Client +} + +func NewClient(apiKey string) *Client { + return &Client{ + apiKey: strings.TrimSpace(apiKey), + baseURL: defaultBaseURL, + http: &http.Client{ + Timeout: 25 * time.Second, + }, + } +} + +func (c *Client) Enabled() bool { + return c != nil && c.apiKey != "" +} + +type SearchOptions struct { + Query string + Limit int + Mode Mode + UserLocation string + StartPublishedDate string +} + +func (c *Client) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) { + out := SearchResponse{Query: strings.TrimSpace(opts.Query), Status: "unavailable"} + if !c.Enabled() { + return out, nil + } + if out.Query == "" { + return out, fmt.Errorf("exa search query is required") + } + + limit := opts.Limit + if limit <= 0 { + limit = 5 + } + if limit > 20 { + limit = 20 + } + + userLocation := strings.TrimSpace(opts.UserLocation) + if userLocation == "" { + userLocation = "TW" + } + + body := map[string]any{ + "query": out.Query, + "type": "auto", + "numResults": limit, + "userLocation": userLocation, + "contents": map[string]any{ + "highlights": true, + }, + } + if opts.Mode == ModeThreadsDiscover { + body["includeDomains"] = []string{"threads.net", "threads.com"} + } + if start := strings.TrimSpace(opts.StartPublishedDate); start != "" { + body["startPublishedDate"] = start + } + + payload, err := json.Marshal(body) + if err != nil { + return out, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload)) + if err != nil { + return out, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", c.apiKey) + + res, err := c.http.Do(req) + if err != nil { + return out, nil + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return out, nil + } + + raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20)) + if err != nil { + return out, nil + } + + var parsed struct { + Results []struct { + Title string `json:"title"` + URL string `json:"url"` + PublishedDate string `json:"publishedDate"` + Author string `json:"author"` + Highlights []string `json:"highlights"` + HighlightScores []float64 `json:"highlightScores"` + } `json:"results"` + } + if err := json.Unmarshal(raw, &parsed); err != nil { + return out, nil + } + + threadsOnly := opts.Mode == ModeThreadsDiscover + for _, item := range parsed.Results { + rawURL := strings.TrimSpace(item.URL) + if rawURL == "" { + continue + } + if threadsOnly && !isThreadsURL(rawURL) { + continue + } + snippet := firstHighlight(item.Highlights) + if snippet == "" { + snippet = strings.TrimSpace(item.Title) + } + score := 0.0 + if len(item.HighlightScores) > 0 { + score = item.HighlightScores[0] + } + out.Results = append(out.Results, SearchResult{ + Title: strings.TrimSpace(item.Title), + Snippet: snippet, + URL: rawURL, + PublishedDate: strings.TrimSpace(item.PublishedDate), + Author: strings.TrimSpace(item.Author), + HighlightScore: score, + }) + if len(out.Results) >= limit { + break + } + } + out.Status = "success" + return out, nil +} + +func firstHighlight(items []string) string { + for _, item := range items { + if trimmed := strings.TrimSpace(item); trimmed != "" { + return trimmed + } + } + return "" +} + +func isThreadsURL(raw string) bool { + lower := strings.ToLower(raw) + return strings.Contains(lower, "threads.com") || strings.Contains(lower, "threads.net") +} diff --git a/haixun-backend/internal/library/exa/client_test.go b/haixun-backend/internal/library/exa/client_test.go new file mode 100644 index 0000000..9970147 --- /dev/null +++ b/haixun-backend/internal/library/exa/client_test.go @@ -0,0 +1,46 @@ +package exa + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSearchParsesHighlights(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("x-api-key") != "test-key" { + t.Fatalf("missing api key header") + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "results": []map[string]any{ + { + "title": "Threads post", + "url": "https://www.threads.net/@alice/post/abc123", + "highlights": []string{"這是一則測試貼文內容"}, + "highlightScores": []float64{0.82}, + }, + }, + }) + })) + defer server.Close() + + client := NewClient("test-key") + client.baseURL = server.URL + + res, err := client.Search(context.Background(), SearchOptions{ + Query: "Threads 貼文", + Limit: 5, + Mode: ModeThreadsDiscover, + }) + if err != nil { + t.Fatalf("search failed: %v", err) + } + if res.Status != "success" || len(res.Results) != 1 { + t.Fatalf("unexpected response: %+v", res) + } + if res.Results[0].Snippet != "這是一則測試貼文內容" { + t.Fatalf("unexpected snippet: %q", res.Results[0].Snippet) + } +} diff --git a/haixun-backend/internal/library/knowledge/brave_collect.go b/haixun-backend/internal/library/knowledge/brave_collect.go index 4fd9b1a..3854699 100644 --- a/haixun-backend/internal/library/knowledge/brave_collect.go +++ b/haixun-backend/internal/library/knowledge/brave_collect.go @@ -6,12 +6,13 @@ import ( "sync" "sync/atomic" - libbrave "haixun-backend/internal/library/brave" + "haixun-backend/internal/library/websearch" ) type BraveSearchLocale struct { - Country string - SearchLang string + Country string + SearchLang string + UserLocation string } type BraveCollectConfig struct { @@ -53,7 +54,19 @@ func DefaultBraveCollectConfig() BraveCollectConfig { func CollectBraveSources( ctx context.Context, - client *libbrave.Client, + client websearch.Client, + locale BraveSearchLocale, + queries []string, + cfg BraveCollectConfig, + onProgress func(i, total int), + heartbeat func() error, +) []BraveSource { + return CollectWebSources(ctx, client, locale, queries, cfg, onProgress, heartbeat) +} + +func CollectWebSources( + ctx context.Context, + client websearch.Client, locale BraveSearchLocale, queries []string, cfg BraveCollectConfig, @@ -64,14 +77,14 @@ func CollectBraveSources( return nil } if cfg.Concurrency <= 1 { - return collectBraveSourcesSequential(ctx, client, locale, queries, cfg, onProgress, heartbeat) + return collectWebSourcesSequential(ctx, client, locale, queries, cfg, onProgress, heartbeat) } - return collectBraveSourcesParallel(ctx, client, locale, queries, cfg, onProgress, heartbeat) + return collectWebSourcesParallel(ctx, client, locale, queries, cfg, onProgress, heartbeat) } -func collectBraveSourcesSequential( +func collectWebSourcesSequential( ctx context.Context, - client *libbrave.Client, + client websearch.Client, locale BraveSearchLocale, queries []string, cfg BraveCollectConfig, @@ -94,7 +107,7 @@ func collectBraveSourcesSequential( return out } } - appendBraveResults(&out, seenURL, query, searchBraveQuery(ctx, client, locale, query, cfg.ResultsPerQuery)) + appendBraveResults(&out, seenURL, query, searchWebQuery(ctx, client, locale, query, cfg.ResultsPerQuery)) if onProgress != nil { onProgress(i, len(queries)) } @@ -136,9 +149,9 @@ func (s *braveCollectState) appendResults(query string, items []BraveSource) { } } -func collectBraveSourcesParallel( +func collectWebSourcesParallel( ctx context.Context, - client *libbrave.Client, + client websearch.Client, locale BraveSearchLocale, queries []string, cfg BraveCollectConfig, @@ -180,7 +193,7 @@ func collectBraveSourcesParallel( } } query := queries[i] - items := searchBraveQuery(ctx, client, locale, query, cfg.ResultsPerQuery) + items := searchWebQuery(ctx, client, locale, query, cfg.ResultsPerQuery) state.appendResults(query, items) done := int(atomic.AddInt32(&state.completed, 1)) if onProgress != nil { @@ -200,19 +213,20 @@ func shouldStopCollect(out []BraveSource, cfg BraveCollectConfig) bool { return len(out) >= cfg.MinSourcesBeforeStop && uniqueSourceCount(out) >= cfg.MinSourcesBeforeStop } -func searchBraveQuery( +func searchWebQuery( ctx context.Context, - client *libbrave.Client, + client websearch.Client, locale BraveSearchLocale, query string, limit int, ) []BraveSource { - res, _ := client.Search(ctx, libbrave.SearchOptions{ - Query: query, - Limit: limit, - Mode: libbrave.ModeKnowledgeExpand, - Country: locale.Country, - SearchLang: locale.SearchLang, + res, _ := client.Search(ctx, websearch.SearchOptions{ + Query: query, + Limit: limit, + Mode: websearch.ModeKnowledgeExpand, + Country: locale.Country, + SearchLang: locale.SearchLang, + UserLocation: locale.UserLocation, }) items := make([]BraveSource, 0, len(res.Results)) for _, item := range res.Results { diff --git a/haixun-backend/internal/library/knowledge/expand_strategy.go b/haixun-backend/internal/library/knowledge/expand_strategy.go index 3007e2f..6b9733b 100644 --- a/haixun-backend/internal/library/knowledge/expand_strategy.go +++ b/haixun-backend/internal/library/knowledge/expand_strategy.go @@ -21,10 +21,14 @@ func ParseExpandStrategy(raw string) ExpandStrategy { } } -func (s ExpandStrategy) RequiresBrave() bool { +func (s ExpandStrategy) RequiresWebSearch() bool { return s == ExpandStrategyBrave || s == ExpandStrategyHybrid } +func (s ExpandStrategy) RequiresBrave() bool { + return s.RequiresWebSearch() +} + // UsesSupplementalBrave 廣度補充是否再打第二輪 Brave(hybrid 改由 LLM 補廣度以省 API)。 func (s ExpandStrategy) UsesSupplementalBrave() bool { return s == ExpandStrategyBrave diff --git a/haixun-backend/internal/library/knowledge/patrol_resolve.go b/haixun-backend/internal/library/knowledge/patrol_resolve.go index 6228459..61b11a3 100644 --- a/haixun-backend/internal/library/knowledge/patrol_resolve.go +++ b/haixun-backend/internal/library/knowledge/patrol_resolve.go @@ -10,4 +10,4 @@ func ResolveScanPatrolKeywords(explicit, saved []string, input PatrolTagInput, n return keywords } return NormalizePatrolKeywordList(CollectPatrolTagsFromGraph(input, nodes)) -} \ No newline at end of file +} diff --git a/haixun-backend/internal/library/knowledge/patrol_resolve_test.go b/haixun-backend/internal/library/knowledge/patrol_resolve_test.go index c557af7..349b458 100644 --- a/haixun-backend/internal/library/knowledge/patrol_resolve_test.go +++ b/haixun-backend/internal/library/knowledge/patrol_resolve_test.go @@ -21,4 +21,4 @@ func TestResolveScanPatrolKeywordsFromResearchMapWithoutGraph(t *testing.T) { if len(got) == 0 { t.Fatal("expected research map questions to produce patrol keywords without graph nodes") } -} \ No newline at end of file +} diff --git a/haixun-backend/internal/library/matrix/copy_generate.go b/haixun-backend/internal/library/matrix/copy_generate.go new file mode 100644 index 0000000..cb31b50 --- /dev/null +++ b/haixun-backend/internal/library/matrix/copy_generate.go @@ -0,0 +1,84 @@ +package matrix + +import ( + "fmt" + "strings" + + libprompt "haixun-backend/internal/library/prompt" +) + +type CopyGenerateInput struct { + Count int + TopicLabel string + TopicBrief string + ResearchMap string + SelectedTags []string + ViralSamples string + PersonaBlock string +} + +func BuildCopyUserPrompt(in CopyGenerateInput) (string, error) { + count := in.Count + if count <= 0 { + count = 5 + } + if count > 12 { + count = 12 + } + return libprompt.MatrixCopyUser(map[string]string{ + "count": fmt.Sprintf("%d", count), + "topic_label": strings.TrimSpace(in.TopicLabel), + "topic_brief": strings.TrimSpace(in.TopicBrief), + "research_map_block": strings.TrimSpace(in.ResearchMap), + "selected_tags_block": formatTagList(in.SelectedTags), + "viral_samples_block": strings.TrimSpace(in.ViralSamples), + "persona_block": strings.TrimSpace(in.PersonaBlock), + }) +} + +func formatTagList(tags []string) string { + if len(tags) == 0 { + return "(尚未選擇)" + } + lines := make([]string, 0, len(tags)) + for _, tag := range tags { + tag = strings.TrimSpace(tag) + if tag == "" { + continue + } + lines = append(lines, "- "+tag) + } + if len(lines) == 0 { + return "(尚未選擇)" + } + return strings.Join(lines, "\n") +} + +func FormatCopyResearchMapBlock(audience, goal string, questions, pillars, exclusions []string) string { + var b strings.Builder + if audience = strings.TrimSpace(audience); audience != "" { + b.WriteString("受眾:") + b.WriteString(audience) + b.WriteString("\n") + } + if goal = strings.TrimSpace(goal); goal != "" { + b.WriteString("內容目標:") + b.WriteString(goal) + b.WriteString("\n") + } + if len(pillars) > 0 { + b.WriteString("支柱:") + b.WriteString(strings.Join(pillars, "、")) + b.WriteString("\n") + } + if len(questions) > 0 { + b.WriteString("受眾問題:") + b.WriteString(strings.Join(questions, ";")) + b.WriteString("\n") + } + if len(exclusions) > 0 { + b.WriteString("排除:") + b.WriteString(strings.Join(exclusions, ";")) + } + return strings.TrimSpace(b.String()) +} diff --git a/haixun-backend/internal/library/matrix/generate.go b/haixun-backend/internal/library/matrix/generate.go index fed5f0e..c9323aa 100644 --- a/haixun-backend/internal/library/matrix/generate.go +++ b/haixun-backend/internal/library/matrix/generate.go @@ -7,6 +7,7 @@ import ( "strings" libprompt "haixun-backend/internal/library/prompt" + "haixun-backend/internal/library/threadspost" ) type Row struct { @@ -121,8 +122,8 @@ func ParseGenerateOutput(raw string) (GenerateResult, error) { func trimText(text string) string { text = strings.TrimSpace(text) runes := []rune(text) - if len(runes) > 500 { - return string(runes[:500]) + if len(runes) > threadspost.MaxPublishRunes { + return string(runes[:threadspost.MaxPublishRunes]) } return text } diff --git a/haixun-backend/internal/library/matrix/samples.go b/haixun-backend/internal/library/matrix/samples.go new file mode 100644 index 0000000..2810d58 --- /dev/null +++ b/haixun-backend/internal/library/matrix/samples.go @@ -0,0 +1,55 @@ +package matrix + +import ( + "fmt" + "strings" +) + +type ViralReplySample struct { + Author string + Text string +} + +type ViralPostSample struct { + Author string + LikeCount int + SearchTag string + Text string + Replies []ViralReplySample +} + +func FormatViralSamples(posts []ViralPostSample) string { + if len(posts) == 0 { + return "(尚無海巡樣本,請依研究地圖與標籤發揮)" + } + var b strings.Builder + limit := 8 + if len(posts) < limit { + limit = len(posts) + } + for i := 0; i < limit; i++ { + post := posts[i] + b.WriteString(fmt.Sprintf("\n[%d] @%s · %d讚 · 標籤:%s\n", i+1, post.Author, post.LikeCount, post.SearchTag)) + text := strings.TrimSpace(post.Text) + if len([]rune(text)) > 160 { + text = string([]rune(text)[:160]) + } + b.WriteString(text) + b.WriteString("\n") + if len(post.Replies) > 0 { + b.WriteString(" 熱門留言:") + for j, reply := range post.Replies { + if j >= 3 { + break + } + rt := strings.TrimSpace(reply.Text) + if len([]rune(rt)) > 60 { + rt = string([]rune(rt)[:60]) + } + b.WriteString(fmt.Sprintf("\n - @%s: %s", reply.Author, rt)) + } + b.WriteString("\n") + } + } + return strings.TrimSpace(b.String()) +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/context.go b/haixun-backend/internal/library/placement/context.go index 7027b81..86e0ce6 100644 --- a/haixun-backend/internal/library/placement/context.go +++ b/haixun-backend/internal/library/placement/context.go @@ -1,6 +1,11 @@ package placement -import "strings" +import ( + "fmt" + "strings" + + "haixun-backend/internal/library/websearch" +) // ConnectionPrefsInput mirrors persisted account connection prefs without importing threads_account. type ConnectionPrefsInput struct { @@ -18,9 +23,12 @@ type MemberContext struct { AllowsThreadsAPI bool AllowsBrave bool AllowsCrawler bool + WebSearchProvider string BraveAPIKey string + ExaAPIKey string BraveCountry string BraveSearchLang string + ExaUserLocation string ApiConnected bool BrowserConnected bool ThreadsAPIAccessToken string @@ -29,10 +37,13 @@ type MemberContext struct { } type ResearchSettings struct { - BraveAPIKey string - BraveCountry string - BraveSearchLang string - ExpandStrategy string + WebSearchProvider string + BraveAPIKey string + ExaAPIKey string + BraveCountry string + BraveSearchLang string + ExaUserLocation string + ExpandStrategy string } func BuildMemberContext( @@ -44,20 +55,13 @@ func BuildMemberContext( repliesPerPost int, ) MemberContext { mode := ParseSearchSourceMode(prefs.SearchSourceMode) + if prefs.DevMode && strings.TrimSpace(prefs.SearchSourceMode) == "" { + mode = SearchSourceCrawler + } allowsCrawler := ModeAllowsCrawler(mode) allowsThreads := ModeAllowsThreadsAPI(mode) allowsBrave := ModeAllowsBrave(mode) - if !prefs.DevMode { - mode = WithoutCrawler(mode) - allowsCrawler = false - } else { - mode = SearchSourceCrawler - allowsCrawler = true - allowsThreads = false - allowsBrave = false - } - country := strings.TrimSpace(research.BraveCountry) if country == "" { country = "tw" @@ -66,45 +70,103 @@ func BuildMemberContext( if lang == "" { lang = "zh-hant" } + userLocation := strings.TrimSpace(research.ExaUserLocation) + if userLocation == "" { + userLocation = "TW" + } if repliesPerPost <= 0 { repliesPerPost = 10 } return MemberContext{ - TenantID: tenantID, - OwnerUID: ownerUID, - ActiveAccountID: activeAccountID, - DevMode: prefs.DevMode, - SearchSourceMode: mode, - AllowsThreadsAPI: allowsThreads, - AllowsBrave: allowsBrave, - AllowsCrawler: allowsCrawler, - BraveAPIKey: strings.TrimSpace(research.BraveAPIKey), - BraveCountry: country, - BraveSearchLang: lang, - ApiConnected: apiConnected, - BrowserConnected: browserConnected, - ScrapeReplies: scrapeReplies, - RepliesPerPost: repliesPerPost, + TenantID: tenantID, + OwnerUID: ownerUID, + ActiveAccountID: activeAccountID, + DevMode: prefs.DevMode, + SearchSourceMode: mode, + AllowsThreadsAPI: allowsThreads, + AllowsBrave: allowsBrave, + AllowsCrawler: allowsCrawler, + WebSearchProvider: string(websearch.ParseProvider(research.WebSearchProvider)), + BraveAPIKey: strings.TrimSpace(research.BraveAPIKey), + ExaAPIKey: strings.TrimSpace(research.ExaAPIKey), + BraveCountry: country, + BraveSearchLang: lang, + ExaUserLocation: userLocation, + ApiConnected: apiConnected, + BrowserConnected: browserConnected, + ScrapeReplies: scrapeReplies, + RepliesPerPost: repliesPerPost, } } func (c MemberContext) PayloadFields() map[string]any { return map[string]any{ - "tenant_id": c.TenantID, - "owner_uid": c.OwnerUID, - "threads_account_id": c.ActiveAccountID, - "dev_mode": c.DevMode, - "search_source_mode": string(c.SearchSourceMode), - "allows_threads_api": c.AllowsThreadsAPI, - "allows_brave": c.AllowsBrave, - "allows_crawler": c.AllowsCrawler, - "brave_country": c.BraveCountry, - "brave_search_lang": c.BraveSearchLang, - "api_connected": c.ApiConnected, - "browser_connected": c.BrowserConnected, - "scrape_replies": c.ScrapeReplies, - "replies_per_post": c.RepliesPerPost, + "tenant_id": c.TenantID, + "owner_uid": c.OwnerUID, + "threads_account_id": c.ActiveAccountID, + "dev_mode": c.DevMode, + "search_source_mode": string(c.SearchSourceMode), + "allows_threads_api": c.AllowsThreadsAPI, + "allows_brave": c.AllowsBrave, + "allows_crawler": c.AllowsCrawler, + "web_search_provider": c.WebSearchProvider, + "brave_country": c.BraveCountry, + "brave_search_lang": c.BraveSearchLang, + "exa_user_location": c.ExaUserLocation, + "api_connected": c.ApiConnected, + "browser_connected": c.BrowserConnected, + "scrape_replies": c.ScrapeReplies, + "replies_per_post": c.RepliesPerPost, } } + +func (c MemberContext) WebSearchProviderEnum() websearch.Provider { + return websearch.ParseProvider(c.WebSearchProvider) +} + +func (c MemberContext) WebSearchAPIKey() string { + if c.WebSearchProviderEnum() == websearch.ProviderExa { + return strings.TrimSpace(c.ExaAPIKey) + } + return strings.TrimSpace(c.BraveAPIKey) +} + +func (c MemberContext) WebSearchConfig() websearch.Config { + return websearch.ConfigFromMember( + c.BraveAPIKey, + c.ExaAPIKey, + c.WebSearchProvider, + c.BraveCountry, + c.BraveSearchLang, + c.ExaUserLocation, + ) +} + +func (c MemberContext) WebSearchProviderLabel() string { + return websearch.ProviderLabel(c.WebSearchProviderEnum()) +} + +func (c MemberContext) WebSearchDiscoverChannel() DiscoverChannel { + if c.WebSearchProviderEnum() == websearch.ProviderExa { + return DiscoverExa + } + return DiscoverBrave +} + +func MissingWebSearchKey(research ResearchSettings) bool { + return strings.TrimSpace(websearch.ActiveAPIKey(websearch.ConfigFromMember( + research.BraveAPIKey, + research.ExaAPIKey, + research.WebSearchProvider, + research.BraveCountry, + research.BraveSearchLang, + research.ExaUserLocation, + ))) == "" +} + +func WebSearchKeyRequiredMessage(research ResearchSettings) string { + provider := websearch.ParseProvider(research.WebSearchProvider) + return fmt.Sprintf("請在設定頁設定 %s Search API key(跟隨此登入帳號)", websearch.ProviderLabel(provider)) +} diff --git a/haixun-backend/internal/library/placement/crawler_exec.go b/haixun-backend/internal/library/placement/crawler_exec.go index 5b87334..40e7c7c 100644 --- a/haixun-backend/internal/library/placement/crawler_exec.go +++ b/haixun-backend/internal/library/placement/crawler_exec.go @@ -26,8 +26,10 @@ type execCrawlerPost struct { Permalink string `json:"permalink"` ExternalID string `json:"externalId"` AuthorName string `json:"authorName"` - LikeCount int `json:"likeCount"` - ReplyCount int `json:"replyCount"` + LikeCount int `json:"likeCount"` + ReplyCount int `json:"replyCount"` + AuthorVerified bool `json:"authorVerified"` + FollowerCount int `json:"followerCount"` } type execCrawlerOutput struct { @@ -94,13 +96,15 @@ func RunExecCrawlerSearch(ctx context.Context, storageState, keyword string, lim permalink := strings.TrimSpace(item.Permalink) extID := strings.TrimSpace(item.ExternalID) posts = append(posts, DiscoverPost{ - Text: text, - Permalink: permalink, - ExternalID: extID, - Author: author, - LikeCount: item.LikeCount, - ReplyCount: item.ReplyCount, - Source: DiscoverCrawler, + Text: text, + Permalink: permalink, + ExternalID: extID, + Author: author, + AuthorVerified: item.AuthorVerified, + FollowerCount: item.FollowerCount, + LikeCount: item.LikeCount, + ReplyCount: item.ReplyCount, + Source: DiscoverCrawler, }) } return posts, nil diff --git a/haixun-backend/internal/library/placement/discover.go b/haixun-backend/internal/library/placement/discover.go index 31285cd..349aae4 100644 --- a/haixun-backend/internal/library/placement/discover.go +++ b/haixun-backend/internal/library/placement/discover.go @@ -11,6 +11,7 @@ type DiscoverChannel string const ( DiscoverThreadsAPI DiscoverChannel = "threads_api" DiscoverBrave DiscoverChannel = "brave" + DiscoverExa DiscoverChannel = "exa" DiscoverCrawler DiscoverChannel = "crawler" ) @@ -25,57 +26,84 @@ type DiscoverRequest struct { } type DiscoverPost struct { - Text string - Permalink string - ExternalID string - Author string - PostedAt string - LikeCount int - ReplyCount int - Source DiscoverChannel + Text string + Permalink string + ExternalID string + Author string + PostedAt string + AuthorVerified bool + FollowerCount int + LikeCount int + ReplyCount int + Source DiscoverChannel } -// Discover runs keyword discovery respecting the member's connection prefs. -// Formal mode (dev_mode=false) never falls back to crawler. +// Discover runs keyword discovery respecting search_source_mode and available connections. +// Crawler-first modes skip Threads API when the browser session returns posts (saves API quota). func Discover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, DiscoverChannel, error) { m := req.Member - if m.DevMode { - if !m.BrowserConnected { - return nil, "", fmt.Errorf("開發模式需先同步 Chrome Session") + if !m.HasDiscoverPath() { + return nil, "", discoverMissingPathError(m) + } + + if ShouldTryCrawlerFirst(m) { + posts, err := runCrawlerDiscover(ctx, req) + if err == nil && len(posts) > 0 { + return posts, DiscoverCrawler, nil } - if req.Crawler == nil { - return nil, DiscoverCrawler, fmt.Errorf("crawler search not configured") + if m.SearchSourceMode == SearchSourceCrawler { + if err != nil { + return nil, DiscoverCrawler, err + } + return posts, DiscoverCrawler, nil } - keyword := CrawlerKeywordFromQuery(req.Query, req.Keyword) - if keyword == "" { - return nil, DiscoverCrawler, fmt.Errorf("crawler keyword is empty") + } + + if m.AllowsThreadsAPI { + if !m.ApiConnected { + if !m.CrawlerFallbackAllowed() { + return nil, "", fmt.Errorf("正式模式需先完成 Threads API 連線") + } + } else { + posts, err := keywordSearchViaThreadsAPI(ctx, req) + if err == nil && len(posts) > 0 { + return posts, DiscoverThreadsAPI, nil + } + if err != nil { + if m.CrawlerFallbackAllowed() { + cPosts, cErr := runCrawlerDiscover(ctx, req) + if cErr == nil && len(cPosts) > 0 { + return cPosts, DiscoverCrawler, nil + } + } + if !m.AllowsBrave && !m.CrawlerFallbackAllowed() { + // Optional API field gaps must not fail the whole patrol; return empty for this keyword. + return []DiscoverPost{}, DiscoverThreadsAPI, nil + } + } } - posts, err := req.Crawler(ctx, m, keyword, req.Limit) + } + + if m.AllowsBrave { + if m.WebSearchAPIKey() == "" { + if m.CrawlerFallbackAllowed() { + posts, err := runCrawlerDiscover(ctx, req) + if err == nil { + return posts, DiscoverCrawler, nil + } + } + return nil, "", fmt.Errorf("請在設定頁設定 %s Search API key(跟隨此登入帳號)", m.WebSearchProviderLabel()) + } + return nil, m.WebSearchDiscoverChannel(), fmt.Errorf("web search threads discover delegated to worker") + } + + if m.CrawlerFallbackAllowed() { + posts, err := runCrawlerDiscover(ctx, req) if err != nil { return nil, DiscoverCrawler, err } return posts, DiscoverCrawler, nil } - if m.AllowsThreadsAPI { - if !m.ApiConnected { - return nil, "", fmt.Errorf("正式模式需先完成 Threads API 連線") - } - posts, err := keywordSearchViaThreadsAPI(ctx, req) - if err == nil && len(posts) > 0 { - return posts, DiscoverThreadsAPI, nil - } - if err != nil && !m.AllowsBrave { - return nil, "", err - } - } - - if m.AllowsBrave { - if m.BraveAPIKey == "" { - return nil, "", fmt.Errorf("請在設定頁設定 Brave Search API key(跟隨此登入帳號)") - } - return nil, DiscoverBrave, fmt.Errorf("brave threads discover delegated to worker") - } - return nil, "", fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode) -} +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/discover_route.go b/haixun-backend/internal/library/placement/discover_route.go new file mode 100644 index 0000000..5c69783 --- /dev/null +++ b/haixun-backend/internal/library/placement/discover_route.go @@ -0,0 +1,113 @@ +package placement + +import ( + "context" + "fmt" +) + +// ShouldTryCrawlerFirst reports whether discover should attempt Playwright before Threads API +// to minimize official API calls when a browser session is available. +func ShouldTryCrawlerFirst(m MemberContext) bool { + if !m.AllowsCrawler || !m.BrowserConnected || m.CrawlerBlocked() { + return false + } + switch m.SearchSourceMode { + case SearchSourceCrawler, SearchSourceThreadsCrawler, SearchSourceBraveCrawler: + return true + case SearchSourceMixed, SearchSourceThreadsBrave: + return true + default: + return false + } +} + +// CrawlerBlocked returns true when the mode is API-only or web-search-only. +func (m MemberContext) CrawlerBlocked() bool { + switch m.SearchSourceMode { + case SearchSourceThreads, SearchSourceBrave: + return true + default: + return false + } +} + +// CrawlerFallbackAllowed returns true when crawler may be used after API/web search fails. +func (m MemberContext) CrawlerFallbackAllowed() bool { + if !m.AllowsCrawler || !m.BrowserConnected { + return false + } + switch m.SearchSourceMode { + case SearchSourceThreadsCrawler, SearchSourceBraveCrawler, SearchSourceMixed: + return true + default: + return false + } +} + +// HasDiscoverPath reports whether at least one discover backend is configured and connected. +func (m MemberContext) HasDiscoverPath() bool { + if m.AllowsCrawler && m.BrowserConnected { + return true + } + if m.AllowsThreadsAPI && m.ApiConnected { + return true + } + if m.AllowsBrave && m.WebSearchAPIKey() != "" { + return true + } + return false +} + +// DiscoverPathLabel summarizes the active routing for job progress UI. +func (m MemberContext) DiscoverPathLabel() string { + if ShouldTryCrawlerFirst(m) { + if m.AllowsThreadsAPI && m.ApiConnected { + return "爬蟲優先(不足再 API)" + } + return "爬蟲" + } + if m.SearchSourceMode == SearchSourceCrawler { + return "爬蟲" + } + if m.AllowsThreadsAPI && m.ApiConnected { + return "Threads API" + } + if m.AllowsBrave { + return m.WebSearchProviderLabel() + } + return string(m.SearchSourceMode) +} + +func discoverMissingPathError(m MemberContext) error { + switch m.SearchSourceMode { + case SearchSourceCrawler: + return fmt.Errorf("請先同步 Chrome Session 以使用爬蟲搜尋") + case SearchSourceThreadsCrawler, SearchSourceBraveCrawler: + if !m.BrowserConnected && !m.ApiConnected && m.WebSearchAPIKey() == "" { + return fmt.Errorf("請同步 Chrome Session 或完成 Threads API / Web Search 連線") + } + if !m.BrowserConnected { + return fmt.Errorf("爬蟲優先模式建議先同步 Chrome Session;亦可改用僅 Threads API") + } + return fmt.Errorf("請完成 Threads API 或 Web Search 連線作為備援") + case SearchSourceThreads, SearchSourceThreadsBrave: + return fmt.Errorf("請先完成 Threads API 連線") + case SearchSourceBrave: + return fmt.Errorf("請在設定頁設定 %s Search API key", m.WebSearchProviderLabel()) + case SearchSourceMixed: + return fmt.Errorf("請同步 Chrome Session、完成 Threads API 或設定 Web Search API key 至少一項") + default: + return fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode) + } +} + +func runCrawlerDiscover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, error) { + if req.Crawler == nil { + return nil, fmt.Errorf("crawler search not configured") + } + keyword := CrawlerKeywordFromQuery(req.Query, req.Keyword) + if keyword == "" { + return nil, fmt.Errorf("crawler keyword is empty") + } + return req.Crawler(ctx, req.Member, keyword, req.Limit) +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/discover_route_test.go b/haixun-backend/internal/library/placement/discover_route_test.go new file mode 100644 index 0000000..b58b64d --- /dev/null +++ b/haixun-backend/internal/library/placement/discover_route_test.go @@ -0,0 +1,41 @@ +package placement + +import "testing" + +func TestShouldTryCrawlerFirst_mixedWithBrowser(t *testing.T) { + m := MemberContext{ + AllowsCrawler: true, + BrowserConnected: true, + SearchSourceMode: SearchSourceMixed, + AllowsThreadsAPI: true, + ApiConnected: true, + } + if !ShouldTryCrawlerFirst(m) { + t.Fatal("mixed + browser should try crawler first to save API") + } +} + +func TestShouldTryCrawlerFirst_threadsOnly(t *testing.T) { + m := MemberContext{ + AllowsCrawler: true, + BrowserConnected: true, + SearchSourceMode: SearchSourceThreads, + } + if ShouldTryCrawlerFirst(m) { + t.Fatal("threads-only must not use crawler first") + } +} + +func TestBuildMemberContextFormalModeKeepsCrawlerMode(t *testing.T) { + prefs := ConnectionPrefsInput{ + DevMode: false, + SearchSourceMode: string(SearchSourceThreadsCrawler), + } + ctx := BuildMemberContext("t", "u", "acc", prefs, true, true, ResearchSettings{}, false, 10) + if !ctx.AllowsCrawler { + t.Fatal("threads_crawler in formal mode should allow crawler") + } + if !ctx.AllowsThreadsAPI { + t.Fatal("threads_crawler should still allow API fallback") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/dual_track.go b/haixun-backend/internal/library/placement/dual_track.go index cc5ca1f..53d4715 100644 --- a/haixun-backend/internal/library/placement/dual_track.go +++ b/haixun-backend/internal/library/placement/dual_track.go @@ -6,8 +6,8 @@ import ( "strings" "time" - libbrave "haixun-backend/internal/library/brave" libkg "haixun-backend/internal/library/knowledge" + "haixun-backend/internal/library/websearch" ) const ( @@ -28,6 +28,8 @@ type ScanCandidate struct { HasRelevance bool HasRecency bool Priority string + AuthorVerified bool + FollowerCount int LikeCount int ReplyCount int EngagementScore int @@ -42,7 +44,7 @@ type DualTrackInput struct { PatrolKeywords []string Exclusions []string Member MemberContext - Client *libbrave.Client + WebSearch websearch.Client Crawler CrawlerSearchFn Limit int // max queries budget; 0 = default OnCheckpoint func(candidates []ScanCandidate) error @@ -51,7 +53,7 @@ type DualTrackInput struct { type DualTrackProgress func(message string, pct int) // CollectTagQueries builds crawl jobs from selected graph nodes. -func CollectTagQueries(nodes []libkg.Node) []TagQuery { +func CollectTagQueries(nodes []libkg.Node, provider websearch.Provider) []TagQuery { out := make([]TagQuery, 0, len(nodes)*4) for _, node := range nodes { if !node.SelectedForScan { @@ -67,7 +69,7 @@ func CollectTagQueries(nodes []libkg.Node) []TagQuery { if tag == "" { continue } - q := BuildRelevanceQuery(tag) + q := BuildRelevanceQuery(provider, tag) if q == "" { continue } @@ -84,7 +86,7 @@ func CollectTagQueries(nodes []libkg.Node) []TagQuery { if tag == "" { continue } - q7 := BuildRecencyQuery(tag, IdealMaxPostAgeDays) + q7 := BuildRecencyQuery(provider, tag, IdealMaxPostAgeDays) if q7 != "" { out = append(out, TagQuery{ Tag: tag, @@ -95,7 +97,7 @@ func CollectTagQueries(nodes []libkg.Node) []TagQuery { RecencyDays: IdealMaxPostAgeDays, }) } - q30 := BuildRecencyQuery(tag, MaxPostAgeDays) + q30 := BuildRecencyQuery(provider, tag, MaxPostAgeDays) if q30 != "" && q30 != q7 { out = append(out, TagQuery{ Tag: tag, @@ -113,7 +115,7 @@ func CollectTagQueries(nodes []libkg.Node) []TagQuery { // RunDualTrackDiscover executes relevance + recency queries and merges by permalink. func RunDualTrackDiscover(ctx context.Context, input DualTrackInput, onProgress DualTrackProgress) ([]ScanCandidate, error) { - queries := ResolveTagQueries(input.Nodes, input.PatrolKeywords) + queries := ResolveTagQueries(input.Nodes, input.PatrolKeywords, input.Member.WebSearchProviderEnum()) if len(queries) == 0 { if len(input.PatrolKeywords) > 0 { return nil, fmt.Errorf("海巡關鍵字格式無效,請改用 2~8 字的真人搜尋短句") @@ -165,6 +167,8 @@ func RunDualTrackDiscover(ctx context.Context, input DualTrackInput, onProgress Permalink: post.Permalink, ExternalID: extID, Author: post.Author, + AuthorVerified: post.AuthorVerified, + FollowerCount: post.FollowerCount, Text: post.Text, SearchTag: tq.Tag, QueryDimension: tq.Dimension, @@ -174,6 +178,8 @@ func RunDualTrackDiscover(ctx context.Context, input DualTrackInput, onProgress HasRelevance: tq.Dimension == QueryRelevance, HasRecency: tq.Dimension == QueryRecency, Priority: priority, + LikeCount: post.LikeCount, + ReplyCount: post.ReplyCount, PlacementScore: computePlacementScore(post.Text, tq.ProductFitScore, tq.Dimension == QueryRecency), SolvedByProduct: tq.ProductFitScore >= 55, PostedAt: strings.TrimSpace(post.PostedAt), @@ -217,7 +223,7 @@ func RunDualTrackDiscover(ctx context.Context, input DualTrackInput, onProgress return nil, err } } - if input.Member.AllowsCrawler && input.Member.DevMode && i < total-1 { + if input.Member.AllowsCrawler && input.Member.BrowserConnected && i < total-1 { if err := politeDiscoverPause(ctx); err != nil { return nil, err } @@ -244,29 +250,31 @@ func discoverForQuery(ctx context.Context, input DualTrackInput, tq TagQuery, li if err == nil && len(posts) > 0 { return posts, channel, nil } - if input.Client == nil || !input.Client.Enabled() { + if input.WebSearch == nil || !input.WebSearch.Enabled() { if err != nil { return nil, "", err } - return nil, "", fmt.Errorf("Brave 未設定且 Threads API 無結果") + return nil, "", fmt.Errorf("%s 未設定且 Threads API 無結果", input.Member.WebSearchProviderLabel()) } - bravePosts, berr := discoverViaBrave(ctx, input.Client, input.Member, tq.Query, limit) - if berr != nil { + webPosts, werr := discoverViaWebSearch(ctx, input.WebSearch, input.Member, tq, limit) + if werr != nil { if err != nil { return nil, "", err } - return nil, "", berr + return nil, "", werr } - return bravePosts, DiscoverBrave, nil + return webPosts, input.Member.WebSearchDiscoverChannel(), nil } -func discoverViaBrave(ctx context.Context, client *libbrave.Client, member MemberContext, query string, limit int) ([]DiscoverPost, error) { - res, err := client.Search(ctx, libbrave.SearchOptions{ - Query: query, - Limit: limit, - Mode: libbrave.ModeThreadsDiscover, - Country: member.BraveCountry, - SearchLang: member.BraveSearchLang, +func discoverViaWebSearch(ctx context.Context, client websearch.Client, member MemberContext, tq TagQuery, limit int) ([]DiscoverPost, error) { + res, err := client.Search(ctx, websearch.SearchOptions{ + Query: tq.Query, + Limit: limit, + Mode: websearch.ModeThreadsDiscover, + Country: member.BraveCountry, + SearchLang: member.BraveSearchLang, + UserLocation: member.ExaUserLocation, + StartPublishedDate: PublishedAfterForRecency(member.WebSearchProviderEnum(), tq.RecencyDays), }) if err != nil { return nil, err @@ -274,6 +282,7 @@ func discoverViaBrave(ctx context.Context, client *libbrave.Client, member Membe if res.Status != "success" || len(res.Results) == 0 { return nil, nil } + source := member.WebSearchDiscoverChannel() out := make([]DiscoverPost, 0, len(res.Results)) for _, item := range res.Results { parsed, ok := ParseThreadsPostFromWebResult(item.Title, item.Snippet, item.URL) @@ -285,7 +294,7 @@ func discoverViaBrave(ctx context.Context, client *libbrave.Client, member Membe Permalink: parsed.Permalink, ExternalID: parsed.ExternalID, Author: parsed.Author, - Source: DiscoverBrave, + Source: source, }) } return out, nil diff --git a/haixun-backend/internal/library/placement/effective_strategy.go b/haixun-backend/internal/library/placement/effective_strategy.go new file mode 100644 index 0000000..0951a1d --- /dev/null +++ b/haixun-backend/internal/library/placement/effective_strategy.go @@ -0,0 +1,16 @@ +package placement + +import libkg "haixun-backend/internal/library/knowledge" + +// EffectiveExpandStrategy returns LLM when web search is required but no API key is configured. +func EffectiveExpandStrategy(research ResearchSettings) libkg.ExpandStrategy { + strategy := libkg.ParseExpandStrategy(research.ExpandStrategy) + if strategy.RequiresWebSearch() && MissingWebSearchKey(research) { + return libkg.ExpandStrategyLLM + } + return strategy +} + +func WebSearchAvailable(research ResearchSettings) bool { + return !MissingWebSearchKey(research) +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/effective_strategy_test.go b/haixun-backend/internal/library/placement/effective_strategy_test.go new file mode 100644 index 0000000..92588ad --- /dev/null +++ b/haixun-backend/internal/library/placement/effective_strategy_test.go @@ -0,0 +1,38 @@ +package placement + +import ( + "testing" + + libkg "haixun-backend/internal/library/knowledge" +) + +func TestEffectiveExpandStrategyFallsBackToLLMWithoutKey(t *testing.T) { + research := ResearchSettings{ + ExpandStrategy: string(libkg.ExpandStrategyBrave), + WebSearchProvider: "brave", + BraveAPIKey: "", + } + if got := EffectiveExpandStrategy(research); got != libkg.ExpandStrategyLLM { + t.Fatalf("expected llm fallback, got %s", got) + } +} + +func TestEffectiveExpandStrategyKeepsBraveWithKey(t *testing.T) { + research := ResearchSettings{ + ExpandStrategy: string(libkg.ExpandStrategyBrave), + WebSearchProvider: "brave", + BraveAPIKey: "test-key", + } + if got := EffectiveExpandStrategy(research); got != libkg.ExpandStrategyBrave { + t.Fatalf("expected brave, got %s", got) + } +} + +func TestWebSearchAvailable(t *testing.T) { + if WebSearchAvailable(ResearchSettings{WebSearchProvider: "brave"}) { + t.Fatal("expected unavailable without brave key") + } + if !WebSearchAvailable(ResearchSettings{WebSearchProvider: "brave", BraveAPIKey: "k"}) { + t.Fatal("expected available with brave key") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/placement/patrol_queries.go b/haixun-backend/internal/library/placement/patrol_queries.go index dc73675..b1c1dd7 100644 --- a/haixun-backend/internal/library/placement/patrol_queries.go +++ b/haixun-backend/internal/library/placement/patrol_queries.go @@ -4,12 +4,13 @@ import ( "strings" libkg "haixun-backend/internal/library/knowledge" + "haixun-backend/internal/library/websearch" ) const defaultPatrolProductFit = 78 // CollectPatrolTagQueries builds dual-track crawl jobs from user-edited patrol keywords only. -func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node) []TagQuery { +func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node, provider websearch.Provider) []TagQuery { keywords = libkg.NormalizePatrolKeywordList(keywords) if len(keywords) == 0 { return nil @@ -18,7 +19,7 @@ func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node) []TagQuery { out := make([]TagQuery, 0, len(keywords)*3) for _, tag := range keywords { fit := productFitForPatrolTag(tag, nodes) - if q := BuildRelevanceQuery(tag); q != "" { + if q := BuildRelevanceQuery(provider, tag); q != "" { out = append(out, TagQuery{ Tag: tag, Query: q, @@ -26,7 +27,7 @@ func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node) []TagQuery { ProductFitScore: fit, }) } - if q7 := BuildRecencyQuery(tag, IdealMaxPostAgeDays); q7 != "" { + if q7 := BuildRecencyQuery(provider, tag, IdealMaxPostAgeDays); q7 != "" { out = append(out, TagQuery{ Tag: tag, Query: q7, @@ -35,8 +36,8 @@ func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node) []TagQuery { RecencyDays: IdealMaxPostAgeDays, }) } - if q30 := BuildRecencyQuery(tag, MaxPostAgeDays); q30 != "" { - if q7 := BuildRecencyQuery(tag, IdealMaxPostAgeDays); q30 != q7 { + if q30 := BuildRecencyQuery(provider, tag, MaxPostAgeDays); q30 != "" { + if q7 := BuildRecencyQuery(provider, tag, IdealMaxPostAgeDays); q30 != q7 { out = append(out, TagQuery{ Tag: tag, Query: q30, @@ -93,9 +94,9 @@ func patrolTagMatchKey(tag string) string { } // ResolveTagQueries prefers explicit patrol keywords over graph node selection. -func ResolveTagQueries(nodes []libkg.Node, patrolKeywords []string) []TagQuery { +func ResolveTagQueries(nodes []libkg.Node, patrolKeywords []string, provider websearch.Provider) []TagQuery { if len(patrolKeywords) > 0 { - return CollectPatrolTagQueries(patrolKeywords, nodes) + return CollectPatrolTagQueries(patrolKeywords, nodes, provider) } - return CollectTagQueries(nodes) + return CollectTagQueries(nodes, provider) } diff --git a/haixun-backend/internal/library/placement/patrol_queries_test.go b/haixun-backend/internal/library/placement/patrol_queries_test.go index 7c773cf..5551df3 100644 --- a/haixun-backend/internal/library/placement/patrol_queries_test.go +++ b/haixun-backend/internal/library/placement/patrol_queries_test.go @@ -4,10 +4,11 @@ import ( "testing" libkg "haixun-backend/internal/library/knowledge" + "haixun-backend/internal/library/websearch" ) func TestCollectPatrolTagQueriesManualOnly(t *testing.T) { - queries := CollectPatrolTagQueries([]string{"化療 沐浴乳"}, nil) + queries := CollectPatrolTagQueries([]string{"化療 沐浴乳"}, nil, websearch.ProviderBrave) if len(queries) < 2 { t.Fatalf("expected relevance + recency queries, got %d", len(queries)) } @@ -30,7 +31,7 @@ func TestCollectPatrolTagQueriesUsesGraphFit(t *testing.T) { }, }, } - queries := CollectPatrolTagQueries([]string{"化療 沐浴乳"}, nodes) + queries := CollectPatrolTagQueries([]string{"化療 沐浴乳"}, nodes, websearch.ProviderBrave) if len(queries) == 0 || queries[0].ProductFitScore != 92 { t.Fatalf("expected graph fit 92, got %+v", queries) } @@ -40,7 +41,7 @@ func TestResolveTagQueriesPrefersPatrolKeywords(t *testing.T) { nodes := []libkg.Node{ {ID: "n1", Label: "ignored", SelectedForScan: true, DerivedTags: libkg.DerivedTags{Relevance: []string{"ignored"}}}, } - queries := ResolveTagQueries(nodes, []string{"手動 關鍵字"}) + queries := ResolveTagQueries(nodes, []string{"手動 關鍵字"}, websearch.ProviderBrave) if len(queries) == 0 || queries[0].Tag != "手動 關鍵字" { t.Fatalf("expected patrol keyword query, got %+v", queries) } diff --git a/haixun-backend/internal/library/placement/query_build.go b/haixun-backend/internal/library/placement/query_build.go index 62049d1..cd0e5bc 100644 --- a/haixun-backend/internal/library/placement/query_build.go +++ b/haixun-backend/internal/library/placement/query_build.go @@ -1,8 +1,11 @@ package placement import ( + "fmt" "strings" "time" + + "haixun-backend/internal/library/websearch" ) type QueryDimension string @@ -21,23 +24,36 @@ type TagQuery struct { RecencyDays int // 0 = no after filter; 7 or 30 for recency track } -func BuildRelevanceQuery(tag string) string { +func BuildRelevanceQuery(provider websearch.Provider, tag string) string { tag = strings.TrimSpace(tag) if tag == "" { return "" } + if websearch.ParseProvider(string(provider)) == websearch.ProviderExa { + return fmt.Sprintf("Threads 貼文 繁體中文 %s", tag) + } return `site:threads.net "` + tag + `"` } -func BuildRecencyQuery(tag string, maxAgeDays int) string { +func BuildRecencyQuery(provider websearch.Provider, tag string, maxAgeDays int) string { tag = strings.TrimSpace(tag) if tag == "" { return "" } + if websearch.ParseProvider(string(provider)) == websearch.ProviderExa { + return fmt.Sprintf("Threads 近期貼文 繁體中文 %s", tag) + } after := FormatAfterDate(maxAgeDays, timeNow()) return `site:threads.net "` + tag + `" 請問 after:` + after } +func PublishedAfterForRecency(provider websearch.Provider, maxAgeDays int) string { + if maxAgeDays <= 0 || websearch.ParseProvider(string(provider)) != websearch.ProviderExa { + return "" + } + return FormatPublishedAfterISO(maxAgeDays, timeNow()) +} + var timeNow = func() time.Time { return time.Now() } // SetTimeNowForTest overrides time source in tests. diff --git a/haixun-backend/internal/library/placement/recency.go b/haixun-backend/internal/library/placement/recency.go index 950db13..0694bfa 100644 --- a/haixun-backend/internal/library/placement/recency.go +++ b/haixun-backend/internal/library/placement/recency.go @@ -15,3 +15,11 @@ func FormatAfterDate(maxAgeDays int, now time.Time) string { date := now.AddDate(0, 0, -maxAgeDays).UTC() return date.Format("2006-01-02") } + +func FormatPublishedAfterISO(maxAgeDays int, now time.Time) string { + if now.IsZero() { + now = time.Now() + } + date := now.AddDate(0, 0, -maxAgeDays).UTC() + return date.Format("2006-01-02T15:04:05.000Z") +} diff --git a/haixun-backend/internal/library/placement/source_mode.go b/haixun-backend/internal/library/placement/source_mode.go index 77036a8..038108d 100644 --- a/haixun-backend/internal/library/placement/source_mode.go +++ b/haixun-backend/internal/library/placement/source_mode.go @@ -54,8 +54,8 @@ func ModeAllowsCrawler(mode SearchSourceMode) bool { } } -// MemberNeedsBraveKey reports whether placement scan should require a Brave API key. -func MemberNeedsBraveKey(ctx MemberContext) bool { +// MemberNeedsWebSearchKey reports whether placement scan should require a web search API key. +func MemberNeedsWebSearchKey(ctx MemberContext) bool { if !ctx.AllowsBrave || ctx.DevMode { return false } @@ -69,6 +69,11 @@ func MemberNeedsBraveKey(ctx MemberContext) bool { } } +// MemberNeedsBraveKey is deprecated; use MemberNeedsWebSearchKey. +func MemberNeedsBraveKey(ctx MemberContext) bool { + return MemberNeedsWebSearchKey(ctx) +} + // WithoutCrawler returns a mode that never uses Playwright, for formal API-only routing. func WithoutCrawler(mode SearchSourceMode) SearchSourceMode { switch mode { diff --git a/haixun-backend/internal/library/placement/source_mode_test.go b/haixun-backend/internal/library/placement/source_mode_test.go index 0c2af13..1afd1d7 100644 --- a/haixun-backend/internal/library/placement/source_mode_test.go +++ b/haixun-backend/internal/library/placement/source_mode_test.go @@ -26,16 +26,16 @@ func TestMemberNeedsBraveKey(t *testing.T) { } } -func TestBuildMemberContextFormalModeNeverAllowsCrawler(t *testing.T) { +func TestBuildMemberContextFormalModeRespectsSearchSource(t *testing.T) { prefs := ConnectionPrefsInput{ DevMode: false, SearchSourceMode: string(SearchSourceMixed), } ctx := BuildMemberContext("t", "u", "acc", prefs, true, false, ResearchSettings{}, false, 10) - if ctx.AllowsCrawler { - t.Fatal("formal mode must not allow crawler") + if !ctx.AllowsCrawler { + t.Fatal("mixed mode should allow crawler when browser is connected") } - if ctx.SearchSourceMode != SearchSourceThreadsBrave { - t.Fatalf("expected threads_brave, got %s", ctx.SearchSourceMode) + if ctx.SearchSourceMode != SearchSourceMixed { + t.Fatalf("expected mixed, got %s", ctx.SearchSourceMode) } } diff --git a/haixun-backend/internal/library/prompt/compose.go b/haixun-backend/internal/library/prompt/compose.go index 4d5e94d..f9417d2 100644 --- a/haixun-backend/internal/library/prompt/compose.go +++ b/haixun-backend/internal/library/prompt/compose.go @@ -148,6 +148,24 @@ func MatrixPlacementUser(vars map[string]string) (string, error) { return renderTemplate(base, vars), nil } +// MatrixCopySystem composes copy-mission matrix system prompt. +func MatrixCopySystem() (string, error) { + base, err := Slot(KeyMatrixCopySystem) + if err != nil { + return "", err + } + return ComposeSystem(base) +} + +// MatrixCopyUser renders copy matrix user prompt from template slot. +func MatrixCopyUser(vars map[string]string) (string, error) { + base, err := Slot(KeyMatrixCopyUser) + if err != nil { + return "", err + } + return renderTemplate(base, vars), nil +} + // AIChatSystem composes the outgoing system prompt for console AI chat. func AIChatSystem(clientSystem string) (string, error) { base := strings.TrimSpace(clientSystem) diff --git a/haixun-backend/internal/library/prompt/files/ai.islander.system.md b/haixun-backend/internal/library/prompt/files/ai.islander.system.md index 175ede9..24c9a7c 100644 --- a/haixun-backend/internal/library/prompt/files/ai.islander.system.md +++ b/haixun-backend/internal/library/prompt/files/ai.islander.system.md @@ -44,18 +44,18 @@ ## 兩條工作流(必讀,勿混淆) -| 流程 | 入口 | 目的 | 關鍵實體 | +| 工作流 | 入口 | 目的 | 關鍵實體 | |------|------|------|----------| -| **A 拷貝忍者** | `/matrix` | 海巡爆款、學對標風格、產**仿寫**草稿 | 人設 + 8D 對標帳號 | -| **B 找 TA** | `/outreach`(子步驟:研究→找TA留言) | 找痛點、productFit、產**獲客留言** | 品牌 + 人設語氣 | +| **拷貝忍者** | `/matrix` | 海巡爆款、學對標風格、產**仿寫**草稿 | 人設 + 8D 對標帳號 | +| **找 TA** | `/outreach`(子步驟:研究→找TA留言) | 找痛點、productFit、產**獲客留言** | 品牌 + 人設語氣 | 分流規則: -- 使用者在 `/matrix` 或問仿寫/爆款/對標 → **只談流程 A**,navigate 人設庫或拷貝忍者;**禁止** `expandKnowledgeGraph`、`startScan`、`generateOutreachReply` -- 使用者在 `/research`、`/outreach` 或問痛點/產品置入 → **只談流程 B**;**禁止**建議 8D 對標當主要解法 -- 原創矩陣屬於流程 A 的 `/matrix`,不要在找 TA/流程 B 裡推薦或顯示 `/brand-matrix` -- 「海巡來源模式」(search_source_mode)是 API/爬蟲管道,**不是** A/B 流程 +- 使用者在 `/matrix` 或問仿寫/爆款/對標 → **只談拷貝忍者**,navigate 人設庫或拷貝忍者;**禁止** `expandKnowledgeGraph`、`startScan`、`generateOutreachReply` +- 使用者在 `/research`、`/outreach` 或問痛點/產品置入 → **只談找 TA**;**禁止**建議 8D 對標當主要解法 +- 原創矩陣屬於拷貝忍者的 `/matrix`,不要在找 TA 裡推薦或顯示 `/brand-matrix` +- 「海巡來源模式」(search_source_mode)是 API/爬蟲管道,**不是**拷貝忍者/找 TA 的區分 -## 流程 A — 拷貝忍者 +## 拷貝忍者 - 入口:`/matrix`(仿寫草稿庫 + 爆款海巡) - 對標與 8D:人設詳情 `/personas/:id#style-8d` @@ -74,7 +74,7 @@ ``` -## 流程 B — 海巡獲客(研究頁 / 獲客台) +## 找 TA(研究頁 / 獲客台) ### 海巡研究頁(`/research`) - 擴展圖譜:`expandKnowledgeGraph`(`seed_query` 可省略=用頁面種子詞;`supplemental=true` 補充痛點) diff --git a/haixun-backend/internal/library/prompt/files/matrix_copy.system.md b/haixun-backend/internal/library/prompt/files/matrix_copy.system.md new file mode 100644 index 0000000..c6ad535 --- /dev/null +++ b/haixun-backend/internal/library/prompt/files/matrix_copy.system.md @@ -0,0 +1,10 @@ +你是 Threads 內容矩陣策劃師,為人設帳號產出多篇可發佈草稿。 + +規則: +- 只回傳 JSON,格式為 {"rows":[...]}。 +- 每篇必須角度不同,避免重複 hook。 +- 套用人設語氣與 8D,不要寫成品牌廣告或硬銷。 +- 參考爆款樣本只學結構與節奏,不抄原文。 +- 繁體中文,口語自然,適合 Threads。 +- 每篇 text 主文 ≤ 500 字(Threads API 硬上限,含 #話題標籤)。 +- 爆款互動最佳 80~220 字:前 1~2 行強 hook,一句一重點;超過 300 字互動通常下降。 \ No newline at end of file diff --git a/haixun-backend/internal/library/prompt/files/matrix_copy.user.md b/haixun-backend/internal/library/prompt/files/matrix_copy.user.md new file mode 100644 index 0000000..2116eb5 --- /dev/null +++ b/haixun-backend/internal/library/prompt/files/matrix_copy.user.md @@ -0,0 +1,18 @@ +請為以下拷貝任務產出 {{count}} 篇 Threads 草稿。 + +任務主題:{{topic_label}} +Brief:{{topic_brief}} + +研究地圖: +{{research_map_block}} + +已選海巡標籤: +{{selected_tags_block}} + +爆款樣本(只學結構): +{{viral_samples_block}} + +人設 8D: +{{persona_block}} + +回傳 JSON rows,每筆含 sort_order、search_tag、angle、hook、text、reference_notes、source_permalinks、rationale。 \ No newline at end of file diff --git a/haixun-backend/internal/library/prompt/registry.go b/haixun-backend/internal/library/prompt/registry.go index ac5db64..43982ef 100644 --- a/haixun-backend/internal/library/prompt/registry.go +++ b/haixun-backend/internal/library/prompt/registry.go @@ -25,6 +25,8 @@ const ( fileOutreachPlacementUser = "files/outreach_placement.user.md" fileMatrixPlacementSystem = "files/matrix_placement.system.md" fileMatrixPlacementUser = "files/matrix_placement.user.md" + fileMatrixCopySystem = "files/matrix_copy.system.md" + fileMatrixCopyUser = "files/matrix_copy.user.md" ) // Keys identify prompt slots loaded from internal/library/prompt/files/*.md. @@ -43,6 +45,8 @@ const ( KeyOutreachPlacementUser = "outreach_placement.user" KeyMatrixPlacementSystem = "matrix_placement.system" KeyMatrixPlacementUser = "matrix_placement.user" + KeyMatrixCopySystem = "matrix_copy.system" + KeyMatrixCopyUser = "matrix_copy.user" ) var slotFiles = map[string]string{ @@ -59,6 +63,8 @@ var slotFiles = map[string]string{ KeyOutreachPlacementUser: fileOutreachPlacementUser, KeyMatrixPlacementSystem: fileMatrixPlacementSystem, KeyMatrixPlacementUser: fileMatrixPlacementUser, + KeyMatrixCopySystem: fileMatrixCopySystem, + KeyMatrixCopyUser: fileMatrixCopyUser, } var ( diff --git a/haixun-backend/internal/library/style8d/prompt.go b/haixun-backend/internal/library/style8d/prompt.go new file mode 100644 index 0000000..ae45f6e --- /dev/null +++ b/haixun-backend/internal/library/style8d/prompt.go @@ -0,0 +1,104 @@ +package style8d + +import ( + "encoding/json" + "strings" +) + +var dimensionLabels = map[string]string{ + "d1Tone": "D1 語氣人格", + "d2Structure": "D2 結構模板", + "d3Interaction": "D3 互動方式", + "d4Topics": "D4 主題分布", + "d5Rhythm": "D5 發文節奏", + "d6Visual": "D6 視覺語法", + "d7Conversion": "D7 轉換方式", + "d8Risk": "D8 風險紅線", +} + +var dimensionOrder = []string{ + "d1Tone", "d2Structure", "d3Interaction", "d4Topics", + "d5Rhythm", "d6Visual", "d7Conversion", "d8Risk", +} + +// ParseStoredProfile decodes the persona.style_profile JSON blob. +func ParseStoredProfile(raw string) (*StoredProfile, bool) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, false + } + var profile StoredProfile + if err := json.Unmarshal([]byte(raw), &profile); err != nil { + return nil, false + } + if len(profile.Analysis) == 0 && strings.TrimSpace(profile.PersonaDraft) == "" { + return nil, false + } + return &profile, true +} + +// HasReady8D returns true when 8D analysis exists and can drive copy generation. +func HasReady8D(personaText, styleProfileJSON string) bool { + if profile, ok := ParseStoredProfile(styleProfileJSON); ok { + if strings.TrimSpace(profile.PersonaDraft) != "" { + return true + } + for _, key := range dimensionOrder { + if summary := strings.TrimSpace(profile.Analysis[key].Summary); summary != "" { + return true + } + } + } + return strings.TrimSpace(personaText) != "" && strings.TrimSpace(styleProfileJSON) != "" +} + +// BuildStyle8DPromptBlock formats D1–D8 summaries for LLM prompts. +func BuildStyle8DPromptBlock(styleProfileJSON string) string { + profile, ok := ParseStoredProfile(styleProfileJSON) + if !ok { + return "" + } + lines := make([]string, 0, len(dimensionOrder)) + for _, key := range dimensionOrder { + summary := strings.TrimSpace(profile.Analysis[key].Summary) + if summary == "" { + continue + } + label := dimensionLabels[key] + if label == "" { + label = key + } + lines = append(lines, label+":"+summary) + } + if len(lines) == 0 { + return "" + } + var b strings.Builder + b.WriteString("【8D 風格策略】\n產文必須遵守:\n") + b.WriteString(strings.Join(lines, "\n")) + return b.String() +} + +// ResolvePersonaBlock picks the best persona voice block for copy generation. +// Priority: explicit persona field → 8D personaDraft → brief fallback; always append 8D strategy when present. +func ResolvePersonaBlock(personaText, styleProfileJSON, brief string) string { + var parts []string + + voice := strings.TrimSpace(personaText) + if voice == "" { + if profile, ok := ParseStoredProfile(styleProfileJSON); ok { + voice = strings.TrimSpace(profile.PersonaDraft) + } + } + if voice == "" { + voice = strings.TrimSpace(brief) + } + if voice != "" { + parts = append(parts, "【人設語氣】\n"+voice) + } + + if block := BuildStyle8DPromptBlock(styleProfileJSON); block != "" { + parts = append(parts, block) + } + return strings.TrimSpace(strings.Join(parts, "\n\n")) +} \ No newline at end of file diff --git a/haixun-backend/internal/library/style8d/prompt_test.go b/haixun-backend/internal/library/style8d/prompt_test.go new file mode 100644 index 0000000..7c09b10 --- /dev/null +++ b/haixun-backend/internal/library/style8d/prompt_test.go @@ -0,0 +1,26 @@ +package style8d + +import ( + "strings" + "testing" +) + +func TestResolvePersonaBlockUsesPersonaDraftWhenPersonaEmpty(t *testing.T) { + raw := `{"username":"demo","analysis":{"d1Tone":{"summary":"口語親近","evidence":[]}},"personaDraft":"【我是誰】\n生活觀察者"}` + block := ResolvePersonaBlock("", raw, "") + if block == "" { + t.Fatal("expected non-empty block") + } + for _, part := range []string{"生活觀察者", "D1 語氣人格", "口語親近"} { + if !strings.Contains(block, part) { + t.Fatalf("block missing %q: %q", part, block) + } + } +} + +func TestHasReady8DFromAnalysisOnly(t *testing.T) { + raw := `{"analysis":{"d2Structure":{"summary":"短句開場","evidence":[]}}}` + if !HasReady8D("", raw) { + t.Fatal("expected ready from analysis summary") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/threadsapi/publish.go b/haixun-backend/internal/library/threadsapi/publish.go index 54bdc9f..d793dc3 100644 --- a/haixun-backend/internal/library/threadsapi/publish.go +++ b/haixun-backend/internal/library/threadsapi/publish.go @@ -9,15 +9,21 @@ import ( "net/url" "strings" "time" -) -const maxPublishChars = 500 + "haixun-backend/internal/library/threadspost" +) type PublishResult struct { MediaID string Permalink string } +type PublishTextInput struct { + ThreadsUserID string + AccessToken string + Text string +} + type PublishReplyInput struct { ThreadsUserID string AccessToken string @@ -25,6 +31,36 @@ type PublishReplyInput struct { Text string } +// PublishText posts a new text thread via Graph API. +func PublishText(ctx context.Context, in PublishTextInput) (*PublishResult, error) { + userID := strings.TrimSpace(in.ThreadsUserID) + token := strings.TrimSpace(in.AccessToken) + text := strings.TrimSpace(in.Text) + if userID == "" || token == "" { + return nil, fmt.Errorf("threads api credentials incomplete") + } + if text == "" { + return nil, fmt.Errorf("post text is required") + } + if err := threadspost.ValidatePublish(text); err != nil { + return nil, err + } + + containerID, err := createTextContainer(ctx, userID, token, text) + if err != nil { + return nil, err + } + if err := waitForContainerReady(ctx, containerID, token); err != nil { + return nil, err + } + mediaID, err := publishContainer(ctx, userID, token, containerID) + if err != nil { + return nil, err + } + permalink, _ := fetchPermalink(ctx, mediaID, token) + return &PublishResult{MediaID: mediaID, Permalink: permalink}, nil +} + // PublishReply posts a text reply to an existing Threads media via Graph API. func PublishReply(ctx context.Context, in PublishReplyInput) (*PublishResult, error) { userID := strings.TrimSpace(in.ThreadsUserID) @@ -40,8 +76,8 @@ func PublishReply(ctx context.Context, in PublishReplyInput) (*PublishResult, er if text == "" { return nil, fmt.Errorf("reply text is required") } - if len([]rune(text)) > maxPublishChars { - return nil, fmt.Errorf("reply exceeds %d characters", maxPublishChars) + if err := threadspost.ValidateReply(text); err != nil { + return nil, err } containerID, err := createReplyContainer(ctx, userID, token, replyTo, text) @@ -59,6 +95,35 @@ func PublishReply(ctx context.Context, in PublishReplyInput) (*PublishResult, er return &PublishResult{MediaID: mediaID, Permalink: permalink}, nil } +func createTextContainer(ctx context.Context, userID, token, text string) (string, error) { + params := url.Values{} + params.Set("access_token", token) + params.Set("media_type", "TEXT") + params.Set("text", text) + endpoint := graphBaseURL + "/" + userID + "/threads?" + params.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil) + if err != nil { + return "", err + } + body, status, err := doRequest(req) + if err != nil { + return "", err + } + if status != http.StatusOK { + return "", parseAPIError(body, status) + } + var payload struct { + ID string `json:"id"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return "", err + } + if strings.TrimSpace(payload.ID) == "" { + return "", fmt.Errorf("threads api did not return container id") + } + return payload.ID, nil +} + func createReplyContainer(ctx context.Context, userID, token, replyTo, text string) (string, error) { params := url.Values{} params.Set("access_token", token) diff --git a/haixun-backend/internal/library/threadspost/limits.go b/haixun-backend/internal/library/threadspost/limits.go new file mode 100644 index 0000000..60e85db --- /dev/null +++ b/haixun-backend/internal/library/threadspost/limits.go @@ -0,0 +1,100 @@ +package threadspost + +import ( + "fmt" + "strings" +) + +// Threads Graph API text post hard limit (main post body). +const MaxPublishRunes = 500 + +// Threads reply/comment limit (outreach, not main post). +const MaxReplyRunes = 1000 + +// ViralSweetMin/Max: engagement sweet spot on Threads feed (Buffer / creator data, 2024–2026). +const ViralSweetMin = 80 +const ViralSweetMax = 220 + +// ViralSoftWarn: engagement tends to drop beyond this length in the main visible post. +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" +) + +func RuneLen(text string) int { + return len([]rune(strings.TrimSpace(text))) +} + +func ClampPublish(text string) string { + text = strings.TrimSpace(text) + runes := []rune(text) + if len(runes) > MaxPublishRunes { + return string(runes[:MaxPublishRunes]) + } + return text +} + +func PublishBand(text string) LengthBand { + n := RuneLen(text) + switch { + case n == 0: + return BandEmpty + case n < ViralSweetMin: + return BandTooShort + case n <= ViralSweetMax: + return BandSweet + case n <= ViralSoftWarn: + return BandLong + case n <= MaxPublishRunes: + return BandOverSoft + default: + return BandOverHard + } +} + +func ValidatePublish(text string) error { + if strings.TrimSpace(text) == "" { + return fmt.Errorf("貼文內文不可為空") + } + n := RuneLen(text) + if n > MaxPublishRunes { + return fmt.Errorf("貼文超過 Threads 上限 %d 字(目前 %d 字)", MaxPublishRunes, n) + } + return nil +} + +func ValidateReply(text string) error { + if strings.TrimSpace(text) == "" { + return fmt.Errorf("留言內文不可為空") + } + n := RuneLen(text) + if n > MaxReplyRunes { + return fmt.Errorf("留言超過 Threads 上限 %d 字(目前 %d 字)", MaxReplyRunes, n) + } + return nil +} + +func PublishHint(text string) string { + switch PublishBand(text) { + case BandTooShort: + return fmt.Sprintf("偏短(爆款常見 %d~%d 字)", ViralSweetMin, ViralSweetMax) + case BandSweet: + return fmt.Sprintf("長度適中(爆款常見 %d~%d 字)", ViralSweetMin, ViralSweetMax) + case BandLong: + return fmt.Sprintf("略長(超過 %d 字互動可能下降)", ViralSoftWarn) + case BandOverSoft: + return fmt.Sprintf("接近 Threads 主文上限 %d 字", MaxPublishRunes) + case BandOverHard: + return fmt.Sprintf("超過 Threads 上限 %d 字,請縮短", MaxPublishRunes) + default: + return "" + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/threadspost/limits_test.go b/haixun-backend/internal/library/threadspost/limits_test.go new file mode 100644 index 0000000..df4166f --- /dev/null +++ b/haixun-backend/internal/library/threadspost/limits_test.go @@ -0,0 +1,49 @@ +package threadspost + +import "testing" + +func TestClampPublish(t *testing.T) { + var long string + for range MaxPublishRunes + 10 { + long += "字" + } + clamped := ClampPublish(long) + if RuneLen(clamped) != MaxPublishRunes { + t.Fatalf("expected %d runes, got %d", MaxPublishRunes, RuneLen(clamped)) + } +} + +func TestValidatePublish(t *testing.T) { + if err := ValidatePublish(""); err == nil { + t.Fatal("expected empty error") + } + if err := ValidatePublish(string(make([]rune, MaxPublishRunes+1))); err == nil { + t.Fatal("expected over-limit error") + } + if err := ValidatePublish("爆款貼文"); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestPublishBand(t *testing.T) { + cases := []struct { + len int + want LengthBand + }{ + {0, BandEmpty}, + {50, BandTooShort}, + {120, BandSweet}, + {250, BandLong}, + {450, BandOverSoft}, + {501, BandOverHard}, + } + for _, c := range cases { + text := "" + for range c.len { + text += "字" + } + if got := PublishBand(text); got != c.want { + t.Fatalf("len %d: got %s want %s", c.len, got, c.want) + } + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/analyze_viral.go b/haixun-backend/internal/library/viral/analyze_viral.go new file mode 100644 index 0000000..a7ef92e --- /dev/null +++ b/haixun-backend/internal/library/viral/analyze_viral.go @@ -0,0 +1,101 @@ +package viral + +import ( + "encoding/json" + "fmt" + "strings" +) + +type ViralAnalysis struct { + HookPattern string `json:"hookPattern"` + StructurePattern string `json:"structurePattern"` + EmotionalTrigger string `json:"emotionalTrigger"` + ReplicationStrategy string `json:"replicationStrategy"` + KeyTakeaways []string `json:"keyTakeaways"` +} + +type AnalyzeViralInput struct { + PostText string + AuthorName string + LikeCount int + ReplyCount int + SearchTag string + TopicLabel string + TopicBrief string + Persona string +} + +func BuildAnalyzeViralSystemPrompt() string { + return strings.TrimSpace(`你是 Threads 爆款結構分析師。拆解貼文為什麼會紅、怎麼仿寫結構,不要建議抄襲原文。 + +只回傳 JSON:hookPattern, structurePattern, emotionalTrigger, replicationStrategy, keyTakeaways(字串陣列 3~5 項)。繁體中文。`) +} + +func BuildAnalyzeViralUserPrompt(in AnalyzeViralInput) string { + var b strings.Builder + b.WriteString("主題:") + b.WriteString(strings.TrimSpace(in.TopicLabel)) + b.WriteString("\n") + if brief := strings.TrimSpace(in.TopicBrief); brief != "" { + b.WriteString("Brief:") + b.WriteString(brief) + b.WriteString("\n") + } + if tag := strings.TrimSpace(in.SearchTag); tag != "" { + b.WriteString("搜尋標籤:") + b.WriteString(tag) + b.WriteString("\n") + } + author := strings.TrimSpace(in.AuthorName) + if author == "" { + author = "匿名" + } + b.WriteString(fmt.Sprintf("\n作者 @%s · %d 讚 · %d 留言\n", author, in.LikeCount, in.ReplyCount)) + b.WriteString("\n貼文:\n") + b.WriteString(strings.TrimSpace(in.PostText)) + return b.String() +} + +func ParseAnalyzeViralOutput(raw string) (ViralAnalysis, error) { + payload, err := extractCopyMapJSON(raw) + if err != nil { + return ViralAnalysis{}, err + } + var out ViralAnalysis + if err := json.Unmarshal(payload, &out); err != nil { + return ViralAnalysis{}, fmt.Errorf("parse viral analysis: %w", err) + } + if strings.TrimSpace(out.HookPattern) == "" && strings.TrimSpace(out.StructurePattern) == "" { + return ViralAnalysis{}, fmt.Errorf("viral analysis missing content") + } + return out, nil +} + +func FormatAnalysisForReplicate(analysis ViralAnalysis) string { + var b strings.Builder + if hook := strings.TrimSpace(analysis.HookPattern); hook != "" { + b.WriteString("Hook 模式:") + b.WriteString(hook) + b.WriteString("\n") + } + if structure := strings.TrimSpace(analysis.StructurePattern); structure != "" { + b.WriteString("結構節奏:") + b.WriteString(structure) + b.WriteString("\n") + } + if emotion := strings.TrimSpace(analysis.EmotionalTrigger); emotion != "" { + b.WriteString("情緒觸發:") + b.WriteString(emotion) + b.WriteString("\n") + } + if strategy := strings.TrimSpace(analysis.ReplicationStrategy); strategy != "" { + b.WriteString("仿寫策略:") + b.WriteString(strategy) + b.WriteString("\n") + } + if len(analysis.KeyTakeaways) > 0 { + b.WriteString("重點:") + b.WriteString(strings.Join(analysis.KeyTakeaways, ";")) + } + return strings.TrimSpace(b.String()) +} diff --git a/haixun-backend/internal/library/viral/discover.go b/haixun-backend/internal/library/viral/discover.go index b223106..b7e08df 100644 --- a/haixun-backend/internal/library/viral/discover.go +++ b/haixun-backend/internal/library/viral/discover.go @@ -10,16 +10,21 @@ import ( const ( defaultLimitPerKeyword = 15 + missionLimitPerKeyword = 10 maxKeywords = 6 maxMergedPosts = 60 + missionMaxMergedPosts = 40 + missionQualityTarget = 12 // stop scanning extra keywords once enough quality posts ) type DiscoverInput struct { - Keywords []string - Exclusions []string - Member placement.MemberContext - Crawler placement.CrawlerSearchFn - Limit int // per keyword; 0 = default + Keywords []string + Exclusions []string + Member placement.MemberContext + Crawler placement.CrawlerSearchFn + Limit int // per keyword; 0 = default + MaxMerged int // total cap; 0 = default + MissionScan bool // leaner defaults to save search API quota } type ProgressFn func(message string, pct int) @@ -32,26 +37,58 @@ func RunDiscover(ctx context.Context, input DiscoverInput, progress ProgressFn) } perKeyword := input.Limit if perKeyword <= 0 { - perKeyword = defaultLimitPerKeyword + if input.MissionScan { + perKeyword = missionLimitPerKeyword + } else { + perKeyword = defaultLimitPerKeyword + } + } + maxMerged := input.MaxMerged + if maxMerged <= 0 { + if input.MissionScan { + maxMerged = missionMaxMergedPosts + } else { + maxMerged = maxMergedPosts + } } merged := map[string]placement.ScanCandidate{} + relaxed := map[string]placement.ScanCandidate{} total := len(keywords) + pathLabel := input.Member.DiscoverPathLabel() + var lastErr error + keywordsAttempted := 0 + for i, keyword := range keywords { + if input.MissionScan && countMissionQuality(merged) >= missionQualityTarget { + if progress != nil { + progress(fmt.Sprintf("已收足 %d 篇品質候選,略過剩餘標籤以節省搜尋次數", missionQualityTarget), 10+(i*70)/max(total, 1)) + } + break + } if progress != nil { pct := 10 + (i*70)/total - progress(fmt.Sprintf("掃描關鍵字「%s」…", keyword), pct) + progress(fmt.Sprintf("掃描「%s」(%s)…", keyword, pathLabel), pct) + } + limit := perKeyword + if input.MissionScan && len(merged) > 0 { + limit = min(perKeyword, 8) } posts, _, err := placement.Discover(ctx, placement.DiscoverRequest{ Query: keyword, Keyword: keyword, - Limit: perKeyword, + Limit: limit, Member: input.Member, Crawler: input.Crawler, }) if err != nil { - return nil, fmt.Errorf("關鍵字「%s」:%w", keyword, err) + lastErr = err + if progress != nil { + progress(fmt.Sprintf("「%s」搜尋略過:%s", keyword, shortenDiscoverErr(err)), 10+(i*70)/max(total, 1)) + } + continue } + keywordsAttempted++ for _, post := range posts { key := strings.TrimSpace(post.Permalink) if key == "" { @@ -61,13 +98,12 @@ func RunDiscover(ctx context.Context, input DiscoverInput, progress ProgressFn) continue } score := ScorePost(post.LikeCount, post.ReplyCount) - if !PassesViralCandidate(post.Text, post.LikeCount, post.ReplyCount, score, input.Exclusions) { - continue - } candidate := placement.ScanCandidate{ Permalink: post.Permalink, ExternalID: post.ExternalID, Author: post.Author, + AuthorVerified: post.AuthorVerified, + FollowerCount: post.FollowerCount, Text: post.Text, SearchTag: keyword, Source: post.Source, @@ -77,19 +113,42 @@ func RunDiscover(ctx context.Context, input DiscoverInput, progress ProgressFn) PlacementScore: score, Priority: PriorityLabel(score), } - if prev, ok := merged[key]; !ok || candidate.EngagementScore > prev.EngagementScore { - merged[key] = candidate + if input.MissionScan { + if PassesMissionQualityCandidate( + post.Text, post.LikeCount, post.ReplyCount, score, + post.AuthorVerified, post.FollowerCount, input.Exclusions, + ) { + mergeCandidate(merged, key, candidate) + continue + } + if PassesViralCandidate(post.Text, post.LikeCount, post.ReplyCount, score, input.Exclusions) { + mergeCandidate(relaxed, key, candidate) + } + continue } + if !PassesViralCandidate(post.Text, post.LikeCount, post.ReplyCount, score, input.Exclusions) { + continue + } + mergeCandidate(merged, key, candidate) } } - out := make([]placement.ScanCandidate, 0, len(merged)) - for _, item := range merged { - out = append(out, item) + if input.MissionScan && len(merged) == 0 && len(relaxed) > 0 { + merged = relaxed + if progress != nil { + progress("未取得藍勾等延伸資料,改以互動門檻收斂爆款候選", 82) + } } + + out := candidatesFromMap(merged) sortByEngagement(out) - if len(out) > maxMergedPosts { - out = out[:maxMergedPosts] + if len(out) > maxMerged { + out = out[:maxMerged] + } + if len(out) == 0 { + if keywordsAttempted == 0 && lastErr != nil { + return nil, fmt.Errorf("所有標籤搜尋均失敗:%w", lastErr) + } } if progress != nil { progress(fmt.Sprintf("合併 %d 篇爆款候選", len(out)), 85) @@ -97,11 +156,37 @@ func RunDiscover(ctx context.Context, input DiscoverInput, progress ProgressFn) return out, nil } +func mergeCandidate(merged map[string]placement.ScanCandidate, key string, candidate placement.ScanCandidate) { + if prev, ok := merged[key]; !ok { + merged[key] = candidate + } else if candidate.EngagementScore > prev.EngagementScore { + merged[key] = MergeAuthorSignals(candidate, prev) + } else { + merged[key] = MergeAuthorSignals(prev, candidate) + } +} + +func candidatesFromMap(merged map[string]placement.ScanCandidate) []placement.ScanCandidate { + out := make([]placement.ScanCandidate, 0, len(merged)) + for _, item := range merged { + out = append(out, item) + } + return out +} + +func shortenDiscoverErr(err error) string { + msg := strings.TrimSpace(err.Error()) + if len(msg) > 80 { + return msg[:80] + "…" + } + return msg +} + func normalizeKeywords(raw []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(raw)) for _, item := range raw { - kw := strings.TrimSpace(item) + kw := DiscoverKeywordFromTag(item) if kw == "" { continue } @@ -117,6 +202,26 @@ func normalizeKeywords(raw []string) []string { return out } +func countMissionQuality(merged map[string]placement.ScanCandidate) int { + n := 0 + for _, item := range merged { + if PassesMissionQualityCandidate( + item.Text, item.LikeCount, item.ReplyCount, item.EngagementScore, + item.AuthorVerified, item.FollowerCount, nil, + ) { + n++ + } + } + return n +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + func sortByEngagement(items []placement.ScanCandidate) { for i := 0; i < len(items); i++ { for j := i + 1; j < len(items); j++ { @@ -125,4 +230,4 @@ func sortByEngagement(items []placement.ScanCandidate) { } } } -} +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/discover_accounts.go b/haixun-backend/internal/library/viral/discover_accounts.go new file mode 100644 index 0000000..f47d4da --- /dev/null +++ b/haixun-backend/internal/library/viral/discover_accounts.go @@ -0,0 +1,205 @@ +package viral + +import ( + "context" + "regexp" + "sort" + "strings" + + "haixun-backend/internal/library/websearch" +) + +const ( + maxAccountDiscoverQueries = 2 + MaxSimilarAccounts = 5 +) + +var threadsProfileRE = regexp.MustCompile(`(?i)threads\.(?:com|net)/@([a-zA-Z0-9._]+)`) + +var reservedUsernames = map[string]struct{}{ + "login": {}, "signup": {}, "search": {}, "explore": {}, "home": {}, + "help": {}, "about": {}, "privacy": {}, "terms": {}, "settings": {}, + "threads": {}, "thread": {}, "instagram": {}, "meta": {}, "www": {}, +} + +type SimilarAccount struct { + Username string `json:"username"` + Reason string `json:"reason"` + Source string `json:"source"` + Confidence string `json:"confidence"` + ProfileURL string `json:"profileUrl"` +} + +type DiscoverAccountsInput struct { + SeedQuery string + Brief string + Pillars []string +} + +type accountCandidate struct { + username string + score int + reason string + source string +} + +func DiscoverSimilarAccounts(ctx context.Context, client websearch.Client, input DiscoverAccountsInput) ([]SimilarAccount, error) { + if client == nil || !client.Enabled() { + return nil, nil + } + seed := strings.TrimSpace(input.SeedQuery) + if seed == "" { + return nil, nil + } + queries := buildAccountDiscoverQueries(seed, input.Brief, input.Pillars) + if len(queries) == 0 { + return nil, nil + } + + seen := map[string]accountCandidate{} + for _, query := range queries { + res, err := client.Search(ctx, websearch.SearchOptions{ + Query: query, + Limit: 12, + Mode: websearch.ModeThreadsDiscover, + }) + if err != nil || res.Status != "success" { + continue + } + for _, item := range res.Results { + blob := strings.TrimSpace(item.URL + " " + item.Title + " " + item.Snippet) + for _, username := range extractUsernames(blob) { + weight := 2 + if strings.Contains(strings.ToLower(item.URL), "/@"+strings.ToLower(username)) { + weight = 4 + } + reason := strings.TrimSpace(item.Snippet) + if reason == "" { + reason = strings.TrimSpace(item.Title) + } + if reason == "" { + reason = "在「" + seed + "」相關搜尋結果中找到" + } + if len([]rune(reason)) > 120 { + reason = string([]rune(reason)[:120]) + } + key := strings.ToLower(username) + prev, ok := seen[key] + if !ok || weight > prev.score { + seen[key] = accountCandidate{ + username: username, + score: weight, + reason: reason, + source: "web", + } + } else if ok { + prev.score += 1 + seen[key] = prev + } + } + } + } + + out := make([]accountCandidate, 0, len(seen)) + for _, item := range seen { + out = append(out, item) + } + sort.Slice(out, func(i, j int) bool { return out[i].score > out[j].score }) + if len(out) > MaxSimilarAccounts { + out = out[:MaxSimilarAccounts] + } + + accounts := make([]SimilarAccount, 0, len(out)) + for _, item := range out { + accounts = append(accounts, SimilarAccount{ + Username: item.username, + Reason: item.reason, + Source: item.source, + Confidence: accountConfidence(item.score), + ProfileURL: "https://www.threads.net/@" + item.username, + }) + } + return accounts, nil +} + +func buildAccountDiscoverQueries(seed, brief string, pillars []string) []string { + quoted := `"` + seed + `"` + queries := []string{ + `site:threads.net ` + quoted, + `threads ` + quoted + ` 創作者`, + } + if hint := strings.TrimSpace(brief); len([]rune(hint)) >= 4 && len([]rune(hint)) <= 24 { + queries = append(queries, `site:threads.net `+quoted+` `+hint) + } + for _, pillar := range pillars { + pillar = strings.TrimSpace(pillar) + if len([]rune(pillar)) >= 4 && len(queries) < maxAccountDiscoverQueries+1 { + queries = append(queries, `site:threads.net "`+pillar+`"`) + } + } + unique := []string{} + seen := map[string]struct{}{} + for _, q := range queries { + q = strings.TrimSpace(q) + if q == "" { + continue + } + if _, ok := seen[q]; ok { + continue + } + seen[q] = struct{}{} + unique = append(unique, q) + if len(unique) >= maxAccountDiscoverQueries { + break + } + } + return unique +} + +func extractUsernames(blob string) []string { + matches := threadsProfileRE.FindAllStringSubmatch(blob, -1) + out := []string{} + seen := map[string]struct{}{} + for _, match := range matches { + if len(match) < 2 { + continue + } + user := strings.TrimSpace(match[1]) + if !isValidUsername(user) { + continue + } + key := strings.ToLower(user) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, user) + } + return out +} + +func isValidUsername(username string) bool { + if username == "" || len(username) < 2 || len(username) > 30 { + return false + } + if _, ok := reservedUsernames[strings.ToLower(username)]; ok { + return false + } + for _, r := range username { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' { + continue + } + return false + } + return true +} + +func accountConfidence(score int) string { + if score >= 5 { + return "high" + } + if score >= 3 { + return "medium" + } + return "low" +} diff --git a/haixun-backend/internal/library/viral/discover_accounts_test.go b/haixun-backend/internal/library/viral/discover_accounts_test.go new file mode 100644 index 0000000..118c22d --- /dev/null +++ b/haixun-backend/internal/library/viral/discover_accounts_test.go @@ -0,0 +1,27 @@ +package viral + +import "testing" + +func TestExtractUsernames(t *testing.T) { + blob := `See https://www.threads.net/@creator_one and threads.com/@creator_two/posts/abc` + got := extractUsernames(blob) + if len(got) != 2 { + t.Fatalf("expected 2 usernames, got %d (%v)", len(got), got) + } +} + +func TestIsValidUsernameRejectsReserved(t *testing.T) { + if isValidUsername("login") { + t.Fatal("reserved username should be rejected") + } + if !isValidUsername("real_creator") { + t.Fatal("valid username rejected") + } +} + +func TestBuildAccountDiscoverQueriesCapsAtTwo(t *testing.T) { + queries := buildAccountDiscoverQueries("轉職", "想找語錄", []string{"支柱A", "支柱B", "支柱C"}) + if len(queries) > maxAccountDiscoverQueries { + t.Fatalf("expected at most %d queries, got %d", maxAccountDiscoverQueries, len(queries)) + } +} diff --git a/haixun-backend/internal/library/viral/discover_budget_test.go b/haixun-backend/internal/library/viral/discover_budget_test.go new file mode 100644 index 0000000..b88372b --- /dev/null +++ b/haixun-backend/internal/library/viral/discover_budget_test.go @@ -0,0 +1,17 @@ +package viral + +import ( + "testing" + + "haixun-backend/internal/library/placement" +) + +func TestCountMissionQuality(t *testing.T) { + merged := map[string]placement.ScanCandidate{ + "a": {Text: "轉職技巧分享", LikeCount: 25, ReplyCount: 4, EngagementScore: 65}, + "b": {Text: "轉職", LikeCount: 3, ReplyCount: 0, EngagementScore: 8}, + } + if got := countMissionQuality(merged); got != 1 { + t.Fatalf("expected 1 quality post, got %d", got) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/discover_graceful_test.go b/haixun-backend/internal/library/viral/discover_graceful_test.go new file mode 100644 index 0000000..c94a577 --- /dev/null +++ b/haixun-backend/internal/library/viral/discover_graceful_test.go @@ -0,0 +1,72 @@ +package viral + +import ( + "context" + "errors" + "testing" + + "haixun-backend/internal/library/placement" +) + +func TestRunDiscover_skipsFailedKeyword(t *testing.T) { + calls := 0 + crawler := func(ctx context.Context, m placement.MemberContext, keyword string, limit int) ([]placement.DiscoverPost, error) { + calls++ + if keyword == "bad" { + return nil, errors.New("api timeout") + } + return []placement.DiscoverPost{{ + Text: "轉職面試技巧分享心得", Author: "ok_user", LikeCount: 30, ReplyCount: 4, + Permalink: "https://www.threads.net/@ok_user/post/1", Source: placement.DiscoverCrawler, + }}, nil + } + member := placement.MemberContext{ + AllowsCrawler: true, + BrowserConnected: true, + SearchSourceMode: placement.SearchSourceCrawler, + } + out, err := RunDiscover(context.Background(), DiscoverInput{ + Keywords: []string{"bad", "轉職"}, + Member: member, + Crawler: crawler, + MissionScan: true, + }, nil) + if err != nil { + t.Fatalf("expected partial success, got err: %v", err) + } + if len(out) != 1 { + t.Fatalf("expected 1 candidate, got %d", len(out)) + } + if calls != 2 { + t.Fatalf("expected 2 keyword attempts, got %d", calls) + } +} + +func TestRunDiscover_missionRelaxedFallbackWithoutVerified(t *testing.T) { + crawler := func(ctx context.Context, m placement.MemberContext, keyword string, limit int) ([]placement.DiscoverPost, error) { + return []placement.DiscoverPost{{ + Text: "轉職心得分享", Author: "plain_user", LikeCount: 12, ReplyCount: 2, + Permalink: "https://www.threads.net/@plain_user/post/2", Source: placement.DiscoverThreadsAPI, + }}, nil + } + member := placement.MemberContext{ + AllowsCrawler: true, + BrowserConnected: true, + SearchSourceMode: placement.SearchSourceCrawler, + } + out, err := RunDiscover(context.Background(), DiscoverInput{ + Keywords: []string{"轉職"}, + Member: member, + Crawler: crawler, + MissionScan: true, + }, nil) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(out) != 1 { + t.Fatalf("expected relaxed fallback candidate, got %d", len(out)) + } + if out[0].AuthorVerified { + t.Fatal("verified should remain false when API omits it") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/enrich_accounts.go b/haixun-backend/internal/library/viral/enrich_accounts.go new file mode 100644 index 0000000..e8b8d95 --- /dev/null +++ b/haixun-backend/internal/library/viral/enrich_accounts.go @@ -0,0 +1,128 @@ +package viral + +import ( + "sort" + "strings" + + "haixun-backend/internal/library/placement" + missionentity "haixun-backend/internal/model/copy_mission/domain/entity" +) + +func EnrichSimilarAccounts(existing []missionentity.SimilarAccount, posts []placement.ScanCandidate, limit int) []missionentity.SimilarAccount { + if limit <= 0 { + limit = MaxSimilarAccounts + } + byUser := map[string]missionentity.SimilarAccount{} + order := []string{} + for _, item := range existing { + user := strings.ToLower(strings.TrimSpace(item.Username)) + if user == "" { + continue + } + if _, ok := byUser[user]; !ok { + order = append(order, user) + } + byUser[user] = item + } + + type authorScore struct { + username string + score int + text string + } + authors := map[string]authorScore{} + for _, post := range posts { + user := strings.TrimSpace(post.Author) + if user == "" || !isValidUsername(user) { + continue + } + key := strings.ToLower(user) + prev := authors[key] + prev.username = user + prev.score += post.EngagementScore + if prev.text == "" && strings.TrimSpace(post.Text) != "" { + prev.text = strings.TrimSpace(post.Text) + } + authors[key] = prev + } + ranked := make([]authorScore, 0, len(authors)) + for _, item := range authors { + ranked = append(ranked, item) + } + sort.Slice(ranked, func(i, j int) bool { return ranked[i].score > ranked[j].score }) + + for _, item := range ranked { + key := strings.ToLower(item.username) + if _, ok := byUser[key]; ok { + continue + } + reason := "本次海巡高互動作者" + if item.text != "" { + runes := []rune(item.text) + if len(runes) > 80 { + reason = string(runes[:80]) + } else { + reason = item.text + } + } + conf := "medium" + if item.score >= 200 { + conf = "high" + } + byUser[key] = missionentity.SimilarAccount{ + Username: item.username, + Reason: reason, + Source: "scan", + Confidence: conf, + ProfileURL: "https://www.threads.net/@" + item.username, + } + order = append(order, key) + } + + out := make([]missionentity.SimilarAccount, 0, len(order)) + for _, key := range order { + if acc, ok := byUser[key]; ok { + out = append(out, acc) + } + } + if len(out) > limit { + out = out[:limit] + } + return out +} + +func AccountTagsFromSimilar(accounts []missionentity.SimilarAccount, max int) []SuggestedTag { + if max <= 0 { + max = 2 + } + out := []SuggestedTag{} + for _, acc := range accounts { + user := strings.TrimSpace(acc.Username) + if user == "" { + continue + } + out = append(out, SuggestedTag{ + Tag: "@" + user, + Reason: acc.Reason, + SearchType: "帳號", + }) + if len(out) >= max { + break + } + } + return out +} + +func ToEntitySimilarAccounts(items []SimilarAccount) []missionentity.SimilarAccount { + out := make([]missionentity.SimilarAccount, 0, len(items)) + for _, item := range items { + out = append(out, missionentity.SimilarAccount{ + Username: item.Username, + Reason: item.Reason, + Source: item.Source, + Confidence: item.Confidence, + ProfileURL: item.ProfileURL, + }) + } + return out +} diff --git a/haixun-backend/internal/library/viral/flexible_json.go b/haixun-backend/internal/library/viral/flexible_json.go new file mode 100644 index 0000000..6b237cf --- /dev/null +++ b/haixun-backend/internal/library/viral/flexible_json.go @@ -0,0 +1,141 @@ +package viral + +import ( + "encoding/json" + "strings" +) + +func firstJSONString(obj map[string]json.RawMessage, keys ...string) string { + for _, key := range keys { + raw, ok := obj[key] + if !ok { + continue + } + var value string + if err := json.Unmarshal(raw, &value); err == nil { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + } + return "" +} + +func flexibleStringFromItem(raw json.RawMessage) string { + if len(raw) == 0 { + return "" + } + var value string + if err := json.Unmarshal(raw, &value); err == nil { + return strings.TrimSpace(value) + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(raw, &obj); err != nil { + return "" + } + if line := firstJSONString(obj, "title", "name", "label", "pillar", "tag", "text", "summary", "description", "value", "content"); line != "" { + return line + } + parts := []string{} + for _, key := range []string{"title", "name", "label"} { + if part := firstJSONString(obj, key); part != "" { + parts = append(parts, part) + break + } + } + if detail := firstJSONString(obj, "description", "summary", "detail", "reason"); detail != "" { + parts = append(parts, detail) + } + return strings.TrimSpace(strings.Join(parts, ":")) +} + +func parseFlexibleStringList(raw json.RawMessage) []string { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + var items []string + if err := json.Unmarshal(raw, &items); err == nil { + return cleanLines(items) + } + var single string + if err := json.Unmarshal(raw, &single); err == nil && strings.TrimSpace(single) != "" { + return cleanLines([]string{single}) + } + var arr []json.RawMessage + if err := json.Unmarshal(raw, &arr); err != nil { + return nil + } + out := make([]string, 0, len(arr)) + for _, item := range arr { + if line := flexibleStringFromItem(item); line != "" { + out = append(out, line) + } + } + return cleanLines(out) +} + +func parseFlexibleSuggestedTags(raw json.RawMessage) []SuggestedTag { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + var tags []SuggestedTag + if err := json.Unmarshal(raw, &tags); err == nil && len(tags) > 0 { + return cleanSuggestedTags(tags) + } + var strs []string + if err := json.Unmarshal(raw, &strs); err == nil { + out := make([]SuggestedTag, 0, len(strs)) + for _, item := range strs { + item = strings.TrimSpace(item) + if item != "" { + out = append(out, SuggestedTag{Tag: item}) + } + } + return cleanSuggestedTags(out) + } + var arr []json.RawMessage + if err := json.Unmarshal(raw, &arr); err != nil { + return nil + } + out := make([]SuggestedTag, 0, len(arr)) + for _, item := range arr { + if tag := decodeSuggestedTagItem(item); tag.Tag != "" { + out = append(out, tag) + } + } + return cleanSuggestedTags(out) +} + +func decodeSuggestedTagItem(raw json.RawMessage) SuggestedTag { + var tag SuggestedTag + if err := json.Unmarshal(raw, &tag); err == nil && strings.TrimSpace(tag.Tag) != "" { + return tag + } + var snake struct { + Tag string `json:"tag"` + Reason string `json:"reason"` + SearchIntent string `json:"search_intent"` + SearchType string `json:"search_type"` + } + if err := json.Unmarshal(raw, &snake); err == nil && strings.TrimSpace(snake.Tag) != "" { + return SuggestedTag{ + Tag: strings.TrimSpace(snake.Tag), + Reason: strings.TrimSpace(snake.Reason), + SearchIntent: strings.TrimSpace(snake.SearchIntent), + SearchType: strings.TrimSpace(snake.SearchType), + } + } + if line := flexibleStringFromItem(raw); line != "" { + return SuggestedTag{Tag: line} + } + return SuggestedTag{} +} + +func pickRawMessage(root map[string]json.RawMessage, keys ...string) json.RawMessage { + for _, key := range keys { + if raw, ok := root[key]; ok && len(raw) > 0 && string(raw) != "null" { + return raw + } + } + return nil +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/mission_inspire.go b/haixun-backend/internal/library/viral/mission_inspire.go new file mode 100644 index 0000000..c08e4d0 --- /dev/null +++ b/haixun-backend/internal/library/viral/mission_inspire.go @@ -0,0 +1,178 @@ +package viral + +import ( + "encoding/json" + "fmt" + "strings" +) + +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 +} + +type MissionInspireTrendSnippet struct { + Query string + Title string + Snippet string + URL string +} + +type MissionInspireOutput struct { + Label string + SeedQuery string + Brief string + TrendReason string + TrendKeywords []string +} + +func BuildMissionInspireSystemPrompt() string { + return strings.TrimSpace(`你是 Threads 拷貝忍者的「靈感骰子」顧問。根據近期網路熱搜、Google Trends 類訊號與創作者人設,產出一組**全新**拷貝任務草稿。 + +規則: +1. 若【近期趨勢訊號】有內容,從中挑一個適合在 Threads 海巡的方向;不要編造不存在的時事 +2. 若趨勢訊號為空(未連線網路搜尋),**必須**改依人設、受眾痛點與常見 Threads 討論型態推測「近期可能被搜尋」的話題,並在 trendReason 說明推測理由(不要假裝有外部熱搜來源) +3. label:任務名稱,6~18 字,像企劃案標題,不要標點堆疊 +4. seedQuery:種子關鍵字/近期熱詞,2~6 個詞用頓號或空格分隔,適合當 Threads 搜尋起點 +5. brief:這次想找什麼,2~4 句,說明要海巡哪類爆款、語氣或格式偏好 +6. trendReason:1~2 句,為何選這個趨勢(可引用訊號來源大意,不要貼 URL) +7. trendKeywords:3~6 個相關熱詞(字串陣列) +8. 避開【近期已做過的任務】相同題材 +9. 繁體中文;只回傳 JSON: +{"label":"","seedQuery":"","brief":"","trendReason":"","trendKeywords":[]}`) +} + +func BuildMissionInspireUserPrompt(in MissionInspireInput) string { + var b strings.Builder + if name := strings.TrimSpace(in.PersonaDisplayName); name != "" { + b.WriteString("【人設名稱】") + b.WriteString(name) + b.WriteString("\n") + } + if p := strings.TrimSpace(in.PersonaBlock); p != "" { + b.WriteString("【人設摘要】\n") + b.WriteString(p) + b.WriteString("\n") + } + if bench := strings.TrimSpace(in.StyleBenchmark); bench != "" { + b.WriteString("【對標帳號】@") + b.WriteString(strings.TrimPrefix(bench, "@")) + b.WriteString("\n") + } + if aud := strings.TrimSpace(in.PersonaAudience); aud != "" { + b.WriteString("【受眾方向】") + b.WriteString(aud) + b.WriteString("\n") + } + if goal := strings.TrimSpace(in.PersonaContentGoal); goal != "" { + b.WriteString("【內容目標】") + b.WriteString(goal) + b.WriteString("\n") + } + if len(in.PersonaPillars) > 0 { + b.WriteString("【內容支柱】") + b.WriteString(strings.Join(in.PersonaPillars, "、")) + b.WriteString("\n") + } + if len(in.RecentMissionLabels) > 0 || len(in.RecentSeedQueries) > 0 { + b.WriteString("【近期已做過的任務(請避開)】\n") + for _, label := range in.RecentMissionLabels { + if label = strings.TrimSpace(label); label != "" { + b.WriteString("- ") + b.WriteString(label) + b.WriteString("\n") + } + } + for _, seed := range in.RecentSeedQueries { + if seed = strings.TrimSpace(seed); seed != "" { + b.WriteString("- 種子:") + b.WriteString(seed) + b.WriteString("\n") + } + } + } + if in.LLMOnly { + b.WriteString("【模式】未設定 Web Search API key,請純依人設與受眾推測靈感(勿假裝有 Google 熱搜)\n") + } else if provider := strings.TrimSpace(in.WebSearchProvider); provider != "" { + b.WriteString("【趨勢來源】") + b.WriteString(provider) + b.WriteString(" 網路搜尋\n") + } + b.WriteString("【近期趨勢訊號】\n") + if len(in.TrendSnippets) == 0 { + b.WriteString("(無外部趨勢結果,請改用人設推測近期 Threads 可能熱議方向)\n") + } else { + for i, item := range in.TrendSnippets { + b.WriteString(fmt.Sprintf("[%d] 查詢:%s\n", i+1, strings.TrimSpace(item.Query))) + if title := strings.TrimSpace(item.Title); title != "" { + b.WriteString("標題:") + b.WriteString(title) + b.WriteString("\n") + } + if snippet := strings.TrimSpace(item.Snippet); snippet != "" { + b.WriteString(snippet) + b.WriteString("\n") + } + b.WriteString("\n") + } + } + b.WriteString("請產出拷貝任務靈感 JSON。") + return b.String() +} + +func InspireTrendSearchQueries(personaBrief, styleBenchmark string) []string { + queries := []string{ + "Google Trends 台灣 今日 熱門搜尋 關鍵字", + "Threads 台灣 熱門 話題 最近 一週", + "近期 網路 熱搜 關鍵字 台灣", + } + context := strings.TrimSpace(personaBrief + " " + styleBenchmark) + if context != "" { + trimmed := context + if len([]rune(trimmed)) > 24 { + trimmed = string([]rune(trimmed)[:24]) + } + queries = append(queries, trimmed+" 熱門 話題 最近") + } + return queries +} + +func ParseMissionInspireOutput(raw string) (MissionInspireOutput, error) { + payload, err := extractCopyMapJSON(raw) + if err != nil { + return MissionInspireOutput{}, err + } + var root map[string]json.RawMessage + if err := json.Unmarshal(payload, &root); err != nil { + return MissionInspireOutput{}, err + } + out := MissionInspireOutput{ + Label: firstJSONString(root, "label", "title", "mission_label"), + SeedQuery: firstJSONString(root, "seedQuery", "seed_query", "seed", "keywords"), + Brief: firstJSONString(root, "brief", "description", "goal"), + TrendReason: firstJSONString(root, "trendReason", "trend_reason", "reason"), + } + if out.Label == "" || out.SeedQuery == "" || out.Brief == "" { + return MissionInspireOutput{}, fmt.Errorf("missing label, seedQuery, or brief") + } + if rawKW, ok := root["trendKeywords"]; ok { + out.TrendKeywords = parseFlexibleStringList(rawKW) + } + if len(out.TrendKeywords) == 0 { + if rawKW, ok := root["trend_keywords"]; ok { + out.TrendKeywords = parseFlexibleStringList(rawKW) + } + } + return out, nil +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/mission_inspire_collect.go b/haixun-backend/internal/library/viral/mission_inspire_collect.go new file mode 100644 index 0000000..7e53380 --- /dev/null +++ b/haixun-backend/internal/library/viral/mission_inspire_collect.go @@ -0,0 +1,66 @@ +package viral + +import ( + "context" + "strings" + "time" + + "haixun-backend/internal/library/placement" + "haixun-backend/internal/library/websearch" +) + +const maxInspireTrendSnippets = 14 + +func CollectMissionInspireTrends( + ctx context.Context, + client websearch.Client, + member placement.MemberContext, + personaBrief, styleBenchmark string, +) []MissionInspireTrendSnippet { + if client == nil || !client.Enabled() { + return nil + } + queries := InspireTrendSearchQueries(personaBrief, styleBenchmark) + out := make([]MissionInspireTrendSnippet, 0, maxInspireTrendSnippets) + seen := map[string]struct{}{} + + for _, query := range queries { + if len(out) >= maxInspireTrendSnippets { + break + } + res, err := client.Search(ctx, websearch.SearchOptions{ + Query: query, + Limit: 5, + Mode: websearch.ModeKnowledgeExpand, + Country: member.BraveCountry, + SearchLang: member.BraveSearchLang, + UserLocation: member.ExaUserLocation, + StartPublishedDate: placement.FormatPublishedAfterISO(7, time.Now().UTC()), + }) + if err != nil || res.Status != "success" || len(res.Results) == 0 { + continue + } + for _, item := range res.Results { + if len(out) >= maxInspireTrendSnippets { + break + } + title := strings.TrimSpace(item.Title) + snippet := strings.TrimSpace(item.Snippet) + if title == "" && snippet == "" { + continue + } + key := strings.ToLower(title + "|" + snippet) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, MissionInspireTrendSnippet{ + Query: query, + Title: title, + Snippet: snippet, + URL: strings.TrimSpace(item.URL), + }) + } + } + return out +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/mission_inspire_test.go b/haixun-backend/internal/library/viral/mission_inspire_test.go new file mode 100644 index 0000000..df7f884 --- /dev/null +++ b/haixun-backend/internal/library/viral/mission_inspire_test.go @@ -0,0 +1,34 @@ +package viral + +import ( + "strings" + "testing" +) + +func TestParseMissionInspireOutput(t *testing.T) { + raw := `{"label":"轉職焦慮語錄","seedQuery":"轉職 被裁 焦慮","brief":"想找最近 Threads 上互動高的轉職心情貼文。","trendReason":"Google 熱搜近期職場話題升溫","trendKeywords":["轉職","裁員","面試"]}` + out, err := ParseMissionInspireOutput(raw) + if err != nil { + t.Fatalf("parse: %v", err) + } + if out.Label != "轉職焦慮語錄" || out.SeedQuery == "" || out.Brief == "" { + t.Fatalf("unexpected output: %+v", out) + } + if len(out.TrendKeywords) != 3 { + t.Fatalf("expected 3 trend keywords, got %d", len(out.TrendKeywords)) + } +} + +func TestBuildMissionInspireUserPromptLLMOnly(t *testing.T) { + prompt := BuildMissionInspireUserPrompt(MissionInspireInput{ + PersonaDisplayName: "測試人設", + PersonaBrief: "職場焦慮", + LLMOnly: true, + }) + if !strings.Contains(prompt, "未設定 Web Search API key") { + t.Fatalf("expected llm-only hint in prompt: %s", prompt) + } + if !strings.Contains(prompt, "無外部趨勢結果") { + t.Fatalf("expected empty trend fallback in prompt: %s", prompt) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/mission_research_map.go b/haixun-backend/internal/library/viral/mission_research_map.go new file mode 100644 index 0000000..d0d1876 --- /dev/null +++ b/haixun-backend/internal/library/viral/mission_research_map.go @@ -0,0 +1,143 @@ +package viral + +import ( + "encoding/json" + "fmt" + "strings" +) + +// MissionResearchMap is the structured copy-mission research map (single LLM call). +type MissionResearchMap struct { + AudienceSummary string `json:"audienceSummary"` + ContentGoal string `json:"contentGoal"` + Questions []string `json:"questions"` + Pillars []string `json:"pillars"` + Exclusions []string `json:"exclusions"` + SuggestedTags []SuggestedTag `json:"suggestedTags"` + BenchmarkNotes string `json:"benchmarkNotes"` +} + +func BuildMissionResearchMapSystemPrompt() string { + return strings.TrimSpace(`你是 Threads 爆款/對標研究顧問。目標:幫創作者找到「近期熱門、高互動、值得仿寫」的話題與搜尋方向。 + +規則: +1. audienceSummary:必填,2~4 句描述「受眾是誰」(年齡/情境/痛點/會在 Threads 搜什麼),不要只寫人設本人 +2. 聚焦「最近會在 Threads 被搜、被討論」的話題,不要寫成學術報告 +3. contentGoal:找到近期互動佳、結構可模仿的爆款貼文 +4. pillars:可模仿方向(語錄型、故事型、清單型等),至少 4 個;**必須是字串陣列**,不要用 {title:...} 物件 +5. questions:受眾會搜的短問題,5+ 個;字串陣列 +6. exclusions:不要模仿的內容,至少 4 個;字串陣列 +7. suggestedTags:6~8 個「像真人會在 Threads 搜尋框打的字」 + - 每個含 tag, reason, searchIntent(痛點|知識|經驗|對比|工具|語錄), searchType(短詞|情境|語錄) + - tag 2~10 字,不要標點、不要完整句子、不要像文章標題 +8. benchmarkNotes:怎樣算值得仿的爆款(互動、hook 清楚) +9. 繁體中文;只回傳 JSON(含 audienceSummary)`) +} + +func BuildMissionResearchMapUserPrompt(in CopyResearchMapInput) string { + var b strings.Builder + b.WriteString("【任務名稱】") + b.WriteString(strings.TrimSpace(in.Label)) + b.WriteString("\n【種子關鍵字/近期熱詞】") + b.WriteString(strings.TrimSpace(in.SeedQuery)) + b.WriteString("\n【這次想找什麼】\n") + b.WriteString(strings.TrimSpace(in.Brief)) + if p := strings.TrimSpace(in.Persona); p != "" { + b.WriteString("\n【我的人設(仿寫時會套用,地圖請對準受眾與話題,不要只寫我自己)】\n") + b.WriteString(p) + } + if bench := strings.TrimSpace(in.StyleBenchmark); bench != "" { + b.WriteString("\n【長期對標帳號參考】@") + b.WriteString(strings.TrimPrefix(bench, "@")) + b.WriteString("\n") + } + if aud := strings.TrimSpace(in.PersonaAudienceSummary); aud != "" { + b.WriteString("\n【人設層級受眾研究(請延伸為本次任務受眾,勿只複製)】\n") + b.WriteString(aud) + if goal := strings.TrimSpace(in.PersonaContentGoal); goal != "" { + b.WriteString("\n內容目標參考:") + b.WriteString(goal) + } + if len(in.PersonaQuestions) > 0 { + b.WriteString("\n受眾提問參考:") + b.WriteString(strings.Join(in.PersonaQuestions, "、")) + } + if len(in.PersonaPillars) > 0 { + b.WriteString("\n內容支柱參考:") + b.WriteString(strings.Join(in.PersonaPillars, "、")) + } + b.WriteString("\n") + } + b.WriteString("\n請產出拷貝任務研究地圖 JSON(必填 audienceSummary 與 suggestedTags 陣列)。") + return b.String() +} + +func ParseMissionResearchMapOutput(raw string) (MissionResearchMap, error) { + payload, err := extractCopyMapJSON(raw) + if err != nil { + return MissionResearchMap{}, err + } + var root map[string]json.RawMessage + if err := json.Unmarshal(payload, &root); err != nil { + return MissionResearchMap{}, fmt.Errorf("parse mission research map: %w", err) + } + out := MissionResearchMap{ + AudienceSummary: firstJSONString(root, "audienceSummary", "audience_summary"), + ContentGoal: firstJSONString(root, "contentGoal", "content_goal"), + BenchmarkNotes: firstJSONString(root, "benchmarkNotes", "benchmark_notes"), + Questions: parseFlexibleStringList(pickRawMessage(root, "questions")), + Pillars: parseFlexibleStringList(pickRawMessage(root, "pillars")), + Exclusions: parseFlexibleStringList(pickRawMessage(root, "exclusions")), + SuggestedTags: parseFlexibleSuggestedTags(pickRawMessage(root, "suggestedTags", "suggested_tags")), + } + if strings.TrimSpace(out.AudienceSummary) == "" { + return MissionResearchMap{}, fmt.Errorf("mission research map missing audienceSummary") + } + if len(out.SuggestedTags) < 4 { + return MissionResearchMap{}, fmt.Errorf("mission research map needs at least 4 suggested tags") + } + return out, nil +} + +func cleanSuggestedTags(tags []SuggestedTag) []SuggestedTag { + out := []SuggestedTag{} + seen := map[string]struct{}{} + for _, item := range tags { + tag := strings.TrimSpace(item.Tag) + if tag == "" { + continue + } + if _, ok := seen[tag]; ok { + continue + } + seen[tag] = struct{}{} + out = append(out, SuggestedTag{ + Tag: tag, + Reason: strings.TrimSpace(item.Reason), + SearchIntent: strings.TrimSpace(item.SearchIntent), + SearchType: strings.TrimSpace(item.SearchType), + }) + } + return out +} + +func ToEntityMissionResearchMap(m MissionResearchMap) map[string]any { + tags := make([]map[string]any, 0, len(m.SuggestedTags)) + for _, item := range m.SuggestedTags { + tags = append(tags, map[string]any{ + "tag": item.Tag, + "reason": item.Reason, + "search_intent": item.SearchIntent, + "search_type": item.SearchType, + }) + } + return map[string]any{ + "audience_summary": m.AudienceSummary, + "content_goal": m.ContentGoal, + "questions": m.Questions, + "pillars": m.Pillars, + "exclusions": m.Exclusions, + "suggested_tags": tags, + "benchmark_notes": m.BenchmarkNotes, + } +} diff --git a/haixun-backend/internal/library/viral/mission_research_map_test.go b/haixun-backend/internal/library/viral/mission_research_map_test.go new file mode 100644 index 0000000..adfd63c --- /dev/null +++ b/haixun-backend/internal/library/viral/mission_research_map_test.go @@ -0,0 +1,55 @@ +package viral + +import "testing" + +func TestParseMissionResearchMapOutput_ObjectPillars(t *testing.T) { + raw := `{ + "audienceSummary": "轉職焦慮的上班族", + "contentGoal": "找近期高互動轉職心情貼文", + "questions": ["轉職會後悔嗎", "被裁怎麼辦"], + "pillars": [ + {"title": "語錄型", "description": "一句話戳中焦慮"}, + {"title": "故事型", "description": "親身轉職經驗"} + ], + "exclusions": ["純業配", "無結構閒聊"], + "suggestedTags": [ + {"tag": "轉職焦慮", "reason": "近期熱詞", "searchIntent": "痛點", "searchType": "短詞"}, + {"tag": "被裁", "reason": "高互動", "searchIntent": "痛點", "searchType": "短詞"}, + {"tag": "面試失敗", "reason": "共鳴", "searchIntent": "經驗", "searchType": "情境"}, + {"tag": "裸辭", "reason": "討論多", "searchIntent": "語錄", "searchType": "短詞"} + ], + "benchmarkNotes": "互動高且 hook 清楚" +}` + out, err := ParseMissionResearchMapOutput(raw) + if err != nil { + t.Fatalf("ParseMissionResearchMapOutput: %v", err) + } + if len(out.Pillars) < 2 { + t.Fatalf("expected parsed pillars, got %#v", out.Pillars) + } + if out.Pillars[0] == "" { + t.Fatal("first pillar should not be empty") + } + if len(out.SuggestedTags) < 4 { + t.Fatalf("expected suggested tags, got %#v", out.SuggestedTags) + } +} + +func TestParseMissionResearchMapOutput_StringSuggestedTags(t *testing.T) { + raw := `{ + "audienceSummary": "新手媽媽", + "contentGoal": "找育兒爆款", + "questions": ["哄睡", "副食品"], + "pillars": ["清單型", "故事型", "語錄型", "問答型"], + "exclusions": ["業配", "晒娃", "無重點", "抄襲"], + "suggestedTags": ["哄睡", "副食品", "崩潰", "育兒"], + "benchmarkNotes": "留言多" +}` + out, err := ParseMissionResearchMapOutput(raw) + if err != nil { + t.Fatalf("ParseMissionResearchMapOutput: %v", err) + } + if len(out.SuggestedTags) != 4 { + t.Fatalf("expected 4 tags, got %#v", out.SuggestedTags) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/quality.go b/haixun-backend/internal/library/viral/quality.go new file mode 100644 index 0000000..bd924db --- /dev/null +++ b/haixun-backend/internal/library/viral/quality.go @@ -0,0 +1,39 @@ +package viral + +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 + MissionInfluencerMinFollowers = 5000 +) + +// PassesMissionQualityCandidate filters copy-mission scan results toward higher-signal posts. +// AuthorVerified and FollowerCount are optional enrichments (often missing on Threads API); +// when absent the default engagement bar still applies — callers should fall back to +// PassesViralCandidate rather than treating empty optional signals as failure. +func PassesMissionQualityCandidate(text string, likes, replies, engagement int, verified bool, followerCount int, exclusions []string) bool { + if !PassesViralCandidate(text, likes, replies, engagement, exclusions) { + return false + } + if verified { + return likes >= MissionVerifiedMinLikes && engagement >= MissionVerifiedMinEngagement + } + if followerCount >= MissionInfluencerMinFollowers { + return likes >= 12 && engagement >= 40 + } + return likes >= MissionQualityMinLikes && engagement >= MissionQualityMinEngagement +} + +func MergeAuthorSignals(prev, next placement.ScanCandidate) placement.ScanCandidate { + if next.AuthorVerified { + prev.AuthorVerified = true + } + if next.FollowerCount > prev.FollowerCount { + prev.FollowerCount = next.FollowerCount + } + return prev +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/quality_test.go b/haixun-backend/internal/library/viral/quality_test.go new file mode 100644 index 0000000..9de7efb --- /dev/null +++ b/haixun-backend/internal/library/viral/quality_test.go @@ -0,0 +1,18 @@ +package viral + +import "testing" + +func TestPassesMissionQualityCandidate_verifiedLowerBar(t *testing.T) { + if !PassesMissionQualityCandidate("轉職面試技巧分享心得", 12, 2, 40, true, 0, nil) { + t.Fatal("verified author should pass with moderate engagement") + } +} + +func TestPassesMissionQualityCandidate_unverifiedStricter(t *testing.T) { + if PassesMissionQualityCandidate("轉職面試技巧分享心得", 12, 2, 40, false, 0, nil) { + t.Fatal("unverified author should not pass with low engagement") + } + if !PassesMissionQualityCandidate("轉職面試技巧分享心得", 25, 4, 65, false, 0, nil) { + t.Fatal("unverified author should pass with strong engagement") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/reference_accounts.go b/haixun-backend/internal/library/viral/reference_accounts.go new file mode 100644 index 0000000..ce47cd9 --- /dev/null +++ b/haixun-backend/internal/library/viral/reference_accounts.go @@ -0,0 +1,201 @@ +package viral + +import ( + "fmt" + "sort" + "strings" + + "haixun-backend/internal/library/placement" + missionentity "haixun-backend/internal/model/copy_mission/domain/entity" +) + +const ( + RefAccountMinBestEngagement = 50 + RefAccountMinBestLikes = 18 + RefAccountMinTotalEngagement = 80 + RefVerifiedMinBestEngagement = 35 + RefVerifiedMinBestLikes = 10 +) + +type ReferenceAccountInput struct { + SeedQuery string + Label string + Posts []placement.ScanCandidate + Limit int +} + +type referenceAuthorAgg struct { + username string + verified bool + followerCount int + totalEngagement int + bestEngagement int + bestLikes int + bestReplies int + postCount int + sampleText string + sampleSearchTag string +} + +// BuildReferenceAccountsFromScan lists authors from patrol posts that match the +// mission topic and pass quality gates. Verified/follower are optional bonuses; +// if strict gates yield nothing, falls back to baseline viral engagement. +func BuildReferenceAccountsFromScan(in ReferenceAccountInput) []missionentity.SimilarAccount { + out := buildReferenceAccountsFromScan(in, true) + if len(out) > 0 { + return out + } + return buildReferenceAccountsFromScan(in, false) +} + +func buildReferenceAccountsFromScan(in ReferenceAccountInput, strictQuality bool) []missionentity.SimilarAccount { + limit := in.Limit + if limit <= 0 { + limit = MaxSimilarAccounts + } + byUser := map[string]referenceAuthorAgg{} + for _, post := range in.Posts { + user := strings.TrimSpace(post.Author) + if user == "" || !isValidUsername(user) { + continue + } + if !postTopicRelevant(post, in.SeedQuery, in.Label) { + continue + } + if strictQuality { + if !PassesMissionQualityCandidate( + post.Text, post.LikeCount, post.ReplyCount, post.EngagementScore, + post.AuthorVerified, post.FollowerCount, nil, + ) { + continue + } + } else if !PassesViralCandidate( + post.Text, post.LikeCount, post.ReplyCount, post.EngagementScore, nil, + ) { + continue + } + key := strings.ToLower(user) + prev := byUser[key] + prev.username = user + if post.AuthorVerified { + prev.verified = true + } + if post.FollowerCount > prev.followerCount { + prev.followerCount = post.FollowerCount + } + prev.postCount++ + prev.totalEngagement += post.EngagementScore + if post.EngagementScore > prev.bestEngagement { + prev.bestEngagement = post.EngagementScore + prev.bestLikes = post.LikeCount + prev.bestReplies = post.ReplyCount + text := strings.TrimSpace(post.Text) + if len([]rune(text)) > 80 { + text = string([]rune(text)[:80]) + } + prev.sampleText = text + prev.sampleSearchTag = strings.TrimSpace(post.SearchTag) + } + byUser[key] = prev + } + + ranked := make([]referenceAuthorAgg, 0, len(byUser)) + for _, item := range byUser { + if qualifiesReferenceAuthor(item, strictQuality) { + ranked = append(ranked, item) + } + } + sort.Slice(ranked, func(i, j int) bool { + if ranked[i].verified != ranked[j].verified { + return ranked[i].verified + } + if ranked[i].followerCount != ranked[j].followerCount { + return ranked[i].followerCount > ranked[j].followerCount + } + if ranked[i].totalEngagement != ranked[j].totalEngagement { + return ranked[i].totalEngagement > ranked[j].totalEngagement + } + return ranked[i].bestEngagement > ranked[j].bestEngagement + }) + if len(ranked) > limit { + ranked = ranked[:limit] + } + + out := make([]missionentity.SimilarAccount, 0, len(ranked)) + for _, item := range ranked { + conf := "medium" + if item.verified { + conf = "high" + } else if item.bestEngagement >= HotEngagementScore || item.totalEngagement >= 120 { + conf = "high" + } + out = append(out, missionentity.SimilarAccount{ + Username: item.username, + Reason: formatReferenceReason(item), + Source: "scan", + Confidence: conf, + ProfileURL: "https://www.threads.net/@" + item.username, + AuthorVerified: item.verified, + FollowerCount: item.followerCount, + EngagementScore: item.bestEngagement, + LikeCount: item.bestLikes, + ReplyCount: item.bestReplies, + PostCount: item.postCount, + }) + } + return out +} + +func qualifiesReferenceAuthor(item referenceAuthorAgg, strictQuality bool) bool { + if item.postCount == 0 { + return false + } + if item.verified { + return item.bestLikes >= RefVerifiedMinBestLikes && + (item.bestEngagement >= RefVerifiedMinBestEngagement || item.totalEngagement >= 60) + } + if strictQuality { + if item.bestLikes < RefAccountMinBestLikes { + return false + } + return item.bestEngagement >= RefAccountMinBestEngagement || item.totalEngagement >= RefAccountMinTotalEngagement + } + return item.bestLikes >= 8 && item.bestEngagement >= MinEngagementScore +} + +func postTopicRelevant(post placement.ScanCandidate, seed, label string) bool { + text := strings.ToLower(strings.TrimSpace(post.Text)) + tag := strings.ToLower(strings.TrimSpace(post.SearchTag)) + terms := topicTerms(seed, label) + if len(terms) == 0 { + return text != "" || tag != "" + } + for _, term := range terms { + term = strings.ToLower(term) + if strings.Contains(text, term) || strings.Contains(tag, term) { + return true + } + } + return false +} + +func topicTerms(seed, label string) []string { + out := []string{} + if s := strings.TrimSpace(seed); s != "" { + out = append(out, s) + } + if l := strings.TrimSpace(label); l != "" { + out = append(out, l) + } + return out +} + +func formatReferenceReason(item referenceAuthorAgg) string { + if item.sampleText != "" { + return item.sampleText + } + if item.sampleSearchTag != "" { + return fmt.Sprintf("標籤「%s」高互動作者", item.sampleSearchTag) + } + return "本次海巡高互動作者" +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/reference_accounts_fallback_test.go b/haixun-backend/internal/library/viral/reference_accounts_fallback_test.go new file mode 100644 index 0000000..e80f1ee --- /dev/null +++ b/haixun-backend/internal/library/viral/reference_accounts_fallback_test.go @@ -0,0 +1,24 @@ +package viral + +import ( + "testing" + + "haixun-backend/internal/library/placement" +) + +func TestBuildReferenceAccountsFromScan_relaxedFallback(t *testing.T) { + got := BuildReferenceAccountsFromScan(ReferenceAccountInput{ + SeedQuery: "轉職", + Label: "轉職", + Posts: []placement.ScanCandidate{ + {Author: "warm_user", Text: "轉職心得分享", SearchTag: "轉職", LikeCount: 10, ReplyCount: 2, EngagementScore: 32}, + }, + Limit: 5, + }) + if len(got) != 1 { + t.Fatalf("expected relaxed fallback account, got %d", len(got)) + } + if got[0].AuthorVerified { + t.Fatal("verified must stay false when not provided") + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/reference_accounts_test.go b/haixun-backend/internal/library/viral/reference_accounts_test.go new file mode 100644 index 0000000..321881a --- /dev/null +++ b/haixun-backend/internal/library/viral/reference_accounts_test.go @@ -0,0 +1,58 @@ +package viral + +import ( + "testing" + + "haixun-backend/internal/library/placement" +) + +func TestBuildReferenceAccountsFromScan_filtersLowEngagement(t *testing.T) { + got := BuildReferenceAccountsFromScan(ReferenceAccountInput{ + SeedQuery: "轉職", + Label: "轉職語錄", + Posts: []placement.ScanCandidate{ + {Author: "weak_user", Text: "轉職心得", SearchTag: "轉職", LikeCount: 2, ReplyCount: 0, EngagementScore: 10}, + {Author: "hot_user", Text: "轉職面試技巧分享", SearchTag: "轉職", LikeCount: 30, ReplyCount: 5, EngagementScore: 85}, + }, + Limit: 5, + }) + if len(got) != 1 { + t.Fatalf("expected 1 account, got %d", len(got)) + } + if got[0].Username != "hot_user" { + t.Fatalf("unexpected username %q", got[0].Username) + } +} + +func TestBuildReferenceAccountsFromScan_prefersVerified(t *testing.T) { + got := BuildReferenceAccountsFromScan(ReferenceAccountInput{ + SeedQuery: "轉職", + Label: "轉職", + Posts: []placement.ScanCandidate{ + {Author: "plain_user", Text: "轉職技巧", SearchTag: "轉職", LikeCount: 22, ReplyCount: 3, EngagementScore: 55}, + {Author: "blue_user", Text: "轉職分享", SearchTag: "轉職", LikeCount: 14, ReplyCount: 2, EngagementScore: 42, AuthorVerified: true}, + }, + Limit: 5, + }) + if len(got) < 2 { + t.Fatalf("expected 2 accounts, got %d", len(got)) + } + if got[0].Username != "blue_user" || !got[0].AuthorVerified { + t.Fatalf("verified author should rank first, got %+v", got[0]) + } +} + +func TestBuildReferenceAccountsFromScan_requiresTopicMatch(t *testing.T) { + posts := []placement.ScanCandidate{ + {Author: "off_topic", Text: "今天天氣真好", SearchTag: "天氣", LikeCount: 40, ReplyCount: 8, EngagementScore: 90}, + } + got := BuildReferenceAccountsFromScan(ReferenceAccountInput{ + SeedQuery: "轉職", + Label: "轉職", + Posts: posts, + Limit: 5, + }) + if len(got) != 0 { + t.Fatalf("expected no accounts for off-topic post, got %d", len(got)) + } +} \ No newline at end of file diff --git a/haixun-backend/internal/library/viral/replicate.go b/haixun-backend/internal/library/viral/replicate.go index 97c67aa..6be7051 100644 --- a/haixun-backend/internal/library/viral/replicate.go +++ b/haixun-backend/internal/library/viral/replicate.go @@ -5,19 +5,20 @@ import ( "fmt" "regexp" "strings" -) -const maxChars = 500 + "haixun-backend/internal/library/threadspost" +) var codeFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```") type ReplicateInput struct { - TopicLabel string - TopicBrief string - Persona string - StyleProfile string - OriginalText string - AuthorName string + TopicLabel string + TopicBrief string + Persona string + StyleProfile string + OriginalText string + AuthorName string + StructureAnalysis string } type ReplicateResult struct { @@ -34,7 +35,8 @@ func BuildSystemPrompt() string { 規則: - 複製的是爆款公式(hook 手法、情緒節奏),不是抄襲原文 - 文筆必須像創作者本人,套用其 8D 風格策略 -- text ≤ 500 字(含 #話題標籤) +- text 主文 ≤ 500 字(Threads API 硬上限,含 #話題標籤) +- 爆款互動最佳 80~220 字:前 1~2 行強 hook,口語精簡;超過 300 字互動通常下降 - 只回傳一個 JSON 物件,欄位:angle, hook, text, rationale, structureNotes`) } @@ -62,6 +64,11 @@ func BuildUserPrompt(input ReplicateInput) string { if author == "" { author = "匿名" } + if analysis := strings.TrimSpace(input.StructureAnalysis); analysis != "" { + b.WriteString("\n爆款結構分析(仿寫時套用):\n") + b.WriteString(analysis) + b.WriteString("\n") + } b.WriteString("\n原文參考(@") b.WriteString(author) b.WriteString(",只學結構不抄內容):\n") @@ -93,8 +100,8 @@ func ParseReplicateOutput(raw string) (ReplicateResult, error) { func trimText(text string) string { text = strings.TrimSpace(text) runes := []rune(text) - if len(runes) > maxChars { - return string(runes[:maxChars]) + if len(runes) > threadspost.MaxPublishRunes { + return string(runes[:threadspost.MaxPublishRunes]) } return text } diff --git a/haixun-backend/internal/library/viral/research_map.go b/haixun-backend/internal/library/viral/research_map.go index e17d124..f102f5c 100644 --- a/haixun-backend/internal/library/viral/research_map.go +++ b/haixun-backend/internal/library/viral/research_map.go @@ -18,11 +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 } var copyMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```") @@ -31,13 +35,14 @@ func BuildCopyResearchMapSystemPrompt() string { return strings.TrimSpace(`你是 Threads 爆款/對標研究顧問。目標是幫創作者找到「高互動、值得仿寫」的參考貼文與對標方向。 規則: -1. contentGoal 要寫:找到近期互動佳、結構可模仿的爆款貼文,分析 hook/節奏/文案公式 -2. pillars:可模仿的內容方向(語錄型、故事型、清單型等),至少 4 個 -3. questions:受眾會搜尋/關心的短問題,5+ 個,適合當爆款掃描關鍵字 -4. exclusions:不要模仿的內容(業配、純晒照、無結構閒聊等),至少 4 個 -5. suggestedTags:2~4 字短詞,10 個左右,用於 Threads 搜尋爆款 -6. benchmarkNotes:一句話說明怎樣算「值得仿的爆款」(互動、留言品質、hook 清楚) -7. 繁體中文;只回傳 JSON:audienceSummary, contentGoal, questions, pillars, exclusions, suggestedTags, benchmarkNotes`) +1. audienceSummary:必填,2~4 句描述「受眾是誰」(情境、痛點、會搜什麼) +2. contentGoal 要寫:找到近期互動佳、結構可模仿的爆款貼文,分析 hook/節奏/文案公式 +3. pillars:可模仿的內容方向(語錄型、故事型、清單型等),至少 4 個 +4. questions:受眾會搜尋/關心的短問題,5+ 個,適合當爆款掃描關鍵字 +5. exclusions:不要模仿的內容(業配、純晒照、無結構閒聊等),至少 4 個 +6. suggestedTags:2~4 字短詞,10 個左右,用於 Threads 搜尋爆款 +7. benchmarkNotes:一句話說明怎樣算「值得仿的爆款」(互動、留言品質、hook 清楚) +8. 繁體中文;只回傳 JSON:audienceSummary, contentGoal, questions, pillars, exclusions, suggestedTags, benchmarkNotes`) } func BuildCopyResearchMapUserPrompt(in CopyResearchMapInput) string { @@ -66,19 +71,28 @@ func ParseCopyResearchMapOutput(raw string) (CopyResearchMap, error) { if err != nil { return CopyResearchMap{}, err } - var out CopyResearchMap - if err := json.Unmarshal(payload, &out); err != nil { + var root map[string]json.RawMessage + if err := json.Unmarshal(payload, &root); err != nil { return CopyResearchMap{}, fmt.Errorf("parse copy research map: %w", err) } - out.AudienceSummary = strings.TrimSpace(out.AudienceSummary) - out.ContentGoal = strings.TrimSpace(out.ContentGoal) - out.BenchmarkNotes = strings.TrimSpace(out.BenchmarkNotes) - out.Questions = cleanLines(out.Questions) - out.Pillars = cleanLines(out.Pillars) - out.Exclusions = cleanLines(out.Exclusions) - out.SuggestedTags = cleanLines(out.SuggestedTags) - if out.AudienceSummary == "" && len(out.SuggestedTags) == 0 { - return CopyResearchMap{}, fmt.Errorf("copy research map missing content") + tagObjs := parseFlexibleSuggestedTags(pickRawMessage(root, "suggestedTags", "suggested_tags")) + tagStrs := make([]string, 0, len(tagObjs)) + for _, item := range tagObjs { + if item.Tag != "" { + tagStrs = append(tagStrs, item.Tag) + } + } + out := CopyResearchMap{ + AudienceSummary: firstJSONString(root, "audienceSummary", "audience_summary"), + ContentGoal: firstJSONString(root, "contentGoal", "content_goal"), + BenchmarkNotes: firstJSONString(root, "benchmarkNotes", "benchmark_notes"), + Questions: parseFlexibleStringList(pickRawMessage(root, "questions")), + Pillars: parseFlexibleStringList(pickRawMessage(root, "pillars")), + Exclusions: parseFlexibleStringList(pickRawMessage(root, "exclusions")), + SuggestedTags: cleanLines(tagStrs), + } + if strings.TrimSpace(out.AudienceSummary) == "" { + return CopyResearchMap{}, fmt.Errorf("copy research map missing audienceSummary") } return out, nil } diff --git a/haixun-backend/internal/library/viral/search_tags.go b/haixun-backend/internal/library/viral/search_tags.go new file mode 100644 index 0000000..70bd221 --- /dev/null +++ b/haixun-backend/internal/library/viral/search_tags.go @@ -0,0 +1,115 @@ +package viral + +import ( + "strings" +) + +const ( + DefaultSelectedTagCount = 5 + MaxScanTags = 6 +) + +type SuggestedTag struct { + Tag string `json:"tag"` + Reason string `json:"reason,omitempty"` + SearchIntent string `json:"searchIntent,omitempty"` + SearchType string `json:"searchType,omitempty"` +} + +// PickDefaultSelectedTags chooses a lean set for scanning (saves search API quota). +func PickDefaultSelectedTags(tags []SuggestedTag) []string { + short := []string{} + scenario := []string{} + quote := []string{} + account := []string{} + + for _, item := range tags { + tag := strings.TrimSpace(item.Tag) + if tag == "" { + continue + } + st := strings.TrimSpace(item.SearchType) + if strings.HasPrefix(tag, "@") || st == "帳號" { + account = append(account, tag) + continue + } + switch st { + case "短詞": + short = append(short, tag) + case "情境": + scenario = append(scenario, tag) + case "語錄": + quote = append(quote, tag) + default: + if len([]rune(tag)) <= 4 { + short = append(short, tag) + } else { + scenario = append(scenario, tag) + } + } + } + + out := []string{} + seen := map[string]struct{}{} + add := func(tag string) { + tag = strings.TrimSpace(tag) + if tag == "" { + return + } + if _, ok := seen[tag]; ok { + return + } + seen[tag] = struct{}{} + out = append(out, tag) + } + for _, tag := range short { + if len(out) >= 2 { + break + } + add(tag) + } + for _, tag := range scenario { + if len(out) >= 4 { + break + } + add(tag) + } + for _, tag := range quote { + if len(out) >= 5 { + break + } + add(tag) + } + for _, tag := range account { + if len(out) >= DefaultSelectedTagCount { + break + } + add(tag) + } + for _, tag := range short { + if len(out) >= DefaultSelectedTagCount { + break + } + add(tag) + } + for _, tag := range scenario { + if len(out) >= DefaultSelectedTagCount { + break + } + add(tag) + } + if len(out) > MaxScanTags { + out = out[:MaxScanTags] + } + return out +} + +func TagStrings(tags []SuggestedTag) []string { + out := make([]string, 0, len(tags)) + for _, item := range tags { + if tag := strings.TrimSpace(item.Tag); tag != "" { + out = append(out, tag) + } + } + return out +} diff --git a/haixun-backend/internal/library/viral/search_tags_test.go b/haixun-backend/internal/library/viral/search_tags_test.go new file mode 100644 index 0000000..30058e8 --- /dev/null +++ b/haixun-backend/internal/library/viral/search_tags_test.go @@ -0,0 +1,29 @@ +package viral + +import "testing" + +func TestPickDefaultSelectedTags(t *testing.T) { + tags := []SuggestedTag{ + {Tag: "轉職", SearchType: "短詞"}, + {Tag: "面試焦慮", SearchType: "短詞"}, + {Tag: "被裁怎麼辦", SearchType: "情境"}, + {Tag: "30歲轉職真實心得", SearchType: "情境"}, + {Tag: "努力不一定成功", SearchType: "語錄"}, + {Tag: "@powerfulceo", SearchType: "帳號"}, + {Tag: "薪水談判", SearchType: "短詞"}, + } + out := PickDefaultSelectedTags(tags) + if len(out) == 0 { + t.Fatal("expected selected tags") + } + if len(out) > DefaultSelectedTagCount { + t.Fatalf("expected at most %d tags, got %d", DefaultSelectedTagCount, len(out)) + } + seen := map[string]struct{}{} + for _, tag := range out { + if _, ok := seen[tag]; ok { + t.Fatalf("duplicate tag %q", tag) + } + seen[tag] = struct{}{} + } +} diff --git a/haixun-backend/internal/library/viral/tags.go b/haixun-backend/internal/library/viral/tags.go new file mode 100644 index 0000000..67de259 --- /dev/null +++ b/haixun-backend/internal/library/viral/tags.go @@ -0,0 +1,22 @@ +package viral + +import "strings" + +func IsAccountTag(tag string) bool { + tag = strings.TrimSpace(tag) + return strings.HasPrefix(tag, "@") && len(tag) > 1 +} + +func NormalizeAccountTag(tag string) string { + tag = strings.TrimSpace(tag) + tag = strings.TrimPrefix(tag, "@") + return strings.TrimSpace(tag) +} + +func DiscoverKeywordFromTag(tag string) string { + tag = strings.TrimSpace(tag) + if IsAccountTag(tag) { + return NormalizeAccountTag(tag) + } + return tag +} diff --git a/haixun-backend/internal/library/viral/tags_test.go b/haixun-backend/internal/library/viral/tags_test.go new file mode 100644 index 0000000..7465faa --- /dev/null +++ b/haixun-backend/internal/library/viral/tags_test.go @@ -0,0 +1,21 @@ +package viral + +import "testing" + +func TestIsAccountTag(t *testing.T) { + if !IsAccountTag("@creator") { + t.Fatal("expected account tag") + } + if IsAccountTag("keyword") { + t.Fatal("expected keyword tag") + } +} + +func TestDiscoverKeywordFromTag(t *testing.T) { + if got := DiscoverKeywordFromTag("@foo_bar"); got != "foo_bar" { + t.Fatalf("got %q", got) + } + if got := DiscoverKeywordFromTag("熱詞"); got != "熱詞" { + t.Fatalf("got %q", got) + } +} diff --git a/haixun-backend/internal/library/websearch/client.go b/haixun-backend/internal/library/websearch/client.go new file mode 100644 index 0000000..295fd15 --- /dev/null +++ b/haixun-backend/internal/library/websearch/client.go @@ -0,0 +1,205 @@ +package websearch + +import ( + "context" + "strings" + + libbrave "haixun-backend/internal/library/brave" + libexa "haixun-backend/internal/library/exa" +) + +type Provider string + +const ( + ProviderBrave Provider = "brave" + ProviderExa Provider = "exa" +) + +func ParseProvider(raw string) Provider { + switch Provider(strings.ToLower(strings.TrimSpace(raw))) { + case ProviderExa: + return ProviderExa + default: + return ProviderBrave + } +} + +type Mode string + +const ( + ModeKnowledgeExpand Mode = "knowledge_expand" + ModeThreadsDiscover Mode = "threads_discover" +) + +type SearchResult struct { + Title string + Snippet string + URL string +} + +type SearchResponse struct { + Results []SearchResult + Query string + Status string + Provider Provider +} + +type SearchOptions struct { + Query string + Limit int + Mode Mode + Country string + SearchLang string + UserLocation string + StartPublishedDate string +} + +type Client interface { + Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) + Enabled() bool + Provider() Provider +} + +type Config struct { + Provider Provider + BraveKey string + ExaKey string + Country string + SearchLang string + UserLocation string +} + +func New(cfg Config) Client { + switch ParseProvider(string(cfg.Provider)) { + case ProviderExa: + return &exaAdapter{ + client: libexa.NewClient(cfg.ExaKey), + provider: ProviderExa, + userLocation: cfg.UserLocation, + } + default: + return &braveAdapter{ + client: libbrave.NewClient(cfg.BraveKey), + provider: ProviderBrave, + country: cfg.Country, + searchLang: cfg.SearchLang, + } + } +} + +func ConfigFromMember(braveKey, exaKey, provider, country, searchLang, userLocation string) Config { + return Config{ + Provider: ParseProvider(provider), + BraveKey: braveKey, + ExaKey: exaKey, + Country: country, + SearchLang: searchLang, + UserLocation: userLocation, + } +} + +type braveAdapter struct { + client *libbrave.Client + provider Provider + country string + searchLang string +} + +func (a *braveAdapter) Provider() Provider { return a.provider } + +func (a *braveAdapter) Enabled() bool { + return a != nil && a.client != nil && a.client.Enabled() +} + +func (a *braveAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) { + mode := libbrave.ModeKnowledgeExpand + if opts.Mode == ModeThreadsDiscover { + mode = libbrave.ModeThreadsDiscover + } + res, err := a.client.Search(ctx, libbrave.SearchOptions{ + Query: opts.Query, + Limit: opts.Limit, + Mode: mode, + Country: firstNonEmpty(opts.Country, a.country), + SearchLang: firstNonEmpty(opts.SearchLang, a.searchLang), + }) + return toBraveResponse(res.Query, res.Status, a.provider, res.Results, err) +} + +type exaAdapter struct { + client *libexa.Client + provider Provider + userLocation string +} + +func (a *exaAdapter) Provider() Provider { return a.provider } + +func (a *exaAdapter) Enabled() bool { + return a != nil && a.client != nil && a.client.Enabled() +} + +func (a *exaAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) { + mode := libexa.ModeKnowledgeExpand + if opts.Mode == ModeThreadsDiscover { + mode = libexa.ModeThreadsDiscover + } + res, err := a.client.Search(ctx, libexa.SearchOptions{ + Query: opts.Query, + Limit: opts.Limit, + Mode: mode, + UserLocation: firstNonEmpty(opts.UserLocation, a.userLocation), + StartPublishedDate: opts.StartPublishedDate, + }) + out := SearchResponse{Query: res.Query, Status: res.Status, Provider: a.provider} + if err != nil { + return out, err + } + for _, item := range res.Results { + out.Results = append(out.Results, SearchResult{ + Title: item.Title, + Snippet: item.Snippet, + URL: item.URL, + }) + } + return out, nil +} + +func toBraveResponse(query, status string, provider Provider, items []libbrave.SearchResult, err error) (SearchResponse, error) { + out := SearchResponse{Query: query, Status: status, Provider: provider} + if err != nil { + return out, err + } + for _, item := range items { + out.Results = append(out.Results, SearchResult{ + Title: item.Title, + Snippet: item.Snippet, + URL: item.URL, + }) + } + return out, nil +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} + +func ActiveAPIKey(cfg Config) string { + if ParseProvider(string(cfg.Provider)) == ProviderExa { + return strings.TrimSpace(cfg.ExaKey) + } + return strings.TrimSpace(cfg.BraveKey) +} + +func ProviderLabel(provider Provider) string { + switch provider { + case ProviderExa: + return "Exa" + default: + return "Brave" + } +} diff --git a/haixun-backend/internal/logic/brand/expand_knowledge_graph_logic.go b/haixun-backend/internal/logic/brand/expand_knowledge_graph_logic.go index 2e99aa4..e4830dd 100644 --- a/haixun-backend/internal/logic/brand/expand_knowledge_graph_logic.go +++ b/haixun-backend/internal/logic/brand/expand_knowledge_graph_logic.go @@ -10,6 +10,7 @@ import ( app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" + "haixun-backend/internal/library/placement" jobdom "haixun-backend/internal/model/job/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" @@ -50,17 +51,14 @@ func (l *ExpandKnowledgeGraphLogic) ExpandKnowledgeGraph(req *types.ExpandKnowle if err != nil { return nil, err } - expandStrategy := libkg.ParseExpandStrategy(research.ExpandStrategy) - if supplemental { + expandStrategy := placement.EffectiveExpandStrategy(research) + if supplemental && placement.WebSearchAvailable(research) { expandStrategy = libkg.ExpandStrategyBrave } memberCtx, err := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) if err != nil { return nil, err } - if expandStrategy.RequiresBrave() && strings.TrimSpace(research.BraveAPIKey) == "" { - return nil, app.For(code.Setting).InputMissingRequired("請到設定頁完成研究資料連線") - } payload := map[string]any{ "brand_id": req.ID, diff --git a/haixun-backend/internal/logic/brand/start_brand_scan_job_logic.go b/haixun-backend/internal/logic/brand/start_brand_scan_job_logic.go index ec895fd..b6c8a81 100644 --- a/haixun-backend/internal/logic/brand/start_brand_scan_job_logic.go +++ b/haixun-backend/internal/logic/brand/start_brand_scan_job_logic.go @@ -120,8 +120,8 @@ func (l *StartBrandScanJobLogic) StartBrandScanJob(req *types.StartBrandScanJobH if !memberCtx.AllowsBrave && !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler { return nil, app.For(code.Setting).InputMissingRequired("目前連線模式無法海巡,請確認 Threads API、Brave 或 Chrome Session") } - if placement.MemberNeedsBraveKey(memberCtx) && strings.TrimSpace(research.BraveAPIKey) == "" { - return nil, app.For(code.Setting).InputMissingRequired("請在設定頁設定 Brave Search API key(跟隨此登入帳號)") + if placement.MemberNeedsWebSearchKey(memberCtx) && placement.MissingWebSearchKey(research) { + return nil, app.For(code.Setting).InputMissingRequired(placement.WebSearchKeyRequiredMessage(research)) } if memberCtx.DevMode && !memberCtx.BrowserConnected { return nil, app.For(code.Setting).InputMissingRequired("開發模式需先同步 Chrome Session") diff --git a/haixun-backend/internal/logic/copy_mission/actor.go b/haixun-backend/internal/logic/copy_mission/actor.go new file mode 100644 index 0000000..882a6f6 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/actor.go @@ -0,0 +1,17 @@ +package copy_mission + +import ( + "context" + + "haixun-backend/internal/library/authctx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" +) + +func actorFrom(ctx context.Context) (tenantID, uid string, err error) { + actor, ok := authctx.ActorFromContext(ctx) + if !ok { + return "", "", app.For(code.Auth).AuthUnauthorized("missing actor") + } + return actor.TenantID, actor.UID, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/create_copy_mission_logic.go b/haixun-backend/internal/logic/copy_mission/create_copy_mission_logic.go new file mode 100644 index 0000000..cc652ca --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/create_copy_mission_logic.go @@ -0,0 +1,57 @@ +package copy_mission + +import ( + "context" + "strings" + + missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateCopyMissionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCopyMissionLogic { + return &CreateCopyMissionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateCopyMissionLogic) CreateCopyMission(req *types.CreateCopyMissionHandlerReq) (*types.CopyMissionData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + initialMap, initialTags := seedMissionResearchFromPersona(*persona) + createReq := missiondomain.CreateRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + Label: req.Label, + SeedQuery: req.SeedQuery, + Brief: req.Brief, + } + if initialMap != nil { + createReq.InitialResearchMap = initialMap + createReq.InitialSelectedTags = initialTags + } + item, err := l.svcCtx.CopyMission.Create(l.ctx, createReq) + if err != nil { + return nil, err + } + data := toCopyMissionData(*item) + return &data, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/delete_copy_mission_logic.go b/haixun-backend/internal/logic/copy_mission/delete_copy_mission_logic.go new file mode 100644 index 0000000..6c64daf --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/delete_copy_mission_logic.go @@ -0,0 +1,33 @@ +package copy_mission + +import ( + "context" + "strings" + + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteCopyMissionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCopyMissionLogic { + return &DeleteCopyMissionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteCopyMissionLogic) DeleteCopyMission(req *types.CopyMissionPath) error { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return err + } + return l.svcCtx.CopyMission.Delete(l.ctx, tenantID, uid, strings.TrimSpace(req.PersonaID), strings.TrimSpace(req.ID)) +} diff --git a/haixun-backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go b/haixun-backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go new file mode 100644 index 0000000..5ce222a --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go @@ -0,0 +1,191 @@ +package copy_mission + +import ( + "context" + "fmt" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + libmatrix "haixun-backend/internal/library/matrix" + libprompt "haixun-backend/internal/library/prompt" + "haixun-backend/internal/library/style8d" + domai "haixun-backend/internal/model/ai/domain/usecase" + aiusecase "haixun-backend/internal/model/ai/usecase" + copydraftentity "haixun-backend/internal/model/copy_draft/domain/entity" + copydraftdomain "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" + scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type GenerateCopyMissionMatrixLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGenerateCopyMissionMatrixLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateCopyMissionMatrixLogic { + return &GenerateCopyMissionMatrixLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *GenerateCopyMissionMatrixLogic) GenerateCopyMissionMatrix( + req *types.GenerateCopyMissionMatrixHandlerReq, +) (*types.GenerateCopyMissionMatrixData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + mission, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID) + if err != nil { + return nil, err + } + if mission.Status != string(missionentity.StatusScanned) && + mission.Status != string(missionentity.StatusDrafted) { + return nil, app.For(code.Persona).ResInvalidState("請先完成海巡再產出內容矩陣") + } + if len(mission.SelectedTags) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("請先選擇海巡標籤") + } + + persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) + + count := req.Count + if count <= 0 { + count = 5 + } + if count > 12 { + count = 12 + } + + posts, err := l.svcCtx.ScanPost.ListForPersona(l.ctx, scanpostdomain.PersonaListRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + CopyMissionID: missionID, + Limit: 12, + }) + if err != nil { + return nil, err + } + samples := matrixSamplesFromScanPosts(posts) + researchBlock := libmatrix.FormatCopyResearchMapBlock( + mission.ResearchMap.AudienceSummary, + mission.ResearchMap.ContentGoal, + mission.ResearchMap.Questions, + mission.ResearchMap.Pillars, + mission.ResearchMap.Exclusions, + ) + userPrompt, err := libmatrix.BuildCopyUserPrompt(libmatrix.CopyGenerateInput{ + Count: count, + TopicLabel: mission.Label, + TopicBrief: mission.Brief, + ResearchMap: researchBlock, + SelectedTags: mission.SelectedTags, + ViralSamples: samples, + PersonaBlock: personaBlock, + }) + if err != nil { + return nil, app.For(code.AI).SysInternal("matrix user prompt load failed") + } + systemPrompt, err := libprompt.MatrixCopySystem() + if err != nil { + return nil, app.For(code.AI).SysInternal("matrix system prompt load failed") + } + credential, err := l.svcCtx.ThreadsAccount.ResolveMemberAiCredential(l.ctx, tenantID, uid) + if err != nil { + return nil, err + } + providerID, err := aiusecase.MapWorkerProvider(credential.Provider) + if err != nil { + return nil, err + } + result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: systemPrompt, + Messages: []domai.Message{ + {Role: "user", Content: userPrompt}, + }, + }) + if err != nil { + return nil, err + } + parsed, err := libmatrix.ParseGenerateOutput(result.Text) + if err != nil { + return nil, app.For(code.AI).SvcThirdParty("內容矩陣 LLM 回傳無法解析:" + err.Error()) + } + + createReqs := make([]copydraftdomain.CreateRequest, 0, len(parsed.Rows)) + for _, row := range parsed.Rows { + createReqs = append(createReqs, copydraftdomain.CreateRequest{ + CopyMissionID: missionID, + DraftType: copydraftentity.DraftTypeMatrix, + SortOrder: row.SortOrder, + Text: row.Text, + Angle: row.Angle, + Hook: row.Hook, + Rationale: row.Rationale, + ReferenceNotes: row.ReferenceNotes, + Sources: row.SourcePermalinks, + }) + } + saved, err := l.svcCtx.CopyDraft.ReplaceMissionMatrix(l.ctx, tenantID, uid, personaID, missionID, createReqs) + if err != nil { + return nil, err + } + + drafted := missionentity.StatusDrafted + _, _ = l.svcCtx.CopyMission.Update(l.ctx, missiondomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + MissionID: missionID, + Patch: missiondomain.MissionPatch{ + Status: &drafted, + }, + }) + + drafts := make([]types.CopyDraftData, 0, len(saved)) + for _, item := range saved { + drafts = append(drafts, toCopyDraftData(item)) + } + return &types.GenerateCopyMissionMatrixData{ + Drafts: drafts, + Message: fmt.Sprintf("已產出 %d 篇矩陣草稿", len(drafts)), + }, nil +} + +func matrixSamplesFromScanPosts(posts []scanpostdomain.ScanPostSummary) string { + samples := make([]libmatrix.ViralPostSample, 0, len(posts)) + for _, post := range posts { + replies := make([]libmatrix.ViralReplySample, 0, len(post.Replies)) + for _, reply := range post.Replies { + replies = append(replies, libmatrix.ViralReplySample{ + Author: reply.Author, + Text: reply.Text, + }) + } + samples = append(samples, libmatrix.ViralPostSample{ + Author: post.Author, + LikeCount: post.LikeCount, + SearchTag: post.SearchTag, + Text: post.Text, + Replies: replies, + }) + } + return libmatrix.FormatViralSamples(samples) +} diff --git a/haixun-backend/internal/logic/copy_mission/get_copy_mission_logic.go b/haixun-backend/internal/logic/copy_mission/get_copy_mission_logic.go new file mode 100644 index 0000000..2bb8467 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/get_copy_mission_logic.go @@ -0,0 +1,38 @@ +package copy_mission + +import ( + "context" + "strings" + + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetCopyMissionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCopyMissionLogic { + return &GetCopyMissionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetCopyMissionLogic) GetCopyMission(req *types.CopyMissionPath) (*types.CopyMissionData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + item, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, strings.TrimSpace(req.PersonaID), strings.TrimSpace(req.ID)) + if err != nil { + return nil, err + } + data := toCopyMissionData(*item) + return &data, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go b/haixun-backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go new file mode 100644 index 0000000..4b23184 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go @@ -0,0 +1,40 @@ +package copy_mission + +import ( + "context" + "strings" + + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type GetCopyMissionScanScheduleLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetCopyMissionScanScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCopyMissionScanScheduleLogic { + return &GetCopyMissionScanScheduleLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *GetCopyMissionScanScheduleLogic) GetCopyMissionScanSchedule( + req *types.CopyMissionPath, +) (*types.CopyMissionScanScheduleData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + if _, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + schedule, err := findCopyMissionScanSchedule(l.ctx, l.svcCtx, missionID) + if err != nil { + return nil, err + } + return toCopyMissionScanScheduleData(schedule, personaID, missionID), nil +} diff --git a/haixun-backend/internal/logic/copy_mission/inspire_copy_mission_logic.go b/haixun-backend/internal/logic/copy_mission/inspire_copy_mission_logic.go new file mode 100644 index 0000000..14c4576 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/inspire_copy_mission_logic.go @@ -0,0 +1,154 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/placement" + "haixun-backend/internal/library/style8d" + libviral "haixun-backend/internal/library/viral" + "haixun-backend/internal/library/websearch" + domai "haixun-backend/internal/model/ai/domain/usecase" + aiusecase "haixun-backend/internal/model/ai/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type InspireCopyMissionLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewInspireCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InspireCopyMissionLogic { + return &InspireCopyMissionLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *InspireCopyMissionLogic) InspireCopyMission(req *types.PersonaCopyMissionsPath) (*types.CopyMissionInspirationData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + + missionList, err := l.svcCtx.CopyMission.List(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + recentLabels := make([]string, 0, 8) + recentSeeds := make([]string, 0, 8) + for _, mission := range missionList.List { + if mission.Status == "archived" { + continue + } + if label := strings.TrimSpace(mission.Label); label != "" { + recentLabels = append(recentLabels, label) + } + if seed := strings.TrimSpace(mission.SeedQuery); seed != "" { + recentSeeds = append(recentSeeds, seed) + } + if len(recentLabels) >= 8 && len(recentSeeds) >= 8 { + break + } + } + + trendSnippets := []libviral.MissionInspireTrendSnippet{} + webSearchProvider := "" + llmOnly := true + + research, researchErr := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid) + if researchErr == nil && placement.WebSearchAvailable(research) { + memberCtx, memberErr := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) + if memberErr == nil { + webClient := websearch.New(memberCtx.WebSearchConfig()) + trendSnippets = libviral.CollectMissionInspireTrends( + l.ctx, + webClient, + memberCtx, + persona.Brief, + persona.StyleBenchmark, + ) + webSearchProvider = memberCtx.WebSearchProviderLabel() + llmOnly = len(trendSnippets) == 0 + } + } + webSearchUsed := len(trendSnippets) > 0 + + credential, err := l.svcCtx.ThreadsAccount.ResolveMemberAiCredential(l.ctx, tenantID, uid) + if err != nil { + return nil, err + } + providerID, err := aiusecase.MapWorkerProvider(credential.Provider) + if err != nil { + return nil, err + } + + personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) + result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: libviral.BuildMissionInspireSystemPrompt(), + Messages: []domai.Message{ + { + Role: "user", + Content: libviral.BuildMissionInspireUserPrompt(libviral.MissionInspireInput{ + PersonaDisplayName: persona.DisplayName, + PersonaBrief: persona.Brief, + PersonaBlock: personaBlock, + StyleBenchmark: persona.StyleBenchmark, + PersonaAudience: persona.CopyResearchMap.AudienceSummary, + PersonaContentGoal: persona.CopyResearchMap.ContentGoal, + PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...), + PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...), + RecentMissionLabels: recentLabels, + RecentSeedQueries: recentSeeds, + TrendSnippets: trendSnippets, + WebSearchProvider: webSearchProvider, + LLMOnly: llmOnly, + }), + }, + }, + }) + if err != nil { + return nil, err + } + + parsed, err := libviral.ParseMissionInspireOutput(result.Text) + if err != nil { + return nil, app.For(code.AI).SvcThirdParty("靈感骰子 LLM 回傳無法解析:" + err.Error()) + } + + sources := make([]types.CopyMissionInspirationSourceData, 0, len(trendSnippets)) + for _, item := range trendSnippets { + sources = append(sources, types.CopyMissionInspirationSourceData{ + Query: item.Query, + Title: item.Title, + Snippet: item.Snippet, + URL: item.URL, + }) + } + + message := "已依近期趨勢產出任務靈感,可微調後建立任務" + if !webSearchUsed { + message = "未設定搜尋 API key,已改由 LLM 依人設推測靈感;補上 Brave/Exa key 可取得更貼近熱搜的結果" + } + + return &types.CopyMissionInspirationData{ + Label: parsed.Label, + SeedQuery: parsed.SeedQuery, + Brief: parsed.Brief, + TrendReason: parsed.TrendReason, + TrendKeywords: parsed.TrendKeywords, + Sources: sources, + WebSearchUsed: webSearchUsed, + Message: message, + }, nil +} \ No newline at end of file diff --git a/haixun-backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go b/haixun-backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go new file mode 100644 index 0000000..7acba5e --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go @@ -0,0 +1,40 @@ +package copy_mission + +import ( + "context" + "strings" + + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type ListCopyMissionCopyDraftsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewListCopyMissionCopyDraftsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCopyMissionCopyDraftsLogic { + return &ListCopyMissionCopyDraftsLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *ListCopyMissionCopyDraftsLogic) ListCopyMissionCopyDrafts(req *types.CopyMissionPath) (*types.ListCopyMissionCopyDraftsData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + + drafts, err := l.svcCtx.CopyDraft.ListByMission(l.ctx, tenantID, uid, personaID, missionID, 50) + if err != nil { + return nil, err + } + list := make([]types.CopyDraftData, 0, len(drafts)) + for _, item := range drafts { + list = append(list, toCopyDraftData(item)) + } + return &types.ListCopyMissionCopyDraftsData{List: list, Total: len(list)}, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go b/haixun-backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go new file mode 100644 index 0000000..6281c70 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go @@ -0,0 +1,49 @@ +package copy_mission + +import ( + "context" + "strings" + + scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type ListCopyMissionScanPostsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewListCopyMissionScanPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCopyMissionScanPostsLogic { + return &ListCopyMissionScanPostsLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *ListCopyMissionScanPostsLogic) ListCopyMissionScanPosts( + req *types.ListCopyMissionScanPostsHandlerReq, +) (*types.ListPersonaViralScanPostsData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + + limit := 100 + if req.Limit > 0 { + limit = req.Limit + } + posts, err := l.svcCtx.ScanPost.ListForPersona(l.ctx, scanpostdomain.PersonaListRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + CopyMissionID: missionID, + Limit: limit, + }) + if err != nil { + return nil, err + } + return toViralScanPostsData(posts), nil +} diff --git a/haixun-backend/internal/logic/copy_mission/list_copy_missions_logic.go b/haixun-backend/internal/logic/copy_mission/list_copy_missions_logic.go new file mode 100644 index 0000000..f99b9f0 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/list_copy_missions_logic.go @@ -0,0 +1,41 @@ +package copy_mission + +import ( + "context" + "strings" + + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListCopyMissionsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewListCopyMissionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCopyMissionsLogic { + return &ListCopyMissionsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ListCopyMissionsLogic) ListCopyMissions(req *types.PersonaCopyMissionsPath) (*types.ListCopyMissionsData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + result, err := l.svcCtx.CopyMission.List(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + return toListData(result), nil +} diff --git a/haixun-backend/internal/logic/copy_mission/mapper.go b/haixun-backend/internal/logic/copy_mission/mapper.go new file mode 100644 index 0000000..ca857c1 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/mapper.go @@ -0,0 +1,237 @@ +package copy_mission + +import ( + copydraftdomain "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" + personadomain "haixun-backend/internal/model/persona/domain/usecase" + scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" + "haixun-backend/internal/types" +) + +func seedMissionResearchFromPersona(persona personadomain.PersonaSummary) (*missionentity.ResearchMap, []string) { + src := persona.CopyResearchMap + if src.AudienceSummary == "" && src.ContentGoal == "" && len(src.SuggestedTags) == 0 { + return nil, nil + } + tags := make([]missionentity.SuggestedTag, 0, len(src.SuggestedTags)) + for _, tag := range src.SuggestedTags { + if tag == "" { + continue + } + tags = append(tags, missionentity.SuggestedTag{Tag: tag}) + } + selected := append([]string(nil), src.SuggestedTags...) + if len(selected) > 6 { + selected = selected[:6] + } + rm := &missionentity.ResearchMap{ + AudienceSummary: src.AudienceSummary, + ContentGoal: src.ContentGoal, + Questions: append([]string(nil), src.Questions...), + Pillars: append([]string(nil), src.Pillars...), + Exclusions: append([]string(nil), src.Exclusions...), + SuggestedTags: tags, + BenchmarkNotes: src.BenchmarkNotes, + } + return rm, selected +} + +func toMissionPatch(req *types.UpdateCopyMissionReq) missiondomain.MissionPatch { + if req == nil { + return missiondomain.MissionPatch{} + } + patch := missiondomain.MissionPatch{ + Label: req.Label, + SeedQuery: req.SeedQuery, + Brief: req.Brief, + } + if req.AudienceSummary != nil { + patch.AudienceSummary = req.AudienceSummary + } + if req.ContentGoal != nil { + patch.ContentGoal = req.ContentGoal + } + if req.Questions != nil { + patch.QuestionsSet = true + patch.Questions = append([]string(nil), req.Questions...) + } + if req.Pillars != nil { + patch.PillarsSet = true + patch.Pillars = append([]string(nil), req.Pillars...) + } + if req.Exclusions != nil { + patch.ExclusionsSet = true + patch.Exclusions = append([]string(nil), req.Exclusions...) + } + if req.BenchmarkNotes != nil { + patch.BenchmarkNotes = req.BenchmarkNotes + } + if req.SelectedTags != nil { + patch.SelectedTagsSet = true + patch.SelectedTags = append([]string(nil), req.SelectedTags...) + } + return patch +} + +func toCopyMissionData(item missiondomain.MissionSummary) types.CopyMissionData { + tags := make([]types.CopySuggestedTagData, 0, len(item.ResearchMap.SuggestedTags)) + for _, tag := range item.ResearchMap.SuggestedTags { + tags = append(tags, types.CopySuggestedTagData{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + accounts := make([]types.CopySimilarAccountData, 0, len(item.ResearchMap.SimilarAccounts)) + for _, acc := range item.ResearchMap.SimilarAccounts { + accounts = append(accounts, types.CopySimilarAccountData{ + Username: acc.Username, + Reason: acc.Reason, + Source: acc.Source, + Confidence: acc.Confidence, + ProfileUrl: acc.ProfileURL, + AuthorVerified: acc.AuthorVerified, + FollowerCount: acc.FollowerCount, + EngagementScore: acc.EngagementScore, + LikeCount: acc.LikeCount, + ReplyCount: acc.ReplyCount, + PostCount: acc.PostCount, + }) + } + return types.CopyMissionData{ + ID: item.ID, + PersonaID: item.PersonaID, + Label: item.Label, + SeedQuery: item.SeedQuery, + Brief: item.Brief, + ResearchMap: types.CopyMissionResearchMapData{ + AudienceSummary: item.ResearchMap.AudienceSummary, + ContentGoal: item.ResearchMap.ContentGoal, + Questions: append([]string(nil), item.ResearchMap.Questions...), + Pillars: append([]string(nil), item.ResearchMap.Pillars...), + Exclusions: append([]string(nil), item.ResearchMap.Exclusions...), + SuggestedTags: tags, + SimilarAccounts: accounts, + BenchmarkNotes: item.ResearchMap.BenchmarkNotes, + }, + SelectedTags: append([]string(nil), item.SelectedTags...), + LastScanJobID: item.LastScanJobID, + Status: item.Status, + CreateAt: item.CreateAt, + UpdateAt: item.UpdateAt, + } +} + +func toListData(result *missiondomain.ListResult) *types.ListCopyMissionsData { + if result == nil { + return &types.ListCopyMissionsData{List: []types.CopyMissionData{}} + } + list := make([]types.CopyMissionData, 0, len(result.List)) + for _, item := range result.List { + list = append(list, toCopyMissionData(item)) + } + return &types.ListCopyMissionsData{List: list} +} + +func toEntityResearchMap(data types.CopyMissionResearchMapData) missionentity.ResearchMap { + tags := make([]missionentity.SuggestedTag, 0, len(data.SuggestedTags)) + for _, tag := range data.SuggestedTags { + tags = append(tags, missionentity.SuggestedTag{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + accounts := make([]missionentity.SimilarAccount, 0, len(data.SimilarAccounts)) + for _, acc := range data.SimilarAccounts { + accounts = append(accounts, missionentity.SimilarAccount{ + Username: acc.Username, + Reason: acc.Reason, + Source: acc.Source, + Confidence: acc.Confidence, + ProfileURL: acc.ProfileUrl, + AuthorVerified: acc.AuthorVerified, + FollowerCount: acc.FollowerCount, + EngagementScore: acc.EngagementScore, + LikeCount: acc.LikeCount, + ReplyCount: acc.ReplyCount, + PostCount: acc.PostCount, + }) + } + return missionentity.ResearchMap{ + AudienceSummary: data.AudienceSummary, + ContentGoal: data.ContentGoal, + Questions: append([]string(nil), data.Questions...), + Pillars: append([]string(nil), data.Pillars...), + Exclusions: append([]string(nil), data.Exclusions...), + SuggestedTags: tags, + SimilarAccounts: accounts, + BenchmarkNotes: data.BenchmarkNotes, + } +} + +func toViralScanPostsData(posts []scanpostdomain.ScanPostSummary) *types.ListPersonaViralScanPostsData { + list := make([]types.ViralScanPostData, 0, len(posts)) + for _, post := range posts { + list = append(list, types.ViralScanPostData{ + ID: post.ID, + SearchTag: post.SearchTag, + Permalink: post.Permalink, + Author: post.Author, + AuthorVerified: post.AuthorVerified, + FollowerCount: post.FollowerCount, + Text: post.Text, + LikeCount: post.LikeCount, + ReplyCount: post.ReplyCount, + EngagementScore: post.EngagementScore, + Source: post.Source, + ScanJobID: post.ScanJobID, + Replies: toScanReplyData(post.Replies), + CreateAt: post.CreateAt, + }) + } + return &types.ListPersonaViralScanPostsData{List: list, Total: len(list)} +} + +func toScanReplyData(replies []scanpostdomain.ScanReplySummary) []types.ScanReplyData { + if len(replies) == 0 { + return nil + } + out := make([]types.ScanReplyData, 0, len(replies)) + for _, reply := range replies { + out = append(out, types.ScanReplyData{ + ExternalID: reply.ExternalID, + Author: reply.Author, + Text: reply.Text, + Permalink: reply.Permalink, + LikeCount: reply.LikeCount, + PostedAt: reply.PostedAt, + }) + } + return out +} + +func toCopyDraftData(item copydraftdomain.CopyDraftSummary) types.CopyDraftData { + return types.CopyDraftData{ + ID: item.ID, + PersonaID: item.PersonaID, + CopyMissionID: item.CopyMissionID, + ScanPostID: item.ScanPostID, + DraftType: item.DraftType, + SortOrder: item.SortOrder, + Text: item.Text, + Angle: item.Angle, + Hook: item.Hook, + Rationale: item.Rationale, + ReferenceNotes: item.ReferenceNotes, + Sources: item.Sources, + Status: item.Status, + PublishedMediaID: item.PublishedMediaID, + PublishedPermalink: item.PublishedPermalink, + PublishedAt: item.PublishedAt, + CreateAt: item.CreateAt, + } +} diff --git a/haixun-backend/internal/logic/copy_mission/schedule_helper.go b/haixun-backend/internal/logic/copy_mission/schedule_helper.go new file mode 100644 index 0000000..23f59d7 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/schedule_helper.go @@ -0,0 +1,49 @@ +package copy_mission + +import ( + "context" + + jobentity "haixun-backend/internal/model/job/domain/entity" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +const viralScanTemplate = "scan-viral" + +func findCopyMissionScanSchedule(ctx context.Context, svcCtx *svc.ServiceContext, missionID string) (*jobentity.Schedule, error) { + schedules, _, _, _, _, err := svcCtx.Job.ListSchedules(ctx, "copy_mission", missionID, 1, 50) + if err != nil { + return nil, err + } + for _, schedule := range schedules { + if schedule != nil && schedule.TemplateType == viralScanTemplate { + return schedule, nil + } + } + return nil, nil +} + +func toCopyMissionScanScheduleData(schedule *jobentity.Schedule, personaID, missionID string) *types.CopyMissionScanScheduleData { + if schedule == nil { + return &types.CopyMissionScanScheduleData{ + PersonaID: personaID, + MissionID: missionID, + Cron: "0 9 * * *", + Timezone: "Asia/Taipei", + Enabled: false, + } + } + data := &types.CopyMissionScanScheduleData{ + ID: schedule.ID.Hex(), + PersonaID: personaID, + MissionID: missionID, + Cron: schedule.Cron, + Timezone: schedule.Timezone, + Enabled: schedule.Enabled, + NextRunAt: schedule.NextRunAt, + } + if schedule.LastRunAt != nil { + data.LastRunAt = *schedule.LastRunAt + } + return data +} diff --git a/haixun-backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go b/haixun-backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go new file mode 100644 index 0000000..8614b29 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go @@ -0,0 +1,64 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + jobdom "haixun-backend/internal/model/job/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type StartCopyMissionAnalyzeJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewStartCopyMissionAnalyzeJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionAnalyzeJobLogic { + return &StartCopyMissionAnalyzeJobLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *StartCopyMissionAnalyzeJobLogic) StartCopyMissionAnalyzeJob( + req *types.CopyMissionPath, +) (*types.StartCopyMissionAnalyzeJobData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + if _, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + + payload := map[string]any{ + "tenant_id": tenantID, + "owner_uid": uid, + "persona_id": personaID, + "copy_mission_id": missionID, + } + run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{ + TemplateType: "analyze-copy-mission", + Scope: "copy_mission", + ScopeID: missionID, + TenantID: tenantID, + OwnerUID: uid, + Payload: payload, + }) + if err != nil { + return nil, err + } + if run == nil { + return nil, app.For(code.Job).ResInvalidState("analyze job not created") + } + return &types.StartCopyMissionAnalyzeJobData{ + JobID: run.ID.Hex(), + Status: string(run.Status), + Message: "受眾與研究地圖分析已在背景執行,完成後可微調標籤並開始海巡", + }, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go b/haixun-backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go new file mode 100644 index 0000000..a37266a --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go @@ -0,0 +1,77 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/style8d" + jobdom "haixun-backend/internal/model/job/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type StartCopyMissionCopyDraftJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewStartCopyMissionCopyDraftJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionCopyDraftJobLogic { + return &StartCopyMissionCopyDraftJobLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *StartCopyMissionCopyDraftJobLogic) StartCopyMissionCopyDraftJob( + req *types.StartCopyMissionCopyDraftJobHandlerReq, +) (*types.StartCopyMissionCopyDraftJobData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + scanPostID := strings.TrimSpace(req.ScanPostID) + if scanPostID == "" { + return nil, app.For(code.Persona).InputMissingRequired("scan_post_id is required") + } + + if _, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + post, err := l.svcCtx.ScanPost.GetForPersona(l.ctx, tenantID, uid, personaID, scanPostID) + if err != nil { + return nil, err + } + if postMission := strings.TrimSpace(post.CopyMissionID); postMission != "" && postMission != missionID { + return nil, app.For(code.Persona).ResInvalidState("爆款不屬於此任務") + } + + payload := map[string]any{ + "persona_id": personaID, + "copy_mission_id": missionID, + "scan_post_id": scanPostID, + } + run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{ + TemplateType: "generate-copy-draft", + Scope: "copy_mission", + ScopeID: missionID, + TenantID: tenantID, + OwnerUID: uid, + Payload: payload, + }) + if err != nil { + return nil, err + } + return &types.StartCopyMissionCopyDraftJobData{ + JobID: run.ID.Hex(), + Status: string(run.Status), + Message: "深仿寫已在背景執行", + }, nil +} \ No newline at end of file diff --git a/haixun-backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go b/haixun-backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go new file mode 100644 index 0000000..6abd97b --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go @@ -0,0 +1,83 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/style8d" + missionentity "haixun-backend/internal/model/copy_mission/domain/entity" + jobdom "haixun-backend/internal/model/job/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type StartCopyMissionMatrixJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewStartCopyMissionMatrixJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionMatrixJobLogic { + return &StartCopyMissionMatrixJobLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *StartCopyMissionMatrixJobLogic) StartCopyMissionMatrixJob( + req *types.StartCopyMissionMatrixJobHandlerReq, +) (*types.StartCopyMissionMatrixJobData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + mission, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID) + if err != nil { + return nil, err + } + if mission.Status != string(missionentity.StatusScanned) && + mission.Status != string(missionentity.StatusDrafted) { + return nil, app.For(code.Persona).ResInvalidState("請先完成海巡再產出內容矩陣") + } + if len(mission.SelectedTags) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("請先選擇海巡標籤") + } + + persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) + if err != nil { + return nil, err + } + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + + count := req.Count + if count <= 0 { + count = 5 + } + if count > 12 { + count = 12 + } + + payload := map[string]any{ + "persona_id": personaID, + "copy_mission_id": missionID, + "count": count, + } + run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{ + TemplateType: "generate-copy-matrix", + Scope: "copy_mission", + ScopeID: missionID, + TenantID: tenantID, + OwnerUID: uid, + Payload: payload, + }) + if err != nil { + return nil, err + } + return &types.StartCopyMissionMatrixJobData{ + JobID: run.ID.Hex(), + Status: string(run.Status), + Message: "內容矩陣產出已在背景執行", + }, nil +} \ No newline at end of file diff --git a/haixun-backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go b/haixun-backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go new file mode 100644 index 0000000..c4b0fc6 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go @@ -0,0 +1,95 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + libviral "haixun-backend/internal/library/viral" + jobdom "haixun-backend/internal/model/job/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type StartCopyMissionScanJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewStartCopyMissionScanJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionScanJobLogic { + return &StartCopyMissionScanJobLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *StartCopyMissionScanJobLogic) StartCopyMissionScanJob( + req *types.CopyMissionPath, +) (*types.StartCopyMissionScanJobData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + mission, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID) + if err != nil { + return nil, err + } + if mission.ResearchMap.AudienceSummary == "" && len(mission.ResearchMap.SuggestedTags) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("請先產生研究地圖") + } + if len(mission.SelectedTags) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("請至少勾選一個搜尋標籤") + } + if len(mission.SelectedTags) > libviral.MaxScanTags { + return nil, app.For(code.Persona).InputMissingRequired("搜尋標籤最多 6 個,請減少勾選以節省搜尋配額") + } + + research, err := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid) + if err != nil { + return nil, err + } + memberCtx, err := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) + if err != nil { + return nil, err + } + if !memberCtx.HasDiscoverPath() { + return nil, app.For(code.Setting).InputMissingRequired("爆款掃描需要 Threads API、Chrome Session 或 Web Search API(請檢查連線模式與搜尋來源)") + } + + if err := l.svcCtx.ScanPost.ClearCopyMissionViralScan(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + if err := l.svcCtx.CopyDraft.ClearByMission(l.ctx, tenantID, uid, personaID, missionID); err != nil { + return nil, err + } + + payload := map[string]any{ + "persona_id": personaID, + "copy_mission_id": missionID, + "keywords": mission.SelectedTags, + "bootstrap": false, + } + for key, value := range memberCtx.PayloadFields() { + payload[key] = value + } + + run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{ + TemplateType: "scan-viral", + Scope: "copy_mission", + ScopeID: missionID, + TenantID: tenantID, + OwnerUID: uid, + Payload: payload, + }) + if err != nil { + return nil, err + } + return &types.StartCopyMissionScanJobData{ + JobID: run.ID.Hex(), + Status: string(run.Status), + Message: "已清除舊爆款與產文草稿,海巡已在背景執行", + }, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/update_copy_mission_logic.go b/haixun-backend/internal/logic/copy_mission/update_copy_mission_logic.go new file mode 100644 index 0000000..ef1c748 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/update_copy_mission_logic.go @@ -0,0 +1,67 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + 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" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateCopyMissionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateCopyMissionLogic { + return &UpdateCopyMissionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateCopyMissionLogic) UpdateCopyMission(req *types.UpdateCopyMissionHandlerReq) (*types.CopyMissionData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + patch := toMissionPatch(&req.UpdateCopyMissionReq) + if req.Status != nil { + status := strings.TrimSpace(*req.Status) + if status != string(missionentity.StatusArchived) && status != string(missionentity.StatusOpen) { + return nil, app.For(code.Persona).InputMissingRequired("status 僅支援 archived 或 open") + } + st := missionentity.Status(status) + patch.Status = &st + } + item, err := l.svcCtx.CopyMission.Update(l.ctx, missiondomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: strings.TrimSpace(req.PersonaID), + MissionID: strings.TrimSpace(req.ID), + Patch: patch, + }) + if err != nil { + return nil, err + } + if req.Status != nil && strings.TrimSpace(*req.Status) == string(missionentity.StatusArchived) { + if schedule, err := findCopyMissionScanSchedule(l.ctx, l.svcCtx, strings.TrimSpace(req.ID)); err == nil && schedule != nil && schedule.Enabled { + disabled := false + _, _ = l.svcCtx.Job.UpdateSchedule(l.ctx, jobdom.UpdateScheduleRequest{ + ID: schedule.ID.Hex(), + Enabled: &disabled, + }) + } + } + data := toCopyMissionData(*item) + return &data, nil +} diff --git a/haixun-backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go b/haixun-backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go new file mode 100644 index 0000000..80b2c13 --- /dev/null +++ b/haixun-backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go @@ -0,0 +1,106 @@ +package copy_mission + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + missionentity "haixun-backend/internal/model/copy_mission/domain/entity" + jobdom "haixun-backend/internal/model/job/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type UpsertCopyMissionScanScheduleLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpsertCopyMissionScanScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpsertCopyMissionScanScheduleLogic { + return &UpsertCopyMissionScanScheduleLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *UpsertCopyMissionScanScheduleLogic) UpsertCopyMissionScanSchedule( + req *types.UpsertCopyMissionScanScheduleHandlerReq, +) (*types.CopyMissionScanScheduleData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.PersonaID) + missionID := strings.TrimSpace(req.ID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + mission, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID) + if err != nil { + return nil, err + } + if mission.Status == string(missionentity.StatusArchived) { + return nil, app.For(code.Persona).ResInvalidState("已封存的任務無法設定排程") + } + if req.Enabled && len(mission.SelectedTags) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("啟用排程前請先勾選並儲存搜尋標籤") + } + + cronExpr := "0 9 * * *" + timezone := "Asia/Taipei" + if strings.TrimSpace(req.Cron) != "" { + cronExpr = strings.TrimSpace(req.Cron) + } + if strings.TrimSpace(req.Timezone) != "" { + timezone = strings.TrimSpace(req.Timezone) + } + + research, err := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid) + if err != nil { + return nil, err + } + memberCtx, err := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) + if err != nil { + return nil, err + } + + payload := map[string]any{ + "persona_id": personaID, + "copy_mission_id": missionID, + "bootstrap": false, + "scheduled": true, + } + for key, value := range memberCtx.PayloadFields() { + payload[key] = value + } + + existing, err := findCopyMissionScanSchedule(l.ctx, l.svcCtx, missionID) + if err != nil { + return nil, err + } + if existing != nil { + updated, err := l.svcCtx.Job.UpdateSchedule(l.ctx, jobdom.UpdateScheduleRequest{ + ID: existing.ID.Hex(), + Cron: cronExpr, + Timezone: timezone, + PayloadTemplate: payload, + Enabled: &req.Enabled, + }) + if err != nil { + return nil, err + } + return toCopyMissionScanScheduleData(updated, personaID, missionID), nil + } + + created, err := l.svcCtx.Job.CreateSchedule(l.ctx, jobdom.CreateScheduleRequest{ + TemplateType: viralScanTemplate, + Scope: "copy_mission", + ScopeID: missionID, + Cron: cronExpr, + Timezone: timezone, + PayloadTemplate: payload, + Enabled: req.Enabled, + }) + if err != nil { + return nil, err + } + return toCopyMissionScanScheduleData(created, personaID, missionID), nil +} diff --git a/haixun-backend/internal/logic/job/actor.go b/haixun-backend/internal/logic/job/actor.go index d9c41b8..7921116 100644 --- a/haixun-backend/internal/logic/job/actor.go +++ b/haixun-backend/internal/logic/job/actor.go @@ -14,4 +14,4 @@ func actorFrom(ctx context.Context) (tenantID, uid string, err error) { return "", "", app.For(code.Auth).AuthUnauthorized("missing actor") } return actor.TenantID, actor.UID, nil -} \ No newline at end of file +} diff --git a/haixun-backend/internal/logic/job/analyze_style8_d_from_worker_logic.go b/haixun-backend/internal/logic/job/analyze_style8_d_from_worker_logic.go index 9ed7d51..d27b13a 100644 --- a/haixun-backend/internal/logic/job/analyze_style8_d_from_worker_logic.go +++ b/haixun-backend/internal/logic/job/analyze_style8_d_from_worker_logic.go @@ -125,14 +125,19 @@ func (l *AnalyzeStyle8DFromWorkerLogic) AnalyzeStyle8DFromWorker(req *types.Anal Steps: steps, }) + personaDraft := strings.TrimSpace(profile.PersonaDraft) + patch := personausecase.PersonaPatch{ + StyleProfile: &profileJSON, + StyleBenchmark: &username, + } + if personaDraft != "" { + patch.Persona = &personaDraft + } _, err = l.svcCtx.Persona.Update(l.ctx, personausecase.UpdateRequest{ TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, - Patch: personausecase.PersonaPatch{ - StyleProfile: &profileJSON, - StyleBenchmark: &username, - }, + Patch: patch, }) if err != nil { return nil, err diff --git a/haixun-backend/internal/logic/job/ownership.go b/haixun-backend/internal/logic/job/ownership.go index 015220f..ab70b97 100644 --- a/haixun-backend/internal/logic/job/ownership.go +++ b/haixun-backend/internal/logic/job/ownership.go @@ -50,4 +50,4 @@ func stringFromPayload(payload map[string]any, key string) string { return "" } return strings.TrimSpace(text) -} \ No newline at end of file +} diff --git a/haixun-backend/internal/logic/job/ownership_test.go b/haixun-backend/internal/logic/job/ownership_test.go index 2c942d4..559c3a7 100644 --- a/haixun-backend/internal/logic/job/ownership_test.go +++ b/haixun-backend/internal/logic/job/ownership_test.go @@ -39,4 +39,4 @@ func TestRunOwnedByUserScopeLegacy(t *testing.T) { if !runOwnedBy(run, "tenant-a", "user-1") { t.Fatal("expected legacy user scope match") } -} \ No newline at end of file +} diff --git a/haixun-backend/internal/logic/member/mapper.go b/haixun-backend/internal/logic/member/mapper.go index 229f7e8..ae262f9 100644 --- a/haixun-backend/internal/logic/member/mapper.go +++ b/haixun-backend/internal/logic/member/mapper.go @@ -24,10 +24,14 @@ func toPlacementSettingsData(settings *placementusecase.Settings) types.MemberPl return types.MemberPlacementSettingsData{} } return types.MemberPlacementSettingsData{ + WebSearchProvider: settings.WebSearchProvider, BraveAPIKey: settings.BraveAPIKey, BraveAPIKeyConfigured: settings.BraveAPIKeyConfigured, + ExaAPIKey: settings.ExaAPIKey, + ExaAPIKeyConfigured: settings.ExaAPIKeyConfigured, BraveCountry: settings.BraveCountry, BraveSearchLang: settings.BraveSearchLang, + ExaUserLocation: settings.ExaUserLocation, ExpandStrategy: settings.ExpandStrategy, } } diff --git a/haixun-backend/internal/logic/member/update_member_placement_settings_logic.go b/haixun-backend/internal/logic/member/update_member_placement_settings_logic.go index 92b9a1f..14e918f 100644 --- a/haixun-backend/internal/logic/member/update_member_placement_settings_logic.go +++ b/haixun-backend/internal/logic/member/update_member_placement_settings_logic.go @@ -33,10 +33,13 @@ func (l *UpdateMemberPlacementSettingsLogic) UpdateMemberPlacementSettings(req * return nil, err } settings, err := l.svcCtx.Placement.Update(l.ctx, tenantID, uid, placementusecase.SettingsPatch{ - BraveAPIKey: req.BraveAPIKey, - BraveCountry: req.BraveCountry, - BraveSearchLang: req.BraveSearchLang, - ExpandStrategy: req.ExpandStrategy, + WebSearchProvider: req.WebSearchProvider, + BraveAPIKey: req.BraveAPIKey, + ExaAPIKey: req.ExaAPIKey, + BraveCountry: req.BraveCountry, + BraveSearchLang: req.BraveSearchLang, + ExaUserLocation: req.ExaUserLocation, + ExpandStrategy: req.ExpandStrategy, }) if err != nil { return nil, err diff --git a/haixun-backend/internal/logic/persona/generate_persona_copy_draft_logic.go b/haixun-backend/internal/logic/persona/generate_persona_copy_draft_logic.go index e508fc9..fbfdf12 100644 --- a/haixun-backend/internal/logic/persona/generate_persona_copy_draft_logic.go +++ b/haixun-backend/internal/logic/persona/generate_persona_copy_draft_logic.go @@ -6,6 +6,7 @@ import ( app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/style8d" libviral "haixun-backend/internal/library/viral" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" @@ -45,6 +46,24 @@ func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( return nil, err } + topicLabel := strings.TrimSpace(post.SearchTag) + topicBrief := strings.TrimSpace(persona.Brief) + if missionID := strings.TrimSpace(post.CopyMissionID); missionID != "" { + if mission, missionErr := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); missionErr == nil { + if label := strings.TrimSpace(mission.Label); label != "" { + topicLabel = label + } + if brief := strings.TrimSpace(mission.Brief); brief != "" { + topicBrief = brief + } + } + } + + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) + credential, err := l.svcCtx.ThreadsAccount.ResolveMemberAiCredential(l.ctx, tenantID, uid) if err != nil { return nil, err @@ -54,6 +73,36 @@ func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( return nil, err } + analysisText := "" + analyzeResult, analyzeErr := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: libviral.BuildAnalyzeViralSystemPrompt(), + Messages: []domai.Message{ + { + Role: "user", + Content: libviral.BuildAnalyzeViralUserPrompt(libviral.AnalyzeViralInput{ + PostText: post.Text, + AuthorName: post.Author, + LikeCount: post.LikeCount, + ReplyCount: post.ReplyCount, + SearchTag: post.SearchTag, + TopicLabel: topicLabel, + TopicBrief: topicBrief, + Persona: personaBlock, + }), + }, + }, + }) + if analyzeErr == nil { + if parsed, parseErr := libviral.ParseAnalyzeViralOutput(analyzeResult.Text); parseErr == nil { + analysisText = libviral.FormatAnalysisForReplicate(parsed) + } + } + result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, @@ -65,12 +114,13 @@ func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( { Role: "user", Content: libviral.BuildUserPrompt(libviral.ReplicateInput{ - TopicLabel: post.SearchTag, - TopicBrief: persona.Brief, - Persona: persona.Persona, - StyleProfile: persona.StyleProfile, - OriginalText: post.Text, - AuthorName: post.Author, + TopicLabel: topicLabel, + TopicBrief: topicBrief, + Persona: personaBlock, + StyleProfile: "", + OriginalText: post.Text, + AuthorName: post.Author, + StructureAnalysis: analysisText, }), }, }, @@ -88,6 +138,7 @@ func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, + CopyMissionID: post.CopyMissionID, ScanPostID: scanPostID, DraftType: "replicate", Text: parsed.Text, @@ -105,8 +156,10 @@ func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( Draft: types.CopyDraftData{ ID: saved.ID, PersonaID: saved.PersonaID, + CopyMissionID: saved.CopyMissionID, ScanPostID: saved.ScanPostID, DraftType: saved.DraftType, + SortOrder: saved.SortOrder, Text: saved.Text, Angle: saved.Angle, Hook: saved.Hook, diff --git a/haixun-backend/internal/logic/persona/list_persona_copy_drafts_logic.go b/haixun-backend/internal/logic/persona/list_persona_copy_drafts_logic.go index cd4e9e6..de35a46 100644 --- a/haixun-backend/internal/logic/persona/list_persona_copy_drafts_logic.go +++ b/haixun-backend/internal/logic/persona/list_persona_copy_drafts_logic.go @@ -36,8 +36,10 @@ func (l *ListPersonaCopyDraftsLogic) ListPersonaCopyDrafts(req *types.PersonaPat list = append(list, types.CopyDraftData{ ID: item.ID, PersonaID: item.PersonaID, + CopyMissionID: item.CopyMissionID, ScanPostID: item.ScanPostID, DraftType: item.DraftType, + SortOrder: item.SortOrder, Text: item.Text, Angle: item.Angle, Hook: item.Hook, diff --git a/haixun-backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go b/haixun-backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go index 2e7bef1..8448035 100644 --- a/haixun-backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go +++ b/haixun-backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go @@ -51,14 +51,35 @@ func (l *ListPersonaViralScanPostsLogic) ListPersonaViralScanPosts( SearchTag: post.SearchTag, Permalink: post.Permalink, Author: post.Author, + AuthorVerified: post.AuthorVerified, + FollowerCount: post.FollowerCount, Text: post.Text, LikeCount: post.LikeCount, ReplyCount: post.ReplyCount, EngagementScore: post.EngagementScore, Source: post.Source, ScanJobID: post.ScanJobID, + Replies: viralScanReplies(post.Replies), CreateAt: post.CreateAt, }) } return &types.ListPersonaViralScanPostsData{List: list, Total: len(list)}, nil } + +func viralScanReplies(replies []scanpostusecase.ScanReplySummary) []types.ScanReplyData { + if len(replies) == 0 { + return nil + } + out := make([]types.ScanReplyData, 0, len(replies)) + for _, reply := range replies { + out = append(out, types.ScanReplyData{ + ExternalID: reply.ExternalID, + Author: reply.Author, + Text: reply.Text, + Permalink: reply.Permalink, + LikeCount: reply.LikeCount, + PostedAt: reply.PostedAt, + }) + } + return out +} diff --git a/haixun-backend/internal/logic/persona/mapper.go b/haixun-backend/internal/logic/persona/mapper.go index 14c227b..0f852e7 100644 --- a/haixun-backend/internal/logic/persona/mapper.go +++ b/haixun-backend/internal/logic/persona/mapper.go @@ -50,6 +50,7 @@ func toPersonaPatch(req *types.UpdatePersonaReq) domusecase.PersonaPatch { return domusecase.PersonaPatch{ DisplayName: req.DisplayName, Persona: req.Persona, + Brief: req.Brief, StyleProfile: req.StyleProfile, StyleBenchmark: req.StyleBenchmark, } diff --git a/haixun-backend/internal/logic/persona/publish_persona_copy_draft_logic.go b/haixun-backend/internal/logic/persona/publish_persona_copy_draft_logic.go new file mode 100644 index 0000000..4536e00 --- /dev/null +++ b/haixun-backend/internal/logic/persona/publish_persona_copy_draft_logic.go @@ -0,0 +1,107 @@ +package persona + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + libthreads "haixun-backend/internal/library/threadsapi" + "haixun-backend/internal/library/threadspost" + copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type PublishPersonaCopyDraftLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPublishPersonaCopyDraftLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishPersonaCopyDraftLogic { + return &PublishPersonaCopyDraftLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *PublishPersonaCopyDraftLogic) PublishPersonaCopyDraft( + req *types.PublishCopyDraftHandlerReq, +) (*types.PublishCopyDraftData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.ID) + draftID := strings.TrimSpace(req.DraftID) + if !req.Confirm { + return nil, app.For(code.Persona).InputMissingRequired("請確認 confirm=true 後再發布貼文") + } + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + + draftItem, err := l.svcCtx.CopyDraft.Get(l.ctx, tenantID, uid, personaID, draftID) + if err != nil { + return nil, err + } + draft := draftItem + if draft.Status == "published" { + return nil, app.For(code.Persona).ResInvalidState("此草稿已發布") + } + + text := strings.TrimSpace(req.Text) + if text == "" { + text = strings.TrimSpace(draft.Text) + } + if text == "" { + return nil, app.For(code.Persona).InputMissingRequired("text is required") + } + if err := threadspost.ValidatePublish(text); err != nil { + return nil, app.For(code.Persona).InputInvalidFormat(err.Error()) + } + if strings.TrimSpace(req.Text) != "" && req.Text != draft.Text { + updatedText, err := l.svcCtx.CopyDraft.Update(l.ctx, copydraftdomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + DraftID: draftID, + Patch: copydraftdomain.CopyDraftPatch{Text: &req.Text}, + }) + if err != nil { + return nil, err + } + draft = updatedText + text = strings.TrimSpace(updatedText.Text) + } + + cred, err := l.svcCtx.ThreadsAccount.ResolveMemberThreadsPublishCredential(l.ctx, tenantID, uid) + if err != nil { + return nil, err + } + result, err := libthreads.PublishText(l.ctx, libthreads.PublishTextInput{ + ThreadsUserID: cred.ThreadsUserID, + AccessToken: cred.AccessToken, + Text: text, + }) + if err != nil { + return nil, app.For(code.ThreadsAccount).SvcThirdParty("Threads API 發布貼文失敗:" + err.Error()) + } + + updated, err := l.svcCtx.CopyDraft.MarkPublished(l.ctx, copydraftdomain.MarkPublishedRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + DraftID: draftID, + MediaID: result.MediaID, + Permalink: result.Permalink, + }) + if err != nil { + return nil, err + } + + return &types.PublishCopyDraftData{ + DraftID: draftID, + MediaID: result.MediaID, + Permalink: result.Permalink, + Status: updated.Status, + Message: "仿寫貼文已透過 Threads API 發布", + }, nil +} diff --git a/haixun-backend/internal/logic/persona/start_persona_viral_scan_job_logic.go b/haixun-backend/internal/logic/persona/start_persona_viral_scan_job_logic.go index 5de2e7a..9e285c0 100644 --- a/haixun-backend/internal/logic/persona/start_persona_viral_scan_job_logic.go +++ b/haixun-backend/internal/logic/persona/start_persona_viral_scan_job_logic.go @@ -53,11 +53,8 @@ func (l *StartPersonaViralScanJobLogic) StartPersonaViralScanJob( if err != nil { return nil, err } - if !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler { - return nil, app.For(code.Setting).InputMissingRequired("爆款掃描需要 Threads API 或 Chrome Session(開發模式)") - } - if memberCtx.DevMode && !memberCtx.BrowserConnected { - return nil, app.For(code.Setting).InputMissingRequired("開發模式需先同步 Chrome Session") + if !memberCtx.HasDiscoverPath() { + return nil, app.For(code.Setting).InputMissingRequired("爆款掃描需要 Threads API、Chrome Session 或 Web Search API(請檢查連線模式與搜尋來源)") } payload := map[string]any{ diff --git a/haixun-backend/internal/logic/persona/update_persona_copy_draft_logic.go b/haixun-backend/internal/logic/persona/update_persona_copy_draft_logic.go new file mode 100644 index 0000000..07c78cb --- /dev/null +++ b/haixun-backend/internal/logic/persona/update_persona_copy_draft_logic.go @@ -0,0 +1,77 @@ +package persona + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/threadspost" + copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +type UpdatePersonaCopyDraftLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdatePersonaCopyDraftLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePersonaCopyDraftLogic { + return &UpdatePersonaCopyDraftLogic{ctx: ctx, svcCtx: svcCtx} +} + +func (l *UpdatePersonaCopyDraftLogic) UpdatePersonaCopyDraft( + req *types.UpdateCopyDraftHandlerReq, +) (*types.CopyDraftData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + personaID := strings.TrimSpace(req.ID) + draftID := strings.TrimSpace(req.DraftID) + if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { + return nil, err + } + if req.Text != nil { + if err := threadspost.ValidatePublish(*req.Text); err != nil { + return nil, app.For(code.Persona).InputInvalidFormat(err.Error()) + } + } + + updated, err := l.svcCtx.CopyDraft.Update(l.ctx, copydraftdomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: uid, + PersonaID: personaID, + DraftID: draftID, + Patch: copydraftdomain.CopyDraftPatch{ + Text: req.Text, + Hook: req.Hook, + Angle: req.Angle, + Status: req.Status, + }, + }) + if err != nil { + return nil, err + } + data := types.CopyDraftData{ + ID: updated.ID, + PersonaID: updated.PersonaID, + CopyMissionID: updated.CopyMissionID, + ScanPostID: updated.ScanPostID, + DraftType: updated.DraftType, + SortOrder: updated.SortOrder, + Text: updated.Text, + Angle: updated.Angle, + Hook: updated.Hook, + Rationale: updated.Rationale, + ReferenceNotes: updated.ReferenceNotes, + Sources: updated.Sources, + Status: updated.Status, + PublishedMediaID: updated.PublishedMediaID, + PublishedPermalink: updated.PublishedPermalink, + PublishedAt: updated.PublishedAt, + CreateAt: updated.CreateAt, + } + return &data, nil +} diff --git a/haixun-backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go b/haixun-backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go index 255e9d3..dfff1bd 100644 --- a/haixun-backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go +++ b/haixun-backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go @@ -7,6 +7,7 @@ import ( app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" + "haixun-backend/internal/library/placement" jobdom "haixun-backend/internal/model/job/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" @@ -42,17 +43,14 @@ func (l *ExpandPlacementTopicGraphLogic) ExpandPlacementTopicGraph(req *types.Ex if err != nil { return nil, err } - expandStrategy := libkg.ParseExpandStrategy(research.ExpandStrategy) - if req.Supplemental { + expandStrategy := placement.EffectiveExpandStrategy(research) + if req.Supplemental && placement.WebSearchAvailable(research) { expandStrategy = libkg.ExpandStrategyBrave } memberCtx, err := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) if err != nil { return nil, err } - if expandStrategy.RequiresBrave() && strings.TrimSpace(research.BraveAPIKey) == "" { - return nil, app.For(code.Setting).InputMissingRequired("請到設定頁完成研究資料連線") - } payload := map[string]any{ "topic_id": scope.TopicID, diff --git a/haixun-backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go b/haixun-backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go index a85fb73..61fe85e 100644 --- a/haixun-backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go +++ b/haixun-backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go @@ -33,10 +33,10 @@ func (l *GeneratePlacementTopicOutreachDraftsLogic) GeneratePlacementTopicOutrea BrandPath: types.BrandPath{ID: scope.BrandID}, GenerateOutreachDraftsReq: types.GenerateOutreachDraftsReq{ ScanPostID: req.ScanPostID, + TopicID: scope.TopicID, Count: req.Count, VoicePersonaID: req.VoicePersonaID, ProductID: scope.Topic.ProductID, }, - TopicID: scope.TopicID, }) } diff --git a/haixun-backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go b/haixun-backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go index 42a28fc..2ca43e1 100644 --- a/haixun-backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go +++ b/haixun-backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go @@ -24,10 +24,11 @@ func (l *GetPlacementTopicGraphLogic) GetPlacementTopicGraph(req *types.Placemen if err != nil { return nil, err } - if _, err := resolveScope(l.ctx, l.svcCtx, tenantID, uid, req.ID); err != nil { + scope, err := resolveScope(l.ctx, l.svcCtx, tenantID, uid, req.ID) + if err != nil { return nil, err } - graph, err := l.svcCtx.KnowledgeGraph.GetByTopic(l.ctx, tenantID, uid, req.ID) + graph, err := l.svcCtx.KnowledgeGraph.GetByTopic(l.ctx, tenantID, uid, req.ID, scope.BrandID) if err != nil { return nil, err } diff --git a/haixun-backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go b/haixun-backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go index a36315d..f0688e3 100644 --- a/haixun-backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go +++ b/haixun-backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go @@ -37,7 +37,7 @@ func (l *StartPlacementTopicScanJobLogic) StartPlacementTopicScanJob(req *types. return nil, err } var graph *kgdom.GraphSummary - if loaded, err := l.svcCtx.KnowledgeGraph.GetByTopic(l.ctx, tenantID, uid, scope.TopicID); err != nil { + if loaded, err := l.svcCtx.KnowledgeGraph.GetByTopic(l.ctx, tenantID, uid, scope.TopicID, scope.BrandID); err != nil { if !brandlogic.IsKnowledgeGraphNotFound(err) { return nil, err } @@ -105,8 +105,8 @@ func (l *StartPlacementTopicScanJobLogic) StartPlacementTopicScanJob(req *types. if !memberCtx.AllowsBrave && !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler { return nil, app.For(code.Setting).InputMissingRequired("目前連線模式無法海巡,請確認 Threads API、Brave 或 Chrome Session") } - if placement.MemberNeedsBraveKey(memberCtx) && strings.TrimSpace(research.BraveAPIKey) == "" { - return nil, app.For(code.Setting).InputMissingRequired("請在設定頁設定 Brave Search API key(跟隨此登入帳號)") + if placement.MemberNeedsWebSearchKey(memberCtx) && placement.MissingWebSearchKey(research) { + return nil, app.For(code.Setting).InputMissingRequired(placement.WebSearchKeyRequiredMessage(research)) } if memberCtx.DevMode && !memberCtx.BrowserConnected { return nil, app.For(code.Setting).InputMissingRequired("開發模式需先同步 Chrome Session") diff --git a/haixun-backend/internal/model/copy_draft/domain/entity/draft.go b/haixun-backend/internal/model/copy_draft/domain/entity/draft.go index 76e501d..ac766b3 100644 --- a/haixun-backend/internal/model/copy_draft/domain/entity/draft.go +++ b/haixun-backend/internal/model/copy_draft/domain/entity/draft.go @@ -2,21 +2,30 @@ package entity const CollectionName = "copy_drafts" -const DraftTypeViralReplica = "viral-replica" +const ( + DraftTypeViralReplica = "viral-replica" + DraftTypeReplicate = "replicate" + DraftTypeMatrix = "matrix" +) type CopyDraft struct { - ID string `bson:"_id"` - TenantID string `bson:"tenant_id"` - OwnerUID string `bson:"owner_uid"` - PersonaID string `bson:"persona_id"` - ScanPostID string `bson:"scan_post_id,omitempty"` - DraftType string `bson:"draft_type"` - Text string `bson:"text"` - Angle string `bson:"angle,omitempty"` - Hook string `bson:"hook,omitempty"` - Rationale string `bson:"rationale,omitempty"` - ReferenceNotes string `bson:"reference_notes,omitempty"` - Sources []string `bson:"sources,omitempty"` - Status string `bson:"status,omitempty"` - CreateAt int64 `bson:"create_at"` + ID string `bson:"_id"` + TenantID string `bson:"tenant_id"` + OwnerUID string `bson:"owner_uid"` + PersonaID string `bson:"persona_id"` + CopyMissionID string `bson:"copy_mission_id,omitempty"` + ScanPostID string `bson:"scan_post_id,omitempty"` + DraftType string `bson:"draft_type"` + SortOrder int `bson:"sort_order,omitempty"` + Text string `bson:"text"` + Angle string `bson:"angle,omitempty"` + Hook string `bson:"hook,omitempty"` + Rationale string `bson:"rationale,omitempty"` + ReferenceNotes string `bson:"reference_notes,omitempty"` + Sources []string `bson:"sources,omitempty"` + Status string `bson:"status,omitempty"` + PublishedMediaID string `bson:"published_media_id,omitempty"` + PublishedPermalink string `bson:"published_permalink,omitempty"` + PublishedAt int64 `bson:"published_at,omitempty"` + CreateAt int64 `bson:"create_at"` } diff --git a/haixun-backend/internal/model/copy_draft/domain/repository/repository.go b/haixun-backend/internal/model/copy_draft/domain/repository/repository.go index c5fc6a5..1780551 100644 --- a/haixun-backend/internal/model/copy_draft/domain/repository/repository.go +++ b/haixun-backend/internal/model/copy_draft/domain/repository/repository.go @@ -9,5 +9,11 @@ import ( type Repository interface { EnsureIndexes(ctx context.Context) error Create(ctx context.Context, draft *entity.CopyDraft) error + CreateMany(ctx context.Context, drafts []*entity.CopyDraft) error + Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*entity.CopyDraft, error) + Update(ctx context.Context, tenantID, ownerUID, personaID, draftID string, patch map[string]interface{}) (*entity.CopyDraft, error) + DeleteByMissionAndType(ctx context.Context, tenantID, ownerUID, personaID, missionID, draftType string) error + DeleteByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error List(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.CopyDraft, error) + ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]entity.CopyDraft, error) } diff --git a/haixun-backend/internal/model/copy_draft/domain/usecase/usecase.go b/haixun-backend/internal/model/copy_draft/domain/usecase/usecase.go index ea6c157..0e33618 100644 --- a/haixun-backend/internal/model/copy_draft/domain/usecase/usecase.go +++ b/haixun-backend/internal/model/copy_draft/domain/usecase/usecase.go @@ -5,26 +5,42 @@ import ( ) type CopyDraftSummary struct { - ID string - PersonaID string - ScanPostID string - DraftType string - Text string - Angle string - Hook string - Rationale string - ReferenceNotes string - Sources []string - Status string - CreateAt int64 + ID string + PersonaID string + CopyMissionID string + ScanPostID string + DraftType string + SortOrder int + Text string + Angle string + Hook string + Rationale string + ReferenceNotes string + Sources []string + Status string + PublishedMediaID string + PublishedPermalink string + PublishedAt int64 + CreateAt int64 +} + +type MarkPublishedRequest struct { + TenantID string + OwnerUID string + PersonaID string + DraftID string + MediaID string + Permalink string } type CreateRequest struct { TenantID string OwnerUID string PersonaID string + CopyMissionID string ScanPostID string DraftType string + SortOrder int Text string Angle string Hook string @@ -33,7 +49,36 @@ type CreateRequest struct { Sources []string } +type CreateManyRequest struct { + TenantID string + OwnerUID string + PersonaID string + Drafts []CreateRequest +} + +type CopyDraftPatch struct { + Text *string + Hook *string + Angle *string + Status *string +} + +type UpdateRequest struct { + TenantID string + OwnerUID string + PersonaID string + DraftID string + Patch CopyDraftPatch +} + type UseCase interface { Create(ctx context.Context, req CreateRequest) (*CopyDraftSummary, error) + CreateMany(ctx context.Context, req CreateManyRequest) ([]CopyDraftSummary, error) + ReplaceMissionMatrix(ctx context.Context, tenantID, ownerUID, personaID, missionID string, drafts []CreateRequest) ([]CopyDraftSummary, error) + Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*CopyDraftSummary, error) + Update(ctx context.Context, req UpdateRequest) (*CopyDraftSummary, error) + MarkPublished(ctx context.Context, req MarkPublishedRequest) (*CopyDraftSummary, error) List(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]CopyDraftSummary, error) + ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]CopyDraftSummary, error) + ClearByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error } diff --git a/haixun-backend/internal/model/copy_draft/repository/mongo.go b/haixun-backend/internal/model/copy_draft/repository/mongo.go index 2e2f91e..bbff516 100644 --- a/haixun-backend/internal/model/copy_draft/repository/mongo.go +++ b/haixun-backend/internal/model/copy_draft/repository/mongo.go @@ -38,6 +38,15 @@ func (r *mongoRepository) EnsureIndexes(ctx context.Context) error { {Key: "create_at", Value: -1}, }, }, + { + Keys: bson.D{ + {Key: "tenant_id", Value: 1}, + {Key: "owner_uid", Value: 1}, + {Key: "persona_id", Value: 1}, + {Key: "copy_mission_id", Value: 1}, + {Key: "sort_order", Value: 1}, + }, + }, }) return err } @@ -50,6 +59,27 @@ func personaFilter(tenantID, ownerUID, personaID string) bson.M { } } +func (r *mongoRepository) CreateMany(ctx context.Context, drafts []*entity.CopyDraft) error { + if r.collection == nil { + return app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + if len(drafts) == 0 { + return nil + } + docs := make([]any, 0, len(drafts)) + for _, draft := range drafts { + if draft == nil { + continue + } + docs = append(docs, draft) + } + if len(docs) == 0 { + return nil + } + _, err := r.collection.InsertMany(ctx, docs) + return err +} + func (r *mongoRepository) Create(ctx context.Context, draft *entity.CopyDraft) error { if r.collection == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") @@ -72,7 +102,7 @@ func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID, personaI limit = 200 } opts := options.Find(). - SetSort(bson.D{{Key: "create_at", Value: -1}}). + SetSort(bson.D{{Key: "sort_order", Value: 1}, {Key: "create_at", Value: -1}}). SetLimit(int64(limit)) cur, err := r.collection.Find(ctx, personaFilter(tenantID, ownerUID, personaID), opts) if err != nil { @@ -85,3 +115,107 @@ func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID, personaI } return out, nil } + +func (r *mongoRepository) Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*entity.CopyDraft, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + draftID = strings.TrimSpace(draftID) + if draftID == "" { + return nil, app.For(code.Persona).InputMissingRequired("draft_id is required") + } + filter := personaFilter(tenantID, ownerUID, personaID) + filter["_id"] = draftID + var out entity.CopyDraft + err := r.collection.FindOne(ctx, filter).Decode(&out) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, app.For(code.Persona).ResNotFound("copy draft not found") + } + return nil, err + } + return &out, nil +} + +func (r *mongoRepository) Update(ctx context.Context, tenantID, ownerUID, personaID, draftID string, patch map[string]interface{}) (*entity.CopyDraft, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + if len(patch) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("patch is required") + } + draftID = strings.TrimSpace(draftID) + filter := personaFilter(tenantID, ownerUID, personaID) + filter["_id"] = draftID + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + var out entity.CopyDraft + err := r.collection.FindOneAndUpdate(ctx, filter, bson.M{"$set": patch}, opts).Decode(&out) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, app.For(code.Persona).ResNotFound("copy draft not found") + } + return nil, err + } + return &out, nil +} + +func (r *mongoRepository) DeleteByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { + if r.collection == nil { + return app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + missionID = strings.TrimSpace(missionID) + if missionID == "" { + return app.For(code.Persona).InputMissingRequired("copy_mission_id is required") + } + filter := personaFilter(tenantID, ownerUID, personaID) + filter["copy_mission_id"] = missionID + _, err := r.collection.DeleteMany(ctx, filter) + return err +} + +func (r *mongoRepository) DeleteByMissionAndType(ctx context.Context, tenantID, ownerUID, personaID, missionID, draftType string) error { + if r.collection == nil { + return app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + missionID = strings.TrimSpace(missionID) + draftType = strings.TrimSpace(draftType) + if missionID == "" || draftType == "" { + return app.For(code.Persona).InputMissingRequired("copy_mission_id and draft_type are required") + } + filter := personaFilter(tenantID, ownerUID, personaID) + filter["copy_mission_id"] = missionID + filter["draft_type"] = draftType + _, err := r.collection.DeleteMany(ctx, filter) + return err +} + +func (r *mongoRepository) ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]entity.CopyDraft, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + missionID = strings.TrimSpace(missionID) + if missionID == "" { + return nil, app.For(code.Persona).InputMissingRequired("copy_mission_id is required") + } + if limit <= 0 { + limit = 50 + } + if limit > 200 { + limit = 200 + } + filter := personaFilter(tenantID, ownerUID, personaID) + filter["copy_mission_id"] = missionID + opts := options.Find(). + SetSort(bson.D{{Key: "sort_order", Value: 1}, {Key: "create_at", Value: -1}}). + SetLimit(int64(limit)) + cur, err := r.collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + var out []entity.CopyDraft + if err := cur.All(ctx, &out); err != nil { + return nil, err + } + return out, nil +} diff --git a/haixun-backend/internal/model/copy_draft/usecase/usecase.go b/haixun-backend/internal/model/copy_draft/usecase/usecase.go index 89bcdd5..c98eb6c 100644 --- a/haixun-backend/internal/model/copy_draft/usecase/usecase.go +++ b/haixun-backend/internal/model/copy_draft/usecase/usecase.go @@ -39,8 +39,10 @@ func (u *copyDraftUseCase) Create(ctx context.Context, req domusecase.CreateRequ TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, + CopyMissionID: strings.TrimSpace(req.CopyMissionID), ScanPostID: strings.TrimSpace(req.ScanPostID), DraftType: draftType, + SortOrder: req.SortOrder, Text: text, Angle: strings.TrimSpace(req.Angle), Hook: strings.TrimSpace(req.Hook), @@ -57,6 +59,186 @@ func (u *copyDraftUseCase) Create(ctx context.Context, req domusecase.CreateRequ return &summary, nil } +func (u *copyDraftUseCase) CreateMany(ctx context.Context, req domusecase.CreateManyRequest) ([]domusecase.CopyDraftSummary, error) { + if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { + return nil, err + } + if len(req.Drafts) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("drafts is required") + } + items := make([]*entity.CopyDraft, 0, len(req.Drafts)) + for idx, draftReq := range req.Drafts { + text := strings.TrimSpace(draftReq.Text) + if text == "" { + continue + } + draftType := strings.TrimSpace(draftReq.DraftType) + if draftType == "" { + draftType = entity.DraftTypeMatrix + } + sortOrder := draftReq.SortOrder + if sortOrder == 0 { + sortOrder = idx + 1 + } + items = append(items, &entity.CopyDraft{ + ID: uuid.NewString(), + TenantID: req.TenantID, + OwnerUID: req.OwnerUID, + PersonaID: req.PersonaID, + CopyMissionID: strings.TrimSpace(draftReq.CopyMissionID), + ScanPostID: strings.TrimSpace(draftReq.ScanPostID), + DraftType: draftType, + SortOrder: sortOrder, + Text: text, + Angle: strings.TrimSpace(draftReq.Angle), + Hook: strings.TrimSpace(draftReq.Hook), + Rationale: strings.TrimSpace(draftReq.Rationale), + ReferenceNotes: strings.TrimSpace(draftReq.ReferenceNotes), + Sources: draftReq.Sources, + Status: "pending", + CreateAt: clock.NowUnixNano(), + }) + } + if len(items) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("draft text is required") + } + if err := u.repo.CreateMany(ctx, items); err != nil { + return nil, err + } + out := make([]domusecase.CopyDraftSummary, 0, len(items)) + for _, item := range items { + out = append(out, toSummary(*item)) + } + return out, nil +} + +func (u *copyDraftUseCase) Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*domusecase.CopyDraftSummary, error) { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return nil, err + } + item, err := u.repo.Get(ctx, tenantID, ownerUID, personaID, draftID) + if err != nil { + return nil, err + } + summary := toSummary(*item) + return &summary, nil +} + +func (u *copyDraftUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.CopyDraftSummary, error) { + if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { + return nil, err + } + draftID := strings.TrimSpace(req.DraftID) + if draftID == "" { + return nil, app.For(code.Persona).InputMissingRequired("draft_id is required") + } + patch := map[string]interface{}{} + if req.Patch.Text != nil { + text := strings.TrimSpace(*req.Patch.Text) + if text == "" { + return nil, app.For(code.Persona).InputMissingRequired("draft text cannot be empty") + } + patch["text"] = text + } + if req.Patch.Hook != nil { + patch["hook"] = strings.TrimSpace(*req.Patch.Hook) + } + if req.Patch.Angle != nil { + patch["angle"] = strings.TrimSpace(*req.Patch.Angle) + } + if req.Patch.Status != nil { + status := strings.TrimSpace(*req.Patch.Status) + if status != "" && status != "pending" && status != "ready" { + return nil, app.For(code.Persona).InputMissingRequired("status must be pending or ready") + } + if status != "" { + patch["status"] = status + } + } + if len(patch) == 0 { + return nil, app.For(code.Persona).InputMissingRequired("no fields to update") + } + item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, draftID, patch) + if err != nil { + return nil, err + } + summary := toSummary(*item) + return &summary, nil +} + +func (u *copyDraftUseCase) MarkPublished(ctx context.Context, req domusecase.MarkPublishedRequest) (*domusecase.CopyDraftSummary, error) { + if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { + return nil, err + } + draftID := strings.TrimSpace(req.DraftID) + mediaID := strings.TrimSpace(req.MediaID) + if draftID == "" || mediaID == "" { + return nil, app.For(code.Persona).InputMissingRequired("draft_id and media_id are required") + } + now := clock.NowUnixNano() + patch := map[string]interface{}{ + "status": "published", + "published_media_id": mediaID, + "published_permalink": strings.TrimSpace(req.Permalink), + "published_at": now, + } + item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, draftID, patch) + if err != nil { + return nil, err + } + summary := toSummary(*item) + return &summary, nil +} + +func (u *copyDraftUseCase) ReplaceMissionMatrix( + ctx context.Context, + tenantID, ownerUID, personaID, missionID string, + drafts []domusecase.CreateRequest, +) ([]domusecase.CopyDraftSummary, error) { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return nil, err + } + missionID = strings.TrimSpace(missionID) + if missionID == "" { + return nil, app.For(code.Persona).InputMissingRequired("copy_mission_id is required") + } + if err := u.repo.DeleteByMissionAndType(ctx, tenantID, ownerUID, personaID, missionID, entity.DraftTypeMatrix); err != nil { + return nil, err + } + return u.CreateMany(ctx, domusecase.CreateManyRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + Drafts: drafts, + }) +} + +func (u *copyDraftUseCase) ClearByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return err + } + missionID = strings.TrimSpace(missionID) + if missionID == "" { + return app.For(code.Persona).InputMissingRequired("copy_mission_id is required") + } + return u.repo.DeleteByMission(ctx, tenantID, ownerUID, personaID, missionID) +} + +func (u *copyDraftUseCase) ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]domusecase.CopyDraftSummary, error) { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return nil, err + } + items, err := u.repo.ListByMission(ctx, tenantID, ownerUID, personaID, missionID, limit) + if err != nil { + return nil, err + } + out := make([]domusecase.CopyDraftSummary, 0, len(items)) + for _, item := range items { + out = append(out, toSummary(item)) + } + return out, nil +} + func (u *copyDraftUseCase) List(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.CopyDraftSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err @@ -84,17 +266,22 @@ func requireActor(tenantID, ownerUID, personaID string) error { func toSummary(item entity.CopyDraft) domusecase.CopyDraftSummary { return domusecase.CopyDraftSummary{ - ID: item.ID, - PersonaID: item.PersonaID, - ScanPostID: item.ScanPostID, - DraftType: item.DraftType, - Text: item.Text, - Angle: item.Angle, - Hook: item.Hook, - Rationale: item.Rationale, - ReferenceNotes: item.ReferenceNotes, - Sources: item.Sources, - Status: item.Status, - CreateAt: item.CreateAt, + ID: item.ID, + PersonaID: item.PersonaID, + CopyMissionID: item.CopyMissionID, + ScanPostID: item.ScanPostID, + DraftType: item.DraftType, + SortOrder: item.SortOrder, + Text: item.Text, + Angle: item.Angle, + Hook: item.Hook, + Rationale: item.Rationale, + ReferenceNotes: item.ReferenceNotes, + Sources: item.Sources, + Status: item.Status, + PublishedMediaID: item.PublishedMediaID, + PublishedPermalink: item.PublishedPermalink, + PublishedAt: item.PublishedAt, + CreateAt: item.CreateAt, } } diff --git a/haixun-backend/internal/model/copy_mission/domain/entity/mission.go b/haixun-backend/internal/model/copy_mission/domain/entity/mission.go new file mode 100644 index 0000000..0e70507 --- /dev/null +++ b/haixun-backend/internal/model/copy_mission/domain/entity/mission.go @@ -0,0 +1,69 @@ +package entity + +const CollectionName = "copy_missions" + +type Status string + +const ( + StatusOpen Status = "open" + StatusMapped Status = "mapped" + StatusScanned Status = "scanned" + StatusDrafted Status = "drafted" + StatusArchived Status = "archived" + StatusDeleted Status = "deleted" +) + +type SuggestedTag struct { + Tag string `bson:"tag" json:"tag"` + Reason string `bson:"reason,omitempty" json:"reason,omitempty"` + SearchIntent string `bson:"search_intent,omitempty" json:"search_intent,omitempty"` + SearchType string `bson:"search_type,omitempty" json:"search_type,omitempty"` +} + +type SimilarAccount struct { + Username string `bson:"username" json:"username"` + Reason string `bson:"reason,omitempty" json:"reason,omitempty"` + Source string `bson:"source,omitempty" json:"source,omitempty"` + Confidence string `bson:"confidence,omitempty" json:"confidence,omitempty"` + ProfileURL string `bson:"profile_url,omitempty" json:"profile_url,omitempty"` + AuthorVerified bool `bson:"author_verified,omitempty" json:"author_verified,omitempty"` + FollowerCount int `bson:"follower_count,omitempty" json:"follower_count,omitempty"` + EngagementScore int `bson:"engagement_score,omitempty" json:"engagement_score,omitempty"` + LikeCount int `bson:"like_count,omitempty" json:"like_count,omitempty"` + ReplyCount int `bson:"reply_count,omitempty" json:"reply_count,omitempty"` + PostCount int `bson:"post_count,omitempty" json:"post_count,omitempty"` +} + +type ResearchMap struct { + AudienceSummary string `bson:"audience_summary,omitempty" json:"audience_summary,omitempty"` + ContentGoal string `bson:"content_goal,omitempty" json:"content_goal,omitempty"` + Questions []string `bson:"questions,omitempty" json:"questions,omitempty"` + Pillars []string `bson:"pillars,omitempty" json:"pillars,omitempty"` + Exclusions []string `bson:"exclusions,omitempty" json:"exclusions,omitempty"` + SuggestedTags []SuggestedTag `bson:"suggested_tags,omitempty" json:"suggested_tags,omitempty"` + SimilarAccounts []SimilarAccount `bson:"similar_accounts,omitempty" json:"similar_accounts,omitempty"` + BenchmarkNotes string `bson:"benchmark_notes,omitempty" json:"benchmark_notes,omitempty"` +} + +func (m ResearchMap) IsEmpty() bool { + return m.AudienceSummary == "" && + m.ContentGoal == "" && + len(m.Questions) == 0 && + len(m.SuggestedTags) == 0 +} + +type Mission struct { + ID string `bson:"_id"` + TenantID string `bson:"tenant_id"` + OwnerUID string `bson:"owner_uid"` + PersonaID string `bson:"persona_id"` + Label string `bson:"label,omitempty"` + SeedQuery string `bson:"seed_query,omitempty"` + Brief string `bson:"brief,omitempty"` + ResearchMap ResearchMap `bson:"research_map,omitempty"` + SelectedTags []string `bson:"selected_tags,omitempty"` + LastScanJobID string `bson:"last_scan_job_id,omitempty"` + Status Status `bson:"status"` + CreateAt int64 `bson:"create_at"` + UpdateAt int64 `bson:"update_at"` +} diff --git a/haixun-backend/internal/model/copy_mission/domain/repository/repository.go b/haixun-backend/internal/model/copy_mission/domain/repository/repository.go new file mode 100644 index 0000000..34e8143 --- /dev/null +++ b/haixun-backend/internal/model/copy_mission/domain/repository/repository.go @@ -0,0 +1,16 @@ +package repository + +import ( + "context" + + "haixun-backend/internal/model/copy_mission/domain/entity" +) + +type Repository interface { + EnsureIndexes(ctx context.Context) error + Create(ctx context.Context, mission *entity.Mission) (*entity.Mission, error) + FindByID(ctx context.Context, tenantID, ownerUID, personaID, missionID string) (*entity.Mission, error) + ListByPersona(ctx context.Context, tenantID, ownerUID, personaID string) ([]*entity.Mission, error) + Update(ctx context.Context, tenantID, ownerUID, personaID, missionID string, patch map[string]interface{}) (*entity.Mission, error) + Delete(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error +} diff --git a/haixun-backend/internal/model/copy_mission/domain/usecase/usecase.go b/haixun-backend/internal/model/copy_mission/domain/usecase/usecase.go new file mode 100644 index 0000000..42370b6 --- /dev/null +++ b/haixun-backend/internal/model/copy_mission/domain/usecase/usecase.go @@ -0,0 +1,104 @@ +package usecase + +import ( + "context" + + "haixun-backend/internal/model/copy_mission/domain/entity" +) + +type SuggestedTagSummary struct { + Tag string + Reason string + SearchIntent string + SearchType string +} + +type SimilarAccountSummary struct { + Username string + Reason string + Source string + Confidence string + ProfileURL string + AuthorVerified bool + FollowerCount int + EngagementScore int + LikeCount int + ReplyCount int + PostCount int +} + +type ResearchMapSummary struct { + AudienceSummary string + ContentGoal string + Questions []string + Pillars []string + Exclusions []string + SuggestedTags []SuggestedTagSummary + SimilarAccounts []SimilarAccountSummary + BenchmarkNotes string +} + +type MissionSummary struct { + ID string + PersonaID string + Label string + SeedQuery string + Brief string + ResearchMap ResearchMapSummary + SelectedTags []string + LastScanJobID string + Status string + CreateAt int64 + UpdateAt int64 +} + +type CreateRequest struct { + TenantID string + OwnerUID string + PersonaID string + Label string + SeedQuery string + Brief string + InitialResearchMap *entity.ResearchMap + InitialSelectedTags []string +} + +type MissionPatch struct { + Label *string + SeedQuery *string + Brief *string + AudienceSummary *string + ContentGoal *string + QuestionsSet bool + Questions []string + PillarsSet bool + Pillars []string + ExclusionsSet bool + Exclusions []string + BenchmarkNotes *string + SelectedTagsSet bool + SelectedTags []string + ResearchMap *entity.ResearchMap + LastScanJobID *string + Status *entity.Status +} + +type UpdateRequest struct { + TenantID string + OwnerUID string + PersonaID string + MissionID string + Patch MissionPatch +} + +type ListResult struct { + List []MissionSummary +} + +type UseCase interface { + List(ctx context.Context, tenantID, ownerUID, personaID string) (*ListResult, error) + Create(ctx context.Context, req CreateRequest) (*MissionSummary, error) + Get(ctx context.Context, tenantID, ownerUID, personaID, missionID string) (*MissionSummary, error) + Update(ctx context.Context, req UpdateRequest) (*MissionSummary, error) + Delete(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error +} diff --git a/haixun-backend/internal/model/copy_mission/repository/mongo.go b/haixun-backend/internal/model/copy_mission/repository/mongo.go new file mode 100644 index 0000000..c4260e2 --- /dev/null +++ b/haixun-backend/internal/model/copy_mission/repository/mongo.go @@ -0,0 +1,142 @@ +package repository + +import ( + "context" + "strings" + + "haixun-backend/internal/library/clock" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/model/copy_mission/domain/entity" + domrepo "haixun-backend/internal/model/copy_mission/domain/repository" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type mongoRepository struct { + collection *mongo.Collection +} + +func NewMongoRepository(db *mongo.Database) domrepo.Repository { + if db == nil { + return &mongoRepository{} + } + return &mongoRepository{collection: db.Collection(entity.CollectionName)} +} + +func (r *mongoRepository) EnsureIndexes(ctx context.Context) error { + if r.collection == nil { + return nil + } + _, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ + {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "update_at", Value: -1}}}, + {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "_id", Value: 1}}, Options: options.Index().SetUnique(true)}, + }) + return err +} + +func (r *mongoRepository) Create(ctx context.Context, mission *entity.Mission) (*entity.Mission, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + now := clock.NowUnixNano() + mission.CreateAt = now + mission.UpdateAt = now + if mission.Status == "" { + mission.Status = entity.StatusOpen + } + _, err := r.collection.InsertOne(ctx, mission) + if err != nil { + return nil, err + } + return mission, nil +} + +func (r *mongoRepository) FindByID(ctx context.Context, tenantID, ownerUID, personaID, missionID string) (*entity.Mission, error) { + return r.findOne(ctx, bson.M{ + "_id": strings.TrimSpace(missionID), + "tenant_id": tenantID, + "owner_uid": ownerUID, + "persona_id": strings.TrimSpace(personaID), + "status": bson.M{"$ne": entity.StatusDeleted}, + }) +} + +func (r *mongoRepository) ListByPersona(ctx context.Context, tenantID, ownerUID, personaID string) ([]*entity.Mission, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + cursor, err := r.collection.Find( + ctx, + bson.M{ + "tenant_id": tenantID, + "owner_uid": ownerUID, + "persona_id": strings.TrimSpace(personaID), + "status": bson.M{"$ne": entity.StatusDeleted}, + }, + options.Find().SetSort(bson.D{{Key: "update_at", Value: -1}}), + ) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + var items []*entity.Mission + if err := cursor.All(ctx, &items); err != nil { + return nil, err + } + return items, nil +} + +func (r *mongoRepository) Update(ctx context.Context, tenantID, ownerUID, personaID, missionID string, patch map[string]interface{}) (*entity.Mission, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + if len(patch) == 0 { + return r.FindByID(ctx, tenantID, ownerUID, personaID, missionID) + } + patch["update_at"] = clock.NowUnixNano() + var out entity.Mission + err := r.collection.FindOneAndUpdate( + ctx, + bson.M{ + "_id": strings.TrimSpace(missionID), + "tenant_id": tenantID, + "owner_uid": ownerUID, + "persona_id": strings.TrimSpace(personaID), + "status": bson.M{"$ne": entity.StatusDeleted}, + }, + bson.M{"$set": patch}, + options.FindOneAndUpdate().SetReturnDocument(options.After), + ).Decode(&out) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, app.For(code.Persona).ResNotFound("找不到拷貝任務") + } + return nil, err + } + return &out, nil +} + +func (r *mongoRepository) Delete(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { + _, err := r.Update(ctx, tenantID, ownerUID, personaID, missionID, map[string]interface{}{ + "status": entity.StatusDeleted, + }) + return err +} + +func (r *mongoRepository) findOne(ctx context.Context, filter bson.M) (*entity.Mission, error) { + if r.collection == nil { + return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") + } + var out entity.Mission + err := r.collection.FindOne(ctx, filter).Decode(&out) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, app.For(code.Persona).ResNotFound("找不到拷貝任務") + } + return nil, err + } + return &out, nil +} diff --git a/haixun-backend/internal/model/copy_mission/usecase/usecase.go b/haixun-backend/internal/model/copy_mission/usecase/usecase.go new file mode 100644 index 0000000..77b896d --- /dev/null +++ b/haixun-backend/internal/model/copy_mission/usecase/usecase.go @@ -0,0 +1,261 @@ +package usecase + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/model/copy_mission/domain/entity" + domrepo "haixun-backend/internal/model/copy_mission/domain/repository" + domusecase "haixun-backend/internal/model/copy_mission/domain/usecase" + + "github.com/google/uuid" +) + +type missionUseCase struct { + repo domrepo.Repository +} + +func NewUseCase(repo domrepo.Repository) domusecase.UseCase { + return &missionUseCase{repo: repo} +} + +func (u *missionUseCase) List(ctx context.Context, tenantID, ownerUID, personaID string) (*domusecase.ListResult, error) { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return nil, err + } + items, err := u.repo.ListByPersona(ctx, tenantID, ownerUID, personaID) + if err != nil { + return nil, err + } + list := make([]domusecase.MissionSummary, 0, len(items)) + for _, item := range items { + list = append(list, toSummary(item)) + } + return &domusecase.ListResult{List: list}, nil +} + +func (u *missionUseCase) Create(ctx context.Context, req domusecase.CreateRequest) (*domusecase.MissionSummary, error) { + if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { + return nil, err + } + label := strings.TrimSpace(req.Label) + seedQuery := strings.TrimSpace(req.SeedQuery) + brief := strings.TrimSpace(req.Brief) + if label == "" { + return nil, app.For(code.Persona).InputMissingRequired("label is required") + } + if seedQuery == "" { + return nil, app.For(code.Persona).InputMissingRequired("seed_query is required") + } + if brief == "" { + return nil, app.For(code.Persona).InputMissingRequired("brief is required") + } + mission := &entity.Mission{ + ID: uuid.NewString(), + TenantID: req.TenantID, + OwnerUID: req.OwnerUID, + PersonaID: strings.TrimSpace(req.PersonaID), + Label: label, + SeedQuery: seedQuery, + Brief: brief, + Status: entity.StatusOpen, + } + if req.InitialResearchMap != nil { + mission.ResearchMap = *req.InitialResearchMap + } + if len(req.InitialSelectedTags) > 0 { + mission.SelectedTags = cleanTags(req.InitialSelectedTags) + } + item, err := u.repo.Create(ctx, mission) + if err != nil { + return nil, err + } + summary := toSummary(item) + return &summary, nil +} + +func (u *missionUseCase) Get(ctx context.Context, tenantID, ownerUID, personaID, missionID string) (*domusecase.MissionSummary, error) { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return nil, err + } + item, err := u.repo.FindByID(ctx, tenantID, ownerUID, personaID, missionID) + if err != nil { + return nil, err + } + summary := toSummary(item) + return &summary, nil +} + +func (u *missionUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.MissionSummary, error) { + if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { + return nil, err + } + patch := map[string]interface{}{} + if req.Patch.Label != nil { + label := strings.TrimSpace(*req.Patch.Label) + if label == "" { + return nil, app.For(code.Persona).InputMissingRequired("label cannot be empty") + } + patch["label"] = label + } + if req.Patch.SeedQuery != nil { + seed := strings.TrimSpace(*req.Patch.SeedQuery) + if seed == "" { + return nil, app.For(code.Persona).InputMissingRequired("seed_query cannot be empty") + } + patch["seed_query"] = seed + } + if req.Patch.Brief != nil { + brief := strings.TrimSpace(*req.Patch.Brief) + if brief == "" { + return nil, app.For(code.Persona).InputMissingRequired("brief cannot be empty") + } + patch["brief"] = brief + } + if req.Patch.AudienceSummary != nil { + patch["research_map.audience_summary"] = strings.TrimSpace(*req.Patch.AudienceSummary) + } + if req.Patch.ContentGoal != nil { + patch["research_map.content_goal"] = strings.TrimSpace(*req.Patch.ContentGoal) + } + if req.Patch.QuestionsSet { + patch["research_map.questions"] = cleanStringList(req.Patch.Questions) + } + if req.Patch.PillarsSet { + patch["research_map.pillars"] = cleanStringList(req.Patch.Pillars) + } + if req.Patch.ExclusionsSet { + patch["research_map.exclusions"] = cleanStringList(req.Patch.Exclusions) + } + if req.Patch.BenchmarkNotes != nil { + patch["research_map.benchmark_notes"] = strings.TrimSpace(*req.Patch.BenchmarkNotes) + } + if req.Patch.SelectedTagsSet { + patch["selected_tags"] = cleanTags(req.Patch.SelectedTags) + } + if req.Patch.ResearchMap != nil { + patch["research_map"] = *req.Patch.ResearchMap + } + if req.Patch.LastScanJobID != nil { + patch["last_scan_job_id"] = strings.TrimSpace(*req.Patch.LastScanJobID) + } + if req.Patch.Status != nil { + patch["status"] = *req.Patch.Status + } + item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, req.MissionID, patch) + if err != nil { + return nil, err + } + summary := toSummary(item) + return &summary, nil +} + +func (u *missionUseCase) Delete(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { + if err := requireActor(tenantID, ownerUID, personaID); err != nil { + return err + } + return u.repo.Delete(ctx, tenantID, ownerUID, personaID, missionID) +} + +func requireActor(tenantID, ownerUID, personaID string) error { + if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { + return app.For(code.Auth).AuthUnauthorized("missing actor") + } + if strings.TrimSpace(personaID) == "" { + return app.For(code.Persona).InputMissingRequired("persona_id is required") + } + return nil +} + +func toSummary(item *entity.Mission) domusecase.MissionSummary { + if item == nil { + return domusecase.MissionSummary{} + } + tags := make([]domusecase.SuggestedTagSummary, 0, len(item.ResearchMap.SuggestedTags)) + for _, tag := range item.ResearchMap.SuggestedTags { + tags = append(tags, domusecase.SuggestedTagSummary{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + accounts := make([]domusecase.SimilarAccountSummary, 0, len(item.ResearchMap.SimilarAccounts)) + for _, acc := range item.ResearchMap.SimilarAccounts { + accounts = append(accounts, domusecase.SimilarAccountSummary{ + Username: acc.Username, + Reason: acc.Reason, + Source: acc.Source, + Confidence: acc.Confidence, + ProfileURL: acc.ProfileURL, + AuthorVerified: acc.AuthorVerified, + FollowerCount: acc.FollowerCount, + EngagementScore: acc.EngagementScore, + LikeCount: acc.LikeCount, + ReplyCount: acc.ReplyCount, + PostCount: acc.PostCount, + }) + } + return domusecase.MissionSummary{ + ID: item.ID, + PersonaID: item.PersonaID, + Label: item.Label, + SeedQuery: item.SeedQuery, + Brief: item.Brief, + ResearchMap: toMapSummary(item.ResearchMap, tags, accounts), + SelectedTags: append([]string(nil), item.SelectedTags...), + LastScanJobID: item.LastScanJobID, + Status: string(item.Status), + CreateAt: item.CreateAt, + UpdateAt: item.UpdateAt, + } +} + +func toMapSummary(m entity.ResearchMap, tags []domusecase.SuggestedTagSummary, accounts []domusecase.SimilarAccountSummary) domusecase.ResearchMapSummary { + return domusecase.ResearchMapSummary{ + AudienceSummary: m.AudienceSummary, + ContentGoal: m.ContentGoal, + Questions: append([]string(nil), m.Questions...), + Pillars: append([]string(nil), m.Pillars...), + Exclusions: append([]string(nil), m.Exclusions...), + SuggestedTags: tags, + SimilarAccounts: accounts, + BenchmarkNotes: m.BenchmarkNotes, + } +} + +func cleanStringList(items []string) []string { + out := make([]string, 0, len(items)) + seen := make(map[string]struct{}, len(items)) + for _, item := range items { + trimmed := strings.TrimSpace(item) + if trimmed == "" { + continue + } + if _, ok := seen[trimmed]; ok { + continue + } + seen[trimmed] = struct{}{} + out = append(out, trimmed) + } + return out +} + +func cleanTags(tags []string) []string { + out := []string{} + seen := map[string]struct{}{} + for _, tag := range tags { + tag = strings.TrimSpace(tag) + if tag == "" { + continue + } + if _, ok := seen[tag]; ok { + continue + } + seen[tag] = struct{}{} + out = append(out, tag) + } + return out +} diff --git a/haixun-backend/internal/model/job/domain/usecase/job.go b/haixun-backend/internal/model/job/domain/usecase/job.go index 16b4650..4c3ebca 100644 --- a/haixun-backend/internal/model/job/domain/usecase/job.go +++ b/haixun-backend/internal/model/job/domain/usecase/job.go @@ -95,6 +95,9 @@ type UseCase interface { EnsureExpandGraphTemplate(ctx context.Context) error EnsurePlacementScanTemplate(ctx context.Context) error EnsureScanViralTemplate(ctx context.Context) error + EnsureAnalyzeCopyMissionTemplate(ctx context.Context) error + EnsureGenerateCopyMatrixTemplate(ctx context.Context) error + EnsureGenerateCopyDraftTemplate(ctx context.Context) error CreateRun(ctx context.Context, req CreateRunRequest) (*entity.Run, error) GetRun(ctx context.Context, jobID string) (*entity.Run, error) diff --git a/haixun-backend/internal/model/job/usecase/usecase.go b/haixun-backend/internal/model/job/usecase/usecase.go index 525c53c..8aafe34 100644 --- a/haixun-backend/internal/model/job/usecase/usecase.go +++ b/haixun-backend/internal/model/job/usecase/usecase.go @@ -14,12 +14,15 @@ import ( ) const ( - demoTemplateType = "demo_long_task" - style8DTemplateType = "style-8d" - expandGraphTemplateType = "expand-graph" - placementScanTemplateType = "placement-scan" - scanViralTemplateType = "scan-viral" - style8DWorkerType = "node" + demoTemplateType = "demo_long_task" + style8DTemplateType = "style-8d" + expandGraphTemplateType = "expand-graph" + placementScanTemplateType = "placement-scan" + scanViralTemplateType = "scan-viral" + analyzeCopyMissionTemplateType = "analyze-copy-mission" + generateCopyMatrixTemplateType = "generate-copy-matrix" + generateCopyDraftTemplateType = "generate-copy-draft" + style8DWorkerType = "node" ) type UseCase = domusecase.UseCase @@ -81,6 +84,99 @@ func (u *jobUseCase) EnsureScanViralTemplate(ctx context.Context) error { return err } +func (u *jobUseCase) EnsureAnalyzeCopyMissionTemplate(ctx context.Context) error { + _, err := u.templates.Upsert(ctx, analyzeCopyMissionTemplate()) + return err +} + +func (u *jobUseCase) EnsureGenerateCopyMatrixTemplate(ctx context.Context) error { + _, err := u.templates.Upsert(ctx, generateCopyMatrixTemplate()) + return err +} + +func (u *jobUseCase) EnsureGenerateCopyDraftTemplate(ctx context.Context) error { + _, err := u.templates.Upsert(ctx, generateCopyDraftTemplate()) + return err +} + +func analyzeCopyMissionTemplate() *entity.Template { + return &entity.Template{ + Type: analyzeCopyMissionTemplateType, + Version: 1, + Name: "Analyze Copy Mission", + Description: "LLM research map and default search tags for a copy ninja mission", + Enabled: true, + Repeatable: true, + ConcurrencyPolicy: string(enum.ConcurrencyRejectSameScope), + DedupeKeys: []string{"scope_id"}, + TimeoutSeconds: 300, + CancelPolicy: entity.CancelPolicy{ + Supported: true, + Mode: "cooperative", + GraceSeconds: 30, + }, + RetryPolicy: entity.RetryPolicy{ + MaxAttempts: 1, + BackoffSeconds: []int{}, + }, + Steps: []entity.TemplateStep{ + {ID: "copy_mission_map", Name: "Copy mission research map", WorkerType: string(enum.WorkerTypeGo), TimeoutSeconds: 300, Cancelable: true}, + }, + } +} + +func generateCopyMatrixTemplate() *entity.Template { + return &entity.Template{ + Type: generateCopyMatrixTemplateType, + Version: 1, + Name: "Generate Copy Matrix", + Description: "LLM content matrix drafts for a copy ninja mission", + Enabled: true, + Repeatable: true, + ConcurrencyPolicy: string(enum.ConcurrencyRejectSameScope), + DedupeKeys: []string{"scope_id"}, + TimeoutSeconds: 600, + CancelPolicy: entity.CancelPolicy{ + Supported: true, + Mode: "cooperative", + GraceSeconds: 30, + }, + RetryPolicy: entity.RetryPolicy{ + MaxAttempts: 1, + BackoffSeconds: []int{}, + }, + Steps: []entity.TemplateStep{ + {ID: "copy_matrix_generate", Name: "Copy matrix generate", WorkerType: string(enum.WorkerTypeGo), TimeoutSeconds: 600, Cancelable: true}, + }, + } +} + +func generateCopyDraftTemplate() *entity.Template { + return &entity.Template{ + Type: generateCopyDraftTemplateType, + Version: 1, + Name: "Generate Copy Draft", + Description: "Deep replicate draft from a viral scan post", + Enabled: true, + Repeatable: true, + ConcurrencyPolicy: string(enum.ConcurrencyAllowParallel), + DedupeKeys: []string{"scan_post_id"}, + TimeoutSeconds: 600, + CancelPolicy: entity.CancelPolicy{ + Supported: true, + Mode: "cooperative", + GraceSeconds: 30, + }, + RetryPolicy: entity.RetryPolicy{ + MaxAttempts: 1, + BackoffSeconds: []int{}, + }, + Steps: []entity.TemplateStep{ + {ID: "copy_draft_generate", Name: "Copy draft generate", WorkerType: string(enum.WorkerTypeGo), TimeoutSeconds: 600, Cancelable: true}, + }, + } +} + func scanViralTemplate() *entity.Template { return &entity.Template{ Type: scanViralTemplateType, diff --git a/haixun-backend/internal/model/knowledge_graph/domain/usecase/usecase.go b/haixun-backend/internal/model/knowledge_graph/domain/usecase/usecase.go index 203e501..eb90f9b 100644 --- a/haixun-backend/internal/model/knowledge_graph/domain/usecase/usecase.go +++ b/haixun-backend/internal/model/knowledge_graph/domain/usecase/usecase.go @@ -54,7 +54,8 @@ type UpdateNodesRequest struct { type UseCase interface { Get(ctx context.Context, tenantID, ownerUID, brandID string) (*GraphSummary, error) - GetByTopic(ctx context.Context, tenantID, ownerUID, topicID string) (*GraphSummary, error) + // brandID enables legacy fallback when graph was stored under brand_id without topic_id. + GetByTopic(ctx context.Context, tenantID, ownerUID, topicID, brandID string) (*GraphSummary, error) Upsert(ctx context.Context, req UpsertRequest) (*GraphSummary, error) UpdateNodes(ctx context.Context, req UpdateNodesRequest) (*GraphSummary, error) AttachTopicID(ctx context.Context, tenantID, ownerUID, brandID, topicID string) error diff --git a/haixun-backend/internal/model/knowledge_graph/usecase/usecase.go b/haixun-backend/internal/model/knowledge_graph/usecase/usecase.go index f814231..06bba43 100644 --- a/haixun-backend/internal/model/knowledge_graph/usecase/usecase.go +++ b/haixun-backend/internal/model/knowledge_graph/usecase/usecase.go @@ -33,7 +33,7 @@ func (u *knowledgeGraphUseCase) Get(ctx context.Context, tenantID, ownerUID, bra return &summary, nil } -func (u *knowledgeGraphUseCase) GetByTopic(ctx context.Context, tenantID, ownerUID, topicID string) (*domusecase.GraphSummary, error) { +func (u *knowledgeGraphUseCase) GetByTopic(ctx context.Context, tenantID, ownerUID, topicID, brandID string) (*domusecase.GraphSummary, error) { if err := requireScope(tenantID, ownerUID, "", topicID); err != nil { return nil, err } @@ -41,6 +41,27 @@ func (u *knowledgeGraphUseCase) GetByTopic(ctx context.Context, tenantID, ownerU if err != nil { return nil, err } + if item == nil { + brandID = strings.TrimSpace(brandID) + if brandID != "" { + legacy, legacyErr := u.repo.FindByBrand(ctx, tenantID, ownerUID, brandID) + if legacyErr != nil { + return nil, legacyErr + } + if legacy != nil { + legacyTopicID := strings.TrimSpace(legacy.TopicID) + if legacyTopicID == "" || legacyTopicID == topicID { + if legacyTopicID == "" { + if err := u.repo.AttachTopicID(ctx, tenantID, ownerUID, brandID, topicID); err != nil { + return nil, err + } + legacy.TopicID = topicID + } + item = legacy + } + } + } + } summary := toSummary(item) return &summary, nil } diff --git a/haixun-backend/internal/model/placement/usecase/settings.go b/haixun-backend/internal/model/placement/usecase/settings.go index da7f850..ad730d7 100644 --- a/haixun-backend/internal/model/placement/usecase/settings.go +++ b/haixun-backend/internal/model/placement/usecase/settings.go @@ -8,6 +8,7 @@ import ( "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/placement" + "haixun-backend/internal/library/websearch" settingdomain "haixun-backend/internal/model/setting/domain/usecase" ) @@ -17,18 +18,25 @@ const ( ) type Settings struct { + WebSearchProvider string BraveAPIKey string BraveAPIKeyConfigured bool + ExaAPIKey string + ExaAPIKeyConfigured bool BraveCountry string BraveSearchLang string + ExaUserLocation string ExpandStrategy string } type SettingsPatch struct { - BraveAPIKey *string - BraveCountry *string - BraveSearchLang *string - ExpandStrategy *string + WebSearchProvider *string + BraveAPIKey *string + ExaAPIKey *string + BraveCountry *string + BraveSearchLang *string + ExaUserLocation *string + ExpandStrategy *string } type UseCase interface { @@ -80,10 +88,13 @@ func (u *placementUseCase) ResearchSettings(ctx context.Context, tenantID, owner return placement.ResearchSettings{}, err } return placement.ResearchSettings{ - BraveAPIKey: stored.BraveAPIKey, - BraveCountry: stored.BraveCountry, - BraveSearchLang: stored.BraveSearchLang, - ExpandStrategy: stored.ExpandStrategy, + WebSearchProvider: stored.WebSearchProvider, + BraveAPIKey: stored.BraveAPIKey, + ExaAPIKey: stored.ExaAPIKey, + BraveCountry: stored.BraveCountry, + BraveSearchLang: stored.BraveSearchLang, + ExaUserLocation: stored.ExaUserLocation, + ExpandStrategy: stored.ExpandStrategy, }, nil } @@ -110,17 +121,22 @@ func (u *placementUseCase) save(ctx context.Context, ownerUID string, value stor } type storedSettings struct { - BraveAPIKey string - BraveCountry string - BraveSearchLang string - ExpandStrategy string + WebSearchProvider string + BraveAPIKey string + ExaAPIKey string + BraveCountry string + BraveSearchLang string + ExaUserLocation string + ExpandStrategy string } func defaultSettings() storedSettings { return storedSettings{ - BraveCountry: "tw", - BraveSearchLang: "zh-hant", - ExpandStrategy: string(libkg.ExpandStrategyHybrid), + WebSearchProvider: string(websearch.ProviderBrave), + BraveCountry: "tw", + BraveSearchLang: "zh-hant", + ExaUserLocation: "TW", + ExpandStrategy: string(libkg.ExpandStrategyHybrid), } } @@ -128,15 +144,24 @@ func mergeSettings(defaults storedSettings, raw map[string]interface{}) storedSe if raw == nil { return defaults } + if v, ok := raw["web_search_provider"].(string); ok && strings.TrimSpace(v) != "" { + defaults.WebSearchProvider = string(websearch.ParseProvider(v)) + } if v, ok := raw["brave_api_key"].(string); ok { defaults.BraveAPIKey = strings.TrimSpace(v) } + if v, ok := raw["exa_api_key"].(string); ok { + defaults.ExaAPIKey = strings.TrimSpace(v) + } if v, ok := raw["brave_country"].(string); ok && strings.TrimSpace(v) != "" { defaults.BraveCountry = strings.TrimSpace(v) } if v, ok := raw["brave_search_lang"].(string); ok && strings.TrimSpace(v) != "" { defaults.BraveSearchLang = strings.TrimSpace(v) } + if v, ok := raw["exa_user_location"].(string); ok && strings.TrimSpace(v) != "" { + defaults.ExaUserLocation = strings.TrimSpace(v) + } if v, ok := raw["expand_strategy"].(string); ok && strings.TrimSpace(v) != "" { defaults.ExpandStrategy = string(libkg.ParseExpandStrategy(v)) } @@ -144,18 +169,30 @@ func mergeSettings(defaults storedSettings, raw map[string]interface{}) storedSe } func applyPatch(current storedSettings, patch SettingsPatch) storedSettings { + if patch.WebSearchProvider != nil && strings.TrimSpace(*patch.WebSearchProvider) != "" { + current.WebSearchProvider = string(websearch.ParseProvider(*patch.WebSearchProvider)) + } if patch.BraveAPIKey != nil { value := strings.TrimSpace(*patch.BraveAPIKey) if value != "" && !isMaskedAPIKey(value) { current.BraveAPIKey = value } } + if patch.ExaAPIKey != nil { + value := strings.TrimSpace(*patch.ExaAPIKey) + if value != "" && !isMaskedAPIKey(value) { + current.ExaAPIKey = value + } + } if patch.BraveCountry != nil && strings.TrimSpace(*patch.BraveCountry) != "" { current.BraveCountry = strings.TrimSpace(*patch.BraveCountry) } if patch.BraveSearchLang != nil && strings.TrimSpace(*patch.BraveSearchLang) != "" { current.BraveSearchLang = strings.TrimSpace(*patch.BraveSearchLang) } + if patch.ExaUserLocation != nil && strings.TrimSpace(*patch.ExaUserLocation) != "" { + current.ExaUserLocation = strings.TrimSpace(*patch.ExaUserLocation) + } if patch.ExpandStrategy != nil && strings.TrimSpace(*patch.ExpandStrategy) != "" { current.ExpandStrategy = string(libkg.ParseExpandStrategy(*patch.ExpandStrategy)) } @@ -164,24 +201,35 @@ func applyPatch(current storedSettings, patch SettingsPatch) storedSettings { func (s storedSettings) toMap() map[string]interface{} { return map[string]interface{}{ - "brave_api_key": s.BraveAPIKey, - "brave_country": s.BraveCountry, - "brave_search_lang": s.BraveSearchLang, - "expand_strategy": s.ExpandStrategy, + "web_search_provider": s.WebSearchProvider, + "brave_api_key": s.BraveAPIKey, + "exa_api_key": s.ExaAPIKey, + "brave_country": s.BraveCountry, + "brave_search_lang": s.BraveSearchLang, + "exa_user_location": s.ExaUserLocation, + "expand_strategy": s.ExpandStrategy, } } func toPublic(stored storedSettings) *Settings { - masked := "" + braveMasked := "" if stored.BraveAPIKey != "" { - masked = maskAPIKey(stored.BraveAPIKey) + braveMasked = maskAPIKey(stored.BraveAPIKey) + } + exaMasked := "" + if stored.ExaAPIKey != "" { + exaMasked = maskAPIKey(stored.ExaAPIKey) } strategy := string(libkg.ParseExpandStrategy(stored.ExpandStrategy)) return &Settings{ - BraveAPIKey: masked, + WebSearchProvider: string(websearch.ParseProvider(stored.WebSearchProvider)), + BraveAPIKey: braveMasked, BraveAPIKeyConfigured: strings.TrimSpace(stored.BraveAPIKey) != "", + ExaAPIKey: exaMasked, + ExaAPIKeyConfigured: strings.TrimSpace(stored.ExaAPIKey) != "", BraveCountry: stored.BraveCountry, BraveSearchLang: stored.BraveSearchLang, + ExaUserLocation: stored.ExaUserLocation, ExpandStrategy: strategy, } } diff --git a/haixun-backend/internal/model/placement_topic/usecase/usecase.go b/haixun-backend/internal/model/placement_topic/usecase/usecase.go index b8f747b..b471ea4 100644 --- a/haixun-backend/internal/model/placement_topic/usecase/usecase.go +++ b/haixun-backend/internal/model/placement_topic/usecase/usecase.go @@ -102,15 +102,23 @@ func (u *topicUseCase) Create(ctx context.Context, req domusecase.CreateRequest) } func (u *topicUseCase) Get(ctx context.Context, tenantID, ownerUID, topicID string) (*domusecase.TopicSummary, error) { + if err := u.migrateLegacyBrands(ctx, tenantID, ownerUID); err != nil { + return nil, err + } item, err := u.assertOwned(ctx, tenantID, ownerUID, topicID) if err != nil { return nil, err } displayName := "" - if brand, err := u.brandRepo.FindByID(ctx, tenantID, ownerUID, item.BrandID); err == nil && brand != nil { + var brand *brandentity.Brand + if loaded, err := u.brandRepo.FindByID(ctx, tenantID, ownerUID, item.BrandID); err == nil && loaded != nil { + brand = loaded displayName = brand.DisplayName } summary := toSummary(item, displayName) + if summary.ResearchMap.IsEmpty() && brand != nil && !brand.ResearchMap.IsEmpty() { + summary.ResearchMap = brand.ResearchMap + } return &summary, nil } diff --git a/haixun-backend/internal/model/scan_post/domain/entity/post.go b/haixun-backend/internal/model/scan_post/domain/entity/post.go index b4189af..dbbd81f 100644 --- a/haixun-backend/internal/model/scan_post/domain/entity/post.go +++ b/haixun-backend/internal/model/scan_post/domain/entity/post.go @@ -21,6 +21,7 @@ type ScanPost struct { BrandID string `bson:"brand_id"` TopicID string `bson:"topic_id,omitempty"` LegacyPersonaID string `bson:"persona_id,omitempty"` + CopyMissionID string `bson:"copy_mission_id,omitempty"` Flow string `bson:"flow,omitempty"` GraphID string `bson:"graph_id"` ScanJobID string `bson:"scan_job_id"` @@ -30,6 +31,8 @@ type ScanPost struct { ExternalID string `bson:"external_id"` Permalink string `bson:"permalink"` Author string `bson:"author"` + AuthorVerified bool `bson:"author_verified,omitempty"` + FollowerCount int `bson:"follower_count,omitempty"` Text string `bson:"text"` Priority string `bson:"priority"` LikeCount int `bson:"like_count,omitempty"` diff --git a/haixun-backend/internal/model/scan_post/domain/repository/repository.go b/haixun-backend/internal/model/scan_post/domain/repository/repository.go index a789c32..5940b62 100644 --- a/haixun-backend/internal/model/scan_post/domain/repository/repository.go +++ b/haixun-backend/internal/model/scan_post/domain/repository/repository.go @@ -16,9 +16,10 @@ type ListFilter struct { } type PersonaListFilter struct { - PersonaID string - Flow string - Limit int + PersonaID string + CopyMissionID string + Flow string + Limit int } type Repository interface { @@ -27,7 +28,7 @@ type Repository interface { UpsertBatchForScan(ctx context.Context, tenantID, ownerUID, brandID, topicID string, posts []entity.ScanPost) (int, error) PruneScanJobPosts(ctx context.Context, tenantID, ownerUID, brandID, topicID, scanJobID string, keepPermalinks []string) error ReplaceForScan(ctx context.Context, tenantID, ownerUID, brandID, scanJobID string, posts []entity.ScanPost) error - ReplaceForViralScan(ctx context.Context, tenantID, ownerUID, personaID, scanJobID string, posts []entity.ScanPost) error + ReplaceForViralScan(ctx context.Context, tenantID, ownerUID, personaID, copyMissionID, scanJobID string, posts []entity.ScanPost) error Get(ctx context.Context, tenantID, ownerUID, brandID, postID string) (*entity.ScanPost, error) GetForPersona(ctx context.Context, tenantID, ownerUID, personaID, postID string) (*entity.ScanPost, error) UpdateOutreach(ctx context.Context, tenantID, ownerUID, brandID, postID string, patch entity.OutreachPatch) (*entity.ScanPost, error) diff --git a/haixun-backend/internal/model/scan_post/domain/usecase/usecase.go b/haixun-backend/internal/model/scan_post/domain/usecase/usecase.go index 1e60959..57186b8 100644 --- a/haixun-backend/internal/model/scan_post/domain/usecase/usecase.go +++ b/haixun-backend/internal/model/scan_post/domain/usecase/usecase.go @@ -19,6 +19,7 @@ type ScanPostSummary struct { ID string BrandID string PersonaID string + CopyMissionID string Flow string GraphNodeID string SearchTag string @@ -26,6 +27,8 @@ type ScanPostSummary struct { ExternalID string Permalink string Author string + AuthorVerified bool + FollowerCount int Text string Priority string LikeCount int @@ -67,18 +70,20 @@ type ReplaceRequest struct { } type ViralReplaceRequest struct { - TenantID string - OwnerUID string - PersonaID string - ScanJobID string - Posts []placement.ScanCandidate + TenantID string + OwnerUID string + PersonaID string + CopyMissionID string + ScanJobID string + Posts []placement.ScanCandidate } type PersonaListRequest struct { - TenantID string - OwnerUID string - PersonaID string - Limit int + TenantID string + OwnerUID string + PersonaID string + CopyMissionID string + Limit int } type UpdateOutreachRequest struct { @@ -107,6 +112,7 @@ type UseCase interface { FinalizeScan(ctx context.Context, req ReplaceRequest) (int, error) ReplaceFromScan(ctx context.Context, req ReplaceRequest) (int, error) ReplaceFromViralScan(ctx context.Context, req ViralReplaceRequest) (int, error) + ClearCopyMissionViralScan(ctx context.Context, tenantID, ownerUID, personaID, copyMissionID string) error Get(ctx context.Context, tenantID, ownerUID, brandID, postID string) (*ScanPostSummary, error) GetForPersona(ctx context.Context, tenantID, ownerUID, personaID, postID string) (*ScanPostSummary, error) UpdateOutreach(ctx context.Context, req UpdateOutreachRequest) (*ScanPostSummary, error) diff --git a/haixun-backend/internal/model/scan_post/repository/mongo.go b/haixun-backend/internal/model/scan_post/repository/mongo.go index f16682a..0531d31 100644 --- a/haixun-backend/internal/model/scan_post/repository/mongo.go +++ b/haixun-backend/internal/model/scan_post/repository/mongo.go @@ -73,11 +73,25 @@ func personaViralFilter(tenantID, ownerUID, personaID string) bson.M { } } -func (r *mongoRepository) ReplaceForViralScan(ctx context.Context, tenantID, ownerUID, personaID, scanJobID string, posts []entity.ScanPost) error { +func personaViralScopeFilter(tenantID, ownerUID, personaID, copyMissionID string) bson.M { + filter := personaViralFilter(tenantID, ownerUID, personaID) + copyMissionID = strings.TrimSpace(copyMissionID) + if copyMissionID != "" { + filter["copy_mission_id"] = copyMissionID + } else { + filter["$or"] = []bson.M{ + {"copy_mission_id": bson.M{"$exists": false}}, + {"copy_mission_id": ""}, + } + } + return filter +} + +func (r *mongoRepository) ReplaceForViralScan(ctx context.Context, tenantID, ownerUID, personaID, copyMissionID, scanJobID string, posts []entity.ScanPost) error { if r.collection == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } - _, err := r.collection.DeleteMany(ctx, personaViralFilter(tenantID, ownerUID, personaID)) + _, err := r.collection.DeleteMany(ctx, personaViralScopeFilter(tenantID, ownerUID, personaID, copyMissionID)) if err != nil { return err } @@ -366,7 +380,7 @@ func (r *mongoRepository) ListForPersona(ctx context.Context, tenantID, ownerUID if r.collection == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } - query := personaViralFilter(tenantID, ownerUID, filter.PersonaID) + query := personaViralScopeFilter(tenantID, ownerUID, filter.PersonaID, filter.CopyMissionID) if flow := strings.TrimSpace(filter.Flow); flow != "" { query["flow"] = flow } @@ -425,8 +439,10 @@ func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID string, f return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } query := topicScopeFilter(tenantID, ownerUID, filter.TopicID, filter.BrandID) - if strings.TrimSpace(filter.Priority) != "" { - query["priority"] = strings.TrimSpace(filter.Priority) + if priorityQuery := priorityListFilter(filter.Priority); priorityQuery != nil { + for key, value := range priorityQuery { + query[key] = value + } } if filter.ProductFitMin > 0 { query["product_fit_score"] = bson.M{"$gte": filter.ProductFitMin} @@ -462,6 +478,21 @@ func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID string, f return out, nil } +// priorityListFilter maps UI track filters to stored priority values. +// gold posts hit both relevance and recency tracks, so they belong in either filter. +func priorityListFilter(priority string) bson.M { + switch strings.TrimSpace(priority) { + case "relevant": + return bson.M{"priority": bson.M{"$in": []string{"relevant", "gold"}}} + case "recent": + return bson.M{"priority": bson.M{"$in": []string{"recent", "gold"}}} + case "gold": + return bson.M{"priority": "gold"} + default: + return nil + } +} + func (r *mongoRepository) CountByBrand(ctx context.Context, tenantID, ownerUID, brandID string) (int, error) { if r.collection == nil { return 0, app.For(code.Brand).DBUnavailable("Mongo is not configured") diff --git a/haixun-backend/internal/model/scan_post/repository/mongo_priority_filter_test.go b/haixun-backend/internal/model/scan_post/repository/mongo_priority_filter_test.go new file mode 100644 index 0000000..f9aca08 --- /dev/null +++ b/haixun-backend/internal/model/scan_post/repository/mongo_priority_filter_test.go @@ -0,0 +1,41 @@ +package repository + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestPriorityListFilterIncludesGoldInTrackFilters(t *testing.T) { + relevant := priorityListFilter("relevant") + inValues, ok := relevant["priority"].(bson.M)["$in"].([]string) + if !ok { + t.Fatalf("expected $in filter, got %v", relevant) + } + if len(inValues) != 2 || inValues[0] != "relevant" || inValues[1] != "gold" { + t.Fatalf("unexpected relevant filter: %v", inValues) + } + + recent := priorityListFilter("recent") + inValues, ok = recent["priority"].(bson.M)["$in"].([]string) + if !ok { + t.Fatalf("expected $in filter, got %v", recent) + } + if len(inValues) != 2 || inValues[0] != "recent" || inValues[1] != "gold" { + t.Fatalf("unexpected recent filter: %v", inValues) + } + + gold := priorityListFilter("gold") + if gold["priority"] != "gold" { + t.Fatalf("expected exact gold filter, got %v", gold) + } +} + +func TestPriorityListFilterEmpty(t *testing.T) { + if priorityListFilter("") != nil { + t.Fatal("empty priority should not add filter") + } + if priorityListFilter("unknown") != nil { + t.Fatal("unknown priority should not add filter") + } +} diff --git a/haixun-backend/internal/model/scan_post/repository/mongo_topic_scope_test.go b/haixun-backend/internal/model/scan_post/repository/mongo_topic_scope_test.go index 08612df..19d6c04 100644 --- a/haixun-backend/internal/model/scan_post/repository/mongo_topic_scope_test.go +++ b/haixun-backend/internal/model/scan_post/repository/mongo_topic_scope_test.go @@ -25,4 +25,4 @@ func TestUntaggedTopicFilter_OnlyMissingTopicID(t *testing.T) { if !ok || len(orClause) != 2 { t.Fatalf("expected two untagged topic clauses, got %v", legacy) } -} \ No newline at end of file +} diff --git a/haixun-backend/internal/model/scan_post/usecase/usecase.go b/haixun-backend/internal/model/scan_post/usecase/usecase.go index e7c5931..0669f88 100644 --- a/haixun-backend/internal/model/scan_post/usecase/usecase.go +++ b/haixun-backend/internal/model/scan_post/usecase/usecase.go @@ -24,6 +24,17 @@ func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &scanPostUseCase{repo: repo} } +func (u *scanPostUseCase) ClearCopyMissionViralScan(ctx context.Context, tenantID, ownerUID, personaID, copyMissionID string) error { + if err := requireViralActor(tenantID, ownerUID, personaID); err != nil { + return err + } + copyMissionID = strings.TrimSpace(copyMissionID) + if copyMissionID == "" { + return app.For(code.Persona).InputMissingRequired("copy_mission_id is required") + } + return u.repo.ReplaceForViralScan(ctx, tenantID, ownerUID, personaID, copyMissionID, "", nil) +} + func (u *scanPostUseCase) ReplaceFromViralScan(ctx context.Context, req domusecase.ViralReplaceRequest) (int, error) { if err := requireViralActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return 0, err @@ -36,12 +47,15 @@ func (u *scanPostUseCase) ReplaceFromViralScan(ctx context.Context, req domuseca TenantID: req.TenantID, OwnerUID: req.OwnerUID, LegacyPersonaID: req.PersonaID, + CopyMissionID: strings.TrimSpace(req.CopyMissionID), Flow: entity.FlowViral, ScanJobID: req.ScanJobID, SearchTag: item.SearchTag, ExternalID: item.ExternalID, Permalink: item.Permalink, Author: item.Author, + AuthorVerified: item.AuthorVerified, + FollowerCount: item.FollowerCount, Text: item.Text, Priority: item.Priority, LikeCount: item.LikeCount, @@ -53,7 +67,7 @@ func (u *scanPostUseCase) ReplaceFromViralScan(ctx context.Context, req domuseca CreateAt: now, }) } - if err := u.repo.ReplaceForViralScan(ctx, req.TenantID, req.OwnerUID, req.PersonaID, req.ScanJobID, entities); err != nil { + if err := u.repo.ReplaceForViralScan(ctx, req.TenantID, req.OwnerUID, req.PersonaID, req.CopyMissionID, req.ScanJobID, entities); err != nil { return 0, err } return len(entities), nil @@ -238,9 +252,10 @@ func (u *scanPostUseCase) ListForPersona(ctx context.Context, req domusecase.Per return nil, err } items, err := u.repo.ListForPersona(ctx, req.TenantID, req.OwnerUID, domrepo.PersonaListFilter{ - PersonaID: req.PersonaID, - Flow: entity.FlowViral, - Limit: req.Limit, + PersonaID: req.PersonaID, + CopyMissionID: req.CopyMissionID, + Flow: entity.FlowViral, + Limit: req.Limit, }) if err != nil { return nil, err @@ -349,6 +364,7 @@ func toSummary(item entity.ScanPost) domusecase.ScanPostSummary { ID: item.ID, BrandID: libmongo.ResolveBrandID(item.BrandID, item.LegacyPersonaID), PersonaID: strings.TrimSpace(item.LegacyPersonaID), + CopyMissionID: item.CopyMissionID, Flow: item.Flow, GraphNodeID: item.GraphNodeID, SearchTag: item.SearchTag, @@ -356,6 +372,8 @@ func toSummary(item entity.ScanPost) domusecase.ScanPostSummary { ExternalID: item.ExternalID, Permalink: item.Permalink, Author: item.Author, + AuthorVerified: item.AuthorVerified, + FollowerCount: item.FollowerCount, Text: item.Text, Priority: item.Priority, LikeCount: item.LikeCount, diff --git a/haixun-backend/internal/svc/service_context.go b/haixun-backend/internal/svc/service_context.go index 36af5df..734f094 100644 --- a/haixun-backend/internal/svc/service_context.go +++ b/haixun-backend/internal/svc/service_context.go @@ -25,6 +25,9 @@ import ( copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" copydraftrepo "haixun-backend/internal/model/copy_draft/repository" copydraftusecase "haixun-backend/internal/model/copy_draft/usecase" + copymissiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" + copymissionrepo "haixun-backend/internal/model/copy_mission/repository" + copymissionusecase "haixun-backend/internal/model/copy_mission/usecase" jobrepo "haixun-backend/internal/model/job/repository" jobusecase "haixun-backend/internal/model/job/usecase" kgdomain "haixun-backend/internal/model/knowledge_graph/domain/usecase" @@ -73,6 +76,7 @@ type ServiceContext struct { Member memberdomain.UseCase Permission permissiondomain.UseCase Persona personadomain.UseCase + CopyMission copymissiondomain.UseCase Brand branddomain.UseCase PlacementTopic placementtopicdomain.UseCase KnowledgeGraph kgdomain.UseCase @@ -166,6 +170,21 @@ func NewServiceContext(c config.Config) *ServiceContext { if err := jobUseCase.EnsureScanViralTemplate(ctx); err != nil { panic(err) } + if err := jobUseCase.EnsureAnalyzeCopyMissionTemplate(ctx); err != nil { + panic(err) + } + if err := jobUseCase.EnsureGenerateCopyMatrixTemplate(ctx); err != nil { + panic(err) + } + if err := jobUseCase.EnsureGenerateCopyDraftTemplate(ctx); err != nil { + panic(err) + } + + copyMissionRepository := copymissionrepo.NewMongoRepository(mongoClient.Database()) + if err := copyMissionRepository.EnsureIndexes(ctx); err != nil { + panic(err) + } + copyMissionUseCase := copymissionusecase.NewUseCase(copyMissionRepository) copyDraftRepository := copydraftrepo.NewMongoRepository(mongoClient.Database()) if err := copyDraftRepository.EnsureIndexes(ctx); err != nil { @@ -248,6 +267,7 @@ func NewServiceContext(c config.Config) *ServiceContext { Member: memberUseCase, Permission: permissionUseCase, Persona: personaUseCase, + CopyMission: copyMissionUseCase, Brand: brandUseCase, PlacementTopic: placementTopicUseCase, KnowledgeGraph: knowledgeGraphUseCase, @@ -292,12 +312,40 @@ func NewServiceContext(c config.Config) *ServiceContext { }) jobworker.RegisterScanViralHandler(runner, jobworker.ScanViralDeps{ Jobs: jobUseCase, + CopyMission: copyMissionUseCase, Persona: personaUseCase, ScanPost: scanPostUseCase, + CopyDraft: copyDraftUseCase, ThreadsAccount: threadsAccountUseCase, Placement: placementUseCase, AI: sc.AI, }) + jobworker.RegisterAnalyzeCopyMissionHandler(runner, jobworker.AnalyzeCopyMissionDeps{ + Jobs: jobUseCase, + CopyMission: copyMissionUseCase, + Persona: personaUseCase, + ThreadsAccount: threadsAccountUseCase, + Placement: placementUseCase, + AI: sc.AI, + }) + jobworker.RegisterGenerateCopyMatrixHandler(runner, jobworker.GenerateCopyMatrixDeps{ + Jobs: jobUseCase, + CopyMission: copyMissionUseCase, + Persona: personaUseCase, + ScanPost: scanPostUseCase, + CopyDraft: copyDraftUseCase, + ThreadsAccount: threadsAccountUseCase, + AI: sc.AI, + }) + jobworker.RegisterGenerateCopyDraftHandler(runner, jobworker.GenerateCopyDraftDeps{ + Jobs: jobUseCase, + CopyMission: copyMissionUseCase, + Persona: personaUseCase, + ScanPost: scanPostUseCase, + CopyDraft: copyDraftUseCase, + ThreadsAccount: threadsAccountUseCase, + AI: sc.AI, + }) go runner.Start(workerCtx) } diff --git a/haixun-backend/internal/types/types.go b/haixun-backend/internal/types/types.go index 9bfb408..1369cd0 100644 --- a/haixun-backend/internal/types/types.go +++ b/haixun-backend/internal/types/types.go @@ -192,18 +192,87 @@ type ContentMatrixRowData struct { } type CopyDraftData struct { - ID string `json:"id"` - PersonaID string `json:"persona_id"` - ScanPostID string `json:"scan_post_id,omitempty"` - DraftType string `json:"draft_type"` - Text string `json:"text"` - Angle string `json:"angle,omitempty"` - Hook string `json:"hook,omitempty"` - Rationale string `json:"rationale,omitempty"` - ReferenceNotes string `json:"reference_notes,omitempty"` - Sources []string `json:"sources,omitempty"` - Status string `json:"status,omitempty"` - CreateAt int64 `json:"create_at"` + ID string `json:"id"` + PersonaID string `json:"persona_id"` + CopyMissionID string `json:"copy_mission_id,omitempty"` + ScanPostID string `json:"scan_post_id,omitempty"` + DraftType string `json:"draft_type"` + SortOrder int `json:"sort_order,omitempty"` + Text string `json:"text"` + Angle string `json:"angle,omitempty"` + Hook string `json:"hook,omitempty"` + Rationale string `json:"rationale,omitempty"` + ReferenceNotes string `json:"reference_notes,omitempty"` + Sources []string `json:"sources,omitempty"` + Status string `json:"status,omitempty"` + PublishedMediaID string `json:"published_media_id,omitempty"` + PublishedPermalink string `json:"published_permalink,omitempty"` + PublishedAt int64 `json:"published_at,omitempty"` + CreateAt int64 `json:"create_at"` +} + +type CopyDraftPath struct { + ID string `path:"id" validate:"required"` + DraftID string `path:"draftId" validate:"required"` +} + +type CopyMissionData struct { + ID string `json:"id"` + PersonaID string `json:"persona_id"` + Label string `json:"label,omitempty"` + SeedQuery string `json:"seed_query,omitempty"` + Brief string `json:"brief,omitempty"` + ResearchMap CopyMissionResearchMapData `json:"research_map,omitempty"` + SelectedTags []string `json:"selected_tags,omitempty"` + LastScanJobID string `json:"last_scan_job_id,omitempty"` + Status string `json:"status,omitempty"` + CreateAt int64 `json:"create_at"` + 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"` + Brief string `json:"brief"` + TrendReason string `json:"trend_reason,omitempty"` + TrendKeywords []string `json:"trend_keywords,omitempty"` + Sources []CopyMissionInspirationSourceData `json:"sources,omitempty"` + WebSearchUsed bool `json:"web_search_used"` + Message string `json:"message"` +} + +type CopyMissionResearchMapData struct { + AudienceSummary string `json:"audience_summary,omitempty"` + ContentGoal string `json:"content_goal,omitempty"` + Questions []string `json:"questions,omitempty"` + Pillars []string `json:"pillars,omitempty"` + Exclusions []string `json:"exclusions,omitempty"` + SuggestedTags []CopySuggestedTagData `json:"suggested_tags,omitempty"` + SimilarAccounts []CopySimilarAccountData `json:"similar_accounts,omitempty"` + BenchmarkNotes string `json:"benchmark_notes,omitempty"` +} + +type CopyMissionScanScheduleData struct { + ID string `json:"id,omitempty"` + PersonaID string `json:"persona_id"` + MissionID string `json:"mission_id"` + Cron string `json:"cron"` + Timezone string `json:"timezone"` + Enabled bool `json:"enabled"` + NextRunAt int64 `json:"next_run_at,omitempty"` + LastRunAt int64 `json:"last_run_at,omitempty"` } type CopyResearchMapData struct { @@ -216,6 +285,27 @@ type CopyResearchMapData struct { BenchmarkNotes string `json:"benchmark_notes,omitempty"` } +type CopySimilarAccountData struct { + 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"` +} + +type CopySuggestedTagData struct { + Tag string `json:"tag"` + Reason string `json:"reason,omitempty"` + SearchIntent string `json:"search_intent,omitempty"` + SearchType string `json:"search_type,omitempty"` +} + type CreateBrandProductHandlerReq struct { BrandPath CreateBrandProductReq @@ -231,6 +321,17 @@ type CreateBrandReq struct { DisplayName string `json:"display_name,optional"` } +type CreateCopyMissionHandlerReq struct { + PersonaCopyMissionsPath + CreateCopyMissionReq +} + +type CreateCopyMissionReq struct { + Label string `json:"label" validate:"required"` + SeedQuery string `json:"seed_query" validate:"required"` + Brief string `json:"brief" validate:"required"` +} + type CreateJobReq struct { TemplateType string `json:"template_type" validate:"required"` // job template type Scope string `json:"scope" validate:"required,oneof=user account system persona brand"` // job scope @@ -313,6 +414,20 @@ type GenerateContentMatrixReq struct { Count int `json:"count,optional"` } +type GenerateCopyMissionMatrixData struct { + Drafts []CopyDraftData `json:"drafts"` + Message string `json:"message"` +} + +type GenerateCopyMissionMatrixHandlerReq struct { + CopyMissionPath + GenerateCopyMissionMatrixReq +} + +type GenerateCopyMissionMatrixReq struct { + Count int `json:"count,optional"` +} + type GenerateOutreachDraftsData struct { ID string `json:"id"` ScanPostID string `json:"scan_post_id"` @@ -325,11 +440,11 @@ type GenerateOutreachDraftsData struct { type GenerateOutreachDraftsHandlerReq struct { BrandPath GenerateOutreachDraftsReq - TopicID string `json:"-"` } type GenerateOutreachDraftsReq struct { ScanPostID string `json:"scan_post_id" validate:"required"` + TopicID string `json:"topic_id,optional"` Count int `json:"count,optional"` VoicePersonaID string `json:"voice_persona_id,optional"` ProductID string `json:"product_id,optional"` @@ -587,6 +702,24 @@ type ListBrandsData struct { List []BrandData `json:"list"` } +type ListCopyMissionCopyDraftsData struct { + List []CopyDraftData `json:"list"` + Total int `json:"total"` +} + +type ListCopyMissionScanPostsHandlerReq struct { + CopyMissionPath + ListCopyMissionScanPostsReq +} + +type ListCopyMissionScanPostsReq struct { + Limit int `form:"limit,optional"` +} + +type ListCopyMissionsData struct { + List []CopyMissionData `json:"list"` +} + type ListJobEventsReq struct { ID string `path:"id" validate:"required"` // job run id Limit int64 `form:"limit,optional"` // max events @@ -680,10 +813,14 @@ type MemberMeData struct { } type MemberPlacementSettingsData struct { + WebSearchProvider string `json:"web_search_provider"` // brave | exa BraveAPIKey string `json:"brave_api_key,omitempty"` BraveAPIKeyConfigured bool `json:"brave_api_key_configured"` + ExaAPIKey string `json:"exa_api_key,omitempty"` + ExaAPIKeyConfigured bool `json:"exa_api_key_configured"` BraveCountry string `json:"brave_country"` BraveSearchLang string `json:"brave_search_lang"` + ExaUserLocation string `json:"exa_user_location"` ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid } @@ -752,6 +889,10 @@ type PermissionNode struct { Children []PermissionNode `json:"children,omitempty"` } +type PersonaCopyMissionsPath struct { + PersonaID string `path:"personaId" validate:"required"` +} + type PersonaData struct { ID string `json:"id"` DisplayName string `json:"display_name,omitempty"` @@ -786,6 +927,24 @@ type PlacementTopicPath struct { ID string `path:"id" validate:"required"` } +type PublishCopyDraftData struct { + DraftID string `json:"draft_id"` + MediaID string `json:"media_id"` + Permalink string `json:"permalink,omitempty"` + Status string `json:"status"` + Message string `json:"message"` +} + +type PublishCopyDraftHandlerReq struct { + CopyDraftPath + PublishCopyDraftReq +} + +type PublishCopyDraftReq struct { + Text string `json:"text,optional"` + Confirm bool `json:"confirm"` +} + type PublishOutreachDraftData struct { ScanPostID string `json:"scan_post_id"` ReplyID string `json:"reply_id"` @@ -919,6 +1078,48 @@ type StartBrandScanJobReq struct { PatrolKeywords []string `json:"patrol_keywords,optional"` } +type StartCopyMissionAnalyzeJobData struct { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` +} + +type StartCopyMissionScanJobData struct { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` +} + +type StartCopyMissionMatrixJobReq struct { + Count int `json:"count,optional"` +} + +type StartCopyMissionMatrixJobHandlerReq struct { + CopyMissionPath + StartCopyMissionMatrixJobReq +} + +type StartCopyMissionMatrixJobData struct { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` +} + +type StartCopyMissionCopyDraftJobReq struct { + ScanPostID string `json:"scan_post_id" validate:"required"` +} + +type StartCopyMissionCopyDraftJobHandlerReq struct { + CopyMissionPath + StartCopyMissionCopyDraftJobReq +} + +type StartCopyMissionCopyDraftJobData struct { + JobID string `json:"job_id"` + Status string `json:"status"` + Message string `json:"message"` +} + type StartPersonaStyleAnalysisData struct { JobID string `json:"job_id"` Status string `json:"status"` @@ -1055,6 +1256,37 @@ type UpdateBrandReq struct { PatrolKeywords []string `json:"patrol_keywords,optional"` } +type UpdateCopyDraftHandlerReq struct { + CopyDraftPath + UpdateCopyDraftReq +} + +type UpdateCopyDraftReq struct { + Text *string `json:"text,optional"` + Hook *string `json:"hook,optional"` + Angle *string `json:"angle,optional"` + Status *string `json:"status,optional"` +} + +type UpdateCopyMissionHandlerReq struct { + CopyMissionPath + UpdateCopyMissionReq +} + +type UpdateCopyMissionReq struct { + Label *string `json:"label,optional"` + SeedQuery *string `json:"seed_query,optional"` + Brief *string `json:"brief,optional"` + AudienceSummary *string `json:"audience_summary,optional"` + ContentGoal *string `json:"content_goal,optional"` + Questions []string `json:"questions,optional"` + Pillars []string `json:"pillars,optional"` + Exclusions []string `json:"exclusions,optional"` + BenchmarkNotes *string `json:"benchmark_notes,optional"` + SelectedTags []string `json:"selected_tags,optional"` + Status *string `json:"status,optional"` +} + type UpdateJobScheduleReq struct { ID string `path:"id" validate:"required"` // schedule id Cron string `json:"cron,optional"` // cron expression @@ -1072,10 +1304,13 @@ type UpdateMemberMeReq struct { } type UpdateMemberPlacementSettingsReq struct { - BraveAPIKey *string `json:"brave_api_key,optional"` - BraveCountry *string `json:"brave_country,optional"` - BraveSearchLang *string `json:"brave_search_lang,optional"` - ExpandStrategy *string `json:"expand_strategy,optional"` + WebSearchProvider *string `json:"web_search_provider,optional"` + BraveAPIKey *string `json:"brave_api_key,optional"` + ExaAPIKey *string `json:"exa_api_key,optional"` + BraveCountry *string `json:"brave_country,optional"` + BraveSearchLang *string `json:"brave_search_lang,optional"` + ExaUserLocation *string `json:"exa_user_location,optional"` + ExpandStrategy *string `json:"expand_strategy,optional"` } type UpdatePersonaHandlerReq struct { @@ -1086,6 +1321,7 @@ type UpdatePersonaHandlerReq struct { type UpdatePersonaReq struct { DisplayName *string `json:"display_name,optional"` Persona *string `json:"persona,optional"` + Brief *string `json:"brief,optional"` StyleProfile *string `json:"style_profile,optional"` StyleBenchmark *string `json:"style_benchmark,optional"` } @@ -1159,6 +1395,17 @@ type UpsertBrandScanScheduleReq struct { Enabled bool `json:"enabled"` } +type UpsertCopyMissionScanScheduleHandlerReq struct { + CopyMissionPath + UpsertCopyMissionScanScheduleReq +} + +type UpsertCopyMissionScanScheduleReq struct { + Cron string `json:"cron,optional"` + Timezone string `json:"timezone,optional"` + Enabled bool `json:"enabled"` +} + type UpsertJobTemplateReq struct { Type string `path:"type" validate:"required"` // template type Version int `json:"version,optional"` // template version @@ -1180,17 +1427,20 @@ type UpsertPlacementTopicScanScheduleHandlerReq struct { } type ViralScanPostData struct { - ID string `json:"id"` - SearchTag string `json:"search_tag"` - Permalink string `json:"permalink"` - Author string `json:"author"` - Text string `json:"text"` - LikeCount int `json:"like_count"` - ReplyCount int `json:"reply_count"` - EngagementScore int `json:"engagement_score"` - Source string `json:"source"` - ScanJobID string `json:"scan_job_id"` - CreateAt int64 `json:"create_at"` + ID string `json:"id"` + 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"` + EngagementScore int `json:"engagement_score"` + Source string `json:"source"` + ScanJobID string `json:"scan_job_id"` + Replies []ScanReplyData `json:"replies,omitempty"` + CreateAt int64 `json:"create_at"` } type WorkerCancelCheckData struct { diff --git a/haixun-backend/internal/worker/job/analyze_copy_mission.go b/haixun-backend/internal/worker/job/analyze_copy_mission.go new file mode 100644 index 0000000..018f2fc --- /dev/null +++ b/haixun-backend/internal/worker/job/analyze_copy_mission.go @@ -0,0 +1,186 @@ +package job + +import ( + "context" + "fmt" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + 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" + missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" + jobdom "haixun-backend/internal/model/job/domain/usecase" + personadomain "haixun-backend/internal/model/persona/domain/usecase" + placementusecase "haixun-backend/internal/model/placement/usecase" + threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" +) + +type AnalyzeCopyMissionDeps struct { + Jobs jobdom.UseCase + CopyMission missiondomain.UseCase + Persona personadomain.UseCase + ThreadsAccount threadsaccountdomain.UseCase + Placement placementusecase.UseCase + AI aiusecase.UseCase +} + +func RegisterAnalyzeCopyMissionHandler(runner *Runner, deps AnalyzeCopyMissionDeps) { + if runner == nil { + return + } + runner.RegisterStepHandler("copy_mission_map", func(ctx context.Context, step StepContext) error { + return runAnalyzeCopyMission(ctx, step, deps) + }) +} + +func runAnalyzeCopyMission(ctx context.Context, step StepContext, deps AnalyzeCopyMissionDeps) error { + payload := step.Run.Payload + tenantID, ownerUID := runActorFromPayload(payload, step.Run) + personaID := stringField(payload, "persona_id") + missionID := stringField(payload, "copy_mission_id") + if tenantID == "" || ownerUID == "" || personaID == "" || missionID == "" { + return fmt.Errorf("analyze-copy-mission payload missing tenant_id, owner_uid, persona_id, or copy_mission_id") + } + + mission, err := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID) + if err != nil { + return err + } + persona, err := deps.Persona.Get(ctx, tenantID, ownerUID, personaID) + if err != nil { + return err + } + + updateProgress := func(summary string, percentage int) { + _ = step.Heartbeat(ctx) + _, _ = deps.Jobs.UpdateProgress(ctx, jobdom.UpdateProgressRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Phase: "copy_mission_map", + Summary: summary, + Percentage: percentage, + }) + } + + updateProgress("產生拷貝任務研究地圖…", 15) + + credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID) + if err != nil { + return err + } + providerID, err := aiusecase.MapWorkerProvider(credential.Provider) + if err != nil { + return err + } + + result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: libviral.BuildMissionResearchMapSystemPrompt(), + Messages: []domai.Message{ + { + Role: "user", + Content: libviral.BuildMissionResearchMapUserPrompt(libviral.CopyResearchMapInput{ + Label: mission.Label, + SeedQuery: mission.SeedQuery, + Brief: mission.Brief, + Persona: persona.Persona, + StyleBenchmark: persona.StyleBenchmark, + PersonaAudienceSummary: persona.CopyResearchMap.AudienceSummary, + PersonaContentGoal: persona.CopyResearchMap.ContentGoal, + PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...), + PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...), + }), + }, + }, + }) + if err != nil { + return err + } + + parsed, err := libviral.ParseMissionResearchMapOutput(result.Text) + if err != nil { + return app.For(code.AI).SvcThirdParty("拷貝任務研究地圖 LLM 回傳無法解析:" + err.Error()) + } + + entityTags := make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags)) + for _, tag := range parsed.SuggestedTags { + entityTags = append(entityTags, missionentity.SuggestedTag{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + researchMap := missionentity.ResearchMap{ + AudienceSummary: parsed.AudienceSummary, + ContentGoal: parsed.ContentGoal, + Questions: parsed.Questions, + Pillars: parsed.Pillars, + Exclusions: parsed.Exclusions, + BenchmarkNotes: parsed.BenchmarkNotes, + } + entityTags = make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags)) + for _, tag := range parsed.SuggestedTags { + entityTags = append(entityTags, missionentity.SuggestedTag{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + researchMap.SimilarAccounts = nil + researchMap.SuggestedTags = entityTags + selected := libviral.PickDefaultSelectedTags(parsed.SuggestedTags) + + mapped := missionentity.StatusMapped + updateProgress("儲存研究地圖與預設標籤…", 85) + _, err = deps.CopyMission.Update(ctx, missiondomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + MissionID: missionID, + Patch: missiondomain.MissionPatch{ + ResearchMap: &researchMap, + SelectedTagsSet: true, + SelectedTags: selected, + Status: &mapped, + }, + }) + if err != nil { + return err + } + + handoff := map[string]any{ + "flow": "copy", + "persona_id": personaID, + "copy_mission_id": missionID, + "summary": fmt.Sprintf("研究地圖就緒,已預選 %d 個搜尋標籤", len(selected)), + "next_route": fmt.Sprintf("/matrix/missions/%s", missionID), + } + + _, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Result: map[string]any{ + "tag_count": len(entityTags), + "account_count": 0, + "selected_count": len(selected), + "handoff": handoff, + }, + }) + return err +} + +func copyMissionIDFromPayload(payload map[string]any) string { + if id := stringField(payload, "copy_mission_id"); id != "" { + return id + } + return strings.TrimSpace(stringField(payload, "mission_id")) +} diff --git a/haixun-backend/internal/worker/job/expand_graph.go b/haixun-backend/internal/worker/job/expand_graph.go index e7c8308..4ea3d4b 100644 --- a/haixun-backend/internal/worker/job/expand_graph.go +++ b/haixun-backend/internal/worker/job/expand_graph.go @@ -6,13 +6,13 @@ import ( "strings" "sync" - libbrave "haixun-backend/internal/library/brave" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/placement" libprompt "haixun-backend/internal/library/prompt" + "haixun-backend/internal/library/websearch" "haixun-backend/internal/model/ai/domain/enum" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" @@ -89,7 +89,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) if err != nil { return err } - braveClient := libbrave.NewClient(memberCtx.BraveAPIKey) + webClient := websearch.New(memberCtx.WebSearchConfig()) credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID) if err != nil { @@ -118,7 +118,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) prefetchedBrave := []libkg.BraveSource{} var prefetchQueries []string - if needResearchMap && expandStrategy.RequiresBrave() { + if needResearchMap && expandStrategy.RequiresWebSearch() { updateProgress("平行產生研究地圖與蒐集參考資料…", 5) var mapErr error var wg sync.WaitGroup @@ -136,7 +136,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) if len(prefetchQueries) == 0 { return } - sources, err := runBraveKnowledgeExpand(ctx, braveClient, memberCtx, prefetchQueries, expandStrategy, func(i, total int) { + sources, err := runWebKnowledgeExpand(ctx, webClient, memberCtx, prefetchQueries, expandStrategy, func(i, total int) { pct := 8 + ((i + 1) * 12 / max(total, 1)) updateProgress(fmt.Sprintf("預先蒐集參考資料 %d/%d…", i+1, total), pct) }, func() error { @@ -157,7 +157,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) if mapErr != nil { return mapErr } - brand, err = deps.Brand.Get(ctx, tenantID, ownerUID, brandID) + brand, err = reloadScopeBrand(ctx, deps, tenantID, ownerUID, scope) if err != nil { return err } @@ -166,7 +166,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) if err := ensureResearchMap(ctx, step, deps, brand, productBrief, providerID, credential, updateProgress); err != nil { return err } - brand, err = deps.Brand.Get(ctx, tenantID, ownerUID, brandID) + brand, err = reloadScopeBrand(ctx, deps, tenantID, ownerUID, scope) if err != nil { return err } @@ -219,7 +219,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) var moreBrave []libkg.BraveSource if len(queries) > 0 { - moreBrave, err = runBraveKnowledgeExpand(ctx, braveClient, memberCtx, queries, expandStrategy, func(i, total int) { + moreBrave, err = runWebKnowledgeExpand(ctx, webClient, memberCtx, queries, expandStrategy, func(i, total int) { pct := 25 + ((i + 1) * 30 / max(total, 1)) updateProgress(fmt.Sprintf("蒐集參考資料 %d/%d…", len(prefetchQueries)+i+1, len(prefetchQueries)+total), pct) }, func() error { @@ -309,7 +309,7 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) planIn := kgPlanInput(brand, seed, productBrief, libkg.L1LabelsFromNodes(graph.Nodes), true, expandStrategy) if expandStrategy.UsesSupplementalBrave() { suppQueries := libkg.PlanQueries(planIn) - extraSources, err := runBraveKnowledgeExpand(ctx, braveClient, memberCtx, suppQueries, expandStrategy, nil, func() error { + extraSources, err := runWebKnowledgeExpand(ctx, webClient, memberCtx, suppQueries, expandStrategy, nil, func() error { cancelled, _ := deps.Jobs.IsCancelRequested(ctx, step.JobID) if cancelled { return errJobCancelled @@ -430,9 +430,9 @@ func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) return err } -func runBraveKnowledgeExpand( +func runWebKnowledgeExpand( ctx context.Context, - client *libbrave.Client, + client websearch.Client, member placement.MemberContext, queries []string, strategy libkg.ExpandStrategy, @@ -440,18 +440,23 @@ func runBraveKnowledgeExpand( heartbeat func() error, ) ([]libkg.BraveSource, error) { if client == nil || !client.Enabled() { - return nil, app.For(code.Setting).InputMissingRequired("請在設定頁完成研究資料連線") + return nil, app.For(code.Setting).InputMissingRequired(placement.WebSearchKeyRequiredMessage(placement.ResearchSettings{ + WebSearchProvider: member.WebSearchProvider, + BraveAPIKey: member.BraveAPIKey, + ExaAPIKey: member.ExaAPIKey, + })) } if len(queries) == 0 { if strategy == libkg.ExpandStrategyHybrid { return nil, nil } - return nil, app.For(code.Setting).InputMissingRequired("沒有可執行的 Brave 查詢") + return nil, app.For(code.Setting).InputMissingRequired("沒有可執行的網路搜尋查詢") } cfg := libkg.DefaultBraveCollectConfig() - out := libkg.CollectBraveSources(ctx, client, libkg.BraveSearchLocale{ - Country: member.BraveCountry, - SearchLang: member.BraveSearchLang, + out := libkg.CollectWebSources(ctx, client, libkg.BraveSearchLocale{ + Country: member.BraveCountry, + SearchLang: member.BraveSearchLang, + UserLocation: member.ExaUserLocation, }, queries, cfg, onProgress, heartbeat) if len(out) == 0 { return nil, app.For(code.Setting).SvcThirdParty("暫時無法取得參考資料,請稍後重試") @@ -645,7 +650,13 @@ func syncResearchMapSources( Query: src.Query, }) } - fresh := scope.Brand + fresh, err := reloadScopeBrand(ctx, deps, tenantID, ownerUID, scope) + if err != nil { + return err + } + if fresh == nil { + return nil + } entityMap := brandentity.ResearchMap{ AudienceSummary: fresh.ResearchMap.AudienceSummary, ContentGoal: fresh.ResearchMap.ContentGoal, diff --git a/haixun-backend/internal/worker/job/generate_copy_draft.go b/haixun-backend/internal/worker/job/generate_copy_draft.go new file mode 100644 index 0000000..4a1fc53 --- /dev/null +++ b/haixun-backend/internal/worker/job/generate_copy_draft.go @@ -0,0 +1,210 @@ +package job + +import ( + "context" + "fmt" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/style8d" + libviral "haixun-backend/internal/library/viral" + domai "haixun-backend/internal/model/ai/domain/usecase" + aiusecase "haixun-backend/internal/model/ai/usecase" + copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" + missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" + jobdom "haixun-backend/internal/model/job/domain/usecase" + personadomain "haixun-backend/internal/model/persona/domain/usecase" + scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" + threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" +) + +type GenerateCopyDraftDeps struct { + Jobs jobdom.UseCase + CopyMission missiondomain.UseCase + Persona personadomain.UseCase + ScanPost scanpostdomain.UseCase + CopyDraft copydraftdomain.UseCase + ThreadsAccount threadsaccountdomain.UseCase + AI aiusecase.UseCase +} + +func RegisterGenerateCopyDraftHandler(runner *Runner, deps GenerateCopyDraftDeps) { + if runner == nil { + return + } + runner.RegisterStepHandler("copy_draft_generate", func(ctx context.Context, step StepContext) error { + return runGenerateCopyDraft(ctx, step, deps) + }) +} + +func runGenerateCopyDraft(ctx context.Context, step StepContext, deps GenerateCopyDraftDeps) error { + payload := step.Run.Payload + tenantID, ownerUID := runActorFromPayload(payload, step.Run) + personaID := stringField(payload, "persona_id") + missionID := copyMissionIDFromPayload(payload) + scanPostID := strings.TrimSpace(stringField(payload, "scan_post_id")) + if tenantID == "" || ownerUID == "" || personaID == "" || scanPostID == "" { + return fmt.Errorf("generate-copy-draft payload missing tenant_id, owner_uid, persona_id, or scan_post_id") + } + + updateProgress := func(summary string, percentage int) { + _ = step.Heartbeat(ctx) + _, _ = deps.Jobs.UpdateProgress(ctx, jobdom.UpdateProgressRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Phase: "copy_draft_generate", + Summary: summary, + Percentage: percentage, + }) + } + + updateProgress("讀取爆款原文…", 12) + + persona, err := deps.Persona.Get(ctx, tenantID, ownerUID, personaID) + if err != nil { + return err + } + post, err := deps.ScanPost.GetForPersona(ctx, tenantID, ownerUID, personaID, scanPostID) + if err != nil { + return err + } + if missionID == "" { + missionID = strings.TrimSpace(post.CopyMissionID) + } + + topicLabel := strings.TrimSpace(post.SearchTag) + topicBrief := strings.TrimSpace(persona.Brief) + if missionID != "" { + if mission, missionErr := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID); missionErr == nil { + if label := strings.TrimSpace(mission.Label); label != "" { + topicLabel = label + } + if brief := strings.TrimSpace(mission.Brief); brief != "" { + topicBrief = brief + } + } + } + + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) + + credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID) + if err != nil { + return err + } + providerID, err := aiusecase.MapWorkerProvider(credential.Provider) + if err != nil { + return err + } + + updateProgress("分析爆款結構…", 35) + + analysisText := "" + analyzeResult, analyzeErr := deps.AI.GenerateText(ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: libviral.BuildAnalyzeViralSystemPrompt(), + Messages: []domai.Message{ + { + Role: "user", + Content: libviral.BuildAnalyzeViralUserPrompt(libviral.AnalyzeViralInput{ + PostText: post.Text, + AuthorName: post.Author, + LikeCount: post.LikeCount, + ReplyCount: post.ReplyCount, + SearchTag: post.SearchTag, + TopicLabel: topicLabel, + TopicBrief: topicBrief, + Persona: personaBlock, + }), + }, + }, + }) + if analyzeErr == nil { + if parsed, parseErr := libviral.ParseAnalyzeViralOutput(analyzeResult.Text); parseErr == nil { + analysisText = libviral.FormatAnalysisForReplicate(parsed) + } + } + + updateProgress("深仿寫產文中…", 65) + + result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: libviral.BuildSystemPrompt(), + Messages: []domai.Message{ + { + Role: "user", + Content: libviral.BuildUserPrompt(libviral.ReplicateInput{ + TopicLabel: topicLabel, + TopicBrief: topicBrief, + Persona: personaBlock, + StyleProfile: "", + OriginalText: post.Text, + AuthorName: post.Author, + StructureAnalysis: analysisText, + }), + }, + }, + }) + if err != nil { + return err + } + + parsed, err := libviral.ParseReplicateOutput(result.Text) + if err != nil { + return app.For(code.AI).SvcThirdParty("仿寫 LLM 回傳無法解析:" + err.Error()) + } + + updateProgress("儲存仿寫草稿…", 90) + + saved, err := deps.CopyDraft.Create(ctx, copydraftdomain.CreateRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + CopyMissionID: post.CopyMissionID, + ScanPostID: scanPostID, + DraftType: "replicate", + Text: parsed.Text, + Angle: parsed.Angle, + Hook: parsed.Hook, + Rationale: parsed.Rationale, + ReferenceNotes: parsed.StructureNotes, + Sources: []string{post.Permalink}, + }) + if err != nil { + return err + } + + nextRoute := "/matrix" + if missionID != "" { + nextRoute = fmt.Sprintf("/matrix/missions/%s#copy-output", missionID) + } + handoff := map[string]any{ + "flow": "copy", + "persona_id": personaID, + "copy_mission_id": missionID, + "scan_post_id": scanPostID, + "summary": "已產出深仿寫草稿", + "next_route": nextRoute, + } + + _, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Result: map[string]any{ + "draft_id": saved.ID, + "handoff": handoff, + }, + }) + return err +} \ No newline at end of file diff --git a/haixun-backend/internal/worker/job/generate_copy_matrix.go b/haixun-backend/internal/worker/job/generate_copy_matrix.go new file mode 100644 index 0000000..412da56 --- /dev/null +++ b/haixun-backend/internal/worker/job/generate_copy_matrix.go @@ -0,0 +1,246 @@ +package job + +import ( + "context" + "fmt" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + libmatrix "haixun-backend/internal/library/matrix" + libprompt "haixun-backend/internal/library/prompt" + "haixun-backend/internal/library/style8d" + domai "haixun-backend/internal/model/ai/domain/usecase" + aiusecase "haixun-backend/internal/model/ai/usecase" + copydraftentity "haixun-backend/internal/model/copy_draft/domain/entity" + copydraftdomain "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" + personadomain "haixun-backend/internal/model/persona/domain/usecase" + scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" + threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" +) + +type GenerateCopyMatrixDeps struct { + Jobs jobdom.UseCase + CopyMission missiondomain.UseCase + Persona personadomain.UseCase + ScanPost scanpostdomain.UseCase + CopyDraft copydraftdomain.UseCase + ThreadsAccount threadsaccountdomain.UseCase + AI aiusecase.UseCase +} + +func RegisterGenerateCopyMatrixHandler(runner *Runner, deps GenerateCopyMatrixDeps) { + if runner == nil { + return + } + runner.RegisterStepHandler("copy_matrix_generate", func(ctx context.Context, step StepContext) error { + return runGenerateCopyMatrix(ctx, step, deps) + }) +} + +func runGenerateCopyMatrix(ctx context.Context, step StepContext, deps GenerateCopyMatrixDeps) error { + payload := step.Run.Payload + tenantID, ownerUID := runActorFromPayload(payload, step.Run) + personaID := stringField(payload, "persona_id") + missionID := copyMissionIDFromPayload(payload) + if tenantID == "" || ownerUID == "" || personaID == "" || missionID == "" { + return fmt.Errorf("generate-copy-matrix payload missing tenant_id, owner_uid, persona_id, or copy_mission_id") + } + + mission, err := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID) + if err != nil { + return err + } + if mission.Status != string(missionentity.StatusScanned) && + mission.Status != string(missionentity.StatusDrafted) { + return app.For(code.Persona).ResInvalidState("請先完成海巡再產出內容矩陣") + } + if len(mission.SelectedTags) == 0 { + return app.For(code.Persona).InputMissingRequired("請先選擇海巡標籤") + } + + persona, err := deps.Persona.Get(ctx, tenantID, ownerUID, personaID) + if err != nil { + return err + } + if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { + return app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") + } + personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) + + count := intField(payload, "count") + if count <= 0 { + count = 5 + } + if count > 12 { + count = 12 + } + + updateProgress := func(summary string, percentage int) { + _ = step.Heartbeat(ctx) + _, _ = deps.Jobs.UpdateProgress(ctx, jobdom.UpdateProgressRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Phase: "copy_matrix_generate", + Summary: summary, + Percentage: percentage, + }) + } + + updateProgress("讀取爆款樣本…", 15) + + posts, err := deps.ScanPost.ListForPersona(ctx, scanpostdomain.PersonaListRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + CopyMissionID: missionID, + Limit: 12, + }) + if err != nil { + return err + } + samples := matrixSamplesFromPosts(posts) + researchBlock := libmatrix.FormatCopyResearchMapBlock( + mission.ResearchMap.AudienceSummary, + mission.ResearchMap.ContentGoal, + mission.ResearchMap.Questions, + mission.ResearchMap.Pillars, + mission.ResearchMap.Exclusions, + ) + userPrompt, err := libmatrix.BuildCopyUserPrompt(libmatrix.CopyGenerateInput{ + Count: count, + TopicLabel: mission.Label, + TopicBrief: mission.Brief, + ResearchMap: researchBlock, + SelectedTags: mission.SelectedTags, + ViralSamples: samples, + PersonaBlock: personaBlock, + }) + if err != nil { + return app.For(code.AI).SysInternal("matrix user prompt load failed") + } + systemPrompt, err := libprompt.MatrixCopySystem() + if err != nil { + return app.For(code.AI).SysInternal("matrix system prompt load failed") + } + + updateProgress("產出內容矩陣草稿…", 45) + + credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID) + if err != nil { + return err + } + providerID, err := aiusecase.MapWorkerProvider(credential.Provider) + if err != nil { + return err + } + result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{ + Provider: providerID, + Model: credential.Model, + Credential: domai.Credential{ + APIKey: credential.APIKey, + }, + System: systemPrompt, + Messages: []domai.Message{ + {Role: "user", Content: userPrompt}, + }, + }) + if err != nil { + return err + } + parsed, err := libmatrix.ParseGenerateOutput(result.Text) + if err != nil { + return app.For(code.AI).SvcThirdParty("內容矩陣 LLM 回傳無法解析:" + err.Error()) + } + + updateProgress("儲存矩陣草稿…", 88) + + createReqs := make([]copydraftdomain.CreateRequest, 0, len(parsed.Rows)) + for _, row := range parsed.Rows { + createReqs = append(createReqs, copydraftdomain.CreateRequest{ + CopyMissionID: missionID, + DraftType: copydraftentity.DraftTypeMatrix, + SortOrder: row.SortOrder, + Text: row.Text, + Angle: row.Angle, + Hook: row.Hook, + Rationale: row.Rationale, + ReferenceNotes: row.ReferenceNotes, + Sources: row.SourcePermalinks, + }) + } + saved, err := deps.CopyDraft.ReplaceMissionMatrix(ctx, tenantID, ownerUID, personaID, missionID, createReqs) + if err != nil { + return err + } + + drafted := missionentity.StatusDrafted + _, _ = deps.CopyMission.Update(ctx, missiondomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + MissionID: missionID, + Patch: missiondomain.MissionPatch{ + Status: &drafted, + }, + }) + + handoff := map[string]any{ + "flow": "copy", + "persona_id": personaID, + "copy_mission_id": missionID, + "summary": fmt.Sprintf("已產出 %d 篇矩陣草稿", len(saved)), + "next_route": fmt.Sprintf("/matrix/missions/%s#copy-output", missionID), + } + + _, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{ + JobID: step.JobID, + WorkerID: step.WorkerID, + Result: map[string]any{ + "draft_count": len(saved), + "handoff": handoff, + }, + }) + return err +} + +func matrixSamplesFromPosts(posts []scanpostdomain.ScanPostSummary) string { + samples := make([]libmatrix.ViralPostSample, 0, len(posts)) + for _, post := range posts { + replies := make([]libmatrix.ViralReplySample, 0, len(post.Replies)) + for _, reply := range post.Replies { + replies = append(replies, libmatrix.ViralReplySample{ + Author: reply.Author, + Text: reply.Text, + }) + } + samples = append(samples, libmatrix.ViralPostSample{ + Author: post.Author, + LikeCount: post.LikeCount, + SearchTag: post.SearchTag, + Text: post.Text, + Replies: replies, + }) + } + return libmatrix.FormatViralSamples(samples) +} + +func intField(payload map[string]any, key string) int { + if payload == nil { + return 0 + } + switch v := payload[key].(type) { + case int: + return v + case int32: + return int(v) + case int64: + return int(v) + case float64: + return int(v) + default: + return 0 + } +} \ No newline at end of file diff --git a/haixun-backend/internal/worker/job/payload_actor.go b/haixun-backend/internal/worker/job/payload_actor.go new file mode 100644 index 0000000..2a943df --- /dev/null +++ b/haixun-backend/internal/worker/job/payload_actor.go @@ -0,0 +1,22 @@ +package job + +import ( + "strings" + + "haixun-backend/internal/model/job/domain/entity" +) + +func runActorFromPayload(payload map[string]any, run *entity.Run) (tenantID, ownerUID string) { + tenantID = stringField(payload, "tenant_id") + ownerUID = stringField(payload, "owner_uid") + if run == nil { + return tenantID, ownerUID + } + if tenantID == "" { + tenantID = strings.TrimSpace(run.TenantID) + } + if ownerUID == "" { + ownerUID = strings.TrimSpace(run.OwnerUID) + } + return tenantID, ownerUID +} diff --git a/haixun-backend/internal/worker/job/placement_scope.go b/haixun-backend/internal/worker/job/placement_scope.go index 860c676..b54e37c 100644 --- a/haixun-backend/internal/worker/job/placement_scope.go +++ b/haixun-backend/internal/worker/job/placement_scope.go @@ -76,3 +76,25 @@ func placementTopicAsBrand(scope *placementScope, topic *topicdomain.TopicSummar out.ResearchMap = topic.ResearchMap return &out } + +func reloadScopeBrand( + ctx context.Context, + deps ExpandGraphDeps, + tenantID, ownerUID string, + scope *placementScope, +) (*branddomain.BrandSummary, error) { + if scope == nil || scope.Brand == nil { + return nil, nil + } + if scope.TopicID != "" && deps.PlacementTopic != nil { + topic, err := deps.PlacementTopic.Get(ctx, tenantID, ownerUID, scope.TopicID) + if err != nil { + return nil, err + } + return placementTopicAsBrand(scope, topic), nil + } + if deps.Brand != nil && strings.TrimSpace(scope.CatalogBrand) != "" { + return deps.Brand.Get(ctx, tenantID, ownerUID, scope.CatalogBrand) + } + return scope.Brand, nil +} diff --git a/haixun-backend/internal/worker/job/scan_placement.go b/haixun-backend/internal/worker/job/scan_placement.go index 495da16..12d9fab 100644 --- a/haixun-backend/internal/worker/job/scan_placement.go +++ b/haixun-backend/internal/worker/job/scan_placement.go @@ -5,16 +5,16 @@ import ( "fmt" "strings" - libbrave "haixun-backend/internal/library/brave" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/placement" + "haixun-backend/internal/library/websearch" branddomain "haixun-backend/internal/model/brand/domain/usecase" jobdom "haixun-backend/internal/model/job/domain/usecase" kgusecase "haixun-backend/internal/model/knowledge_graph/domain/usecase" - placementtopicdomain "haixun-backend/internal/model/placement_topic/domain/usecase" placementusecase "haixun-backend/internal/model/placement/usecase" + placementtopicdomain "haixun-backend/internal/model/placement_topic/domain/usecase" scanpostusecase "haixun-backend/internal/model/scan_post/domain/usecase" threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" ) @@ -66,7 +66,7 @@ func runScanPlacement(ctx context.Context, step StepContext, deps ScanPlacementD var graphSummary *kgusecase.GraphSummary var graphErr error if topicID != "" { - graphSummary, graphErr = deps.KnowledgeGraph.GetByTopic(ctx, tenantID, ownerUID, topicID) + graphSummary, graphErr = deps.KnowledgeGraph.GetByTopic(ctx, tenantID, ownerUID, topicID, brandID) } else { graphSummary, graphErr = deps.KnowledgeGraph.Get(ctx, tenantID, ownerUID, brandID) } @@ -145,8 +145,12 @@ func runScanPlacement(ctx context.Context, step StepContext, deps ScanPlacementD if !memberCtx.AllowsBrave && !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler { return fmt.Errorf("目前連線模式無法海巡,請確認 Threads API、Brave 或 Chrome Session 設定") } - if placement.MemberNeedsBraveKey(memberCtx) && strings.TrimSpace(memberCtx.BraveAPIKey) == "" { - return fmt.Errorf("請在設定頁設定 Brave Search API key(跟隨此登入帳號)") + if placement.MemberNeedsWebSearchKey(memberCtx) && strings.TrimSpace(memberCtx.WebSearchAPIKey()) == "" { + return fmt.Errorf("%s", placement.WebSearchKeyRequiredMessage(placement.ResearchSettings{ + WebSearchProvider: memberCtx.WebSearchProvider, + BraveAPIKey: memberCtx.BraveAPIKey, + ExaAPIKey: memberCtx.ExaAPIKey, + })) } if memberCtx.DevMode && !memberCtx.BrowserConnected { return fmt.Errorf("開發模式需先同步 Chrome Session") @@ -178,7 +182,7 @@ func runScanPlacement(ctx context.Context, step StepContext, deps ScanPlacementD return err } - braveClient := libbrave.NewClient(memberCtx.BraveAPIKey) + webClient := websearch.New(memberCtx.WebSearchConfig()) crawlerFn := placement.WrapPoliteCrawler(makeCrawlerSearchFn(deps, tenantID, ownerUID)) graphNodes := []libkg.Node{} if graphSummary != nil { @@ -197,7 +201,7 @@ func runScanPlacement(ctx context.Context, step StepContext, deps ScanPlacementD PatrolKeywords: patrolKeywords, Exclusions: exclusions, Member: memberCtx, - Client: braveClient, + WebSearch: webClient, Crawler: crawlerFn, OnCheckpoint: func(batch []placement.ScanCandidate) error { if len(batch) == 0 { diff --git a/haixun-backend/internal/worker/job/scan_viral.go b/haixun-backend/internal/worker/job/scan_viral.go index 764eaf0..0b1d95c 100644 --- a/haixun-backend/internal/worker/job/scan_viral.go +++ b/haixun-backend/internal/worker/job/scan_viral.go @@ -11,6 +11,9 @@ 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" + 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" personadomain "haixun-backend/internal/model/persona/domain/usecase" @@ -21,8 +24,10 @@ import ( type ScanViralDeps struct { Jobs jobdom.UseCase + CopyMission missiondomain.UseCase Persona personadomain.UseCase ScanPost scanpostusecase.UseCase + CopyDraft copydraftusecase.UseCase ThreadsAccount threadsaccountdomain.UseCase Placement placementusecase.UseCase AI aiusecase.UseCase @@ -39,9 +44,9 @@ func RegisterScanViralHandler(runner *Runner, deps ScanViralDeps) { func runScanViral(ctx context.Context, step StepContext, deps ScanViralDeps) error { payload := step.Run.Payload - tenantID := stringField(payload, "tenant_id") - ownerUID := stringField(payload, "owner_uid") + tenantID, ownerUID := runActorFromPayload(payload, step.Run) personaID := personaIDFromPayload(payload) + missionID := copyMissionIDFromPayload(payload) if tenantID == "" || ownerUID == "" || personaID == "" { return fmt.Errorf("scan-viral payload missing tenant_id, owner_uid, or persona_id") } @@ -50,6 +55,14 @@ func runScanViral(ctx context.Context, step StepContext, deps ScanViralDeps) err if err != nil { return err } + var mission *missiondomain.MissionSummary + if missionID != "" { + item, err := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID) + if err != nil { + return err + } + mission = item + } research, err := deps.Placement.ResearchSettings(ctx, tenantID, ownerUID) if err != nil { @@ -59,11 +72,8 @@ func runScanViral(ctx context.Context, step StepContext, deps ScanViralDeps) err if err != nil { return err } - if !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler { - return fmt.Errorf("爆款掃描需要 Threads API 或 Chrome Session(開發模式)") - } - if memberCtx.DevMode && !memberCtx.BrowserConnected { - return fmt.Errorf("開發模式需先同步 Chrome Session") + if !memberCtx.HasDiscoverPath() { + return fmt.Errorf("爆款掃描需要 Threads API、Chrome Session 或 Web Search API(請檢查連線模式與搜尋來源)") } updateProgress := func(summary string, percentage int) { @@ -78,7 +88,7 @@ func runScanViral(ctx context.Context, step StepContext, deps ScanViralDeps) err } bootstrap := boolField(payload, "bootstrap") - if bootstrap && persona.CopyResearchMap.AudienceSummary == "" && len(persona.CopyResearchMap.SuggestedTags) == 0 { + if mission == nil && bootstrap && persona.CopyResearchMap.AudienceSummary == "" && len(persona.CopyResearchMap.SuggestedTags) == 0 { updateProgress("產生拷貝忍者研究地圖…", 8) if err := ensureCopyResearchMap(ctx, deps, tenantID, ownerUID, persona, memberCtx, updateProgress); err != nil { return err @@ -90,44 +100,124 @@ func runScanViral(ctx context.Context, step StepContext, deps ScanViralDeps) err } keywords := stringSliceField(payload, "keywords") - if len(keywords) == 0 { + exclusions := persona.CopyResearchMap.Exclusions + missionScan := mission != nil + if missionScan { + if len(keywords) == 0 { + keywords = append([]string(nil), mission.SelectedTags...) + } + exclusions = mission.ResearchMap.Exclusions + if len(keywords) == 0 { + return fmt.Errorf("請先產生研究地圖並勾選搜尋標籤") + } + } else if len(keywords) == 0 { keywords = deriveViralKeywords(persona) } if len(keywords) == 0 { return fmt.Errorf("請提供爆款掃描關鍵字,或先完成研究地圖/對標帳號") } - exclusions := persona.CopyResearchMap.Exclusions + if missionScan && missionID != "" { + updateProgress("清除舊爆款與產文草稿…", 8) + if deps.ScanPost != nil { + if err := deps.ScanPost.ClearCopyMissionViralScan(ctx, tenantID, ownerUID, personaID, missionID); err != nil { + return err + } + } + if deps.CopyDraft != nil { + if err := deps.CopyDraft.ClearByMission(ctx, tenantID, ownerUID, personaID, missionID); err != nil { + return err + } + } + } updateProgress("準備爆款掃描…", 12) crawlerFn := makeCrawlerSearchFn(ScanPlacementDeps{ThreadsAccount: deps.ThreadsAccount}, tenantID, ownerUID) candidates, err := libviral.RunDiscover(ctx, libviral.DiscoverInput{ - Keywords: keywords, - Exclusions: exclusions, - Member: memberCtx, - Crawler: crawlerFn, + Keywords: keywords, + Exclusions: exclusions, + Member: memberCtx, + Crawler: crawlerFn, + MissionScan: missionScan, }, updateProgress) if err != nil { return err } + if missionScan && memberCtx.ScrapeReplies && memberCtx.ApiConnected { + updateProgress("收集高互動留言樣本…", 88) + candidates = placement.AttachReplies(ctx, placement.ScrapeRepliesInput{ + Posts: candidates, + Member: memberCtx, + RepliesPerPost: memberCtx.RepliesPerPost, + MaxPosts: 6, + }) + } + updateProgress(fmt.Sprintf("寫入 %d 篇爆款候選…", len(candidates)), 92) count, err := deps.ScanPost.ReplaceFromViralScan(ctx, scanpostusecase.ViralReplaceRequest{ - TenantID: tenantID, - OwnerUID: ownerUID, - PersonaID: personaID, - ScanJobID: step.JobID, - Posts: candidates, + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + CopyMissionID: missionID, + ScanJobID: step.JobID, + Posts: candidates, }) if err != nil { return err } + if missionScan && missionID != "" && mission != nil { + scanned := missionentity.StatusScanned + jobID := step.JobID + referenceAccounts := libviral.BuildReferenceAccountsFromScan(libviral.ReferenceAccountInput{ + SeedQuery: mission.SeedQuery, + Label: mission.Label, + Posts: candidates, + Limit: libviral.MaxSimilarAccounts, + }) + entityTags := make([]missionentity.SuggestedTag, 0, len(mission.ResearchMap.SuggestedTags)) + for _, tag := range mission.ResearchMap.SuggestedTags { + entityTags = append(entityTags, missionentity.SuggestedTag{ + Tag: tag.Tag, + Reason: tag.Reason, + SearchIntent: tag.SearchIntent, + SearchType: tag.SearchType, + }) + } + updatedMap := missionentity.ResearchMap{ + AudienceSummary: mission.ResearchMap.AudienceSummary, + ContentGoal: mission.ResearchMap.ContentGoal, + Questions: append([]string(nil), mission.ResearchMap.Questions...), + Pillars: append([]string(nil), mission.ResearchMap.Pillars...), + Exclusions: append([]string(nil), mission.ResearchMap.Exclusions...), + SuggestedTags: entityTags, + SimilarAccounts: referenceAccounts, + BenchmarkNotes: mission.ResearchMap.BenchmarkNotes, + } + _, _ = deps.CopyMission.Update(ctx, missiondomain.UpdateRequest{ + TenantID: tenantID, + OwnerUID: ownerUID, + PersonaID: personaID, + MissionID: missionID, + Patch: missiondomain.MissionPatch{ + LastScanJobID: &jobID, + Status: &scanned, + ResearchMap: &updatedMap, + }, + }) + } + + nextRoute := "/matrix" + if missionID != "" { + nextRoute = fmt.Sprintf("/matrix/missions/%s", missionID) + } handoff := map[string]any{ - "flow": "copy", - "persona_id": personaID, - "summary": fmt.Sprintf("爆款掃描完成:%d 篇候選", count), - "next_route": "/matrix", + "flow": "copy", + "persona_id": personaID, + "copy_mission_id": missionID, + "summary": fmt.Sprintf("爆款掃描完成:%d 篇候選", count), + "next_route": nextRoute, } _, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{ diff --git a/haixun-backend/web/public/downloads/haixun-threads-sync.zip b/haixun-backend/web/public/downloads/haixun-threads-sync.zip index 0561a97..d8a2a7d 100644 Binary files a/haixun-backend/web/public/downloads/haixun-threads-sync.zip and b/haixun-backend/web/public/downloads/haixun-threads-sync.zip differ diff --git a/haixun-backend/web/src/App.tsx b/haixun-backend/web/src/App.tsx index 7fa9fab..1c73c58 100644 --- a/haixun-backend/web/src/App.tsx +++ b/haixun-backend/web/src/App.tsx @@ -17,6 +17,7 @@ import { BrandProductEditPage } from './pages/BrandProductEditPage' import { BrandsPage } from './pages/BrandsPage' import { PersonaDetailPage } from './pages/PersonaDetailPage' import { MatrixEntryRoute } from './components/MatrixEntryRoute' +import { CopyMissionDetailPage } from './pages/CopyMissionDetailPage' import { OutreachEntryRoute } from './components/OutreachEntryRoute' import { PlacementTopicResearchMapPage } from './pages/PlacementTopicResearchMapPage' import { PlacementTopicSettingsPage } from './pages/PlacementTopicSettingsPage' @@ -50,6 +51,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/haixun-backend/web/src/components/AccountConnectionMode.tsx b/haixun-backend/web/src/components/AccountConnectionMode.tsx index 4841af2..4001505 100644 --- a/haixun-backend/web/src/components/AccountConnectionMode.tsx +++ b/haixun-backend/web/src/components/AccountConnectionMode.tsx @@ -102,7 +102,7 @@ export function AccountConnectionMode({ accountId, connectionsPath }: AccountCon
連線模式

- 決定此帳號走 Threads 官方 API,或本機爬蟲。OAuth、Chrome 同步等細節在{' '} + 發文/留言走正式 API 或開發爬蟲;海巡搜尋來源可獨立設定(爬蟲與 API 可自由切換)。細節見{' '} 連線設定

@@ -147,30 +147,29 @@ export function AccountConnectionMode({ accountId, connectionsPath }: AccountCon onClick={() => saveDevMode(true)} /> - {!devMode ? ( -
- 海巡搜尋來源 -

- 正式模式下的貼文搜尋管道。開發模式一律走本機爬蟲(已內建禮貌間隔,降低被封風險)。 -

-
- {SEARCH_SOURCE_OPTIONS.map((option) => ( - saveSearchSourceMode(option.id)} - /> - ))} -
-
- ) : ( -

- 開發模式使用 Playwright 爬蟲,每次查詢間隔約 8~12 秒,避免 Threads 限流。 +

+ 海巡搜尋來源 +

+ 拷貝/海巡任務依此管道找爆款。藍勾、粉絲數為加分資訊,拿不到仍正常海巡;選「爬蟲優先」可省 API 次數。

- )} +
+ {SEARCH_SOURCE_OPTIONS.map((option) => ( + saveSearchSourceMode(option.id)} + /> + ))} +
+ {devMode ? ( +

+ 開發模式:發文仍走爬蟲;海巡亦可用上方來源。爬蟲查詢間隔約 8~12 秒。 +

+ ) : null} +
) : (

無法載入連線設定。

diff --git a/haixun-backend/web/src/components/AccountSwitcher.tsx b/haixun-backend/web/src/components/AccountSwitcher.tsx index 34eaac8..a03913e 100644 --- a/haixun-backend/web/src/components/AccountSwitcher.tsx +++ b/haixun-backend/web/src/components/AccountSwitcher.tsx @@ -104,11 +104,6 @@ export function AccountSwitcher() { return (
- {accountGuideActive && !open ? ( - - 先建立帳號 - - ) : null} diff --git a/haixun-backend/web/src/components/CopyMissionActiveJobsCard.tsx b/haixun-backend/web/src/components/CopyMissionActiveJobsCard.tsx new file mode 100644 index 0000000..d3b55de --- /dev/null +++ b/haixun-backend/web/src/components/CopyMissionActiveJobsCard.tsx @@ -0,0 +1,135 @@ +import { Fragment, type ReactNode } from 'react' +import { Link } from 'react-router-dom' +import { + copyMissionDetailPath, + GENERATE_COPY_DRAFT_PIPELINE_STEPS, + GENERATE_COPY_MATRIX_PIPELINE_STEPS, + replicateJobScanPostId, +} from '../lib/copyFlow' +import type { JobData } from '../types/api' +import { Card } from './ui' +import { CopyMissionAnalyzeJobPanel } from './CopyMissionAnalyzeJobPanel' +import { CopyMissionJobPanel } from './CopyMissionJobPanel' +import { ViralScanJobPanel } from './ViralScanJobPanel' + +type Props = { + missionId: string + analyzeJob: JobData | null + scanJob: JobData | null + matrixJob: JobData | null + replicateJobs: JobData[] +} + +function JobSectionHeader({ + title, + hash, + missionId, +}: { + title: string + hash: string + missionId: string +}) { + return ( +
+

{title}

+ + 查看區塊 + +
+ ) +} + +function SectionDivider() { + return
+} + +type Section = { + key: string + title: string + hash: string + content: ReactNode +} + +export function CopyMissionActiveJobsCard({ + missionId, + analyzeJob, + scanJob, + matrixJob, + replicateJobs, +}: Props) { + const sections: Section[] = [] + + if (analyzeJob) { + sections.push({ + key: 'analyze', + title: '研究地圖', + hash: '#copy-research', + content: , + }) + } + if (scanJob) { + sections.push({ + key: 'scan', + title: '爆款海巡', + hash: '#copy-output', + content: , + }) + } + if (matrixJob) { + sections.push({ + key: 'matrix', + title: '內容矩陣', + hash: '#copy-output', + content: ( + + ), + }) + } + for (const job of replicateJobs) { + const postId = replicateJobScanPostId(job) + const shortId = postId ? postId.slice(0, 8) : job.id.slice(0, 8) + sections.push({ + key: `replicate-${job.id}`, + title: `深仿寫 · ${shortId}`, + hash: '#copy-output', + content: ( + + ), + }) + } + + if (!sections.length) return null + + return ( +
+ +

進行中任務

+

+ 重新整理頁面後仍會從這裡追蹤進度;完成後此區塊會自動消失。 +

+
+ {sections.map((section, index) => ( + + {index > 0 ? : null} +
+ + {section.content} +
+
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/haixun-backend/web/src/components/CopyMissionAnalyzeJobPanel.tsx b/haixun-backend/web/src/components/CopyMissionAnalyzeJobPanel.tsx new file mode 100644 index 0000000..34c6969 --- /dev/null +++ b/haixun-backend/web/src/components/CopyMissionAnalyzeJobPanel.tsx @@ -0,0 +1,69 @@ +import { ANALYZE_COPY_MISSION_PIPELINE_STEPS } from '../lib/copyFlow' +import { jobStatusBadgeClass, jobStatusLabel } from '../lib/jobStatus' +import type { JobData } from '../types/api' +import { ProgressBar, StatusBadge } from './ui' + +const STEP_STATUS_LABEL: Record = { + pending: '等待', + running: '進行中', + succeeded: '完成', + failed: '失敗', + skipped: '略過', + cancelled: '取消', +} + +function analyzeJobHint(job: JobData): string { + if (job.status === 'cancel_requested') { + return '研究地圖任務取消中,通常幾秒內完成。' + } + if (job.status === 'queued' || job.status === 'pending') { + return '任務已排隊,背景 worker 即將接手…' + } + return job.progress?.summary?.trim() || '研究地圖產生中,請稍候…' +} + +export function CopyMissionAnalyzeJobPanel({ job }: { job: JobData }) { + const steps = job.progress?.steps ?? [] + const stepMap = new Map(steps.map((s) => [s.id, s])) + const pct = job.progress?.percentage ?? 0 + + return ( +
+

{analyzeJobHint(job)}

+
+ + {jobStatusLabel(job.status)} + + {pct}% +
+ +
    + {ANALYZE_COPY_MISSION_PIPELINE_STEPS.map((step) => { + const live = stepMap.get(step.id) + const status = live?.status ?? 'pending' + const isDone = status === 'succeeded' || status === 'done' + const isRunning = status === 'running' + return ( +
  1. + {step.title} + · {STEP_STATUS_LABEL[status] ?? status} +
  2. + ) + })} +
+ {job.error ? ( +

錯誤:{job.error}

+ ) : null} +
+ ) +} \ No newline at end of file diff --git a/haixun-backend/web/src/components/CopyMissionDiceButton.tsx b/haixun-backend/web/src/components/CopyMissionDiceButton.tsx new file mode 100644 index 0000000..c90d5b0 --- /dev/null +++ b/haixun-backend/web/src/components/CopyMissionDiceButton.tsx @@ -0,0 +1,50 @@ +type Props = { + rolling: boolean + disabled?: boolean + onClick: () => void +} + +function DiceIcon({ rolling }: { rolling: boolean }) { + return ( + + + + + + + + + ) +} + +export function CopyMissionDiceButton({ rolling, disabled, onClick }: Props) { + return ( + + ) +} \ No newline at end of file diff --git a/haixun-backend/web/src/components/CopyMissionDraftCard.tsx b/haixun-backend/web/src/components/CopyMissionDraftCard.tsx new file mode 100644 index 0000000..8710a9c --- /dev/null +++ b/haixun-backend/web/src/components/CopyMissionDraftCard.tsx @@ -0,0 +1,157 @@ +import { useEffect, useState } from 'react' +import { threadsPostPublishBlocked } from '../lib/threadsPostLimits' +import type { CopyDraftData } from '../types/api' +import { ThreadsPostCharMeter } from './ThreadsPostCharMeter' +import { Badge, Button, Field, Textarea } from './ui' + +type DraftEdit = { text: string; hook: string } + +type Props = { + draft: CopyDraftData + editing: boolean + edit: DraftEdit + onEdit: () => void + onCancelEdit: () => void + onChange: (patch: Partial) => void + onSave: () => void + onMarkReady: () => void + onCopy: () => void | Promise + onPublish: () => void + saving?: boolean + publishing?: boolean +} + +function draftKindLabel(type?: string) { + return type === 'matrix' ? '矩陣' : '深仿寫' +} + +function draftKindTone(type?: string): 'brand' | 'sky' { + return type === 'matrix' ? 'brand' : 'sky' +} + +function statusBadge(draft: CopyDraftData) { + if (draft.status === 'published') return 已發布 + if (draft.status === 'ready') return 可發布 + return 草稿 +} + +export function CopyMissionDraftCard({ + draft, + editing, + edit, + onEdit, + onCancelEdit, + onChange, + onSave, + onMarkReady, + onCopy, + onPublish, + saving, + publishing, +}: Props) { + const hook = editing ? edit.hook : draft.hook + const text = editing ? edit.text : draft.text + const bodyText = editing ? edit.text : draft.text + const saveBlocked = threadsPostPublishBlocked(bodyText) + const publishBlocked = saveBlocked + const [copied, setCopied] = useState(false) + + useEffect(() => { + if (!copied) return + const timer = window.setTimeout(() => setCopied(false), 2000) + return () => window.clearTimeout(timer) + }, [copied]) + + const handleCopy = async () => { + const ok = await onCopy() + if (ok) setCopied(true) + } + + return ( +
+
+
+ {draftKindLabel(draft.draft_type)} + {draft.sort_order ? #{draft.sort_order} : null} + {draft.angle ? {draft.angle} : null} + {statusBadge(draft)} +
+ {!editing ? ( +
+ + + {draft.status !== 'published' ? ( + + ) : null} +
+ ) : null} +
+ + {hook ? ( +

+ Hook + {editing ? ( + +