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() {
- 決定此帳號走 Threads 官方 API,或本機爬蟲。OAuth、Chrome 同步等細節在{' '}
+ 發文/留言走正式 API 或開發爬蟲;海巡搜尋來源可獨立設定(爬蟲與 API 可自由切換)。細節見{' '}
- 正式模式下的貼文搜尋管道。開發模式一律走本機爬蟲(已內建禮貌間隔,降低被封風險)。 -
-- 開發模式使用 Playwright 爬蟲,每次查詢間隔約 8~12 秒,避免 Threads 限流。 +
+ 拷貝/海巡任務依此管道找爆款。藍勾、粉絲數為加分資訊,拿不到仍正常海巡;選「爬蟲優先」可省 API 次數。
- )} ++ 開發模式:發文仍走爬蟲;海巡亦可用上方來源。爬蟲查詢間隔約 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 (