From 2e0b6257355917f102246c82bb13ade1fd7419a1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 9 Jul 2026 03:45:28 +0000 Subject: [PATCH] fix all --- .gitignore | 26 +- .grok/skills/spec-driven/SKILL.md | 134 ++ .../spec-driven/references/templates/plan.md | 48 + .../references/templates/requirements.md | 77 + .../spec-driven/references/templates/spec.md | 85 + .../spec-driven/references/templates/task.md | 54 + .../skills/spec-driven/references/workflow.md | 54 + .grok/skills/spec-driven/scripts/init-run.sh | 51 + AGENTS.md | 431 +----- README.md | 50 + apps/web/.gitignore | 24 + apps/web/.oxlintrc.json | 8 + apps/web/README.md | 32 + apps/web/index.html | 13 + apps/web/package-lock.json | 1365 +++++++++++++++++ apps/web/package.json | 25 + apps/web/public/favicon.svg | 1 + apps/web/public/icons.svg | 24 + apps/web/src/App.css | 50 + apps/web/src/App.tsx | 19 + apps/web/src/assets/hero.png | Bin 0 -> 13057 bytes apps/web/src/assets/react.svg | 1 + apps/web/src/assets/vite.svg | 1 + apps/web/src/index.css | 16 + apps/web/src/main.tsx | 10 + apps/web/tsconfig.app.json | 26 + {backend => apps}/web/tsconfig.json | 0 apps/web/tsconfig.node.json | 23 + apps/web/vite.config.ts | 7 + .../dev-console/dist/assets/index-56hNfpGK.js | 3 - backend/dev-console/dist/index.html | 225 --- backend/generate/api/copy_mission.api | 396 ----- .../create_copy_mission_handler.go | 32 - .../delete_copy_mission_handler.go | 32 - ...lete_copy_mission_matrix_drafts_handler.go | 32 - .../expand_copy_mission_graph_handler.go | 32 - .../generate_copy_mission_matrix_handler.go | 32 - .../get_copy_mission_graph_handler.go | 32 - .../get_copy_mission_scan_schedule_handler.go | 32 - .../list_copy_mission_copy_drafts_handler.go | 32 - .../list_copy_mission_scan_posts_handler.go | 32 - .../list_copy_missions_handler.go | 32 - ...ch_copy_mission_audience_sample_handler.go | 32 - .../patch_copy_mission_graph_nodes_handler.go | 32 - ...ch_copy_mission_similar_account_handler.go | 32 - .../schedule_copy_mission_drafts_handler.go | 32 - .../start_copy_mission_analyze_job_handler.go | 32 - ...art_copy_mission_copy_draft_job_handler.go | 32 - .../start_copy_mission_matrix_job_handler.go | 32 - .../start_copy_mission_scan_job_handler.go | 32 - .../update_copy_mission_handler.go | 32 - ...sert_copy_mission_scan_schedule_handler.go | 32 - ...rt_publish_inventory_refill_job_handler.go | 32 - .../internal/library/copymission/knowledge.go | 388 ----- .../library/copymission/knowledge_test.go | 121 -- .../library/copymission/reference_urls.go | 66 - .../copymission/reference_urls_test.go | 34 - .../library/copymission/research_items.go | 79 - .../copymission/research_items_test.go | 28 - .../library/copymission/research_sources.go | 19 - backend/internal/library/embedding/client.go | 138 -- .../prompt/files/matrix_copy.system.md | 25 - .../library/prompt/files/matrix_copy.user.md | 33 - backend/internal/library/storage/s3.go | 84 - .../copy_mission/create_copy_mission_logic.go | 33 - .../copy_mission/delete_copy_mission_logic.go | 33 - ...delete_copy_mission_matrix_drafts_logic.go | 33 - .../expand_copy_mission_graph_logic.go | 33 - .../generate_copy_mission_matrix_logic.go | 33 - .../get_copy_mission_graph_logic.go | 33 - .../copy_mission/get_copy_mission_logic.go | 33 - .../get_copy_mission_scan_schedule_logic.go | 33 - .../list_copy_mission_copy_drafts_logic.go | 33 - .../list_copy_mission_scan_posts_logic.go | 33 - .../copy_mission/list_copy_missions_logic.go | 33 - ...atch_copy_mission_audience_sample_logic.go | 33 - .../patch_copy_mission_graph_nodes_logic.go | 33 - ...atch_copy_mission_similar_account_logic.go | 33 - .../schedule_copy_mission_drafts_logic.go | 33 - .../start_copy_mission_analyze_job_logic.go | 33 - ...start_copy_mission_copy_draft_job_logic.go | 33 - .../start_copy_mission_matrix_job_logic.go | 33 - .../start_copy_mission_scan_job_logic.go | 33 - .../copy_mission/update_copy_mission_logic.go | 33 - ...upsert_copy_mission_scan_schedule_logic.go | 33 - ...tart_publish_inventory_refill_job_logic.go | 61 - .../crm_contact/domain/entity/contact.go | 30 - .../domain/repository/repository.go | 25 - .../crm_contact/domain/usecase/usecase.go | 47 - .../model/crm_contact/repository/mongo.go | 217 --- .../model/crm_contact/usecase/usecase.go | 176 --- .../publish_draft/domain/entity/draft.go | 15 - .../domain/repository/repository.go | 21 - .../publish_draft/domain/usecase/usecase.go | 48 - docs/product/README.md | 44 + docs/product/haixun-console/README.md | 42 + docs/product/haixun-console/plan.md | 138 ++ docs/product/haixun-console/requirements.md | 331 ++++ docs/product/haixun-console/spec.md | 463 ++++++ docs/product/haixun-console/tasks/INDEX.md | 35 + .../tasks/T001-scaffold-apps-web.md | 52 + .../tasks/T002-design-tokens.md | 48 + .../tasks/T003-ui-primitives.md | 48 + .../haixun-console/tasks/T004-app-shell.md | 49 + .../tasks/T005-router-placeholders.md | 48 + .../haixun-console/tasks/T006-mock-auth.md | 51 + .../tasks/T007-repo-interfaces.md | 48 + .../tasks/T008-mock-repos-seed.md | 48 + .../haixun-console/tasks/T009-crew-page.md | 45 + .../haixun-console/tasks/T010-studio-list.md | 45 + .../haixun-console/tasks/T011-wizard-shell.md | 47 + .../tasks/T012-wizard-topic-crew.md | 47 + .../tasks/T013-wizard-script-steps.md | 47 + .../tasks/T014-wizard-preview-submit.md | 48 + .../haixun-console/tasks/T015-outbox-ui.md | 46 + .../tasks/T016-outbox-simulate.md | 47 + .../haixun-console/tasks/T017-mock-jobs.md | 45 + .../tasks/T018-notifications.md | 46 + .../haixun-console/tasks/T019-today-page.md | 43 + .../tasks/T020-scout-settings.md | 46 + .../tasks/T021-seed-example-play.md | 43 + .../haixun-console/tasks/T022-rwd-smoke.md | 44 + .dockerignore => old/.dockerignore | 0 old/AGENTS.md | 419 +++++ Makefile => old/Makefile | 0 old/README.md | 30 + .../ai_threads_auto_account_ops.md | 0 {backend => old/backend}/Dockerfile | 0 {backend => old/backend}/Makefile | 0 {backend => old/backend}/README.md | 0 {backend => old/backend}/cmd/tool/main.go | 0 {backend => old/backend}/cmd/worker/main.go | 0 .../backend}/dev-console/README.md | 0 .../backend}/dev-console/index.html | 0 .../backend}/dev-console/package-lock.json | 0 .../backend}/dev-console/package.json | 0 .../backend}/dev-console/vite.config.ts | 0 .../backend}/docs/job-system-plan.md | 0 .../docs/threads-automation-gap-plan.md | 0 .../backend}/etc/gateway.prod.yaml | 0 .../backend}/etc/gateway.worker.prod.yaml | 0 .../backend}/etc/gateway.worker.yaml | 0 {backend => old/backend}/etc/gateway.yaml | 0 {backend => old/backend}/gateway.go | 0 {backend => old/backend}/generate/api/ai.api | 0 .../backend}/generate/api/auth.api | 18 + .../backend}/generate/api/brand.api | 0 .../backend}/generate/api/common.api | 0 old/backend/generate/api/copy_mission.api | 60 + .../backend}/generate/api/gateway.api | 0 {backend => old/backend}/generate/api/job.api | 0 .../backend}/generate/api/member.api | 0 .../backend}/generate/api/normal.api | 0 .../backend}/generate/api/permission.api | 0 .../backend}/generate/api/persona.api | 0 .../backend}/generate/api/placement_topic.api | 0 .../backend}/generate/api/setting.api | 0 .../backend}/generate/api/threads_account.api | 65 +- .../backend}/generate/api/worker_internal.api | 0 .../backend}/generate/goctl/api/handler.tpl | 0 {backend => old/backend}/go.mod | 0 {backend => old/backend}/go.sum | 0 .../backend}/internal/bootstrap/admin.go | 0 .../backend}/internal/bootstrap/admin_test.go | 16 + .../backend}/internal/bootstrap/envfile.go | 0 .../internal/bootstrap/envfile_test.go | 0 .../backend}/internal/bootstrap/init.go | 2 - .../backend}/internal/config/config.go | 0 .../internal/handler/ai/chat_handler.go | 0 .../handler/ai/chat_stream_handler.go | 0 .../ai/islander_chat_stream_handler.go | 0 .../ai/list_ai_provider_models_handler.go | 0 .../handler/ai/list_ai_providers_handler.go | 0 .../internal/handler/auth/login_handler.go | 0 .../internal/handler/auth/logout_handler.go | 0 .../internal/handler/auth/refresh_handler.go | 0 .../internal/handler/auth/register_handler.go | 0 .../auth/resend_verification_email_handler.go | 20 + .../handler/auth/verify_email_handler.go | 12 +- .../handler/brand/create_brand_handler.go | 0 .../brand/create_brand_product_handler.go | 0 .../handler/brand/delete_brand_handler.go | 0 .../brand/delete_brand_product_handler.go | 0 .../brand/expand_knowledge_graph_handler.go | 0 .../generate_brand_content_matrix_handler.go | 0 .../brand/generate_outreach_drafts_handler.go | 0 .../brand/get_brand_content_matrix_handler.go | 0 .../handler/brand/get_brand_handler.go | 0 .../brand/get_brand_scan_schedule_handler.go | 0 .../brand/get_knowledge_graph_handler.go | 0 .../brand/list_brand_products_handler.go | 0 .../brand/list_brand_scan_posts_handler.go | 0 .../handler/brand/list_brands_handler.go | 0 .../patch_knowledge_graph_nodes_handler.go | 0 .../brand/patch_scan_post_outreach_handler.go | 0 .../brand/publish_outreach_draft_handler.go | 0 .../brand/start_brand_scan_job_handler.go | 0 .../handler/brand/update_brand_handler.go | 0 .../brand/update_brand_product_handler.go | 0 .../brand/update_outreach_draft_handler.go | 0 .../upsert_brand_scan_schedule_handler.go | 0 .../inspire_copy_mission_handler.go | 0 .../job/ack_worker_job_cancel_handler.go | 0 .../analyze_style8_d_from_worker_handler.go | 0 .../handler/job/cancel_job_handler.go | 0 .../job/check_worker_job_cancel_handler.go | 0 .../handler/job/claim_worker_job_handler.go | 0 .../job/complete_worker_job_handler.go | 0 .../handler/job/create_job_handler.go | 0 .../job/create_job_schedule_handler.go | 0 .../job/delete_job_schedule_handler.go | 0 .../job/disable_job_schedule_handler.go | 0 .../job/enable_job_schedule_handler.go | 0 .../handler/job/fail_worker_job_handler.go | 0 .../internal/handler/job/get_job_handler.go | 0 .../handler/job/get_job_template_handler.go | 0 ..._worker_threads_account_session_handler.go | 0 .../handler/job/list_job_events_handler.go | 0 .../handler/job/list_job_schedules_handler.go | 0 .../handler/job/list_job_templates_handler.go | 0 .../internal/handler/job/list_jobs_handler.go | 0 .../job/refresh_worker_job_lock_handler.go | 0 .../internal/handler/job/retry_job_handler.go | 0 ...rsona_style_profile_from_worker_handler.go | 0 .../job/update_job_schedule_handler.go | 0 .../job/update_worker_job_progress_handler.go | 0 .../job/upsert_job_template_handler.go | 0 .../member/get_member_ai_settings_handler.go | 0 .../member/get_member_capabilities_handler.go | 0 .../handler/member/get_member_me_handler.go | 0 .../get_member_placement_settings_handler.go | 0 .../list_member_ai_provider_models_handler.go | 0 .../handler/member/list_members_handler.go | 0 .../update_member_ai_settings_handler.go | 0 .../member/update_member_me_handler.go | 0 .../member/update_member_password_handler.go | 0 ...pdate_member_placement_settings_handler.go | 0 .../member/update_member_profile_handler.go | 0 .../member/update_member_roles_handler.go | 0 .../member/upload_member_avatar_handler.go | 0 .../internal/handler/normal/health_handler.go | 0 .../permission/get_me_permissions_handler.go | 0 .../get_permission_catalog_handler.go | 0 .../persona/create_content_plan_handler.go | 0 .../persona/create_feedback_event_handler.go | 0 .../persona/create_formula_pool_handler.go | 0 .../create_knowledge_source_handler.go | 0 .../handler/persona/create_persona_handler.go | 0 .../delete_persona_copy_draft_handler.go | 0 .../handler/persona/delete_persona_handler.go | 0 .../persona/delete_style_preset_handler.go | 0 .../generate_from_content_formula_handler.go | 0 .../generate_persona_copy_draft_handler.go | 0 .../generate_persona_topic_matrix_handler.go | 0 .../handler/persona/get_persona_handler.go | 0 .../persona/list_content_plans_handler.go | 0 .../persona/list_formula_pools_handler.go | 0 .../persona/list_knowledge_chunks_handler.go | 0 .../persona/list_knowledge_sources_handler.go | 0 .../list_persona_content_inbox_handler.go | 0 .../list_persona_copy_drafts_handler.go | 0 .../list_persona_viral_scan_posts_handler.go | 0 .../handler/persona/list_personas_handler.go | 0 .../persona/list_style_presets_handler.go | 0 .../persona/list_topic_candidates_handler.go | 0 .../prune_persona_copy_drafts_handler.go | 0 .../publish_persona_copy_draft_handler.go | 0 .../schedule_persona_copy_draft_handler.go | 0 .../schedule_persona_drafts_handler.go | 0 ...start_persona_formula_draft_job_handler.go | 0 ...start_persona_rewrite_draft_job_handler.go | 0 ...ersona_style_analysis_from_text_handler.go | 0 .../start_persona_style_analysis_handler.go | 0 .../start_persona_topic_matrix_job_handler.go | 0 .../start_persona_viral_scan_job_handler.go | 0 .../persona/update_content_plan_handler.go | 0 .../update_persona_copy_draft_handler.go | 0 .../handler/persona/update_persona_handler.go | 0 .../persona/upsert_style_preset_handler.go | 0 ...lete_placement_topic_scan_posts_handler.go | 0 .../create_placement_topic_handler.go | 0 .../delete_placement_topic_handler.go | 0 ...elete_placement_topic_scan_post_handler.go | 0 .../expand_placement_topic_graph_handler.go | 0 ..._placement_topic_content_matrix_handler.go | 0 ...placement_topic_outreach_drafts_handler.go | 0 ..._placement_topic_content_matrix_handler.go | 0 .../get_placement_topic_graph_handler.go | 0 .../get_placement_topic_handler.go | 0 ...t_placement_topic_scan_schedule_handler.go | 0 ...list_placement_topic_scan_posts_handler.go | 0 .../list_placement_topics_handler.go | 0 ...tch_placement_topic_graph_nodes_handler.go | 0 ...cement_topic_scan_post_outreach_handler.go | 0 ..._placement_topic_outreach_draft_handler.go | 0 ...cement_topic_outreach_draft_job_handler.go | 0 .../start_placement_topic_scan_job_handler.go | 0 .../update_placement_topic_handler.go | 0 ..._placement_topic_outreach_draft_handler.go | 0 ...t_placement_topic_scan_schedule_handler.go | 0 .../internal/handler/register_extra.go | 11 + .../backend}/internal/handler/routes.go | 251 +-- .../handler/setting/delete_setting_handler.go | 0 .../handler/setting/get_setting_handler.go | 0 .../handler/setting/list_settings_handler.go | 0 .../handler/setting/upsert_setting_handler.go | 0 .../handler/static/extension_download.go | 0 .../internal/handler/static/public_asset.go | 50 + .../activate_threads_account_handler.go | 0 .../analyze_content_formula_handler.go | 0 .../cancel_publish_queue_item_handler.go | 0 .../create_publish_queue_item_handler.go | 0 .../create_threads_account_handler.go | 0 .../delete_content_formula_handler.go | 0 .../delete_publish_queue_item_handler.go | 0 .../delete_threads_account_handler.go | 0 .../disconnect_threads_api_handler.go | 0 .../generate_own_post_formula_handler.go | 0 .../generate_own_post_reply_draft_handler.go | 0 .../get_content_formula_handler.go | 0 .../get_publish_dashboard_summary_handler.go | 0 .../get_publish_guard_policy_handler.go | 0 .../get_publish_inventory_policy_handler.go | 0 .../get_publish_queue_item_handler.go | 0 .../get_publish_slot_insights_handler.go | 0 ...get_threads_account_ai_settings_handler.go | 0 .../get_threads_account_connection_handler.go | 0 .../get_threads_account_handler.go | 0 .../get_threads_diagnostics_handler.go | 0 .../get_threads_oauth_config_handler.go | 0 ...ort_own_post_to_content_formula_handler.go | 0 .../import_threads_account_session_handler.go | 0 .../list_content_formulas_handler.go | 0 .../list_mention_inbox_handler.go | 0 .../list_own_post_formulas_handler.go | 0 .../list_publish_alerts_handler.go | 0 .../list_publish_queue_events_handler.go | 0 .../list_publish_queue_items_handler.go | 0 .../list_publish_queue_range_handler.go | 0 ...eads_account_ai_provider_models_handler.go | 0 .../list_threads_accounts_handler.go | 0 .../list_threads_oauth_logs_handler.go | 0 .../list_threads_post_performance_handler.go | 0 .../list_threads_publish_health_handler.go | 0 .../patch_content_formula_handler.go | 0 .../patch_publish_queue_item_handler.go | 0 .../publish_mention_reply_handler.go | 20 +- .../publish_threads_reply_handler.go | 58 + .../threads_account/reply_multipart.go | 28 + .../resume_publish_guard_handler.go | 0 .../retry_publish_queue_item_handler.go | 0 .../run_threads_api_playground_handler.go | 0 .../run_threads_api_smoke_test_handler.go | 0 .../search_content_formula_posts_handler.go | 0 .../start_threads_oauth_handler.go | 0 .../sync_mention_inbox_handler.go | 0 .../threads_oauth_callback_handler.go | 0 ...ate_threads_account_ai_settings_handler.go | 0 ...date_threads_account_connection_handler.go | 0 .../update_threads_account_handler.go | 0 .../upload_publish_attachment_handler.go | 40 + .../upsert_publish_guard_policy_handler.go | 0 ...upsert_publish_inventory_policy_handler.go | 0 .../internal/library/authctx/context.go | 0 .../internal/library/brave/breaker.go | 0 .../backend}/internal/library/brave/client.go | 0 .../backend}/internal/library/clock/clock.go | 0 .../internal/library/clock/clock_test.go | 0 .../library/copymission/matrix_lock.go | 0 .../internal/library/copyvoice/human.go | 0 .../internal/library/copyvoice/human_test.go | 0 .../internal/library/crypto/aesgcm.go | 0 .../internal/library/errors/code/types.go | 0 .../internal/library/errors/errors.go | 0 .../backend}/internal/library/exa/client.go | 0 .../internal/library/exa/client_test.go | 0 .../library/formula/analyze_pasted.go | 0 .../library/formula/generate_from_formula.go | 0 .../formula/generate_from_formula_test.go | 0 .../internal/library/formula/jsonflex.go | 0 .../internal/library/formula/jsonflex_test.go | 0 .../internal/library/knowledge/bootstrap.go | 0 .../library/knowledge/bootstrap_test.go | 0 .../library/knowledge/brave_collect.go | 0 .../library/knowledge/brave_collect_test.go | 0 .../library/knowledge/expand_strategy.go | 0 .../library/knowledge/expand_strategy_test.go | 0 .../internal/library/knowledge/graph.go | 0 .../library/knowledge/graph_rescore.go | 0 .../library/knowledge/graph_selection_test.go | 0 .../library/knowledge/patrol_input.go | 0 .../library/knowledge/patrol_phrase.go | 0 .../library/knowledge/patrol_phrase_test.go | 0 .../internal/library/knowledge/patrol_rank.go | 0 .../library/knowledge/patrol_rank_test.go | 0 .../library/knowledge/patrol_resolve.go | 0 .../library/knowledge/patrol_resolve_test.go | 0 .../library/knowledge/patrol_search.go | 0 .../library/knowledge/patrol_search_test.go | 0 .../internal/library/knowledge/patrol_tags.go | 0 .../library/knowledge/patrol_tags_test.go | 0 .../internal/library/knowledge/quality.go | 0 .../library/knowledge/quality_test.go | 0 .../internal/library/knowledge/queries.go | 0 .../library/knowledge/queries_test.go | 0 .../library/knowledge/research_map_fit.go | 0 .../library/knowledge/search_keyword_shape.go | 0 .../knowledge/search_keyword_shape_test.go | 0 .../library/knowledge/semantic_fit.go | 0 .../library/knowledge/semantic_fit_test.go | 0 .../internal/library/knowledge/synth.go | 0 .../internal/library/knowledge/synth_test.go | 0 .../internal/library/llmjson/sanitize.go | 0 .../internal/library/llmjson/sanitize_test.go | 0 .../backend}/internal/library/mailer/smtp.go | 0 .../internal/library/matrix/generate.go | 0 .../internal/library/matrix/generate_test.go | 0 .../internal/library/matrix/reference.go | 0 .../internal/library/matrix/research_map.go | 56 +- .../internal/library/matrix/samples.go | 0 .../internal/library/mongo/brand_scope.go | 0 .../backend}/internal/library/mongo/client.go | 0 .../internal/library/mongo/ensure_index.go | 0 .../library/outreach/draft_context.go | 0 .../internal/library/outreach/draft_format.go | 0 .../library/outreach/draft_format_test.go | 0 .../internal/library/outreach/generate.go | 0 .../library/outreach/generate_draft.go | 0 .../library/outreach/generate_test.go | 0 .../library/outreach/persona_voice.go | 0 .../library/outreach/persona_voice_test.go | 0 .../library/outreach/previous_drafts.go | 0 .../library/outreach/previous_drafts_test.go | 0 .../internal/library/ownpost/post_formula.go | 0 .../library/ownpost/post_formula_test.go | 0 .../internal/library/ownpost/reply_draft.go | 0 .../library/ownpost/reply_draft_test.go | 0 .../internal/library/permmatch/match.go | 0 .../internal/library/permmatch/match_test.go | 0 .../library/personacopy/formula_draft.go | 0 .../library/personacopy/rewrite_draft.go | 0 .../library/personacopy/topic_matrix.go | 0 .../internal/library/placement/ai_generate.go | 0 .../library/placement/capabilities.go | 0 .../internal/library/placement/context.go | 0 .../placement/context_api_only_test.go | 0 .../library/placement/context_prompt.go | 0 .../library/placement/context_prompt_test.go | 0 .../library/placement/crawler_exec.go | 0 .../library/placement/crawler_exec_test.go | 0 .../library/placement/crawler_polite.go | 0 .../internal/library/placement/discover.go | 0 .../placement/discover_for_query_test.go | 0 .../library/placement/discover_route.go | 0 .../library/placement/discover_route_test.go | 0 .../internal/library/placement/dual_track.go | 0 .../library/placement/effective_strategy.go | 0 .../placement/effective_strategy_test.go | 0 .../internal/library/placement/exclusion.go | 0 .../internal/library/placement/external_id.go | 0 .../library/placement/external_id_test.go | 0 .../internal/library/placement/filter.go | 0 .../internal/library/placement/filter_test.go | 0 .../library/placement/media_resolve.go | 0 .../library/placement/parse_threads.go | 0 .../library/placement/patrol_queries.go | 0 .../library/placement/patrol_queries_test.go | 0 .../library/placement/post_relevance.go | 0 .../library/placement/post_relevance_test.go | 0 .../library/placement/product_catalog.go | 0 .../library/placement/product_context.go | 0 .../product_context_placement_url_test.go | 0 .../library/placement/product_match.go | 0 .../library/placement/product_match_test.go | 0 .../internal/library/placement/query_build.go | 0 .../library/placement/query_build_test.go | 0 .../internal/library/placement/recency.go | 0 .../library/placement/recency_cascade.go | 0 .../library/placement/recency_cascade_test.go | 0 .../library/placement/research_map.go | 0 .../library/placement/research_map_test.go | 0 .../library/placement/resolve_media_exec.go | 0 .../library/placement/resolve_reply_target.go | 0 .../placement/resolve_reply_target_test.go | 0 .../library/placement/scrape_replies.go | 0 .../library/placement/scrape_replies_test.go | 0 .../internal/library/placement/source_mode.go | 0 .../library/placement/source_mode_test.go | 0 .../internal/library/placement/threads_api.go | 0 .../internal/library/placement/topic_fit.go | 0 .../internal/library/prompt/compose.go | 18 - .../internal/library/prompt/compose_test.go | 0 .../internal/library/prompt/errors.go | 0 .../library/prompt/files/ai.chat.system.md | 0 .../prompt/files/ai.islander.system.md | 0 .../prompt/files/knowledge_graph.queries.json | 0 .../files/knowledge_graph.supplemental.md | 0 .../prompt/files/knowledge_graph.system.md | 0 .../prompt/files/knowledge_graph.user.md | 0 .../files/knowledge_graph_llm.system.md | 0 .../prompt/files/knowledge_graph_llm.user.md | 0 .../prompt/files/matrix_placement.system.md | 0 .../prompt/files/matrix_placement.user.md | 0 .../prompt/files/outreach_placement.system.md | 0 .../prompt/files/outreach_placement.user.md | 0 .../internal/library/prompt/files/overlay.md | 0 .../library/prompt/files/style8d.schema.md | 0 .../library/prompt/files/style8d.system.md | 0 .../internal/library/prompt/registry.go | 18 +- .../internal/library/publishschedule/slots.go | 0 .../library/publishschedule/slots_test.go | 0 .../library/ratelimit/token_bucket.go | 0 .../internal/library/redact/redact.go | 0 .../internal/library/redact/redact_test.go | 0 .../backend}/internal/library/redis/client.go | 0 .../internal/library/storage/asset_ref.go | 44 + .../library/storage/asset_ref_test.go | 34 + .../library/storage/ephemeral_asset.go | 132 ++ .../library/storage/ephemeral_asset_test.go | 30 + .../internal/library/storage/public_asset.go | 19 + .../library/storage/public_asset_test.go | 22 + .../library/storage/publish_attachment.go | 68 + .../storage/publish_attachment_test.go | 42 + old/backend/internal/library/storage/s3.go | 173 +++ .../library/storage/s3_public_url_test.go | 34 + .../library/storage/threads_image_url.go | 49 + .../library/storage/threads_image_url_test.go | 26 + .../internal/library/style8d/analyze.go | 0 .../internal/library/style8d/analyze_test.go | 0 .../internal/library/style8d/prompt.go | 0 .../internal/library/style8d/prompt_test.go | 0 .../internal/library/style8d/runner.go | 0 .../library/style8d/text_input_test.go | 0 .../internal/library/threadsapi/client.go | 27 +- .../library/threadsapi/client_error_test.go | 20 + .../internal/library/threadsapi/extended.go | 0 .../library/threadsapi/fields_test.go | 0 .../internal/library/threadsapi/media.go | 0 .../internal/library/threadsapi/media_test.go | 0 .../internal/library/threadsapi/oauth.go | 0 .../threadsapi/oauth_permission_test.go | 0 .../internal/library/threadsapi/oauth_test.go | 0 .../internal/library/threadsapi/publish.go | 209 +-- .../library/threadsapi/publish_media.go | 155 ++ .../internal/library/threadsapi/replies.go | 0 .../library/threadsapi/replies_test.go | 0 .../library/threadsapi/resolve_media.go | 0 .../threadsapi/resolve_media_hint_test.go | 0 .../library/threadsapi/resolve_media_test.go | 0 .../internal/library/threadsapi/scopes.go | 0 .../internal/library/threadsapi/url.go | 0 .../internal/library/threadspost/format.go | 0 .../library/threadspost/format_test.go | 0 .../internal/library/threadspost/limits.go | 20 + .../library/threadspost/limits_test.go | 0 .../internal/library/threadspost/mimic.go | 0 .../library/threadspost/mimic_test.go | 0 .../internal/library/validate/validate.go | 0 .../internal/library/viral/analyze_viral.go | 0 .../library/viral/analyze_viral_test.go | 0 .../internal/library/viral/audience.go | 0 .../internal/library/viral/discover.go | 0 .../library/viral/discover_accounts.go | 0 .../library/viral/discover_accounts_test.go | 0 .../library/viral/discover_budget_test.go | 0 .../library/viral/discover_graceful_test.go | 0 .../internal/library/viral/enrich_accounts.go | 0 .../internal/library/viral/flexible_json.go | 0 .../internal/library/viral/free_trends.go | 0 .../internal/library/viral/known_accounts.go | 0 .../internal/library/viral/merge_accounts.go | 0 .../internal/library/viral/mission_inspire.go | 0 .../library/viral/mission_inspire_collect.go | 0 .../library/viral/mission_inspire_test.go | 0 .../library/viral/mission_research_map.go | 0 .../viral/mission_research_map_test.go | 0 .../internal/library/viral/mission_topic.go | 0 .../internal/library/viral/quality.go | 0 .../internal/library/viral/quality_test.go | 0 .../library/viral/reference_accounts.go | 0 .../viral/reference_accounts_fallback_test.go | 0 .../library/viral/reference_accounts_test.go | 0 .../internal/library/viral/replicate.go | 0 .../internal/library/viral/research_map.go | 0 .../backend}/internal/library/viral/score.go | 0 .../internal/library/viral/search_tags.go | 0 .../library/viral/search_tags_test.go | 0 .../backend}/internal/library/viral/tags.go | 0 .../internal/library/viral/tags_test.go | 0 .../internal/library/webpage/digest.go | 0 .../internal/library/webpage/digest_test.go | 0 .../internal/library/webpage/format.go | 0 .../internal/library/websearch/client.go | 0 .../backend}/internal/logic/ai/actor.go | 0 .../backend}/internal/logic/ai/chat_logic.go | 0 .../internal/logic/ai/chat_stream_logic.go | 0 .../backend}/internal/logic/ai/credential.go | 0 .../logic/ai/islander_chat_stream_logic.go | 0 .../logic/ai/list_ai_provider_models_logic.go | 0 .../logic/ai/list_ai_providers_logic.go | 0 .../backend}/internal/logic/ai/mapper.go | 0 .../internal/logic/auth/email_verification.go | 78 + .../internal/logic/auth/login_logic.go | 8 +- .../internal/logic/auth/logout_logic.go | 0 .../backend}/internal/logic/auth/mapper.go | 0 .../internal/logic/auth/refresh_logic.go | 0 .../internal/logic/auth/register_logic.go | 28 +- .../auth/resend_verification_email_logic.go | 44 + .../internal/logic/auth/verify_email_logic.go | 39 + .../backend}/internal/logic/authz/admin.go | 0 .../backend}/internal/logic/brand/actor.go | 0 .../logic/brand/create_brand_logic.go | 0 .../logic/brand/create_brand_product_logic.go | 0 .../logic/brand/delete_brand_logic.go | 0 .../logic/brand/delete_brand_product_logic.go | 0 .../brand/expand_knowledge_graph_logic.go | 0 .../generate_brand_content_matrix_logic.go | 0 .../brand/generate_outreach_drafts_logic.go | 0 .../brand/get_brand_content_matrix_logic.go | 0 .../internal/logic/brand/get_brand_logic.go | 0 .../brand/get_brand_scan_schedule_logic.go | 0 .../logic/brand/get_knowledge_graph_logic.go | 0 .../logic/brand/list_brand_products_logic.go | 0 .../brand/list_brand_scan_posts_logic.go | 0 .../internal/logic/brand/list_brands_logic.go | 0 .../backend}/internal/logic/brand/mapper.go | 0 .../patch_knowledge_graph_nodes_logic.go | 0 .../brand/patch_scan_post_outreach_logic.go | 0 .../brand/publish_outreach_draft_logic.go | 0 .../logic/brand/start_brand_scan_job_logic.go | 0 .../logic/brand/update_brand_logic.go | 0 .../logic/brand/update_brand_product_logic.go | 0 .../brand/update_outreach_draft_logic.go | 0 .../brand/upsert_brand_scan_schedule_logic.go | 0 .../internal/logic/copy_mission/actor.go | 0 .../inspire_copy_mission_logic.go | 0 .../logic/job/ack_worker_job_cancel_logic.go | 0 .../backend}/internal/logic/job/actor.go | 0 .../job/analyze_style8_d_from_worker_logic.go | 0 .../internal/logic/job/cancel_job_logic.go | 0 .../job/check_worker_job_cancel_logic.go | 0 .../logic/job/claim_worker_job_logic.go | 0 .../logic/job/complete_worker_job_logic.go | 0 .../internal/logic/job/create_job_logic.go | 0 .../logic/job/create_job_schedule_logic.go | 0 .../logic/job/delete_job_schedule_logic.go | 0 .../logic/job/disable_job_schedule_logic.go | 0 .../logic/job/enable_job_schedule_logic.go | 0 .../logic/job/fail_worker_job_logic.go | 0 .../internal/logic/job/get_job_logic.go | 0 .../logic/job/get_job_template_logic.go | 0 ...et_worker_threads_account_session_logic.go | 0 .../logic/job/list_job_events_logic.go | 0 .../logic/job/list_job_schedules_logic.go | 0 .../logic/job/list_job_templates_logic.go | 0 .../internal/logic/job/list_jobs_logic.go | 0 .../backend}/internal/logic/job/mapper.go | 0 .../backend}/internal/logic/job/ownership.go | 0 .../internal/logic/job/ownership_test.go | 0 .../job/refresh_worker_job_lock_logic.go | 0 .../internal/logic/job/retry_job_logic.go | 0 ...persona_style_profile_from_worker_logic.go | 0 .../logic/job/update_job_schedule_logic.go | 0 .../job/update_worker_job_progress_logic.go | 0 .../logic/job/upsert_job_template_logic.go | 0 .../internal/logic/job/worker_helpers.go | 0 .../logic/member/ai_settings_mapper.go | 0 .../internal/logic/member/avatar_ref.go | 47 + .../member/get_member_ai_settings_logic.go | 0 .../member/get_member_capabilities_logic.go | 0 .../logic/member/get_member_me_logic.go | 2 +- .../get_member_placement_settings_logic.go | 0 .../list_member_ai_provider_models_logic.go | 0 .../logic/member/list_members_logic.go | 2 +- .../backend}/internal/logic/member/mapper.go | 0 .../member/update_member_ai_settings_logic.go | 0 .../logic/member/update_member_me_logic.go | 5 +- .../member/update_member_password_logic.go | 0 .../update_member_placement_settings_logic.go | 0 .../member/update_member_profile_logic.go | 5 +- .../logic/member/update_member_roles_logic.go | 0 .../member/upload_member_avatar_logic.go | 6 +- .../internal/logic/normal/health_logic.go | 0 .../permission/get_me_permissions_logic.go | 0 .../get_permission_catalog_logic.go | 0 .../internal/logic/permission/mapper.go | 0 .../backend}/internal/logic/persona/actor.go | 0 .../logic/persona/content_ops_mapper.go | 0 .../logic/persona/copy_draft_mapper.go | 0 .../persona/create_content_plan_logic.go | 0 .../persona/create_feedback_event_logic.go | 0 .../persona/create_formula_pool_logic.go | 0 .../persona/create_knowledge_source_logic.go | 0 .../logic/persona/create_persona_logic.go | 0 .../delete_persona_copy_draft_logic.go | 0 .../logic/persona/delete_persona_logic.go | 0 .../persona/delete_style_preset_logic.go | 0 .../generate_from_content_formula_logic.go | 0 .../generate_persona_copy_draft_logic.go | 0 .../generate_persona_topic_matrix_logic.go | 0 .../logic/persona/get_persona_logic.go | 0 .../logic/persona/list_content_plans_logic.go | 0 .../logic/persona/list_formula_pools_logic.go | 0 .../persona/list_knowledge_chunks_logic.go | 0 .../persona/list_knowledge_sources_logic.go | 0 .../list_persona_content_inbox_logic.go | 0 .../persona/list_persona_copy_drafts_logic.go | 0 .../list_persona_viral_scan_posts_logic.go | 0 .../logic/persona/list_personas_logic.go | 0 .../logic/persona/list_style_presets_logic.go | 0 .../persona/list_topic_candidates_logic.go | 0 .../backend}/internal/logic/persona/mapper.go | 0 .../prune_persona_copy_drafts_logic.go | 0 .../publish_persona_copy_draft_logic.go | 0 .../schedule_persona_copy_draft_logic.go | 0 .../persona/schedule_persona_drafts_logic.go | 0 .../start_persona_formula_draft_job_logic.go | 0 .../start_persona_rewrite_draft_job_logic.go | 0 ..._persona_style_analysis_from_text_logic.go | 0 .../start_persona_style_analysis_logic.go | 0 .../start_persona_topic_matrix_job_logic.go | 0 .../start_persona_viral_scan_job_logic.go | 0 .../persona/update_content_plan_logic.go | 0 .../update_persona_copy_draft_logic.go | 0 .../logic/persona/update_persona_logic.go | 0 .../persona/upsert_style_preset_logic.go | 0 .../internal/logic/placement_topic/actor.go | 0 ...delete_placement_topic_scan_posts_logic.go | 0 .../create_placement_topic_logic.go | 0 .../delete_placement_topic_logic.go | 0 .../delete_placement_topic_scan_post_logic.go | 0 .../expand_placement_topic_graph_logic.go | 0 ...te_placement_topic_content_matrix_logic.go | 0 ...e_placement_topic_outreach_drafts_logic.go | 0 ...et_placement_topic_content_matrix_logic.go | 0 .../get_placement_topic_graph_logic.go | 0 .../get_placement_topic_logic.go | 0 ...get_placement_topic_scan_schedule_logic.go | 0 .../list_placement_topic_scan_posts_logic.go | 0 .../list_placement_topics_logic.go | 0 .../internal/logic/placement_topic/mapper.go | 0 ...patch_placement_topic_graph_nodes_logic.go | 0 ...lacement_topic_scan_post_outreach_logic.go | 0 ...sh_placement_topic_outreach_draft_logic.go | 0 .../internal/logic/placement_topic/scope.go | 0 ...lacement_topic_outreach_draft_job_logic.go | 0 .../start_placement_topic_scan_job_logic.go | 0 .../update_placement_topic_logic.go | 0 ...te_placement_topic_outreach_draft_logic.go | 0 ...ert_placement_topic_scan_schedule_logic.go | 0 .../backend}/internal/logic/setting/authz.go | 0 .../logic/setting/delete_setting_logic.go | 0 .../logic/setting/get_setting_logic.go | 0 .../logic/setting/list_settings_logic.go | 0 .../backend}/internal/logic/setting/mapper.go | 0 .../logic/setting/upsert_setting_logic.go | 0 .../activate_threads_account_logic.go | 0 .../internal/logic/threads_account/actor.go | 0 .../analyze_content_formula_logic.go | 0 .../cancel_publish_queue_item_logic.go | 0 .../threads_account/content_formula_mapper.go | 0 .../create_publish_queue_item_logic.go | 2 + .../create_threads_account_logic.go | 0 .../delete_content_formula_logic.go | 0 .../delete_publish_queue_item_logic.go | 0 .../delete_threads_account_logic.go | 0 .../disconnect_threads_api_logic.go | 0 .../generate_own_post_formula_logic.go | 0 .../generate_own_post_reply_draft_logic.go | 0 .../get_content_formula_logic.go | 0 .../get_publish_dashboard_summary_logic.go | 0 .../get_publish_guard_policy_logic.go | 0 .../get_publish_inventory_policy_logic.go | 0 .../get_publish_queue_item_logic.go | 0 .../get_publish_slot_insights_logic.go | 0 .../get_threads_account_ai_settings_logic.go | 0 .../get_threads_account_connection_logic.go | 0 .../get_threads_account_logic.go | 0 .../get_threads_diagnostics_logic.go | 0 .../get_threads_oauth_config_logic.go | 0 ...mport_own_post_to_content_formula_logic.go | 0 .../import_threads_account_session_logic.go | 0 .../list_content_formulas_logic.go | 0 .../list_mention_inbox_logic.go | 0 .../list_own_post_formulas_logic.go | 0 .../list_publish_alerts_logic.go | 0 .../list_publish_queue_events_logic.go | 0 .../list_publish_queue_items_logic.go | 0 .../list_publish_queue_range_logic.go | 0 ...hreads_account_ai_provider_models_logic.go | 0 .../list_threads_accounts_logic.go | 0 .../list_threads_oauth_logs_logic.go | 0 .../list_threads_post_performance_logic.go | 0 .../list_threads_publish_health_logic.go | 0 .../internal/logic/threads_account/mapper.go | 2 + .../threads_account/mention_inbox_mapper.go | 0 .../own_post_formula_mapper.go | 0 .../patch_content_formula_logic.go | 0 .../patch_publish_queue_item_logic.go | 2 + .../threads_account/publish_attachment.go | 55 + .../publish_mention_reply_logic.go | 12 +- .../publish_threads_reply_logic.go | 87 ++ .../logic/threads_account/reply_image.go | 88 ++ .../resume_publish_guard_logic.go | 0 .../retry_publish_queue_item_logic.go | 0 .../run_threads_api_playground_logic.go | 14 +- .../run_threads_api_smoke_test_logic.go | 0 .../search_content_formula_posts_logic.go | 0 .../start_threads_oauth_logic.go | 0 .../sync_mention_inbox_logic.go | 0 .../threads_oauth_callback_logic.go | 0 ...pdate_threads_account_ai_settings_logic.go | 0 ...update_threads_account_connection_logic.go | 0 .../update_threads_account_logic.go | 0 .../upload_publish_attachment_logic.go | 82 + .../upsert_publish_guard_policy_logic.go | 0 .../upsert_publish_inventory_policy_logic.go | 0 .../backend}/internal/middleware/auth.go | 0 .../backend}/internal/middleware/auth_test.go | 0 .../internal/middleware/authjwt_middleware.go | 0 .../middleware/emailverified_middleware.go | 77 + .../emailverified_middleware_test.go | 26 + .../middleware/memberauth_middleware.go | 0 .../middleware/permissionrbac_middleware.go | 0 .../middleware/workersecret_middleware.go | 0 .../internal/model/ai/domain/enum/provider.go | 0 .../internal/model/ai/domain/usecase/ai.go | 0 .../model/ai/provider/openai_compatible.go | 0 .../ai/provider/openai_compatible_test.go | 0 .../internal/model/ai/usecase/provider_map.go | 0 .../internal/model/ai/usecase/usecase.go | 0 .../auth/domain/repository/token_revoke.go | 0 .../model/auth/domain/usecase/token.go | 0 .../auth/repository/token_revoke_redis.go | 0 .../internal/model/auth/usecase/token.go | 0 .../model/brand/domain/entity/brand.go | 0 .../model/brand/domain/entity/product.go | 0 .../model/brand/domain/entity/research_map.go | 0 .../brand/domain/repository/repository.go | 0 .../model/brand/domain/usecase/usecase.go | 0 .../internal/model/brand/repository/mongo.go | 0 .../internal/model/brand/usecase/products.go | 0 .../usecase/products_placement_url_test.go | 0 .../internal/model/brand/usecase/usecase.go | 0 .../content_formula/domain/entity/formula.go | 0 .../domain/repository/repository.go | 0 .../content_formula/domain/usecase/usecase.go | 0 .../model/content_formula/repository/mongo.go | 0 .../model/content_formula/usecase/usecase.go | 0 .../content_matrix/domain/entity/matrix.go | 0 .../domain/repository/repository.go | 0 .../content_matrix/domain/usecase/usecase.go | 0 .../model/content_matrix/repository/mongo.go | 0 .../model/content_matrix/usecase/usecase.go | 0 .../model/content_ops/domain/entity/entity.go | 0 .../domain/repository/repository.go | 0 .../content_ops/domain/usecase/usecase.go | 0 .../model/content_ops/repository/mongo.go | 0 .../model/content_ops/usecase/usecase.go | 0 .../model/copy_draft/domain/entity/draft.go | 0 .../domain/repository/repository.go | 0 .../copy_draft/domain/usecase/usecase.go | 0 .../model/copy_draft/repository/mongo.go | 0 .../model/copy_draft/usecase/usecase.go | 0 .../copy_mission/domain/entity/mission.go | 0 .../domain/repository/repository.go | 0 .../copy_mission/domain/usecase/usecase.go | 0 .../model/copy_mission/repository/mongo.go | 0 .../model/copy_mission/usecase/usecase.go | 0 .../internal/model/job/cron/next_run.go | 0 .../internal/model/job/cron/next_run_test.go | 0 .../internal/model/job/domain/entity/event.go | 0 .../internal/model/job/domain/entity/run.go | 0 .../model/job/domain/entity/schedule.go | 0 .../model/job/domain/entity/template.go | 0 .../internal/model/job/domain/enum/status.go | 0 .../model/job/domain/repository/event.go | 0 .../model/job/domain/repository/queue.go | 0 .../model/job/domain/repository/run.go | 0 .../model/job/domain/repository/schedule.go | 0 .../model/job/domain/repository/template.go | 0 .../internal/model/job/domain/usecase/job.go | 5 - .../model/job/repository/mongo_event.go | 0 .../model/job/repository/mongo_run.go | 0 .../model/job/repository/mongo_schedule.go | 0 .../model/job/repository/mongo_template.go | 0 .../model/job/repository/redis_queue.go | 0 .../model/job/repository/redis_queue_test.go | 0 .../internal/model/job/resume/resume.go | 0 .../internal/model/job/resume/resume_test.go | 0 .../internal/model/job/usecase/cancel_test.go | 0 .../internal/model/job/usecase/dedupe_test.go | 0 .../model/job/usecase/enqueue_run_test.go | 0 .../internal/model/job/usecase/helpers.go | 0 .../internal/model/job/usecase/maintenance.go | 0 .../job/usecase/maintenance_stuck_test.go | 0 .../model/job/usecase/maintenance_test.go | 0 .../model/job/usecase/refresh_lock_test.go | 0 .../job/usecase/retry_max_attempts_test.go | 0 .../model/job/usecase/retry_resume_test.go | 0 .../internal/model/job/usecase/retry_test.go | 0 .../internal/model/job/usecase/schedule.go | 0 .../model/job/usecase/schedule_payload.go | 0 .../job/usecase/schedule_payload_test.go | 0 .../model/job/usecase/schedule_test.go | 0 .../internal/model/job/usecase/step_resume.go | 0 .../internal/model/job/usecase/template.go | 0 .../model/job/usecase/template_test.go | 0 .../internal/model/job/usecase/test_mocks.go | 0 .../internal/model/job/usecase/usecase.go | 209 +-- .../knowledge_graph/domain/entity/graph.go | 0 .../domain/repository/repository.go | 0 .../knowledge_graph/domain/usecase/usecase.go | 0 .../model/knowledge_graph/repository/mongo.go | 0 .../knowledge_graph/usecase/topic_graph.go | 0 .../model/knowledge_graph/usecase/usecase.go | 0 .../model/member/domain/entity/member.go | 2 + .../model/member/domain/repository/member.go | 1 + .../model/member/domain/usecase/member.go | 2 + .../internal/model/member/repository/mongo.go | 41 + .../internal/model/member/usecase/usecase.go | 73 + .../mention_inbox/domain/entity/mention.go | 0 .../domain/repository/repository.go | 0 .../mention_inbox/domain/usecase/usecase.go | 3 +- .../model/mention_inbox/repository/mongo.go | 0 .../model/mention_inbox/usecase/usecase.go | 10 +- .../outreach_draft/domain/entity/draft.go | 0 .../domain/repository/repository.go | 0 .../outreach_draft/domain/usecase/usecase.go | 0 .../model/outreach_draft/repository/mongo.go | 0 .../model/outreach_draft/usecase/errors.go | 0 .../model/outreach_draft/usecase/usecase.go | 0 .../own_post_formula/domain/entity/review.go | 0 .../domain/repository/repository.go | 0 .../own_post_formula/repository/mongo.go | 0 .../model/own_post_formula/usecase/usecase.go | 0 .../permission/domain/entity/permission.go | 0 .../domain/repository/permission.go | 0 .../permission/domain/usecase/permission.go | 0 .../permission/repository/mongo_permission.go | 0 .../repository/mongo_role_permission.go | 0 .../model/permission/repository/object_id.go | 0 .../model/permission/usecase/usecase.go | 4 + .../model/permission/usecase/usecase_test.go | 0 .../domain/entity/copy_research_map.go | 0 .../model/persona/domain/entity/persona.go | 0 .../persona/domain/repository/repository.go | 0 .../model/persona/domain/usecase/usecase.go | 0 .../model/persona/repository/mongo.go | 0 .../internal/model/persona/usecase/usecase.go | 0 .../model/placement/usecase/settings.go | 0 .../usecase/settings_dev_mode_test.go | 0 .../placement_topic/domain/entity/topic.go | 0 .../domain/repository/repository.go | 0 .../placement_topic/domain/usecase/usecase.go | 0 .../model/placement_topic/repository/mongo.go | 0 .../model/placement_topic/usecase/usecase.go | 0 .../domain/entity/analytics.go | 0 .../domain/repository/repository.go | 0 .../domain/usecase/usecase.go | 0 .../publish_analytics/repository/mongo.go | 0 .../publish_analytics/usecase/schedule.go | 0 .../publish_analytics/usecase/usecase.go | 0 .../publish_guard/domain/entity/policy.go | 0 .../domain/repository/repository.go | 0 .../publish_guard/domain/usecase/usecase.go | 0 .../model/publish_guard/repository/mongo.go | 0 .../model/publish_guard/usecase/usecase.go | 0 .../publish_guard/usecase/usecase_test.go | 0 .../publish_inventory/domain/entity/policy.go | 0 .../domain/repository/repository.go | 0 .../domain/usecase/usecase.go | 0 .../publish_inventory/repository/mongo.go | 0 .../publish_inventory/usecase/usecase.go | 0 .../publish_queue/domain/entity/queue_item.go | 2 + .../domain/repository/repository.go | 0 .../publish_queue/domain/usecase/usecase.go | 8 + .../model/publish_queue/repository/mongo.go | 0 .../model/publish_queue/usecase/usecase.go | 117 +- .../domain/entity/event.go | 0 .../domain/repository/repository.go | 0 .../domain/usecase/usecase.go | 0 .../publish_queue_event/repository/mongo.go | 0 .../publish_queue_event/usecase/usecase.go | 0 .../scan_post/domain/entity/outreach_patch.go | 0 .../model/scan_post/domain/entity/post.go | 0 .../scan_post/domain/repository/repository.go | 0 .../model/scan_post/domain/usecase/usecase.go | 0 .../model/scan_post/repository/mongo.go | 0 .../repository/mongo_priority_filter_test.go | 0 .../repository/mongo_recent7d_filter_test.go | 0 .../repository/mongo_topic_scope_test.go | 0 .../model/scan_post/usecase/errors.go | 0 .../model/scan_post/usecase/usecase.go | 0 .../model/setting/domain/entity/setting.go | 0 .../setting/domain/repository/setting.go | 0 .../model/setting/domain/usecase/setting.go | 0 .../model/setting/repository/mongo.go | 0 .../internal/model/setting/usecase/usecase.go | 0 .../style_preset/domain/entity/preset.go | 0 .../domain/repository/repository.go | 0 .../style_preset/domain/usecase/usecase.go | 0 .../model/style_preset/repository/mongo.go | 0 .../model/style_preset/usecase/usecase.go | 0 .../threads_account/domain/entity/account.go | 0 .../threads_account/domain/entity/secrets.go | 0 .../domain/repository/repository.go | 0 .../threads_account/domain/usecase/usecase.go | 2 + .../model/threads_account/repository/mongo.go | 0 .../repository/oauth_state_redis.go | 0 .../repository/secrets_mongo.go | 0 .../threads_account/usecase/ai_credentials.go | 0 .../threads_account/usecase/api_playground.go | 14 +- .../threads_account/usecase/api_smoke.go | 0 .../usecase/connection_prefs_test.go | 0 .../threads_account/usecase/known_accounts.go | 0 .../model/threads_account/usecase/oauth.go | 0 .../usecase/oauth_debug_log.go | 0 .../usecase/placement_context.go | 0 .../usecase/placement_context_test.go | 0 .../usecase/post_performance.go | 0 .../usecase/publish_credentials.go | 0 .../threads_account/usecase/token_refresh.go | 0 .../model/threads_account/usecase/usecase.go | 0 .../backend}/internal/response/request.go | 0 .../backend}/internal/response/response.go | 0 .../backend}/internal/svc/job_workers.go | 0 .../backend}/internal/svc/service_context.go | 21 +- .../backend}/internal/types/types.go | 120 +- .../internal/worker/job/crawler_search.go | 0 .../internal/worker/job/expand_graph.go | 0 .../worker/job/generate_formula_draft.go | 0 .../worker/job/generate_outreach_draft.go | 0 .../worker/job/generate_rewrite_draft.go | 0 .../worker/job/generate_topic_matrix.go | 0 .../internal/worker/job/payload_actor.go | 0 .../internal/worker/job/payload_helpers.go | 0 .../internal/worker/job/placement_scope.go | 0 .../internal/worker/job/publish_analytics.go | 0 .../worker/job/publish_queue_sweeper.go | 0 .../backend}/internal/worker/job/reaper.go | 0 .../worker/job/refresh_threads_token.go | 0 .../backend}/internal/worker/job/runner.go | 0 .../internal/worker/job/runner_test.go | 0 .../internal/worker/job/scan_placement.go | 0 .../internal/worker/job/scan_viral.go | 0 .../backend}/internal/worker/job/scheduler.go | 0 .../worker/job/threads_token_sweeper.go | 0 {backend => old/backend}/web/Dockerfile | 0 {backend => old/backend}/web/index.html | 0 .../backend}/web/package-lock.json | 0 {backend => old/backend}/web/package.json | 0 {backend => old/backend}/web/src/App.tsx | 6 + .../backend}/web/src/api/client.ts | 28 +- .../backend}/web/src/api/haixun.ts | 36 +- .../backend}/web/src/auth/AuthContext.tsx | 11 +- .../backend}/web/src/components/AcIcon.tsx | 0 .../backend}/web/src/components/AuthDecor.tsx | 0 .../backend}/web/src/components/AuthShell.tsx | 0 .../web/src/components/DevToolsPanel.tsx | 0 .../src/components/ExtensionInstallCard.tsx | 0 .../web/src/components/HoverHelpTip.tsx | 0 .../web/src/components/JobMonitor.tsx | 0 .../backend}/web/src/components/Layout.tsx | 0 .../src/components/MentionContextBlock.tsx | 0 .../src/components/MentionProfileLookup.tsx | 0 .../web/src/components/PublishImageAttach.tsx | 102 ++ .../src/components/PublishReplyFeedback.tsx | 43 + .../web/src/components/ReplyImageAttach.tsx | 77 + .../web/src/components/ThemeToggle.tsx | 0 .../backend}/web/src/components/ui.tsx | 0 {backend => old/backend}/web/src/index.css | 28 + .../backend}/web/src/lib/acAssets.ts | 0 .../backend}/web/src/lib/activeAccount.ts | 0 old/backend/web/src/lib/assetUrl.ts | 49 + old/backend/web/src/lib/emailVerification.ts | 7 + .../backend}/web/src/lib/extensionInstall.ts | 0 .../backend}/web/src/lib/extensionSync.ts | 0 .../backend}/web/src/lib/format.ts | 0 .../backend}/web/src/lib/formulaCache.ts | 0 .../backend}/web/src/lib/jobActions.ts | 0 .../backend}/web/src/lib/jobEvents.ts | 0 .../backend}/web/src/lib/jobLabels.ts | 5 - .../backend}/web/src/lib/jobStatus.ts | 0 .../backend}/web/src/lib/mentionContext.ts | 0 .../backend}/web/src/lib/mentionInbox.ts | 0 .../backend}/web/src/lib/mentionInboxCache.ts | 0 .../backend}/web/src/lib/mentionLookup.ts | 0 .../backend}/web/src/lib/patrolPersona.ts | 0 .../backend}/web/src/lib/patrolPlan.ts | 0 .../backend}/web/src/lib/postScore.ts | 0 .../backend}/web/src/lib/postStats.ts | 0 .../backend}/web/src/lib/publishPageCache.ts | 0 .../backend}/web/src/lib/publishPersona.ts | 0 .../web/src/lib/publishReplyFeedback.ts | 84 + .../backend}/web/src/lib/replyMedia.ts | 0 .../backend}/web/src/lib/replyTree.ts | 0 .../backend}/web/src/lib/safeStorage.ts | 0 .../backend}/web/src/lib/threadsOAuth.ts | 0 {backend => old/backend}/web/src/main.tsx | 0 .../backend}/web/src/pages/AccountsPage.tsx | 0 .../backend}/web/src/pages/AuthPage.tsx | 10 +- .../backend}/web/src/pages/BrandsPage.tsx | 0 .../backend}/web/src/pages/DashboardPage.tsx | 0 old/backend/web/src/pages/EmailVerifyPage.tsx | 115 ++ .../backend}/web/src/pages/GapsPage.tsx | 0 .../backend}/web/src/pages/JobsPage.tsx | 0 .../backend}/web/src/pages/MissionsPage.tsx | 13 +- .../backend}/web/src/pages/PatrolPage.tsx | 0 .../backend}/web/src/pages/PersonasPage.tsx | 0 .../backend}/web/src/pages/PublishPage.tsx | 0 .../web/src/pages/RegisterMemberPage.tsx | 0 .../backend}/web/src/pages/SettingsPage.tsx | 20 +- .../src/pages/publish/MentionsInboxPanel.tsx | 114 +- .../web/src/pages/publish/OwnPostsHub.tsx | 65 +- .../web/src/pages/publish/ReplyInboxPanel.tsx | 24 +- .../backend}/web/src/theme/ThemeContext.tsx | 0 .../backend}/web/tsconfig.app.json | 0 old/backend/web/tsconfig.json | 7 + .../backend}/web/tsconfig.node.json | 0 {backend => old/backend}/web/vite.config.ts | 4 + {backend => old/backend}/worker/Dockerfile | 0 .../backend}/worker/keyword-search-server.ts | 0 .../backend}/worker/package-lock.json | 0 {backend => old/backend}/worker/package.json | 0 .../backend}/worker/resolve-media-server.ts | 0 .../backend}/worker/style-8d-worker.ts | 0 .../worker/threads-keyword-search-cli.ts | 0 .../backend}/worker/threads-keyword-search.ts | 0 .../backend}/worker/threads-media-id.ts | 0 .../worker/threads-profile-scraper.ts | 0 .../worker/threads-resolve-media-cli.ts | 0 .../backend}/worker/threads-resolve-media.ts | 0 {backend => old/backend}/worker/tsconfig.json | 0 {deploy => old/deploy}/.env.dev.example | 25 + {deploy => old/deploy}/.env.prod.example | 0 {deploy => old/deploy}/README.md | 0 {deploy => old/deploy}/docker-compose.yml | 35 - {deploy => old/deploy}/nginx/default.conf | 0 {deploy => old/deploy}/setup.sh | 0 old/docs/architecture.md | 276 ++++ old/docs/cleanup-log.md | 87 ++ old/docs/improvement-notes.md | 165 ++ {docs => old/docs}/post-project-plan.md | 0 .../extension}/haixun-threads-sync/README.md | 0 .../haixun-threads-sync/content-haixun.js | 0 .../haixun-threads-sync/manifest.json | 0 .../haixun-threads-sync/options.html | 0 .../extension}/haixun-threads-sync/options.js | 0 .../extension}/haixun-threads-sync/popup.html | 0 .../extension}/haixun-threads-sync/popup.js | 0 .../haixun-threads-sync/service-worker.js | 0 .../haixun-threads-sync/storage-state.js | 0 {infra => old/infra}/README.md | 0 {infra => old/infra}/docker-compose.yml | 3 +- {infra => old/infra}/etc/haixun.env.example | 0 {infra => old/infra}/nginx/haixun-dev.conf | 11 + {infra => old/infra}/nginx/haixun.conf | 0 {infra => old/infra}/start-api.sh | 0 {infra => old/infra}/start-infra.sh | 0 {infra => old/infra}/start-ngrok.sh | 0 {infra => old/infra}/start-node-worker.sh | 0 .../infra}/systemd/haixun-gateway.service | 0 .../infra}/systemd/haixun-node-worker.service | 0 .../infra}/systemd/haixun-worker.service | 0 {scripts => old/scripts}/check-dev.sh | 0 {scripts => old/scripts}/dev-all | 0 {scripts => old/scripts}/dev-all-stop.sh | 0 {scripts => old/scripts}/dev-all.sh | 6 +- {scripts => old/scripts}/package-extension.sh | 0 1170 files changed, 8603 insertions(+), 4877 deletions(-) create mode 100644 .grok/skills/spec-driven/SKILL.md create mode 100644 .grok/skills/spec-driven/references/templates/plan.md create mode 100644 .grok/skills/spec-driven/references/templates/requirements.md create mode 100644 .grok/skills/spec-driven/references/templates/spec.md create mode 100644 .grok/skills/spec-driven/references/templates/task.md create mode 100644 .grok/skills/spec-driven/references/workflow.md create mode 100755 .grok/skills/spec-driven/scripts/init-run.sh create mode 100644 README.md create mode 100644 apps/web/.gitignore create mode 100644 apps/web/.oxlintrc.json create mode 100644 apps/web/README.md create mode 100644 apps/web/index.html create mode 100644 apps/web/package-lock.json create mode 100644 apps/web/package.json create mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/public/icons.svg create mode 100644 apps/web/src/App.css create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/assets/hero.png create mode 100644 apps/web/src/assets/react.svg create mode 100644 apps/web/src/assets/vite.svg create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/tsconfig.app.json rename {backend => apps}/web/tsconfig.json (100%) create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts delete mode 100644 backend/dev-console/dist/assets/index-56hNfpGK.js delete mode 100644 backend/dev-console/dist/index.html delete mode 100644 backend/generate/api/copy_mission.api delete mode 100644 backend/internal/handler/copy_mission/create_copy_mission_handler.go delete mode 100644 backend/internal/handler/copy_mission/delete_copy_mission_handler.go delete mode 100644 backend/internal/handler/copy_mission/delete_copy_mission_matrix_drafts_handler.go delete mode 100644 backend/internal/handler/copy_mission/expand_copy_mission_graph_handler.go delete mode 100644 backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go delete mode 100644 backend/internal/handler/copy_mission/get_copy_mission_graph_handler.go delete mode 100644 backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go delete mode 100644 backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go delete mode 100644 backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go delete mode 100644 backend/internal/handler/copy_mission/list_copy_missions_handler.go delete mode 100644 backend/internal/handler/copy_mission/patch_copy_mission_audience_sample_handler.go delete mode 100644 backend/internal/handler/copy_mission/patch_copy_mission_graph_nodes_handler.go delete mode 100644 backend/internal/handler/copy_mission/patch_copy_mission_similar_account_handler.go delete mode 100644 backend/internal/handler/copy_mission/schedule_copy_mission_drafts_handler.go delete mode 100644 backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go delete mode 100644 backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go delete mode 100644 backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go delete mode 100644 backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go delete mode 100644 backend/internal/handler/copy_mission/update_copy_mission_handler.go delete mode 100644 backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go delete mode 100644 backend/internal/handler/threads_account/start_publish_inventory_refill_job_handler.go delete mode 100644 backend/internal/library/copymission/knowledge.go delete mode 100644 backend/internal/library/copymission/knowledge_test.go delete mode 100644 backend/internal/library/copymission/reference_urls.go delete mode 100644 backend/internal/library/copymission/reference_urls_test.go delete mode 100644 backend/internal/library/copymission/research_items.go delete mode 100644 backend/internal/library/copymission/research_items_test.go delete mode 100644 backend/internal/library/copymission/research_sources.go delete mode 100644 backend/internal/library/embedding/client.go delete mode 100644 backend/internal/library/prompt/files/matrix_copy.system.md delete mode 100644 backend/internal/library/prompt/files/matrix_copy.user.md delete mode 100644 backend/internal/library/storage/s3.go delete mode 100644 backend/internal/logic/copy_mission/create_copy_mission_logic.go delete mode 100644 backend/internal/logic/copy_mission/delete_copy_mission_logic.go delete mode 100644 backend/internal/logic/copy_mission/delete_copy_mission_matrix_drafts_logic.go delete mode 100644 backend/internal/logic/copy_mission/expand_copy_mission_graph_logic.go delete mode 100644 backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go delete mode 100644 backend/internal/logic/copy_mission/get_copy_mission_graph_logic.go delete mode 100644 backend/internal/logic/copy_mission/get_copy_mission_logic.go delete mode 100644 backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go delete mode 100644 backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go delete mode 100644 backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go delete mode 100644 backend/internal/logic/copy_mission/list_copy_missions_logic.go delete mode 100644 backend/internal/logic/copy_mission/patch_copy_mission_audience_sample_logic.go delete mode 100644 backend/internal/logic/copy_mission/patch_copy_mission_graph_nodes_logic.go delete mode 100644 backend/internal/logic/copy_mission/patch_copy_mission_similar_account_logic.go delete mode 100644 backend/internal/logic/copy_mission/schedule_copy_mission_drafts_logic.go delete mode 100644 backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go delete mode 100644 backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go delete mode 100644 backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go delete mode 100644 backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go delete mode 100644 backend/internal/logic/copy_mission/update_copy_mission_logic.go delete mode 100644 backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go delete mode 100644 backend/internal/logic/threads_account/start_publish_inventory_refill_job_logic.go delete mode 100644 backend/internal/model/crm_contact/domain/entity/contact.go delete mode 100644 backend/internal/model/crm_contact/domain/repository/repository.go delete mode 100644 backend/internal/model/crm_contact/domain/usecase/usecase.go delete mode 100644 backend/internal/model/crm_contact/repository/mongo.go delete mode 100644 backend/internal/model/crm_contact/usecase/usecase.go delete mode 100644 backend/internal/model/publish_draft/domain/entity/draft.go delete mode 100644 backend/internal/model/publish_draft/domain/repository/repository.go delete mode 100644 backend/internal/model/publish_draft/domain/usecase/usecase.go create mode 100644 docs/product/README.md create mode 100644 docs/product/haixun-console/README.md create mode 100644 docs/product/haixun-console/plan.md create mode 100644 docs/product/haixun-console/requirements.md create mode 100644 docs/product/haixun-console/spec.md create mode 100644 docs/product/haixun-console/tasks/INDEX.md create mode 100644 docs/product/haixun-console/tasks/T001-scaffold-apps-web.md create mode 100644 docs/product/haixun-console/tasks/T002-design-tokens.md create mode 100644 docs/product/haixun-console/tasks/T003-ui-primitives.md create mode 100644 docs/product/haixun-console/tasks/T004-app-shell.md create mode 100644 docs/product/haixun-console/tasks/T005-router-placeholders.md create mode 100644 docs/product/haixun-console/tasks/T006-mock-auth.md create mode 100644 docs/product/haixun-console/tasks/T007-repo-interfaces.md create mode 100644 docs/product/haixun-console/tasks/T008-mock-repos-seed.md create mode 100644 docs/product/haixun-console/tasks/T009-crew-page.md create mode 100644 docs/product/haixun-console/tasks/T010-studio-list.md create mode 100644 docs/product/haixun-console/tasks/T011-wizard-shell.md create mode 100644 docs/product/haixun-console/tasks/T012-wizard-topic-crew.md create mode 100644 docs/product/haixun-console/tasks/T013-wizard-script-steps.md create mode 100644 docs/product/haixun-console/tasks/T014-wizard-preview-submit.md create mode 100644 docs/product/haixun-console/tasks/T015-outbox-ui.md create mode 100644 docs/product/haixun-console/tasks/T016-outbox-simulate.md create mode 100644 docs/product/haixun-console/tasks/T017-mock-jobs.md create mode 100644 docs/product/haixun-console/tasks/T018-notifications.md create mode 100644 docs/product/haixun-console/tasks/T019-today-page.md create mode 100644 docs/product/haixun-console/tasks/T020-scout-settings.md create mode 100644 docs/product/haixun-console/tasks/T021-seed-example-play.md create mode 100644 docs/product/haixun-console/tasks/T022-rwd-smoke.md rename .dockerignore => old/.dockerignore (100%) create mode 100644 old/AGENTS.md rename Makefile => old/Makefile (100%) create mode 100644 old/README.md rename ai_threads_auto_account_ops.md => old/ai_threads_auto_account_ops.md (100%) rename {backend => old/backend}/Dockerfile (100%) rename {backend => old/backend}/Makefile (100%) rename {backend => old/backend}/README.md (100%) rename {backend => old/backend}/cmd/tool/main.go (100%) rename {backend => old/backend}/cmd/worker/main.go (100%) rename {backend => old/backend}/dev-console/README.md (100%) rename {backend => old/backend}/dev-console/index.html (100%) rename {backend => old/backend}/dev-console/package-lock.json (100%) rename {backend => old/backend}/dev-console/package.json (100%) rename {backend => old/backend}/dev-console/vite.config.ts (100%) rename {backend => old/backend}/docs/job-system-plan.md (100%) rename {backend => old/backend}/docs/threads-automation-gap-plan.md (100%) rename {backend => old/backend}/etc/gateway.prod.yaml (100%) rename {backend => old/backend}/etc/gateway.worker.prod.yaml (100%) rename {backend => old/backend}/etc/gateway.worker.yaml (100%) rename {backend => old/backend}/etc/gateway.yaml (100%) rename {backend => old/backend}/gateway.go (100%) rename {backend => old/backend}/generate/api/ai.api (100%) rename {backend => old/backend}/generate/api/auth.api (78%) rename {backend => old/backend}/generate/api/brand.api (100%) rename {backend => old/backend}/generate/api/common.api (100%) create mode 100644 old/backend/generate/api/copy_mission.api rename {backend => old/backend}/generate/api/gateway.api (100%) rename {backend => old/backend}/generate/api/job.api (100%) rename {backend => old/backend}/generate/api/member.api (100%) rename {backend => old/backend}/generate/api/normal.api (100%) rename {backend => old/backend}/generate/api/permission.api (100%) rename {backend => old/backend}/generate/api/persona.api (100%) rename {backend => old/backend}/generate/api/placement_topic.api (100%) rename {backend => old/backend}/generate/api/setting.api (100%) rename {backend => old/backend}/generate/api/threads_account.api (95%) rename {backend => old/backend}/generate/api/worker_internal.api (100%) rename {backend => old/backend}/generate/goctl/api/handler.tpl (100%) rename {backend => old/backend}/go.mod (100%) rename {backend => old/backend}/go.sum (100%) rename {backend => old/backend}/internal/bootstrap/admin.go (100%) rename {backend => old/backend}/internal/bootstrap/admin_test.go (92%) rename {backend => old/backend}/internal/bootstrap/envfile.go (100%) rename {backend => old/backend}/internal/bootstrap/envfile_test.go (100%) rename {backend => old/backend}/internal/bootstrap/init.go (98%) rename {backend => old/backend}/internal/config/config.go (100%) rename {backend => old/backend}/internal/handler/ai/chat_handler.go (100%) rename {backend => old/backend}/internal/handler/ai/chat_stream_handler.go (100%) rename {backend => old/backend}/internal/handler/ai/islander_chat_stream_handler.go (100%) rename {backend => old/backend}/internal/handler/ai/list_ai_provider_models_handler.go (100%) rename {backend => old/backend}/internal/handler/ai/list_ai_providers_handler.go (100%) rename {backend => old/backend}/internal/handler/auth/login_handler.go (100%) rename {backend => old/backend}/internal/handler/auth/logout_handler.go (100%) rename {backend => old/backend}/internal/handler/auth/refresh_handler.go (100%) rename {backend => old/backend}/internal/handler/auth/register_handler.go (100%) create mode 100644 old/backend/internal/handler/auth/resend_verification_email_handler.go rename backend/internal/handler/copy_mission/get_copy_mission_handler.go => old/backend/internal/handler/auth/verify_email_handler.go (68%) rename {backend => old/backend}/internal/handler/brand/create_brand_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/create_brand_product_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/delete_brand_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/delete_brand_product_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/expand_knowledge_graph_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/generate_brand_content_matrix_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/generate_outreach_drafts_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/get_brand_content_matrix_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/get_brand_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/get_brand_scan_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/get_knowledge_graph_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/list_brand_products_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/list_brand_scan_posts_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/list_brands_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/patch_knowledge_graph_nodes_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/patch_scan_post_outreach_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/publish_outreach_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/start_brand_scan_job_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/update_brand_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/update_brand_product_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/update_outreach_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/brand/upsert_brand_scan_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/copy_mission/inspire_copy_mission_handler.go (100%) rename {backend => old/backend}/internal/handler/job/ack_worker_job_cancel_handler.go (100%) rename {backend => old/backend}/internal/handler/job/analyze_style8_d_from_worker_handler.go (100%) rename {backend => old/backend}/internal/handler/job/cancel_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/check_worker_job_cancel_handler.go (100%) rename {backend => old/backend}/internal/handler/job/claim_worker_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/complete_worker_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/create_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/create_job_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/job/delete_job_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/job/disable_job_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/job/enable_job_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/job/fail_worker_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/get_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/get_job_template_handler.go (100%) rename {backend => old/backend}/internal/handler/job/get_worker_threads_account_session_handler.go (100%) rename {backend => old/backend}/internal/handler/job/list_job_events_handler.go (100%) rename {backend => old/backend}/internal/handler/job/list_job_schedules_handler.go (100%) rename {backend => old/backend}/internal/handler/job/list_job_templates_handler.go (100%) rename {backend => old/backend}/internal/handler/job/list_jobs_handler.go (100%) rename {backend => old/backend}/internal/handler/job/refresh_worker_job_lock_handler.go (100%) rename {backend => old/backend}/internal/handler/job/retry_job_handler.go (100%) rename {backend => old/backend}/internal/handler/job/store_persona_style_profile_from_worker_handler.go (100%) rename {backend => old/backend}/internal/handler/job/update_job_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/job/update_worker_job_progress_handler.go (100%) rename {backend => old/backend}/internal/handler/job/upsert_job_template_handler.go (100%) rename {backend => old/backend}/internal/handler/member/get_member_ai_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/member/get_member_capabilities_handler.go (100%) rename {backend => old/backend}/internal/handler/member/get_member_me_handler.go (100%) rename {backend => old/backend}/internal/handler/member/get_member_placement_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/member/list_member_ai_provider_models_handler.go (100%) rename {backend => old/backend}/internal/handler/member/list_members_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_ai_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_me_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_password_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_placement_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_profile_handler.go (100%) rename {backend => old/backend}/internal/handler/member/update_member_roles_handler.go (100%) rename {backend => old/backend}/internal/handler/member/upload_member_avatar_handler.go (100%) rename {backend => old/backend}/internal/handler/normal/health_handler.go (100%) rename {backend => old/backend}/internal/handler/permission/get_me_permissions_handler.go (100%) rename {backend => old/backend}/internal/handler/permission/get_permission_catalog_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/create_content_plan_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/create_feedback_event_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/create_formula_pool_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/create_knowledge_source_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/create_persona_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/delete_persona_copy_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/delete_persona_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/delete_style_preset_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/generate_from_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/generate_persona_copy_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/generate_persona_topic_matrix_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/get_persona_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_content_plans_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_formula_pools_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_knowledge_chunks_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_knowledge_sources_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_persona_content_inbox_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_persona_copy_drafts_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_persona_viral_scan_posts_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_personas_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_style_presets_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/list_topic_candidates_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/prune_persona_copy_drafts_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/publish_persona_copy_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/schedule_persona_copy_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/schedule_persona_drafts_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_formula_draft_job_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_rewrite_draft_job_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_style_analysis_from_text_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_style_analysis_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_topic_matrix_job_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/start_persona_viral_scan_job_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/update_content_plan_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/update_persona_copy_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/update_persona_handler.go (100%) rename {backend => old/backend}/internal/handler/persona/upsert_style_preset_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/create_placement_topic_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/delete_placement_topic_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/expand_placement_topic_graph_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/get_placement_topic_graph_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/get_placement_topic_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/list_placement_topics_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/start_placement_topic_outreach_draft_job_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/update_placement_topic_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/update_placement_topic_outreach_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go (100%) rename {backend => old/backend}/internal/handler/register_extra.go (72%) rename {backend => old/backend}/internal/handler/routes.go (89%) rename {backend => old/backend}/internal/handler/setting/delete_setting_handler.go (100%) rename {backend => old/backend}/internal/handler/setting/get_setting_handler.go (100%) rename {backend => old/backend}/internal/handler/setting/list_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/setting/upsert_setting_handler.go (100%) rename {backend => old/backend}/internal/handler/static/extension_download.go (100%) create mode 100644 old/backend/internal/handler/static/public_asset.go rename {backend => old/backend}/internal/handler/threads_account/activate_threads_account_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/analyze_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/cancel_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/create_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/create_threads_account_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/delete_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/delete_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/delete_threads_account_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/disconnect_threads_api_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/generate_own_post_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/generate_own_post_reply_draft_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_publish_dashboard_summary_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_publish_guard_policy_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_publish_inventory_policy_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_publish_slot_insights_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_threads_account_ai_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_threads_account_connection_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_threads_account_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_threads_diagnostics_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/get_threads_oauth_config_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/import_own_post_to_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/import_threads_account_session_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_content_formulas_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_mention_inbox_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_own_post_formulas_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_publish_alerts_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_publish_queue_events_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_publish_queue_items_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_publish_queue_range_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_threads_account_ai_provider_models_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_threads_accounts_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_threads_oauth_logs_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_threads_post_performance_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/list_threads_publish_health_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/patch_content_formula_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/patch_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/publish_mention_reply_handler.go (60%) create mode 100644 old/backend/internal/handler/threads_account/publish_threads_reply_handler.go create mode 100644 old/backend/internal/handler/threads_account/reply_multipart.go rename {backend => old/backend}/internal/handler/threads_account/resume_publish_guard_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/retry_publish_queue_item_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/run_threads_api_playground_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/run_threads_api_smoke_test_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/search_content_formula_posts_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/start_threads_oauth_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/sync_mention_inbox_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/threads_oauth_callback_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/update_threads_account_ai_settings_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/update_threads_account_connection_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/update_threads_account_handler.go (100%) create mode 100644 old/backend/internal/handler/threads_account/upload_publish_attachment_handler.go rename {backend => old/backend}/internal/handler/threads_account/upsert_publish_guard_policy_handler.go (100%) rename {backend => old/backend}/internal/handler/threads_account/upsert_publish_inventory_policy_handler.go (100%) rename {backend => old/backend}/internal/library/authctx/context.go (100%) rename {backend => old/backend}/internal/library/brave/breaker.go (100%) rename {backend => old/backend}/internal/library/brave/client.go (100%) rename {backend => old/backend}/internal/library/clock/clock.go (100%) rename {backend => old/backend}/internal/library/clock/clock_test.go (100%) rename {backend => old/backend}/internal/library/copymission/matrix_lock.go (100%) rename {backend => old/backend}/internal/library/copyvoice/human.go (100%) rename {backend => old/backend}/internal/library/copyvoice/human_test.go (100%) rename {backend => old/backend}/internal/library/crypto/aesgcm.go (100%) rename {backend => old/backend}/internal/library/errors/code/types.go (100%) rename {backend => old/backend}/internal/library/errors/errors.go (100%) rename {backend => old/backend}/internal/library/exa/client.go (100%) rename {backend => old/backend}/internal/library/exa/client_test.go (100%) rename {backend => old/backend}/internal/library/formula/analyze_pasted.go (100%) rename {backend => old/backend}/internal/library/formula/generate_from_formula.go (100%) rename {backend => old/backend}/internal/library/formula/generate_from_formula_test.go (100%) rename {backend => old/backend}/internal/library/formula/jsonflex.go (100%) rename {backend => old/backend}/internal/library/formula/jsonflex_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/bootstrap.go (100%) rename {backend => old/backend}/internal/library/knowledge/bootstrap_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/brave_collect.go (100%) rename {backend => old/backend}/internal/library/knowledge/brave_collect_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/expand_strategy.go (100%) rename {backend => old/backend}/internal/library/knowledge/expand_strategy_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/graph.go (100%) rename {backend => old/backend}/internal/library/knowledge/graph_rescore.go (100%) rename {backend => old/backend}/internal/library/knowledge/graph_selection_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_input.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_phrase.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_phrase_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_rank.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_rank_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_resolve.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_resolve_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_search.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_search_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_tags.go (100%) rename {backend => old/backend}/internal/library/knowledge/patrol_tags_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/quality.go (100%) rename {backend => old/backend}/internal/library/knowledge/quality_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/queries.go (100%) rename {backend => old/backend}/internal/library/knowledge/queries_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/research_map_fit.go (100%) rename {backend => old/backend}/internal/library/knowledge/search_keyword_shape.go (100%) rename {backend => old/backend}/internal/library/knowledge/search_keyword_shape_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/semantic_fit.go (100%) rename {backend => old/backend}/internal/library/knowledge/semantic_fit_test.go (100%) rename {backend => old/backend}/internal/library/knowledge/synth.go (100%) rename {backend => old/backend}/internal/library/knowledge/synth_test.go (100%) rename {backend => old/backend}/internal/library/llmjson/sanitize.go (100%) rename {backend => old/backend}/internal/library/llmjson/sanitize_test.go (100%) rename {backend => old/backend}/internal/library/mailer/smtp.go (100%) rename {backend => old/backend}/internal/library/matrix/generate.go (100%) rename {backend => old/backend}/internal/library/matrix/generate_test.go (100%) rename {backend => old/backend}/internal/library/matrix/reference.go (100%) rename backend/internal/library/matrix/copy_generate.go => old/backend/internal/library/matrix/research_map.go (59%) rename {backend => old/backend}/internal/library/matrix/samples.go (100%) rename {backend => old/backend}/internal/library/mongo/brand_scope.go (100%) rename {backend => old/backend}/internal/library/mongo/client.go (100%) rename {backend => old/backend}/internal/library/mongo/ensure_index.go (100%) rename {backend => old/backend}/internal/library/outreach/draft_context.go (100%) rename {backend => old/backend}/internal/library/outreach/draft_format.go (100%) rename {backend => old/backend}/internal/library/outreach/draft_format_test.go (100%) rename {backend => old/backend}/internal/library/outreach/generate.go (100%) rename {backend => old/backend}/internal/library/outreach/generate_draft.go (100%) rename {backend => old/backend}/internal/library/outreach/generate_test.go (100%) rename {backend => old/backend}/internal/library/outreach/persona_voice.go (100%) rename {backend => old/backend}/internal/library/outreach/persona_voice_test.go (100%) rename {backend => old/backend}/internal/library/outreach/previous_drafts.go (100%) rename {backend => old/backend}/internal/library/outreach/previous_drafts_test.go (100%) rename {backend => old/backend}/internal/library/ownpost/post_formula.go (100%) rename {backend => old/backend}/internal/library/ownpost/post_formula_test.go (100%) rename {backend => old/backend}/internal/library/ownpost/reply_draft.go (100%) rename {backend => old/backend}/internal/library/ownpost/reply_draft_test.go (100%) rename {backend => old/backend}/internal/library/permmatch/match.go (100%) rename {backend => old/backend}/internal/library/permmatch/match_test.go (100%) rename {backend => old/backend}/internal/library/personacopy/formula_draft.go (100%) rename {backend => old/backend}/internal/library/personacopy/rewrite_draft.go (100%) rename {backend => old/backend}/internal/library/personacopy/topic_matrix.go (100%) rename {backend => old/backend}/internal/library/placement/ai_generate.go (100%) rename {backend => old/backend}/internal/library/placement/capabilities.go (100%) rename {backend => old/backend}/internal/library/placement/context.go (100%) rename {backend => old/backend}/internal/library/placement/context_api_only_test.go (100%) rename {backend => old/backend}/internal/library/placement/context_prompt.go (100%) rename {backend => old/backend}/internal/library/placement/context_prompt_test.go (100%) rename {backend => old/backend}/internal/library/placement/crawler_exec.go (100%) rename {backend => old/backend}/internal/library/placement/crawler_exec_test.go (100%) rename {backend => old/backend}/internal/library/placement/crawler_polite.go (100%) rename {backend => old/backend}/internal/library/placement/discover.go (100%) rename {backend => old/backend}/internal/library/placement/discover_for_query_test.go (100%) rename {backend => old/backend}/internal/library/placement/discover_route.go (100%) rename {backend => old/backend}/internal/library/placement/discover_route_test.go (100%) rename {backend => old/backend}/internal/library/placement/dual_track.go (100%) rename {backend => old/backend}/internal/library/placement/effective_strategy.go (100%) rename {backend => old/backend}/internal/library/placement/effective_strategy_test.go (100%) rename {backend => old/backend}/internal/library/placement/exclusion.go (100%) rename {backend => old/backend}/internal/library/placement/external_id.go (100%) rename {backend => old/backend}/internal/library/placement/external_id_test.go (100%) rename {backend => old/backend}/internal/library/placement/filter.go (100%) rename {backend => old/backend}/internal/library/placement/filter_test.go (100%) rename {backend => old/backend}/internal/library/placement/media_resolve.go (100%) rename {backend => old/backend}/internal/library/placement/parse_threads.go (100%) rename {backend => old/backend}/internal/library/placement/patrol_queries.go (100%) rename {backend => old/backend}/internal/library/placement/patrol_queries_test.go (100%) rename {backend => old/backend}/internal/library/placement/post_relevance.go (100%) rename {backend => old/backend}/internal/library/placement/post_relevance_test.go (100%) rename {backend => old/backend}/internal/library/placement/product_catalog.go (100%) rename {backend => old/backend}/internal/library/placement/product_context.go (100%) rename {backend => old/backend}/internal/library/placement/product_context_placement_url_test.go (100%) rename {backend => old/backend}/internal/library/placement/product_match.go (100%) rename {backend => old/backend}/internal/library/placement/product_match_test.go (100%) rename {backend => old/backend}/internal/library/placement/query_build.go (100%) rename {backend => old/backend}/internal/library/placement/query_build_test.go (100%) rename {backend => old/backend}/internal/library/placement/recency.go (100%) rename {backend => old/backend}/internal/library/placement/recency_cascade.go (100%) rename {backend => old/backend}/internal/library/placement/recency_cascade_test.go (100%) rename {backend => old/backend}/internal/library/placement/research_map.go (100%) rename {backend => old/backend}/internal/library/placement/research_map_test.go (100%) rename {backend => old/backend}/internal/library/placement/resolve_media_exec.go (100%) rename {backend => old/backend}/internal/library/placement/resolve_reply_target.go (100%) rename {backend => old/backend}/internal/library/placement/resolve_reply_target_test.go (100%) rename {backend => old/backend}/internal/library/placement/scrape_replies.go (100%) rename {backend => old/backend}/internal/library/placement/scrape_replies_test.go (100%) rename {backend => old/backend}/internal/library/placement/source_mode.go (100%) rename {backend => old/backend}/internal/library/placement/source_mode_test.go (100%) rename {backend => old/backend}/internal/library/placement/threads_api.go (100%) rename {backend => old/backend}/internal/library/placement/topic_fit.go (100%) rename {backend => old/backend}/internal/library/prompt/compose.go (90%) rename {backend => old/backend}/internal/library/prompt/compose_test.go (100%) rename {backend => old/backend}/internal/library/prompt/errors.go (100%) rename {backend => old/backend}/internal/library/prompt/files/ai.chat.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/ai.islander.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph.queries.json (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph.supplemental.md (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph.user.md (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph_llm.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/knowledge_graph_llm.user.md (100%) rename {backend => old/backend}/internal/library/prompt/files/matrix_placement.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/matrix_placement.user.md (100%) rename {backend => old/backend}/internal/library/prompt/files/outreach_placement.system.md (100%) rename {backend => old/backend}/internal/library/prompt/files/outreach_placement.user.md (100%) rename {backend => old/backend}/internal/library/prompt/files/overlay.md (100%) rename {backend => old/backend}/internal/library/prompt/files/style8d.schema.md (100%) rename {backend => old/backend}/internal/library/prompt/files/style8d.system.md (100%) rename {backend => old/backend}/internal/library/prompt/registry.go (83%) rename {backend => old/backend}/internal/library/publishschedule/slots.go (100%) rename {backend => old/backend}/internal/library/publishschedule/slots_test.go (100%) rename {backend => old/backend}/internal/library/ratelimit/token_bucket.go (100%) rename {backend => old/backend}/internal/library/redact/redact.go (100%) rename {backend => old/backend}/internal/library/redact/redact_test.go (100%) rename {backend => old/backend}/internal/library/redis/client.go (100%) create mode 100644 old/backend/internal/library/storage/asset_ref.go create mode 100644 old/backend/internal/library/storage/asset_ref_test.go create mode 100644 old/backend/internal/library/storage/ephemeral_asset.go create mode 100644 old/backend/internal/library/storage/ephemeral_asset_test.go create mode 100644 old/backend/internal/library/storage/public_asset.go create mode 100644 old/backend/internal/library/storage/public_asset_test.go create mode 100644 old/backend/internal/library/storage/publish_attachment.go create mode 100644 old/backend/internal/library/storage/publish_attachment_test.go create mode 100644 old/backend/internal/library/storage/s3.go create mode 100644 old/backend/internal/library/storage/s3_public_url_test.go create mode 100644 old/backend/internal/library/storage/threads_image_url.go create mode 100644 old/backend/internal/library/storage/threads_image_url_test.go rename {backend => old/backend}/internal/library/style8d/analyze.go (100%) rename {backend => old/backend}/internal/library/style8d/analyze_test.go (100%) rename {backend => old/backend}/internal/library/style8d/prompt.go (100%) rename {backend => old/backend}/internal/library/style8d/prompt_test.go (100%) rename {backend => old/backend}/internal/library/style8d/runner.go (100%) rename {backend => old/backend}/internal/library/style8d/text_input_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/client.go (76%) create mode 100644 old/backend/internal/library/threadsapi/client_error_test.go rename {backend => old/backend}/internal/library/threadsapi/extended.go (100%) rename {backend => old/backend}/internal/library/threadsapi/fields_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/media.go (100%) rename {backend => old/backend}/internal/library/threadsapi/media_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/oauth.go (100%) rename {backend => old/backend}/internal/library/threadsapi/oauth_permission_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/oauth_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/publish.go (62%) create mode 100644 old/backend/internal/library/threadsapi/publish_media.go rename {backend => old/backend}/internal/library/threadsapi/replies.go (100%) rename {backend => old/backend}/internal/library/threadsapi/replies_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/resolve_media.go (100%) rename {backend => old/backend}/internal/library/threadsapi/resolve_media_hint_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/resolve_media_test.go (100%) rename {backend => old/backend}/internal/library/threadsapi/scopes.go (100%) rename {backend => old/backend}/internal/library/threadsapi/url.go (100%) rename {backend => old/backend}/internal/library/threadspost/format.go (100%) rename {backend => old/backend}/internal/library/threadspost/format_test.go (100%) rename {backend => old/backend}/internal/library/threadspost/limits.go (83%) rename {backend => old/backend}/internal/library/threadspost/limits_test.go (100%) rename {backend => old/backend}/internal/library/threadspost/mimic.go (100%) rename {backend => old/backend}/internal/library/threadspost/mimic_test.go (100%) rename {backend => old/backend}/internal/library/validate/validate.go (100%) rename {backend => old/backend}/internal/library/viral/analyze_viral.go (100%) rename {backend => old/backend}/internal/library/viral/analyze_viral_test.go (100%) rename {backend => old/backend}/internal/library/viral/audience.go (100%) rename {backend => old/backend}/internal/library/viral/discover.go (100%) rename {backend => old/backend}/internal/library/viral/discover_accounts.go (100%) rename {backend => old/backend}/internal/library/viral/discover_accounts_test.go (100%) rename {backend => old/backend}/internal/library/viral/discover_budget_test.go (100%) rename {backend => old/backend}/internal/library/viral/discover_graceful_test.go (100%) rename {backend => old/backend}/internal/library/viral/enrich_accounts.go (100%) rename {backend => old/backend}/internal/library/viral/flexible_json.go (100%) rename {backend => old/backend}/internal/library/viral/free_trends.go (100%) rename {backend => old/backend}/internal/library/viral/known_accounts.go (100%) rename {backend => old/backend}/internal/library/viral/merge_accounts.go (100%) rename {backend => old/backend}/internal/library/viral/mission_inspire.go (100%) rename {backend => old/backend}/internal/library/viral/mission_inspire_collect.go (100%) rename {backend => old/backend}/internal/library/viral/mission_inspire_test.go (100%) rename {backend => old/backend}/internal/library/viral/mission_research_map.go (100%) rename {backend => old/backend}/internal/library/viral/mission_research_map_test.go (100%) rename {backend => old/backend}/internal/library/viral/mission_topic.go (100%) rename {backend => old/backend}/internal/library/viral/quality.go (100%) rename {backend => old/backend}/internal/library/viral/quality_test.go (100%) rename {backend => old/backend}/internal/library/viral/reference_accounts.go (100%) rename {backend => old/backend}/internal/library/viral/reference_accounts_fallback_test.go (100%) rename {backend => old/backend}/internal/library/viral/reference_accounts_test.go (100%) rename {backend => old/backend}/internal/library/viral/replicate.go (100%) rename {backend => old/backend}/internal/library/viral/research_map.go (100%) rename {backend => old/backend}/internal/library/viral/score.go (100%) rename {backend => old/backend}/internal/library/viral/search_tags.go (100%) rename {backend => old/backend}/internal/library/viral/search_tags_test.go (100%) rename {backend => old/backend}/internal/library/viral/tags.go (100%) rename {backend => old/backend}/internal/library/viral/tags_test.go (100%) rename {backend => old/backend}/internal/library/webpage/digest.go (100%) rename {backend => old/backend}/internal/library/webpage/digest_test.go (100%) rename {backend => old/backend}/internal/library/webpage/format.go (100%) rename {backend => old/backend}/internal/library/websearch/client.go (100%) rename {backend => old/backend}/internal/logic/ai/actor.go (100%) rename {backend => old/backend}/internal/logic/ai/chat_logic.go (100%) rename {backend => old/backend}/internal/logic/ai/chat_stream_logic.go (100%) rename {backend => old/backend}/internal/logic/ai/credential.go (100%) rename {backend => old/backend}/internal/logic/ai/islander_chat_stream_logic.go (100%) rename {backend => old/backend}/internal/logic/ai/list_ai_provider_models_logic.go (100%) rename {backend => old/backend}/internal/logic/ai/list_ai_providers_logic.go (100%) rename {backend => old/backend}/internal/logic/ai/mapper.go (100%) create mode 100644 old/backend/internal/logic/auth/email_verification.go rename {backend => old/backend}/internal/logic/auth/login_logic.go (72%) rename {backend => old/backend}/internal/logic/auth/logout_logic.go (100%) rename {backend => old/backend}/internal/logic/auth/mapper.go (100%) rename {backend => old/backend}/internal/logic/auth/refresh_logic.go (100%) rename {backend => old/backend}/internal/logic/auth/register_logic.go (50%) create mode 100644 old/backend/internal/logic/auth/resend_verification_email_logic.go create mode 100644 old/backend/internal/logic/auth/verify_email_logic.go rename {backend => old/backend}/internal/logic/authz/admin.go (100%) rename {backend => old/backend}/internal/logic/brand/actor.go (100%) rename {backend => old/backend}/internal/logic/brand/create_brand_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/create_brand_product_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/delete_brand_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/delete_brand_product_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/expand_knowledge_graph_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/generate_brand_content_matrix_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/generate_outreach_drafts_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/get_brand_content_matrix_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/get_brand_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/get_brand_scan_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/get_knowledge_graph_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/list_brand_products_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/list_brand_scan_posts_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/list_brands_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/mapper.go (100%) rename {backend => old/backend}/internal/logic/brand/patch_knowledge_graph_nodes_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/patch_scan_post_outreach_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/publish_outreach_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/start_brand_scan_job_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/update_brand_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/update_brand_product_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/update_outreach_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/brand/upsert_brand_scan_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/copy_mission/actor.go (100%) rename {backend => old/backend}/internal/logic/copy_mission/inspire_copy_mission_logic.go (100%) rename {backend => old/backend}/internal/logic/job/ack_worker_job_cancel_logic.go (100%) rename {backend => old/backend}/internal/logic/job/actor.go (100%) rename {backend => old/backend}/internal/logic/job/analyze_style8_d_from_worker_logic.go (100%) rename {backend => old/backend}/internal/logic/job/cancel_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/check_worker_job_cancel_logic.go (100%) rename {backend => old/backend}/internal/logic/job/claim_worker_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/complete_worker_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/create_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/create_job_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/job/delete_job_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/job/disable_job_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/job/enable_job_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/job/fail_worker_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/get_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/get_job_template_logic.go (100%) rename {backend => old/backend}/internal/logic/job/get_worker_threads_account_session_logic.go (100%) rename {backend => old/backend}/internal/logic/job/list_job_events_logic.go (100%) rename {backend => old/backend}/internal/logic/job/list_job_schedules_logic.go (100%) rename {backend => old/backend}/internal/logic/job/list_job_templates_logic.go (100%) rename {backend => old/backend}/internal/logic/job/list_jobs_logic.go (100%) rename {backend => old/backend}/internal/logic/job/mapper.go (100%) rename {backend => old/backend}/internal/logic/job/ownership.go (100%) rename {backend => old/backend}/internal/logic/job/ownership_test.go (100%) rename {backend => old/backend}/internal/logic/job/refresh_worker_job_lock_logic.go (100%) rename {backend => old/backend}/internal/logic/job/retry_job_logic.go (100%) rename {backend => old/backend}/internal/logic/job/store_persona_style_profile_from_worker_logic.go (100%) rename {backend => old/backend}/internal/logic/job/update_job_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/job/update_worker_job_progress_logic.go (100%) rename {backend => old/backend}/internal/logic/job/upsert_job_template_logic.go (100%) rename {backend => old/backend}/internal/logic/job/worker_helpers.go (100%) rename {backend => old/backend}/internal/logic/member/ai_settings_mapper.go (100%) create mode 100644 old/backend/internal/logic/member/avatar_ref.go rename {backend => old/backend}/internal/logic/member/get_member_ai_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/member/get_member_capabilities_logic.go (100%) rename {backend => old/backend}/internal/logic/member/get_member_me_logic.go (90%) rename {backend => old/backend}/internal/logic/member/get_member_placement_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/member/list_member_ai_provider_models_logic.go (100%) rename {backend => old/backend}/internal/logic/member/list_members_logic.go (93%) rename {backend => old/backend}/internal/logic/member/mapper.go (100%) rename {backend => old/backend}/internal/logic/member/update_member_ai_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/member/update_member_me_logic.go (88%) rename {backend => old/backend}/internal/logic/member/update_member_password_logic.go (100%) rename {backend => old/backend}/internal/logic/member/update_member_placement_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/member/update_member_profile_logic.go (90%) rename {backend => old/backend}/internal/logic/member/update_member_roles_logic.go (100%) rename {backend => old/backend}/internal/logic/member/upload_member_avatar_logic.go (89%) rename {backend => old/backend}/internal/logic/normal/health_logic.go (100%) rename {backend => old/backend}/internal/logic/permission/get_me_permissions_logic.go (100%) rename {backend => old/backend}/internal/logic/permission/get_permission_catalog_logic.go (100%) rename {backend => old/backend}/internal/logic/permission/mapper.go (100%) rename {backend => old/backend}/internal/logic/persona/actor.go (100%) rename {backend => old/backend}/internal/logic/persona/content_ops_mapper.go (100%) rename {backend => old/backend}/internal/logic/persona/copy_draft_mapper.go (100%) rename {backend => old/backend}/internal/logic/persona/create_content_plan_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/create_feedback_event_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/create_formula_pool_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/create_knowledge_source_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/create_persona_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/delete_persona_copy_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/delete_persona_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/delete_style_preset_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/generate_from_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/generate_persona_copy_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/generate_persona_topic_matrix_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/get_persona_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_content_plans_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_formula_pools_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_knowledge_chunks_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_knowledge_sources_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_persona_content_inbox_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_persona_copy_drafts_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_persona_viral_scan_posts_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_personas_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_style_presets_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/list_topic_candidates_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/mapper.go (100%) rename {backend => old/backend}/internal/logic/persona/prune_persona_copy_drafts_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/publish_persona_copy_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/schedule_persona_copy_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/schedule_persona_drafts_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_formula_draft_job_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_rewrite_draft_job_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_style_analysis_from_text_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_style_analysis_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_topic_matrix_job_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/start_persona_viral_scan_job_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/update_content_plan_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/update_persona_copy_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/update_persona_logic.go (100%) rename {backend => old/backend}/internal/logic/persona/upsert_style_preset_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/actor.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/batch_delete_placement_topic_scan_posts_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/create_placement_topic_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/delete_placement_topic_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/delete_placement_topic_scan_post_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/expand_placement_topic_graph_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/generate_placement_topic_content_matrix_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/get_placement_topic_content_matrix_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/get_placement_topic_graph_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/get_placement_topic_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/get_placement_topic_scan_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/list_placement_topic_scan_posts_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/list_placement_topics_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/mapper.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/patch_placement_topic_graph_nodes_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/patch_placement_topic_scan_post_outreach_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/publish_placement_topic_outreach_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/scope.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/start_placement_topic_outreach_draft_job_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/update_placement_topic_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/update_placement_topic_outreach_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/placement_topic/upsert_placement_topic_scan_schedule_logic.go (100%) rename {backend => old/backend}/internal/logic/setting/authz.go (100%) rename {backend => old/backend}/internal/logic/setting/delete_setting_logic.go (100%) rename {backend => old/backend}/internal/logic/setting/get_setting_logic.go (100%) rename {backend => old/backend}/internal/logic/setting/list_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/setting/mapper.go (100%) rename {backend => old/backend}/internal/logic/setting/upsert_setting_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/activate_threads_account_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/actor.go (100%) rename {backend => old/backend}/internal/logic/threads_account/analyze_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/cancel_publish_queue_item_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/content_formula_mapper.go (100%) rename {backend => old/backend}/internal/logic/threads_account/create_publish_queue_item_logic.go (94%) rename {backend => old/backend}/internal/logic/threads_account/create_threads_account_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/delete_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/delete_publish_queue_item_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/delete_threads_account_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/disconnect_threads_api_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/generate_own_post_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/generate_own_post_reply_draft_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_publish_dashboard_summary_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_publish_guard_policy_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_publish_inventory_policy_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_publish_queue_item_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_publish_slot_insights_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_threads_account_ai_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_threads_account_connection_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_threads_account_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_threads_diagnostics_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/get_threads_oauth_config_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/import_own_post_to_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/import_threads_account_session_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_content_formulas_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_mention_inbox_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_own_post_formulas_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_publish_alerts_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_publish_queue_events_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_publish_queue_items_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_publish_queue_range_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_threads_account_ai_provider_models_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_threads_accounts_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_threads_oauth_logs_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_threads_post_performance_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/list_threads_publish_health_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/mapper.go (99%) rename {backend => old/backend}/internal/logic/threads_account/mention_inbox_mapper.go (100%) rename {backend => old/backend}/internal/logic/threads_account/own_post_formula_mapper.go (100%) rename {backend => old/backend}/internal/logic/threads_account/patch_content_formula_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/patch_publish_queue_item_logic.go (94%) create mode 100644 old/backend/internal/logic/threads_account/publish_attachment.go rename {backend => old/backend}/internal/logic/threads_account/publish_mention_reply_logic.go (76%) create mode 100644 old/backend/internal/logic/threads_account/publish_threads_reply_logic.go create mode 100644 old/backend/internal/logic/threads_account/reply_image.go rename {backend => old/backend}/internal/logic/threads_account/resume_publish_guard_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/retry_publish_queue_item_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/run_threads_api_playground_logic.go (75%) rename {backend => old/backend}/internal/logic/threads_account/run_threads_api_smoke_test_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/search_content_formula_posts_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/start_threads_oauth_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/sync_mention_inbox_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/threads_oauth_callback_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/update_threads_account_ai_settings_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/update_threads_account_connection_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/update_threads_account_logic.go (100%) create mode 100644 old/backend/internal/logic/threads_account/upload_publish_attachment_logic.go rename {backend => old/backend}/internal/logic/threads_account/upsert_publish_guard_policy_logic.go (100%) rename {backend => old/backend}/internal/logic/threads_account/upsert_publish_inventory_policy_logic.go (100%) rename {backend => old/backend}/internal/middleware/auth.go (100%) rename {backend => old/backend}/internal/middleware/auth_test.go (100%) rename {backend => old/backend}/internal/middleware/authjwt_middleware.go (100%) create mode 100644 old/backend/internal/middleware/emailverified_middleware.go create mode 100644 old/backend/internal/middleware/emailverified_middleware_test.go rename {backend => old/backend}/internal/middleware/memberauth_middleware.go (100%) rename {backend => old/backend}/internal/middleware/permissionrbac_middleware.go (100%) rename {backend => old/backend}/internal/middleware/workersecret_middleware.go (100%) rename {backend => old/backend}/internal/model/ai/domain/enum/provider.go (100%) rename {backend => old/backend}/internal/model/ai/domain/usecase/ai.go (100%) rename {backend => old/backend}/internal/model/ai/provider/openai_compatible.go (100%) rename {backend => old/backend}/internal/model/ai/provider/openai_compatible_test.go (100%) rename {backend => old/backend}/internal/model/ai/usecase/provider_map.go (100%) rename {backend => old/backend}/internal/model/ai/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/auth/domain/repository/token_revoke.go (100%) rename {backend => old/backend}/internal/model/auth/domain/usecase/token.go (100%) rename {backend => old/backend}/internal/model/auth/repository/token_revoke_redis.go (100%) rename {backend => old/backend}/internal/model/auth/usecase/token.go (100%) rename {backend => old/backend}/internal/model/brand/domain/entity/brand.go (100%) rename {backend => old/backend}/internal/model/brand/domain/entity/product.go (100%) rename {backend => old/backend}/internal/model/brand/domain/entity/research_map.go (100%) rename {backend => old/backend}/internal/model/brand/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/brand/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/brand/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/brand/usecase/products.go (100%) rename {backend => old/backend}/internal/model/brand/usecase/products_placement_url_test.go (100%) rename {backend => old/backend}/internal/model/brand/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_formula/domain/entity/formula.go (100%) rename {backend => old/backend}/internal/model/content_formula/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/content_formula/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_formula/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/content_formula/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_matrix/domain/entity/matrix.go (100%) rename {backend => old/backend}/internal/model/content_matrix/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/content_matrix/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_matrix/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/content_matrix/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_ops/domain/entity/entity.go (100%) rename {backend => old/backend}/internal/model/content_ops/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/content_ops/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/content_ops/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/content_ops/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/copy_draft/domain/entity/draft.go (100%) rename {backend => old/backend}/internal/model/copy_draft/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/copy_draft/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/copy_draft/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/copy_draft/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/copy_mission/domain/entity/mission.go (100%) rename {backend => old/backend}/internal/model/copy_mission/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/copy_mission/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/copy_mission/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/copy_mission/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/job/cron/next_run.go (100%) rename {backend => old/backend}/internal/model/job/cron/next_run_test.go (100%) rename {backend => old/backend}/internal/model/job/domain/entity/event.go (100%) rename {backend => old/backend}/internal/model/job/domain/entity/run.go (100%) rename {backend => old/backend}/internal/model/job/domain/entity/schedule.go (100%) rename {backend => old/backend}/internal/model/job/domain/entity/template.go (100%) rename {backend => old/backend}/internal/model/job/domain/enum/status.go (100%) rename {backend => old/backend}/internal/model/job/domain/repository/event.go (100%) rename {backend => old/backend}/internal/model/job/domain/repository/queue.go (100%) rename {backend => old/backend}/internal/model/job/domain/repository/run.go (100%) rename {backend => old/backend}/internal/model/job/domain/repository/schedule.go (100%) rename {backend => old/backend}/internal/model/job/domain/repository/template.go (100%) rename {backend => old/backend}/internal/model/job/domain/usecase/job.go (93%) rename {backend => old/backend}/internal/model/job/repository/mongo_event.go (100%) rename {backend => old/backend}/internal/model/job/repository/mongo_run.go (100%) rename {backend => old/backend}/internal/model/job/repository/mongo_schedule.go (100%) rename {backend => old/backend}/internal/model/job/repository/mongo_template.go (100%) rename {backend => old/backend}/internal/model/job/repository/redis_queue.go (100%) rename {backend => old/backend}/internal/model/job/repository/redis_queue_test.go (100%) rename {backend => old/backend}/internal/model/job/resume/resume.go (100%) rename {backend => old/backend}/internal/model/job/resume/resume_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/cancel_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/dedupe_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/enqueue_run_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/helpers.go (100%) rename {backend => old/backend}/internal/model/job/usecase/maintenance.go (100%) rename {backend => old/backend}/internal/model/job/usecase/maintenance_stuck_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/maintenance_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/refresh_lock_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/retry_max_attempts_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/retry_resume_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/retry_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/schedule.go (100%) rename {backend => old/backend}/internal/model/job/usecase/schedule_payload.go (100%) rename {backend => old/backend}/internal/model/job/usecase/schedule_payload_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/schedule_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/step_resume.go (100%) rename {backend => old/backend}/internal/model/job/usecase/template.go (100%) rename {backend => old/backend}/internal/model/job/usecase/template_test.go (100%) rename {backend => old/backend}/internal/model/job/usecase/test_mocks.go (100%) rename {backend => old/backend}/internal/model/job/usecase/usecase.go (80%) rename {backend => old/backend}/internal/model/knowledge_graph/domain/entity/graph.go (100%) rename {backend => old/backend}/internal/model/knowledge_graph/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/knowledge_graph/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/knowledge_graph/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/knowledge_graph/usecase/topic_graph.go (100%) rename {backend => old/backend}/internal/model/knowledge_graph/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/member/domain/entity/member.go (93%) rename {backend => old/backend}/internal/model/member/domain/repository/member.go (93%) rename {backend => old/backend}/internal/model/member/domain/usecase/member.go (90%) rename {backend => old/backend}/internal/model/member/repository/mongo.go (86%) rename {backend => old/backend}/internal/model/member/usecase/usecase.go (77%) rename {backend => old/backend}/internal/model/mention_inbox/domain/entity/mention.go (100%) rename {backend => old/backend}/internal/model/mention_inbox/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/mention_inbox/domain/usecase/usecase.go (97%) rename {backend => old/backend}/internal/model/mention_inbox/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/mention_inbox/usecase/usecase.go (96%) rename {backend => old/backend}/internal/model/outreach_draft/domain/entity/draft.go (100%) rename {backend => old/backend}/internal/model/outreach_draft/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/outreach_draft/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/outreach_draft/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/outreach_draft/usecase/errors.go (100%) rename {backend => old/backend}/internal/model/outreach_draft/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/own_post_formula/domain/entity/review.go (100%) rename {backend => old/backend}/internal/model/own_post_formula/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/own_post_formula/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/own_post_formula/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/permission/domain/entity/permission.go (100%) rename {backend => old/backend}/internal/model/permission/domain/repository/permission.go (100%) rename {backend => old/backend}/internal/model/permission/domain/usecase/permission.go (100%) rename {backend => old/backend}/internal/model/permission/repository/mongo_permission.go (100%) rename {backend => old/backend}/internal/model/permission/repository/mongo_role_permission.go (100%) rename {backend => old/backend}/internal/model/permission/repository/object_id.go (100%) rename {backend => old/backend}/internal/model/permission/usecase/usecase.go (96%) rename {backend => old/backend}/internal/model/permission/usecase/usecase_test.go (100%) rename {backend => old/backend}/internal/model/persona/domain/entity/copy_research_map.go (100%) rename {backend => old/backend}/internal/model/persona/domain/entity/persona.go (100%) rename {backend => old/backend}/internal/model/persona/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/persona/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/persona/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/persona/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/placement/usecase/settings.go (100%) rename {backend => old/backend}/internal/model/placement/usecase/settings_dev_mode_test.go (100%) rename {backend => old/backend}/internal/model/placement_topic/domain/entity/topic.go (100%) rename {backend => old/backend}/internal/model/placement_topic/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/placement_topic/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/placement_topic/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/placement_topic/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/domain/entity/analytics.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/usecase/schedule.go (100%) rename {backend => old/backend}/internal/model/publish_analytics/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_guard/domain/entity/policy.go (100%) rename {backend => old/backend}/internal/model/publish_guard/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/publish_guard/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_guard/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/publish_guard/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_guard/usecase/usecase_test.go (100%) rename {backend => old/backend}/internal/model/publish_inventory/domain/entity/policy.go (100%) rename {backend => old/backend}/internal/model/publish_inventory/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/publish_inventory/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_inventory/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/publish_inventory/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_queue/domain/entity/queue_item.go (92%) rename {backend => old/backend}/internal/model/publish_queue/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/publish_queue/domain/usecase/usecase.go (95%) rename {backend => old/backend}/internal/model/publish_queue/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/publish_queue/usecase/usecase.go (87%) rename {backend => old/backend}/internal/model/publish_queue_event/domain/entity/event.go (100%) rename {backend => old/backend}/internal/model/publish_queue_event/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/publish_queue_event/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/publish_queue_event/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/publish_queue_event/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/scan_post/domain/entity/outreach_patch.go (100%) rename {backend => old/backend}/internal/model/scan_post/domain/entity/post.go (100%) rename {backend => old/backend}/internal/model/scan_post/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/scan_post/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/scan_post/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/scan_post/repository/mongo_priority_filter_test.go (100%) rename {backend => old/backend}/internal/model/scan_post/repository/mongo_recent7d_filter_test.go (100%) rename {backend => old/backend}/internal/model/scan_post/repository/mongo_topic_scope_test.go (100%) rename {backend => old/backend}/internal/model/scan_post/usecase/errors.go (100%) rename {backend => old/backend}/internal/model/scan_post/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/setting/domain/entity/setting.go (100%) rename {backend => old/backend}/internal/model/setting/domain/repository/setting.go (100%) rename {backend => old/backend}/internal/model/setting/domain/usecase/setting.go (100%) rename {backend => old/backend}/internal/model/setting/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/setting/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/style_preset/domain/entity/preset.go (100%) rename {backend => old/backend}/internal/model/style_preset/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/style_preset/domain/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/style_preset/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/style_preset/usecase/usecase.go (100%) rename {backend => old/backend}/internal/model/threads_account/domain/entity/account.go (100%) rename {backend => old/backend}/internal/model/threads_account/domain/entity/secrets.go (100%) rename {backend => old/backend}/internal/model/threads_account/domain/repository/repository.go (100%) rename {backend => old/backend}/internal/model/threads_account/domain/usecase/usecase.go (99%) rename {backend => old/backend}/internal/model/threads_account/repository/mongo.go (100%) rename {backend => old/backend}/internal/model/threads_account/repository/oauth_state_redis.go (100%) rename {backend => old/backend}/internal/model/threads_account/repository/secrets_mongo.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/ai_credentials.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/api_playground.go (93%) rename {backend => old/backend}/internal/model/threads_account/usecase/api_smoke.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/connection_prefs_test.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/known_accounts.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/oauth.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/oauth_debug_log.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/placement_context.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/placement_context_test.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/post_performance.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/publish_credentials.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/token_refresh.go (100%) rename {backend => old/backend}/internal/model/threads_account/usecase/usecase.go (100%) rename {backend => old/backend}/internal/response/request.go (100%) rename {backend => old/backend}/internal/response/response.go (100%) rename {backend => old/backend}/internal/svc/job_workers.go (100%) rename {backend => old/backend}/internal/svc/service_context.go (97%) rename {backend => old/backend}/internal/types/types.go (96%) rename {backend => old/backend}/internal/worker/job/crawler_search.go (100%) rename {backend => old/backend}/internal/worker/job/expand_graph.go (100%) rename {backend => old/backend}/internal/worker/job/generate_formula_draft.go (100%) rename {backend => old/backend}/internal/worker/job/generate_outreach_draft.go (100%) rename {backend => old/backend}/internal/worker/job/generate_rewrite_draft.go (100%) rename {backend => old/backend}/internal/worker/job/generate_topic_matrix.go (100%) rename {backend => old/backend}/internal/worker/job/payload_actor.go (100%) rename {backend => old/backend}/internal/worker/job/payload_helpers.go (100%) rename {backend => old/backend}/internal/worker/job/placement_scope.go (100%) rename {backend => old/backend}/internal/worker/job/publish_analytics.go (100%) rename {backend => old/backend}/internal/worker/job/publish_queue_sweeper.go (100%) rename {backend => old/backend}/internal/worker/job/reaper.go (100%) rename {backend => old/backend}/internal/worker/job/refresh_threads_token.go (100%) rename {backend => old/backend}/internal/worker/job/runner.go (100%) rename {backend => old/backend}/internal/worker/job/runner_test.go (100%) rename {backend => old/backend}/internal/worker/job/scan_placement.go (100%) rename {backend => old/backend}/internal/worker/job/scan_viral.go (100%) rename {backend => old/backend}/internal/worker/job/scheduler.go (100%) rename {backend => old/backend}/internal/worker/job/threads_token_sweeper.go (100%) rename {backend => old/backend}/web/Dockerfile (100%) rename {backend => old/backend}/web/index.html (100%) rename {backend => old/backend}/web/package-lock.json (100%) rename {backend => old/backend}/web/package.json (100%) rename {backend => old/backend}/web/src/App.tsx (96%) rename {backend => old/backend}/web/src/api/client.ts (85%) rename {backend => old/backend}/web/src/api/haixun.ts (96%) rename {backend => old/backend}/web/src/auth/AuthContext.tsx (90%) rename {backend => old/backend}/web/src/components/AcIcon.tsx (100%) rename {backend => old/backend}/web/src/components/AuthDecor.tsx (100%) rename {backend => old/backend}/web/src/components/AuthShell.tsx (100%) rename {backend => old/backend}/web/src/components/DevToolsPanel.tsx (100%) rename {backend => old/backend}/web/src/components/ExtensionInstallCard.tsx (100%) rename {backend => old/backend}/web/src/components/HoverHelpTip.tsx (100%) rename {backend => old/backend}/web/src/components/JobMonitor.tsx (100%) rename {backend => old/backend}/web/src/components/Layout.tsx (100%) rename {backend => old/backend}/web/src/components/MentionContextBlock.tsx (100%) rename {backend => old/backend}/web/src/components/MentionProfileLookup.tsx (100%) create mode 100644 old/backend/web/src/components/PublishImageAttach.tsx create mode 100644 old/backend/web/src/components/PublishReplyFeedback.tsx create mode 100644 old/backend/web/src/components/ReplyImageAttach.tsx rename {backend => old/backend}/web/src/components/ThemeToggle.tsx (100%) rename {backend => old/backend}/web/src/components/ui.tsx (100%) rename {backend => old/backend}/web/src/index.css (99%) rename {backend => old/backend}/web/src/lib/acAssets.ts (100%) rename {backend => old/backend}/web/src/lib/activeAccount.ts (100%) create mode 100644 old/backend/web/src/lib/assetUrl.ts create mode 100644 old/backend/web/src/lib/emailVerification.ts rename {backend => old/backend}/web/src/lib/extensionInstall.ts (100%) rename {backend => old/backend}/web/src/lib/extensionSync.ts (100%) rename {backend => old/backend}/web/src/lib/format.ts (100%) rename {backend => old/backend}/web/src/lib/formulaCache.ts (100%) rename {backend => old/backend}/web/src/lib/jobActions.ts (100%) rename {backend => old/backend}/web/src/lib/jobEvents.ts (100%) rename {backend => old/backend}/web/src/lib/jobLabels.ts (89%) rename {backend => old/backend}/web/src/lib/jobStatus.ts (100%) rename {backend => old/backend}/web/src/lib/mentionContext.ts (100%) rename {backend => old/backend}/web/src/lib/mentionInbox.ts (100%) rename {backend => old/backend}/web/src/lib/mentionInboxCache.ts (100%) rename {backend => old/backend}/web/src/lib/mentionLookup.ts (100%) rename {backend => old/backend}/web/src/lib/patrolPersona.ts (100%) rename {backend => old/backend}/web/src/lib/patrolPlan.ts (100%) rename {backend => old/backend}/web/src/lib/postScore.ts (100%) rename {backend => old/backend}/web/src/lib/postStats.ts (100%) rename {backend => old/backend}/web/src/lib/publishPageCache.ts (100%) rename {backend => old/backend}/web/src/lib/publishPersona.ts (100%) create mode 100644 old/backend/web/src/lib/publishReplyFeedback.ts rename {backend => old/backend}/web/src/lib/replyMedia.ts (100%) rename {backend => old/backend}/web/src/lib/replyTree.ts (100%) rename {backend => old/backend}/web/src/lib/safeStorage.ts (100%) rename {backend => old/backend}/web/src/lib/threadsOAuth.ts (100%) rename {backend => old/backend}/web/src/main.tsx (100%) rename {backend => old/backend}/web/src/pages/AccountsPage.tsx (100%) rename {backend => old/backend}/web/src/pages/AuthPage.tsx (85%) rename {backend => old/backend}/web/src/pages/BrandsPage.tsx (100%) rename {backend => old/backend}/web/src/pages/DashboardPage.tsx (100%) create mode 100644 old/backend/web/src/pages/EmailVerifyPage.tsx rename {backend => old/backend}/web/src/pages/GapsPage.tsx (100%) rename {backend => old/backend}/web/src/pages/JobsPage.tsx (100%) rename {backend => old/backend}/web/src/pages/MissionsPage.tsx (98%) rename {backend => old/backend}/web/src/pages/PatrolPage.tsx (100%) rename {backend => old/backend}/web/src/pages/PersonasPage.tsx (100%) rename {backend => old/backend}/web/src/pages/PublishPage.tsx (100%) rename {backend => old/backend}/web/src/pages/RegisterMemberPage.tsx (100%) rename {backend => old/backend}/web/src/pages/SettingsPage.tsx (95%) rename {backend => old/backend}/web/src/pages/publish/MentionsInboxPanel.tsx (85%) rename {backend => old/backend}/web/src/pages/publish/OwnPostsHub.tsx (91%) rename {backend => old/backend}/web/src/pages/publish/ReplyInboxPanel.tsx (89%) rename {backend => old/backend}/web/src/theme/ThemeContext.tsx (100%) rename {backend => old/backend}/web/tsconfig.app.json (100%) create mode 100644 old/backend/web/tsconfig.json rename {backend => old/backend}/web/tsconfig.node.json (100%) rename {backend => old/backend}/web/vite.config.ts (86%) rename {backend => old/backend}/worker/Dockerfile (100%) rename {backend => old/backend}/worker/keyword-search-server.ts (100%) rename {backend => old/backend}/worker/package-lock.json (100%) rename {backend => old/backend}/worker/package.json (100%) rename {backend => old/backend}/worker/resolve-media-server.ts (100%) rename {backend => old/backend}/worker/style-8d-worker.ts (100%) rename {backend => old/backend}/worker/threads-keyword-search-cli.ts (100%) rename {backend => old/backend}/worker/threads-keyword-search.ts (100%) rename {backend => old/backend}/worker/threads-media-id.ts (100%) rename {backend => old/backend}/worker/threads-profile-scraper.ts (100%) rename {backend => old/backend}/worker/threads-resolve-media-cli.ts (100%) rename {backend => old/backend}/worker/threads-resolve-media.ts (100%) rename {backend => old/backend}/worker/tsconfig.json (100%) rename {deploy => old/deploy}/.env.dev.example (57%) rename {deploy => old/deploy}/.env.prod.example (100%) rename {deploy => old/deploy}/README.md (100%) rename {deploy => old/deploy}/docker-compose.yml (88%) rename {deploy => old/deploy}/nginx/default.conf (100%) rename {deploy => old/deploy}/setup.sh (100%) create mode 100644 old/docs/architecture.md create mode 100644 old/docs/cleanup-log.md create mode 100644 old/docs/improvement-notes.md rename {docs => old/docs}/post-project-plan.md (100%) rename {extension => old/extension}/haixun-threads-sync/README.md (100%) rename {extension => old/extension}/haixun-threads-sync/content-haixun.js (100%) rename {extension => old/extension}/haixun-threads-sync/manifest.json (100%) rename {extension => old/extension}/haixun-threads-sync/options.html (100%) rename {extension => old/extension}/haixun-threads-sync/options.js (100%) rename {extension => old/extension}/haixun-threads-sync/popup.html (100%) rename {extension => old/extension}/haixun-threads-sync/popup.js (100%) rename {extension => old/extension}/haixun-threads-sync/service-worker.js (100%) rename {extension => old/extension}/haixun-threads-sync/storage-state.js (100%) rename {infra => old/infra}/README.md (100%) rename {infra => old/infra}/docker-compose.yml (94%) rename {infra => old/infra}/etc/haixun.env.example (100%) rename {infra => old/infra}/nginx/haixun-dev.conf (92%) rename {infra => old/infra}/nginx/haixun.conf (100%) rename {infra => old/infra}/start-api.sh (100%) rename {infra => old/infra}/start-infra.sh (100%) rename {infra => old/infra}/start-ngrok.sh (100%) rename {infra => old/infra}/start-node-worker.sh (100%) rename {infra => old/infra}/systemd/haixun-gateway.service (100%) rename {infra => old/infra}/systemd/haixun-node-worker.service (100%) rename {infra => old/infra}/systemd/haixun-worker.service (100%) rename {scripts => old/scripts}/check-dev.sh (100%) rename {scripts => old/scripts}/dev-all (100%) rename {scripts => old/scripts}/dev-all-stop.sh (100%) rename {scripts => old/scripts}/dev-all.sh (97%) rename {scripts => old/scripts}/package-extension.sh (100%) diff --git a/.gitignore b/.gitignore index eae3dac..d78f071 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,32 @@ # --- secrets / env --- -deploy/.env -deploy/.env.dev -deploy/.env.prod -infra/.env *.env **/.env !.env.example !*.env.example +deploy/.env +deploy/.env.dev +deploy/.env.prod +infra/.env +old/deploy/.env +old/deploy/.env.dev +old/deploy/.env.prod +old/infra/.env # --- runtime / logs --- .run/ +old/.run/ *.log # --- build artifacts --- -backend/bin/ -backend/web/dist/ -backend/dev-console/dist/ -frontend/dist/ dist/ -backend/web/public/downloads/*.zip +**/dist/ +apps/*/dist/ +apps/web/dist/ +services/*/bin/ +old/backend/bin/ +old/backend/web/dist/ +old/backend/dev-console/dist/ +old/backend/web/public/downloads/*.zip # --- dependencies --- node_modules/ diff --git a/.grok/skills/spec-driven/SKILL.md b/.grok/skills/spec-driven/SKILL.md new file mode 100644 index 0000000..62330f0 --- /dev/null +++ b/.grok/skills/spec-driven/SKILL.md @@ -0,0 +1,134 @@ +--- +name: spec-driven +description: > + Spec-driven product workflow: requirements → spec.md → plan.md → small task.md files + with explicit inputs/outputs (≈100–200 line changes each). Use when the user wants to + restart cleanly, write requirements, draft a product spec, break a plan into tasks, + run /spec-driven, or says 重新開始 / 需求文件 / 拆 task / 從需求做到 task. +--- + +# Spec-Driven Workflow + +Freeze **what we want**, then **how it behaves**, then **how we deliver**, then **one-step tasks**. Never invent code while a phase is unapproved. + +## Artifact root + +```text +docs/product// + README.md # phase status + requirements.md + spec.md # after requirements approved + plan.md # after spec approved + tasks/ + INDEX.md + T001-short-slug.md +``` + +Templates live in this skill: + +```text +references/templates/{requirements,spec,plan,task}.md +references/workflow.md +``` + +Optional scaffold: + +```bash +bash .grok/skills/spec-driven/scripts/init-run.sh "" +``` + +## Invocation + +| Form | Behavior | +|------|----------| +| `/spec-driven` | Ask slug + goal if missing; continue current phase | +| `/spec-driven ` | Work on that run | +| `/spec-driven --status` | Report phase + file presence only | +| `/spec-driven --phase N` | Only if prior artifacts exist; still require user OK for *this* phase output | + +Phases: `0=init`, `1=requirements`, `2=spec`, `3=plan`, `4=tasks`, `5=ready-to-implement`. + +## Hard gates + +1. **One phase per turn cycle.** Write the current phase document(s), summarize for the user, **stop**. +2. Advance only when the user clearly approves (e.g. 「需求 OK」「spec 通過」「可以拆 task」). +3. Do **not** implement product code until: + - `plan.md` approved, **and** + - `tasks/INDEX.md` exists with at least one `todo` task, **and** + - user says to implement a specific `T###` (or all). +4. Do **not** skip templates' required sections. +5. If prior phase file is missing → stop and say which file is needed. + +## Step 0 — Init + +1. Resolve `slug` (kebab-case, e.g. `haixun-console`). +2. If `docs/product//` missing → run `init-run.sh` or create the same structure. +3. Write/update `README.md` with goal, phase=`requirements`, dates. +4. Ask user to confirm scope in one sentence if ambiguous. + +## Step 1 — Requirements + +1. Read templates + existing product context (`docs/architecture.md`, related plans, `AGENTS.md` constraints). +2. Write `requirements.md` using the template (product language only). +3. **Forbidden in requirements:** API paths, Go package paths, PR lists, implementation steps. +4. Present: In/Out scope, P0 vs later, success criteria, open questions. +5. Set README phase note: `awaiting-requirements-approval`. +6. **STOP.** + +## Step 2 — Spec + +Preconditions: `requirements.md` exists and user approved. + +1. Write `spec.md` from requirements (behavior contracts). +2. Include Retain / Replace / Remove vs current codebase where relevant. +3. Follow project rules when specifying contracts: JSON envelope, `page/pageSize`, unix nanoseconds UTC, job guarded updates, auth header matrix. +4. **Forbidden in spec:** milestone calendars, task IDs, "we'll refactor later" without a concrete behavior. +5. Present summary → README `awaiting-spec-approval` → **STOP.** + +## Step 3 — Plan + +Preconditions: `spec.md` approved. + +1. Write `plan.md` with milestones M0…Mn, dependencies, risks, verification commands. +2. Map each milestone to a **task id range** (e.g. M1 → T010–T019) but do not write full task files yet unless user asked to combine plan+tasks in one approval (default: plan only). +3. Present summary → **STOP.** + +## Step 4 — Tasks + +Preconditions: `plan.md` approved. + +1. Create `tasks/INDEX.md` and one file per task from `references/templates/task.md`. +2. **Task size rule:** estimate **100–200 lines** of production code change. If larger, split. If smaller and independent, still OK but avoid micro-noise (<~30 lines unless critical fix). +3. Every task MUST have: Goal, Inputs, Outputs (paths + behavior), Out of scope, Acceptance checks (commands), Depends on. +4. No vague tasks ("improve UX", "cleanup backend"). +5. Present INDEX + count → **STOP** until user approves task breakdown. +6. On approval: README phase=`ready-to-implement`. + +## Step 5 — Implement (only when asked) + +1. Pick exactly one `T###` (or user-specified set). +2. Mark INDEX `doing` → implement only that task's Outputs → acceptance → `done`. +3. Do not pull in neighbor tasks "while you're there" unless user expands scope. +4. Prefer `make test` / `go test ./...` / `make web-build` as listed in the task. + +## Status reporting (`--status`) + +Print: + +- slug, goal, phase +- which of requirements/spec/plan/tasks exist +- task counts by status from INDEX + +## Style + +- Chinese or English matching the user; product docs for this repo default to **繁體中文**. +- Complete sentences in requirements/spec; tasks can be tighter checklists. +- Link related existing docs; do not duplicate architecture textbooks into requirements. + +## Anti-patterns + +- Writing all four layers in one go without approval +- Coding during requirements/spec +- Tasks without file paths or acceptance commands +- Treating `docs/post-project-plan.md` as approved requirements without copying into this pipeline +- Inflating a "whole product" requirements doc without P0/P1/P2 prioritization diff --git a/.grok/skills/spec-driven/references/templates/plan.md b/.grok/skills/spec-driven/references/templates/plan.md new file mode 100644 index 0000000..329a806 --- /dev/null +++ b/.grok/skills/spec-driven/references/templates/plan.md @@ -0,0 +1,48 @@ +# Plan: <名稱> + +> Status: `draft` | `approved` +> Source: `docs/product//spec.md` +> Last updated: `YYYY-MM-DD` + +## 1. 交付策略 + +<先穩什麼、再換什麼、最後加什麼 — 3–6 句> + +## 2. 里程碑 + +### M0 — <名稱> + +- **目標:** +- **完成定義:** +- **建議 task 區間:** T0xx–T0yy +- **驗證:** + +### M1 — … + +## 3. 依賴 + +```text +M0 → M1 → M2 + ↘ M3 +``` + +## 4. 風險與緩解 + +| 風險 | 緩解 | +|------|------| +| | | + +## 5. 刻意不做(本 plan 週期內) + +- + +## 6. 全域驗證指令 + +```bash +cd backend && go test ./... +make web-build # 若動前端 +``` + +## 7. 批准 + +- [ ] Plan 已批准: diff --git a/.grok/skills/spec-driven/references/templates/requirements.md b/.grok/skills/spec-driven/references/templates/requirements.md new file mode 100644 index 0000000..448ed58 --- /dev/null +++ b/.grok/skills/spec-driven/references/templates/requirements.md @@ -0,0 +1,77 @@ +# Requirements: <產品 / 功能名稱> + +> Status: `draft` | `approved` +> Slug: `` +> Last updated: `YYYY-MM-DD` + +## 1. 一句話 + +<用一句話說明這個產品或這次範圍要達成什麼> + +## 2. 誰用、在什麼情境 + +| 角色 | 目標 | +|------|------| +| | | + +## 3. 背景與動機(As-is → 為什麼改) + +- **現況問題:** +- **不改的後果:** +- **參考文件(非需求本體):** + +## 4. 目標(To-be) + +### 4.1 必須達成(P0) + +1. +2. + +### 4.2 應該有(P1) + +1. + +### 4.3 以後再說(P2) + +1. + +## 5. 範圍 + +### In scope + +- + +### Out of scope(明確不做) + +- + +## 6. 使用者故事(可驗收) + +| ID | 作為… | 我想要… | 以便… | 驗收要點 | +|----|--------|---------|--------|----------| +| US-01 | | | | | + +## 7. 成功標準 + +- [ ] +- [ ] + +## 8. 約束與假設 + +- 技術 / 合規 / 時程約束: +- 假設(未驗證當真): + +## 9. 風險 + +| 風險 | 影響 | 緩解(需求層,非實作細節) | +|------|------|---------------------------| +| | | | + +## 10. 開放問題 + +1. +2. + +## 11. 批准 + +- [ ] 需求已批准(日期 / 誰): diff --git a/.grok/skills/spec-driven/references/templates/spec.md b/.grok/skills/spec-driven/references/templates/spec.md new file mode 100644 index 0000000..6b6dc91 --- /dev/null +++ b/.grok/skills/spec-driven/references/templates/spec.md @@ -0,0 +1,85 @@ +# Spec: <名稱> + +> Status: `draft` | `approved` +> Source: `docs/product//requirements.md` +> Last updated: `YYYY-MM-DD` + +## 1. 摘要 + +<2–4 句,對齊需求 P0> + +## 2. 名詞與實體 + +| 名詞 | 定義 | +|------|------| +| | | + +## 3. 狀態機(若有) + +```text +stateA --> stateB : 觸發條件 +``` + +## 4. 使用者流程(行為) + +### 流程 A:… + +1. 使用者… +2. 系統… +3. 失敗時… + +## 5. 介面契約 + +### 5.1 HTTP / API(若適用) + +- Auth: +- Envelope:`code/message/data/error` +- 列表:`page` / `pageSize` → `pagination` + `list` +- 時間:unix nanoseconds UTC + +| Method | Path | 用途 | 主要 request/response 欄位 | +|--------|------|------|---------------------------| +| | | | | + +### 5.2 前端頁面 / 導覽(若適用) + +| 路由 / hash | 目的 | 主要操作 | +|-------------|------|----------| +| | | | + +### 5.3 Job / 背景任務(若適用) + +| Template type | Worker | 觸發 | 成功結果 | +|---------------|--------|------|----------| +| | | | | + +## 6. 與現況對照 + +| 能力 | Retain | Replace | Remove | 備註 | +|------|--------|---------|--------|------| +| | | | | | + +## 7. 非功能 + +- 安全 / 權限: +- 錯誤碼 scope: +- 可觀測性(job 進度、錯誤訊息): +- 效能邊界(可接受延遲 / 筆數上限): + +## 8. 資料保存(邏輯層,非 Mongo 細節亦可) + +| 實體 | 關鍵欄位 | 生命週期 | +|------|----------|----------| +| | | | + +## 9. 驗收場景(Given / When / Then) + +1. **Given** … **When** … **Then** … + +## 10. 開放規格問題 + +1. + +## 11. 批准 + +- [ ] Spec 已批准: diff --git a/.grok/skills/spec-driven/references/templates/task.md b/.grok/skills/spec-driven/references/templates/task.md new file mode 100644 index 0000000..57e50ba --- /dev/null +++ b/.grok/skills/spec-driven/references/templates/task.md @@ -0,0 +1,54 @@ +# T### — <短標題> + +> Status: `todo` | `doing` | `done` | `blocked` +> Milestone: `M#` +> Est. change: `~NNN lines` (target 100–200) + +## Goal + +完成後系統應:<一句可驗證的 Done> + +## Depends on + +- T###(若無:`—`) + +## Inputs + +- 讀取 / 依賴: + - 檔案: + - 既有能力 / API: + - Spec 段落: + +## Outputs + +### 程式變更(預期路徑) + +| 路徑 | 動作 | 說明 | +|------|------|------| +| | add/edit/delete | | + +### 行為變更 + +- + +### API / 契約(若有) + +- + +## Out of scope + +- + +## Acceptance + +- [ ] +- [ ] 指令: + +```bash +# e.g. +cd backend && go test ./path/... +``` + +## Notes + +<可選:實作提示,非必做細節> diff --git a/.grok/skills/spec-driven/references/workflow.md b/.grok/skills/spec-driven/references/workflow.md new file mode 100644 index 0000000..c79f89b --- /dev/null +++ b/.grok/skills/spec-driven/references/workflow.md @@ -0,0 +1,54 @@ +# Spec-Driven 四階段說明 + +## 為什麼要閘門 + +邊想邊寫碼會讓範圍漂掉。四份文件各解一個問題: + +| 文件 | 回答的問題 | 讀者 | +|------|------------|------| +| requirements | 我們要什麼?不要什麼? | 產品 / 你自己 | +| spec | 系統對外行為是什麼? | 實作者 + 審查 | +| plan | 分幾段交付、依賴與風險? | 排程 | +| task | 下一小時具體改什麼? | 動手 agent / 人 | + +## 閘門 checklist + +### 需求通過 + +- [ ] 一句話產品目標清楚 +- [ ] In / Out of scope 無互相打架 +- [ ] P0 可獨立交付價值 +- [ ] 成功標準可驗收(不是「更好用」) +- [ ] 開放問題已列出(可帶預設假設) + +### Spec 通過 + +- [ ] 主要實體與狀態可畫出 +- [ ] 關鍵使用者路徑有行為描述 +- [ ] Retain / Replace / Remove 對現況有交代 +- [ ] 錯誤、權限、時間、分頁等跨切約定有寫 +- [ ] 沒有偷偷寫實作步驟當規格 + +### Plan 通過 + +- [ ] 里程碑順序合理、依賴無環 +- [ ] 每里程碑有驗收方式 +- [ ] 風險與不做清單明確 +- [ ] 對應 task 區間可預估 + +### Tasks 通過 + +- [ ] 每 task 單一 Done +- [ ] 預估 100–200 行級(過大已拆) +- [ ] Inputs / Outputs / 不做 / 驗收齊 +- [ ] INDEX 可追蹤狀態 + +## 與現有技能的關係 + +| 技能 | 關係 | +|------|------| +| `/design` | 偏設計文件 + PR Plan;本 skill 偏產品需求→小 task | +| `/execute-plan` | 可在 **plan/tasks 批准後** 用來並行實作,非必須 | +| `/check-work` | 單一 task 做完後驗證 | + +第一版 **不必** 自動串 execute-plan;使用者說「實作 T003」即可。 diff --git a/.grok/skills/spec-driven/scripts/init-run.sh b/.grok/skills/spec-driven/scripts/init-run.sh new file mode 100755 index 0000000..d849f71 --- /dev/null +++ b/.grok/skills/spec-driven/scripts/init-run.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Usage: init-run.sh "" +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" +SLUG="${1:-}" +GOAL="${2:-}" + +if [[ -z "$SLUG" ]]; then + echo "usage: $0 \"\"" >&2 + exit 1 +fi + +DIR="$ROOT/docs/product/$SLUG" +mkdir -p "$DIR/tasks" + +if [[ ! -f "$DIR/README.md" ]]; then + cat >"$DIR/README.md" <"$DIR/tasks/INDEX.md" <<'EOF' +# Tasks INDEX + +| ID | Title | Milestone | Status | Est. lines | +|----|-------|-----------|--------|------------| +| — | (尚未拆 task;plan 批准後填入) | | | | +EOF +fi + +echo "Initialized $DIR" diff --git a/AGENTS.md b/AGENTS.md index 8cdad13..2f707f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,413 +1,46 @@ +# Agent 規範(重構主線) -# Agent Handoff Notes +## 封存與主線 -這個資料夾是新的巡樓後端核心,請優先維持乾淨邊界,不要把舊 Next.js 或 `template-monorepo` 的業務包袱搬進來。 +- **`old/`**:舊系統整包封存,**只讀參考**。不要在 `old/` 修功能當交付。 +- **新碼**:只寫在 repo 根下新目錄(`apps/`、`services/` 等,以 `docs/product` 為準)。 +- **產品真相**:`docs/product//`(requirements → spec → plan → tasks)。 +- **流程**:`/spec-driven`(`.grok/skills/spec-driven/`)。 -## 核心原則 +## 交付順序(硬性) -- 全系統時間一律 **UTC+0**;寫入 Mongo / API 的時間欄位一律 **unix nanoseconds**(`int64`)。排程的 `timezone` 只用於 cron 解讀與下發 payload,不作為儲存時區。 -- 複製模式,不複製舊業務。 -- `logic` 做 API 編排,`model/usecase` 做可重複使用能力。 -- provider adapter 不讀 setting、不碰 Mongo、不知道 HTTP。 -- setting 是通用 key-value model,不依賴 AI 或其他業務。 -- token / API key 第一版每次 request 帶入,不寫入 config。 -- SSE contract 由本服務 normalize,前端不要讀 provider 原始 chunk。 -- JSON API 必須使用 `code/message/data/error` envelope 與 `SSCCCDDD` 錯誤碼。 -- 列表 API 必須使用 `page/pageSize` query,並在 `data` 回傳 `pagination/list`。 -- Job 狀態轉移必須使用 guarded/conditional update;不要在 API/worker 直接裸 `Update` 覆蓋 job 狀態。 -- Redis job lock 的 value 是 `workerID`;release / refresh 必須檢查 owner,長任務必須 heartbeat。 -- Auth 目前是 native email/password + JWT,不包含 OAuth / OTP / MFA / Zitadel。不要為了相容 template-monorepo 把重依賴搬進來。 -- AI provider token 與會員 JWT 是兩種不同 token;AI token 每次 request header 帶入,會員 JWT 由 `/api/v1/auth/*` 簽發。 +1. Phase A 地基 +2. Phase B 前端 + mock(操作順再往下) +3. Phase C 接真 API +4. Phase D 業務加深 -## 設計文件 +B 未過關前,不大寫業務後端。 -- `docs/job-system-plan.md`:通用 job system 規劃,包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。 -- `docs/post-project-plan.md`:發文計畫(文案任務 Flow A 砍掉重做)— 主題發想、風格分析、矩陣產 N 篇草稿、定時發送。 -- `docs/scan-placement-plan.md`:海巡獲客(流程 B)— 知識圖譜、Brave 擴展、雙軌爬取(7 天重點 / 30 天補充)、產品匹配、島民交接。 +## 後端契約精神(實作時遵守) -## 新增 API 流程 +- 時間:**UTC**;儲存 **unix nanoseconds** `int64`。 +- JSON:`code` / `message` / `data` / `error`;成功碼 `102000`。 +- 列表:`page` / `pageSize` → `pagination` + `list`。 +- Job:狀態 **guarded** 更新;Redis lock value = `workerID`。 +- 會員 JWT vs AI provider token 分離。 +- **禁止**未實作 API 回成功空資料。 -1. 修改 `generate/api/*.api`。 -2. 優先使用 `make gen-api` 重新產生 handler/logic/types。 -3. 若手寫 handler,仍需遵守 `response.Write` 與 validator 流程。 -4. SSE endpoint 不使用 `response.Write`,直接輸出 `text/event-stream`。 -5. 更新 `README.md` 的 API 與架構說明。 +## 前端精神(Harbor Desk) -## Response / Error Code +- 新殼在 `apps/web`;不引入 MUI/Ant/Chakra。 +- 不複製任天堂 / Nook UI;不用 emoji 當主 icon。 +- 字體:Inter + Taipei Sans TC。 +- mock | live 同一 repository 介面。 +- 詳見 `docs/product/haixun-console/spec.md`。 -錯誤碼格式是 `SSCCCDDD`: +## 參考舊碼 + +需要對照實作時: ```text -SS = scope -CCC = category -DDD = detail +old/backend/ # 舊 Go / web / node workers +old/AGENTS.md # 舊詳細規範 +old/docs/ # 舊計劃文件 ``` -目前 scope: - -```text -10 = Facade -32 = Setting -33 = AI -34 = Job -35 = Auth -36 = Member -37 = Permission -``` - -建立錯誤時使用: - -```go -errs.For(code.AI).InputMissingRequired("缺少 AI provider token") -errs.For(code.Setting).ResNotFound("找不到設定") -errs.For(code.Job).ResInvalidState("job state changed; update rejected") -errs.For(code.Auth).AuthUnauthorized("missing bearer token") -``` - -不要直接手寫 `33104000` 這種數字,也不要回傳裸 `error` 給 handler 後讓使用者看到內部錯誤。 - -## Pagination - -列表 API 使用: - -```text -?page=1&pageSize=10 -``` - -response: - -```json -{ - "code": 102000, - "message": "SUCCESS", - "data": { - "pagination": { - "total": 100, - "page": 1, - "pageSize": 10, - "totalPages": 10 - }, - "list": [] - } -} -``` - -`page/pageSize` 必須是 server 正規化後的值。不要使用 `offset/limit/items`。 - -## 新增 Model 流程 - -模組放在: - -```text -internal/model// - domain/entity - domain/repository - domain/usecase - repository - usecase -``` - -依賴方向: - -```text -handler -> logic -> model/domain/usecase -model/usecase -> model/domain/repository -model/repository -> Mongo / Redis -``` - -不要讓 `logic` import `model//repository`。 - -## Auth / Permission 擴充 - -目前已接: - -```text -POST /api/v1/auth/register -POST /api/v1/auth/login -POST /api/v1/auth/refresh -POST /api/v1/auth/logout # requires member JWT -GET /api/v1/members/me -PATCH /api/v1/members/me -GET /api/v1/permissions/catalog -GET /api/v1/permissions/me -``` - -Auth matrix(`internal/handler/routes.go`): - -| 路由 | 需要會員 JWT | -|------|----------------| -| `GET /api/v1/health` | 否 | -| `POST /api/v1/auth/register/login/refresh` | 否 | -| `POST /api/v1/auth/logout` | 是(`Authorization`) | -| `GET /api/v1/ai/providers` | 否 | -| `POST /api/v1/ai/chat/stream/models` | 是(`X-Member-Authorization`)+ provider token(`Authorization`) | -| `/api/v1/members/*`、`/api/v1/permissions/me` | 是(`Authorization`) | -| `/api/v1/permissions/catalog`、`/api/v1/settings/*`、`/api/v1/jobs*`、`/api/v1/job/*` | 是(`Authorization`) | - -規則: - -- 保護路由用 `internal/middleware.Auth`(`Authorization: Bearer `);AI 變更路由用 `middleware.MemberAuth`(`X-Member-Authorization`),因 `Authorization` 保留給 provider API key。 -- logic 從 `authctx.ActorFromContext` 讀 `tenant_id` / `uid`。 -- 不要在 handler 直接 parse JWT;token 驗證集中在 `model/auth/usecase`。 -- 密碼只存 bcrypt hash,不回傳、不寫 log。 -- `members.roles` 第一版是簡化 role key。正式 RBAC 可逐步補 roles collection,但不要破壞 `role_permissions` 的 tenant + role_key contract。 -- `Auth.DevHeaderFallback` 只給本機開發,正式環境應關閉。 - -## AI Provider 擴充 - -新增 provider 時: - -1. 在 `internal/model/ai/domain/enum` 新增 provider id。 -2. 在 `internal/model/ai/provider` 新增 adapter。 -3. 在 `internal/model/ai/usecase` registry 註冊 provider 與 models。 -4. 確保 adapter 回傳統一 `StreamEvent`。 -5. 不要改 `logic/ai` 的 SSE 格式。 - -## Job Worker 擴充 - -新增 job step 時優先註冊 runner handler: - -```go -runner.RegisterStepHandler("analyze_8d", func(ctx context.Context, step job.StepContext) error { - if err := step.Heartbeat(ctx); err != nil { - return err - } - // do work, check cancel via job usecase if needed - return nil -}) -``` - -規則: - -- Handler 不要直接操作 Mongo / Redis,透過 job usecase 更新進度、完成、失敗或取消。 -- 長任務每個 checkpoint 呼叫 `StepContext.Heartbeat` 或 `RefreshRunLock`。 -- 收到 cancel signal 後呼叫 `AcknowledgeCancel(jobId, workerID)`,不要自行把狀態改成 `cancelled`。 -- release lock 時必須帶 `workerID`;不要新增無 owner 的 release helper。 - -## 前端設計規則(`web/`) - -巡樓 Console 前端在 `haixun-backend/web/`,視覺為**沉穩田園巡檢台**(動森感:天空、雲朵、奶油卡片、青綠 brand;**不是**任天堂 UI 複製)。 - -**字體固定** Inter + Taipei Sans TC;圖示僅 `AcIcon` / `AuthDecor` 內原創 SVG 線條圖。**禁止** emoji、貼圖 JPG、咖啡色木質頂欄、Nook / 任天堂命名。樣式集中在 `index.css`(`--hx-*` token + `hx-*` / `ac-*` / `auth-*` class)。 - -不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。 - -### 視覺架構(登入前後共用) - -```text -全頁背景 .hx-scene 灰藍天空 → 淡草地單一漸層(淺/深各一套,見 index.css) -裝飾層 SceneDecor 雲朵(多朵緩動)+ 淡光暈 + 小葉子;登入與 Layout 共用 -奶油卡片 .auth-ticket 2px line 邊框、圓角 2rem、surface 底、可選 .ac-dialog-texture 點陣 -頂部品牌列 圖示 .auth-ticket-icon(brand-soft 底)+ ink 標題;不用獨立色塊 ribbon -主內容 表單或 Outlet;內文頁用 PageTitle / Card(綠色 .ac-title-bar 僅內容區小標) -桌面側欄 .ac-pocket-device 掌上終端外框;PATROL PAD 狀態列;固定尺寸 + .ac-pocket-scroll 內捲 -手機 .ac-dock 底部最多 4 格 +「更多」sheet -``` - -| 區域 | 元件 | 關鍵 class | -|------|------|------------| -| 未登入 | `AuthShell` + `LoginPage` / `RegisterPage` | `hx-scene` `auth-scene` `auth-ticket` `auth-welcome` `auth-shell-form` | -| 已登入外殼 | `Layout` | `hx-scene` `ac-app-shell` `ac-app-header` `auth-ticket` `ac-app-main-inner` | -| 背景裝飾 | `AuthDecor.tsx` → `SceneDecor` | `hx-scene-deco` `auth-cloud--*` | -| 品牌小圖 | `AuthTicketIcon` | `auth-ticket-icon`(小屋+樹,原創 SVG) | -| 側欄導覽 | `Layout` + `navApps` | `ac-pocket-device` `ac-app-tile` | -| 手機導覽 | `MobileBottomNav` | `ac-dock` | - -**登入頁刻意不做的事**:上方不要獨立大色塊 header;表單上方**不要**再放 `ac-title-bar`「登入」大牌(品牌已在 `auth-welcome`)。註冊頁同理可省略重複大標。 - -**已淘汰、勿加回**:`ac-island`(改用 `hx-scene`)、`ac-wood-bar` / 咖啡色木質頂欄、`public/ac/` 貼圖、Nook Phone 文案。 - -### 技術棧與指令 - -```text -web/ - src/ - api/ # API client(envelope、JWT refresh) - auth/ # AuthContext - components/ # Layout、AuthShell、AuthDecor、ui、ThemeToggle、MobileBottomNav、AcIcon - theme/ # ThemeContext(淺色 / 深色) - pages/ # 路由頁面 - lib/ # acAssets(導覽 icon key)、jobStatus 等 - index.css # 設計 token 與場景樣式唯一來源 -``` - -```bash -make web-dev # dev server :5173,proxy 到 :8890 -make web-build # tsc + vite build -``` - -### 字型 - -| 語言 | 字型 | 載入方式 | -|------|------|----------| -| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`,在 `index.css` `@import` Regular + Bold | -| 英文 | **Inter**(與 simular.co 相同,Google Fonts 免費) | `web/index.html` link | - -規則: - -- `body` / 中文標題:`Inter` + `Taipei Sans TC` 混排(`--font-sans`)。 -- 純英文裝飾字(導覽副標、Hero 小字):加 class `display-en`,使用 `--font-en`。 -- 中文 `line-height` 維持 **1.7+**;不要用過細字重當標題(標題用 `font-bold` / `font-black`)。 -- 只載入 Taipei Sans TC **Regular + Bold**,不要載入 Light,避免小字過細。 -- 不要改回 Noto Sans TC,也不要手寫 `#333` 這類裸色碼當主色。 - -### 對比度與字級 - -- 內文、表頭、表單 label、卡片說明:優先 `text-ink` / `text-ink-secondary`,**不要**拿 `text-muted` 當主要閱讀文字。 -- `text-muted` 只給次要提示(筆數、hint、placeholder 用 `text-subtle`)。 -- 表單輸入字級 **15px**(`text-[15px]`),輸入框底用 `bg-surface` 白底,確保與背景拉開。 -- 淺色 `muted` 約 `#5a6578`、深色約 `#b8c4d6`;改色時以「小字仍可舒適閱讀」為準,不要回到 `#94a3b8` 那種淡灰。 - -### 主題(淺色 / 深色) - -- `ThemeProvider`(`src/theme/ThemeContext.tsx`)包住 App;偏好存 `localStorage` key:`haixun.theme`(`light` | `dark`)。 -- `index.html` 內嵌 script 在 React 載入前設定 `data-theme`,避免閃爍。 -- 所有顏色必須走 CSS 變數 `--hx-*`,再映射到 Tailwind `@theme`(`bg-canvas`、`text-brand` 等)。 -- 切換按鈕用 `ThemeToggle`(`ac-btn-secondary` 樣式);`Layout` 頂欄與 `AuthShell` 右上角都要有。 -- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。 - -淺色:低飽和灰藍天空 + 灰綠草地 + 奶油 `surface` + **brand 青綠**;深色:黃昏低對比、同一套 token 自動切換。頂欄與卡片內品牌區都用 **surface / ink / brand**,不要再用木色 `#c4a882` 當 header 底。 - -### 場景與卡片 class(維護時對照) - -| Class | 用途 | -|-------|------| -| `.hx-scene` | 全頁天空→草地漸層(登入 + 已登入根節點) | -| `.hx-scene-deco` / `SceneDecor` | 背景雲、光暈、葉子(`pointer-events: none`) | -| `.auth-ticket` | 奶油主卡片外框(登入卡、已登入主內容區) | -| `.auth-welcome` | 卡片內品牌列:圖示 + 標題 + 一句 tagline,底部分隔線 | -| `.ac-app-header` | 已登入 sticky 頂欄:半透明 surface + blur,**非**木色 | -| `.ac-title-bar` | 內容區綠色小標題(裝置色漸層);用於 `PageTitle` 等,**不**用於登入頁表單上方大牌 | -| `.ac-pocket-device` | 側欄掌上終端;`--pocket-width`(28rem)、`--pocket-screen-height` 固定,內容在 `.ac-pocket-scroll` 捲動 | -| `.ac-app-tile` / `.ac-dock` | App 格導覽、手機底欄 | -| `.auth-shell-form` | 登入/註冊表單放大字級(僅 auth 頁) | - -側欄標示用 **PATROL PAD** 等中性英文裝飾字(`display-en`);圖示僅 `AcIcon` SVG。 - -### 色彩 token(語意命名) - -開發時只用這些 Tailwind class(值定義在 `web/src/index.css`): - -| Token | 用途 | -|-------|------| -| `canvas` | 全頁背景 | -| `surface` / `surface-muted` | 卡片、輸入框底 | -| `ink` / `ink-secondary` / `muted` | 主文 / 次文 / 輔助 | -| `line` | 邊框 | -| `brand` / `brand-hover` / `brand-soft` | 主 CTA、active 導覽、連結 hover | -| `glow` | 裝飾色塊(`.glow-blob-alt`) | -| `success` / `warning` / `danger`(含 `*-soft`) | 狀態、錯誤、Job badge | - -主按鈕一律 `Button variant="primary"` → `bg-brand`,不要用全黑按鈕。 - -### 圓角與陰影 - -```text ---radius-sm 0.75rem 小元素、code ---radius-md 1.25rem Input / Textarea ---radius-lg 1.75rem Card ---radius-xl 2.25rem Hero、QuickLink、StatCard ---radius-pill 9999px Button、Badge、導覽 pill -``` - -陰影用 utility:`shadow-card`(一般卡片)、`shadow-soft`(主按鈕、Hero、`.auth-ticket`)。內容 Hero 可用 `ac-bulletin` + `ac-hero-gradient` token;全頁裝飾雲朵走 `SceneDecor`,不要另加會打架的強色 blob。 - -### 共用元件(優先復用) - -新頁面必須從 `src/components/ui.tsx` 組裝,不要另寫一套按鈕樣式: - -| 元件 | 用途 | -|------|------| -| `PageTitle` | 頁面標題 + 副標 | -| `Card` | 內容區塊 | -| `Field` + `Input` / `Textarea` | 表單 | -| `Button` | `primary` / `ghost` / `danger` / `soft` | -| `Badge` | 標籤 pill(`brand` / `sky` / `success` / `warning` / `danger` / `neutral`) | -| `StatCard` / `QuickLinkCard` | 總覽統計與快捷入口 | -| `ErrorText` / `CopyableId` | 錯誤與可複製 ID | - -`Button` 必須渲染 `{children}`;文案用**中文動詞**(例:「建立背景任務」「重新載入任務列表」),不要留空白小框。 - -### RWD(手機) - -- `< lg`:隱藏左側欄;**底部固定導覽**最多 **4 格**(總覽 / 任務 / 排程 / **更多**),不要把漢堡或 ⋯ 選單放在左上角。 -- 「更多」以底部 sheet 展開:AI、模板、設定、會員、權限、主題切換、登出。 -- 主內容加 `layout-main` 底部 padding,避開 tab bar + `safe-area-inset-bottom`。 -- 寬表格包 `overflow-x-auto` + `min-w-*`,避免小螢幕擠爆版面。 - -### 版面與導覽 - -- 已登入(桌面):`Layout` = `hx-scene` 背景 + `SceneDecor` + `ac-app-header`(品牌 + 角色 chip + `ThemeToggle`)+ 左 `ac-pocket-device` + 右 `auth-ticket` 主內容 `Outlet`。 -- 已登入(手機):同上頂欄;導覽走 `MobileBottomNav`(總覽/任務/排程/更多)。 -- 側欄 App 來源:`src/lib/acAssets.ts` 的 `navApps`;圖示 key 對應 `AcIcon`。 -- Active 導覽:`ac-app-tile--active`(brand-soft 底 + brand 字色);hover:`bg-brand-soft text-brand`。 -- 未登入:`AuthShell` 置中 `auth-ticket` + 右上 `ThemeToggle`;`auth-welcome` 內品牌,表單緊接說明文字。 -- 語氣:年輕、直接、短句;可帶「島民」「巡樓」等原創文案,避免企業八股與任天堂用語。 - -### API 與狀態 - -- JSON 一律走 `api/client.ts`(`code/message/data` envelope);需登入加 `{ auth: true }`。 -- AI 路由用 `X-Member-Authorization`;provider token 用 `Authorization`(見後端 Auth matrix)。 -- Job 狀態中文與 badge 色:`src/lib/jobStatus.ts`(`jobStatusLabel` / `jobStatusBadgeClass`),列表有進行中任務時可每 3 秒 refresh。 -- 不要在前端 parse JWT;`uid` / `tenant_id` 從 `AuthContext` 讀。 - -### 新增頁面流程 - -1. 在 `App.tsx` 掛路由(需登入的放在 `Layout` 底下,自動享有 `hx-scene` + 頂欄 + 主內容 `auth-ticket`)。 -2. 頁面內用 `PageTitle`(含 `.ac-title-bar` 小標)+ `Card` / `ac-bulletin` + `ui.tsx` 元件;色票只引用 semantic token。 -3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。 -4. 新導覽項:改 `acAssets.ts` 的 `navApps`,並在 `AcIcon` 補 SVG path。 -5. 完成後執行 `make web-build`。 - -### 島民頁面互動(可推廣 runtime) - -掛在 `Layout` 底下的新頁面**自動**支援島民操作,不需每頁手寫 executor。 - -模組入口:`web/src/lib/islander/index.ts` - -| 層 | 職責 | -|----|------| -| `pageSnapshot` | 掃描 `.ac-app-shell` 內可互動元素,產生 `hx-*` ref | -| `islanderActions` | 解析/剝除 `islander-actions` JSON 區塊 | -| `actionExecutor` | 執行 navigate/click/fill/select/scroll 等;可 `registerIslanderActionHandler` 擴充 | -| `islanderAgent` | 串流回覆 → 執行 action → 回傳結果 → 自動 follow-up | -| `buildIslanderContext` | 組裝送給後端的頁面快照 | - -**零設定(預設)**:路由掛在 `Layout` 即可;島民讀 DOM + `PageTitle` / `h1` 辨識頁面。預設**不**主動介紹這一頁;僅在使用者明確問頁面/操作時才附【可互動元素】(`userWantsPageContext`)。 - -**可選增強**(擇一): - -1. `useIslanderPage({ title, purpose, hints, suggestions })` — 頁面內動態註冊說明 -2. `registerIslanderPage(/^\/foo/, { title, ... })` — 在 `siteGuide.ts` 或模組 init 靜態註冊 -3. HTML 慣例:`data-islander-label`(元素名稱)、`data-islander-kind`(類型)、`data-islander-ignore`(排除)、`data-islander-page-title`(頁名) - -Action 協定(AI 回覆末尾): - -```islander-actions -[{ "type": "navigate", "path": "/settings" }, { "type": "click", "ref": "hx-3" }] -``` - -### 前端禁忌 - -- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。 -- 不要為單頁新增第三套配色、木質頂欄、或漸層彩虹按鈕。 -- 不要在登入/註冊頁加回獨立大牌 `ac-title-bar` 或咖啡色 header ribbon。 -- 不要讓 SSE / AI 直接吃 provider 原始 chunk(後端已 normalize)。 -- 不要用 `offset/limit` 呼叫列表 API;用 `page` / `pageSize`。 - -## 驗證 - -完成變更後至少執行: - -```bash -cd haixun-backend -go mod tidy -make fmt -go test ./... -``` - -有動到前端時另執行: - -```bash -make web-build -``` +複製模式可以,**不要**把半成品與死碼整包搬回主線。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..54543c5 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Thread-Master / 巡樓(重構主線) + +自 **2026-07-09** 起:舊程式已整包封存至 [`old/`](./old/),**從零重寫**。 + +## 現在讀哪裡 + +| 路徑 | 說明 | +|------|------| +| [`docs/product/haixun-console/`](./docs/product/haixun-console/) | 產品需求 → spec → plan → tasks(真相來源) | +| [`old/`](./old/) | 舊 monorepo **只讀封存**,不在上面開新功能 | +| [`.grok/skills/spec-driven/`](./.grok/skills/spec-driven/) | `/spec-driven` 流程 skill | + +## 目標結構(建立中) + +```text +thread-master/ + apps/web/ # Harbor Desk 前端(Phase B,mock 優先) + services/api/ # 新後端(Phase A/C;名稱以 plan 為準) + docs/product/ # 產品文件 + old/ # 舊碼封存 +``` + +## 交付順序 + +```text +A 地基(登入 / 權限 / Threads / Job / 通知) + → B 新前端 + Mock(流程與風格先過關) + → C 接真後端 + → D 業務加深(海巡等) +``` + +## 前端(apps/web) + +```bash +cd apps/web +npm install +npm run dev # 本機開發 +npm run build # 型別檢查 + 生產建置 +``` + +產品任務進度:`docs/product/haixun-console/tasks/INDEX.md`(目前從 T001 起)。 + +## 開發流程 + +```text +/spec-driven haixun-console +# 或依 tasks 逐題實作,例如「做 T002」 +``` + +查舊行為請到 `old/backend/`(只讀)。 diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/apps/web/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..d6f50ca --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Harbor Desk · 巡樓 + + +
+ + + diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json new file mode 100644 index 0000000..bc383bf --- /dev/null +++ b/apps/web/package-lock.json @@ -0,0 +1,1365 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz", + "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz", + "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz", + "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz", + "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz", + "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz", + "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz", + "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz", + "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz", + "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz", + "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz", + "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz", + "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz", + "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz", + "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz", + "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz", + "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz", + "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz", + "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz", + "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz", + "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.73.0", + "@oxlint/binding-android-arm64": "1.73.0", + "@oxlint/binding-darwin-arm64": "1.73.0", + "@oxlint/binding-darwin-x64": "1.73.0", + "@oxlint/binding-freebsd-x64": "1.73.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.73.0", + "@oxlint/binding-linux-arm-musleabihf": "1.73.0", + "@oxlint/binding-linux-arm64-gnu": "1.73.0", + "@oxlint/binding-linux-arm64-musl": "1.73.0", + "@oxlint/binding-linux-ppc64-gnu": "1.73.0", + "@oxlint/binding-linux-riscv64-gnu": "1.73.0", + "@oxlint/binding-linux-riscv64-musl": "1.73.0", + "@oxlint/binding-linux-s390x-gnu": "1.73.0", + "@oxlint/binding-linux-x64-gnu": "1.73.0", + "@oxlint/binding-linux-x64-musl": "1.73.0", + "@oxlint/binding-openharmony-arm64": "1.73.0", + "@oxlint/binding-win32-arm64-msvc": "1.73.0", + "@oxlint/binding-win32-ia32-msvc": "1.73.0", + "@oxlint/binding-win32-x64-msvc": "1.73.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..662ec05 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@harbor/web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/icons.svg b/apps/web/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/apps/web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/App.css b/apps/web/src/App.css new file mode 100644 index 0000000..d7737c6 --- /dev/null +++ b/apps/web/src/App.css @@ -0,0 +1,50 @@ +.scaffold { + min-height: 100vh; + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.75rem; + padding: 2rem; + box-sizing: border-box; + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; + color: #1e293b; + background: #f8fafc; +} + +.scaffold-kicker { + margin: 0; + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #2a9d8f; +} + +.scaffold h1 { + margin: 0; + font-size: 1.75rem; + font-weight: 700; +} + +.scaffold-lead { + margin: 0; + max-width: 28rem; + line-height: 1.6; + color: #475569; +} + +.scaffold-meta { + margin: 0.5rem 0 0; + font-size: 0.875rem; + color: #64748b; +} + +.scaffold code { + font-size: 0.85em; + padding: 0.1em 0.35em; + border-radius: 0.25rem; + background: #e2e8f0; + color: #0f172a; +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..aa12c99 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,19 @@ +import "./App.css"; + +/** + * T001 scaffold placeholder. Routing / Harbor Desk shell start at T004–T005. + */ +export default function App() { + return ( +
+

Harbor Desk

+

apps/web

+

+ 巡樓 Console 前端骨架(T001)。下一步:設計 token 與 App Shell。 +

+

+ 啟動:npm run dev · 產物:npm run build +

+
+ ); +} diff --git a/apps/web/src/assets/hero.png b/apps/web/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/apps/web/src/assets/react.svg b/apps/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/assets/vite.svg b/apps/web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/apps/web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..819ad3c --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,16 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + min-height: 100%; +} + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/apps/web/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/backend/web/tsconfig.json b/apps/web/tsconfig.json similarity index 100% rename from backend/web/tsconfig.json rename to apps/web/tsconfig.json diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/apps/web/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/backend/dev-console/dist/assets/index-56hNfpGK.js b/backend/dev-console/dist/assets/index-56hNfpGK.js deleted file mode 100644 index f55f5b3..0000000 --- a/backend/dev-console/dist/assets/index-56hNfpGK.js +++ /dev/null @@ -1,3 +0,0 @@ -(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const r of o)if(r.type==="childList")for(const f of r.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&i(f)}).observe(document,{childList:!0,subtree:!0});function s(o){const r={};return o.integrity&&(r.integrity=o.integrity),o.referrerPolicy&&(r.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?r.credentials="include":o.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(o){if(o.ep)return;o.ep=!0;const r=s(o);fetch(o.href,r)}})();const I=102e3,v="haixun.dev-console",t=e=>document.getElementById(e),a=w();function w(){try{return JSON.parse(localStorage.getItem(v)||"{}")}catch{return{}}}function d(){localStorage.setItem(v,JSON.stringify(a))}function c(e,n){const s=t("log"),i=n===void 0?String(e):`${e} ${JSON.stringify(n,null,2)}`;s.textContent=`${new Date().toLocaleTimeString()} ${i} - -`+s.textContent}async function l(e,{method:n="GET",body:s,auth:i=!1}={}){const o={"Content-Type":"application/json"};i&&a.accessToken&&(o.Authorization=`Bearer ${a.accessToken}`);const r=await fetch(e,{method:n,headers:o,body:s===void 0?void 0:JSON.stringify(s)}),f=await r.text();let m;try{m=JSON.parse(f)}catch{throw new Error(`非 JSON 回應 ${e}: HTTP ${r.status}`)}if(m.code!==I)throw new Error(m.message||`API 錯誤 ${m.code}`);return m.data}function u(e,n){const s=t("authStatus");s.textContent=e,s.className="status"+(n?" ok":n===!1?" err":"")}function T(e){return`${e||"—"}`}async function $(){try{await l("/api/v1/health"),t("connStatus").textContent="API 連線正常",t("connStatus").className="status ok"}catch(e){t("connStatus").textContent="API 離線:"+e.message,t("connStatus").className="status err"}}function S(e){const n=t("personaList");if(n.innerHTML="",!(e!=null&&e.length)){n.innerHTML='
  • 尚無人設
  • ';return}for(const s of e){const i=document.createElement("li");i.className=s.id===a.selectedPersonaId?"active":"",i.innerHTML=`${s.display_name||s.id}${s.style_benchmark?"@"+s.style_benchmark:"未分析"}`,i.onclick=()=>g(s.id),n.appendChild(i)}}async function h(){const e=await l("/api/v1/personas",{auth:!0});a.personas=e.list||[],S(a.personas),d(),c("人設列表已更新",{count:a.personas.length})}function g(e){a.selectedPersonaId=e,d(),S(a.personas||[]),t("btnStart8d").disabled=!e,t("btnReloadPersona").disabled=!e,y()}async function y(){const e=a.selectedPersonaId;if(!e)return;const n=await l(`/api/v1/personas/${encodeURIComponent(e)}`,{auth:!0});t("personaMeta").textContent=`${n.display_name||e} · benchmark @${n.style_benchmark||"—"}`,t("personaDetail").value=JSON.stringify(n,null,2),c("人設詳情",{id:e,style_benchmark:n.style_benchmark})}async function C(){const e=await l("/api/v1/auth/login",{method:"POST",body:{tenant_id:t("tenantId").value.trim(),email:t("email").value.trim(),password:t("password").value}});a.accessToken=e.access_token,a.refreshToken=e.refresh_token,a.uid=e.uid,d(),u(`已登入 uid=${e.uid}`,!0),c("登入成功",{uid:e.uid}),await h(),a.selectedPersonaId&&g(a.selectedPersonaId)}async function N(){const e=await l("/api/v1/auth/register",{method:"POST",body:{tenant_id:t("tenantId").value.trim(),email:t("email").value.trim(),password:t("password").value,display_name:t("email").value.split("@")[0]}});a.accessToken=e.access_token,a.refreshToken=e.refresh_token,a.uid=e.uid,d(),u(`註冊並登入 uid=${e.uid}`,!0),c("註冊成功",{uid:e.uid}),await h()}function O(){a.accessToken="",a.refreshToken="",a.uid="",d(),u("已登出",!1),t("personaList").innerHTML="",t("personaDetail").value="",t("btnStart8d").disabled=!0,t("btnReloadPersona").disabled=!0,c("已登出")}async function _(){const e=t("displayName").value.trim()||"新人設",n=await l("/api/v1/personas",{method:"POST",auth:!0,body:{display_name:e}});t("displayName").value="",await h(),g(n.id),c("建立人設",{id:n.id})}async function L(){const e=a.selectedPersonaId,n=t("benchmark").value.trim().replace(/^@/,"");if(!e||!n)throw new Error("請選人設並填對標帳號");const s=await l(`/api/v1/personas/${encodeURIComponent(e)}/style-analysis`,{method:"POST",auth:!0,body:{benchmark_username:n}});t("jobId").value=s.job_id,t("jobHint").textContent=s.message||"任務已建立",t("jobHint").className="status ok",c("8D 任務已建立",s),await b(),k()}function x(e){var s,i,o;if(!e)return;const n=((s=e.progress)==null?void 0:s.steps)||[];t("jobSteps").innerHTML=n.map(r=>`
    ${T(r.status)} ${r.id} — ${r.message||""}
    `).join(""),t("jobDetail").textContent=JSON.stringify({id:e.id,status:e.status,phase:e.phase,percentage:(i=e.progress)==null?void 0:i.percentage,summary:(o=e.progress)==null?void 0:o.summary,error:e.error,result:e.result},null,2),["succeeded","completed","failed","cancelled"].includes(e.status)&&(P(),(e.status==="succeeded"||e.status==="completed")&&y().catch(()=>{}))}async function b(){const e=t("jobId").value.trim();if(!e)return;const n=await l(`/api/v1/jobs/${encodeURIComponent(e)}`,{auth:!0});return x(n),c("Job 狀態",{id:n.id,status:n.status,phase:n.phase}),n}let p=null;function k(){P(),p=setInterval(()=>b().catch(e=>c("輪詢失敗",e.message)),3e3),t("btnAutoPoll").textContent="停止輪詢"}function P(){p&&clearInterval(p),p=null,t("btnAutoPoll").textContent="自動輪詢"}t("btnLogin").onclick=()=>C().catch(e=>{u(e.message,!1),c("登入失敗",e.message)});t("btnRegister").onclick=()=>N().catch(e=>{u(e.message,!1),c("註冊失敗",e.message)});t("btnLogout").onclick=O;t("btnCreatePersona").onclick=()=>_().catch(e=>c("建立失敗",e.message));t("btnRefreshPersonas").onclick=()=>h().catch(e=>c("載入失敗",e.message));t("btnReloadPersona").onclick=()=>y().catch(e=>c("載入失敗",e.message));t("btnStart8d").onclick=()=>L().catch(e=>{t("jobHint").textContent=e.message,t("jobHint").className="status err",c("8D 失敗",e.message)});t("btnPollJob").onclick=()=>b().catch(e=>c("查詢失敗",e.message));t("btnAutoPoll").onclick=()=>p?P():k();a.tenantId&&(t("tenantId").value=a.tenantId);t("tenantId").onchange=()=>{a.tenantId=t("tenantId").value,d()};a.accessToken&&(u(`token 已還原 uid=${a.uid||"?"}`,!0),h().then(()=>{a.selectedPersonaId&&g(a.selectedPersonaId)}).catch(()=>u("token 可能過期,請重新登入",!1)));$(); diff --git a/backend/dev-console/dist/index.html b/backend/dev-console/dist/index.html deleted file mode 100644 index 29c0d8d..0000000 --- a/backend/dev-console/dist/index.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - 巡樓 Dev Console - - - - -
    -
    -

    巡樓 Dev Console

    -

    暫用開發面板 · 人設 + 8D 公開爬蟲 · 重做前端前夠用就好

    -
    -
    尚未連線
    -
    - -
    -
    -

    1. 登入

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - - - -
    -
    - -
    -

    2. 人設

    -
    - - - - -
    -
      -

      點選一筆人設後,下方可跑 8D 分析。

      -
      - -
      -

      3. 8D 風格分析(公開爬蟲,不需 session)

      -
      - - - -
      -

      需先啟動 Go API(:8890)與 Node worker(style-8d)。設定頁要有研究用 AI API key。

      -
      -
      - - - - -
      -
      -
      (尚無任務)
      -
      - -
      -

      4. 人設詳情

      -
      - - -
      - -
      - -
      -

      Log

      -
      
      -      
      -
      - - - \ No newline at end of file diff --git a/backend/generate/api/copy_mission.api b/backend/generate/api/copy_mission.api deleted file mode 100644 index cb60235..0000000 --- a/backend/generate/api/copy_mission.api +++ /dev/null @@ -1,396 +0,0 @@ -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"` - MatchedSource []string `json:"matched_source,omitempty"` - Confidence string `json:"confidence,omitempty"` - Status string `json:"status,omitempty"` - TopicRelevance float64 `json:"topic_relevance,omitempty"` - LastSeenAt int64 `json:"last_seen_at,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"` - } - - CopyAudienceSampleData { - Username string `json:"username"` - SamplePostId string `json:"sample_post_id,omitempty"` - SampleText string `json:"sample_text,omitempty"` - ReplyLikeCount int `json:"reply_like_count,omitempty"` - Appearances int `json:"appearances,omitempty"` - FirstSeenAt int64 `json:"first_seen_at"` - LastSeenAt int64 `json:"last_seen_at,omitempty"` - Status string `json:"status,omitempty"` - } - - CopyMissionResearchItemData { - Title string `json:"title,omitempty"` - URL string `json:"url,omitempty"` - Snippet string `json:"snippet,omitempty"` - Query string `json:"query,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"` - KnowledgeItems []string `json:"knowledge_items,omitempty"` - SelectedKnowledgeItems []string `json:"selected_knowledge_items,omitempty"` - ResearchItems []CopyMissionResearchItemData `json:"research_items,omitempty"` - SuggestedTags []CopySuggestedTagData `json:"suggested_tags,omitempty"` - SimilarAccounts []CopySimilarAccountData `json:"similar_accounts,omitempty"` - AudienceSamples []CopyAudienceSampleData `json:"audience_samples,omitempty"` - BenchmarkNotes string `json:"benchmark_notes,omitempty"` - } - - ExpandCopyMissionGraphHandlerReq { - CopyMissionPath - ExpandKnowledgeGraphReq - } - - PatchCopyMissionGraphNodesHandlerReq { - CopyMissionPath - PatchKnowledgeGraphNodesReq - } - - PatchSimilarAccountReq { - Status string `json:"status"` - } - - PatchAudienceSampleReq { - Status string `json:"status"` - } - - CopyMissionSimilarAccountPath { - PersonaID string `path:"personaId" validate:"required"` - ID string `path:"id" validate:"required"` - Username string `path:"username" validate:"required"` - } - - PatchSimilarAccountHandlerReq { - CopyMissionSimilarAccountPath - PatchSimilarAccountReq - } - - PatchAudienceSampleHandlerReq { - CopyMissionSimilarAccountPath - PatchAudienceSampleReq - } - - 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"` - KnowledgeItems []string `json:"knowledge_items,optional"` - SelectedKnowledgeItems []string `json:"selected_knowledge_items,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"` - } - - CopyMissionInspirationReq { - Keyword string `json:"keyword,optional"` - ContentDirection string `json:"content_direction,optional"` - UseWebSearch bool `json:"use_web_search,optional"` - AvoidTopics []string `json:"avoid_topics,optional"` - } - - CopyMissionInspirationHandlerReq { - PersonaCopyMissionsPath - CopyMissionInspirationReq - } - - 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"` - } - - DeleteCopyMissionMatrixDraftsReq { - DraftIDs []string `json:"draft_ids,optional"` - } - - DeleteCopyMissionMatrixDraftsHandlerReq { - CopyMissionPath - DeleteCopyMissionMatrixDraftsReq - } - - DeleteCopyMissionMatrixDraftsData { - Deleted int `json:"deleted"` - Message string `json:"message"` - } - - CopyMissionScheduleCopyDraftsReq { - AccountID string `json:"account_id" validate:"required"` - DraftIDs []string `json:"draft_ids" validate:"required,min=1"` - StartAt int64 `json:"start_at,optional"` - Timezone string `json:"timezone,optional"` - Slots []PublishSlotData `json:"slots,optional"` - Mode string `json:"mode,optional"` - } - - ScheduleCopyMissionDraftsHandlerReq { - CopyMissionPath - CopyMissionScheduleCopyDraftsReq - } - - CopyMissionScheduleCopyDraftsData { - Scheduled int `json:"scheduled"` - List []PublishQueueItemData `json:"list"` - Message string `json:"message"` - } - - CopyMissionInspirationSourceData { - Query string `json:"query,omitempty"` - Title string `json:"title,omitempty"` - Snippet string `json:"snippet,omitempty"` - URL string `json:"url,omitempty"` - } - - CopyMissionInspirationData { - TopicCandidateID string `json:"topic_candidate_id,omitempty"` - ContentPlanID string `json:"content_plan_id,omitempty"` - 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"` - Angles []string `json:"angles,omitempty"` - Mission string `json:"mission,omitempty"` - TargetAudience string `json:"target_audience,omitempty"` - OpeningType string `json:"opening_type,omitempty"` - BodyType string `json:"body_type,omitempty"` - Emotion string `json:"emotion,omitempty"` - CtaType string `json:"cta_type,omitempty"` - RiskLevel string `json:"risk_level,omitempty"` - Avoid []string `json:"avoid,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 (CopyMissionInspirationHandlerReq) 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 deleteCopyMissionMatrixDrafts - post /:personaId/copy-missions/:id/matrix-drafts/delete (DeleteCopyMissionMatrixDraftsHandlerReq) returns (DeleteCopyMissionMatrixDraftsData) - - @handler scheduleCopyMissionDrafts - post /:personaId/copy-missions/:id/copy-drafts/schedule (ScheduleCopyMissionDraftsHandlerReq) returns (CopyMissionScheduleCopyDraftsData) - - @handler getCopyMissionScanSchedule - get /:personaId/copy-missions/:id/scan-schedule (CopyMissionPath) returns (CopyMissionScanScheduleData) - - @handler upsertCopyMissionScanSchedule - put /:personaId/copy-missions/:id/scan-schedule (UpsertCopyMissionScanScheduleHandlerReq) returns (CopyMissionScanScheduleData) - - @handler patchCopyMissionSimilarAccount - patch /:personaId/copy-missions/:id/similar-accounts/:username (PatchSimilarAccountHandlerReq) returns (CopyMissionData) - - @handler patchCopyMissionAudienceSample - patch /:personaId/copy-missions/:id/audience-samples/:username (PatchAudienceSampleHandlerReq) returns (CopyMissionData) - - @handler expandCopyMissionGraph - post /:personaId/copy-missions/:id/knowledge-graph/expand (ExpandCopyMissionGraphHandlerReq) returns (ExpandKnowledgeGraphData) - - @handler getCopyMissionGraph - get /:personaId/copy-missions/:id/knowledge-graph (CopyMissionPath) returns (KnowledgeGraphData) - - @handler patchCopyMissionGraphNodes - patch /:personaId/copy-missions/:id/knowledge-graph/nodes (PatchCopyMissionGraphNodesHandlerReq) returns (KnowledgeGraphData) -} diff --git a/backend/internal/handler/copy_mission/create_copy_mission_handler.go b/backend/internal/handler/copy_mission/create_copy_mission_handler.go deleted file mode 100644 index 870a129..0000000 --- a/backend/internal/handler/copy_mission/create_copy_mission_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/delete_copy_mission_handler.go b/backend/internal/handler/copy_mission/delete_copy_mission_handler.go deleted file mode 100644 index 2340be4..0000000 --- a/backend/internal/handler/copy_mission/delete_copy_mission_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/delete_copy_mission_matrix_drafts_handler.go b/backend/internal/handler/copy_mission/delete_copy_mission_matrix_drafts_handler.go deleted file mode 100644 index fe3aeea..0000000 --- a/backend/internal/handler/copy_mission/delete_copy_mission_matrix_drafts_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func DeleteCopyMissionMatrixDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.DeleteCopyMissionMatrixDraftsHandlerReq - 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.NewDeleteCopyMissionMatrixDraftsLogic(r.Context(), svcCtx) - data, err := l.DeleteCopyMissionMatrixDrafts(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/expand_copy_mission_graph_handler.go b/backend/internal/handler/copy_mission/expand_copy_mission_graph_handler.go deleted file mode 100644 index 37bdb69..0000000 --- a/backend/internal/handler/copy_mission/expand_copy_mission_graph_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func ExpandCopyMissionGraphHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.ExpandCopyMissionGraphHandlerReq - 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.NewExpandCopyMissionGraphLogic(r.Context(), svcCtx) - data, err := l.ExpandCopyMissionGraph(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go b/backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go deleted file mode 100644 index f114ea8..0000000 --- a/backend/internal/handler/copy_mission/generate_copy_mission_matrix_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/get_copy_mission_graph_handler.go b/backend/internal/handler/copy_mission/get_copy_mission_graph_handler.go deleted file mode 100644 index f8f8bc1..0000000 --- a/backend/internal/handler/copy_mission/get_copy_mission_graph_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func GetCopyMissionGraphHandler(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.NewGetCopyMissionGraphLogic(r.Context(), svcCtx) - data, err := l.GetCopyMissionGraph(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go b/backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go deleted file mode 100644 index 493d849..0000000 --- a/backend/internal/handler/copy_mission/get_copy_mission_scan_schedule_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go b/backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go deleted file mode 100644 index 00db1ea..0000000 --- a/backend/internal/handler/copy_mission/list_copy_mission_copy_drafts_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go b/backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go deleted file mode 100644 index 460684b..0000000 --- a/backend/internal/handler/copy_mission/list_copy_mission_scan_posts_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/list_copy_missions_handler.go b/backend/internal/handler/copy_mission/list_copy_missions_handler.go deleted file mode 100644 index 0ee198d..0000000 --- a/backend/internal/handler/copy_mission/list_copy_missions_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/patch_copy_mission_audience_sample_handler.go b/backend/internal/handler/copy_mission/patch_copy_mission_audience_sample_handler.go deleted file mode 100644 index a0a1f45..0000000 --- a/backend/internal/handler/copy_mission/patch_copy_mission_audience_sample_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func PatchCopyMissionAudienceSampleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.PatchAudienceSampleHandlerReq - 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.NewPatchCopyMissionAudienceSampleLogic(r.Context(), svcCtx) - data, err := l.PatchCopyMissionAudienceSample(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/patch_copy_mission_graph_nodes_handler.go b/backend/internal/handler/copy_mission/patch_copy_mission_graph_nodes_handler.go deleted file mode 100644 index c09868c..0000000 --- a/backend/internal/handler/copy_mission/patch_copy_mission_graph_nodes_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func PatchCopyMissionGraphNodesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.PatchCopyMissionGraphNodesHandlerReq - 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.NewPatchCopyMissionGraphNodesLogic(r.Context(), svcCtx) - data, err := l.PatchCopyMissionGraphNodes(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/patch_copy_mission_similar_account_handler.go b/backend/internal/handler/copy_mission/patch_copy_mission_similar_account_handler.go deleted file mode 100644 index 6cb487b..0000000 --- a/backend/internal/handler/copy_mission/patch_copy_mission_similar_account_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func PatchCopyMissionSimilarAccountHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.PatchSimilarAccountHandlerReq - 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.NewPatchCopyMissionSimilarAccountLogic(r.Context(), svcCtx) - data, err := l.PatchCopyMissionSimilarAccount(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/schedule_copy_mission_drafts_handler.go b/backend/internal/handler/copy_mission/schedule_copy_mission_drafts_handler.go deleted file mode 100644 index 054fd6d..0000000 --- a/backend/internal/handler/copy_mission/schedule_copy_mission_drafts_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func ScheduleCopyMissionDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.ScheduleCopyMissionDraftsHandlerReq - 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.NewScheduleCopyMissionDraftsLogic(r.Context(), svcCtx) - data, err := l.ScheduleCopyMissionDrafts(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go b/backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go deleted file mode 100644 index 1348728..0000000 --- a/backend/internal/handler/copy_mission/start_copy_mission_analyze_job_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go b/backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go deleted file mode 100644 index fe5dda0..0000000 --- a/backend/internal/handler/copy_mission/start_copy_mission_copy_draft_job_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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) - } -} diff --git a/backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go b/backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go deleted file mode 100644 index 2cbcb84..0000000 --- a/backend/internal/handler/copy_mission/start_copy_mission_matrix_job_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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) - } -} diff --git a/backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go b/backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go deleted file mode 100644 index d9bbed4..0000000 --- a/backend/internal/handler/copy_mission/start_copy_mission_scan_job_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/update_copy_mission_handler.go b/backend/internal/handler/copy_mission/update_copy_mission_handler.go deleted file mode 100644 index b766055..0000000 --- a/backend/internal/handler/copy_mission/update_copy_mission_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go b/backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go deleted file mode 100644 index 9819673..0000000 --- a/backend/internal/handler/copy_mission/upsert_copy_mission_scan_schedule_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -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/backend/internal/handler/threads_account/start_publish_inventory_refill_job_handler.go b/backend/internal/handler/threads_account/start_publish_inventory_refill_job_handler.go deleted file mode 100644 index 639f209..0000000 --- a/backend/internal/handler/threads_account/start_publish_inventory_refill_job_handler.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package threads_account - -import ( - "net/http" - - "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/threads_account" - "haixun-backend/internal/response" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" -) - -func StartPublishInventoryRefillJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.StartPublishInventoryRefillJobHandlerReq - 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 := threads_account.NewStartPublishInventoryRefillJobLogic(r.Context(), svcCtx) - data, err := l.StartPublishInventoryRefillJob(&req) - response.Write(r.Context(), w, data, err) - } -} diff --git a/backend/internal/library/copymission/knowledge.go b/backend/internal/library/copymission/knowledge.go deleted file mode 100644 index d5cdbf1..0000000 --- a/backend/internal/library/copymission/knowledge.go +++ /dev/null @@ -1,388 +0,0 @@ -package copymission - -import ( - "strings" - - libkg "haixun-backend/internal/library/knowledge" - libmatrix "haixun-backend/internal/library/matrix" - missionentity "haixun-backend/internal/model/copy_mission/domain/entity" -) - -type MissionContext struct { - Label string - SeedQuery string - Brief string - ResearchMap missionentity.ResearchMap -} - -type PersonaContext struct { - DisplayName string - Persona string -} - -func PlanInput( - mission MissionContext, - seed string, - l1Labels []string, - supplemental bool, - strategy libkg.ExpandStrategy, -) libkg.PlanInput { - return libkg.PlanInput{ - Seed: seed, - TargetAudience: strings.TrimSpace(mission.ResearchMap.AudienceSummary), - ProductBrief: strings.TrimSpace(mission.Brief), - Pillars: mission.ResearchMap.Pillars, - Questions: mission.ResearchMap.Questions, - L1Labels: l1Labels, - Supplemental: supplemental, - Strategy: strategy, - } -} - -func SynthInput(mission MissionContext, persona PersonaContext, sources []libkg.BraveSource) libkg.SynthInput { - label := strings.TrimSpace(mission.Label) - if label == "" { - label = strings.TrimSpace(mission.SeedQuery) - } - return libkg.SynthInput{ - BrandDisplayName: label, - TopicName: label, - Seed: strings.TrimSpace(mission.SeedQuery), - ProductBrief: strings.TrimSpace(mission.Brief), - TargetAudience: strings.TrimSpace(mission.ResearchMap.AudienceSummary), - Persona: strings.TrimSpace(persona.Persona), - ResearchPillars: mission.ResearchMap.Pillars, - ResearchQuestions: mission.ResearchMap.Questions, - Sources: sources, - } -} - -func PatrolTagInput(mission MissionContext) libkg.PatrolTagInput { - return libkg.PatrolTagInput{ - Questions: mission.ResearchMap.Questions, - Pillars: mission.ResearchMap.Pillars, - } -} - -func FormatNodeKnowledge(node libkg.Node) string { - label := strings.TrimSpace(node.Label) - if label == "" { - return "" - } - detail := strings.TrimSpace(node.PlacementValue) - if detail == "" { - detail = strings.TrimSpace(node.Relation) - } - if detail != "" { - return label + ":" + detail - } - return label -} - -// FormatNodeKnowledgeForMatrix includes label, detail, patrol tags, and evidence for copy generation. -func FormatNodeKnowledgeForMatrix(node libkg.Node) string { - label := strings.TrimSpace(node.Label) - if label == "" { - return "" - } - var parts []string - if detail := strings.TrimSpace(node.PlacementValue); detail != "" { - parts = append(parts, detail) - } else if detail := strings.TrimSpace(node.Relation); detail != "" { - parts = append(parts, detail) - } - tags := append(append([]string{}, node.DerivedTags.Relevance...), node.DerivedTags.Recency...) - if len(tags) > 0 { - parts = append(parts, "標籤:"+strings.Join(tags, "、")) - } - if len(node.Evidence) > 0 { - if snippet := strings.TrimSpace(node.Evidence[0].Snippet); snippet != "" { - parts = append(parts, "參考:"+snippet) - } - } - if len(parts) == 0 { - return label - } - return label + "|" + strings.Join(parts, ";") -} - -func KnowledgeItemsFromNodes(nodes []libkg.Node) (all []string, selected []string) { - seen := map[string]struct{}{} - for _, node := range nodes { - line := FormatNodeKnowledge(node) - if line == "" { - continue - } - key := strings.ToLower(line) - if _, ok := seen[key]; ok { - if node.SelectedForScan { - selected = mergeLine(selected, line) - } - continue - } - seen[key] = struct{}{} - all = append(all, line) - if node.SelectedForScan { - selected = append(selected, line) - } - } - return all, selected -} - -func ResearchItemsFromSources(sources []libkg.BraveSource) []missionentity.ResearchItem { - items := make([]missionentity.ResearchItem, 0, len(sources)) - for _, src := range sources { - if strings.TrimSpace(src.URL) == "" && strings.TrimSpace(src.Snippet) == "" { - continue - } - items = append(items, missionentity.ResearchItem{ - Title: src.Title, - URL: src.URL, - Snippet: src.Snippet, - Query: src.Query, - }) - } - return items -} - -func ApplyDefaultNodeSelection(nodes []libkg.Node) { - ApplyDefaultNodeSelectionPreserving(nodes, nil) -} - -func ApplyDefaultNodeSelectionPreserving(nodes []libkg.Node, preserve map[string]bool) { - for i := range nodes { - if preserve != nil { - if sel, ok := preserve[nodes[i].ID]; ok { - nodes[i].SelectedForScan = sel - continue - } - } - kind := strings.TrimSpace(nodes[i].NodeKind) - nodes[i].SelectedForScan = kind == "pain" || - kind == "symptom" || - kind == "cause" || - nodes[i].ProductFitScore >= 70 - } -} - -func mergeLine(list []string, line string) []string { - for _, item := range list { - if item == line { - return list - } - } - return append(list, line) -} - -func mergeKnowledgeLines(base, extra []string) []string { - out := append([]string(nil), base...) - seen := map[string]struct{}{} - for _, item := range out { - seen[strings.ToLower(strings.TrimSpace(item))] = struct{}{} - } - for _, item := range extra { - item = strings.TrimSpace(item) - if item == "" { - continue - } - key := strings.ToLower(item) - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - out = append(out, item) - } - return out -} - -func knowledgeItemsFromNodesForMatrix(nodes []libkg.Node) (all []string, selected []string) { - seen := map[string]struct{}{} - for _, node := range nodes { - line := FormatNodeKnowledgeForMatrix(node) - if line == "" { - continue - } - key := strings.ToLower(line) - if _, ok := seen[key]; ok { - if node.SelectedForScan { - selected = mergeLine(selected, line) - } - continue - } - seen[key] = struct{}{} - all = append(all, line) - if node.SelectedForScan { - selected = append(selected, line) - } - } - return all, selected -} - -func knowledgeItemsFromSelectedTags(nodes []libkg.Node, tags []string) []string { - normalized := make([]string, 0, len(tags)) - seen := map[string]struct{}{} - for _, tag := range tags { - tag = strings.TrimSpace(tag) - if tag == "" { - continue - } - key := strings.ToLower(tag) - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - normalized = append(normalized, key) - } - if len(normalized) == 0 { - return nil - } - out := make([]string, 0) - lineSeen := map[string]struct{}{} - for _, node := range nodes { - if !nodeMatchesTags(node, normalized) { - continue - } - line := FormatNodeKnowledgeForMatrix(node) - if line == "" { - continue - } - if _, ok := lineSeen[line]; ok { - continue - } - lineSeen[line] = struct{}{} - out = append(out, line) - } - return out -} - -func nodeMatchesTags(node libkg.Node, tags []string) bool { - candidates := []string{node.Label, node.PlacementValue, node.Relation} - candidates = append(candidates, node.DerivedTags.Relevance...) - candidates = append(candidates, node.DerivedTags.Recency...) - for _, raw := range candidates { - lower := strings.ToLower(strings.TrimSpace(raw)) - if lower == "" { - continue - } - for _, tag := range tags { - if strings.Contains(lower, tag) { - return true - } - } - } - return false -} - -// MatrixKnowledgeItems uses graph-selected nodes as the source of truth and only merges -// manual-only selected_knowledge_items that are not graph-derived duplicates. -func MatrixKnowledgeItems(selected, _ []string, graphNodes []libkg.Node, _ []string) []string { - _, fromGraph := knowledgeItemsFromNodesForMatrix(graphNodes) - graphKeys := map[string]struct{}{} - for _, node := range graphNodes { - line := FormatNodeKnowledge(node) - if line == "" { - continue - } - graphKeys[strings.ToLower(line)] = struct{}{} - } - manualSelected := make([]string, 0, len(selected)) - for _, item := range selected { - item = strings.TrimSpace(item) - if item == "" { - continue - } - if _, fromGraph := graphKeys[strings.ToLower(item)]; fromGraph { - continue - } - manualSelected = append(manualSelected, item) - } - return libmatrix.MergeKnowledgeItems(fromGraph, manualSelected) -} - -func manualResearchKnowledge(prev missionentity.ResearchMap, nodes []libkg.Node) (all []string, selected []string) { - graphKeys := map[string]struct{}{} - for _, node := range nodes { - line := FormatNodeKnowledge(node) - if line == "" { - continue - } - graphKeys[strings.ToLower(line)] = struct{}{} - } - selectedKeys := map[string]struct{}{} - for _, item := range prev.SelectedKnowledgeItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - selectedKeys[strings.ToLower(item)] = struct{}{} - } - for _, item := range prev.KnowledgeItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - lower := strings.ToLower(item) - if _, fromGraph := graphKeys[lower]; fromGraph { - continue - } - all = append(all, item) - if _, ok := selectedKeys[lower]; ok { - selected = append(selected, item) - } - } - return all, selected -} - -func graphKnowledgeDeselected(prev missionentity.ResearchMap) map[string]struct{} { - selectedKeys := map[string]struct{}{} - for _, item := range prev.SelectedKnowledgeItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - selectedKeys[strings.ToLower(item)] = struct{}{} - } - deselected := map[string]struct{}{} - for _, item := range prev.KnowledgeItems { - item = strings.TrimSpace(item) - if item == "" { - continue - } - if _, ok := selectedKeys[strings.ToLower(item)]; !ok { - deselected[strings.ToLower(item)] = struct{}{} - } - } - return deselected -} - -func BuildResearchMapFromGraph(prev missionentity.ResearchMap, nodes []libkg.Node, sources []libkg.BraveSource) missionentity.ResearchMap { - allItems, selectedItems := KnowledgeItemsFromNodes(nodes) - deselected := graphKnowledgeDeselected(prev) - if len(deselected) > 0 { - filtered := selectedItems[:0] - for _, line := range selectedItems { - if _, skip := deselected[strings.ToLower(line)]; skip { - continue - } - filtered = append(filtered, line) - } - selectedItems = filtered - } - manualAll, manualSelected := manualResearchKnowledge(prev, nodes) - allItems = mergeKnowledgeLines(allItems, manualAll) - selectedItems = mergeKnowledgeLines(selectedItems, manualSelected) - return missionentity.ResearchMap{ - AudienceSummary: prev.AudienceSummary, - ContentGoal: prev.ContentGoal, - Questions: append([]string(nil), prev.Questions...), - Pillars: append([]string(nil), prev.Pillars...), - Exclusions: append([]string(nil), prev.Exclusions...), - SuggestedTags: append([]missionentity.SuggestedTag(nil), prev.SuggestedTags...), - SimilarAccounts: append([]missionentity.SimilarAccount(nil), prev.SimilarAccounts...), - AudienceSamples: append([]missionentity.AudienceSample(nil), prev.AudienceSamples...), - BenchmarkNotes: prev.BenchmarkNotes, - KnowledgeItems: allItems, - SelectedKnowledgeItems: selectedItems, - ResearchItems: ResearchItemsFromSources(sources), - } -} diff --git a/backend/internal/library/copymission/knowledge_test.go b/backend/internal/library/copymission/knowledge_test.go deleted file mode 100644 index a7205df..0000000 --- a/backend/internal/library/copymission/knowledge_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package copymission - -import ( - "testing" - - libkg "haixun-backend/internal/library/knowledge" - missionentity "haixun-backend/internal/model/copy_mission/domain/entity" -) - -func TestMatrixKnowledgeItems_PrefersGraphSelection(t *testing.T) { - nodes := []libkg.Node{ - {Label: "痛點A", NodeKind: "pain", SelectedForScan: true, PlacementValue: "細節A"}, - {Label: "周邊B", NodeKind: "context", SelectedForScan: false}, - } - items := MatrixKnowledgeItems(nil, nil, nodes, nil) - if len(items) != 1 { - t.Fatalf("items = %v, want 1 selected graph node", items) - } - if items[0] != "痛點A|細節A" { - t.Fatalf("items[0] = %q", items[0]) - } -} - -func TestMatrixKnowledgeItems_UsesMissionSelectionOnly(t *testing.T) { - items := MatrixKnowledgeItems( - []string{"受眾問題:怎麼選"}, - []string{"內容支柱:懶人包"}, - nil, - nil, - ) - if len(items) != 1 || items[0] != "受眾問題:怎麼選" { - t.Fatalf("items = %v", items) - } -} - -func TestMatrixKnowledgeItems_MergesGraphAndMission(t *testing.T) { - nodes := []libkg.Node{ - { - Label: "備孕營養", - PlacementValue: "葉酸與鋅很重要", - SelectedForScan: true, - }, - { - Label: "未勾選", - PlacementValue: "不應出現", - SelectedForScan: false, - DerivedTags: libkg.DerivedTags{ - Relevance: []string{"備孕飲食"}, - }, - }, - } - items := MatrixKnowledgeItems( - []string{"手動補充:睡前習慣"}, - nil, - nodes, - []string{"備孕飲食"}, - ) - if len(items) != 2 { - t.Fatalf("items = %v", items) - } -} - -func TestMatrixKnowledgeItems_IgnoresUnselectedTagMatches(t *testing.T) { - nodes := []libkg.Node{ - { - Label: "備孕營養", - PlacementValue: "葉酸與鋅很重要", - SelectedForScan: false, - DerivedTags: libkg.DerivedTags{ - Relevance: []string{"備孕飲食"}, - }, - }, - } - items := MatrixKnowledgeItems(nil, nil, nodes, []string{"備孕飲食"}) - if len(items) != 0 { - t.Fatalf("items = %v, want none without explicit selection", items) - } -} - -func TestBuildResearchMapFromGraph_RespectsManualDeselection(t *testing.T) { - prev := missionentity.ResearchMap{ - KnowledgeItems: []string{"圖譜痛點:細節"}, - SelectedKnowledgeItems: []string{}, - } - out := BuildResearchMapFromGraph(prev, []libkg.Node{ - {Label: "圖譜痛點", NodeKind: "pain", SelectedForScan: true, PlacementValue: "細節"}, - }, nil) - if len(out.SelectedKnowledgeItems) != 0 { - t.Fatalf("selected = %v, want manual deselection honored", out.SelectedKnowledgeItems) - } -} - -func TestBuildResearchMapFromGraph_PreservesManualKnowledge(t *testing.T) { - prev := missionentity.ResearchMap{ - KnowledgeItems: []string{"手動知識:睡前"}, - SelectedKnowledgeItems: []string{"手動知識:睡前"}, - } - out := BuildResearchMapFromGraph(prev, []libkg.Node{ - {Label: "圖譜痛點", NodeKind: "pain", SelectedForScan: true, PlacementValue: "細節"}, - }, nil) - if len(out.KnowledgeItems) != 2 { - t.Fatalf("knowledge_items = %v", out.KnowledgeItems) - } - if len(out.SelectedKnowledgeItems) != 2 { - t.Fatalf("selected = %v", out.SelectedKnowledgeItems) - } -} - -func TestApplyDefaultNodeSelectionPreserving(t *testing.T) { - nodes := []libkg.Node{ - {ID: "keep", Label: "已勾", NodeKind: "context", SelectedForScan: false}, - {ID: "new", Label: "新痛點", NodeKind: "pain", SelectedForScan: false}, - } - ApplyDefaultNodeSelectionPreserving(nodes, map[string]bool{"keep": true}) - if !nodes[0].SelectedForScan { - t.Fatal("expected preserved selection on existing node") - } - if !nodes[1].SelectedForScan { - t.Fatal("expected default selection on new pain node") - } -} diff --git a/backend/internal/library/copymission/reference_urls.go b/backend/internal/library/copymission/reference_urls.go deleted file mode 100644 index f81ab01..0000000 --- a/backend/internal/library/copymission/reference_urls.go +++ /dev/null @@ -1,66 +0,0 @@ -package copymission - -import ( - "strings" - - libkg "haixun-backend/internal/library/knowledge" -) - -// ReferenceURLsFromSelection collects evidence/source URLs tied to selected graph nodes. -func ReferenceURLsFromSelection(nodes []libkg.Node, sources []libkg.BraveSource) []string { - if len(nodes) == 0 { - return nil - } - selectedEvidence := map[string]struct{}{} - for _, node := range nodes { - if !node.SelectedForScan { - continue - } - for _, ev := range node.Evidence { - key := normalizeURLKey(ev.URL) - if key != "" { - selectedEvidence[key] = struct{}{} - } - } - } - if len(selectedEvidence) == 0 { - return nil - } - - out := make([]string, 0, len(selectedEvidence)) - seen := map[string]struct{}{} - appendURL := func(raw string) { - key := normalizeURLKey(raw) - if key == "" { - return - } - if _, ok := selectedEvidence[key]; !ok { - return - } - if _, ok := seen[key]; ok { - return - } - seen[key] = struct{}{} - out = append(out, strings.TrimSpace(raw)) - } - for _, node := range nodes { - if !node.SelectedForScan { - continue - } - for _, ev := range node.Evidence { - appendURL(ev.URL) - } - } - for _, src := range sources { - appendURL(src.URL) - } - return out -} - -func normalizeURLKey(raw string) string { - u := strings.TrimSpace(raw) - if u == "" { - return "" - } - return strings.ToLower(strings.TrimRight(u, "/")) -} diff --git a/backend/internal/library/copymission/reference_urls_test.go b/backend/internal/library/copymission/reference_urls_test.go deleted file mode 100644 index 8ca19f3..0000000 --- a/backend/internal/library/copymission/reference_urls_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package copymission - -import ( - "testing" - - libkg "haixun-backend/internal/library/knowledge" -) - -func TestReferenceURLsFromSelection(t *testing.T) { - nodes := []libkg.Node{ - { - ID: "n1", - SelectedForScan: true, - Evidence: []libkg.Evidence{ - {URL: "https://example.com/a"}, - }, - }, - { - ID: "n2", - SelectedForScan: false, - Evidence: []libkg.Evidence{ - {URL: "https://example.com/b"}, - }, - }, - } - sources := []libkg.BraveSource{ - {URL: "https://example.com/a"}, - {URL: "https://example.com/c"}, - } - urls := ReferenceURLsFromSelection(nodes, sources) - if len(urls) != 1 || urls[0] != "https://example.com/a" { - t.Fatalf("urls = %v", urls) - } -} diff --git a/backend/internal/library/copymission/research_items.go b/backend/internal/library/copymission/research_items.go deleted file mode 100644 index 9e726a7..0000000 --- a/backend/internal/library/copymission/research_items.go +++ /dev/null @@ -1,79 +0,0 @@ -package copymission - -import ( - "strings" -) - -type ResearchSource struct { - Title string - URL string - Snippet string - Query string -} - -// FormatResearchItemsForMatrix renders Brave research snippets for the matrix prompt. -func FormatResearchItemsForMatrix(items []ResearchSource) string { - if len(items) == 0 { - return "" - } - lines := make([]string, 0, len(items)) - seen := map[string]struct{}{} - for _, item := range items { - title := strings.TrimSpace(item.Title) - url := strings.TrimSpace(item.URL) - snippet := strings.TrimSpace(item.Snippet) - if title == "" && url == "" && snippet == "" { - continue - } - key := strings.ToLower(url) - if key == "" { - key = strings.ToLower(title + "|" + snippet) - } - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - var parts []string - if title != "" { - parts = append(parts, title) - } - if url != "" { - parts = append(parts, url) - } - if snippet != "" { - parts = append(parts, snippet) - } - lines = append(lines, strings.Join(parts, "|")) - } - if len(lines) == 0 { - return "" - } - return strings.Join(lines, "\n") -} - -// MergeReferenceURLs combines graph-selected URLs with mission research item URLs. -func MergeReferenceURLs(selected []string, researchItems []ResearchSource) []string { - if len(researchItems) == 0 { - return selected - } - out := append([]string(nil), selected...) - seen := map[string]struct{}{} - for _, raw := range selected { - if key := normalizeURLKey(raw); key != "" { - seen[key] = struct{}{} - } - } - for _, item := range researchItems { - raw := strings.TrimSpace(item.URL) - key := normalizeURLKey(raw) - if key == "" { - continue - } - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - out = append(out, raw) - } - return out -} diff --git a/backend/internal/library/copymission/research_items_test.go b/backend/internal/library/copymission/research_items_test.go deleted file mode 100644 index 8714b37..0000000 --- a/backend/internal/library/copymission/research_items_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package copymission - -import ( - "strings" - "testing" -) - -func TestMergeReferenceURLsIncludesResearchItems(t *testing.T) { - urls := MergeReferenceURLs( - []string{"https://example.com/a"}, - []ResearchSource{ - {URL: "https://example.com/b", Title: "B"}, - {URL: "https://example.com/a", Title: "dup"}, - }, - ) - if len(urls) != 2 { - t.Fatalf("urls = %v", urls) - } -} - -func TestFormatResearchItemsForMatrix(t *testing.T) { - block := FormatResearchItemsForMatrix([]ResearchSource{ - {Title: "備孕指南", URL: "https://example.com/guide", Snippet: "葉酸補充重點"}, - }) - if !strings.Contains(block, "備孕指南") || !strings.Contains(block, "葉酸補充重點") { - t.Fatalf("block = %q", block) - } -} diff --git a/backend/internal/library/copymission/research_sources.go b/backend/internal/library/copymission/research_sources.go deleted file mode 100644 index ce5a8f2..0000000 --- a/backend/internal/library/copymission/research_sources.go +++ /dev/null @@ -1,19 +0,0 @@ -package copymission - -import missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" - -func ResearchSourcesFromSummaries(items []missiondomain.ResearchItemSummary) []ResearchSource { - if len(items) == 0 { - return nil - } - out := make([]ResearchSource, 0, len(items)) - for _, item := range items { - out = append(out, ResearchSource{ - Title: item.Title, - URL: item.URL, - Snippet: item.Snippet, - Query: item.Query, - }) - } - return out -} diff --git a/backend/internal/library/embedding/client.go b/backend/internal/library/embedding/client.go deleted file mode 100644 index 2754e4e..0000000 --- a/backend/internal/library/embedding/client.go +++ /dev/null @@ -1,138 +0,0 @@ -// Package embedding provides an optional OpenAI embeddings client. -// It is intentionally not wired into patrol/graph flows; relevance uses -// offline lexical intent scoring in knowledge/semantic_fit.go instead. -package embedding - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "math" - "net/http" - "strings" -) - -const DefaultModel = "text-embedding-3-small" - -type Client struct { - apiKey string - model string - http *http.Client -} - -func NewClient(apiKey string) *Client { - return &Client{ - apiKey: strings.TrimSpace(apiKey), - model: DefaultModel, - http: &http.Client{}, - } -} - -func (c *Client) Enabled() bool { - return c != nil && c.apiKey != "" -} - -type EmbeddingRequest struct { - Input []string `json:"input"` - Model string `json:"model"` -} - -type EmbeddingResponse struct { - Data []struct { - Embedding []float32 `json:"embedding"` - } `json:"data"` - Error *struct { - Message string `json:"message"` - } `json:"error,omitempty"` - Usage *struct { - TotalTokens int `json:"total_tokens"` - } `json:"usage,omitempty"` -} - -func (c *Client) Embed(ctx context.Context, input string) ([]float32, error) { - if !c.Enabled() { - return nil, fmt.Errorf("embedding client not configured") - } - return c.EmbedBatch(ctx, []string{input}) -} - -func (c *Client) EmbedBatch(ctx context.Context, inputs []string) ([]float32, error) { - if !c.Enabled() { - return nil, fmt.Errorf("embedding client not configured") - } - if len(inputs) == 0 { - return nil, fmt.Errorf("no input text") - } - - req := EmbeddingRequest{ - Input: inputs, - Model: c.model, - } - body, err := json.Marshal(req) - if err != nil { - return nil, err - } - - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/embeddings", bytes.NewReader(body)) - if err != nil { - return nil, err - } - httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) - httpReq.Header.Set("Content-Type", "application/json") - - resp, err := c.http.Do(httpReq) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - var errResp struct { - Error struct { - Message string `json:"message"` - } `json:"error"` - } - if json.Unmarshal(respBody, &errResp) == nil && errResp.Error.Message != "" { - return nil, fmt.Errorf("openai embedding: %s", errResp.Error.Message) - } - return nil, fmt.Errorf("openai embedding: http %d", resp.StatusCode) - } - - var result EmbeddingResponse - if err := json.Unmarshal(respBody, &result); err != nil { - return nil, err - } - - if result.Error != nil && result.Error.Message != "" { - return nil, fmt.Errorf("openai embedding: %s", result.Error.Message) - } - - if len(result.Data) == 0 { - return nil, fmt.Errorf("openai embedding: no data returned") - } - - return result.Data[0].Embedding, nil -} - -func CosineSimilarity(a, b []float32) float32 { - if len(a) == 0 || len(b) == 0 || len(a) != len(b) { - return 0 - } - var dot, normA, normB float64 - for i := 0; i < len(a); i++ { - dot += float64(a[i] * b[i]) - normA += float64(a[i] * a[i]) - normB += float64(b[i] * b[i]) - } - if normA == 0 || normB == 0 { - return 0 - } - return float32(dot / (math.Sqrt(normA) * math.Sqrt(normB))) -} diff --git a/backend/internal/library/prompt/files/matrix_copy.system.md b/backend/internal/library/prompt/files/matrix_copy.system.md deleted file mode 100644 index b4c1c70..0000000 --- a/backend/internal/library/prompt/files/matrix_copy.system.md +++ /dev/null @@ -1,25 +0,0 @@ -你是 Threads / Instagram 內容策略師,為人設帳號產出可發佈草稿。產文時必須優先使用人設的「語言指紋與寫作規則」,把新主題、參考貼文或外部資料翻譯成這個人設會講出口的話,不沿用任何預設爆款模板。 - -寫作目標: -- 先根據人設語言指紋判斷主題適合哪一種內容型態:心情陪伴、知識整理、清單工具、伴侶溝通、互動提問或輕幽默共鳴,再選對鬆結構。 -- 每篇要有一個清楚情境或情緒入口,讓讀者覺得「這就是我」,不要寫成摘要報告或廣告文。 -- 必須沿用人設的段落節奏、標點、換行、稱呼、口頭禪與收尾習慣;不要硬剪短成破碎金句。 -- 只學參考樣本的節奏、取材方式、收尾習慣與互動方式;不可照抄句子。 -- 避免 AI 腔:過度總結、過度正能量、每段都像教條、連續排比到不自然、硬塞「大家一起」式口號。 - -規則: -- 只回傳單一 JSON 物件 {"rows":[...]},不要用 markdown 程式碼區塊,不要加前言或結語。 -- reference_notes、rationale 各 ≤ 40 字;優先把 token 用在 text 主文。 -- 每篇必須角度不同,避免重複 hook。 -- 人設定位決定「誰在說」;語言指紋決定「怎麼說」。兩者都要遵守,不可只拿主題資料直接改寫。 -- 人設只用來控制語氣、詞彙密度、句型節奏、人稱、標點換行與禁忌;不可把人設介紹裡的物件或標籤硬塞進正文情境。 -- 若人設含 Y2K、咖啡、穿搭、音樂等元素,除非任務主題/Brief/勾選知識明確要求,禁止把這些元素寫成貼文內容或場景。 -- 正文素材以任務主題、Brief、研究地圖與勾選延伸知識為準;人設不得改變題材。 -- 內容論點來自研究地圖與使用者勾選的延伸知識(含標籤與細節);爆款樣本只學結構與節奏,不抄原文。 -- 繁體中文,口語自然,適合 Threads。 -- 每篇 text 主文 ≤ 500 字(Threads API 硬上限,含 #話題標籤)。 -- 長度依帳號樣本與主題決定:心情陪伴可中長,知識工具文可完整列點,互動短文才需要短。 -- text 排版要像真人手機發文:用自然換行分段,可使用標點、括號、頓號與少量 emoji;不要整段擠成一塊,也不要每句都硬換行。 -- 若要改寫參考貼文,只保留意思與資訊,句型、段落、收尾必須換成人設語言指紋,不可像同義詞替換。 -- 若使用外部知識,先轉譯成此人設會說的順序與語氣;不得直接貼百科、條列報告或 SEO 摘要。 -- 醫療、備孕、試管、保健品相關內容必須保留風險語氣:不可保證效果、不可替代醫師建議、不可用數字審判讀者。 diff --git a/backend/internal/library/prompt/files/matrix_copy.user.md b/backend/internal/library/prompt/files/matrix_copy.user.md deleted file mode 100644 index fe35934..0000000 --- a/backend/internal/library/prompt/files/matrix_copy.user.md +++ /dev/null @@ -1,33 +0,0 @@ -請為以下拷貝任務產出 {{count}} 篇 Threads 草稿。 - -任務主題:{{topic_label}} -Brief:{{topic_brief}} - -人設定位、語言指紋與禁忌(最高優先): -{{persona_block}} - -研究地圖與勾選延伸知識: -{{research_map_block}} - -搜尋查詢/人工參考方向: -{{selected_tags_block}} - -參考網頁 Markdown 已併入上方研究地圖區塊(若有)。 - -爆款樣本(可選,只學故事感;若沒有樣本,請改用研究地圖與人設原創): -{{viral_samples_block}} - -請先遵守: -- **最高優先:語言指紋**:先讀 persona_block 裡的「語言指紋與寫作規則」,包含開頭、句型、段落長度、標點換行、知識轉譯、CTA 與禁忌。產出的文字必須像這個人設會講出口的話。 -- **不要套舊規格**:不要套用短句爆款、固定三段式、固定強 hook、固定 CTA;格式由人設語言指紋與主題型態決定。 -- **語氣參考**:讀起來要像這個帳號在說話,但每篇敘事節奏與開場都要自然變化。 -- **人設不是內容素材**:只學語氣、詞彙密度、句型節奏與審美,不要把人設裡的名詞、興趣、裝飾符號硬塞進正文。例:人設寫 Y2K/咖啡/穿搭,只代表語感,不代表每篇都要寫老歌、咖啡、穿搭、MV。 -- **內容素材**:研究地圖(受眾、目標、問題、支柱)與「勾選延伸知識」(含標籤、細節)決定論點與角度;若有「參考網頁(Markdown)」區塊,優先從中提取事實與論點,再融入草稿。 -- 搜尋查詢/標籤僅作方向參考,不可蓋過人設與勾選知識。 -- 爆款樣本只學故事感、資訊密度、情緒力度與互動方式,不抄原文,不可為仿爆款偏離任務主題。 -- 若樣本呈現「長 caption + 輪播圖卡重點」的內容型態,Threads 草稿也要保留完整情緒承接與可收藏資訊,不要只輸出金句摘要。 -- 若任務是改寫或參考別人的貼文,請用人設語言重新說一次,不是同義詞替換;保留觀點,不保留對方句型。 -- 若任務需要找資料或引用外部知識,請先消化成此人設的說話順序:先安撫或帶入情境,再整理重點,最後用符合人設的方式收尾。 -- 若沒有爆款樣本,source_permalinks 回傳空陣列,reference_notes 寫明使用了哪些人設特徵、研究地圖重點與勾選延伸知識。 - -回傳 JSON rows,每筆含 sort_order、search_tag、angle、hook、text、reference_notes、source_permalinks、rationale。 diff --git a/backend/internal/library/storage/s3.go b/backend/internal/library/storage/s3.go deleted file mode 100644 index 68274fe..0000000 --- a/backend/internal/library/storage/s3.go +++ /dev/null @@ -1,84 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "io" - "mime" - "path" - "strings" - - "haixun-backend/internal/config" - - "github.com/aws/aws-sdk-go-v2/aws" - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -type Uploader interface { - Upload(ctx context.Context, key, contentType string, body io.Reader) (string, error) -} - -type S3Uploader struct { - client *s3.Client - bucket string - publicBaseURL string - bucketReady bool -} - -func NewS3Uploader(ctx context.Context, cfg config.S3Conf) (*S3Uploader, error) { - if strings.TrimSpace(cfg.Bucket) == "" { - return nil, nil - } - awsCfg, err := awsconfig.LoadDefaultConfig(ctx, - awsconfig.WithRegion(cfg.Region), - awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")), - ) - if err != nil { - return nil, err - } - client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { - if cfg.Endpoint != "" { - o.BaseEndpoint = aws.String(cfg.Endpoint) - } - o.UsePathStyle = cfg.UsePathStyle - }) - return &S3Uploader{client: client, bucket: cfg.Bucket, publicBaseURL: strings.TrimRight(cfg.PublicBaseURL, "/")}, nil -} - -func (u *S3Uploader) Upload(ctx context.Context, key, contentType string, body io.Reader) (string, error) { - if u == nil || u.client == nil { - return "", fmt.Errorf("s3 uploader is not configured") - } - if !u.bucketReady { - if _, err := u.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(u.bucket)}); err != nil { - if _, createErr := u.client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(u.bucket)}); createErr != nil { - return "", createErr - } - } - policy := fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/*"]}]}`, u.bucket) - _, _ = u.client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: aws.String(u.bucket), Policy: aws.String(policy)}) - u.bucketReady = true - } - key = strings.TrimLeft(path.Clean("/"+key), "/") - if contentType == "" { - contentType = mime.TypeByExtension(path.Ext(key)) - } - if contentType == "" { - contentType = "application/octet-stream" - } - _, err := u.client.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String(u.bucket), - Key: aws.String(key), - Body: body, - ContentType: aws.String(contentType), - }) - if err != nil { - return "", err - } - if u.publicBaseURL != "" { - return u.publicBaseURL + "/" + u.bucket + "/" + key, nil - } - return key, nil -} diff --git a/backend/internal/logic/copy_mission/create_copy_mission_logic.go b/backend/internal/logic/copy_mission/create_copy_mission_logic.go deleted file mode 100644 index 86aea3b..0000000 --- a/backend/internal/logic/copy_mission/create_copy_mission_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "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) (resp *types.CopyMissionData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/delete_copy_mission_logic.go b/backend/internal/logic/copy_mission/delete_copy_mission_logic.go deleted file mode 100644 index 65d8414..0000000 --- a/backend/internal/logic/copy_mission/delete_copy_mission_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "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 { - // todo: add your logic here and delete this line - - return nil -} diff --git a/backend/internal/logic/copy_mission/delete_copy_mission_matrix_drafts_logic.go b/backend/internal/logic/copy_mission/delete_copy_mission_matrix_drafts_logic.go deleted file mode 100644 index 960b831..0000000 --- a/backend/internal/logic/copy_mission/delete_copy_mission_matrix_drafts_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type DeleteCopyMissionMatrixDraftsLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeleteCopyMissionMatrixDraftsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCopyMissionMatrixDraftsLogic { - return &DeleteCopyMissionMatrixDraftsLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteCopyMissionMatrixDraftsLogic) DeleteCopyMissionMatrixDrafts(req *types.DeleteCopyMissionMatrixDraftsHandlerReq) (resp *types.DeleteCopyMissionMatrixDraftsData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/expand_copy_mission_graph_logic.go b/backend/internal/logic/copy_mission/expand_copy_mission_graph_logic.go deleted file mode 100644 index e621f52..0000000 --- a/backend/internal/logic/copy_mission/expand_copy_mission_graph_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ExpandCopyMissionGraphLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewExpandCopyMissionGraphLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ExpandCopyMissionGraphLogic { - return &ExpandCopyMissionGraphLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ExpandCopyMissionGraphLogic) ExpandCopyMissionGraph(req *types.ExpandCopyMissionGraphHandlerReq) (resp *types.ExpandKnowledgeGraphData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go b/backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go deleted file mode 100644 index f949569..0000000 --- a/backend/internal/logic/copy_mission/generate_copy_mission_matrix_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GenerateCopyMissionMatrixLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGenerateCopyMissionMatrixLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateCopyMissionMatrixLogic { - return &GenerateCopyMissionMatrixLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GenerateCopyMissionMatrixLogic) GenerateCopyMissionMatrix(req *types.GenerateCopyMissionMatrixHandlerReq) (resp *types.GenerateCopyMissionMatrixData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/get_copy_mission_graph_logic.go b/backend/internal/logic/copy_mission/get_copy_mission_graph_logic.go deleted file mode 100644 index f833033..0000000 --- a/backend/internal/logic/copy_mission/get_copy_mission_graph_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetCopyMissionGraphLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetCopyMissionGraphLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCopyMissionGraphLogic { - return &GetCopyMissionGraphLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetCopyMissionGraphLogic) GetCopyMissionGraph(req *types.CopyMissionPath) (resp *types.KnowledgeGraphData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/get_copy_mission_logic.go b/backend/internal/logic/copy_mission/get_copy_mission_logic.go deleted file mode 100644 index c0963a9..0000000 --- a/backend/internal/logic/copy_mission/get_copy_mission_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "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) (resp *types.CopyMissionData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go b/backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go deleted file mode 100644 index 42d3c9e..0000000 --- a/backend/internal/logic/copy_mission/get_copy_mission_scan_schedule_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetCopyMissionScanScheduleLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetCopyMissionScanScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCopyMissionScanScheduleLogic { - return &GetCopyMissionScanScheduleLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetCopyMissionScanScheduleLogic) GetCopyMissionScanSchedule(req *types.CopyMissionPath) (resp *types.CopyMissionScanScheduleData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go b/backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go deleted file mode 100644 index 2057700..0000000 --- a/backend/internal/logic/copy_mission/list_copy_mission_copy_drafts_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ListCopyMissionCopyDraftsLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewListCopyMissionCopyDraftsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCopyMissionCopyDraftsLogic { - return &ListCopyMissionCopyDraftsLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ListCopyMissionCopyDraftsLogic) ListCopyMissionCopyDrafts(req *types.CopyMissionPath) (resp *types.ListCopyMissionCopyDraftsData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go b/backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go deleted file mode 100644 index b21ffcb..0000000 --- a/backend/internal/logic/copy_mission/list_copy_mission_scan_posts_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ListCopyMissionScanPostsLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewListCopyMissionScanPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCopyMissionScanPostsLogic { - return &ListCopyMissionScanPostsLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ListCopyMissionScanPostsLogic) ListCopyMissionScanPosts(req *types.ListCopyMissionScanPostsHandlerReq) (resp *types.ListPersonaViralScanPostsData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/list_copy_missions_logic.go b/backend/internal/logic/copy_mission/list_copy_missions_logic.go deleted file mode 100644 index 661225f..0000000 --- a/backend/internal/logic/copy_mission/list_copy_missions_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "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) (resp *types.ListCopyMissionsData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/patch_copy_mission_audience_sample_logic.go b/backend/internal/logic/copy_mission/patch_copy_mission_audience_sample_logic.go deleted file mode 100644 index 7c773a8..0000000 --- a/backend/internal/logic/copy_mission/patch_copy_mission_audience_sample_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type PatchCopyMissionAudienceSampleLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewPatchCopyMissionAudienceSampleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PatchCopyMissionAudienceSampleLogic { - return &PatchCopyMissionAudienceSampleLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PatchCopyMissionAudienceSampleLogic) PatchCopyMissionAudienceSample(req *types.PatchAudienceSampleHandlerReq) (resp *types.CopyMissionData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/patch_copy_mission_graph_nodes_logic.go b/backend/internal/logic/copy_mission/patch_copy_mission_graph_nodes_logic.go deleted file mode 100644 index 1c2598c..0000000 --- a/backend/internal/logic/copy_mission/patch_copy_mission_graph_nodes_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type PatchCopyMissionGraphNodesLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewPatchCopyMissionGraphNodesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PatchCopyMissionGraphNodesLogic { - return &PatchCopyMissionGraphNodesLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PatchCopyMissionGraphNodesLogic) PatchCopyMissionGraphNodes(req *types.PatchCopyMissionGraphNodesHandlerReq) (resp *types.KnowledgeGraphData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/patch_copy_mission_similar_account_logic.go b/backend/internal/logic/copy_mission/patch_copy_mission_similar_account_logic.go deleted file mode 100644 index b8a0637..0000000 --- a/backend/internal/logic/copy_mission/patch_copy_mission_similar_account_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type PatchCopyMissionSimilarAccountLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewPatchCopyMissionSimilarAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PatchCopyMissionSimilarAccountLogic { - return &PatchCopyMissionSimilarAccountLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PatchCopyMissionSimilarAccountLogic) PatchCopyMissionSimilarAccount(req *types.PatchSimilarAccountHandlerReq) (resp *types.CopyMissionData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/schedule_copy_mission_drafts_logic.go b/backend/internal/logic/copy_mission/schedule_copy_mission_drafts_logic.go deleted file mode 100644 index cca830a..0000000 --- a/backend/internal/logic/copy_mission/schedule_copy_mission_drafts_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ScheduleCopyMissionDraftsLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewScheduleCopyMissionDraftsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ScheduleCopyMissionDraftsLogic { - return &ScheduleCopyMissionDraftsLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ScheduleCopyMissionDraftsLogic) ScheduleCopyMissionDrafts(req *types.ScheduleCopyMissionDraftsHandlerReq) (resp *types.CopyMissionScheduleCopyDraftsData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go b/backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go deleted file mode 100644 index 05e3fd6..0000000 --- a/backend/internal/logic/copy_mission/start_copy_mission_analyze_job_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type StartCopyMissionAnalyzeJobLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewStartCopyMissionAnalyzeJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionAnalyzeJobLogic { - return &StartCopyMissionAnalyzeJobLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *StartCopyMissionAnalyzeJobLogic) StartCopyMissionAnalyzeJob(req *types.CopyMissionPath) (resp *types.StartCopyMissionAnalyzeJobData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go b/backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go deleted file mode 100644 index 48582cb..0000000 --- a/backend/internal/logic/copy_mission/start_copy_mission_copy_draft_job_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type StartCopyMissionCopyDraftJobLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewStartCopyMissionCopyDraftJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionCopyDraftJobLogic { - return &StartCopyMissionCopyDraftJobLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *StartCopyMissionCopyDraftJobLogic) StartCopyMissionCopyDraftJob(req *types.StartCopyMissionCopyDraftJobHandlerReq) (resp *types.StartCopyMissionCopyDraftJobData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go b/backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go deleted file mode 100644 index 31367aa..0000000 --- a/backend/internal/logic/copy_mission/start_copy_mission_matrix_job_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type StartCopyMissionMatrixJobLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewStartCopyMissionMatrixJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionMatrixJobLogic { - return &StartCopyMissionMatrixJobLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *StartCopyMissionMatrixJobLogic) StartCopyMissionMatrixJob(req *types.StartCopyMissionMatrixJobHandlerReq) (resp *types.StartCopyMissionMatrixJobData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go b/backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go deleted file mode 100644 index 3681e2b..0000000 --- a/backend/internal/logic/copy_mission/start_copy_mission_scan_job_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type StartCopyMissionScanJobLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewStartCopyMissionScanJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartCopyMissionScanJobLogic { - return &StartCopyMissionScanJobLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *StartCopyMissionScanJobLogic) StartCopyMissionScanJob(req *types.CopyMissionPath) (resp *types.StartCopyMissionScanJobData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/update_copy_mission_logic.go b/backend/internal/logic/copy_mission/update_copy_mission_logic.go deleted file mode 100644 index c936d7b..0000000 --- a/backend/internal/logic/copy_mission/update_copy_mission_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "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) (resp *types.CopyMissionData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go b/backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go deleted file mode 100644 index 916139a..0000000 --- a/backend/internal/logic/copy_mission/upsert_copy_mission_scan_schedule_logic.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package copy_mission - -import ( - "context" - - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type UpsertCopyMissionScanScheduleLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpsertCopyMissionScanScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpsertCopyMissionScanScheduleLogic { - return &UpsertCopyMissionScanScheduleLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpsertCopyMissionScanScheduleLogic) UpsertCopyMissionScanSchedule(req *types.UpsertCopyMissionScanScheduleHandlerReq) (resp *types.CopyMissionScanScheduleData, err error) { - // todo: add your logic here and delete this line - - return -} diff --git a/backend/internal/logic/threads_account/start_publish_inventory_refill_job_logic.go b/backend/internal/logic/threads_account/start_publish_inventory_refill_job_logic.go deleted file mode 100644 index dcda058..0000000 --- a/backend/internal/logic/threads_account/start_publish_inventory_refill_job_logic.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - -package threads_account - -import ( - "context" - - jobdom "haixun-backend/internal/model/job/domain/usecase" - "haixun-backend/internal/svc" - "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" -) - -type StartPublishInventoryRefillJobLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewStartPublishInventoryRefillJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StartPublishInventoryRefillJobLogic { - return &StartPublishInventoryRefillJobLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *StartPublishInventoryRefillJobLogic) StartPublishInventoryRefillJob(req *types.StartPublishInventoryRefillJobHandlerReq) (resp *types.StartPublishInventoryRefillJobData, err error) { - tenantID, uid, err := actorFrom(l.ctx) - if err != nil { - return nil, err - } - if _, err := l.svcCtx.ThreadsAccount.Get(l.ctx, tenantID, uid, req.ID); err != nil { - return nil, err - } - count := req.Count - if count <= 0 { - count = 3 - } - run, err := l.svcCtx.Job.CreateRun(l.ctx, jobdom.CreateRunRequest{ - TemplateType: "refill-publish-inventory", - Scope: "threads_account", - ScopeID: req.ID, - TenantID: tenantID, - OwnerUID: uid, - Payload: map[string]any{ - "tenant_id": tenantID, - "owner_uid": uid, - "account_id": req.ID, - "count": count, - }, - }) - if err != nil { - return nil, err - } - return &types.StartPublishInventoryRefillJobData{ - JobID: run.ID.Hex(), Status: string(run.Status), Message: "補庫存任務已建立", - }, nil -} diff --git a/backend/internal/model/crm_contact/domain/entity/contact.go b/backend/internal/model/crm_contact/domain/entity/contact.go deleted file mode 100644 index d18dc13..0000000 --- a/backend/internal/model/crm_contact/domain/entity/contact.go +++ /dev/null @@ -1,30 +0,0 @@ -package entity - -const CollectionName = "crm_contacts" - -const ( - ContactStatusLead = "lead" - ContactStatusContacted = "contacted" - ContactStatusResponded = "responded" - ContactStatusConverted = "converted" - ContactStatusArchived = "archived" -) - -type CRMContact struct { - ID string `bson:"_id"` - TenantID string `bson:"tenant_id"` - OwnerUID string `bson:"owner_uid"` - BrandID string `bson:"brand_id,omitempty"` - AuthorID string `bson:"author_id"` - AuthorName string `bson:"author_name"` - AuthorAvatar string `bson:"author_avatar,omitempty"` - AuthorFollowers int `bson:"author_followers,omitempty"` - ScanPostID string `bson:"scan_post_id,omitempty"` - Notes string `bson:"notes,omitempty"` - Tags []string `bson:"tags,omitempty"` - Status string `bson:"status"` - OutreachCount int `bson:"outreach_count,omitempty"` - LastContactedAt int64 `bson:"last_contacted_at,omitempty"` - CreateAt int64 `bson:"create_at"` - UpdateAt int64 `bson:"update_at"` -} diff --git a/backend/internal/model/crm_contact/domain/repository/repository.go b/backend/internal/model/crm_contact/domain/repository/repository.go deleted file mode 100644 index b9f0797..0000000 --- a/backend/internal/model/crm_contact/domain/repository/repository.go +++ /dev/null @@ -1,25 +0,0 @@ -package repository - -import ( - "context" - "haixun-backend/internal/model/crm_contact/domain/entity" -) - -type ListFilter struct { - BrandID string - Status string - Tag string - Limit int - Offset int -} - -type Repository interface { - EnsureIndexes(ctx context.Context) error - Create(ctx context.Context, contact *entity.CRMContact) error - Get(ctx context.Context, tenantID, ownerUID, contactID string) (*entity.CRMContact, error) - GetByAuthor(ctx context.Context, tenantID, ownerUID, authorID string) (*entity.CRMContact, error) - Update(ctx context.Context, tenantID, ownerUID, contactID string, patch map[string]interface{}) (*entity.CRMContact, error) - Delete(ctx context.Context, tenantID, ownerUID, contactID string) error - List(ctx context.Context, tenantID, ownerUID string, filter ListFilter) ([]entity.CRMContact, int, error) - UpsertByAuthor(ctx context.Context, contact *entity.CRMContact) (*entity.CRMContact, error) -} diff --git a/backend/internal/model/crm_contact/domain/usecase/usecase.go b/backend/internal/model/crm_contact/domain/usecase/usecase.go deleted file mode 100644 index 4cd8c96..0000000 --- a/backend/internal/model/crm_contact/domain/usecase/usecase.go +++ /dev/null @@ -1,47 +0,0 @@ -package usecase - -import "context" - -type CRMContactSummary struct { - ID string - BrandID string - AuthorID string - AuthorName string - AuthorAvatar string - AuthorFollowers int - ScanPostID string - Notes string - Tags []string - Status string - OutreachCount int - LastContactedAt int64 - CreateAt int64 - UpdateAt int64 -} - -type ListRequest struct { - TenantID string - OwnerUID string - BrandID string - Status string - Tag string - Page int - PageSize int -} - -type UpdateRequest struct { - TenantID string - OwnerUID string - ContactID string - Notes *string - Tags *[]string - Status *string -} - -type UseCase interface { - Create(ctx context.Context, tenantID, ownerUID, brandID, authorID, authorName string) (*CRMContactSummary, error) - Get(ctx context.Context, tenantID, ownerUID, contactID string) (*CRMContactSummary, error) - Update(ctx context.Context, req UpdateRequest) (*CRMContactSummary, error) - Delete(ctx context.Context, tenantID, ownerUID, contactID string) error - List(ctx context.Context, req ListRequest) ([]CRMContactSummary, int, error) -} diff --git a/backend/internal/model/crm_contact/repository/mongo.go b/backend/internal/model/crm_contact/repository/mongo.go deleted file mode 100644 index 101400e..0000000 --- a/backend/internal/model/crm_contact/repository/mongo.go +++ /dev/null @@ -1,217 +0,0 @@ -package repository - -import ( - "context" - "strings" - - app "haixun-backend/internal/library/errors" - "haixun-backend/internal/library/errors/code" - "haixun-backend/internal/model/crm_contact/domain/entity" - domrepo "haixun-backend/internal/model/crm_contact/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 baseFilter(tenantID, ownerUID string) bson.M { - return bson.M{ - "tenant_id": strings.TrimSpace(tenantID), - "owner_uid": strings.TrimSpace(ownerUID), - } -} - -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: "brand_id", Value: 1}, - {Key: "create_at", Value: -1}, - }, - }, - { - Keys: bson.D{ - {Key: "tenant_id", Value: 1}, - {Key: "owner_uid", Value: 1}, - {Key: "author_id", Value: 1}, - }, - Options: options.Index().SetUnique(true), - }, - }) - return err -} - -func (r *mongoRepository) Create(ctx context.Context, contact *entity.CRMContact) error { - if r.collection == nil { - return app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - if contact == nil { - return app.For(code.Persona).InputMissingRequired("contact is required") - } - _, err := r.collection.InsertOne(ctx, contact) - return err -} - -func (r *mongoRepository) Get(ctx context.Context, tenantID, ownerUID, contactID string) (*entity.CRMContact, error) { - if r.collection == nil { - return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - contactID = strings.TrimSpace(contactID) - if contactID == "" { - return nil, app.For(code.Persona).InputMissingRequired("contact_id is required") - } - filter := baseFilter(tenantID, ownerUID) - filter["_id"] = contactID - var out entity.CRMContact - err := r.collection.FindOne(ctx, filter).Decode(&out) - if err != nil { - if err == mongo.ErrNoDocuments { - return nil, app.For(code.Persona).ResNotFound("crm contact not found") - } - return nil, err - } - return &out, nil -} - -func (r *mongoRepository) GetByAuthor(ctx context.Context, tenantID, ownerUID, authorID string) (*entity.CRMContact, error) { - if r.collection == nil { - return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - authorID = strings.TrimSpace(authorID) - if authorID == "" { - return nil, app.For(code.Persona).InputMissingRequired("author_id is required") - } - filter := baseFilter(tenantID, ownerUID) - filter["author_id"] = authorID - var out entity.CRMContact - err := r.collection.FindOne(ctx, filter).Decode(&out) - if err != nil { - if err == mongo.ErrNoDocuments { - return nil, app.For(code.Persona).ResNotFound("crm contact not found") - } - return nil, err - } - return &out, nil -} - -func (r *mongoRepository) Update(ctx context.Context, tenantID, ownerUID, contactID string, patch map[string]interface{}) (*entity.CRMContact, 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") - } - contactID = strings.TrimSpace(contactID) - filter := baseFilter(tenantID, ownerUID) - filter["_id"] = contactID - opts := options.FindOneAndUpdate().SetReturnDocument(options.After) - var out entity.CRMContact - 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("crm contact not found") - } - return nil, err - } - return &out, nil -} - -func (r *mongoRepository) Delete(ctx context.Context, tenantID, ownerUID, contactID string) error { - if r.collection == nil { - return app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - contactID = strings.TrimSpace(contactID) - if contactID == "" { - return app.For(code.Persona).InputMissingRequired("contact_id is required") - } - filter := baseFilter(tenantID, ownerUID) - filter["_id"] = contactID - _, err := r.collection.DeleteOne(ctx, filter) - return err -} - -func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID string, filter domrepo.ListFilter) ([]entity.CRMContact, int, error) { - if r.collection == nil { - return nil, 0, app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - query := baseFilter(tenantID, ownerUID) - if filter.BrandID != "" { - query["brand_id"] = filter.BrandID - } - if filter.Status != "" { - query["status"] = filter.Status - } - if filter.Tag != "" { - query["tags"] = filter.Tag - } - - limit := filter.Limit - if limit <= 0 { - limit = 20 - } - if limit > 100 { - limit = 100 - } - skip := filter.Offset - if skip < 0 { - skip = 0 - } - - total, err := r.collection.CountDocuments(ctx, query) - if err != nil { - return nil, 0, err - } - - opts := options.Find(). - SetSort(bson.D{{Key: "create_at", Value: -1}}). - SetSkip(int64(skip)). - SetLimit(int64(limit)) - cur, err := r.collection.Find(ctx, query, opts) - if err != nil { - return nil, 0, err - } - defer cur.Close(ctx) - - var out []entity.CRMContact - if err := cur.All(ctx, &out); err != nil { - return nil, 0, err - } - return out, int(total), nil -} - -func (r *mongoRepository) UpsertByAuthor(ctx context.Context, contact *entity.CRMContact) (*entity.CRMContact, error) { - if r.collection == nil { - return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") - } - if contact == nil { - return nil, app.For(code.Persona).InputMissingRequired("contact is required") - } - filter := baseFilter(contact.TenantID, contact.OwnerUID) - filter["author_id"] = contact.AuthorID - update := bson.M{"$set": contact} - opts := options.FindOneAndUpdate(). - SetUpsert(true). - SetReturnDocument(options.After) - var out entity.CRMContact - err := r.collection.FindOneAndUpdate(ctx, filter, update, opts).Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} diff --git a/backend/internal/model/crm_contact/usecase/usecase.go b/backend/internal/model/crm_contact/usecase/usecase.go deleted file mode 100644 index e0f6ed0..0000000 --- a/backend/internal/model/crm_contact/usecase/usecase.go +++ /dev/null @@ -1,176 +0,0 @@ -package usecase - -import ( - "context" - "strings" - - "haixun-backend/internal/library/clock" - app "haixun-backend/internal/library/errors" - "haixun-backend/internal/library/errors/code" - "haixun-backend/internal/model/crm_contact/domain/entity" - domrepo "haixun-backend/internal/model/crm_contact/domain/repository" - domusecase "haixun-backend/internal/model/crm_contact/domain/usecase" - - "github.com/google/uuid" -) - -type crmContactUseCase struct { - repo domrepo.Repository -} - -func NewUseCase(repo domrepo.Repository) domusecase.UseCase { - return &crmContactUseCase{repo: repo} -} - -func (u *crmContactUseCase) Create(ctx context.Context, tenantID, ownerUID, brandID, authorID, authorName string) (*domusecase.CRMContactSummary, error) { - if err := requireActor(tenantID, ownerUID); err != nil { - return nil, err - } - authorID = strings.TrimSpace(authorID) - if authorID == "" { - return nil, app.For(code.Persona).InputMissingRequired("author_id is required") - } - authorName = strings.TrimSpace(authorName) - if authorName == "" { - return nil, app.For(code.Persona).InputMissingRequired("author_name is required") - } - now := clock.NowUnixNano() - contact := &entity.CRMContact{ - ID: uuid.NewString(), - TenantID: tenantID, - OwnerUID: ownerUID, - BrandID: strings.TrimSpace(brandID), - AuthorID: authorID, - AuthorName: authorName, - Status: entity.ContactStatusLead, - CreateAt: now, - UpdateAt: now, - } - if err := u.repo.Create(ctx, contact); err != nil { - return nil, err - } - summary := toSummary(*contact) - return &summary, nil -} - -func (u *crmContactUseCase) Get(ctx context.Context, tenantID, ownerUID, contactID string) (*domusecase.CRMContactSummary, error) { - if err := requireActor(tenantID, ownerUID); err != nil { - return nil, err - } - contactID = strings.TrimSpace(contactID) - if contactID == "" { - return nil, app.For(code.Persona).InputMissingRequired("contact_id is required") - } - item, err := u.repo.Get(ctx, tenantID, ownerUID, contactID) - if err != nil { - return nil, err - } - summary := toSummary(*item) - return &summary, nil -} - -func (u *crmContactUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.CRMContactSummary, error) { - if err := requireActor(req.TenantID, req.OwnerUID); err != nil { - return nil, err - } - contactID := strings.TrimSpace(req.ContactID) - if contactID == "" { - return nil, app.For(code.Persona).InputMissingRequired("contact_id is required") - } - patch := map[string]interface{}{} - if req.Notes != nil { - patch["notes"] = strings.TrimSpace(*req.Notes) - } - if req.Tags != nil { - patch["tags"] = *req.Tags - } - if req.Status != nil { - status := strings.TrimSpace(*req.Status) - if status != "" && status != entity.ContactStatusLead && status != entity.ContactStatusContacted && status != entity.ContactStatusResponded && status != entity.ContactStatusConverted && status != entity.ContactStatusArchived { - return nil, app.For(code.Persona).InputInvalidFormat("invalid contact status") - } - if status != "" { - patch["status"] = status - } - } - if len(patch) == 0 { - return nil, app.For(code.Persona).InputMissingRequired("no fields to update") - } - patch["update_at"] = clock.NowUnixNano() - item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, contactID, patch) - if err != nil { - return nil, err - } - summary := toSummary(*item) - return &summary, nil -} - -func (u *crmContactUseCase) Delete(ctx context.Context, tenantID, ownerUID, contactID string) error { - if err := requireActor(tenantID, ownerUID); err != nil { - return err - } - contactID = strings.TrimSpace(contactID) - if contactID == "" { - return app.For(code.Persona).InputMissingRequired("contact_id is required") - } - return u.repo.Delete(ctx, tenantID, ownerUID, contactID) -} - -func (u *crmContactUseCase) List(ctx context.Context, req domusecase.ListRequest) ([]domusecase.CRMContactSummary, int, error) { - if err := requireActor(req.TenantID, req.OwnerUID); err != nil { - return nil, 0, err - } - page := req.Page - pageSize := req.PageSize - if page < 1 { - page = 1 - } - if pageSize < 1 { - pageSize = 20 - } - if pageSize > 100 { - pageSize = 100 - } - filter := domrepo.ListFilter{ - BrandID: strings.TrimSpace(req.BrandID), - Status: strings.TrimSpace(req.Status), - Tag: strings.TrimSpace(req.Tag), - Limit: pageSize, - Offset: (page - 1) * pageSize, - } - items, total, err := u.repo.List(ctx, req.TenantID, req.OwnerUID, filter) - if err != nil { - return nil, 0, err - } - out := make([]domusecase.CRMContactSummary, 0, len(items)) - for _, item := range items { - out = append(out, toSummary(item)) - } - return out, total, nil -} - -func requireActor(tenantID, ownerUID string) error { - if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { - return app.For(code.Auth).AuthUnauthorized("missing actor") - } - return nil -} - -func toSummary(item entity.CRMContact) domusecase.CRMContactSummary { - return domusecase.CRMContactSummary{ - ID: item.ID, - BrandID: item.BrandID, - AuthorID: item.AuthorID, - AuthorName: item.AuthorName, - AuthorAvatar: item.AuthorAvatar, - AuthorFollowers: item.AuthorFollowers, - ScanPostID: item.ScanPostID, - Notes: item.Notes, - Tags: item.Tags, - Status: item.Status, - OutreachCount: item.OutreachCount, - LastContactedAt: item.LastContactedAt, - CreateAt: item.CreateAt, - UpdateAt: item.UpdateAt, - } -} diff --git a/backend/internal/model/publish_draft/domain/entity/draft.go b/backend/internal/model/publish_draft/domain/entity/draft.go deleted file mode 100644 index c3071f8..0000000 --- a/backend/internal/model/publish_draft/domain/entity/draft.go +++ /dev/null @@ -1,15 +0,0 @@ -package entity - -const CollectionName = "publish_text_drafts" - -type Draft struct { - ID string `bson:"_id"` - TenantID string `bson:"tenant_id"` - OwnerUID string `bson:"owner_uid"` - AccountID string `bson:"account_id"` - Title string `bson:"title,omitempty"` - Text string `bson:"text"` - Tags []string `bson:"tags,omitempty"` - CreateAt int64 `bson:"create_at"` - UpdateAt int64 `bson:"update_at"` -} diff --git a/backend/internal/model/publish_draft/domain/repository/repository.go b/backend/internal/model/publish_draft/domain/repository/repository.go deleted file mode 100644 index 0011df8..0000000 --- a/backend/internal/model/publish_draft/domain/repository/repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - "context" - - "haixun-backend/internal/model/publish_draft/domain/entity" -) - -type ListResult struct { - Items []entity.Draft - Total int64 -} - -type Repository interface { - EnsureIndexes(ctx context.Context) error - Create(ctx context.Context, draft *entity.Draft) error - Get(ctx context.Context, tenantID, ownerUID, accountID, draftID string) (*entity.Draft, error) - List(ctx context.Context, tenantID, ownerUID, accountID string, page, pageSize int) (*ListResult, error) - Update(ctx context.Context, draft *entity.Draft) (*entity.Draft, error) - Delete(ctx context.Context, tenantID, ownerUID, accountID, draftID string) error -} diff --git a/backend/internal/model/publish_draft/domain/usecase/usecase.go b/backend/internal/model/publish_draft/domain/usecase/usecase.go deleted file mode 100644 index e62e87d..0000000 --- a/backend/internal/model/publish_draft/domain/usecase/usecase.go +++ /dev/null @@ -1,48 +0,0 @@ -package usecase - -import "context" - -type DraftSummary struct { - ID string - AccountID string - Title string - Text string - Tags []string - CreateAt int64 - UpdateAt int64 -} - -type CreateRequest struct { - TenantID string - OwnerUID string - AccountID string - Title string - Text string - Tags []string -} - -type UpdateRequest struct { - TenantID string - OwnerUID string - AccountID string - DraftID string - Title *string - Text *string - Tags *[]string -} - -type ListResult struct { - Items []DraftSummary - Total int64 - Page int - PageSize int - TotalPages int -} - -type UseCase interface { - Create(ctx context.Context, req CreateRequest) (*DraftSummary, error) - Get(ctx context.Context, tenantID, ownerUID, accountID, draftID string) (*DraftSummary, error) - List(ctx context.Context, tenantID, ownerUID, accountID string, page, pageSize int) (*ListResult, error) - Update(ctx context.Context, req UpdateRequest) (*DraftSummary, error) - Delete(ctx context.Context, tenantID, ownerUID, accountID, draftID string) error -} diff --git a/docs/product/README.md b/docs/product/README.md new file mode 100644 index 0000000..165e406 --- /dev/null +++ b/docs/product/README.md @@ -0,0 +1,44 @@ +# 產品文件(Spec-Driven) + +本目錄是 **需求 → 規格 → 計劃 → 小任務** 的產物根,由專案 skill `/spec-driven` 驅動。 + +```text +docs/product/ + README.md # 本說明 + / + README.md # 目前 phase 與狀態 + requirements.md + spec.md # 需求批准後 + plan.md # spec 批准後 + tasks/ + INDEX.md + T001-....md +``` + +## 怎麼用 + +```text +/spec-driven # 開新的或繼續未完成 +/spec-driven haixun-console # 指定產品 run +/spec-driven haixun-console --status # 只看進度 +``` + +**規則:** 每階段寫完要等人批准,才能進下一階段;未批准 plan / tasks 前不寫業務程式。 + +模板與流程細節:`.grok/skills/spec-driven/`。 + +## 現有 run + +| Slug | 說明 | +|------|------| +| [haixun-console](./haixun-console/) | 巡樓 Console **從零重寫**(`old/` 封存;地基 → FE mock → 後端) | + +## 與其他 docs 的關係 + +| 路徑 | 角色 | +|------|------| +| `docs/architecture.md` | 程式怎麼長(現況) | +| `docs/improvement-notes.md` | 技術改進建議(非產品需求) | +| `docs/post-project-plan.md` | 文案流重做構想(輸入,須併入 requirements 才算正式) | +| `AGENTS.md` | 實作約束 | +| `docs/product/*` | **產品決策真相來源**(本流水線) | diff --git a/docs/product/haixun-console/README.md b/docs/product/haixun-console/README.md new file mode 100644 index 0000000..573f8b0 --- /dev/null +++ b/docs/product/haixun-console/README.md @@ -0,0 +1,42 @@ +# Product run: haixun-console + +| Field | Value | +|-------|--------| +| Goal | 從零重寫:地基 → 前端 mock → 接真;多帳互回(reply 含 lead) | +| Phase | **tasks / ready-to-implement** | +| Requirements | **approved** | +| Spec | **approved** | +| Plan | **approved** | +| Legacy | `old/` 封存 | +| Updated | 2026-07-09 | + +## Delivery order + +```text +M0–M9 (T001–T022) → Phase B mock UI +M10 → 人審 B OK +M20+ (T100+) → Phase C live API(尚未拆細) +``` + +## Artifacts + +| File | Status | +|------|--------| +| [requirements.md](./requirements.md) | approved | +| [spec.md](./spec.md) | approved | +| [plan.md](./plan.md) | approved | +| [tasks/INDEX.md](./tasks/INDEX.md) | **22 tasks ready** | + +## Start implementing + +```text +從 T001 開始 +docs/product/haixun-console/tasks/T001-scaffold-apps-web.md +``` + +完成一題就把 INDEX 狀態改成 `done`,再做下一題。 + +## Defaults + +- `apps/web` greenfield · Harbor Desk · Speakers = lead ∪ cast(reply 可選 lead) +- B 可純前端 · mock localStorage · Scout 占位 diff --git a/docs/product/haixun-console/plan.md b/docs/product/haixun-console/plan.md new file mode 100644 index 0000000..394a762 --- /dev/null +++ b/docs/product/haixun-console/plan.md @@ -0,0 +1,138 @@ +# Plan: 巡樓 Console 從零重寫 + +> Status: **`approved`**(與 task 拆分一併啟動) +> Source: `docs/product/haixun-console/spec.md` +> Last updated: `2026-07-09` + +## 1. 交付策略 + +舊碼已在 `old/` 封存。主線 **greenfield**: + +1. **先 B(前端 mock)** 讓 Harbor Desk 流程(含 lead 可回覆)你摸到滿意。 +2. **A 的 live 地基**不阻塞 B;可在 B 過關後或平行用 `services/api` 補。 +3. **C** 同一 UI 接真;**D** 海巡等。 + +本 plan 的 **可立即開工區間是 M0–M2(tasks T001–T022)**,對應 Phase B 完整 mock。 +Phase C/D 只定里程碑與 task 編號預留,細 task 待 B 過關再拆。 + +## 2. 里程碑 + +### M0 — 倉庫骨架 + +- **目標:** 可 `npm run dev` 的空應用 +- **完成定義:** `apps/web` Vite+React+TS 起來;根 README 有啟動指令 +- **Tasks:** T001 +- **驗證:** `cd apps/web && npm install && npm run build` + +### M1 — 設計系統 + App Shell + +- **目標:** Harbor Desk 外殼可導覽 +- **完成定義:** token、基礎元件、桌面側欄+手機底欄、5 入口路由占位 +- **Tasks:** T002–T005 +- **驗證:** 手動點 5 入口;build 通過 + +### M2 — Auth mock + 路由守衛 + +- **目標:** 未登入進不了 `/app/**` +- **完成定義:** mock 登入/登出/重整保 session +- **Tasks:** T006 +- **驗證:** 登入後進 Today;logout 回 login + +### M3 — 資料層 mock + +- **目標:** repository 介面 + localStorage + seed +- **完成定義:** accounts/plays/outbox/jobs/notifications 可讀寫;`mock|live` 切換點存在(live 可 stub 丟錯) +- **Tasks:** T007–T008 +- **驗證:** 重整後 seed/草稿仍在 + +### M4 — Crew(帳號) + +- **目標:** 看帳號列表、mock 新增、不可用狀態 +- **Tasks:** T009 +- **驗證:** 列表 ≥2 假帳;連線 error 不可當 lead/cast + +### M5 — Studio 列表 + Wizard 船隊/劇本 + +- **目標:** 建立串場;lead+cast;步驟含 lead 發言 +- **Tasks:** T010–T013 +- **驗證:** root(A)→reply(B)→reply(A) 可編輯保存 + +### M6 — 預覽 / 船期 / 提交 + +- **目標:** 預覽泡泡、排程、進 Outbox +- **Tasks:** T014 +- **驗證:** submit 後 Outbox 有對應 bundle + +### M7 — Outbox 發送模擬 + +- **目標:** 逐步狀態、主失敗不發後續、重試 +- **Tasks:** T015–T016 +- **驗證:** 模擬成功全 published;root fail 時 reply 不 published + +### M8 — Job + 通知 + Today + +- **目標:** 任務進度、鈴鐺、今日總覽 +- **Tasks:** T017–T019 +- **驗證:** 模擬 job 完成出通知;Today 有捷徑 + +### M9 — Scout / Settings / 收尾 + +- **目標:** 占位頁、datasource 切換、RWD 基線、範例劇本 +- **Tasks:** T020–T022 +- **驗證:** Scout 無假成功;手機底欄可用;build 通過 + +### M10 — Phase B 過關(人審) + +- **目標:** 你主觀認可流程與風格 +- **Tasks:** 無新碼;走查 checklist(requirements Phase B) +- **驗證:** 文件勾選 + 你回「B OK」 + +### M20 — Phase C 接真(預留,B 後再拆細 task) + +- `services/api` 最小 auth/me/permissions +- Threads OAuth + accounts +- Jobs + notifications live +- ThreadPlay/Outbox live 垂直切片 + +預留 task 號:**T100+**(本輪不寫檔) + +### M30 — Phase D(預留) + +- Scout 海巡滿血等 → **T200+** + +## 3. 依賴 + +```text +M0 → M1 → M2 → M3 → M4 + ↘ M5 → M6 → M7 + ↘ M8 + ↘ M9 → M10 (人審) + ↘ M20 → M30 +``` + +## 4. 風險與緩解 + +| 風險 | 緩解 | +|------|------| +| Task 膨脹超 200 行 | 實作時再拆子 task,更新 INDEX | +| Mock 契約與 C 不一致 | T007 介面以 spec §5 為準,C 只換 adapter | +| 過早寫 Go | M10 前禁止大範圍 `services/api` 業務 | + +## 5. 刻意不做(本 plan 週期內 = 到 M10) + +- 海巡真功能、舊碼搬回、重型 UI 庫 +- live Threads 發文(屬 M20) +- email 驗證閘 + +## 6. 全域驗證指令 + +```bash +cd apps/web && npm install && npm run build +# 之後若有測試: +# npm test +``` + +## 7. 批准 + +- [x] Plan 已批准:2026-07-09(使用者:好請幫我開始拆) +- 實作順序:從 **T001** 起,完成一題勾 INDEX 再下一題 diff --git a/docs/product/haixun-console/requirements.md b/docs/product/haixun-console/requirements.md new file mode 100644 index 0000000..9fd8ffa --- /dev/null +++ b/docs/product/haixun-console/requirements.md @@ -0,0 +1,331 @@ +# Requirements: 巡樓 Console(整包重構) + +> Status: **`approved`** +> Slug: `haixun-console` +> Last updated: `2026-07-09` +> 批准:使用者確認 OK(2026-07-09) +> 策略:舊碼整包封存 `old/`;**主線從零重寫**;先地基 → 前端 mock → 再接後端;文案含多帳號互回 +> 變更:reply 步驟可選 **lead**(主帳可接續回覆) + +--- + +## 1. 一句話 + +用 Grok 4.5 **重構**巡樓工作台:先把登入、Threads 串接、Job、通知、權限等地基做對;**前端用全新流程與視覺 + 假資料**把操作順到你滿意,再補真實後端與業務,避免在舊混亂上疊功能。 + +--- + +## 2. 誰用、在什麼情境 + +| 角色 | 目標 | +|------|------| +| 經營者 | 綁多個 Threads、產文案(主帳+配角互回)、海巡外展、排程發文;看任務與通知 | +| 管理員 | 建會員、管權限(封閉 beta) | +| (系統)Worker | 長任務執行並回報進度 | + +--- + +## 3. 背景與動機 + +### 為什麼要重構(不是小修) + +- 現況前後端半成品交織:假成功 API、幽靈 worker、巨型頁、文件漂移 → **在舊樹上修會繼續亂**。 +- 你希望流程與風格都重想;業務好不好用要先**用手走完**,不是先堆後端。 +- 交付方式:**舊 monorepo 已封存至 `old/`**,根目錄從零建立 `apps/web`、`services/*` 等;`old/` 只讀參考。 + +### 參考(非最終規格) + +- 封存:`old/backend/`、`old/docs/`、`old/AGENTS.md` +- 產品:`docs/product/haixun-console/` + +--- + +## 4. 交付策略(怎麼重構)— 硬性順序 + +```text +Phase A 地基(最小可用平台) + → Phase B 新前端 + Mock(流程與風格定稿) + → Phase C 後端接真(替換 mock,同一套 UI) + → Phase D 業務加深(海巡、進階互回、成效…) +``` + +| Phase | 目標 | 完成定義(產品語言) | +|-------|------|----------------------| +| **A 地基** | 平台能站起來 | 登入會話可用;權限可擋路由;Threads 帳號能綁/能呼叫 API(或明確 mock 開關);Job 能建立/查狀態/通知;**無假成功** | +| **B 前端 Mock** | 操作順、好看 | 全主要流程用假資料可點完;你認可 IA + 視覺;API 層可切 `mock | live` | +| **C 接後端** | 假資料變真 | 同一 UI,逐頁/逐域替換 live;契約與 mock 對齊 | +| **D 業務加深** | 護城河 | 海巡閉環、進階互回/AI 分角、護欄與成效等 | + +**原則:** + +1. **B 未通過前,不開大範圍業務後端**(避免又寫一堆用不到的 API)。 +2. A 只做「所有業務都會用到的管子」,不做完整海巡/完整矩陣。 +3. 新碼只在根目錄新結構;**不在 `old/` 上繼續開發**。 +4. 舊 monorepo **已封存**於 `old/`(2026-07-09)。 + +--- + +## 5. 目標分級 + +### 5.1 P0-A — 地基(平台) + +1. **登入與會話** + email/password 登入、logout、refresh;未登入不可進主殼。 + (封閉註冊:admin 建號或 seed;公開註冊非 P0。) +2. **權限** + 至少:已登入會員 vs admin;前端路由與關鍵操作可依角色顯示/阻擋;後端同語意。 +3. **Threads API 串接完成(基礎)** + - 綁定帳號(OAuth 或文件化的連線方式) + - 列表已綁帳號、連線健康/錯誤可讀 + - 具備「用指定帳號打 Threads API」的統一出口(發文/回覆可先最小 smoke,完整業務在 C/D) +4. **Job 系統(最小)** + 建立任務、查列表/詳情、狀態(queued/running/succeeded/failed/cancelled)、進度摘要。 +5. **Notification(任務通知)** + 任務完成/失敗時,Console 內可感知(鈴鐺列表或 toast + 可點進 job);不要求 email/push 推播為 P0。 +6. **契約底線** + JSON envelope、錯誤可讀、時間 UTC nano(後端)、**禁止 stub 回成功**。 + +### 5.2 P0-B — 新前端 + Mock(你要先滿意的部分) + +1. **全新資訊架構與視覺**(見 §8),不沿用「掌機側欄 + 巨型單頁」當預設。 +2. **Mock 驅動的完整主流程**(假資料也要真實感): + - 登入殼 → 總覽(今日待辦) + - 帳號(多 Threads) + - **發文計畫 / 串場**(主題 → 主帳+cast → 草稿串 → 預覽 → 排程預覽) + - 出站佇列(Outbox)狀態 + - 任務與通知中心 + - 設定(AI key 可先 UI only) +3. **多帳號互回(流程先做對)** + - 主帳號 **lead** 1 個:負責樓主貼(root) + - **cast** 多選:本串可出場的其他帳號 + - **發言池 = lead ∪ cast**:每一則「回覆步驟」都可指定池內**任一帳號**,**包含 lead** + (例:lead 發主貼 → cast 帳回 → **lead 再回那則** → 另一 cast 再接) + - 單線接龍步驟可編輯、可預覽「誰說什麼」 + - Mock 發布過程:逐步狀態動畫/列表即可 +4. **雙模式資料層** + `dataSource: mock | api`;B 預設 mock;切 api 時契約不變。 +5. **RWD**:手機可走完核心流(不必完美,但不可壞掉)。 + +### 5.3 P0-C — 接後端(在 B 過關後) + +1. Auth / me / permissions 接真。 +2. Threads 帳號與 OAuth 接真。 +3. Job + 站內通知接真。 +4. 發文計畫 / 互回草稿 / 佇列逐步接真(可按垂直切片:先單帳發文,再互回)。 + +### 5.4 P1 — 業務加深 + +1. 海巡:品牌 → 主題 → 掃描 → 外展草稿。 +2. 互回進階:AI 分角色產整串、間隔/護欄 per-account、失敗單步重試完善。 +3. 提及收件匣、成效儀表。 +4. Job 可靠度硬化(dedupe/heartbeat 等)。 +5. Email 驗證閘道(若仍需要)。 + +### 5.5 P2 + +公開註冊、Islander agent、extension 安全同步、大型多租戶計費等。 + +--- + +## 6. 範圍 + +### In scope + +- **從零重寫**(舊碼在 `old/` 封存,主線新樹)。 +- 地基:Auth、Permission、Threads 基礎串接、Job、站內 Notification。 +- 全新前端體驗 + mock 業務流(含多帳互回)。 +- 之後同一套 UI 接後端。 +- 產品文件:`docs/product/haixun-console/` 為決策真相。 + +### 多帳號互回 — 行為邊界 + +| 項目 | P0 要 | P0 不要 | +|------|-------|---------| +| lead | 必選 1 個;**root 固定 lead**;**也可在 reply 再出場** | 主帳只能發主貼、不能回後續 | +| cast | 多選其他帳;與 lead 組成**發言池** | 跨會員借號 | +| reply 發言者 | **lead 或任一 cast**(主帳可回別人的回覆) | 僅 cast 能回 | +| 串結構 | 單線接龍:每步接上一步 | 樹狀多分支(以後再說) | + +### Out of scope(本重構週期明確不做) + +- 在 `old/` 或舊 UI 上繼續疊功能當最終產品。 +- 把 `old/` 整包「搬回」當新主線而不重寫。 +- 復原舊 copy_mission 五段 worker 管線。 +- 自動 refill 庫存 bot、公開自助註冊、MFA、Zitadel。 +- 重型 UI 框架(MUI/Ant/Chakra)。 +- 任天堂 / Nook 元素、emoji 當主圖示。 +- 把海巡與互回做成同一套語意。 +- 未綁定帳號、跨會員借號。 +- B 階段沒過就大寫海巡/矩陣後端。 + +--- + +## 7. 使用者故事 + +### 地基 + +| ID | 作為… | 我想要… | 以便… | 驗收 | +|----|--------|---------|--------|------| +| US-A1 | 經營者 | 登入並維持會話 | 進工作台 | 登入進主殼;logout 後不可進 | +| US-A2 | 經營者 | 依權限看到該看的選單 | 不誤操作 | 無 permission 時入口隱藏或 403 | +| US-A3 | 經營者 | 綁定並看到 Threads 帳號狀態 | 後續發文有對象 | 列表可見;錯誤可讀 | +| US-A4 | 經營者 | 看背景任務狀態與完成通知 | 知道跑完沒 | job 列表;完成/失敗有通知可點 | + +### 前端 Mock / 文案流 + +| ID | 作為… | 我想要… | 以便… | 驗收 | +|----|--------|---------|--------|------| +| US-B1 | 經營者 | 在新 UI 上走完「今日 → 開一個發文計畫」 | 覺得流程順 | mock 下無卡死、步驟清楚 | +| US-B2 | 經營者 | 指定主帳 + 選其他帳互回,且 lead 也可再回後續留言 | 多號串場、主帳能接話 | 每句顯示帳號;reply 步驟可選 lead 或 cast;預覽順序正確 | +| US-B3 | 經營者 | 在 Outbox 看到這串的排程狀態 | 掌握發送 | mock 狀態流轉合理 | +| US-B4 | 經營者 | 從通知跳到對應任務或計畫 | 減少找頁 | 一鍵到達 | + +### 接真後(C) + +| ID | 作為… | 我想要… | 以便… | 驗收 | +|----|--------|---------|--------|------| +| US-C1 | 經營者 | 同一套畫面改打真 API 仍可用 | 上線 | mock/api 契約一致;關鍵流 live 通過 | + +--- + +## 8. 新前端:流程與風格方向(提案,待你選/改) + +> 現況問題(簡述):導覽像「很多 App 格」但主線不清;單頁過重;視覺偏玩具掌機,專業經營感不足;海巡/文案/發布心智模型混。 + +### 8.1 產品隱喻(建議) + +**「泊岸工作台 Harbor Desk」** — 不是掌上遊戲機,而是**靠港調度台**: + +- 今天要出港的內容(發文/互回) +- 船隊帳號(多 Threads) +- 遠航探查(海巡,P1) +- 船期與回報(Outbox + Jobs + 通知) + +語氣:年輕、清楚、短句;可留「島」「巡」詞,但 UI **不要**雲朵滿版搶戲。 + +### 8.2 資訊架構(建議導覽) + +最多 **5 個主入口**(桌面側欄 + 手機底欄): + +| 入口 | 做什麼 | +|------|--------| +| **今日 Today** | 待發、失敗需處理、進行中 job、快捷「開新串」 | +| **帳號 Crew** | 綁定/健康/選預設主帳 | +| **創作 Studio** | 發文計畫 wizard + 草稿庫(多帳互回在這裡) | +| **探查 Scout** | 海巡(P1 再滿血;B 可放空狀態說明) | +| **發送 Outbox** | 佇列、歷史、護欄狀態 | + +**全域:** 頂欄右側 = 通知鈴 + 主題 + 使用者選單(設定/登出)。 +**不做:** 十幾個 App tile、Jobs/Gaps 當一級又難找又佔位(Jobs 併入通知與 Today)。 + +### 8.3 創作主流程(Studio wizard,B 階段就要順) + +```text +1. 主題 一句話 + 可選「幫我想」 +2. 船隊 選 lead(主帳)+ cast(其他可出場帳,可多選) +3. 劇本 單線步驟:第 0 步 root=lead;之後每步 = **發言池內帳號(含 lead)** + 文案 +4. 預覽 像 Threads 對話泡泡;可見 lead 穿插回覆 +5. 船期 主貼時間 + 回覆間隔 +6. 提交 進 Outbox(mock 狀態:排程中 → 發送中 → 完成/失敗) +``` + +任一兩可回上一步;每步自動存 draft(mock localStorage 即可)。 + +### 8.4 視覺方向(建議,可整包替換舊 hx-scene) + +| 面向 | 建議 | +|------|------| +| 調性 | **安靜專業 + 一點溫度**:偏「設計工具 / 編輯台」,不是遊戲 HUD | +| 色彩 | 深 slate 字 + 暖白紙面 + **單一 brand**(建議霧青/海色 `#2A9D8F` 系);深淺主題都走 token | +| 字體 | 維持 **Inter + Taipei Sans TC**(閱讀與中文標題夠用) | +| 版面 | 左窄導覽(icon+字)+ 右內容;**不要**固定超寬掌機外框 | +| 卡片 | 1px 細邊、中等圓角(12–16px)、輕陰影;少裝飾 blob | +| 動效 | 僅頁面切換與列表 150–200ms;不搶操作 | +| 圖示 | 原創線條 SVG 一套即可;禁止 emoji 當主 icon | +| 密度 | 表單與表格可讀優先;中文 line-height ≥ 1.6 | + +**明確告別:** 舊「奶油票券 + 滿天雲 + PATROL PAD 掌機」作為預設殼(若你仍想保留可作「懷舊主題」P2,不當預設)。 + +### 8.5 Mock 資料原則 + +- 預置 2–3 個假 Threads 帳號(不同顯示名/頭像色)。 +- 預置 1 條進行中 job、1 條失敗通知。 +- 預置 1 份範例互回劇本:root(lead) → reply(cast) → reply(lead)(示範主帳接話)。 +- 所有列表可空狀態插畫/短文案,不空白死頁。 + +--- + +## 9. 成功標準 + +### Phase A + +- [ ] US-A1~A4 在「真後端或契約完整 mock server」下可演示。 +- [ ] 無對外 stub 成功 API。 + +### Phase B(**你主觀過關為準**) + +- [ ] 你親自走完 Studio 互回 wizard + Outbox + 通知,認為**流程順、風格可接受**。 +- [ ] 能做出 root(lead) → reply(cast) → reply(lead) 並預覽/進 Outbox。 +- [ ] 主 IA ≤ 5 個一級入口;無巨型單頁不可維護感(單檔建議有分頁/步驟元件)。 +- [ ] `mock | api` 可切換架構已就位(api 可暫時 501,但不能亂成功)。 + +### Phase C + +- [ ] 登入、帳號、job/通知、至少「單串發文或 mock 同等」一條 live 路徑。 +- [ ] 同一 UI 不需重畫。 + +### 全域 + +- [ ] 產品決策以本文 + 後續 spec/plan/tasks 為準。 + +--- + +## 10. 約束與假設 + +- **重構工具:** 以 Grok 4.5 / 本 repo agent 為主;`/spec-driven` 管文件閘門。 +- **可重寫 monorepo**;舊碼是零件庫與行為參考。 +- **後端契約精神可保留**(envelope、JWT 分離、job 狀態機),實作可新建。 +- **前端:** 輕量 React + Vite(或同等);不引入重型 UI kit;**新 design token 允許全新一輪命名**。 +- **假設:** B 階段你會參與 1~2 輪流程/視覺拍板,否則 mock 白做。 +- **假設:** Threads App 憑證在接 C 時可取得。 +- **互回:** 僅自有已綁帳號;P0-B 單線接龍;**reply 發言池含 lead**(主帳可回別人的回覆)。 + +--- + +## 11. 風險 + +| 風險 | 緩解 | +|------|------| +| 重寫範圍爆炸 | 死守 A→B→C→D;B 過關才加深後端 | +| Mock 與真 API 契約漂移 | 先定 thin types / MSW 或 mock 模組與未來 API 同 shape | +| 視覺改了又改 | B 只定 **一套** 預設主題;大改須改 requirements/spec | +| 舊資料/舊 job 遷移 | C 再定;B 不承諾遷移 | +| 多帳互回後端複雜 | B 只鎖 UX;C 垂直切片接真 | + +--- + +## 12. 開放問題(請你拍板) + +1. **重寫落點:** ~~二選一~~ → **已決策:舊碼進 `old/`;新前端 `apps/web`;新後端另建(如 `services/api`),不從舊樹原地改。** +2. **登入是否要 email 驗證才能進?** **預設:A 可先關,P1 再開。** +3. **視覺提案「Harbor Desk」是否當預設?** 若否,請給 3 個形容詞(例如:極簡黑白 / 編輯器感 / 更活潑)。 +4. **Scout(海巡)在 B 階段:** 完全隱藏,還是放「即將推出」空頁?**預設:空頁占位,免導覽以後大改。** +5. **Mock 持久化:** 僅 memory,還是 localStorage 跨重新整理?**預設:localStorage。** + +--- + +## 13. 附錄:與舊需求的差異(摘要) + +| 舊 | 新 | +|----|-----| +| 不在舊樹上重寫 monorepo | **允許整包重構/重寫** | +| 海巡/文案/發布 P0 一次閉環 | **先 A 地基 + B 前端 mock,再 C 接真** | +| 沿用田園掌機 UI | **新 IA + Harbor Desk 風格提案** | +| 多帳互回(已納入) | **保留,且 B 階段就要流程順** | + +--- + +## 14. 批准 + +- [x] 需求已批准:2026-07-09(使用者 OK;採預設:`apps/web`、Harbor Desk、海巡空頁、email 驗證 A 先關、mock localStorage) +- 下一步:審核 [spec.md](./spec.md) → 回 `spec OK` 後寫 plan diff --git a/docs/product/haixun-console/spec.md b/docs/product/haixun-console/spec.md new file mode 100644 index 0000000..d280384 --- /dev/null +++ b/docs/product/haixun-console/spec.md @@ -0,0 +1,463 @@ +# Spec: 巡樓 Console 重構(Harbor Desk) + +> Status: **`approved`** +> Source: `docs/product/haixun-console/requirements.md`(approved 2026-07-09) +> Last updated: `2026-07-09` +> 批准:使用者要求開始拆 plan/tasks(2026-07-09) +> 變更:reply 步驟發言池含 lead(主帳可回別人的回覆) +> 範圍:Phase A 地基契約 + Phase B 前端/Mock 行為;Phase C 接真映射;Phase D 僅邊界預留 + +--- + +## 1. 摘要 + +重構後的巡樓 Console 分四階段交付。本規格鎖定: + +1. **平台地基**:登入會話、角色權限、Threads 帳號基礎、Job、站內通知。 +2. **新前端 Harbor Desk**:5 入口 IA、Studio 多帳互回 wizard、Outbox、通知;**預設 mock 資料**。 +3. **同一契約**在 `mock | live` 切換;B 未過關不開大型業務後端。 + +落點(2026-07-09 已封存舊碼): + +| 項目 | 決策 | +|------|------| +| 舊 monorepo | **`old/` 整包封存,只讀** | +| 新前端 | repo 根 `apps/web`(從零) | +| 新後端 | `services/api`(從零;A 可先 mock-only 前端) | +| 視覺 | Harbor Desk 為唯一預設主題 | +| 海巡 Scout | B 階段占位空頁 | +| Email 驗證閘 | A/B 關閉 | +| Mock 持久化 | `localStorage` | + +--- + +## 2. 名詞與實體 + +| 名詞 | 定義 | +|------|------| +| Member | 登入會員(email/password + JWT) | +| Role | 簡化:`member` \| `admin`(可多值) | +| ThreadsAccount | 已綁定的 Threads 經營帳號(OAuth token 由後端保管) | +| Lead | 本串**主帳號**:固定發 `root` 樓主貼;並屬於發言池,可在後續 `reply` 再出場 | +| Cast | 本串**額外可出場帳號**(通常不含 lead);與 lead 組成發言池 | +| Speakers(發言池) | `{ lead_account_id } ∪ cast_account_ids`;**每一則 reply 步驟的 `account_id` 必須 ∈ Speakers**,**允許 = lead** | +| ThreadPlay | **發文計畫 / 串場劇本**(一則主貼 + 有序回覆步驟) | +| PlayStep | 劇本一步:`kind=root\|reply`、綁定 `account_id`、文案、可選排程偏移;`reply` 語意為接上一步(單線) | +| OutboxItem | 出站佇列項目:對應某 PlayStep 的一次發送意圖與狀態 | +| Job | 背景任務 run(進度、狀態、結果摘要) | +| Notification | 站內通知(多由 Job 終態產生) | +| DataSource | `mock` \| `live`:前端資料來源模式 | + +--- + +## 3. 狀態機 + +### 3.1 Job + +```text +pending --> queued --> running --> succeeded + running --> failed + running --> cancel_requested --> cancelled +queued|pending --> cancelled (若支援提前取消) +* --> expired (lock 逾時,後端語意;mock 可簡化省略) +``` + +### 3.2 ThreadPlay + +```text +draft --> ready (劇本與帳號齊全、通過預覽) +ready --> scheduling (寫入 Outbox) +scheduling --> active (至少一步 in flight 或 scheduled) +active --> completed (全部步驟 succeeded 或 skipped) +active --> partial_failed (有 failed,可重試) +draft|ready --> archived +``` + +### 3.3 PlayStep / OutboxItem + +```text +draft --> scheduled --> publishing --> published + publishing --> failed +scheduled --> cancelled +failed --> scheduled (retry) +``` + +**規則:** + +- `root` 步驟未 `published` 前,後續 `reply` 不得進入 `publishing`。 +- `root` 若 `failed`,後續步驟保持 `scheduled` 或改 `blocked`(顯示「等待主貼」)。 +- `root.account_id` **必須** = `lead_account_id`。 +- 任一 `reply.account_id` **必須** ∈ Speakers(**含 lead**);校驗失敗不可 submit。 +- P0 單線:第 n 步 reply 視為回覆第 n−1 步已發出的那則(不另選 parent;D 再談樹狀)。 + +### 3.4 ThreadsAccount.connection + +```text +unknown --> connected +unknown --> error +connected --> error +error --> connected (重新授權/診斷成功) +``` + +--- + +## 4. 使用者流程 + +### 4.1 登入(A/B) + +1. 使用者開 `/login`,輸入 email、password(可選 tenant 若多租戶 UI 需要;mock 固定 `default`)。 +2. 成功 → 存 access/refresh(live)或 session flag(mock)→ 進 `/app/today`。 +3. 失敗 → 表單錯誤,不進主殼。 +4. 重新整理:有有效會話則進主殼,否則回登入。 +5. 登出 → 清會話 → 登入頁。 + +### 4.2 帳號 Crew(B mock / C live) + +1. 列表顯示已綁帳號:顯示名、頭像色、connection、是否預設 lead 候選。 +2. 「連接 Threads」:live 走 OAuth;mock 新增一個假帳號。 +3. 標記不健康帳號不可被選為 Lead/Cast(disabled + 原因)。 + +### 4.3 創作 Studio — 互回 wizard(B 核心) + +| 步驟 | 畫面行為 | +|------|----------| +| 1 主題 | 標題/一句主題;可「示範填入」 | +| 2 船隊 | 必選 1 Lead;多選 Cast(≥0,UI 列表可預設排除 lead,避免重複勾);不可選 error 帳號 | +| 3 劇本 | 第 0 步固定 `root` + `account_id=lead`(不可改帳);其後每步 `reply`:**下拉發言池 = lead + 全部 cast**(**含 lead**,以便主帳回別人的回覆)+ textarea;可增刪排序(root 固定頂) | +| 4 預覽 | 對話泡泡:帳號名 + 文案;root 標記;可見 lead 出現在非第 0 步 | +| 5 船期 | 主貼時間(datetime);回覆間隔(分鐘,預設 5);可預覽各步預計時間 | +| 6 提交 | 建/更新 ThreadPlay=`scheduling`,生成 OutboxItem;導向 Outbox 或 Today | + +任一步「上一步」保留表單狀態。草稿自動存(mock:`localStorage` key 見 §8)。 + +### 4.4 Outbox + +1. 列表:Play 標題、狀態、步驟進度(2/5)。 +2. 點進詳情:逐步狀態時間線。 +3. Mock「模擬發送」:依序 root→replies 推進狀態(可加速)。 +4. 失敗步驟顯示錯誤文案 +「重試」(mock 直接 succeeded)。 + +### 4.5 Today + +1. 卡片:待處理失敗、今日排程、進行中 Job、快捷「開新串場」。 +2. 點通知/失敗 → 對應 Outbox 或 Job 詳情。 + +### 4.6 通知 + +1. 頂欄鈴鐺:未讀數。 +2. 下拉最近 N 筆;點擊標記已讀並 navigate。 +3. Job `succeeded|failed` 產生通知(mock 在模擬結束時寫入)。 + +### 4.7 Scout(B) + +占位頁:標題「探查」、短說明「海巡獲客將在下一階段開放」、無假成功操作。 + +### 4.8 設定 + +- 顯示目前 `DataSource` 切換(dev 可見;prod 預設 live 且可藏)。 +- AI key:B 僅 UI 存 local(不送出);C 再定義。 + +--- + +## 5. 介面契約 + +### 5.0 共用規則 + +| 規則 | 規格 | +|------|------| +| Envelope | `{ code, message, data?, error? }`;成功 `code=102000` | +| 列表 | query `page`/`pageSize`;`data: { pagination, list }` | +| 時間 | API/DB:**unix nanoseconds int64 UTC**;UI 顯示本地時區 | +| Auth | `Authorization: Bearer `;401 → refresh 一次 → 仍失敗登出 | +| 禁止 | 未實作端點回 200+空成功;應 501 或明確業務錯誤 | + +前端 repository 介面(概念): + +```text +AuthRepo, MemberRepo, PermissionRepo, +ThreadsAccountRepo, ThreadPlayRepo, OutboxRepo, +JobRepo, NotificationRepo +``` + +`createRepos(dataSource)` → mock 或 live 實作同一 TypeScript interface。 + +### 5.1 HTTP / API(Phase A 最小 + C 對齊) + +> 路徑為**目標契約**;可與舊 `/api/v1` 對齊以利搬碼。Mock 不經 HTTP 亦可,但 shape 相同。 + +#### Auth / Member / Permission + +| Method | Path | 用途 | 要點 | +|--------|------|------|------| +| POST | `/api/v1/auth/login` | 登入 | body: email, password, tenant_id? → tokens | +| POST | `/api/v1/auth/refresh` | 換票 | refresh_token → tokens | +| POST | `/api/v1/auth/logout` | 登出 | auth | +| GET | `/api/v1/members/me` | 我 | tenant_id, uid, email, roles[] | +| GET | `/api/v1/permissions/me` | 我的權限 | keys[] 或 * | + +Admin 建號可延後;mock 內建一組使用者即可。 + +#### Threads accounts + +| Method | Path | 用途 | +|--------|------|------| +| GET | `/api/v1/threads-accounts` | 列表(pagination) | +| POST | `/api/v1/threads-accounts/oauth/start` | 取得授權 URL(live) | +| GET | `/api/v1/threads-accounts/:id` | 詳情 / 健康 | +| POST | `/api/v1/threads-accounts/:id/diagnostics` | 連線診斷(可選 A) | + +#### Jobs + +| Method | Path | 用途 | +|--------|------|------| +| GET | `/api/v1/jobs` | 列表 | +| GET | `/api/v1/jobs/:id` | 詳情 | +| POST | `/api/v1/jobs/:id/cancel` | 取消 | +| POST | `/api/v1/jobs` | 建立(A 可僅 demo 或內部) | + +Job 資源欄位(最低):`id, template_type, status, progress_summary, progress_percent, error?, created_at, updated_at, completed_at?` + +#### Notifications + +| Method | Path | 用途 | +|--------|------|------| +| GET | `/api/v1/notifications` | 列表 | +| POST | `/api/v1/notifications/:id/read` | 已讀 | +| POST | `/api/v1/notifications/read-all` | 全已讀 | + +欄位:`id, title, body, kind, ref_type, ref_id, read_at?, created_at` + +#### ThreadPlay / Outbox(B mock 完整;C 再 live) + +| Method | Path | 用途 | +|--------|------|------| +| GET/POST | `/api/v1/thread-plays` | 列表 / 建立 | +| GET/PATCH | `/api/v1/thread-plays/:id` | 讀 / 更新劇本 | +| POST | `/api/v1/thread-plays/:id/submit` | 提交進 Outbox | +| GET | `/api/v1/outbox` | 佇列列表 | +| GET | `/api/v1/outbox/:id` | 詳情(含 steps) | +| POST | `/api/v1/outbox/:id/steps/:stepId/retry` | 重試一步 | + +**ThreadPlay 主體(邏輯 JSON):** + +```json +{ + "id": "…", + "title": "週末咖啡話題", + "topic": "…", + "status": "draft", + "lead_account_id": "…", + "cast_account_ids": ["…"], + "steps": [ + { + "id": "s0", + "sort_order": 0, + "kind": "root", + "account_id": "…", + "text": "主貼文案", + "delay_from_previous_sec": 0 + }, + { + "id": "s1", + "sort_order": 1, + "kind": "reply", + "account_id": "…", + "text": "回覆文案", + "delay_from_previous_sec": 300 + } + ], + "schedule_start_at": 0, + "created_at": 0, + "updated_at": 0 +} +``` + +### 5.2 前端路由(`apps/web`) + +使用 **path router**(非 hash),basename 可 `/`。 + +| Path | 名稱 | 需登入 | Phase | +|------|------|--------|-------| +| `/login` | 登入 | 否 | B | +| `/app` | 佈局殼 | 是 | B | +| `/app/today` | 今日 | 是 | B | +| `/app/crew` | 帳號 | 是 | B | +| `/app/studio` | 創作列表 | 是 | B | +| `/app/studio/new` | 新 wizard | 是 | B | +| `/app/studio/:playId` | 編輯 wizard | 是 | B | +| `/app/outbox` | 發送佇列 | 是 | B | +| `/app/outbox/:id` | 佇列詳情 | 是 | B | +| `/app/jobs` | 任務(可二級) | 是 | B | +| `/app/jobs/:id` | 任務詳情 | 是 | B | +| `/app/scout` | 探查占位 | 是 | B | +| `/app/settings` | 設定 | 是 | B | + +**導覽(一級,≤5):** Today · Crew · Studio · Scout · Outbox +Jobs 從 Today/通知進入;Settings 在使用者選單。 + +### 5.3 Job types(A 最小) + +| template_type | 用途 | Phase | +|---------------|------|-------| +| `demo_long_task` | 驗證 job+通知 | A/B mock 可模擬 | +| (業務 job) | 發佈串場、8D… | C/D | + +B mock 可不跑真 worker:Timer 推進 job 狀態並寫 notification。 + +--- + +## 6. 與現況對照 + +| 能力 | 決策 | 備註 | +|------|------|------| +| 整個舊 monorepo | **Archive** | 實體路徑 `old/`;主線不引用為 package | +| 舊 Auth/JWT/envelope | **參考後重寫** | 契約精神可相容 | +| 舊 Job 核心 | **參考後重寫** | 可抄模式,不搬半成品 | +| copy_mission 管線 | **Discard** | 不復活 | +| 舊 web Console | **Archive** | 新 `apps/web` | +| 海巡 Patrol 全套 | **Defer D** | Scout 占位 | +| 田園掌機 UI | **Discard 預設** | Harbor Desk | +| 時間/envelope/job 規則 | **Keep as policy** | 見根目錄 `AGENTS.md` | + +--- + +## 7. 非功能 + +### 安全 / 權限 + +- 未登入不可進 `/app/**`。 +- `admin` 專屬操作(若有)前後端雙檢。 +- Live:token 不進 URL;refresh 單飛。 +- Mock:不模擬權限繞過成功寫入 live。 + +### 錯誤 + +- UI 可讀中文 message。 +- 區分 401(登出)vs 5xx(保留會話,顯示重試)。 + +### 可觀測 + +- Job 進度摘要必顯示。 +- Outbox 步驟級狀態必顯示。 + +### 效能(B) + +- 首屏可互動 < 3s(本機 dev 網路)。 +- 列表 mock ≤ 50 筆即可。 + +### 無障礙(基線) + +- 表單 label 關聯;主按鈕對比足夠;焦點可見。 + +--- + +## 8. 資料保存(邏輯) + +### 8.1 Mock localStorage keys + +| Key | 內容 | +|-----|------| +| `harbor.datasource` | `mock` \| `live` | +| `harbor.session` | mock 登入態 | +| `harbor.accounts` | ThreadsAccount[] | +| `harbor.plays` | ThreadPlay[] | +| `harbor.outbox` | Outbox 聚合 | +| `harbor.jobs` | Job[] | +| `harbor.notifications` | Notification[] | +| `harbor.play_draft` | wizard 未提交草稿 | + +### 8.2 實體欄位(最低) + +| 實體 | 關鍵欄位 | +|------|----------| +| Member | uid, tenant_id, email, roles | +| ThreadsAccount | id, username, display_name, connection, is_usable | +| ThreadPlay | 見 §5.1 JSON | +| OutboxBundle | id, play_id, status, steps[] | +| OutboxStep | step_id, account_id, status, error?, published_at?, scheduled_at? | +| Job | 見 §5.1 | +| Notification | 見 §5.1 | + +--- + +## 9. UI 規格(Harbor Desk) + +### 9.1 Layout + +```text +┌─────────────────────────────────────────────┐ +│ Logo [今日]… 🔔 主題 使用者 ▾ │ topbar +├──────────┬──────────────────────────────────┤ +│ Today │ Page title │ +│ Crew │ Content │ +│ Studio │ │ +│ Scout │ │ +│ Outbox │ │ +└──────────┴──────────────────────────────────┘ +``` + +- 桌面:左導覽 ~220px。 +- 手機:底欄 5 格(Today/Crew/Studio/Scout/Outbox);設定進使用者選單。 +- 內容最大寬 ~1080px,表單閱讀寬 ~640–720px(wizard)。 + +### 9.2 視覺 token(建議名,實作可微調色值) + +| Token | 用途 | 建議 | +|-------|------|------| +| `--hb-bg` | 頁背景 | 冷灰白 | +| `--hb-surface` | 卡片 | 白 | +| `--hb-ink` | 主文 | 近黑 slate | +| `--hb-muted` | 次文 | 可讀灰(勿過淡) | +| `--hb-line` | 邊框 | 淺灰 | +| `--hb-brand` | 主色/CTA | 霧海青 | +| `--hb-danger` | 錯誤 | 紅 | +| `--hb-success` | 成功 | 綠 | +| `--radius` | 卡片 | 12–16px | +| `--font-sans` | 混排 | Inter, Taipei Sans TC, system | + +禁止:滿版動態雲、木質頂欄、emoji 主 icon、彩虹漸層按鈕。 + +### 9.3 元件基線 + +Button(primary/ghost/danger)、Input、Textarea、Select、Card、Badge(狀態)、EmptyState、Stepper(wizard)、Toast、BellMenu。 + +--- + +## 10. 驗收場景(Given / When / Then) + +1. **Given** 未登入 **When** 開 `/app/today` **Then** 導向 `/login`。 +2. **Given** mock 帳密正確 **When** 登入 **Then** 進 Today 且看到至少一張待辦/空狀態。 +3. **Given** 兩個 usable 帳號 A、B **When** Studio:lead=A cast=B,劇本為 root(A) → reply(B) → reply(A) **Then** 可預覽三句;submit 後 Outbox 三步順序正確且第二則回覆發言者為 lead。 +3b. **Given** 劇本某 reply 選了不在 Speakers 的帳號 **When** 提交 **Then** 阻擋並提示。 +4. **Given** 已提交串 **When** 模擬發送 **Then** root→reply 依序 published;通知出現成功。 +5. **Given** root 模擬失敗 **When** 查看 **Then** reply 不標 published;可重試 root。 +6. **Given** Job 模擬完成 **When** 開鈴鐺 **Then** 未讀 +1,點進 job 詳情。 +7. **Given** Scout **When** 進入 **Then** 僅占位,無「成功建立海巡」類假操作。 +8. **Given** live 模式且後端 5xx **When** 拉 me **Then** 不強制清 token(與需求 US-09 對齊,C 必須)。 + +--- + +## 11. Phase 邊界(實作不得越級) + +| Phase | 做 | 不做 | +|-------|----|------| +| A | 契約+可選最小 live auth/job;或純 mock 平台殼 | 海巡、完整互回後端 | +| B | `apps/web` 全 mock 體驗 | 大寫業務 API | +| C | 逐 repo 接 live | 改 IA 大翻(除非修 bug) | +| D | 海巡等 | — | + +--- + +## 12. 開放規格問題 + +1. **Tenant UI:** login 是否顯示 tenant 欄?**預設:隱藏,mock/live 固定 default tenant。** +2. **Lead 能否出現在 reply?** → **已決策:必須支援**(主帳回別人的回覆)。Cast 多選 UI 仍可不重複勾 lead;發言下拉一定含 lead。 +3. **A 是否必須先有可跑的 Go gateway,還是 B 可完全純前端?** **預設:B 可純前端 mock;A 的 live 可與 B 平行,但不阻塞 B 過關。** +4. **Outbox 與舊 publish_queue 是否同 collection?** **預設:C 再定映射;規格層當新模型。** + +--- + +## 13. 批准 + +- [x] Spec 已批准:2026-07-09(使用者:好請幫我開始拆) +- 下一步:依 [plan.md](./plan.md) 與 [tasks/](./tasks/) 實作 diff --git a/docs/product/haixun-console/tasks/INDEX.md b/docs/product/haixun-console/tasks/INDEX.md new file mode 100644 index 0000000..5a07f17 --- /dev/null +++ b/docs/product/haixun-console/tasks/INDEX.md @@ -0,0 +1,35 @@ +# Tasks INDEX — haixun-console + +> Phase B 主線:T001–T022 +> Phase C/D:T100+ / T200+ 預留(未建檔) + +| ID | Title | Milestone | Status | Est. lines | +|----|-------|-----------|--------|------------| +| T001 | Scaffold apps/web (Vite React TS) | M0 | done | ~120 | +| T002 | Harbor design tokens + global CSS + fonts | M1 | todo | ~150 | +| T003 | UI primitives (Button Input Card …) | M1 | todo | ~180 | +| T004 | App shell layout + desktop nav + mobile dock | M1 | todo | ~180 | +| T005 | Router + route table + placeholder pages | M1 | todo | ~150 | +| T006 | Mock auth session + login page + guard | M2 | todo | ~160 | +| T007 | Domain types + repo interfaces + createRepos | M3 | todo | ~180 | +| T008 | Mock repos + localStorage + seed data | M3 | todo | ~200 | +| T009 | Crew page (accounts list + mock connect) | M4 | todo | ~160 | +| T010 | Studio list page (plays) | M5 | todo | ~120 | +| T011 | Studio wizard shell + stepper + draft persist | M5 | todo | ~160 | +| T012 | Wizard steps: topic + crew (lead/cast) | M5 | todo | ~160 | +| T013 | Wizard script steps (speakers include lead) | M5 | todo | ~180 | +| T014 | Wizard preview + schedule + submit to outbox | M6 | todo | ~180 | +| T015 | Outbox list + detail timeline | M7 | todo | ~160 | +| T016 | Outbox simulate publish + retry rules | M7 | todo | ~180 | +| T017 | Mock jobs + progress timer | M8 | todo | ~150 | +| T018 | Notification center + bell menu | M8 | todo | ~160 | +| T019 | Today dashboard page | M8 | todo | ~150 | +| T020 | Scout placeholder + Settings (datasource) | M9 | todo | ~120 | +| T021 | Seed example play root→cast→lead | M9 | todo | ~100 | +| T022 | RWD polish + build smoke checklist | M9 | todo | ~120 | + +## How to work + +1. Pick next `todo` whose Depends on are `done`. +2. Set status `doing` → implement only that task → acceptance → `done`. +3. Do not start T100+ until Phase B human OK. diff --git a/docs/product/haixun-console/tasks/T001-scaffold-apps-web.md b/docs/product/haixun-console/tasks/T001-scaffold-apps-web.md new file mode 100644 index 0000000..dda30f7 --- /dev/null +++ b/docs/product/haixun-console/tasks/T001-scaffold-apps-web.md @@ -0,0 +1,52 @@ +# T001 — Scaffold apps/web (Vite React TS) + +> Status: `done` +> Milestone: `M0` +> Est. change: `~120 lines` +> Completed: 2026-07-09 + +## Goal + +repo 根下有可啟動的 `apps/web`(Vite + React + TypeScript)。 + +## Depends on + +- — + +## Inputs + +- Spec §5.2 路由前綴、§1 落點 `apps/web` +- 根 `README.md` / `AGENTS.md` + +## Outputs + +### 程式變更(預期路徑) + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/package.json` | add | scripts: dev, build, preview | +| `apps/web/vite.config.ts` | add | React plugin | +| `apps/web/tsconfig*.json` | add | strict TS | +| `apps/web/index.html` | add | root mount | +| `apps/web/src/main.tsx` | add | render App | +| `apps/web/src/App.tsx` | add | hello 占位 | +| `README.md` | edit | 補充 `cd apps/web && npm run dev` | + +### 行為變更 + +- `npm run dev` 可開本機頁面 + +## Out of scope + +- 路由、設計系統、mock 資料 +- 後端 + +## Acceptance + +- [ ] `cd apps/web && npm install && npm run build` 成功 +- [ ] `npm run dev` 可開啟(手動) +- [ ] 不依賴 `old/` + +```bash +cd apps/web && npm install && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T002-design-tokens.md b/docs/product/haixun-console/tasks/T002-design-tokens.md new file mode 100644 index 0000000..8efb739 --- /dev/null +++ b/docs/product/haixun-console/tasks/T002-design-tokens.md @@ -0,0 +1,48 @@ +# T002 — Harbor design tokens + global CSS + fonts + +> Status: `todo` +> Milestone: `M1` +> Est. change: `~150 lines` + +## Goal + +全域 CSS 變數(Harbor Desk)與 Inter + Taipei Sans TC 已載入。 + +## Depends on + +- T001 + +## Inputs + +- Spec §9.2 視覺 token、§9 禁止項 +- 字體:Inter(CDN 或 npm)、Taipei Sans TC(npm 如 `taipei-sans-tc` 或等價) + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/styles/tokens.css` | add | `--hb-*` tokens light(+ 可選 dark data-theme) | +| `apps/web/src/styles/global.css` | add | reset、body 字體、line-height | +| `apps/web/index.html` 或 css import | edit | 字體載入 | +| `apps/web/src/main.tsx` | edit | import global.css | + +### 行為變更 + +- 頁面使用 token 色,無 emoji 主視覺 + +## Out of scope + +- 完整元件庫(T003) +- 舊 `hx-*` / 雲朵場景 + +## Acceptance + +- [ ] body 可見 Inter/中文字體 +- [ ] CSS 變數 `--hb-brand` 等存在 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T003-ui-primitives.md b/docs/product/haixun-console/tasks/T003-ui-primitives.md new file mode 100644 index 0000000..53d8b55 --- /dev/null +++ b/docs/product/haixun-console/tasks/T003-ui-primitives.md @@ -0,0 +1,48 @@ +# T003 — UI primitives + +> Status: `todo` +> Milestone: `M1` +> Est. change: `~180 lines` + +## Goal + +可重用基礎元件:Button、Input、Textarea、Select、Card、Badge、EmptyState。 + +## Depends on + +- T002 + +## Inputs + +- Spec §9.3 +- tokens from T002 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/components/ui/*.tsx` | add | 各元件 | +| `apps/web/src/components/ui/index.ts` | add | barrel export | +| 可選 `*.module.css` 或共用 class | add | 用 `--hb-*` | + +### 行為變更 + +- Button variants: primary / ghost / danger +- Badge 可表達 success/danger/muted + +## Out of scope + +- Stepper、BellMenu、Toast(可後續 task 用 primitive 組) +- 業務頁 + +## Acceptance + +- [ ] 各元件有 children/label 可渲染 +- [ ] `npm run build` 通過 +- [ ] 無重型 UI 庫依賴 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T004-app-shell.md b/docs/product/haixun-console/tasks/T004-app-shell.md new file mode 100644 index 0000000..27f7975 --- /dev/null +++ b/docs/product/haixun-console/tasks/T004-app-shell.md @@ -0,0 +1,49 @@ +# T004 — App shell layout + nav + mobile dock + +> Status: `todo` +> Milestone: `M1` +> Est. change: `~180 lines` + +## Goal + +登入後主殼:頂欄(logo、通知占位、使用者占位)+ 左導覽 5 項 + 手機底欄 5 項。 + +## Depends on + +- T003 + +## Inputs + +- Spec §9.1 Layout、§5.2 五入口:Today / Crew / Studio / Scout / Outbox + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/components/layout/AppShell.tsx` | add | 殼 | +| `apps/web/src/components/layout/SidebarNav.tsx` | add | 桌面 | +| `apps/web/src/components/layout/MobileDock.tsx` | add | 手機 | +| `apps/web/src/components/layout/Topbar.tsx` | add | 頂欄 | +| styles | add/edit | layout CSS | + +### 行為變更 + +- 導覽項可高亮 active(依 path) +- 內容區 `` 或 children + +## Out of scope + +- 真通知、真登出邏輯(T006/T018) +- Scout 內容 + +## Acceptance + +- [ ] 桌面見側欄 5 項 +- [ ] 窄螢幕見底欄 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T005-router-placeholders.md b/docs/product/haixun-console/tasks/T005-router-placeholders.md new file mode 100644 index 0000000..0e85958 --- /dev/null +++ b/docs/product/haixun-console/tasks/T005-router-placeholders.md @@ -0,0 +1,48 @@ +# T005 — Router + route table + placeholder pages + +> Status: `todo` +> Milestone: `M1` +> Est. change: `~150 lines` + +## Goal + +React Router path 路由就緒;各 `/app/*` 有占位頁。 + +## Depends on + +- T004 + +## Inputs + +- Spec §5.2 路由表 +- 依賴:`react-router-dom` + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/App.tsx` | edit | BrowserRouter routes | +| `apps/web/src/pages/LoginPage.tsx` | add | 占位 | +| `apps/web/src/pages/TodayPage.tsx` 等 | add | 各入口占位 | +| `apps/web/src/pages/jobs/*` | add | 可選二級占位 | + +### 行為變更 + +- `/login`、`/app/today`… 可直連 +- `/app` redirect → `/app/today` + +## Out of scope + +- Auth guard(T006) +- 業務內容 + +## Acceptance + +- [ ] 手動點導覽可切頁無 404 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T006-mock-auth.md b/docs/product/haixun-console/tasks/T006-mock-auth.md new file mode 100644 index 0000000..ae4dbc7 --- /dev/null +++ b/docs/product/haixun-console/tasks/T006-mock-auth.md @@ -0,0 +1,51 @@ +# T006 — Mock auth session + login + guard + +> Status: `todo` +> Milestone: `M2` +> Est. change: `~160 lines` + +## Goal + +mock 登入後可進 `/app/**`;未登入強制 `/login`;logout 清 session。 + +## Depends on + +- T005 + +## Inputs + +- Spec §4.1、§5.0 Auth +- mock 帳密可寫死 e.g. `demo@harbor.local` / `demo`(文件註明) + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/auth/AuthContext.tsx` | add | session state | +| `apps/web/src/auth/RequireAuth.tsx` | add | 路由守衛 | +| `apps/web/src/pages/LoginPage.tsx` | edit | 表單 + 錯誤 | +| `apps/web/src/lib/storage.ts` | add | localStorage helpers(可先做) | +| Topbar | edit | 登出按鈕 | + +### 行為變更 + +- 重整後 mock session 仍在 +- 錯密碼顯示錯誤、不進 app + +## Out of scope + +- JWT / live API +- email 驗證 + +## Acceptance + +- [ ] 未登入開 `/app/today` → `/login` +- [ ] 正確登入 → Today +- [ ] 登出 → login 且不可回 app +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T007-repo-interfaces.md b/docs/product/haixun-console/tasks/T007-repo-interfaces.md new file mode 100644 index 0000000..e4c5d8b --- /dev/null +++ b/docs/product/haixun-console/tasks/T007-repo-interfaces.md @@ -0,0 +1,48 @@ +# T007 — Domain types + repo interfaces + createRepos + +> Status: `todo` +> Milestone: `M3` +> Est. change: `~180 lines` + +## Goal + +TypeScript domain 型別 + repository 介面;`createRepos('mock'|'live')` 工廠就位(live 可 throw/not implemented)。 + +## Depends on + +- T006 + +## Inputs + +- Spec §2 實體、§5.1 JSON 形狀、§5.0 共用規則 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/domain/types.ts` | add | Member, ThreadsAccount, ThreadPlay, PlayStep, Outbox*, Job, Notification | +| `apps/web/src/data/repos.ts` | add | interface 定義 | +| `apps/web/src/data/createRepos.ts` | add | 工廠 | +| `apps/web/src/data/DataContext.tsx` | add | 提供 repos 給 React | + +### 行為變更 + +- 型別含 `lead_account_id`, `cast_account_ids`, steps with kind root|reply +- Speakers 校驗 helper 可放 `domain/speakers.ts` + +## Out of scope + +- mock 實作細節(T008) +- HTTP client live + +## Acceptance + +- [ ] 型別可 compile +- [ ] `createRepos('mock')` 可呼叫(實作可先空再由 T008 補) +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T008-mock-repos-seed.md b/docs/product/haixun-console/tasks/T008-mock-repos-seed.md new file mode 100644 index 0000000..76c2093 --- /dev/null +++ b/docs/product/haixun-console/tasks/T008-mock-repos-seed.md @@ -0,0 +1,48 @@ +# T008 — Mock repos + localStorage + seed + +> Status: `todo` +> Milestone: `M3` +> Est. change: `~200 lines` + +## Goal + +全部 mock repository 讀寫 localStorage;首次進入寫入 seed 帳號/通知/job。 + +## Depends on + +- T007 + +## Inputs + +- Spec §8.1 keys、§8.5 mock 原則 +- keys 前綴 `harbor.*` + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/data/mock/*.ts` | add | 各 repo 實作 | +| `apps/web/src/data/mock/seed.ts` | add | 初始資料 | +| `apps/web/src/data/createRepos.ts` | edit | 接上 mock | + +### 行為變更 + +- 至少 2 個 usable ThreadsAccount +- 1 job running 或 demo、1 unread notification(可選) +- CRUD ThreadPlay / Outbox 可用(即使 UI 尚未接) + +## Out of scope + +- 模擬發送狀態機完整邏輯可簡化,細節在 T016 強化 + +## Acceptance + +- [ ] 重整後 seed 不重複爆量(idempotent seed) +- [ ] 手動在 console 或暫用 dev helper 可 list accounts +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T009-crew-page.md b/docs/product/haixun-console/tasks/T009-crew-page.md new file mode 100644 index 0000000..61729b3 --- /dev/null +++ b/docs/product/haixun-console/tasks/T009-crew-page.md @@ -0,0 +1,45 @@ +# T009 — Crew page (accounts) + +> Status: `todo` +> Milestone: `M4` +> Est. change: `~160 lines` + +## Goal + +`/app/crew` 顯示帳號列表;mock「連接」新增假帳;error 狀態標示不可用。 + +## Depends on + +- T008 + +## Inputs + +- Spec §4.2 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/CrewPage.tsx` | edit | 列表 UI | +| 可選 `components/crew/*` | add | 卡片 | + +### 行為變更 + +- 顯示 username/display_name/connection badge +- 按鈕「模擬連接帳號」→ 新增 usable 帳號 + +## Out of scope + +- 真 OAuth + +## Acceptance + +- [ ] 登入後 Crew 可見 seed 帳號 +- [ ] 可新增 mock 帳號 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T010-studio-list.md b/docs/product/haixun-console/tasks/T010-studio-list.md new file mode 100644 index 0000000..cd535fe --- /dev/null +++ b/docs/product/haixun-console/tasks/T010-studio-list.md @@ -0,0 +1,45 @@ +# T010 — Studio list page + +> Status: `todo` +> Milestone: `M5` +> Est. change: `~120 lines` + +## Goal + +`/app/studio` 列出 ThreadPlay;可進「開新串」與編輯既有。 + +## Depends on + +- T009 + +## Inputs + +- Spec §4.3、路由 `/app/studio`, `/app/studio/new`, `/app/studio/:id` + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/StudioPage.tsx` | edit | 列表 + CTA | +| Link 到 wizard | — | | + +### 行為變更 + +- 空狀態引導開新串 +- 顯示 title/status + +## Out of scope + +- wizard 內容(T011+) + +## Acceptance + +- [ ] 列表渲染 mock plays(可為空) +- [ ] 「開新串」進 `/app/studio/new` +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T011-wizard-shell.md b/docs/product/haixun-console/tasks/T011-wizard-shell.md new file mode 100644 index 0000000..e09db76 --- /dev/null +++ b/docs/product/haixun-console/tasks/T011-wizard-shell.md @@ -0,0 +1,47 @@ +# T011 — Studio wizard shell + stepper + draft persist + +> Status: `todo` +> Milestone: `M5` +> Est. change: `~160 lines` + +## Goal + +6 步 wizard 殼(主題→船隊→劇本→預覽→船期→提交)+ 步驟指示;草稿 autosave localStorage。 + +## Depends on + +- T010 + +## Inputs + +- Spec §4.3 wizard 步驟 +- key `harbor.play_draft`(spec §8.1) + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/studio/WizardPage.tsx` | add | 殼 | +| `apps/web/src/components/studio/Stepper.tsx` | add | | +| `apps/web/src/pages/studio/wizardState.ts` | add | draft type + load/save | + +### 行為變更 + +- 上一步/下一步 +- 重新整理不丟未提交 draft(同 browser) + +## Out of scope + +- 各步表單細節(T012–T014) + +## Acceptance + +- [ ] 6 步可切換 +- [ ] draft 持久化 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T012-wizard-topic-crew.md b/docs/product/haixun-console/tasks/T012-wizard-topic-crew.md new file mode 100644 index 0000000..ff23709 --- /dev/null +++ b/docs/product/haixun-console/tasks/T012-wizard-topic-crew.md @@ -0,0 +1,47 @@ +# T012 — Wizard: topic + crew (lead/cast) + +> Status: `todo` +> Milestone: `M5` +> Est. change: `~160 lines` + +## Goal + +步驟 1 主題、步驟 2 船隊:選 1 lead + 多選 cast;不可選 error 帳號。 + +## Depends on + +- T011 + +## Inputs + +- Spec §4.3 步驟 1–2;Crew accounts from repos + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/studio/steps/TopicStep.tsx` | add | | +| `apps/web/src/pages/studio/steps/CrewStep.tsx` | add | radio lead + multi cast | +| WizardPage | edit | 接入 | + +### 行為變更 + +- cast 列表預設不重複勾 lead(lead 仍是 lead) +- 未選 lead 不可進下一步 + +## Out of scope + +- 劇本步驟(T013) + +## Acceptance + +- [ ] 必須選 lead +- [ ] 可多選 cast +- [ ] error 帳號 disabled +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T013-wizard-script-steps.md b/docs/product/haixun-console/tasks/T013-wizard-script-steps.md new file mode 100644 index 0000000..c5035f3 --- /dev/null +++ b/docs/product/haixun-console/tasks/T013-wizard-script-steps.md @@ -0,0 +1,47 @@ +# T013 — Wizard script steps (speakers include lead) + +> Status: `todo` +> Milestone: `M5` +> Est. change: `~180 lines` + +## Goal + +步驟 3 劇本:root 固定 lead;reply 每步帳號下拉 = **lead ∪ cast**(**含 lead**)。 + +## Depends on + +- T012 + +## Inputs + +- Spec §2 Speakers、§4.3 步驟 3、驗收 root→cast→lead + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/studio/steps/ScriptStep.tsx` | add | 步驟列表 | +| `apps/web/src/domain/speakers.ts` | add/edit | speakersOf(play)、validate | + +### 行為變更 + +- 新增/刪除 reply 步(root 不可刪) +- 每步文案 textarea +- reply 可選 lead +- 校驗:reply.account_id ∈ Speakers + +## Out of scope + +- 預覽與提交(T014) + +## Acceptance + +- [ ] 可建 root(A) reply(B) reply(A) +- [ ] 不在 Speakers 的選項不出現 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T014-wizard-preview-submit.md b/docs/product/haixun-console/tasks/T014-wizard-preview-submit.md new file mode 100644 index 0000000..b869cf9 --- /dev/null +++ b/docs/product/haixun-console/tasks/T014-wizard-preview-submit.md @@ -0,0 +1,48 @@ +# T014 — Wizard preview + schedule + submit + +> Status: `todo` +> Milestone: `M6` +> Est. change: `~180 lines` + +## Goal + +步驟 4 預覽泡泡、步驟 5 船期、步驟 6 提交:建立 ThreadPlay + Outbox,導向 Outbox 或詳情。 + +## Depends on + +- T013 + +## Inputs + +- Spec §4.3 步驟 4–6、§3 狀態機 draft→scheduling + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/studio/steps/PreviewStep.tsx` | add | | +| `apps/web/src/pages/studio/steps/ScheduleStep.tsx` | add | start + interval minutes | +| `apps/web/src/pages/studio/steps/SubmitStep.tsx` | add | | +| repos mock submit | edit | play + outbox steps | + +### 行為變更 + +- 預覽顯示每句帳號名 +- submit 失敗若校驗不過(無 lead / 空文案) +- 成功清 draft、導航 + +## Out of scope + +- 模擬發送(T016) + +## Acceptance + +- [ ] 預覽可見 lead 穿插回覆 +- [ ] submit 後 Outbox 有資料 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T015-outbox-ui.md b/docs/product/haixun-console/tasks/T015-outbox-ui.md new file mode 100644 index 0000000..8dff2c4 --- /dev/null +++ b/docs/product/haixun-console/tasks/T015-outbox-ui.md @@ -0,0 +1,46 @@ +# T015 — Outbox list + detail timeline + +> Status: `todo` +> Milestone: `M7` +> Est. change: `~160 lines` + +## Goal + +`/app/outbox` 列表與 `/app/outbox/:id` 步驟時間線。 + +## Depends on + +- T014 + +## Inputs + +- Spec §4.4 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/OutboxPage.tsx` | edit | 列表 | +| `apps/web/src/pages/OutboxDetailPage.tsx` | add | 時間線 | +| routes | edit | | + +### 行為變更 + +- 顯示 play 標題、狀態、步驟進度 n/m +- 詳情逐步 account + status badge + +## Out of scope + +- 模擬發送按鈕邏輯細節可接 T016 + +## Acceptance + +- [ ] 提交後列表看得到 +- [ ] 詳情步驟順序正確 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T016-outbox-simulate.md b/docs/product/haixun-console/tasks/T016-outbox-simulate.md new file mode 100644 index 0000000..483690d --- /dev/null +++ b/docs/product/haixun-console/tasks/T016-outbox-simulate.md @@ -0,0 +1,47 @@ +# T016 — Outbox simulate publish + retry rules + +> Status: `todo` +> Milestone: `M7` +> Est. change: `~180 lines` + +## Goal + +「模擬發送」依序 root→replies;root 未成功不發後續;失敗可重試該步;可模擬 root 失敗。 + +## Depends on + +- T015 + +## Inputs + +- Spec §3.3 規則、§4.4、驗收 4–5 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/data/mock/outboxSimulate.ts` | add | 狀態機 | +| OutboxDetailPage | edit | 模擬成功 / 模擬 root 失敗 / 重試 | +| 可選觸發 notification | edit | 完成時寫通知(或 T018 接) | + +### 行為變更 + +- publishing 中 UI 可 disable 重入 +- root failed → replies blocked/scheduled 不 published + +## Out of scope + +- 真 Threads API + +## Acceptance + +- [ ] 全成功路徑 +- [ ] root 失敗路徑 +- [ ] 重試 root 後可繼續 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T017-mock-jobs.md b/docs/product/haixun-console/tasks/T017-mock-jobs.md new file mode 100644 index 0000000..2df5cea --- /dev/null +++ b/docs/product/haixun-console/tasks/T017-mock-jobs.md @@ -0,0 +1,45 @@ +# T017 — Mock jobs + progress timer + +> Status: `todo` +> Milestone: `M8` +> Est. change: `~150 lines` + +## Goal + +Job 列表/詳情;可啟動 demo job,timer 推進 progress → succeeded/failed。 + +## Depends on + +- T008(可與 T016 平行,但建議在 T016 後) + +## Inputs + +- Spec §3.1、§5.3 demo_long_task、路由 `/app/jobs` + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/JobsPage.tsx` | edit | | +| `apps/web/src/pages/JobDetailPage.tsx` | add | | +| `apps/web/src/data/mock/jobRunner.ts` | add | setInterval 推進 | + +### 行為變更 + +- 狀態 badge +- 完成時可 emit 事件或直接寫 notification store + +## Out of scope + +- 真 worker + +## Acceptance + +- [ ] 可建立/看到 demo job 跑完 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T018-notifications.md b/docs/product/haixun-console/tasks/T018-notifications.md new file mode 100644 index 0000000..f5aadec --- /dev/null +++ b/docs/product/haixun-console/tasks/T018-notifications.md @@ -0,0 +1,46 @@ +# T018 — Notification center + bell menu + +> Status: `todo` +> Milestone: `M8` +> Est. change: `~160 lines` + +## Goal + +頂欄鈴鐺:未讀數、下拉列表、點擊已讀並導向 ref(job/outbox)。 + +## Depends on + +- T017(至少 job 完成會產通知;outbox 完成也可產) + +## Inputs + +- Spec §4.6 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/components/layout/BellMenu.tsx` | add | | +| Topbar | edit | 接入 | +| mock notification helpers | edit | create on job/outbox complete | + +### 行為變更 + +- 未讀 badge +- 點擊 navigate + mark read + +## Out of scope + +- email/push + +## Acceptance + +- [ ] job 或 outbox 完成後未讀 +1 +- [ ] 點擊可跳轉 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T019-today-page.md b/docs/product/haixun-console/tasks/T019-today-page.md new file mode 100644 index 0000000..7533386 --- /dev/null +++ b/docs/product/haixun-console/tasks/T019-today-page.md @@ -0,0 +1,43 @@ +# T019 — Today dashboard + +> Status: `todo` +> Milestone: `M8` +> Est. change: `~150 lines` + +## Goal + +`/app/today`:待處理失敗、今日排程摘要、進行中 job、快捷「開新串」。 + +## Depends on + +- T016, T017, T018(軟依賴:至少 outbox+jobs 有資料源) + +## Inputs + +- Spec §4.5 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/TodayPage.tsx` | edit | 卡片區塊 | + +### 行為變更 + +- 快捷進 `/app/studio/new` +- 失敗 outbox 可點進詳情 + +## Out of scope + +- 複雜分析圖表 + +## Acceptance + +- [ ] 登入後落地 Today 有意義內容或空狀態 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T020-scout-settings.md b/docs/product/haixun-console/tasks/T020-scout-settings.md new file mode 100644 index 0000000..4cdf399 --- /dev/null +++ b/docs/product/haixun-console/tasks/T020-scout-settings.md @@ -0,0 +1,46 @@ +# T020 — Scout placeholder + Settings (datasource) + +> Status: `todo` +> Milestone: `M9` +> Est. change: `~120 lines` + +## Goal + +Scout 占位無假成功;Settings 可切 `mock|live`(live 可提示未接)與顯示 mock 帳密說明。 + +## Depends on + +- T006, T007 + +## Inputs + +- Spec §4.7、§4.8 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/pages/ScoutPage.tsx` | edit | EmptyState 文案 | +| `apps/web/src/pages/SettingsPage.tsx` | edit | datasource toggle | +| storage `harbor.datasource` | edit | | + +### 行為變更 + +- Scout 無 submit 成功操作 +- 切 live 不崩潰(可顯示「尚未接上 API」) + +## Out of scope + +- 海巡功能 + +## Acceptance + +- [ ] Scout 符合占位 +- [ ] Settings 可切換並重整後仍在 +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T021-seed-example-play.md b/docs/product/haixun-console/tasks/T021-seed-example-play.md new file mode 100644 index 0000000..1392dbb --- /dev/null +++ b/docs/product/haixun-console/tasks/T021-seed-example-play.md @@ -0,0 +1,43 @@ +# T021 — Seed example play root→cast→lead + +> Status: `todo` +> Milestone: `M9` +> Est. change: `~100 lines` + +## Goal + +seed 內含示範串:root(lead) → reply(cast) → reply(lead),方便一進 Studio/Outbox 就看懂互回。 + +## Depends on + +- T008, T013 + +## Inputs + +- Spec 驗收 3、requirements 範例劇本 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| `apps/web/src/data/mock/seed.ts` | edit | 範例 play + 可選 outbox | + +### 行為變更 + +- 首次 seed 可看到三步對話語意 + +## Out of scope + +- AI 產文 + +## Acceptance + +- [ ] 清 localStorage 後重登,仍有範例(或文件說明 clear key) +- [ ] 範例第三步 account 為 lead +- [ ] `npm run build` 通過 + +```bash +cd apps/web && npm run build +``` diff --git a/docs/product/haixun-console/tasks/T022-rwd-smoke.md b/docs/product/haixun-console/tasks/T022-rwd-smoke.md new file mode 100644 index 0000000..1c3e8da --- /dev/null +++ b/docs/product/haixun-console/tasks/T022-rwd-smoke.md @@ -0,0 +1,44 @@ +# T022 — RWD polish + build smoke checklist + +> Status: `todo` +> Milestone: `M9` +> Est. change: `~120 lines` + +## Goal + +手機寬度可走完:登入 → Today → Studio 開串(簡)→ Outbox;build 穩定。 + +## Depends on + +- T014–T020 主路徑完成後 + +## Inputs + +- Spec §5.2 RWD、requirements Phase B 成功標準 + +## Outputs + +### 程式變更 + +| 路徑 | 動作 | 說明 | +|------|------|------| +| layout CSS | edit | 修 overflow、dock 遮擋 | +| 可選 `docs/product/.../B-CHECKLIST.md` | add | 人手測清單 | + +### 行為變更 + +- 主內容不被底欄遮住(padding-bottom) + +## Out of scope + +- 完美 pixel polish + +## Acceptance + +- [ ] `npm run build` 通過 +- [ ] 375px 寬手動點通主路徑無橫向爆版 +- [ ] INDEX 上 T001–T022 除本 task 外皆 done 後,本 task done → 等人審 B OK + +```bash +cd apps/web && npm run build +``` diff --git a/.dockerignore b/old/.dockerignore similarity index 100% rename from .dockerignore rename to old/.dockerignore diff --git a/old/AGENTS.md b/old/AGENTS.md new file mode 100644 index 0000000..2b151e7 --- /dev/null +++ b/old/AGENTS.md @@ -0,0 +1,419 @@ + +# Agent Handoff Notes + +這個資料夾是新的巡樓後端核心,請優先維持乾淨邊界,不要把舊 Next.js 或 `template-monorepo` 的業務包袱搬進來。 + +## 產品決策與重啟流程 + +- **產品真相來源:** `docs/product//`(requirements → spec → plan → tasks)。 +- **流程 skill:** 專案內 `/spec-driven`(`.grok/skills/spec-driven/`)。未批准 plan/tasks 前不要開大範圍實作。 +- 現況架構導覽:`docs/architecture.md`。技術債備忘:`docs/improvement-notes.md`(不取代 product requirements)。 + +## 核心原則 + +- 全系統時間一律 **UTC+0**;寫入 Mongo / API 的時間欄位一律 **unix nanoseconds**(`int64`)。排程的 `timezone` 只用於 cron 解讀與下發 payload,不作為儲存時區。 +- 複製模式,不複製舊業務。 +- `logic` 做 API 編排,`model/usecase` 做可重複使用能力。 +- provider adapter 不讀 setting、不碰 Mongo、不知道 HTTP。 +- setting 是通用 key-value model,不依賴 AI 或其他業務。 +- token / API key 第一版每次 request 帶入,不寫入 config。 +- SSE contract 由本服務 normalize,前端不要讀 provider 原始 chunk。 +- JSON API 必須使用 `code/message/data/error` envelope 與 `SSCCCDDD` 錯誤碼。 +- 列表 API 必須使用 `page/pageSize` query,並在 `data` 回傳 `pagination/list`。 +- Job 狀態轉移必須使用 guarded/conditional update;不要在 API/worker 直接裸 `Update` 覆蓋 job 狀態。 +- Redis job lock 的 value 是 `workerID`;release / refresh 必須檢查 owner,長任務必須 heartbeat。 +- Auth 目前是 native email/password + JWT,不包含 OAuth / OTP / MFA / Zitadel。不要為了相容 template-monorepo 把重依賴搬進來。 +- AI provider token 與會員 JWT 是兩種不同 token;AI token 每次 request header 帶入,會員 JWT 由 `/api/v1/auth/*` 簽發。 + +## 設計文件 + +- `docs/job-system-plan.md`:通用 job system 規劃,包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。 +- `docs/post-project-plan.md`:發文計畫(文案任務 Flow A 砍掉重做)— 主題發想、風格分析、矩陣產 N 篇草稿、定時發送。 +- `docs/scan-placement-plan.md`:海巡獲客(流程 B)— 知識圖譜、Brave 擴展、雙軌爬取(7 天重點 / 30 天補充)、產品匹配、島民交接。 + +## 新增 API 流程 + +1. 修改 `generate/api/*.api`。 +2. 優先使用 `make gen-api` 重新產生 handler/logic/types。 +3. 若手寫 handler,仍需遵守 `response.Write` 與 validator 流程。 +4. SSE endpoint 不使用 `response.Write`,直接輸出 `text/event-stream`。 +5. 更新 `README.md` 的 API 與架構說明。 + +## Response / Error Code + +錯誤碼格式是 `SSCCCDDD`: + +```text +SS = scope +CCC = category +DDD = detail +``` + +目前 scope: + +```text +10 = Facade +32 = Setting +33 = AI +34 = Job +35 = Auth +36 = Member +37 = Permission +``` + +建立錯誤時使用: + +```go +errs.For(code.AI).InputMissingRequired("缺少 AI provider token") +errs.For(code.Setting).ResNotFound("找不到設定") +errs.For(code.Job).ResInvalidState("job state changed; update rejected") +errs.For(code.Auth).AuthUnauthorized("missing bearer token") +``` + +不要直接手寫 `33104000` 這種數字,也不要回傳裸 `error` 給 handler 後讓使用者看到內部錯誤。 + +## Pagination + +列表 API 使用: + +```text +?page=1&pageSize=10 +``` + +response: + +```json +{ + "code": 102000, + "message": "SUCCESS", + "data": { + "pagination": { + "total": 100, + "page": 1, + "pageSize": 10, + "totalPages": 10 + }, + "list": [] + } +} +``` + +`page/pageSize` 必須是 server 正規化後的值。不要使用 `offset/limit/items`。 + +## 新增 Model 流程 + +模組放在: + +```text +internal/model// + domain/entity + domain/repository + domain/usecase + repository + usecase +``` + +依賴方向: + +```text +handler -> logic -> model/domain/usecase +model/usecase -> model/domain/repository +model/repository -> Mongo / Redis +``` + +不要讓 `logic` import `model//repository`。 + +## Auth / Permission 擴充 + +目前已接: + +```text +POST /api/v1/auth/register +POST /api/v1/auth/login +POST /api/v1/auth/refresh +POST /api/v1/auth/logout # requires member JWT +GET /api/v1/members/me +PATCH /api/v1/members/me +GET /api/v1/permissions/catalog +GET /api/v1/permissions/me +``` + +Auth matrix(`internal/handler/routes.go`): + +| 路由 | 需要會員 JWT | +|------|----------------| +| `GET /api/v1/health` | 否 | +| `POST /api/v1/auth/register/login/refresh` | 否 | +| `POST /api/v1/auth/logout` | 是(`Authorization`) | +| `GET /api/v1/ai/providers` | 否 | +| `POST /api/v1/ai/chat/stream/models` | 是(`X-Member-Authorization`)+ provider token(`Authorization`) | +| `/api/v1/members/*`、`/api/v1/permissions/me` | 是(`Authorization`) | +| `/api/v1/permissions/catalog`、`/api/v1/settings/*`、`/api/v1/jobs*`、`/api/v1/job/*` | 是(`Authorization`) | + +規則: + +- 保護路由用 `internal/middleware.Auth`(`Authorization: Bearer `);AI 變更路由用 `middleware.MemberAuth`(`X-Member-Authorization`),因 `Authorization` 保留給 provider API key。 +- logic 從 `authctx.ActorFromContext` 讀 `tenant_id` / `uid`。 +- 不要在 handler 直接 parse JWT;token 驗證集中在 `model/auth/usecase`。 +- 密碼只存 bcrypt hash,不回傳、不寫 log。 +- `members.roles` 第一版是簡化 role key。正式 RBAC 可逐步補 roles collection,但不要破壞 `role_permissions` 的 tenant + role_key contract。 +- `Auth.DevHeaderFallback` 只給本機開發,正式環境應關閉。 + +## AI Provider 擴充 + +新增 provider 時: + +1. 在 `internal/model/ai/domain/enum` 新增 provider id。 +2. 在 `internal/model/ai/provider` 新增 adapter。 +3. 在 `internal/model/ai/usecase` registry 註冊 provider 與 models。 +4. 確保 adapter 回傳統一 `StreamEvent`。 +5. 不要改 `logic/ai` 的 SSE 格式。 + +## Job Worker 擴充 + +新增 job step 時優先註冊 runner handler: + +```go +runner.RegisterStepHandler("analyze_8d", func(ctx context.Context, step job.StepContext) error { + if err := step.Heartbeat(ctx); err != nil { + return err + } + // do work, check cancel via job usecase if needed + return nil +}) +``` + +規則: + +- Handler 不要直接操作 Mongo / Redis,透過 job usecase 更新進度、完成、失敗或取消。 +- 長任務每個 checkpoint 呼叫 `StepContext.Heartbeat` 或 `RefreshRunLock`。 +- 收到 cancel signal 後呼叫 `AcknowledgeCancel(jobId, workerID)`,不要自行把狀態改成 `cancelled`。 +- release lock 時必須帶 `workerID`;不要新增無 owner 的 release helper。 + +## 前端設計規則(`web/`) + +巡樓 Console 前端在 `haixun-backend/web/`,視覺為**沉穩田園巡檢台**(動森感:天空、雲朵、奶油卡片、青綠 brand;**不是**任天堂 UI 複製)。 + +**字體固定** Inter + Taipei Sans TC;圖示僅 `AcIcon` / `AuthDecor` 內原創 SVG 線條圖。**禁止** emoji、貼圖 JPG、咖啡色木質頂欄、Nook / 任天堂命名。樣式集中在 `index.css`(`--hx-*` token + `hx-*` / `ac-*` / `auth-*` class)。 + +不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。 + +### 視覺架構(登入前後共用) + +```text +全頁背景 .hx-scene 灰藍天空 → 淡草地單一漸層(淺/深各一套,見 index.css) +裝飾層 SceneDecor 雲朵(多朵緩動)+ 淡光暈 + 小葉子;登入與 Layout 共用 +奶油卡片 .auth-ticket 2px line 邊框、圓角 2rem、surface 底、可選 .ac-dialog-texture 點陣 +頂部品牌列 圖示 .auth-ticket-icon(brand-soft 底)+ ink 標題;不用獨立色塊 ribbon +主內容 表單或 Outlet;內文頁用 PageTitle / Card(綠色 .ac-title-bar 僅內容區小標) +桌面側欄 .ac-pocket-device 掌上終端外框;PATROL PAD 狀態列;固定尺寸 + .ac-pocket-scroll 內捲 +手機 .ac-dock 底部最多 4 格 +「更多」sheet +``` + +| 區域 | 元件 | 關鍵 class | +|------|------|------------| +| 未登入 | `AuthShell` + `LoginPage` / `RegisterPage` | `hx-scene` `auth-scene` `auth-ticket` `auth-welcome` `auth-shell-form` | +| 已登入外殼 | `Layout` | `hx-scene` `ac-app-shell` `ac-app-header` `auth-ticket` `ac-app-main-inner` | +| 背景裝飾 | `AuthDecor.tsx` → `SceneDecor` | `hx-scene-deco` `auth-cloud--*` | +| 品牌小圖 | `AuthTicketIcon` | `auth-ticket-icon`(小屋+樹,原創 SVG) | +| 側欄導覽 | `Layout` + `navApps` | `ac-pocket-device` `ac-app-tile` | +| 手機導覽 | `MobileBottomNav` | `ac-dock` | + +**登入頁刻意不做的事**:上方不要獨立大色塊 header;表單上方**不要**再放 `ac-title-bar`「登入」大牌(品牌已在 `auth-welcome`)。註冊頁同理可省略重複大標。 + +**已淘汰、勿加回**:`ac-island`(改用 `hx-scene`)、`ac-wood-bar` / 咖啡色木質頂欄、`public/ac/` 貼圖、Nook Phone 文案。 + +### 技術棧與指令 + +```text +web/ + src/ + api/ # API client(envelope、JWT refresh) + auth/ # AuthContext + components/ # Layout、AuthShell、AuthDecor、ui、ThemeToggle、MobileBottomNav、AcIcon + theme/ # ThemeContext(淺色 / 深色) + pages/ # 路由頁面 + lib/ # acAssets(導覽 icon key)、jobStatus 等 + index.css # 設計 token 與場景樣式唯一來源 +``` + +```bash +make web-dev # dev server :5173,proxy 到 :8890 +make web-build # tsc + vite build +``` + +### 字型 + +| 語言 | 字型 | 載入方式 | +|------|------|----------| +| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`,在 `index.css` `@import` Regular + Bold | +| 英文 | **Inter**(與 simular.co 相同,Google Fonts 免費) | `web/index.html` link | + +規則: + +- `body` / 中文標題:`Inter` + `Taipei Sans TC` 混排(`--font-sans`)。 +- 純英文裝飾字(導覽副標、Hero 小字):加 class `display-en`,使用 `--font-en`。 +- 中文 `line-height` 維持 **1.7+**;不要用過細字重當標題(標題用 `font-bold` / `font-black`)。 +- 只載入 Taipei Sans TC **Regular + Bold**,不要載入 Light,避免小字過細。 +- 不要改回 Noto Sans TC,也不要手寫 `#333` 這類裸色碼當主色。 + +### 對比度與字級 + +- 內文、表頭、表單 label、卡片說明:優先 `text-ink` / `text-ink-secondary`,**不要**拿 `text-muted` 當主要閱讀文字。 +- `text-muted` 只給次要提示(筆數、hint、placeholder 用 `text-subtle`)。 +- 表單輸入字級 **15px**(`text-[15px]`),輸入框底用 `bg-surface` 白底,確保與背景拉開。 +- 淺色 `muted` 約 `#5a6578`、深色約 `#b8c4d6`;改色時以「小字仍可舒適閱讀」為準,不要回到 `#94a3b8` 那種淡灰。 + +### 主題(淺色 / 深色) + +- `ThemeProvider`(`src/theme/ThemeContext.tsx`)包住 App;偏好存 `localStorage` key:`haixun.theme`(`light` | `dark`)。 +- `index.html` 內嵌 script 在 React 載入前設定 `data-theme`,避免閃爍。 +- 所有顏色必須走 CSS 變數 `--hx-*`,再映射到 Tailwind `@theme`(`bg-canvas`、`text-brand` 等)。 +- 切換按鈕用 `ThemeToggle`(`ac-btn-secondary` 樣式);`Layout` 頂欄與 `AuthShell` 右上角都要有。 +- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。 + +淺色:低飽和灰藍天空 + 灰綠草地 + 奶油 `surface` + **brand 青綠**;深色:黃昏低對比、同一套 token 自動切換。頂欄與卡片內品牌區都用 **surface / ink / brand**,不要再用木色 `#c4a882` 當 header 底。 + +### 場景與卡片 class(維護時對照) + +| Class | 用途 | +|-------|------| +| `.hx-scene` | 全頁天空→草地漸層(登入 + 已登入根節點) | +| `.hx-scene-deco` / `SceneDecor` | 背景雲、光暈、葉子(`pointer-events: none`) | +| `.auth-ticket` | 奶油主卡片外框(登入卡、已登入主內容區) | +| `.auth-welcome` | 卡片內品牌列:圖示 + 標題 + 一句 tagline,底部分隔線 | +| `.ac-app-header` | 已登入 sticky 頂欄:半透明 surface + blur,**非**木色 | +| `.ac-title-bar` | 內容區綠色小標題(裝置色漸層);用於 `PageTitle` 等,**不**用於登入頁表單上方大牌 | +| `.ac-pocket-device` | 側欄掌上終端;`--pocket-width`(28rem)、`--pocket-screen-height` 固定,內容在 `.ac-pocket-scroll` 捲動 | +| `.ac-app-tile` / `.ac-dock` | App 格導覽、手機底欄 | +| `.auth-shell-form` | 登入/註冊表單放大字級(僅 auth 頁) | + +側欄標示用 **PATROL PAD** 等中性英文裝飾字(`display-en`);圖示僅 `AcIcon` SVG。 + +### 色彩 token(語意命名) + +開發時只用這些 Tailwind class(值定義在 `web/src/index.css`): + +| Token | 用途 | +|-------|------| +| `canvas` | 全頁背景 | +| `surface` / `surface-muted` | 卡片、輸入框底 | +| `ink` / `ink-secondary` / `muted` | 主文 / 次文 / 輔助 | +| `line` | 邊框 | +| `brand` / `brand-hover` / `brand-soft` | 主 CTA、active 導覽、連結 hover | +| `glow` | 裝飾色塊(`.glow-blob-alt`) | +| `success` / `warning` / `danger`(含 `*-soft`) | 狀態、錯誤、Job badge | + +主按鈕一律 `Button variant="primary"` → `bg-brand`,不要用全黑按鈕。 + +### 圓角與陰影 + +```text +--radius-sm 0.75rem 小元素、code +--radius-md 1.25rem Input / Textarea +--radius-lg 1.75rem Card +--radius-xl 2.25rem Hero、QuickLink、StatCard +--radius-pill 9999px Button、Badge、導覽 pill +``` + +陰影用 utility:`shadow-card`(一般卡片)、`shadow-soft`(主按鈕、Hero、`.auth-ticket`)。內容 Hero 可用 `ac-bulletin` + `ac-hero-gradient` token;全頁裝飾雲朵走 `SceneDecor`,不要另加會打架的強色 blob。 + +### 共用元件(優先復用) + +新頁面必須從 `src/components/ui.tsx` 組裝,不要另寫一套按鈕樣式: + +| 元件 | 用途 | +|------|------| +| `PageTitle` | 頁面標題 + 副標 | +| `Card` | 內容區塊 | +| `Field` + `Input` / `Textarea` | 表單 | +| `Button` | `primary` / `ghost` / `danger` / `soft` | +| `Badge` | 標籤 pill(`brand` / `sky` / `success` / `warning` / `danger` / `neutral`) | +| `StatCard` / `QuickLinkCard` | 總覽統計與快捷入口 | +| `ErrorText` / `CopyableId` | 錯誤與可複製 ID | + +`Button` 必須渲染 `{children}`;文案用**中文動詞**(例:「建立背景任務」「重新載入任務列表」),不要留空白小框。 + +### RWD(手機) + +- `< lg`:隱藏左側欄;**底部固定導覽**最多 **4 格**(總覽 / 任務 / 排程 / **更多**),不要把漢堡或 ⋯ 選單放在左上角。 +- 「更多」以底部 sheet 展開:AI、模板、設定、會員、權限、主題切換、登出。 +- 主內容加 `layout-main` 底部 padding,避開 tab bar + `safe-area-inset-bottom`。 +- 寬表格包 `overflow-x-auto` + `min-w-*`,避免小螢幕擠爆版面。 + +### 版面與導覽 + +- 已登入(桌面):`Layout` = `hx-scene` 背景 + `SceneDecor` + `ac-app-header`(品牌 + 角色 chip + `ThemeToggle`)+ 左 `ac-pocket-device` + 右 `auth-ticket` 主內容 `Outlet`。 +- 已登入(手機):同上頂欄;導覽走 `MobileBottomNav`(總覽/任務/排程/更多)。 +- 側欄 App 來源:`src/lib/acAssets.ts` 的 `navApps`;圖示 key 對應 `AcIcon`。 +- Active 導覽:`ac-app-tile--active`(brand-soft 底 + brand 字色);hover:`bg-brand-soft text-brand`。 +- 未登入:`AuthShell` 置中 `auth-ticket` + 右上 `ThemeToggle`;`auth-welcome` 內品牌,表單緊接說明文字。 +- 語氣:年輕、直接、短句;可帶「島民」「巡樓」等原創文案,避免企業八股與任天堂用語。 + +### API 與狀態 + +- JSON 一律走 `api/client.ts`(`code/message/data` envelope);需登入加 `{ auth: true }`。 +- AI 路由用 `X-Member-Authorization`;provider token 用 `Authorization`(見後端 Auth matrix)。 +- Job 狀態中文與 badge 色:`src/lib/jobStatus.ts`(`jobStatusLabel` / `jobStatusBadgeClass`),列表有進行中任務時可每 3 秒 refresh。 +- 不要在前端 parse JWT;`uid` / `tenant_id` 從 `AuthContext` 讀。 + +### 新增頁面流程 + +1. 在 `App.tsx` 掛路由(需登入的放在 `Layout` 底下,自動享有 `hx-scene` + 頂欄 + 主內容 `auth-ticket`)。 +2. 頁面內用 `PageTitle`(含 `.ac-title-bar` 小標)+ `Card` / `ac-bulletin` + `ui.tsx` 元件;色票只引用 semantic token。 +3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。 +4. 新導覽項:改 `acAssets.ts` 的 `navApps`,並在 `AcIcon` 補 SVG path。 +5. 完成後執行 `make web-build`。 + +### 島民頁面互動(可推廣 runtime) + +掛在 `Layout` 底下的新頁面**自動**支援島民操作,不需每頁手寫 executor。 + +模組入口:`web/src/lib/islander/index.ts` + +| 層 | 職責 | +|----|------| +| `pageSnapshot` | 掃描 `.ac-app-shell` 內可互動元素,產生 `hx-*` ref | +| `islanderActions` | 解析/剝除 `islander-actions` JSON 區塊 | +| `actionExecutor` | 執行 navigate/click/fill/select/scroll 等;可 `registerIslanderActionHandler` 擴充 | +| `islanderAgent` | 串流回覆 → 執行 action → 回傳結果 → 自動 follow-up | +| `buildIslanderContext` | 組裝送給後端的頁面快照 | + +**零設定(預設)**:路由掛在 `Layout` 即可;島民讀 DOM + `PageTitle` / `h1` 辨識頁面。預設**不**主動介紹這一頁;僅在使用者明確問頁面/操作時才附【可互動元素】(`userWantsPageContext`)。 + +**可選增強**(擇一): + +1. `useIslanderPage({ title, purpose, hints, suggestions })` — 頁面內動態註冊說明 +2. `registerIslanderPage(/^\/foo/, { title, ... })` — 在 `siteGuide.ts` 或模組 init 靜態註冊 +3. HTML 慣例:`data-islander-label`(元素名稱)、`data-islander-kind`(類型)、`data-islander-ignore`(排除)、`data-islander-page-title`(頁名) + +Action 協定(AI 回覆末尾): + +```islander-actions +[{ "type": "navigate", "path": "/settings" }, { "type": "click", "ref": "hx-3" }] +``` + +### 前端禁忌 + +- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。 +- 不要為單頁新增第三套配色、木質頂欄、或漸層彩虹按鈕。 +- 不要在登入/註冊頁加回獨立大牌 `ac-title-bar` 或咖啡色 header ribbon。 +- 不要讓 SSE / AI 直接吃 provider 原始 chunk(後端已 normalize)。 +- 不要用 `offset/limit` 呼叫列表 API;用 `page` / `pageSize`。 + +## 驗證 + +完成變更後至少執行: + +```bash +cd haixun-backend +go mod tidy +make fmt +go test ./... +``` + +有動到前端時另執行: + +```bash +make web-build +``` diff --git a/Makefile b/old/Makefile similarity index 100% rename from Makefile rename to old/Makefile diff --git a/old/README.md b/old/README.md new file mode 100644 index 0000000..56d4c4c --- /dev/null +++ b/old/README.md @@ -0,0 +1,30 @@ +# 封存區(Legacy Archive) + +此目錄為 **2026-07-09** 起封存的舊巡樓 monorepo 內容,**不再作為開發主線**。 + +新產品從 repo 根目錄 **從零重寫**,產物與決策見: + +- `docs/product/haixun-console/`(requirements / spec / plan / tasks) +- 新前端目標:`apps/web`(建立中) +- 新後端目標:`services/` 或 `backend/`(建立中,以 product 文件為準) + +## 內容大致對應 + +| 路徑 | 原角色 | +|------|--------| +| `old/backend/` | 舊 Go gateway、workers、web Console、Node workers | +| `old/deploy/`、`old/infra/`、`old/scripts/` | 部署與本機腳本 | +| `old/extension/` | Chrome 擴充 | +| `old/docs/` | 舊架構/計劃文件(非 product 流水線) | +| `old/AGENTS.md` | 舊 agent 規範(可當參考,根目錄有新版精簡規範) | + +## 使用規則 + +1. **只讀參考**:搬模式、契約、演算法時可查,不要在 `old/` 上繼續開功能分支當主線。 +2. **不要**把 `old/` 加回 CI 主 build(除非明確做遷移工具)。 +3. 需要對照舊 API 時優先看 `old/backend/generate/api/` 與 `old/backend/README.md`。 +4. 確認新系統穩定後,可另開 PR 決定是否從 git 歷史移除大目錄(目前僅搬移保留歷史)。 + +## 還原(不建議) + +若誤搬,可用 git 還原該次 commit / checkout 路徑。正常開發請只改 repo 根下的新結構。 diff --git a/ai_threads_auto_account_ops.md b/old/ai_threads_auto_account_ops.md similarity index 100% rename from ai_threads_auto_account_ops.md rename to old/ai_threads_auto_account_ops.md diff --git a/backend/Dockerfile b/old/backend/Dockerfile similarity index 100% rename from backend/Dockerfile rename to old/backend/Dockerfile diff --git a/backend/Makefile b/old/backend/Makefile similarity index 100% rename from backend/Makefile rename to old/backend/Makefile diff --git a/backend/README.md b/old/backend/README.md similarity index 100% rename from backend/README.md rename to old/backend/README.md diff --git a/backend/cmd/tool/main.go b/old/backend/cmd/tool/main.go similarity index 100% rename from backend/cmd/tool/main.go rename to old/backend/cmd/tool/main.go diff --git a/backend/cmd/worker/main.go b/old/backend/cmd/worker/main.go similarity index 100% rename from backend/cmd/worker/main.go rename to old/backend/cmd/worker/main.go diff --git a/backend/dev-console/README.md b/old/backend/dev-console/README.md similarity index 100% rename from backend/dev-console/README.md rename to old/backend/dev-console/README.md diff --git a/backend/dev-console/index.html b/old/backend/dev-console/index.html similarity index 100% rename from backend/dev-console/index.html rename to old/backend/dev-console/index.html diff --git a/backend/dev-console/package-lock.json b/old/backend/dev-console/package-lock.json similarity index 100% rename from backend/dev-console/package-lock.json rename to old/backend/dev-console/package-lock.json diff --git a/backend/dev-console/package.json b/old/backend/dev-console/package.json similarity index 100% rename from backend/dev-console/package.json rename to old/backend/dev-console/package.json diff --git a/backend/dev-console/vite.config.ts b/old/backend/dev-console/vite.config.ts similarity index 100% rename from backend/dev-console/vite.config.ts rename to old/backend/dev-console/vite.config.ts diff --git a/backend/docs/job-system-plan.md b/old/backend/docs/job-system-plan.md similarity index 100% rename from backend/docs/job-system-plan.md rename to old/backend/docs/job-system-plan.md diff --git a/backend/docs/threads-automation-gap-plan.md b/old/backend/docs/threads-automation-gap-plan.md similarity index 100% rename from backend/docs/threads-automation-gap-plan.md rename to old/backend/docs/threads-automation-gap-plan.md diff --git a/backend/etc/gateway.prod.yaml b/old/backend/etc/gateway.prod.yaml similarity index 100% rename from backend/etc/gateway.prod.yaml rename to old/backend/etc/gateway.prod.yaml diff --git a/backend/etc/gateway.worker.prod.yaml b/old/backend/etc/gateway.worker.prod.yaml similarity index 100% rename from backend/etc/gateway.worker.prod.yaml rename to old/backend/etc/gateway.worker.prod.yaml diff --git a/backend/etc/gateway.worker.yaml b/old/backend/etc/gateway.worker.yaml similarity index 100% rename from backend/etc/gateway.worker.yaml rename to old/backend/etc/gateway.worker.yaml diff --git a/backend/etc/gateway.yaml b/old/backend/etc/gateway.yaml similarity index 100% rename from backend/etc/gateway.yaml rename to old/backend/etc/gateway.yaml diff --git a/backend/gateway.go b/old/backend/gateway.go similarity index 100% rename from backend/gateway.go rename to old/backend/gateway.go diff --git a/backend/generate/api/ai.api b/old/backend/generate/api/ai.api similarity index 100% rename from backend/generate/api/ai.api rename to old/backend/generate/api/ai.api diff --git a/backend/generate/api/auth.api b/old/backend/generate/api/auth.api similarity index 78% rename from backend/generate/api/auth.api rename to old/backend/generate/api/auth.api index 21c928a..2382124 100644 --- a/backend/generate/api/auth.api +++ b/old/backend/generate/api/auth.api @@ -30,6 +30,18 @@ type ( LogoutData { OK bool `json:"ok"` } + + AuthVerifyEmailReq { + Code string `json:"code" validate:"required,len=6"` + } + + AuthVerifyEmailData { + EmailVerified bool `json:"email_verified"` + } + + AuthResendVerificationData { + Sent bool `json:"sent"` + } ) @server( @@ -59,4 +71,10 @@ service gateway { @handler logout post /logout returns (LogoutData) + + @handler verifyEmail + post /verify-email (AuthVerifyEmailReq) returns (AuthVerifyEmailData) + + @handler resendVerificationEmail + post /resend-verification-email returns (AuthResendVerificationData) } diff --git a/backend/generate/api/brand.api b/old/backend/generate/api/brand.api similarity index 100% rename from backend/generate/api/brand.api rename to old/backend/generate/api/brand.api diff --git a/backend/generate/api/common.api b/old/backend/generate/api/common.api similarity index 100% rename from backend/generate/api/common.api rename to old/backend/generate/api/common.api diff --git a/old/backend/generate/api/copy_mission.api b/old/backend/generate/api/copy_mission.api new file mode 100644 index 0000000..709fbe7 --- /dev/null +++ b/old/backend/generate/api/copy_mission.api @@ -0,0 +1,60 @@ +syntax = "v1" + +// Flow A full copy_mission pipeline removed (stubs / orphan workers). +// Only topic inspiration remains until Post Project replaces MissionsPage. +// See docs/post-project-plan.md and docs/cleanup-log.md. + +type ( + CopyMissionInspirationReq { + Keyword string `json:"keyword,optional"` + ContentDirection string `json:"content_direction,optional"` + UseWebSearch bool `json:"use_web_search,optional"` + AvoidTopics []string `json:"avoid_topics,optional"` + } + + CopyMissionInspirationHandlerReq { + PersonaID string `path:"personaId" validate:"required"` + CopyMissionInspirationReq + } + + CopyMissionInspirationSourceData { + Query string `json:"query,omitempty"` + Title string `json:"title,omitempty"` + Snippet string `json:"snippet,omitempty"` + URL string `json:"url,omitempty"` + } + + CopyMissionInspirationData { + TopicCandidateID string `json:"topic_candidate_id,omitempty"` + ContentPlanID string `json:"content_plan_id,omitempty"` + 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"` + Angles []string `json:"angles,omitempty"` + Mission string `json:"mission,omitempty"` + TargetAudience string `json:"target_audience,omitempty"` + OpeningType string `json:"opening_type,omitempty"` + BodyType string `json:"body_type,omitempty"` + Emotion string `json:"emotion,omitempty"` + CtaType string `json:"cta_type,omitempty"` + RiskLevel string `json:"risk_level,omitempty"` + Avoid []string `json:"avoid,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: "Topic inspiration only (legacy Flow A pipeline removed). Requires Bearer JWT." +) +service gateway { + @handler inspireCopyMission + post /:personaId/copy-mission-inspiration (CopyMissionInspirationHandlerReq) returns (CopyMissionInspirationData) +} diff --git a/backend/generate/api/gateway.api b/old/backend/generate/api/gateway.api similarity index 100% rename from backend/generate/api/gateway.api rename to old/backend/generate/api/gateway.api diff --git a/backend/generate/api/job.api b/old/backend/generate/api/job.api similarity index 100% rename from backend/generate/api/job.api rename to old/backend/generate/api/job.api diff --git a/backend/generate/api/member.api b/old/backend/generate/api/member.api similarity index 100% rename from backend/generate/api/member.api rename to old/backend/generate/api/member.api diff --git a/backend/generate/api/normal.api b/old/backend/generate/api/normal.api similarity index 100% rename from backend/generate/api/normal.api rename to old/backend/generate/api/normal.api diff --git a/backend/generate/api/permission.api b/old/backend/generate/api/permission.api similarity index 100% rename from backend/generate/api/permission.api rename to old/backend/generate/api/permission.api diff --git a/backend/generate/api/persona.api b/old/backend/generate/api/persona.api similarity index 100% rename from backend/generate/api/persona.api rename to old/backend/generate/api/persona.api diff --git a/backend/generate/api/placement_topic.api b/old/backend/generate/api/placement_topic.api similarity index 100% rename from backend/generate/api/placement_topic.api rename to old/backend/generate/api/placement_topic.api diff --git a/backend/generate/api/setting.api b/old/backend/generate/api/setting.api similarity index 100% rename from backend/generate/api/setting.api rename to old/backend/generate/api/setting.api diff --git a/backend/generate/api/threads_account.api b/old/backend/generate/api/threads_account.api similarity index 95% rename from backend/generate/api/threads_account.api rename to old/backend/generate/api/threads_account.api index da1d081..dec95a2 100644 --- a/backend/generate/api/threads_account.api +++ b/old/backend/generate/api/threads_account.api @@ -206,6 +206,8 @@ type ( ReplyID string `json:"reply_id,optional"` ReplyToID string `json:"reply_to_id,optional"` Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` Permalink string `json:"permalink,optional"` HintText string `json:"hint_text,optional"` SkipPrime bool `json:"skip_prime,optional"` @@ -302,9 +304,11 @@ type ( } CreatePublishQueueReq { - Text string `json:"text" validate:"required"` - TopicTag string `json:"topic_tag,optional"` - ScheduledAt int64 `json:"scheduled_at,optional"` + Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` + TopicTag string `json:"topic_tag,optional"` + ScheduledAt int64 `json:"scheduled_at,optional"` } CreatePublishQueueHandlerReq { @@ -313,9 +317,11 @@ type ( } PatchPublishQueueReq { - Text *string `json:"text,optional"` - TopicTag *string `json:"topic_tag,optional"` - ScheduledAt *int64 `json:"scheduled_at,optional"` + Text *string `json:"text,optional"` + ImageKey *string `json:"image_key,optional"` + ImageKeys *[]string `json:"image_keys,optional"` + TopicTag *string `json:"topic_tag,optional"` + ScheduledAt *int64 `json:"scheduled_at,optional"` } PatchPublishQueueHandlerReq { @@ -330,7 +336,9 @@ type ( CopyMissionID string `json:"copy_mission_id,omitempty"` CopyDraftID string `json:"copy_draft_id,omitempty"` Text string `json:"text"` - TopicTag string `json:"topic_tag,omitempty"` + ImageKey string `json:"image_key,omitempty"` + ImageKeys []string `json:"image_keys,omitempty"` + TopicTag string `json:"topic_tag,omitempty"` ScheduledAt int64 `json:"scheduled_at"` Status string `json:"status"` MediaID string `json:"media_id,optional"` @@ -439,20 +447,6 @@ type ( UpsertPublishInventoryPolicyReq } - StartPublishInventoryRefillJobReq { - Count int `json:"count,optional"` - } - - StartPublishInventoryRefillJobHandlerReq { - ThreadsAccountPath - StartPublishInventoryRefillJobReq - } - - StartPublishInventoryRefillJobData { - JobID string `json:"job_id"` - Status string `json:"status"` - Message string `json:"message"` - } PublishSlotInsightData { Weekday int `json:"weekday"` @@ -631,7 +625,14 @@ type ( } PublishMentionReplyReq { - Text string `json:"text"` + Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` + } + + UploadPublishAttachmentData { + AttachmentKey string `json:"attachment_key"` + PublicURL string `json:"public_url"` } PublishMentionReplyPath { @@ -649,6 +650,17 @@ type ( Permalink string `json:"permalink,omitempty"` } + PublishThreadsReplyHandlerReq { + ID string `path:"id" validate:"required"` + ReplyToID string `form:"reply_to_id" validate:"required"` + Text string `form:"text,optional"` + } + + PublishThreadsReplyData { + MediaID string `json:"media_id,omitempty"` + Permalink string `json:"permalink,omitempty"` + } + GenerateOwnPostFormulaReq { MediaID string `json:"media_id"` PersonaID string `json:"persona_id,optional"` @@ -916,6 +928,9 @@ service gateway { @handler listThreadsPostPerformance get /:id/post-performance (ListThreadsPostPerformanceHandlerReq) returns (ThreadsPostPerformanceData) + @handler uploadPublishAttachment + post /:id/publish-attachments (ThreadsAccountPath) returns (UploadPublishAttachmentData) + @handler createPublishQueueItem post /:id/publish-queue (CreatePublishQueueHandlerReq) returns (PublishQueueItemData) @@ -943,9 +958,6 @@ service gateway { @handler upsertPublishInventoryPolicy put /:id/publish-inventory-policy (UpsertPublishInventoryPolicyHandlerReq) returns (PublishInventoryPolicyData) - @handler startPublishInventoryRefillJob - post /:id/publish-inventory/refill-jobs (StartPublishInventoryRefillJobHandlerReq) returns (StartPublishInventoryRefillJobData) - @handler getPublishSlotInsights get /:id/publish-slot-insights (ThreadsAccountPath) returns (PublishSlotInsightsData) @@ -985,6 +997,9 @@ service gateway { @handler publishMentionReply post /:id/mention-inbox/:media_id/reply (PublishMentionReplyHandlerReq) returns (PublishMentionReplyData) + @handler publishThreadsReply + post /:id/publish-reply (PublishThreadsReplyHandlerReq) returns (PublishThreadsReplyData) + @handler generateOwnPostFormula post /:id/own-post-formula (GenerateOwnPostFormulaHandlerReq) returns (GenerateOwnPostFormulaData) diff --git a/backend/generate/api/worker_internal.api b/old/backend/generate/api/worker_internal.api similarity index 100% rename from backend/generate/api/worker_internal.api rename to old/backend/generate/api/worker_internal.api diff --git a/backend/generate/goctl/api/handler.tpl b/old/backend/generate/goctl/api/handler.tpl similarity index 100% rename from backend/generate/goctl/api/handler.tpl rename to old/backend/generate/goctl/api/handler.tpl diff --git a/backend/go.mod b/old/backend/go.mod similarity index 100% rename from backend/go.mod rename to old/backend/go.mod diff --git a/backend/go.sum b/old/backend/go.sum similarity index 100% rename from backend/go.sum rename to old/backend/go.sum diff --git a/backend/internal/bootstrap/admin.go b/old/backend/internal/bootstrap/admin.go similarity index 100% rename from backend/internal/bootstrap/admin.go rename to old/backend/internal/bootstrap/admin.go diff --git a/backend/internal/bootstrap/admin_test.go b/old/backend/internal/bootstrap/admin_test.go similarity index 92% rename from backend/internal/bootstrap/admin_test.go rename to old/backend/internal/bootstrap/admin_test.go index 64da60e..f29d19c 100644 --- a/backend/internal/bootstrap/admin_test.go +++ b/old/backend/internal/bootstrap/admin_test.go @@ -112,6 +112,22 @@ func (m *memoryMemberRepo) SetEmailVerificationCode(_ context.Context, tenantID, return app.For(code.Member).ResNotFound("member not found") } +func (m *memoryMemberRepo) ConfirmEmailVerification(_ context.Context, tenantID, uid, verifyCode string) (*entity.Member, error) { + for _, item := range m.byEmail { + if item.TenantID == tenantID && item.UID == uid { + if item.EmailVerifyCode != verifyCode { + return nil, app.For(code.Auth).AuthUnauthorized("invalid verification code") + } + item.EmailVerified = true + item.EmailVerifyCode = "" + item.EmailVerifyExpiresAt = 0 + cp := *item + return &cp, nil + } + } + return nil, app.For(code.Member).ResNotFound("member not found") +} + func TestEnsureAdminMemberCreatesAdmin(t *testing.T) { repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{}} member, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{ diff --git a/backend/internal/bootstrap/envfile.go b/old/backend/internal/bootstrap/envfile.go similarity index 100% rename from backend/internal/bootstrap/envfile.go rename to old/backend/internal/bootstrap/envfile.go diff --git a/backend/internal/bootstrap/envfile_test.go b/old/backend/internal/bootstrap/envfile_test.go similarity index 100% rename from backend/internal/bootstrap/envfile_test.go rename to old/backend/internal/bootstrap/envfile_test.go diff --git a/backend/internal/bootstrap/init.go b/old/backend/internal/bootstrap/init.go similarity index 98% rename from backend/internal/bootstrap/init.go rename to old/backend/internal/bootstrap/init.go index e9c9a8e..1911f51 100644 --- a/backend/internal/bootstrap/init.go +++ b/old/backend/internal/bootstrap/init.go @@ -13,7 +13,6 @@ import ( contentopsrepo "haixun-backend/internal/model/content_ops/repository" copydraftrepo "haixun-backend/internal/model/copy_draft/repository" copymissionrepo "haixun-backend/internal/model/copy_mission/repository" - crmcontactrepo "haixun-backend/internal/model/crm_contact/repository" jobrepo "haixun-backend/internal/model/job/repository" jobusecase "haixun-backend/internal/model/job/usecase" kgrepo "haixun-backend/internal/model/knowledge_graph/repository" @@ -119,7 +118,6 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport {"content_formulas", contentformularepo.NewMongoRepository(db).EnsureIndexes}, {"content_ops", contentopsrepo.NewMongoRepository(db).EnsureIndexes}, {"mention_inbox", mentioninboxrepo.NewMongoRepository(db).EnsureIndexes}, - {"crm_contacts", crmcontactrepo.NewMongoRepository(db).EnsureIndexes}, } for _, repo := range repos { if err := repo.fn(ctx); err != nil { diff --git a/backend/internal/config/config.go b/old/backend/internal/config/config.go similarity index 100% rename from backend/internal/config/config.go rename to old/backend/internal/config/config.go diff --git a/backend/internal/handler/ai/chat_handler.go b/old/backend/internal/handler/ai/chat_handler.go similarity index 100% rename from backend/internal/handler/ai/chat_handler.go rename to old/backend/internal/handler/ai/chat_handler.go diff --git a/backend/internal/handler/ai/chat_stream_handler.go b/old/backend/internal/handler/ai/chat_stream_handler.go similarity index 100% rename from backend/internal/handler/ai/chat_stream_handler.go rename to old/backend/internal/handler/ai/chat_stream_handler.go diff --git a/backend/internal/handler/ai/islander_chat_stream_handler.go b/old/backend/internal/handler/ai/islander_chat_stream_handler.go similarity index 100% rename from backend/internal/handler/ai/islander_chat_stream_handler.go rename to old/backend/internal/handler/ai/islander_chat_stream_handler.go diff --git a/backend/internal/handler/ai/list_ai_provider_models_handler.go b/old/backend/internal/handler/ai/list_ai_provider_models_handler.go similarity index 100% rename from backend/internal/handler/ai/list_ai_provider_models_handler.go rename to old/backend/internal/handler/ai/list_ai_provider_models_handler.go diff --git a/backend/internal/handler/ai/list_ai_providers_handler.go b/old/backend/internal/handler/ai/list_ai_providers_handler.go similarity index 100% rename from backend/internal/handler/ai/list_ai_providers_handler.go rename to old/backend/internal/handler/ai/list_ai_providers_handler.go diff --git a/backend/internal/handler/auth/login_handler.go b/old/backend/internal/handler/auth/login_handler.go similarity index 100% rename from backend/internal/handler/auth/login_handler.go rename to old/backend/internal/handler/auth/login_handler.go diff --git a/backend/internal/handler/auth/logout_handler.go b/old/backend/internal/handler/auth/logout_handler.go similarity index 100% rename from backend/internal/handler/auth/logout_handler.go rename to old/backend/internal/handler/auth/logout_handler.go diff --git a/backend/internal/handler/auth/refresh_handler.go b/old/backend/internal/handler/auth/refresh_handler.go similarity index 100% rename from backend/internal/handler/auth/refresh_handler.go rename to old/backend/internal/handler/auth/refresh_handler.go diff --git a/backend/internal/handler/auth/register_handler.go b/old/backend/internal/handler/auth/register_handler.go similarity index 100% rename from backend/internal/handler/auth/register_handler.go rename to old/backend/internal/handler/auth/register_handler.go diff --git a/old/backend/internal/handler/auth/resend_verification_email_handler.go b/old/backend/internal/handler/auth/resend_verification_email_handler.go new file mode 100644 index 0000000..0f101cd --- /dev/null +++ b/old/backend/internal/handler/auth/resend_verification_email_handler.go @@ -0,0 +1,20 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package auth + +import ( + "net/http" + + "haixun-backend/internal/logic/auth" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" +) + +func ResendVerificationEmailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := auth.NewResendVerificationEmailLogic(r.Context(), svcCtx) + data, err := l.ResendVerificationEmail() + response.Write(r.Context(), w, data, err) + } +} diff --git a/backend/internal/handler/copy_mission/get_copy_mission_handler.go b/old/backend/internal/handler/auth/verify_email_handler.go similarity index 68% rename from backend/internal/handler/copy_mission/get_copy_mission_handler.go rename to old/backend/internal/handler/auth/verify_email_handler.go index 7cbb8d2..2f833ec 100644 --- a/backend/internal/handler/copy_mission/get_copy_mission_handler.go +++ b/old/backend/internal/handler/auth/verify_email_handler.go @@ -1,21 +1,21 @@ // Code scaffolded by goctl. Safe to edit. // goctl 1.10.1 -package copy_mission +package auth import ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" - "haixun-backend/internal/logic/copy_mission" + "haixun-backend/internal/logic/auth" "haixun-backend/internal/response" "haixun-backend/internal/svc" "haixun-backend/internal/types" ) -func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +func VerifyEmailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.CopyMissionPath + var req types.AuthVerifyEmailReq if err := httpx.Parse(r, &req); err != nil { response.Write(r.Context(), w, nil, response.WrapRequestError(err)) return @@ -25,8 +25,8 @@ func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return } - l := copy_mission.NewGetCopyMissionLogic(r.Context(), svcCtx) - data, err := l.GetCopyMission(&req) + l := auth.NewVerifyEmailLogic(r.Context(), svcCtx) + data, err := l.VerifyEmail(&req) response.Write(r.Context(), w, data, err) } } diff --git a/backend/internal/handler/brand/create_brand_handler.go b/old/backend/internal/handler/brand/create_brand_handler.go similarity index 100% rename from backend/internal/handler/brand/create_brand_handler.go rename to old/backend/internal/handler/brand/create_brand_handler.go diff --git a/backend/internal/handler/brand/create_brand_product_handler.go b/old/backend/internal/handler/brand/create_brand_product_handler.go similarity index 100% rename from backend/internal/handler/brand/create_brand_product_handler.go rename to old/backend/internal/handler/brand/create_brand_product_handler.go diff --git a/backend/internal/handler/brand/delete_brand_handler.go b/old/backend/internal/handler/brand/delete_brand_handler.go similarity index 100% rename from backend/internal/handler/brand/delete_brand_handler.go rename to old/backend/internal/handler/brand/delete_brand_handler.go diff --git a/backend/internal/handler/brand/delete_brand_product_handler.go b/old/backend/internal/handler/brand/delete_brand_product_handler.go similarity index 100% rename from backend/internal/handler/brand/delete_brand_product_handler.go rename to old/backend/internal/handler/brand/delete_brand_product_handler.go diff --git a/backend/internal/handler/brand/expand_knowledge_graph_handler.go b/old/backend/internal/handler/brand/expand_knowledge_graph_handler.go similarity index 100% rename from backend/internal/handler/brand/expand_knowledge_graph_handler.go rename to old/backend/internal/handler/brand/expand_knowledge_graph_handler.go diff --git a/backend/internal/handler/brand/generate_brand_content_matrix_handler.go b/old/backend/internal/handler/brand/generate_brand_content_matrix_handler.go similarity index 100% rename from backend/internal/handler/brand/generate_brand_content_matrix_handler.go rename to old/backend/internal/handler/brand/generate_brand_content_matrix_handler.go diff --git a/backend/internal/handler/brand/generate_outreach_drafts_handler.go b/old/backend/internal/handler/brand/generate_outreach_drafts_handler.go similarity index 100% rename from backend/internal/handler/brand/generate_outreach_drafts_handler.go rename to old/backend/internal/handler/brand/generate_outreach_drafts_handler.go diff --git a/backend/internal/handler/brand/get_brand_content_matrix_handler.go b/old/backend/internal/handler/brand/get_brand_content_matrix_handler.go similarity index 100% rename from backend/internal/handler/brand/get_brand_content_matrix_handler.go rename to old/backend/internal/handler/brand/get_brand_content_matrix_handler.go diff --git a/backend/internal/handler/brand/get_brand_handler.go b/old/backend/internal/handler/brand/get_brand_handler.go similarity index 100% rename from backend/internal/handler/brand/get_brand_handler.go rename to old/backend/internal/handler/brand/get_brand_handler.go diff --git a/backend/internal/handler/brand/get_brand_scan_schedule_handler.go b/old/backend/internal/handler/brand/get_brand_scan_schedule_handler.go similarity index 100% rename from backend/internal/handler/brand/get_brand_scan_schedule_handler.go rename to old/backend/internal/handler/brand/get_brand_scan_schedule_handler.go diff --git a/backend/internal/handler/brand/get_knowledge_graph_handler.go b/old/backend/internal/handler/brand/get_knowledge_graph_handler.go similarity index 100% rename from backend/internal/handler/brand/get_knowledge_graph_handler.go rename to old/backend/internal/handler/brand/get_knowledge_graph_handler.go diff --git a/backend/internal/handler/brand/list_brand_products_handler.go b/old/backend/internal/handler/brand/list_brand_products_handler.go similarity index 100% rename from backend/internal/handler/brand/list_brand_products_handler.go rename to old/backend/internal/handler/brand/list_brand_products_handler.go diff --git a/backend/internal/handler/brand/list_brand_scan_posts_handler.go b/old/backend/internal/handler/brand/list_brand_scan_posts_handler.go similarity index 100% rename from backend/internal/handler/brand/list_brand_scan_posts_handler.go rename to old/backend/internal/handler/brand/list_brand_scan_posts_handler.go diff --git a/backend/internal/handler/brand/list_brands_handler.go b/old/backend/internal/handler/brand/list_brands_handler.go similarity index 100% rename from backend/internal/handler/brand/list_brands_handler.go rename to old/backend/internal/handler/brand/list_brands_handler.go diff --git a/backend/internal/handler/brand/patch_knowledge_graph_nodes_handler.go b/old/backend/internal/handler/brand/patch_knowledge_graph_nodes_handler.go similarity index 100% rename from backend/internal/handler/brand/patch_knowledge_graph_nodes_handler.go rename to old/backend/internal/handler/brand/patch_knowledge_graph_nodes_handler.go diff --git a/backend/internal/handler/brand/patch_scan_post_outreach_handler.go b/old/backend/internal/handler/brand/patch_scan_post_outreach_handler.go similarity index 100% rename from backend/internal/handler/brand/patch_scan_post_outreach_handler.go rename to old/backend/internal/handler/brand/patch_scan_post_outreach_handler.go diff --git a/backend/internal/handler/brand/publish_outreach_draft_handler.go b/old/backend/internal/handler/brand/publish_outreach_draft_handler.go similarity index 100% rename from backend/internal/handler/brand/publish_outreach_draft_handler.go rename to old/backend/internal/handler/brand/publish_outreach_draft_handler.go diff --git a/backend/internal/handler/brand/start_brand_scan_job_handler.go b/old/backend/internal/handler/brand/start_brand_scan_job_handler.go similarity index 100% rename from backend/internal/handler/brand/start_brand_scan_job_handler.go rename to old/backend/internal/handler/brand/start_brand_scan_job_handler.go diff --git a/backend/internal/handler/brand/update_brand_handler.go b/old/backend/internal/handler/brand/update_brand_handler.go similarity index 100% rename from backend/internal/handler/brand/update_brand_handler.go rename to old/backend/internal/handler/brand/update_brand_handler.go diff --git a/backend/internal/handler/brand/update_brand_product_handler.go b/old/backend/internal/handler/brand/update_brand_product_handler.go similarity index 100% rename from backend/internal/handler/brand/update_brand_product_handler.go rename to old/backend/internal/handler/brand/update_brand_product_handler.go diff --git a/backend/internal/handler/brand/update_outreach_draft_handler.go b/old/backend/internal/handler/brand/update_outreach_draft_handler.go similarity index 100% rename from backend/internal/handler/brand/update_outreach_draft_handler.go rename to old/backend/internal/handler/brand/update_outreach_draft_handler.go diff --git a/backend/internal/handler/brand/upsert_brand_scan_schedule_handler.go b/old/backend/internal/handler/brand/upsert_brand_scan_schedule_handler.go similarity index 100% rename from backend/internal/handler/brand/upsert_brand_scan_schedule_handler.go rename to old/backend/internal/handler/brand/upsert_brand_scan_schedule_handler.go diff --git a/backend/internal/handler/copy_mission/inspire_copy_mission_handler.go b/old/backend/internal/handler/copy_mission/inspire_copy_mission_handler.go similarity index 100% rename from backend/internal/handler/copy_mission/inspire_copy_mission_handler.go rename to old/backend/internal/handler/copy_mission/inspire_copy_mission_handler.go diff --git a/backend/internal/handler/job/ack_worker_job_cancel_handler.go b/old/backend/internal/handler/job/ack_worker_job_cancel_handler.go similarity index 100% rename from backend/internal/handler/job/ack_worker_job_cancel_handler.go rename to old/backend/internal/handler/job/ack_worker_job_cancel_handler.go diff --git a/backend/internal/handler/job/analyze_style8_d_from_worker_handler.go b/old/backend/internal/handler/job/analyze_style8_d_from_worker_handler.go similarity index 100% rename from backend/internal/handler/job/analyze_style8_d_from_worker_handler.go rename to old/backend/internal/handler/job/analyze_style8_d_from_worker_handler.go diff --git a/backend/internal/handler/job/cancel_job_handler.go b/old/backend/internal/handler/job/cancel_job_handler.go similarity index 100% rename from backend/internal/handler/job/cancel_job_handler.go rename to old/backend/internal/handler/job/cancel_job_handler.go diff --git a/backend/internal/handler/job/check_worker_job_cancel_handler.go b/old/backend/internal/handler/job/check_worker_job_cancel_handler.go similarity index 100% rename from backend/internal/handler/job/check_worker_job_cancel_handler.go rename to old/backend/internal/handler/job/check_worker_job_cancel_handler.go diff --git a/backend/internal/handler/job/claim_worker_job_handler.go b/old/backend/internal/handler/job/claim_worker_job_handler.go similarity index 100% rename from backend/internal/handler/job/claim_worker_job_handler.go rename to old/backend/internal/handler/job/claim_worker_job_handler.go diff --git a/backend/internal/handler/job/complete_worker_job_handler.go b/old/backend/internal/handler/job/complete_worker_job_handler.go similarity index 100% rename from backend/internal/handler/job/complete_worker_job_handler.go rename to old/backend/internal/handler/job/complete_worker_job_handler.go diff --git a/backend/internal/handler/job/create_job_handler.go b/old/backend/internal/handler/job/create_job_handler.go similarity index 100% rename from backend/internal/handler/job/create_job_handler.go rename to old/backend/internal/handler/job/create_job_handler.go diff --git a/backend/internal/handler/job/create_job_schedule_handler.go b/old/backend/internal/handler/job/create_job_schedule_handler.go similarity index 100% rename from backend/internal/handler/job/create_job_schedule_handler.go rename to old/backend/internal/handler/job/create_job_schedule_handler.go diff --git a/backend/internal/handler/job/delete_job_schedule_handler.go b/old/backend/internal/handler/job/delete_job_schedule_handler.go similarity index 100% rename from backend/internal/handler/job/delete_job_schedule_handler.go rename to old/backend/internal/handler/job/delete_job_schedule_handler.go diff --git a/backend/internal/handler/job/disable_job_schedule_handler.go b/old/backend/internal/handler/job/disable_job_schedule_handler.go similarity index 100% rename from backend/internal/handler/job/disable_job_schedule_handler.go rename to old/backend/internal/handler/job/disable_job_schedule_handler.go diff --git a/backend/internal/handler/job/enable_job_schedule_handler.go b/old/backend/internal/handler/job/enable_job_schedule_handler.go similarity index 100% rename from backend/internal/handler/job/enable_job_schedule_handler.go rename to old/backend/internal/handler/job/enable_job_schedule_handler.go diff --git a/backend/internal/handler/job/fail_worker_job_handler.go b/old/backend/internal/handler/job/fail_worker_job_handler.go similarity index 100% rename from backend/internal/handler/job/fail_worker_job_handler.go rename to old/backend/internal/handler/job/fail_worker_job_handler.go diff --git a/backend/internal/handler/job/get_job_handler.go b/old/backend/internal/handler/job/get_job_handler.go similarity index 100% rename from backend/internal/handler/job/get_job_handler.go rename to old/backend/internal/handler/job/get_job_handler.go diff --git a/backend/internal/handler/job/get_job_template_handler.go b/old/backend/internal/handler/job/get_job_template_handler.go similarity index 100% rename from backend/internal/handler/job/get_job_template_handler.go rename to old/backend/internal/handler/job/get_job_template_handler.go diff --git a/backend/internal/handler/job/get_worker_threads_account_session_handler.go b/old/backend/internal/handler/job/get_worker_threads_account_session_handler.go similarity index 100% rename from backend/internal/handler/job/get_worker_threads_account_session_handler.go rename to old/backend/internal/handler/job/get_worker_threads_account_session_handler.go diff --git a/backend/internal/handler/job/list_job_events_handler.go b/old/backend/internal/handler/job/list_job_events_handler.go similarity index 100% rename from backend/internal/handler/job/list_job_events_handler.go rename to old/backend/internal/handler/job/list_job_events_handler.go diff --git a/backend/internal/handler/job/list_job_schedules_handler.go b/old/backend/internal/handler/job/list_job_schedules_handler.go similarity index 100% rename from backend/internal/handler/job/list_job_schedules_handler.go rename to old/backend/internal/handler/job/list_job_schedules_handler.go diff --git a/backend/internal/handler/job/list_job_templates_handler.go b/old/backend/internal/handler/job/list_job_templates_handler.go similarity index 100% rename from backend/internal/handler/job/list_job_templates_handler.go rename to old/backend/internal/handler/job/list_job_templates_handler.go diff --git a/backend/internal/handler/job/list_jobs_handler.go b/old/backend/internal/handler/job/list_jobs_handler.go similarity index 100% rename from backend/internal/handler/job/list_jobs_handler.go rename to old/backend/internal/handler/job/list_jobs_handler.go diff --git a/backend/internal/handler/job/refresh_worker_job_lock_handler.go b/old/backend/internal/handler/job/refresh_worker_job_lock_handler.go similarity index 100% rename from backend/internal/handler/job/refresh_worker_job_lock_handler.go rename to old/backend/internal/handler/job/refresh_worker_job_lock_handler.go diff --git a/backend/internal/handler/job/retry_job_handler.go b/old/backend/internal/handler/job/retry_job_handler.go similarity index 100% rename from backend/internal/handler/job/retry_job_handler.go rename to old/backend/internal/handler/job/retry_job_handler.go diff --git a/backend/internal/handler/job/store_persona_style_profile_from_worker_handler.go b/old/backend/internal/handler/job/store_persona_style_profile_from_worker_handler.go similarity index 100% rename from backend/internal/handler/job/store_persona_style_profile_from_worker_handler.go rename to old/backend/internal/handler/job/store_persona_style_profile_from_worker_handler.go diff --git a/backend/internal/handler/job/update_job_schedule_handler.go b/old/backend/internal/handler/job/update_job_schedule_handler.go similarity index 100% rename from backend/internal/handler/job/update_job_schedule_handler.go rename to old/backend/internal/handler/job/update_job_schedule_handler.go diff --git a/backend/internal/handler/job/update_worker_job_progress_handler.go b/old/backend/internal/handler/job/update_worker_job_progress_handler.go similarity index 100% rename from backend/internal/handler/job/update_worker_job_progress_handler.go rename to old/backend/internal/handler/job/update_worker_job_progress_handler.go diff --git a/backend/internal/handler/job/upsert_job_template_handler.go b/old/backend/internal/handler/job/upsert_job_template_handler.go similarity index 100% rename from backend/internal/handler/job/upsert_job_template_handler.go rename to old/backend/internal/handler/job/upsert_job_template_handler.go diff --git a/backend/internal/handler/member/get_member_ai_settings_handler.go b/old/backend/internal/handler/member/get_member_ai_settings_handler.go similarity index 100% rename from backend/internal/handler/member/get_member_ai_settings_handler.go rename to old/backend/internal/handler/member/get_member_ai_settings_handler.go diff --git a/backend/internal/handler/member/get_member_capabilities_handler.go b/old/backend/internal/handler/member/get_member_capabilities_handler.go similarity index 100% rename from backend/internal/handler/member/get_member_capabilities_handler.go rename to old/backend/internal/handler/member/get_member_capabilities_handler.go diff --git a/backend/internal/handler/member/get_member_me_handler.go b/old/backend/internal/handler/member/get_member_me_handler.go similarity index 100% rename from backend/internal/handler/member/get_member_me_handler.go rename to old/backend/internal/handler/member/get_member_me_handler.go diff --git a/backend/internal/handler/member/get_member_placement_settings_handler.go b/old/backend/internal/handler/member/get_member_placement_settings_handler.go similarity index 100% rename from backend/internal/handler/member/get_member_placement_settings_handler.go rename to old/backend/internal/handler/member/get_member_placement_settings_handler.go diff --git a/backend/internal/handler/member/list_member_ai_provider_models_handler.go b/old/backend/internal/handler/member/list_member_ai_provider_models_handler.go similarity index 100% rename from backend/internal/handler/member/list_member_ai_provider_models_handler.go rename to old/backend/internal/handler/member/list_member_ai_provider_models_handler.go diff --git a/backend/internal/handler/member/list_members_handler.go b/old/backend/internal/handler/member/list_members_handler.go similarity index 100% rename from backend/internal/handler/member/list_members_handler.go rename to old/backend/internal/handler/member/list_members_handler.go diff --git a/backend/internal/handler/member/update_member_ai_settings_handler.go b/old/backend/internal/handler/member/update_member_ai_settings_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_ai_settings_handler.go rename to old/backend/internal/handler/member/update_member_ai_settings_handler.go diff --git a/backend/internal/handler/member/update_member_me_handler.go b/old/backend/internal/handler/member/update_member_me_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_me_handler.go rename to old/backend/internal/handler/member/update_member_me_handler.go diff --git a/backend/internal/handler/member/update_member_password_handler.go b/old/backend/internal/handler/member/update_member_password_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_password_handler.go rename to old/backend/internal/handler/member/update_member_password_handler.go diff --git a/backend/internal/handler/member/update_member_placement_settings_handler.go b/old/backend/internal/handler/member/update_member_placement_settings_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_placement_settings_handler.go rename to old/backend/internal/handler/member/update_member_placement_settings_handler.go diff --git a/backend/internal/handler/member/update_member_profile_handler.go b/old/backend/internal/handler/member/update_member_profile_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_profile_handler.go rename to old/backend/internal/handler/member/update_member_profile_handler.go diff --git a/backend/internal/handler/member/update_member_roles_handler.go b/old/backend/internal/handler/member/update_member_roles_handler.go similarity index 100% rename from backend/internal/handler/member/update_member_roles_handler.go rename to old/backend/internal/handler/member/update_member_roles_handler.go diff --git a/backend/internal/handler/member/upload_member_avatar_handler.go b/old/backend/internal/handler/member/upload_member_avatar_handler.go similarity index 100% rename from backend/internal/handler/member/upload_member_avatar_handler.go rename to old/backend/internal/handler/member/upload_member_avatar_handler.go diff --git a/backend/internal/handler/normal/health_handler.go b/old/backend/internal/handler/normal/health_handler.go similarity index 100% rename from backend/internal/handler/normal/health_handler.go rename to old/backend/internal/handler/normal/health_handler.go diff --git a/backend/internal/handler/permission/get_me_permissions_handler.go b/old/backend/internal/handler/permission/get_me_permissions_handler.go similarity index 100% rename from backend/internal/handler/permission/get_me_permissions_handler.go rename to old/backend/internal/handler/permission/get_me_permissions_handler.go diff --git a/backend/internal/handler/permission/get_permission_catalog_handler.go b/old/backend/internal/handler/permission/get_permission_catalog_handler.go similarity index 100% rename from backend/internal/handler/permission/get_permission_catalog_handler.go rename to old/backend/internal/handler/permission/get_permission_catalog_handler.go diff --git a/backend/internal/handler/persona/create_content_plan_handler.go b/old/backend/internal/handler/persona/create_content_plan_handler.go similarity index 100% rename from backend/internal/handler/persona/create_content_plan_handler.go rename to old/backend/internal/handler/persona/create_content_plan_handler.go diff --git a/backend/internal/handler/persona/create_feedback_event_handler.go b/old/backend/internal/handler/persona/create_feedback_event_handler.go similarity index 100% rename from backend/internal/handler/persona/create_feedback_event_handler.go rename to old/backend/internal/handler/persona/create_feedback_event_handler.go diff --git a/backend/internal/handler/persona/create_formula_pool_handler.go b/old/backend/internal/handler/persona/create_formula_pool_handler.go similarity index 100% rename from backend/internal/handler/persona/create_formula_pool_handler.go rename to old/backend/internal/handler/persona/create_formula_pool_handler.go diff --git a/backend/internal/handler/persona/create_knowledge_source_handler.go b/old/backend/internal/handler/persona/create_knowledge_source_handler.go similarity index 100% rename from backend/internal/handler/persona/create_knowledge_source_handler.go rename to old/backend/internal/handler/persona/create_knowledge_source_handler.go diff --git a/backend/internal/handler/persona/create_persona_handler.go b/old/backend/internal/handler/persona/create_persona_handler.go similarity index 100% rename from backend/internal/handler/persona/create_persona_handler.go rename to old/backend/internal/handler/persona/create_persona_handler.go diff --git a/backend/internal/handler/persona/delete_persona_copy_draft_handler.go b/old/backend/internal/handler/persona/delete_persona_copy_draft_handler.go similarity index 100% rename from backend/internal/handler/persona/delete_persona_copy_draft_handler.go rename to old/backend/internal/handler/persona/delete_persona_copy_draft_handler.go diff --git a/backend/internal/handler/persona/delete_persona_handler.go b/old/backend/internal/handler/persona/delete_persona_handler.go similarity index 100% rename from backend/internal/handler/persona/delete_persona_handler.go rename to old/backend/internal/handler/persona/delete_persona_handler.go diff --git a/backend/internal/handler/persona/delete_style_preset_handler.go b/old/backend/internal/handler/persona/delete_style_preset_handler.go similarity index 100% rename from backend/internal/handler/persona/delete_style_preset_handler.go rename to old/backend/internal/handler/persona/delete_style_preset_handler.go diff --git a/backend/internal/handler/persona/generate_from_content_formula_handler.go b/old/backend/internal/handler/persona/generate_from_content_formula_handler.go similarity index 100% rename from backend/internal/handler/persona/generate_from_content_formula_handler.go rename to old/backend/internal/handler/persona/generate_from_content_formula_handler.go diff --git a/backend/internal/handler/persona/generate_persona_copy_draft_handler.go b/old/backend/internal/handler/persona/generate_persona_copy_draft_handler.go similarity index 100% rename from backend/internal/handler/persona/generate_persona_copy_draft_handler.go rename to old/backend/internal/handler/persona/generate_persona_copy_draft_handler.go diff --git a/backend/internal/handler/persona/generate_persona_topic_matrix_handler.go b/old/backend/internal/handler/persona/generate_persona_topic_matrix_handler.go similarity index 100% rename from backend/internal/handler/persona/generate_persona_topic_matrix_handler.go rename to old/backend/internal/handler/persona/generate_persona_topic_matrix_handler.go diff --git a/backend/internal/handler/persona/get_persona_handler.go b/old/backend/internal/handler/persona/get_persona_handler.go similarity index 100% rename from backend/internal/handler/persona/get_persona_handler.go rename to old/backend/internal/handler/persona/get_persona_handler.go diff --git a/backend/internal/handler/persona/list_content_plans_handler.go b/old/backend/internal/handler/persona/list_content_plans_handler.go similarity index 100% rename from backend/internal/handler/persona/list_content_plans_handler.go rename to old/backend/internal/handler/persona/list_content_plans_handler.go diff --git a/backend/internal/handler/persona/list_formula_pools_handler.go b/old/backend/internal/handler/persona/list_formula_pools_handler.go similarity index 100% rename from backend/internal/handler/persona/list_formula_pools_handler.go rename to old/backend/internal/handler/persona/list_formula_pools_handler.go diff --git a/backend/internal/handler/persona/list_knowledge_chunks_handler.go b/old/backend/internal/handler/persona/list_knowledge_chunks_handler.go similarity index 100% rename from backend/internal/handler/persona/list_knowledge_chunks_handler.go rename to old/backend/internal/handler/persona/list_knowledge_chunks_handler.go diff --git a/backend/internal/handler/persona/list_knowledge_sources_handler.go b/old/backend/internal/handler/persona/list_knowledge_sources_handler.go similarity index 100% rename from backend/internal/handler/persona/list_knowledge_sources_handler.go rename to old/backend/internal/handler/persona/list_knowledge_sources_handler.go diff --git a/backend/internal/handler/persona/list_persona_content_inbox_handler.go b/old/backend/internal/handler/persona/list_persona_content_inbox_handler.go similarity index 100% rename from backend/internal/handler/persona/list_persona_content_inbox_handler.go rename to old/backend/internal/handler/persona/list_persona_content_inbox_handler.go diff --git a/backend/internal/handler/persona/list_persona_copy_drafts_handler.go b/old/backend/internal/handler/persona/list_persona_copy_drafts_handler.go similarity index 100% rename from backend/internal/handler/persona/list_persona_copy_drafts_handler.go rename to old/backend/internal/handler/persona/list_persona_copy_drafts_handler.go diff --git a/backend/internal/handler/persona/list_persona_viral_scan_posts_handler.go b/old/backend/internal/handler/persona/list_persona_viral_scan_posts_handler.go similarity index 100% rename from backend/internal/handler/persona/list_persona_viral_scan_posts_handler.go rename to old/backend/internal/handler/persona/list_persona_viral_scan_posts_handler.go diff --git a/backend/internal/handler/persona/list_personas_handler.go b/old/backend/internal/handler/persona/list_personas_handler.go similarity index 100% rename from backend/internal/handler/persona/list_personas_handler.go rename to old/backend/internal/handler/persona/list_personas_handler.go diff --git a/backend/internal/handler/persona/list_style_presets_handler.go b/old/backend/internal/handler/persona/list_style_presets_handler.go similarity index 100% rename from backend/internal/handler/persona/list_style_presets_handler.go rename to old/backend/internal/handler/persona/list_style_presets_handler.go diff --git a/backend/internal/handler/persona/list_topic_candidates_handler.go b/old/backend/internal/handler/persona/list_topic_candidates_handler.go similarity index 100% rename from backend/internal/handler/persona/list_topic_candidates_handler.go rename to old/backend/internal/handler/persona/list_topic_candidates_handler.go diff --git a/backend/internal/handler/persona/prune_persona_copy_drafts_handler.go b/old/backend/internal/handler/persona/prune_persona_copy_drafts_handler.go similarity index 100% rename from backend/internal/handler/persona/prune_persona_copy_drafts_handler.go rename to old/backend/internal/handler/persona/prune_persona_copy_drafts_handler.go diff --git a/backend/internal/handler/persona/publish_persona_copy_draft_handler.go b/old/backend/internal/handler/persona/publish_persona_copy_draft_handler.go similarity index 100% rename from backend/internal/handler/persona/publish_persona_copy_draft_handler.go rename to old/backend/internal/handler/persona/publish_persona_copy_draft_handler.go diff --git a/backend/internal/handler/persona/schedule_persona_copy_draft_handler.go b/old/backend/internal/handler/persona/schedule_persona_copy_draft_handler.go similarity index 100% rename from backend/internal/handler/persona/schedule_persona_copy_draft_handler.go rename to old/backend/internal/handler/persona/schedule_persona_copy_draft_handler.go diff --git a/backend/internal/handler/persona/schedule_persona_drafts_handler.go b/old/backend/internal/handler/persona/schedule_persona_drafts_handler.go similarity index 100% rename from backend/internal/handler/persona/schedule_persona_drafts_handler.go rename to old/backend/internal/handler/persona/schedule_persona_drafts_handler.go diff --git a/backend/internal/handler/persona/start_persona_formula_draft_job_handler.go b/old/backend/internal/handler/persona/start_persona_formula_draft_job_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_formula_draft_job_handler.go rename to old/backend/internal/handler/persona/start_persona_formula_draft_job_handler.go diff --git a/backend/internal/handler/persona/start_persona_rewrite_draft_job_handler.go b/old/backend/internal/handler/persona/start_persona_rewrite_draft_job_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_rewrite_draft_job_handler.go rename to old/backend/internal/handler/persona/start_persona_rewrite_draft_job_handler.go diff --git a/backend/internal/handler/persona/start_persona_style_analysis_from_text_handler.go b/old/backend/internal/handler/persona/start_persona_style_analysis_from_text_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_style_analysis_from_text_handler.go rename to old/backend/internal/handler/persona/start_persona_style_analysis_from_text_handler.go diff --git a/backend/internal/handler/persona/start_persona_style_analysis_handler.go b/old/backend/internal/handler/persona/start_persona_style_analysis_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_style_analysis_handler.go rename to old/backend/internal/handler/persona/start_persona_style_analysis_handler.go diff --git a/backend/internal/handler/persona/start_persona_topic_matrix_job_handler.go b/old/backend/internal/handler/persona/start_persona_topic_matrix_job_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_topic_matrix_job_handler.go rename to old/backend/internal/handler/persona/start_persona_topic_matrix_job_handler.go diff --git a/backend/internal/handler/persona/start_persona_viral_scan_job_handler.go b/old/backend/internal/handler/persona/start_persona_viral_scan_job_handler.go similarity index 100% rename from backend/internal/handler/persona/start_persona_viral_scan_job_handler.go rename to old/backend/internal/handler/persona/start_persona_viral_scan_job_handler.go diff --git a/backend/internal/handler/persona/update_content_plan_handler.go b/old/backend/internal/handler/persona/update_content_plan_handler.go similarity index 100% rename from backend/internal/handler/persona/update_content_plan_handler.go rename to old/backend/internal/handler/persona/update_content_plan_handler.go diff --git a/backend/internal/handler/persona/update_persona_copy_draft_handler.go b/old/backend/internal/handler/persona/update_persona_copy_draft_handler.go similarity index 100% rename from backend/internal/handler/persona/update_persona_copy_draft_handler.go rename to old/backend/internal/handler/persona/update_persona_copy_draft_handler.go diff --git a/backend/internal/handler/persona/update_persona_handler.go b/old/backend/internal/handler/persona/update_persona_handler.go similarity index 100% rename from backend/internal/handler/persona/update_persona_handler.go rename to old/backend/internal/handler/persona/update_persona_handler.go diff --git a/backend/internal/handler/persona/upsert_style_preset_handler.go b/old/backend/internal/handler/persona/upsert_style_preset_handler.go similarity index 100% rename from backend/internal/handler/persona/upsert_style_preset_handler.go rename to old/backend/internal/handler/persona/upsert_style_preset_handler.go diff --git a/backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go b/old/backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go rename to old/backend/internal/handler/placement_topic/batch_delete_placement_topic_scan_posts_handler.go diff --git a/backend/internal/handler/placement_topic/create_placement_topic_handler.go b/old/backend/internal/handler/placement_topic/create_placement_topic_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/create_placement_topic_handler.go rename to old/backend/internal/handler/placement_topic/create_placement_topic_handler.go diff --git a/backend/internal/handler/placement_topic/delete_placement_topic_handler.go b/old/backend/internal/handler/placement_topic/delete_placement_topic_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/delete_placement_topic_handler.go rename to old/backend/internal/handler/placement_topic/delete_placement_topic_handler.go diff --git a/backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go b/old/backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go rename to old/backend/internal/handler/placement_topic/delete_placement_topic_scan_post_handler.go diff --git a/backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go b/old/backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go rename to old/backend/internal/handler/placement_topic/expand_placement_topic_graph_handler.go diff --git a/backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go b/old/backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go rename to old/backend/internal/handler/placement_topic/generate_placement_topic_content_matrix_handler.go diff --git a/backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go b/old/backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go rename to old/backend/internal/handler/placement_topic/generate_placement_topic_outreach_drafts_handler.go diff --git a/backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go b/old/backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go rename to old/backend/internal/handler/placement_topic/get_placement_topic_content_matrix_handler.go diff --git a/backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go b/old/backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go rename to old/backend/internal/handler/placement_topic/get_placement_topic_graph_handler.go diff --git a/backend/internal/handler/placement_topic/get_placement_topic_handler.go b/old/backend/internal/handler/placement_topic/get_placement_topic_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/get_placement_topic_handler.go rename to old/backend/internal/handler/placement_topic/get_placement_topic_handler.go diff --git a/backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go b/old/backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go rename to old/backend/internal/handler/placement_topic/get_placement_topic_scan_schedule_handler.go diff --git a/backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go b/old/backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go rename to old/backend/internal/handler/placement_topic/list_placement_topic_scan_posts_handler.go diff --git a/backend/internal/handler/placement_topic/list_placement_topics_handler.go b/old/backend/internal/handler/placement_topic/list_placement_topics_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/list_placement_topics_handler.go rename to old/backend/internal/handler/placement_topic/list_placement_topics_handler.go diff --git a/backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go b/old/backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go rename to old/backend/internal/handler/placement_topic/patch_placement_topic_graph_nodes_handler.go diff --git a/backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go b/old/backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go rename to old/backend/internal/handler/placement_topic/patch_placement_topic_scan_post_outreach_handler.go diff --git a/backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go b/old/backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go rename to old/backend/internal/handler/placement_topic/publish_placement_topic_outreach_draft_handler.go diff --git a/backend/internal/handler/placement_topic/start_placement_topic_outreach_draft_job_handler.go b/old/backend/internal/handler/placement_topic/start_placement_topic_outreach_draft_job_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/start_placement_topic_outreach_draft_job_handler.go rename to old/backend/internal/handler/placement_topic/start_placement_topic_outreach_draft_job_handler.go diff --git a/backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go b/old/backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go rename to old/backend/internal/handler/placement_topic/start_placement_topic_scan_job_handler.go diff --git a/backend/internal/handler/placement_topic/update_placement_topic_handler.go b/old/backend/internal/handler/placement_topic/update_placement_topic_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/update_placement_topic_handler.go rename to old/backend/internal/handler/placement_topic/update_placement_topic_handler.go diff --git a/backend/internal/handler/placement_topic/update_placement_topic_outreach_draft_handler.go b/old/backend/internal/handler/placement_topic/update_placement_topic_outreach_draft_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/update_placement_topic_outreach_draft_handler.go rename to old/backend/internal/handler/placement_topic/update_placement_topic_outreach_draft_handler.go diff --git a/backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go b/old/backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go similarity index 100% rename from backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go rename to old/backend/internal/handler/placement_topic/upsert_placement_topic_scan_schedule_handler.go diff --git a/backend/internal/handler/register_extra.go b/old/backend/internal/handler/register_extra.go similarity index 72% rename from backend/internal/handler/register_extra.go rename to old/backend/internal/handler/register_extra.go index e973666..2adfda0 100644 --- a/backend/internal/handler/register_extra.go +++ b/old/backend/internal/handler/register_extra.go @@ -5,11 +5,17 @@ import ( "haixun-backend/internal/handler/static" threads_account "haixun-backend/internal/handler/threads_account" + "haixun-backend/internal/library/storage" "haixun-backend/internal/svc" "github.com/zeromicro/go-zero/rest" ) +func publicAssetReader(serverCtx *svc.ServiceContext) storage.ObjectReader { + reader, _ := serverCtx.AvatarStorage.(storage.ObjectReader) + return reader +} + // RegisterExtraHandlers mounts routes that are not generated by goctl. func RegisterExtraHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( @@ -19,6 +25,11 @@ func RegisterExtraHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/downloads/haixun-threads-sync.zip", Handler: static.ExtensionDownloadHandler(), }, + { + Method: http.MethodGet, + Path: "/public/assets", + Handler: static.PublicAssetHandler(publicAssetReader(serverCtx)), + }, }, rest.WithPrefix("/api/v1"), ) diff --git a/backend/internal/handler/routes.go b/old/backend/internal/handler/routes.go similarity index 89% rename from backend/internal/handler/routes.go rename to old/backend/internal/handler/routes.go index 1734322..5acaf98 100644 --- a/backend/internal/handler/routes.go +++ b/old/backend/internal/handler/routes.go @@ -103,6 +103,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/register", Handler: auth.RegisterHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/resend-verification-email", + Handler: auth.ResendVerificationEmailHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/verify-email", + Handler: auth.VerifyEmailHandler(serverCtx), + }, }..., ), rest.WithPrefix("/api/v1/auth"), @@ -231,121 +241,77 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithMiddlewares( []rest.Middleware{serverCtx.AuthJWT}, []rest.Route{ + // Flow A copy_mission CRUD/jobs removed (stubs returned false success). + // Keep inspire only until Post Project replaces Missions inspiration. { Method: http.MethodPost, Path: "/:personaId/copy-mission-inspiration", Handler: copy_mission.InspireCopyMissionHandler(serverCtx), }, - { - Method: http.MethodGet, - Path: "/:personaId/copy-missions", - Handler: copy_mission.ListCopyMissionsHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/:personaId/copy-missions", - Handler: copy_mission.CreateCopyMissionHandler(serverCtx), - }, - { - Method: http.MethodGet, - Path: "/:personaId/copy-missions/:id", - Handler: copy_mission.GetCopyMissionHandler(serverCtx), - }, - { - Method: http.MethodPatch, - 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: "/:personaId/copy-missions/:id/analyze-jobs", - Handler: copy_mission.StartCopyMissionAnalyzeJobHandler(serverCtx), - }, - { - Method: http.MethodPatch, - Path: "/:personaId/copy-missions/:id/audience-samples/:username", - Handler: copy_mission.PatchCopyMissionAudienceSampleHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/:personaId/copy-missions/:id/copy-draft-jobs", - Handler: copy_mission.StartCopyMissionCopyDraftJobHandler(serverCtx), - }, - { - Method: http.MethodGet, - Path: "/:personaId/copy-missions/:id/copy-drafts", - Handler: copy_mission.ListCopyMissionCopyDraftsHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/:personaId/copy-missions/:id/copy-drafts/schedule", - Handler: copy_mission.ScheduleCopyMissionDraftsHandler(serverCtx), - }, - { - Method: http.MethodGet, - Path: "/:personaId/copy-missions/:id/knowledge-graph", - Handler: copy_mission.GetCopyMissionGraphHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/:personaId/copy-missions/:id/knowledge-graph/expand", - Handler: copy_mission.ExpandCopyMissionGraphHandler(serverCtx), - }, - { - Method: http.MethodPatch, - Path: "/:personaId/copy-missions/:id/knowledge-graph/nodes", - Handler: copy_mission.PatchCopyMissionGraphNodesHandler(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-drafts/delete", - Handler: copy_mission.DeleteCopyMissionMatrixDraftsHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/:personaId/copy-missions/:id/matrix-jobs", - Handler: copy_mission.StartCopyMissionMatrixJobHandler(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), - }, - { - Method: http.MethodPatch, - Path: "/:personaId/copy-missions/:id/similar-accounts/:username", - Handler: copy_mission.PatchCopyMissionSimilarAccountHandler(serverCtx), - }, }..., ), rest.WithPrefix("/api/v1/personas"), ) + 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}, @@ -430,65 +396,6 @@ 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}, @@ -1079,6 +986,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/:id/publish-alerts", Handler: threads_account.ListPublishAlertsHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/:id/publish-attachments", + Handler: threads_account.UploadPublishAttachmentHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/:id/publish-reply", + Handler: threads_account.PublishThreadsReplyHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/:id/publish-calendar", @@ -1114,11 +1031,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/:id/publish-inventory-policy", Handler: threads_account.UpsertPublishInventoryPolicyHandler(serverCtx), }, - { - Method: http.MethodPost, - Path: "/:id/publish-inventory/refill-jobs", - Handler: threads_account.StartPublishInventoryRefillJobHandler(serverCtx), - }, + // refill-publish-inventory job removed (no step handler / template not seeded) { Method: http.MethodPost, Path: "/:id/publish-queue", diff --git a/backend/internal/handler/setting/delete_setting_handler.go b/old/backend/internal/handler/setting/delete_setting_handler.go similarity index 100% rename from backend/internal/handler/setting/delete_setting_handler.go rename to old/backend/internal/handler/setting/delete_setting_handler.go diff --git a/backend/internal/handler/setting/get_setting_handler.go b/old/backend/internal/handler/setting/get_setting_handler.go similarity index 100% rename from backend/internal/handler/setting/get_setting_handler.go rename to old/backend/internal/handler/setting/get_setting_handler.go diff --git a/backend/internal/handler/setting/list_settings_handler.go b/old/backend/internal/handler/setting/list_settings_handler.go similarity index 100% rename from backend/internal/handler/setting/list_settings_handler.go rename to old/backend/internal/handler/setting/list_settings_handler.go diff --git a/backend/internal/handler/setting/upsert_setting_handler.go b/old/backend/internal/handler/setting/upsert_setting_handler.go similarity index 100% rename from backend/internal/handler/setting/upsert_setting_handler.go rename to old/backend/internal/handler/setting/upsert_setting_handler.go diff --git a/backend/internal/handler/static/extension_download.go b/old/backend/internal/handler/static/extension_download.go similarity index 100% rename from backend/internal/handler/static/extension_download.go rename to old/backend/internal/handler/static/extension_download.go diff --git a/old/backend/internal/handler/static/public_asset.go b/old/backend/internal/handler/static/public_asset.go new file mode 100644 index 0000000..fd7625b --- /dev/null +++ b/old/backend/internal/handler/static/public_asset.go @@ -0,0 +1,50 @@ +package static + +import ( + "io" + "net/http" + "strings" + + "haixun-backend/internal/library/storage" +) + +func PublicAssetHandler(reader storage.ObjectReader) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if reader == nil { + http.Error(w, "asset storage is not configured", http.StatusServiceUnavailable) + return + } + key := strings.TrimSpace(r.URL.Query().Get("key")) + if key == "" { + http.Error(w, "missing asset key", http.StatusBadRequest) + return + } + if !storage.IsPublicAssetKey(key) { + http.Error(w, "asset not found", http.StatusNotFound) + return + } + + if strings.HasPrefix(key, storage.EphemeralKeyPrefix) { + data, contentType, ok := storage.DefaultEphemeralAssetStore().Get(key) + if !ok { + http.Error(w, "asset not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "no-store") + _, _ = w.Write(data) + return + } + + body, contentType, err := reader.GetObject(r.Context(), key) + if err != nil { + http.Error(w, "asset not found", http.StatusNotFound) + return + } + defer body.Close() + + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "public, max-age=3600") + _, _ = io.Copy(w, body) + } +} \ No newline at end of file diff --git a/backend/internal/handler/threads_account/activate_threads_account_handler.go b/old/backend/internal/handler/threads_account/activate_threads_account_handler.go similarity index 100% rename from backend/internal/handler/threads_account/activate_threads_account_handler.go rename to old/backend/internal/handler/threads_account/activate_threads_account_handler.go diff --git a/backend/internal/handler/threads_account/analyze_content_formula_handler.go b/old/backend/internal/handler/threads_account/analyze_content_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/analyze_content_formula_handler.go rename to old/backend/internal/handler/threads_account/analyze_content_formula_handler.go diff --git a/backend/internal/handler/threads_account/cancel_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/cancel_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/cancel_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/cancel_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/create_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/create_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/create_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/create_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/create_threads_account_handler.go b/old/backend/internal/handler/threads_account/create_threads_account_handler.go similarity index 100% rename from backend/internal/handler/threads_account/create_threads_account_handler.go rename to old/backend/internal/handler/threads_account/create_threads_account_handler.go diff --git a/backend/internal/handler/threads_account/delete_content_formula_handler.go b/old/backend/internal/handler/threads_account/delete_content_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/delete_content_formula_handler.go rename to old/backend/internal/handler/threads_account/delete_content_formula_handler.go diff --git a/backend/internal/handler/threads_account/delete_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/delete_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/delete_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/delete_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/delete_threads_account_handler.go b/old/backend/internal/handler/threads_account/delete_threads_account_handler.go similarity index 100% rename from backend/internal/handler/threads_account/delete_threads_account_handler.go rename to old/backend/internal/handler/threads_account/delete_threads_account_handler.go diff --git a/backend/internal/handler/threads_account/disconnect_threads_api_handler.go b/old/backend/internal/handler/threads_account/disconnect_threads_api_handler.go similarity index 100% rename from backend/internal/handler/threads_account/disconnect_threads_api_handler.go rename to old/backend/internal/handler/threads_account/disconnect_threads_api_handler.go diff --git a/backend/internal/handler/threads_account/generate_own_post_formula_handler.go b/old/backend/internal/handler/threads_account/generate_own_post_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/generate_own_post_formula_handler.go rename to old/backend/internal/handler/threads_account/generate_own_post_formula_handler.go diff --git a/backend/internal/handler/threads_account/generate_own_post_reply_draft_handler.go b/old/backend/internal/handler/threads_account/generate_own_post_reply_draft_handler.go similarity index 100% rename from backend/internal/handler/threads_account/generate_own_post_reply_draft_handler.go rename to old/backend/internal/handler/threads_account/generate_own_post_reply_draft_handler.go diff --git a/backend/internal/handler/threads_account/get_content_formula_handler.go b/old/backend/internal/handler/threads_account/get_content_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_content_formula_handler.go rename to old/backend/internal/handler/threads_account/get_content_formula_handler.go diff --git a/backend/internal/handler/threads_account/get_publish_dashboard_summary_handler.go b/old/backend/internal/handler/threads_account/get_publish_dashboard_summary_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_publish_dashboard_summary_handler.go rename to old/backend/internal/handler/threads_account/get_publish_dashboard_summary_handler.go diff --git a/backend/internal/handler/threads_account/get_publish_guard_policy_handler.go b/old/backend/internal/handler/threads_account/get_publish_guard_policy_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_publish_guard_policy_handler.go rename to old/backend/internal/handler/threads_account/get_publish_guard_policy_handler.go diff --git a/backend/internal/handler/threads_account/get_publish_inventory_policy_handler.go b/old/backend/internal/handler/threads_account/get_publish_inventory_policy_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_publish_inventory_policy_handler.go rename to old/backend/internal/handler/threads_account/get_publish_inventory_policy_handler.go diff --git a/backend/internal/handler/threads_account/get_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/get_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/get_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/get_publish_slot_insights_handler.go b/old/backend/internal/handler/threads_account/get_publish_slot_insights_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_publish_slot_insights_handler.go rename to old/backend/internal/handler/threads_account/get_publish_slot_insights_handler.go diff --git a/backend/internal/handler/threads_account/get_threads_account_ai_settings_handler.go b/old/backend/internal/handler/threads_account/get_threads_account_ai_settings_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_threads_account_ai_settings_handler.go rename to old/backend/internal/handler/threads_account/get_threads_account_ai_settings_handler.go diff --git a/backend/internal/handler/threads_account/get_threads_account_connection_handler.go b/old/backend/internal/handler/threads_account/get_threads_account_connection_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_threads_account_connection_handler.go rename to old/backend/internal/handler/threads_account/get_threads_account_connection_handler.go diff --git a/backend/internal/handler/threads_account/get_threads_account_handler.go b/old/backend/internal/handler/threads_account/get_threads_account_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_threads_account_handler.go rename to old/backend/internal/handler/threads_account/get_threads_account_handler.go diff --git a/backend/internal/handler/threads_account/get_threads_diagnostics_handler.go b/old/backend/internal/handler/threads_account/get_threads_diagnostics_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_threads_diagnostics_handler.go rename to old/backend/internal/handler/threads_account/get_threads_diagnostics_handler.go diff --git a/backend/internal/handler/threads_account/get_threads_oauth_config_handler.go b/old/backend/internal/handler/threads_account/get_threads_oauth_config_handler.go similarity index 100% rename from backend/internal/handler/threads_account/get_threads_oauth_config_handler.go rename to old/backend/internal/handler/threads_account/get_threads_oauth_config_handler.go diff --git a/backend/internal/handler/threads_account/import_own_post_to_content_formula_handler.go b/old/backend/internal/handler/threads_account/import_own_post_to_content_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/import_own_post_to_content_formula_handler.go rename to old/backend/internal/handler/threads_account/import_own_post_to_content_formula_handler.go diff --git a/backend/internal/handler/threads_account/import_threads_account_session_handler.go b/old/backend/internal/handler/threads_account/import_threads_account_session_handler.go similarity index 100% rename from backend/internal/handler/threads_account/import_threads_account_session_handler.go rename to old/backend/internal/handler/threads_account/import_threads_account_session_handler.go diff --git a/backend/internal/handler/threads_account/list_content_formulas_handler.go b/old/backend/internal/handler/threads_account/list_content_formulas_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_content_formulas_handler.go rename to old/backend/internal/handler/threads_account/list_content_formulas_handler.go diff --git a/backend/internal/handler/threads_account/list_mention_inbox_handler.go b/old/backend/internal/handler/threads_account/list_mention_inbox_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_mention_inbox_handler.go rename to old/backend/internal/handler/threads_account/list_mention_inbox_handler.go diff --git a/backend/internal/handler/threads_account/list_own_post_formulas_handler.go b/old/backend/internal/handler/threads_account/list_own_post_formulas_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_own_post_formulas_handler.go rename to old/backend/internal/handler/threads_account/list_own_post_formulas_handler.go diff --git a/backend/internal/handler/threads_account/list_publish_alerts_handler.go b/old/backend/internal/handler/threads_account/list_publish_alerts_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_publish_alerts_handler.go rename to old/backend/internal/handler/threads_account/list_publish_alerts_handler.go diff --git a/backend/internal/handler/threads_account/list_publish_queue_events_handler.go b/old/backend/internal/handler/threads_account/list_publish_queue_events_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_publish_queue_events_handler.go rename to old/backend/internal/handler/threads_account/list_publish_queue_events_handler.go diff --git a/backend/internal/handler/threads_account/list_publish_queue_items_handler.go b/old/backend/internal/handler/threads_account/list_publish_queue_items_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_publish_queue_items_handler.go rename to old/backend/internal/handler/threads_account/list_publish_queue_items_handler.go diff --git a/backend/internal/handler/threads_account/list_publish_queue_range_handler.go b/old/backend/internal/handler/threads_account/list_publish_queue_range_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_publish_queue_range_handler.go rename to old/backend/internal/handler/threads_account/list_publish_queue_range_handler.go diff --git a/backend/internal/handler/threads_account/list_threads_account_ai_provider_models_handler.go b/old/backend/internal/handler/threads_account/list_threads_account_ai_provider_models_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_threads_account_ai_provider_models_handler.go rename to old/backend/internal/handler/threads_account/list_threads_account_ai_provider_models_handler.go diff --git a/backend/internal/handler/threads_account/list_threads_accounts_handler.go b/old/backend/internal/handler/threads_account/list_threads_accounts_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_threads_accounts_handler.go rename to old/backend/internal/handler/threads_account/list_threads_accounts_handler.go diff --git a/backend/internal/handler/threads_account/list_threads_oauth_logs_handler.go b/old/backend/internal/handler/threads_account/list_threads_oauth_logs_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_threads_oauth_logs_handler.go rename to old/backend/internal/handler/threads_account/list_threads_oauth_logs_handler.go diff --git a/backend/internal/handler/threads_account/list_threads_post_performance_handler.go b/old/backend/internal/handler/threads_account/list_threads_post_performance_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_threads_post_performance_handler.go rename to old/backend/internal/handler/threads_account/list_threads_post_performance_handler.go diff --git a/backend/internal/handler/threads_account/list_threads_publish_health_handler.go b/old/backend/internal/handler/threads_account/list_threads_publish_health_handler.go similarity index 100% rename from backend/internal/handler/threads_account/list_threads_publish_health_handler.go rename to old/backend/internal/handler/threads_account/list_threads_publish_health_handler.go diff --git a/backend/internal/handler/threads_account/patch_content_formula_handler.go b/old/backend/internal/handler/threads_account/patch_content_formula_handler.go similarity index 100% rename from backend/internal/handler/threads_account/patch_content_formula_handler.go rename to old/backend/internal/handler/threads_account/patch_content_formula_handler.go diff --git a/backend/internal/handler/threads_account/patch_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/patch_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/patch_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/patch_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/publish_mention_reply_handler.go b/old/backend/internal/handler/threads_account/publish_mention_reply_handler.go similarity index 60% rename from backend/internal/handler/threads_account/publish_mention_reply_handler.go rename to old/backend/internal/handler/threads_account/publish_mention_reply_handler.go index 0568b7c..3c76856 100644 --- a/backend/internal/handler/threads_account/publish_mention_reply_handler.go +++ b/old/backend/internal/handler/threads_account/publish_mention_reply_handler.go @@ -1,7 +1,9 @@ package threads_account import ( + "io" "net/http" + "strings" "github.com/zeromicro/go-zero/rest/httpx" "haixun-backend/internal/logic/threads_account" @@ -21,8 +23,24 @@ func PublishMentionReplyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { response.Write(r.Context(), w, nil, response.WrapRequestError(err)) return } + r.Body = http.MaxBytesReader(w, r.Body, maxPublishAttachmentBytes+1<<20) + if strings.TrimSpace(req.Text) == "" { + req.Text = strings.TrimSpace(r.FormValue("text")) + } + + imageFile, err := readOptionalReplyImage(r) + if err != nil { + response.Write(r.Context(), w, nil, response.WrapRequestError(err)) + return + } + if imageFile != nil && imageFile.Body != nil { + if closer, ok := imageFile.Body.(io.Closer); ok { + defer closer.Close() + } + } + l := threads_account.NewPublishMentionReplyLogic(r.Context(), svcCtx) - data, err := l.PublishMentionReply(&req) + data, err := l.PublishMentionReply(&req, imageFile) response.Write(r.Context(), w, data, err) } } diff --git a/old/backend/internal/handler/threads_account/publish_threads_reply_handler.go b/old/backend/internal/handler/threads_account/publish_threads_reply_handler.go new file mode 100644 index 0000000..52b1e12 --- /dev/null +++ b/old/backend/internal/handler/threads_account/publish_threads_reply_handler.go @@ -0,0 +1,58 @@ +package threads_account + +import ( + "errors" + "net/http" + "strings" + + "github.com/zeromicro/go-zero/rest/httpx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/logic/threads_account" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +func PublishThreadsReplyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PublishThreadsReplyHandlerReq + if err := httpx.Parse(r, &req); err != nil { + response.Write(r.Context(), w, nil, response.WrapRequestError(err)) + return + } + r.Body = http.MaxBytesReader(w, r.Body, maxPublishAttachmentBytes+1<<20) + if strings.TrimSpace(req.ReplyToID) == "" { + req.ReplyToID = strings.TrimSpace(r.FormValue("reply_to_id")) + } + if strings.TrimSpace(req.Text) == "" { + req.Text = strings.TrimSpace(r.FormValue("text")) + } + if err := svcCtx.Validator.ValidateAll(&req); err != nil { + response.Write(r.Context(), w, nil, response.WrapRequestError(err)) + return + } + + var imageFile *threads_account.ReplyImageFile + file, header, err := r.FormFile("image") + if err == nil { + defer file.Close() + imageFile = &threads_account.ReplyImageFile{ + Filename: header.Filename, + ContentType: header.Header.Get("Content-Type"), + Body: file, + } + } else if !errors.Is(err, http.ErrMissingFile) { + if errors.As(err, new(*http.MaxBytesError)) { + response.Write(r.Context(), w, nil, app.For(code.ThreadsAccount).InputInvalidFormat("request body too large")) + return + } + response.Write(r.Context(), w, nil, response.WrapRequestError(err)) + return + } + + l := threads_account.NewPublishThreadsReplyLogic(r.Context(), svcCtx) + data, err := l.PublishThreadsReply(&req, imageFile) + response.Write(r.Context(), w, data, err) + } +} \ No newline at end of file diff --git a/old/backend/internal/handler/threads_account/reply_multipart.go b/old/backend/internal/handler/threads_account/reply_multipart.go new file mode 100644 index 0000000..b65026f --- /dev/null +++ b/old/backend/internal/handler/threads_account/reply_multipart.go @@ -0,0 +1,28 @@ +package threads_account + +import ( + "errors" + "net/http" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/logic/threads_account" +) + +func readOptionalReplyImage(r *http.Request) (*threads_account.ReplyImageFile, error) { + file, header, err := r.FormFile("image") + if errors.Is(err, http.ErrMissingFile) { + return nil, nil + } + if err != nil { + if errors.As(err, new(*http.MaxBytesError)) { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat("request body too large") + } + return nil, err + } + return &threads_account.ReplyImageFile{ + Filename: header.Filename, + ContentType: header.Header.Get("Content-Type"), + Body: file, + }, nil +} \ No newline at end of file diff --git a/backend/internal/handler/threads_account/resume_publish_guard_handler.go b/old/backend/internal/handler/threads_account/resume_publish_guard_handler.go similarity index 100% rename from backend/internal/handler/threads_account/resume_publish_guard_handler.go rename to old/backend/internal/handler/threads_account/resume_publish_guard_handler.go diff --git a/backend/internal/handler/threads_account/retry_publish_queue_item_handler.go b/old/backend/internal/handler/threads_account/retry_publish_queue_item_handler.go similarity index 100% rename from backend/internal/handler/threads_account/retry_publish_queue_item_handler.go rename to old/backend/internal/handler/threads_account/retry_publish_queue_item_handler.go diff --git a/backend/internal/handler/threads_account/run_threads_api_playground_handler.go b/old/backend/internal/handler/threads_account/run_threads_api_playground_handler.go similarity index 100% rename from backend/internal/handler/threads_account/run_threads_api_playground_handler.go rename to old/backend/internal/handler/threads_account/run_threads_api_playground_handler.go diff --git a/backend/internal/handler/threads_account/run_threads_api_smoke_test_handler.go b/old/backend/internal/handler/threads_account/run_threads_api_smoke_test_handler.go similarity index 100% rename from backend/internal/handler/threads_account/run_threads_api_smoke_test_handler.go rename to old/backend/internal/handler/threads_account/run_threads_api_smoke_test_handler.go diff --git a/backend/internal/handler/threads_account/search_content_formula_posts_handler.go b/old/backend/internal/handler/threads_account/search_content_formula_posts_handler.go similarity index 100% rename from backend/internal/handler/threads_account/search_content_formula_posts_handler.go rename to old/backend/internal/handler/threads_account/search_content_formula_posts_handler.go diff --git a/backend/internal/handler/threads_account/start_threads_oauth_handler.go b/old/backend/internal/handler/threads_account/start_threads_oauth_handler.go similarity index 100% rename from backend/internal/handler/threads_account/start_threads_oauth_handler.go rename to old/backend/internal/handler/threads_account/start_threads_oauth_handler.go diff --git a/backend/internal/handler/threads_account/sync_mention_inbox_handler.go b/old/backend/internal/handler/threads_account/sync_mention_inbox_handler.go similarity index 100% rename from backend/internal/handler/threads_account/sync_mention_inbox_handler.go rename to old/backend/internal/handler/threads_account/sync_mention_inbox_handler.go diff --git a/backend/internal/handler/threads_account/threads_oauth_callback_handler.go b/old/backend/internal/handler/threads_account/threads_oauth_callback_handler.go similarity index 100% rename from backend/internal/handler/threads_account/threads_oauth_callback_handler.go rename to old/backend/internal/handler/threads_account/threads_oauth_callback_handler.go diff --git a/backend/internal/handler/threads_account/update_threads_account_ai_settings_handler.go b/old/backend/internal/handler/threads_account/update_threads_account_ai_settings_handler.go similarity index 100% rename from backend/internal/handler/threads_account/update_threads_account_ai_settings_handler.go rename to old/backend/internal/handler/threads_account/update_threads_account_ai_settings_handler.go diff --git a/backend/internal/handler/threads_account/update_threads_account_connection_handler.go b/old/backend/internal/handler/threads_account/update_threads_account_connection_handler.go similarity index 100% rename from backend/internal/handler/threads_account/update_threads_account_connection_handler.go rename to old/backend/internal/handler/threads_account/update_threads_account_connection_handler.go diff --git a/backend/internal/handler/threads_account/update_threads_account_handler.go b/old/backend/internal/handler/threads_account/update_threads_account_handler.go similarity index 100% rename from backend/internal/handler/threads_account/update_threads_account_handler.go rename to old/backend/internal/handler/threads_account/update_threads_account_handler.go diff --git a/old/backend/internal/handler/threads_account/upload_publish_attachment_handler.go b/old/backend/internal/handler/threads_account/upload_publish_attachment_handler.go new file mode 100644 index 0000000..29d3ef8 --- /dev/null +++ b/old/backend/internal/handler/threads_account/upload_publish_attachment_handler.go @@ -0,0 +1,40 @@ +package threads_account + +import ( + "errors" + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/logic/threads_account" + "haixun-backend/internal/response" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" +) + +const maxPublishAttachmentBytes = 8 << 20 + +func UploadPublishAttachmentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ThreadsAccountPath + if err := httpx.Parse(r, &req); err != nil { + response.Write(r.Context(), w, nil, response.WrapRequestError(err)) + return + } + r.Body = http.MaxBytesReader(w, r.Body, maxPublishAttachmentBytes) + file, header, err := r.FormFile("image") + if err != nil { + if errors.As(err, new(*http.MaxBytesError)) { + response.Write(r.Context(), w, nil, app.For(code.ThreadsAccount).InputInvalidFormat("image must be smaller than 8MB")) + return + } + response.Write(r.Context(), w, nil, app.For(code.ThreadsAccount).InputMissingRequired("image file is required")) + return + } + defer file.Close() + l := threads_account.NewUploadPublishAttachmentLogic(r.Context(), svcCtx) + data, err := l.UploadPublishAttachment(req.ID, header.Filename, header.Header.Get("Content-Type"), file) + response.Write(r.Context(), w, data, err) + } +} \ No newline at end of file diff --git a/backend/internal/handler/threads_account/upsert_publish_guard_policy_handler.go b/old/backend/internal/handler/threads_account/upsert_publish_guard_policy_handler.go similarity index 100% rename from backend/internal/handler/threads_account/upsert_publish_guard_policy_handler.go rename to old/backend/internal/handler/threads_account/upsert_publish_guard_policy_handler.go diff --git a/backend/internal/handler/threads_account/upsert_publish_inventory_policy_handler.go b/old/backend/internal/handler/threads_account/upsert_publish_inventory_policy_handler.go similarity index 100% rename from backend/internal/handler/threads_account/upsert_publish_inventory_policy_handler.go rename to old/backend/internal/handler/threads_account/upsert_publish_inventory_policy_handler.go diff --git a/backend/internal/library/authctx/context.go b/old/backend/internal/library/authctx/context.go similarity index 100% rename from backend/internal/library/authctx/context.go rename to old/backend/internal/library/authctx/context.go diff --git a/backend/internal/library/brave/breaker.go b/old/backend/internal/library/brave/breaker.go similarity index 100% rename from backend/internal/library/brave/breaker.go rename to old/backend/internal/library/brave/breaker.go diff --git a/backend/internal/library/brave/client.go b/old/backend/internal/library/brave/client.go similarity index 100% rename from backend/internal/library/brave/client.go rename to old/backend/internal/library/brave/client.go diff --git a/backend/internal/library/clock/clock.go b/old/backend/internal/library/clock/clock.go similarity index 100% rename from backend/internal/library/clock/clock.go rename to old/backend/internal/library/clock/clock.go diff --git a/backend/internal/library/clock/clock_test.go b/old/backend/internal/library/clock/clock_test.go similarity index 100% rename from backend/internal/library/clock/clock_test.go rename to old/backend/internal/library/clock/clock_test.go diff --git a/backend/internal/library/copymission/matrix_lock.go b/old/backend/internal/library/copymission/matrix_lock.go similarity index 100% rename from backend/internal/library/copymission/matrix_lock.go rename to old/backend/internal/library/copymission/matrix_lock.go diff --git a/backend/internal/library/copyvoice/human.go b/old/backend/internal/library/copyvoice/human.go similarity index 100% rename from backend/internal/library/copyvoice/human.go rename to old/backend/internal/library/copyvoice/human.go diff --git a/backend/internal/library/copyvoice/human_test.go b/old/backend/internal/library/copyvoice/human_test.go similarity index 100% rename from backend/internal/library/copyvoice/human_test.go rename to old/backend/internal/library/copyvoice/human_test.go diff --git a/backend/internal/library/crypto/aesgcm.go b/old/backend/internal/library/crypto/aesgcm.go similarity index 100% rename from backend/internal/library/crypto/aesgcm.go rename to old/backend/internal/library/crypto/aesgcm.go diff --git a/backend/internal/library/errors/code/types.go b/old/backend/internal/library/errors/code/types.go similarity index 100% rename from backend/internal/library/errors/code/types.go rename to old/backend/internal/library/errors/code/types.go diff --git a/backend/internal/library/errors/errors.go b/old/backend/internal/library/errors/errors.go similarity index 100% rename from backend/internal/library/errors/errors.go rename to old/backend/internal/library/errors/errors.go diff --git a/backend/internal/library/exa/client.go b/old/backend/internal/library/exa/client.go similarity index 100% rename from backend/internal/library/exa/client.go rename to old/backend/internal/library/exa/client.go diff --git a/backend/internal/library/exa/client_test.go b/old/backend/internal/library/exa/client_test.go similarity index 100% rename from backend/internal/library/exa/client_test.go rename to old/backend/internal/library/exa/client_test.go diff --git a/backend/internal/library/formula/analyze_pasted.go b/old/backend/internal/library/formula/analyze_pasted.go similarity index 100% rename from backend/internal/library/formula/analyze_pasted.go rename to old/backend/internal/library/formula/analyze_pasted.go diff --git a/backend/internal/library/formula/generate_from_formula.go b/old/backend/internal/library/formula/generate_from_formula.go similarity index 100% rename from backend/internal/library/formula/generate_from_formula.go rename to old/backend/internal/library/formula/generate_from_formula.go diff --git a/backend/internal/library/formula/generate_from_formula_test.go b/old/backend/internal/library/formula/generate_from_formula_test.go similarity index 100% rename from backend/internal/library/formula/generate_from_formula_test.go rename to old/backend/internal/library/formula/generate_from_formula_test.go diff --git a/backend/internal/library/formula/jsonflex.go b/old/backend/internal/library/formula/jsonflex.go similarity index 100% rename from backend/internal/library/formula/jsonflex.go rename to old/backend/internal/library/formula/jsonflex.go diff --git a/backend/internal/library/formula/jsonflex_test.go b/old/backend/internal/library/formula/jsonflex_test.go similarity index 100% rename from backend/internal/library/formula/jsonflex_test.go rename to old/backend/internal/library/formula/jsonflex_test.go diff --git a/backend/internal/library/knowledge/bootstrap.go b/old/backend/internal/library/knowledge/bootstrap.go similarity index 100% rename from backend/internal/library/knowledge/bootstrap.go rename to old/backend/internal/library/knowledge/bootstrap.go diff --git a/backend/internal/library/knowledge/bootstrap_test.go b/old/backend/internal/library/knowledge/bootstrap_test.go similarity index 100% rename from backend/internal/library/knowledge/bootstrap_test.go rename to old/backend/internal/library/knowledge/bootstrap_test.go diff --git a/backend/internal/library/knowledge/brave_collect.go b/old/backend/internal/library/knowledge/brave_collect.go similarity index 100% rename from backend/internal/library/knowledge/brave_collect.go rename to old/backend/internal/library/knowledge/brave_collect.go diff --git a/backend/internal/library/knowledge/brave_collect_test.go b/old/backend/internal/library/knowledge/brave_collect_test.go similarity index 100% rename from backend/internal/library/knowledge/brave_collect_test.go rename to old/backend/internal/library/knowledge/brave_collect_test.go diff --git a/backend/internal/library/knowledge/expand_strategy.go b/old/backend/internal/library/knowledge/expand_strategy.go similarity index 100% rename from backend/internal/library/knowledge/expand_strategy.go rename to old/backend/internal/library/knowledge/expand_strategy.go diff --git a/backend/internal/library/knowledge/expand_strategy_test.go b/old/backend/internal/library/knowledge/expand_strategy_test.go similarity index 100% rename from backend/internal/library/knowledge/expand_strategy_test.go rename to old/backend/internal/library/knowledge/expand_strategy_test.go diff --git a/backend/internal/library/knowledge/graph.go b/old/backend/internal/library/knowledge/graph.go similarity index 100% rename from backend/internal/library/knowledge/graph.go rename to old/backend/internal/library/knowledge/graph.go diff --git a/backend/internal/library/knowledge/graph_rescore.go b/old/backend/internal/library/knowledge/graph_rescore.go similarity index 100% rename from backend/internal/library/knowledge/graph_rescore.go rename to old/backend/internal/library/knowledge/graph_rescore.go diff --git a/backend/internal/library/knowledge/graph_selection_test.go b/old/backend/internal/library/knowledge/graph_selection_test.go similarity index 100% rename from backend/internal/library/knowledge/graph_selection_test.go rename to old/backend/internal/library/knowledge/graph_selection_test.go diff --git a/backend/internal/library/knowledge/patrol_input.go b/old/backend/internal/library/knowledge/patrol_input.go similarity index 100% rename from backend/internal/library/knowledge/patrol_input.go rename to old/backend/internal/library/knowledge/patrol_input.go diff --git a/backend/internal/library/knowledge/patrol_phrase.go b/old/backend/internal/library/knowledge/patrol_phrase.go similarity index 100% rename from backend/internal/library/knowledge/patrol_phrase.go rename to old/backend/internal/library/knowledge/patrol_phrase.go diff --git a/backend/internal/library/knowledge/patrol_phrase_test.go b/old/backend/internal/library/knowledge/patrol_phrase_test.go similarity index 100% rename from backend/internal/library/knowledge/patrol_phrase_test.go rename to old/backend/internal/library/knowledge/patrol_phrase_test.go diff --git a/backend/internal/library/knowledge/patrol_rank.go b/old/backend/internal/library/knowledge/patrol_rank.go similarity index 100% rename from backend/internal/library/knowledge/patrol_rank.go rename to old/backend/internal/library/knowledge/patrol_rank.go diff --git a/backend/internal/library/knowledge/patrol_rank_test.go b/old/backend/internal/library/knowledge/patrol_rank_test.go similarity index 100% rename from backend/internal/library/knowledge/patrol_rank_test.go rename to old/backend/internal/library/knowledge/patrol_rank_test.go diff --git a/backend/internal/library/knowledge/patrol_resolve.go b/old/backend/internal/library/knowledge/patrol_resolve.go similarity index 100% rename from backend/internal/library/knowledge/patrol_resolve.go rename to old/backend/internal/library/knowledge/patrol_resolve.go diff --git a/backend/internal/library/knowledge/patrol_resolve_test.go b/old/backend/internal/library/knowledge/patrol_resolve_test.go similarity index 100% rename from backend/internal/library/knowledge/patrol_resolve_test.go rename to old/backend/internal/library/knowledge/patrol_resolve_test.go diff --git a/backend/internal/library/knowledge/patrol_search.go b/old/backend/internal/library/knowledge/patrol_search.go similarity index 100% rename from backend/internal/library/knowledge/patrol_search.go rename to old/backend/internal/library/knowledge/patrol_search.go diff --git a/backend/internal/library/knowledge/patrol_search_test.go b/old/backend/internal/library/knowledge/patrol_search_test.go similarity index 100% rename from backend/internal/library/knowledge/patrol_search_test.go rename to old/backend/internal/library/knowledge/patrol_search_test.go diff --git a/backend/internal/library/knowledge/patrol_tags.go b/old/backend/internal/library/knowledge/patrol_tags.go similarity index 100% rename from backend/internal/library/knowledge/patrol_tags.go rename to old/backend/internal/library/knowledge/patrol_tags.go diff --git a/backend/internal/library/knowledge/patrol_tags_test.go b/old/backend/internal/library/knowledge/patrol_tags_test.go similarity index 100% rename from backend/internal/library/knowledge/patrol_tags_test.go rename to old/backend/internal/library/knowledge/patrol_tags_test.go diff --git a/backend/internal/library/knowledge/quality.go b/old/backend/internal/library/knowledge/quality.go similarity index 100% rename from backend/internal/library/knowledge/quality.go rename to old/backend/internal/library/knowledge/quality.go diff --git a/backend/internal/library/knowledge/quality_test.go b/old/backend/internal/library/knowledge/quality_test.go similarity index 100% rename from backend/internal/library/knowledge/quality_test.go rename to old/backend/internal/library/knowledge/quality_test.go diff --git a/backend/internal/library/knowledge/queries.go b/old/backend/internal/library/knowledge/queries.go similarity index 100% rename from backend/internal/library/knowledge/queries.go rename to old/backend/internal/library/knowledge/queries.go diff --git a/backend/internal/library/knowledge/queries_test.go b/old/backend/internal/library/knowledge/queries_test.go similarity index 100% rename from backend/internal/library/knowledge/queries_test.go rename to old/backend/internal/library/knowledge/queries_test.go diff --git a/backend/internal/library/knowledge/research_map_fit.go b/old/backend/internal/library/knowledge/research_map_fit.go similarity index 100% rename from backend/internal/library/knowledge/research_map_fit.go rename to old/backend/internal/library/knowledge/research_map_fit.go diff --git a/backend/internal/library/knowledge/search_keyword_shape.go b/old/backend/internal/library/knowledge/search_keyword_shape.go similarity index 100% rename from backend/internal/library/knowledge/search_keyword_shape.go rename to old/backend/internal/library/knowledge/search_keyword_shape.go diff --git a/backend/internal/library/knowledge/search_keyword_shape_test.go b/old/backend/internal/library/knowledge/search_keyword_shape_test.go similarity index 100% rename from backend/internal/library/knowledge/search_keyword_shape_test.go rename to old/backend/internal/library/knowledge/search_keyword_shape_test.go diff --git a/backend/internal/library/knowledge/semantic_fit.go b/old/backend/internal/library/knowledge/semantic_fit.go similarity index 100% rename from backend/internal/library/knowledge/semantic_fit.go rename to old/backend/internal/library/knowledge/semantic_fit.go diff --git a/backend/internal/library/knowledge/semantic_fit_test.go b/old/backend/internal/library/knowledge/semantic_fit_test.go similarity index 100% rename from backend/internal/library/knowledge/semantic_fit_test.go rename to old/backend/internal/library/knowledge/semantic_fit_test.go diff --git a/backend/internal/library/knowledge/synth.go b/old/backend/internal/library/knowledge/synth.go similarity index 100% rename from backend/internal/library/knowledge/synth.go rename to old/backend/internal/library/knowledge/synth.go diff --git a/backend/internal/library/knowledge/synth_test.go b/old/backend/internal/library/knowledge/synth_test.go similarity index 100% rename from backend/internal/library/knowledge/synth_test.go rename to old/backend/internal/library/knowledge/synth_test.go diff --git a/backend/internal/library/llmjson/sanitize.go b/old/backend/internal/library/llmjson/sanitize.go similarity index 100% rename from backend/internal/library/llmjson/sanitize.go rename to old/backend/internal/library/llmjson/sanitize.go diff --git a/backend/internal/library/llmjson/sanitize_test.go b/old/backend/internal/library/llmjson/sanitize_test.go similarity index 100% rename from backend/internal/library/llmjson/sanitize_test.go rename to old/backend/internal/library/llmjson/sanitize_test.go diff --git a/backend/internal/library/mailer/smtp.go b/old/backend/internal/library/mailer/smtp.go similarity index 100% rename from backend/internal/library/mailer/smtp.go rename to old/backend/internal/library/mailer/smtp.go diff --git a/backend/internal/library/matrix/generate.go b/old/backend/internal/library/matrix/generate.go similarity index 100% rename from backend/internal/library/matrix/generate.go rename to old/backend/internal/library/matrix/generate.go diff --git a/backend/internal/library/matrix/generate_test.go b/old/backend/internal/library/matrix/generate_test.go similarity index 100% rename from backend/internal/library/matrix/generate_test.go rename to old/backend/internal/library/matrix/generate_test.go diff --git a/backend/internal/library/matrix/reference.go b/old/backend/internal/library/matrix/reference.go similarity index 100% rename from backend/internal/library/matrix/reference.go rename to old/backend/internal/library/matrix/reference.go diff --git a/backend/internal/library/matrix/copy_generate.go b/old/backend/internal/library/matrix/research_map.go similarity index 59% rename from backend/internal/library/matrix/copy_generate.go rename to old/backend/internal/library/matrix/research_map.go index 3ac92da..6dda1af 100644 --- a/backend/internal/library/matrix/copy_generate.go +++ b/old/backend/internal/library/matrix/research_map.go @@ -1,59 +1,8 @@ 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") -} +import "strings" +// SelectKnowledgeItems prefers explicit selections, otherwise falls back to all items. func SelectKnowledgeItems(selected, all []string) []string { source := selected if len(source) == 0 { @@ -89,6 +38,7 @@ func dedupeKnowledgeLines(lines []string) []string { return out } +// FormatCopyResearchMapBlock renders a compact research-map block for LLM prompts. func FormatCopyResearchMapBlock(audience, goal string, questions, pillars, exclusions []string, knowledgeItems ...[]string) string { var b strings.Builder if audience = strings.TrimSpace(audience); audience != "" { diff --git a/backend/internal/library/matrix/samples.go b/old/backend/internal/library/matrix/samples.go similarity index 100% rename from backend/internal/library/matrix/samples.go rename to old/backend/internal/library/matrix/samples.go diff --git a/backend/internal/library/mongo/brand_scope.go b/old/backend/internal/library/mongo/brand_scope.go similarity index 100% rename from backend/internal/library/mongo/brand_scope.go rename to old/backend/internal/library/mongo/brand_scope.go diff --git a/backend/internal/library/mongo/client.go b/old/backend/internal/library/mongo/client.go similarity index 100% rename from backend/internal/library/mongo/client.go rename to old/backend/internal/library/mongo/client.go diff --git a/backend/internal/library/mongo/ensure_index.go b/old/backend/internal/library/mongo/ensure_index.go similarity index 100% rename from backend/internal/library/mongo/ensure_index.go rename to old/backend/internal/library/mongo/ensure_index.go diff --git a/backend/internal/library/outreach/draft_context.go b/old/backend/internal/library/outreach/draft_context.go similarity index 100% rename from backend/internal/library/outreach/draft_context.go rename to old/backend/internal/library/outreach/draft_context.go diff --git a/backend/internal/library/outreach/draft_format.go b/old/backend/internal/library/outreach/draft_format.go similarity index 100% rename from backend/internal/library/outreach/draft_format.go rename to old/backend/internal/library/outreach/draft_format.go diff --git a/backend/internal/library/outreach/draft_format_test.go b/old/backend/internal/library/outreach/draft_format_test.go similarity index 100% rename from backend/internal/library/outreach/draft_format_test.go rename to old/backend/internal/library/outreach/draft_format_test.go diff --git a/backend/internal/library/outreach/generate.go b/old/backend/internal/library/outreach/generate.go similarity index 100% rename from backend/internal/library/outreach/generate.go rename to old/backend/internal/library/outreach/generate.go diff --git a/backend/internal/library/outreach/generate_draft.go b/old/backend/internal/library/outreach/generate_draft.go similarity index 100% rename from backend/internal/library/outreach/generate_draft.go rename to old/backend/internal/library/outreach/generate_draft.go diff --git a/backend/internal/library/outreach/generate_test.go b/old/backend/internal/library/outreach/generate_test.go similarity index 100% rename from backend/internal/library/outreach/generate_test.go rename to old/backend/internal/library/outreach/generate_test.go diff --git a/backend/internal/library/outreach/persona_voice.go b/old/backend/internal/library/outreach/persona_voice.go similarity index 100% rename from backend/internal/library/outreach/persona_voice.go rename to old/backend/internal/library/outreach/persona_voice.go diff --git a/backend/internal/library/outreach/persona_voice_test.go b/old/backend/internal/library/outreach/persona_voice_test.go similarity index 100% rename from backend/internal/library/outreach/persona_voice_test.go rename to old/backend/internal/library/outreach/persona_voice_test.go diff --git a/backend/internal/library/outreach/previous_drafts.go b/old/backend/internal/library/outreach/previous_drafts.go similarity index 100% rename from backend/internal/library/outreach/previous_drafts.go rename to old/backend/internal/library/outreach/previous_drafts.go diff --git a/backend/internal/library/outreach/previous_drafts_test.go b/old/backend/internal/library/outreach/previous_drafts_test.go similarity index 100% rename from backend/internal/library/outreach/previous_drafts_test.go rename to old/backend/internal/library/outreach/previous_drafts_test.go diff --git a/backend/internal/library/ownpost/post_formula.go b/old/backend/internal/library/ownpost/post_formula.go similarity index 100% rename from backend/internal/library/ownpost/post_formula.go rename to old/backend/internal/library/ownpost/post_formula.go diff --git a/backend/internal/library/ownpost/post_formula_test.go b/old/backend/internal/library/ownpost/post_formula_test.go similarity index 100% rename from backend/internal/library/ownpost/post_formula_test.go rename to old/backend/internal/library/ownpost/post_formula_test.go diff --git a/backend/internal/library/ownpost/reply_draft.go b/old/backend/internal/library/ownpost/reply_draft.go similarity index 100% rename from backend/internal/library/ownpost/reply_draft.go rename to old/backend/internal/library/ownpost/reply_draft.go diff --git a/backend/internal/library/ownpost/reply_draft_test.go b/old/backend/internal/library/ownpost/reply_draft_test.go similarity index 100% rename from backend/internal/library/ownpost/reply_draft_test.go rename to old/backend/internal/library/ownpost/reply_draft_test.go diff --git a/backend/internal/library/permmatch/match.go b/old/backend/internal/library/permmatch/match.go similarity index 100% rename from backend/internal/library/permmatch/match.go rename to old/backend/internal/library/permmatch/match.go diff --git a/backend/internal/library/permmatch/match_test.go b/old/backend/internal/library/permmatch/match_test.go similarity index 100% rename from backend/internal/library/permmatch/match_test.go rename to old/backend/internal/library/permmatch/match_test.go diff --git a/backend/internal/library/personacopy/formula_draft.go b/old/backend/internal/library/personacopy/formula_draft.go similarity index 100% rename from backend/internal/library/personacopy/formula_draft.go rename to old/backend/internal/library/personacopy/formula_draft.go diff --git a/backend/internal/library/personacopy/rewrite_draft.go b/old/backend/internal/library/personacopy/rewrite_draft.go similarity index 100% rename from backend/internal/library/personacopy/rewrite_draft.go rename to old/backend/internal/library/personacopy/rewrite_draft.go diff --git a/backend/internal/library/personacopy/topic_matrix.go b/old/backend/internal/library/personacopy/topic_matrix.go similarity index 100% rename from backend/internal/library/personacopy/topic_matrix.go rename to old/backend/internal/library/personacopy/topic_matrix.go diff --git a/backend/internal/library/placement/ai_generate.go b/old/backend/internal/library/placement/ai_generate.go similarity index 100% rename from backend/internal/library/placement/ai_generate.go rename to old/backend/internal/library/placement/ai_generate.go diff --git a/backend/internal/library/placement/capabilities.go b/old/backend/internal/library/placement/capabilities.go similarity index 100% rename from backend/internal/library/placement/capabilities.go rename to old/backend/internal/library/placement/capabilities.go diff --git a/backend/internal/library/placement/context.go b/old/backend/internal/library/placement/context.go similarity index 100% rename from backend/internal/library/placement/context.go rename to old/backend/internal/library/placement/context.go diff --git a/backend/internal/library/placement/context_api_only_test.go b/old/backend/internal/library/placement/context_api_only_test.go similarity index 100% rename from backend/internal/library/placement/context_api_only_test.go rename to old/backend/internal/library/placement/context_api_only_test.go diff --git a/backend/internal/library/placement/context_prompt.go b/old/backend/internal/library/placement/context_prompt.go similarity index 100% rename from backend/internal/library/placement/context_prompt.go rename to old/backend/internal/library/placement/context_prompt.go diff --git a/backend/internal/library/placement/context_prompt_test.go b/old/backend/internal/library/placement/context_prompt_test.go similarity index 100% rename from backend/internal/library/placement/context_prompt_test.go rename to old/backend/internal/library/placement/context_prompt_test.go diff --git a/backend/internal/library/placement/crawler_exec.go b/old/backend/internal/library/placement/crawler_exec.go similarity index 100% rename from backend/internal/library/placement/crawler_exec.go rename to old/backend/internal/library/placement/crawler_exec.go diff --git a/backend/internal/library/placement/crawler_exec_test.go b/old/backend/internal/library/placement/crawler_exec_test.go similarity index 100% rename from backend/internal/library/placement/crawler_exec_test.go rename to old/backend/internal/library/placement/crawler_exec_test.go diff --git a/backend/internal/library/placement/crawler_polite.go b/old/backend/internal/library/placement/crawler_polite.go similarity index 100% rename from backend/internal/library/placement/crawler_polite.go rename to old/backend/internal/library/placement/crawler_polite.go diff --git a/backend/internal/library/placement/discover.go b/old/backend/internal/library/placement/discover.go similarity index 100% rename from backend/internal/library/placement/discover.go rename to old/backend/internal/library/placement/discover.go diff --git a/backend/internal/library/placement/discover_for_query_test.go b/old/backend/internal/library/placement/discover_for_query_test.go similarity index 100% rename from backend/internal/library/placement/discover_for_query_test.go rename to old/backend/internal/library/placement/discover_for_query_test.go diff --git a/backend/internal/library/placement/discover_route.go b/old/backend/internal/library/placement/discover_route.go similarity index 100% rename from backend/internal/library/placement/discover_route.go rename to old/backend/internal/library/placement/discover_route.go diff --git a/backend/internal/library/placement/discover_route_test.go b/old/backend/internal/library/placement/discover_route_test.go similarity index 100% rename from backend/internal/library/placement/discover_route_test.go rename to old/backend/internal/library/placement/discover_route_test.go diff --git a/backend/internal/library/placement/dual_track.go b/old/backend/internal/library/placement/dual_track.go similarity index 100% rename from backend/internal/library/placement/dual_track.go rename to old/backend/internal/library/placement/dual_track.go diff --git a/backend/internal/library/placement/effective_strategy.go b/old/backend/internal/library/placement/effective_strategy.go similarity index 100% rename from backend/internal/library/placement/effective_strategy.go rename to old/backend/internal/library/placement/effective_strategy.go diff --git a/backend/internal/library/placement/effective_strategy_test.go b/old/backend/internal/library/placement/effective_strategy_test.go similarity index 100% rename from backend/internal/library/placement/effective_strategy_test.go rename to old/backend/internal/library/placement/effective_strategy_test.go diff --git a/backend/internal/library/placement/exclusion.go b/old/backend/internal/library/placement/exclusion.go similarity index 100% rename from backend/internal/library/placement/exclusion.go rename to old/backend/internal/library/placement/exclusion.go diff --git a/backend/internal/library/placement/external_id.go b/old/backend/internal/library/placement/external_id.go similarity index 100% rename from backend/internal/library/placement/external_id.go rename to old/backend/internal/library/placement/external_id.go diff --git a/backend/internal/library/placement/external_id_test.go b/old/backend/internal/library/placement/external_id_test.go similarity index 100% rename from backend/internal/library/placement/external_id_test.go rename to old/backend/internal/library/placement/external_id_test.go diff --git a/backend/internal/library/placement/filter.go b/old/backend/internal/library/placement/filter.go similarity index 100% rename from backend/internal/library/placement/filter.go rename to old/backend/internal/library/placement/filter.go diff --git a/backend/internal/library/placement/filter_test.go b/old/backend/internal/library/placement/filter_test.go similarity index 100% rename from backend/internal/library/placement/filter_test.go rename to old/backend/internal/library/placement/filter_test.go diff --git a/backend/internal/library/placement/media_resolve.go b/old/backend/internal/library/placement/media_resolve.go similarity index 100% rename from backend/internal/library/placement/media_resolve.go rename to old/backend/internal/library/placement/media_resolve.go diff --git a/backend/internal/library/placement/parse_threads.go b/old/backend/internal/library/placement/parse_threads.go similarity index 100% rename from backend/internal/library/placement/parse_threads.go rename to old/backend/internal/library/placement/parse_threads.go diff --git a/backend/internal/library/placement/patrol_queries.go b/old/backend/internal/library/placement/patrol_queries.go similarity index 100% rename from backend/internal/library/placement/patrol_queries.go rename to old/backend/internal/library/placement/patrol_queries.go diff --git a/backend/internal/library/placement/patrol_queries_test.go b/old/backend/internal/library/placement/patrol_queries_test.go similarity index 100% rename from backend/internal/library/placement/patrol_queries_test.go rename to old/backend/internal/library/placement/patrol_queries_test.go diff --git a/backend/internal/library/placement/post_relevance.go b/old/backend/internal/library/placement/post_relevance.go similarity index 100% rename from backend/internal/library/placement/post_relevance.go rename to old/backend/internal/library/placement/post_relevance.go diff --git a/backend/internal/library/placement/post_relevance_test.go b/old/backend/internal/library/placement/post_relevance_test.go similarity index 100% rename from backend/internal/library/placement/post_relevance_test.go rename to old/backend/internal/library/placement/post_relevance_test.go diff --git a/backend/internal/library/placement/product_catalog.go b/old/backend/internal/library/placement/product_catalog.go similarity index 100% rename from backend/internal/library/placement/product_catalog.go rename to old/backend/internal/library/placement/product_catalog.go diff --git a/backend/internal/library/placement/product_context.go b/old/backend/internal/library/placement/product_context.go similarity index 100% rename from backend/internal/library/placement/product_context.go rename to old/backend/internal/library/placement/product_context.go diff --git a/backend/internal/library/placement/product_context_placement_url_test.go b/old/backend/internal/library/placement/product_context_placement_url_test.go similarity index 100% rename from backend/internal/library/placement/product_context_placement_url_test.go rename to old/backend/internal/library/placement/product_context_placement_url_test.go diff --git a/backend/internal/library/placement/product_match.go b/old/backend/internal/library/placement/product_match.go similarity index 100% rename from backend/internal/library/placement/product_match.go rename to old/backend/internal/library/placement/product_match.go diff --git a/backend/internal/library/placement/product_match_test.go b/old/backend/internal/library/placement/product_match_test.go similarity index 100% rename from backend/internal/library/placement/product_match_test.go rename to old/backend/internal/library/placement/product_match_test.go diff --git a/backend/internal/library/placement/query_build.go b/old/backend/internal/library/placement/query_build.go similarity index 100% rename from backend/internal/library/placement/query_build.go rename to old/backend/internal/library/placement/query_build.go diff --git a/backend/internal/library/placement/query_build_test.go b/old/backend/internal/library/placement/query_build_test.go similarity index 100% rename from backend/internal/library/placement/query_build_test.go rename to old/backend/internal/library/placement/query_build_test.go diff --git a/backend/internal/library/placement/recency.go b/old/backend/internal/library/placement/recency.go similarity index 100% rename from backend/internal/library/placement/recency.go rename to old/backend/internal/library/placement/recency.go diff --git a/backend/internal/library/placement/recency_cascade.go b/old/backend/internal/library/placement/recency_cascade.go similarity index 100% rename from backend/internal/library/placement/recency_cascade.go rename to old/backend/internal/library/placement/recency_cascade.go diff --git a/backend/internal/library/placement/recency_cascade_test.go b/old/backend/internal/library/placement/recency_cascade_test.go similarity index 100% rename from backend/internal/library/placement/recency_cascade_test.go rename to old/backend/internal/library/placement/recency_cascade_test.go diff --git a/backend/internal/library/placement/research_map.go b/old/backend/internal/library/placement/research_map.go similarity index 100% rename from backend/internal/library/placement/research_map.go rename to old/backend/internal/library/placement/research_map.go diff --git a/backend/internal/library/placement/research_map_test.go b/old/backend/internal/library/placement/research_map_test.go similarity index 100% rename from backend/internal/library/placement/research_map_test.go rename to old/backend/internal/library/placement/research_map_test.go diff --git a/backend/internal/library/placement/resolve_media_exec.go b/old/backend/internal/library/placement/resolve_media_exec.go similarity index 100% rename from backend/internal/library/placement/resolve_media_exec.go rename to old/backend/internal/library/placement/resolve_media_exec.go diff --git a/backend/internal/library/placement/resolve_reply_target.go b/old/backend/internal/library/placement/resolve_reply_target.go similarity index 100% rename from backend/internal/library/placement/resolve_reply_target.go rename to old/backend/internal/library/placement/resolve_reply_target.go diff --git a/backend/internal/library/placement/resolve_reply_target_test.go b/old/backend/internal/library/placement/resolve_reply_target_test.go similarity index 100% rename from backend/internal/library/placement/resolve_reply_target_test.go rename to old/backend/internal/library/placement/resolve_reply_target_test.go diff --git a/backend/internal/library/placement/scrape_replies.go b/old/backend/internal/library/placement/scrape_replies.go similarity index 100% rename from backend/internal/library/placement/scrape_replies.go rename to old/backend/internal/library/placement/scrape_replies.go diff --git a/backend/internal/library/placement/scrape_replies_test.go b/old/backend/internal/library/placement/scrape_replies_test.go similarity index 100% rename from backend/internal/library/placement/scrape_replies_test.go rename to old/backend/internal/library/placement/scrape_replies_test.go diff --git a/backend/internal/library/placement/source_mode.go b/old/backend/internal/library/placement/source_mode.go similarity index 100% rename from backend/internal/library/placement/source_mode.go rename to old/backend/internal/library/placement/source_mode.go diff --git a/backend/internal/library/placement/source_mode_test.go b/old/backend/internal/library/placement/source_mode_test.go similarity index 100% rename from backend/internal/library/placement/source_mode_test.go rename to old/backend/internal/library/placement/source_mode_test.go diff --git a/backend/internal/library/placement/threads_api.go b/old/backend/internal/library/placement/threads_api.go similarity index 100% rename from backend/internal/library/placement/threads_api.go rename to old/backend/internal/library/placement/threads_api.go diff --git a/backend/internal/library/placement/topic_fit.go b/old/backend/internal/library/placement/topic_fit.go similarity index 100% rename from backend/internal/library/placement/topic_fit.go rename to old/backend/internal/library/placement/topic_fit.go diff --git a/backend/internal/library/prompt/compose.go b/old/backend/internal/library/prompt/compose.go similarity index 90% rename from backend/internal/library/prompt/compose.go rename to old/backend/internal/library/prompt/compose.go index f9417d2..4d5e94d 100644 --- a/backend/internal/library/prompt/compose.go +++ b/old/backend/internal/library/prompt/compose.go @@ -148,24 +148,6 @@ 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/backend/internal/library/prompt/compose_test.go b/old/backend/internal/library/prompt/compose_test.go similarity index 100% rename from backend/internal/library/prompt/compose_test.go rename to old/backend/internal/library/prompt/compose_test.go diff --git a/backend/internal/library/prompt/errors.go b/old/backend/internal/library/prompt/errors.go similarity index 100% rename from backend/internal/library/prompt/errors.go rename to old/backend/internal/library/prompt/errors.go diff --git a/backend/internal/library/prompt/files/ai.chat.system.md b/old/backend/internal/library/prompt/files/ai.chat.system.md similarity index 100% rename from backend/internal/library/prompt/files/ai.chat.system.md rename to old/backend/internal/library/prompt/files/ai.chat.system.md diff --git a/backend/internal/library/prompt/files/ai.islander.system.md b/old/backend/internal/library/prompt/files/ai.islander.system.md similarity index 100% rename from backend/internal/library/prompt/files/ai.islander.system.md rename to old/backend/internal/library/prompt/files/ai.islander.system.md diff --git a/backend/internal/library/prompt/files/knowledge_graph.queries.json b/old/backend/internal/library/prompt/files/knowledge_graph.queries.json similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph.queries.json rename to old/backend/internal/library/prompt/files/knowledge_graph.queries.json diff --git a/backend/internal/library/prompt/files/knowledge_graph.supplemental.md b/old/backend/internal/library/prompt/files/knowledge_graph.supplemental.md similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph.supplemental.md rename to old/backend/internal/library/prompt/files/knowledge_graph.supplemental.md diff --git a/backend/internal/library/prompt/files/knowledge_graph.system.md b/old/backend/internal/library/prompt/files/knowledge_graph.system.md similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph.system.md rename to old/backend/internal/library/prompt/files/knowledge_graph.system.md diff --git a/backend/internal/library/prompt/files/knowledge_graph.user.md b/old/backend/internal/library/prompt/files/knowledge_graph.user.md similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph.user.md rename to old/backend/internal/library/prompt/files/knowledge_graph.user.md diff --git a/backend/internal/library/prompt/files/knowledge_graph_llm.system.md b/old/backend/internal/library/prompt/files/knowledge_graph_llm.system.md similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph_llm.system.md rename to old/backend/internal/library/prompt/files/knowledge_graph_llm.system.md diff --git a/backend/internal/library/prompt/files/knowledge_graph_llm.user.md b/old/backend/internal/library/prompt/files/knowledge_graph_llm.user.md similarity index 100% rename from backend/internal/library/prompt/files/knowledge_graph_llm.user.md rename to old/backend/internal/library/prompt/files/knowledge_graph_llm.user.md diff --git a/backend/internal/library/prompt/files/matrix_placement.system.md b/old/backend/internal/library/prompt/files/matrix_placement.system.md similarity index 100% rename from backend/internal/library/prompt/files/matrix_placement.system.md rename to old/backend/internal/library/prompt/files/matrix_placement.system.md diff --git a/backend/internal/library/prompt/files/matrix_placement.user.md b/old/backend/internal/library/prompt/files/matrix_placement.user.md similarity index 100% rename from backend/internal/library/prompt/files/matrix_placement.user.md rename to old/backend/internal/library/prompt/files/matrix_placement.user.md diff --git a/backend/internal/library/prompt/files/outreach_placement.system.md b/old/backend/internal/library/prompt/files/outreach_placement.system.md similarity index 100% rename from backend/internal/library/prompt/files/outreach_placement.system.md rename to old/backend/internal/library/prompt/files/outreach_placement.system.md diff --git a/backend/internal/library/prompt/files/outreach_placement.user.md b/old/backend/internal/library/prompt/files/outreach_placement.user.md similarity index 100% rename from backend/internal/library/prompt/files/outreach_placement.user.md rename to old/backend/internal/library/prompt/files/outreach_placement.user.md diff --git a/backend/internal/library/prompt/files/overlay.md b/old/backend/internal/library/prompt/files/overlay.md similarity index 100% rename from backend/internal/library/prompt/files/overlay.md rename to old/backend/internal/library/prompt/files/overlay.md diff --git a/backend/internal/library/prompt/files/style8d.schema.md b/old/backend/internal/library/prompt/files/style8d.schema.md similarity index 100% rename from backend/internal/library/prompt/files/style8d.schema.md rename to old/backend/internal/library/prompt/files/style8d.schema.md diff --git a/backend/internal/library/prompt/files/style8d.system.md b/old/backend/internal/library/prompt/files/style8d.system.md similarity index 100% rename from backend/internal/library/prompt/files/style8d.system.md rename to old/backend/internal/library/prompt/files/style8d.system.md diff --git a/backend/internal/library/prompt/registry.go b/old/backend/internal/library/prompt/registry.go similarity index 83% rename from backend/internal/library/prompt/registry.go rename to old/backend/internal/library/prompt/registry.go index 43982ef..b9009d6 100644 --- a/backend/internal/library/prompt/registry.go +++ b/old/backend/internal/library/prompt/registry.go @@ -23,10 +23,8 @@ const ( fileKnowledgeGraphQueries = "files/knowledge_graph.queries.json" fileOutreachPlacementSystem = "files/outreach_placement.system.md" 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" + fileMatrixPlacementSystem = "files/matrix_placement.system.md" + fileMatrixPlacementUser = "files/matrix_placement.user.md" ) // Keys identify prompt slots loaded from internal/library/prompt/files/*.md. @@ -43,10 +41,8 @@ const ( KeyKnowledgeGraphQueries = "knowledge_graph.queries" KeyOutreachPlacementSystem = "outreach_placement.system" KeyOutreachPlacementUser = "outreach_placement.user" - KeyMatrixPlacementSystem = "matrix_placement.system" - KeyMatrixPlacementUser = "matrix_placement.user" - KeyMatrixCopySystem = "matrix_copy.system" - KeyMatrixCopyUser = "matrix_copy.user" + KeyMatrixPlacementSystem = "matrix_placement.system" + KeyMatrixPlacementUser = "matrix_placement.user" ) var slotFiles = map[string]string{ @@ -61,10 +57,8 @@ var slotFiles = map[string]string{ KeyKnowledgeGraphSupplemental: fileKnowledgeGraphSupplemental, KeyOutreachPlacementSystem: fileOutreachPlacementSystem, KeyOutreachPlacementUser: fileOutreachPlacementUser, - KeyMatrixPlacementSystem: fileMatrixPlacementSystem, - KeyMatrixPlacementUser: fileMatrixPlacementUser, - KeyMatrixCopySystem: fileMatrixCopySystem, - KeyMatrixCopyUser: fileMatrixCopyUser, + KeyMatrixPlacementSystem: fileMatrixPlacementSystem, + KeyMatrixPlacementUser: fileMatrixPlacementUser, } var ( diff --git a/backend/internal/library/publishschedule/slots.go b/old/backend/internal/library/publishschedule/slots.go similarity index 100% rename from backend/internal/library/publishschedule/slots.go rename to old/backend/internal/library/publishschedule/slots.go diff --git a/backend/internal/library/publishschedule/slots_test.go b/old/backend/internal/library/publishschedule/slots_test.go similarity index 100% rename from backend/internal/library/publishschedule/slots_test.go rename to old/backend/internal/library/publishschedule/slots_test.go diff --git a/backend/internal/library/ratelimit/token_bucket.go b/old/backend/internal/library/ratelimit/token_bucket.go similarity index 100% rename from backend/internal/library/ratelimit/token_bucket.go rename to old/backend/internal/library/ratelimit/token_bucket.go diff --git a/backend/internal/library/redact/redact.go b/old/backend/internal/library/redact/redact.go similarity index 100% rename from backend/internal/library/redact/redact.go rename to old/backend/internal/library/redact/redact.go diff --git a/backend/internal/library/redact/redact_test.go b/old/backend/internal/library/redact/redact_test.go similarity index 100% rename from backend/internal/library/redact/redact_test.go rename to old/backend/internal/library/redact/redact_test.go diff --git a/backend/internal/library/redis/client.go b/old/backend/internal/library/redis/client.go similarity index 100% rename from backend/internal/library/redis/client.go rename to old/backend/internal/library/redis/client.go diff --git a/old/backend/internal/library/storage/asset_ref.go b/old/backend/internal/library/storage/asset_ref.go new file mode 100644 index 0000000..03d3302 --- /dev/null +++ b/old/backend/internal/library/storage/asset_ref.go @@ -0,0 +1,44 @@ +package storage + +import ( + "net/url" + "strings" +) + +// NormalizeObjectKey converts stored avatar/object refs to a stable S3 object key. +// Accepts legacy full URLs, /{bucket}/... paths, or raw keys like avatars/{tenant}/{uid}.png. +func NormalizeObjectKey(bucket, ref string) string { + ref = strings.TrimSpace(ref) + if ref == "" || bucket == "" { + return ref + } + bucket = strings.Trim(bucket, "/") + ref = strings.Trim(ref, "/") + + if strings.Contains(ref, "://") { + parsed, err := url.Parse(ref) + if err != nil { + return ref + } + ref = strings.Trim(parsed.Path, "/") + } + + prefix := bucket + "/" + if strings.HasPrefix(ref, prefix) { + return strings.TrimPrefix(ref, prefix) + } + if strings.HasPrefix(ref, "/"+prefix) { + return strings.TrimPrefix(strings.TrimPrefix(ref, "/"), prefix) + } + return ref +} + +// PublicObjectPath builds a same-origin browser path: /{bucket}/{key}. +func PublicObjectPath(bucket, key string) string { + bucket = strings.Trim(strings.TrimSpace(bucket), "/") + key = strings.Trim(strings.TrimSpace(key), "/") + if bucket == "" || key == "" { + return "" + } + return "/" + bucket + "/" + key +} diff --git a/old/backend/internal/library/storage/asset_ref_test.go b/old/backend/internal/library/storage/asset_ref_test.go new file mode 100644 index 0000000..6c535ce --- /dev/null +++ b/old/backend/internal/library/storage/asset_ref_test.go @@ -0,0 +1,34 @@ +package storage + +import "testing" + +func TestNormalizeObjectKey(t *testing.T) { + bucket := "haixun-assets" + key := "avatars/default/uid.png" + + cases := []struct { + name string + ref string + want string + }{ + {name: "raw key", ref: key, want: key}, + {name: "public path", ref: "/haixun-assets/" + key, want: key}, + {name: "loopback url", ref: "http://127.0.0.1:9000/haixun-assets/" + key, want: key}, + {name: "public site url", ref: "https://threads-tool.30cm.net/haixun-assets/" + key, want: key}, + {name: "empty", ref: "", want: ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := NormalizeObjectKey(bucket, tc.ref); got != tc.want { + t.Fatalf("NormalizeObjectKey(%q) = %q, want %q", tc.ref, got, tc.want) + } + }) + } +} + +func TestPublicObjectPath(t *testing.T) { + if got := PublicObjectPath("haixun-assets", "avatars/default/uid.png"); got != "/haixun-assets/avatars/default/uid.png" { + t.Fatalf("unexpected path: %q", got) + } +} diff --git a/old/backend/internal/library/storage/ephemeral_asset.go b/old/backend/internal/library/storage/ephemeral_asset.go new file mode 100644 index 0000000..ba50e0f --- /dev/null +++ b/old/backend/internal/library/storage/ephemeral_asset.go @@ -0,0 +1,132 @@ +package storage + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/url" + "strings" + "sync" + "time" +) + +const EphemeralKeyPrefix = "ephemeral/" + +const defaultEphemeralTTL = 15 * time.Minute + +type ephemeralEntry struct { + data []byte + contentType string + expiresAt time.Time +} + +// EphemeralAssetStore holds short-lived blobs for Threads image_url fetch (reply direct transfer). +type EphemeralAssetStore struct { + mu sync.RWMutex + items map[string]*ephemeralEntry +} + +func NewEphemeralAssetStore() *EphemeralAssetStore { + return &EphemeralAssetStore{items: make(map[string]*ephemeralEntry)} +} + +var defaultEphemeralStore = NewEphemeralAssetStore() + +func DefaultEphemeralAssetStore() *EphemeralAssetStore { + return defaultEphemeralStore +} + +func (s *EphemeralAssetStore) Register(data []byte, contentType string, ttl time.Duration) (string, error) { + if s == nil { + return "", fmt.Errorf("ephemeral store is not configured") + } + if len(data) == 0 { + return "", fmt.Errorf("ephemeral asset is empty") + } + if ttl <= 0 { + ttl = defaultEphemeralTTL + } + id, err := randomEphemeralID() + if err != nil { + return "", err + } + s.mu.Lock() + s.items[id] = &ephemeralEntry{ + data: append([]byte(nil), data...), + contentType: strings.TrimSpace(contentType), + expiresAt: time.Now().Add(ttl), + } + s.mu.Unlock() + return EphemeralKeyPrefix + id, nil +} + +func (s *EphemeralAssetStore) Get(key string) ([]byte, string, bool) { + if s == nil { + return nil, "", false + } + id, ok := parseEphemeralID(key) + if !ok { + return nil, "", false + } + s.mu.RLock() + entry := s.items[id] + s.mu.RUnlock() + if entry == nil || time.Now().After(entry.expiresAt) { + s.Delete(key) + return nil, "", false + } + contentType := entry.contentType + if contentType == "" { + contentType = "application/octet-stream" + } + return append([]byte(nil), entry.data...), contentType, true +} + +func (s *EphemeralAssetStore) Delete(key string) { + if s == nil { + return + } + id, ok := parseEphemeralID(key) + if !ok { + return + } + s.mu.Lock() + delete(s.items, id) + s.mu.Unlock() +} + +func parseEphemeralID(key string) (string, bool) { + key = strings.TrimSpace(key) + if !strings.HasPrefix(key, EphemeralKeyPrefix) { + return "", false + } + id := strings.TrimSpace(strings.TrimPrefix(key, EphemeralKeyPrefix)) + if id == "" { + return "", false + } + return id, true +} + +func randomEphemeralID() (string, error) { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} + +// BuildEphemeralPublicURL turns a staged ephemeral key into a Threads-fetchable URL. +func BuildEphemeralPublicURL(publicBaseURL, ephemeralKey string) (string, error) { + ephemeralKey = strings.TrimSpace(ephemeralKey) + if !strings.HasPrefix(ephemeralKey, EphemeralKeyPrefix) { + return "", fmt.Errorf("invalid ephemeral asset key") + } + base := strings.TrimRight(strings.TrimSpace(publicBaseURL), "/") + if base == "" { + return "", fmt.Errorf("HAIXUN_STORAGE_S3_PUBLIC_BASE_URL is required for Threads image publish") + } + if strings.HasSuffix(base, "/public/assets") { + return base + "?key=" + url.QueryEscape(ephemeralKey), nil + } + return base + "/" + ephemeralKey, nil +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/ephemeral_asset_test.go b/old/backend/internal/library/storage/ephemeral_asset_test.go new file mode 100644 index 0000000..1e3cf88 --- /dev/null +++ b/old/backend/internal/library/storage/ephemeral_asset_test.go @@ -0,0 +1,30 @@ +package storage + +import "testing" + +func TestEphemeralAssetStoreRegisterGetDelete(t *testing.T) { + store := NewEphemeralAssetStore() + key, err := store.Register([]byte("png"), "image/png", 0) + if err != nil { + t.Fatalf("Register: %v", err) + } + data, ct, ok := store.Get(key) + if !ok || ct != "image/png" || string(data) != "png" { + t.Fatalf("Get() = %q %q %v", data, ct, ok) + } + store.Delete(key) + if _, _, ok := store.Get(key); ok { + t.Fatal("expected deleted") + } +} + +func TestBuildEphemeralPublicURL(t *testing.T) { + url, err := BuildEphemeralPublicURL("https://example.com/api/v1/public/assets", EphemeralKeyPrefix+"abc") + if err != nil { + t.Fatalf("BuildEphemeralPublicURL: %v", err) + } + want := "https://example.com/api/v1/public/assets?key=ephemeral%2Fabc" + if url != want { + t.Fatalf("got %q want %q", url, want) + } +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/public_asset.go b/old/backend/internal/library/storage/public_asset.go new file mode 100644 index 0000000..47167cc --- /dev/null +++ b/old/backend/internal/library/storage/public_asset.go @@ -0,0 +1,19 @@ +package storage + +import "strings" + +const AvatarKeyPrefix = "avatars/" + +// IsPublicAssetKey reports whether an object key may be served without auth. +func IsPublicAssetKey(key string) bool { + key = strings.TrimSpace(key) + if key == "" { + return false + } + if strings.Contains(key, "..") { + return false + } + return strings.HasPrefix(key, PublishAttachmentKeyPrefix) || + strings.HasPrefix(key, AvatarKeyPrefix) || + strings.HasPrefix(key, EphemeralKeyPrefix) +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/public_asset_test.go b/old/backend/internal/library/storage/public_asset_test.go new file mode 100644 index 0000000..e23bcb4 --- /dev/null +++ b/old/backend/internal/library/storage/public_asset_test.go @@ -0,0 +1,22 @@ +package storage + +import "testing" + +func TestIsPublicAssetKey(t *testing.T) { + cases := []struct { + key string + want bool + }{ + {"publish-attachments/t1/a1/x.png", true}, + {"ephemeral/abc123", true}, + {"avatars/default/u.png", true}, + {"secrets/token.txt", false}, + {"publish-attachments/../etc/passwd", false}, + {"", false}, + } + for _, tc := range cases { + if got := IsPublicAssetKey(tc.key); got != tc.want { + t.Fatalf("IsPublicAssetKey(%q) = %v, want %v", tc.key, got, tc.want) + } + } +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/publish_attachment.go b/old/backend/internal/library/storage/publish_attachment.go new file mode 100644 index 0000000..49fdd73 --- /dev/null +++ b/old/backend/internal/library/storage/publish_attachment.go @@ -0,0 +1,68 @@ +package storage + +import ( + "context" + "fmt" + "strings" +) + +const PublishAttachmentKeyPrefix = "publish-attachments/" + +const MaxPublishAttachments = 20 + +func NormalizeImageKeys(single string, list []string) []string { + out := make([]string, 0, len(list)+1) + seen := make(map[string]struct{}, len(list)+1) + add := func(key string) { + key = strings.TrimSpace(key) + if key == "" { + return + } + if _, ok := seen[key]; ok { + return + } + seen[key] = struct{}{} + out = append(out, key) + } + add(single) + for _, key := range list { + add(key) + } + if len(out) > MaxPublishAttachments { + return out[:MaxPublishAttachments] + } + return out +} + +func ValidatePublishAttachmentKey(key string) error { + key = strings.TrimSpace(key) + if key == "" { + return nil + } + if !strings.HasPrefix(key, PublishAttachmentKeyPrefix) { + return fmt.Errorf("invalid publish attachment key") + } + return nil +} + +// PurgePublishAttachment deletes an ephemeral publish attachment; ignores empty/invalid keys. +func PurgePublishAttachment(ctx context.Context, store ObjectDeleter, key string) error { + key = strings.TrimSpace(key) + if key == "" { + return nil + } + if err := ValidatePublishAttachmentKey(key); err != nil { + return nil + } + if store == nil { + return nil + } + return store.DeleteObject(ctx, key) +} + +// PurgePublishAttachments deletes multiple ephemeral publish attachments. +func PurgePublishAttachments(ctx context.Context, store ObjectDeleter, keys []string) { + for _, key := range keys { + _ = PurgePublishAttachment(ctx, store, key) + } +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/publish_attachment_test.go b/old/backend/internal/library/storage/publish_attachment_test.go new file mode 100644 index 0000000..cd780f0 --- /dev/null +++ b/old/backend/internal/library/storage/publish_attachment_test.go @@ -0,0 +1,42 @@ +package storage + +import ( + "context" + "testing" +) + +type fakeDeleter struct { + deleted []string +} + +func (f *fakeDeleter) DeleteObject(_ context.Context, key string) error { + f.deleted = append(f.deleted, key) + return nil +} + +func TestPurgePublishAttachment(t *testing.T) { + ctx := context.Background() + deleter := &fakeDeleter{} + + if err := PurgePublishAttachment(ctx, deleter, ""); err != nil { + t.Fatalf("empty key: %v", err) + } + if len(deleter.deleted) != 0 { + t.Fatalf("expected no delete for empty key") + } + + if err := PurgePublishAttachment(ctx, deleter, "avatars/foo.png"); err != nil { + t.Fatalf("invalid key: %v", err) + } + if len(deleter.deleted) != 0 { + t.Fatalf("expected no delete for non-publish key") + } + + key := "publish-attachments/t1/a1/test.png" + if err := PurgePublishAttachment(ctx, deleter, key); err != nil { + t.Fatalf("purge: %v", err) + } + if len(deleter.deleted) != 1 || deleter.deleted[0] != key { + t.Fatalf("deleted = %v, want [%s]", deleter.deleted, key) + } +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/s3.go b/old/backend/internal/library/storage/s3.go new file mode 100644 index 0000000..2fc80c3 --- /dev/null +++ b/old/backend/internal/library/storage/s3.go @@ -0,0 +1,173 @@ +package storage + +import ( + "context" + "fmt" + "io" + "mime" + "net/url" + "path" + "strings" + + "haixun-backend/internal/config" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type Uploader interface { + Upload(ctx context.Context, key, contentType string, body io.Reader) (string, error) +} + +// PublicURLResolver turns stored object keys into externally reachable URLs (Threads image publish). +type PublicURLResolver interface { + ResolvePublicURL(key string) (string, error) +} + +// ObjectDeleter removes stored objects by key. +type ObjectDeleter interface { + DeleteObject(ctx context.Context, key string) error +} + +// ObjectReader fetches stored objects by key. +type ObjectReader interface { + GetObject(ctx context.Context, key string) (io.ReadCloser, string, error) +} + +// PublishAttachmentStore resolves public URLs and deletes ephemeral publish attachments. +type PublishAttachmentStore interface { + PublicURLResolver + ObjectDeleter +} + +type S3Uploader struct { + client *s3.Client + bucket string + publicBaseURL string + bucketReady bool +} + +func NewS3Uploader(ctx context.Context, cfg config.S3Conf) (*S3Uploader, error) { + if strings.TrimSpace(cfg.Bucket) == "" { + return nil, nil + } + awsCfg, err := awsconfig.LoadDefaultConfig(ctx, + awsconfig.WithRegion(cfg.Region), + awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")), + ) + if err != nil { + return nil, err + } + client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + if cfg.Endpoint != "" { + o.BaseEndpoint = aws.String(cfg.Endpoint) + } + o.UsePathStyle = cfg.UsePathStyle + }) + return &S3Uploader{client: client, bucket: cfg.Bucket, publicBaseURL: strings.TrimRight(cfg.PublicBaseURL, "/")}, nil +} + +func (u *S3Uploader) Upload(ctx context.Context, key, contentType string, body io.Reader) (string, error) { + if u == nil || u.client == nil { + return "", fmt.Errorf("s3 uploader is not configured") + } + if !u.bucketReady { + if _, err := u.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(u.bucket)}); err != nil { + if _, createErr := u.client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(u.bucket)}); createErr != nil { + return "", createErr + } + } + policy := fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/*"]}]}`, u.bucket) + _, _ = u.client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{Bucket: aws.String(u.bucket), Policy: aws.String(policy)}) + u.bucketReady = true + } + key = strings.TrimLeft(path.Clean("/"+key), "/") + if contentType == "" { + contentType = mime.TypeByExtension(path.Ext(key)) + } + if contentType == "" { + contentType = "application/octet-stream" + } + _, err := u.client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(u.bucket), + Key: aws.String(key), + Body: body, + ContentType: aws.String(contentType), + }) + if err != nil { + return "", err + } + return key, nil +} + +func (u *S3Uploader) ResolvePublicURL(key string) (string, error) { + if u == nil { + return "", fmt.Errorf("s3 uploader is not configured") + } + key = strings.TrimSpace(key) + if key == "" { + return "", fmt.Errorf("asset key is required") + } + key = NormalizeObjectKey(u.bucket, key) + base := strings.TrimRight(strings.TrimSpace(u.publicBaseURL), "/") + if base == "" { + return "", fmt.Errorf("HAIXUN_STORAGE_S3_PUBLIC_BASE_URL is required for Threads image publish") + } + if strings.HasSuffix(base, "/public/assets") { + return base + "?key=" + url.QueryEscape(key), nil + } + if strings.HasSuffix(base, "/"+u.bucket) { + return base + "/" + key, nil + } + return base + "/" + key, nil +} + +func (u *S3Uploader) GetObject(ctx context.Context, key string) (io.ReadCloser, string, error) { + if u == nil || u.client == nil { + return nil, "", fmt.Errorf("s3 uploader is not configured") + } + key = strings.TrimSpace(key) + if key == "" { + return nil, "", fmt.Errorf("asset key is required") + } + if !IsPublicAssetKey(key) { + return nil, "", fmt.Errorf("asset key is not public") + } + key = NormalizeObjectKey(u.bucket, key) + out, err := u.client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(u.bucket), + Key: aws.String(key), + }) + if err != nil { + return nil, "", err + } + contentType := "" + if out.ContentType != nil { + contentType = strings.TrimSpace(*out.ContentType) + } + if contentType == "" { + contentType = mime.TypeByExtension(path.Ext(key)) + } + if contentType == "" { + contentType = "application/octet-stream" + } + return out.Body, contentType, nil +} + +func (u *S3Uploader) DeleteObject(ctx context.Context, key string) error { + if u == nil || u.client == nil { + return fmt.Errorf("s3 uploader is not configured") + } + key = strings.TrimSpace(key) + if key == "" { + return nil + } + key = NormalizeObjectKey(u.bucket, key) + _, err := u.client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(u.bucket), + Key: aws.String(key), + }) + return err +} diff --git a/old/backend/internal/library/storage/s3_public_url_test.go b/old/backend/internal/library/storage/s3_public_url_test.go new file mode 100644 index 0000000..7b70f32 --- /dev/null +++ b/old/backend/internal/library/storage/s3_public_url_test.go @@ -0,0 +1,34 @@ +package storage + +import "testing" + +func TestS3UploaderResolvePublicURL(t *testing.T) { + u := &S3Uploader{bucket: "haixun-assets", publicBaseURL: "https://example.com/haixun-assets"} + url, err := u.ResolvePublicURL("publish-attachments/t1/a1/test.png") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := "https://example.com/haixun-assets/publish-attachments/t1/a1/test.png" + if url != want { + t.Fatalf("ResolvePublicURL() = %q, want %q", url, want) + } +} + +func TestS3UploaderResolvePublicURLAPIProxy(t *testing.T) { + u := &S3Uploader{bucket: "haixun-assets", publicBaseURL: "https://threads-tool.30cm.net/api/v1/public/assets"} + url, err := u.ResolvePublicURL("publish-attachments/t1/a1/test.png") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := "https://threads-tool.30cm.net/api/v1/public/assets?key=publish-attachments%2Ft1%2Fa1%2Ftest.png" + if url != want { + t.Fatalf("ResolvePublicURL() = %q, want %q", url, want) + } +} + +func TestS3UploaderResolvePublicURLEmptyBase(t *testing.T) { + u := &S3Uploader{bucket: "haixun-assets"} + if _, err := u.ResolvePublicURL("publish-attachments/t1/a1/test.png"); err == nil { + t.Fatal("expected error when public base url is empty") + } +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/threads_image_url.go b/old/backend/internal/library/storage/threads_image_url.go new file mode 100644 index 0000000..aa83828 --- /dev/null +++ b/old/backend/internal/library/storage/threads_image_url.go @@ -0,0 +1,49 @@ +package storage + +import ( + "fmt" + "net" + "net/url" + "strings" +) + +// ValidateThreadsFetchableURL ensures Meta servers can download an image for publish. +func ValidateThreadsFetchableURL(raw string) error { + raw = strings.TrimSpace(raw) + if raw == "" { + return fmt.Errorf("圖片網址不可為空") + } + parsed, err := url.Parse(raw) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return fmt.Errorf("圖片網址格式無效") + } + scheme := strings.ToLower(parsed.Scheme) + if scheme != "https" && scheme != "http" { + return fmt.Errorf("圖片網址必須是 http 或 https") + } + host := strings.ToLower(parsed.Hostname()) + if host == "" { + return fmt.Errorf("圖片網址缺少主機名稱") + } + if isPrivateOrLoopbackHost(host) { + return fmt.Errorf( + "圖片網址 %s 無法被 Threads 伺服器存取(localhost / 內網)。請將 HAIXUN_STORAGE_S3_PUBLIC_BASE_URL 設為公開 HTTPS,例如 https://你的網域/api/v1/public/assets", + raw, + ) + } + if scheme != "https" { + return fmt.Errorf("Threads 附圖網址建議使用 HTTPS(目前為 %s)", scheme) + } + return nil +} + +func isPrivateOrLoopbackHost(host string) bool { + if host == "localhost" { + return true + } + ip := net.ParseIP(host) + if ip == nil { + return false + } + return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() +} \ No newline at end of file diff --git a/old/backend/internal/library/storage/threads_image_url_test.go b/old/backend/internal/library/storage/threads_image_url_test.go new file mode 100644 index 0000000..7e9d057 --- /dev/null +++ b/old/backend/internal/library/storage/threads_image_url_test.go @@ -0,0 +1,26 @@ +package storage + +import "testing" + +func TestValidateThreadsFetchableURL(t *testing.T) { + cases := []struct { + url string + valid bool + }{ + {"https://threads-tool.30cm.net/haixun-assets/publish-attachments/t/a/x.png", true}, + {"http://127.0.0.1:9000/haixun-assets/x.png", false}, + {"http://localhost:9000/x.png", false}, + {"http://192.168.1.10/x.png", false}, + {"http://example.com/x.png", false}, + {"", false}, + } + for _, tc := range cases { + err := ValidateThreadsFetchableURL(tc.url) + if tc.valid && err != nil { + t.Fatalf("ValidateThreadsFetchableURL(%q) = %v, want nil", tc.url, err) + } + if !tc.valid && err == nil { + t.Fatalf("ValidateThreadsFetchableURL(%q) = nil, want error", tc.url) + } + } +} \ No newline at end of file diff --git a/backend/internal/library/style8d/analyze.go b/old/backend/internal/library/style8d/analyze.go similarity index 100% rename from backend/internal/library/style8d/analyze.go rename to old/backend/internal/library/style8d/analyze.go diff --git a/backend/internal/library/style8d/analyze_test.go b/old/backend/internal/library/style8d/analyze_test.go similarity index 100% rename from backend/internal/library/style8d/analyze_test.go rename to old/backend/internal/library/style8d/analyze_test.go diff --git a/backend/internal/library/style8d/prompt.go b/old/backend/internal/library/style8d/prompt.go similarity index 100% rename from backend/internal/library/style8d/prompt.go rename to old/backend/internal/library/style8d/prompt.go diff --git a/backend/internal/library/style8d/prompt_test.go b/old/backend/internal/library/style8d/prompt_test.go similarity index 100% rename from backend/internal/library/style8d/prompt_test.go rename to old/backend/internal/library/style8d/prompt_test.go diff --git a/backend/internal/library/style8d/runner.go b/old/backend/internal/library/style8d/runner.go similarity index 100% rename from backend/internal/library/style8d/runner.go rename to old/backend/internal/library/style8d/runner.go diff --git a/backend/internal/library/style8d/text_input_test.go b/old/backend/internal/library/style8d/text_input_test.go similarity index 100% rename from backend/internal/library/style8d/text_input_test.go rename to old/backend/internal/library/style8d/text_input_test.go diff --git a/backend/internal/library/threadsapi/client.go b/old/backend/internal/library/threadsapi/client.go similarity index 76% rename from backend/internal/library/threadsapi/client.go rename to old/backend/internal/library/threadsapi/client.go index 63c0acc..c1c0125 100644 --- a/backend/internal/library/threadsapi/client.go +++ b/old/backend/internal/library/threadsapi/client.go @@ -105,20 +105,35 @@ func (c *Client) KeywordSearch(ctx context.Context, opts KeywordSearchOptions) ( func parseAPIError(body []byte, status int) error { var payload struct { Error struct { - Message string `json:"message"` + Message string `json:"message"` + Type string `json:"type"` + Code int `json:"code"` + ErrorSubcode int `json:"error_subcode"` + ErrorUserMsg string `json:"error_user_msg"` + FbtraceID string `json:"fbtrace_id"` } `json:"error"` ErrorMessage string `json:"error_message"` } _ = json.Unmarshal(body, &payload) - msg := strings.TrimSpace(payload.Error.Message) + msg := strings.TrimSpace(payload.Error.ErrorUserMsg) + if msg == "" { + msg = strings.TrimSpace(payload.Error.Message) + } if msg == "" { msg = strings.TrimSpace(payload.ErrorMessage) } if msg == "" { - msg = string(body) + msg = strings.TrimSpace(string(body)) } - if len(msg) > 240 { - msg = msg[:240] + if len(msg) > 400 { + msg = msg[:400] } - return fmt.Errorf("threads api %d: %s", status, msg) + out := fmt.Sprintf("threads api %d: %s", status, msg) + if payload.Error.Code != 0 || payload.Error.ErrorSubcode != 0 { + out += fmt.Sprintf(" (code=%d subcode=%d)", payload.Error.Code, payload.Error.ErrorSubcode) + } + if trace := strings.TrimSpace(payload.Error.FbtraceID); trace != "" { + out += fmt.Sprintf(" [fbtrace_id=%s]", trace) + } + return fmt.Errorf("%s", out) } diff --git a/old/backend/internal/library/threadsapi/client_error_test.go b/old/backend/internal/library/threadsapi/client_error_test.go new file mode 100644 index 0000000..d62e28a --- /dev/null +++ b/old/backend/internal/library/threadsapi/client_error_test.go @@ -0,0 +1,20 @@ +package threadsapi + +import ( + "strings" + "testing" +) + +func TestParseAPIErrorIncludesUserMessageAndCodes(t *testing.T) { + body := []byte(`{"error":{"message":"An unknown error occurred","type":"OAuthException","code":1,"error_subcode":99,"error_user_msg":"圖片無法下載","fbtrace_id":"abc123"}}`) + err := parseAPIError(body, 400) + got := err.Error() + if got == "" { + t.Fatal("expected error") + } + for _, want := range []string{"400", "圖片無法下載", "code=1", "subcode=99", "fbtrace_id=abc123"} { + if !strings.Contains(got, want) { + t.Fatalf("parseAPIError() = %q, want fragment %q", got, want) + } + } +} \ No newline at end of file diff --git a/backend/internal/library/threadsapi/extended.go b/old/backend/internal/library/threadsapi/extended.go similarity index 100% rename from backend/internal/library/threadsapi/extended.go rename to old/backend/internal/library/threadsapi/extended.go diff --git a/backend/internal/library/threadsapi/fields_test.go b/old/backend/internal/library/threadsapi/fields_test.go similarity index 100% rename from backend/internal/library/threadsapi/fields_test.go rename to old/backend/internal/library/threadsapi/fields_test.go diff --git a/backend/internal/library/threadsapi/media.go b/old/backend/internal/library/threadsapi/media.go similarity index 100% rename from backend/internal/library/threadsapi/media.go rename to old/backend/internal/library/threadsapi/media.go diff --git a/backend/internal/library/threadsapi/media_test.go b/old/backend/internal/library/threadsapi/media_test.go similarity index 100% rename from backend/internal/library/threadsapi/media_test.go rename to old/backend/internal/library/threadsapi/media_test.go diff --git a/backend/internal/library/threadsapi/oauth.go b/old/backend/internal/library/threadsapi/oauth.go similarity index 100% rename from backend/internal/library/threadsapi/oauth.go rename to old/backend/internal/library/threadsapi/oauth.go diff --git a/backend/internal/library/threadsapi/oauth_permission_test.go b/old/backend/internal/library/threadsapi/oauth_permission_test.go similarity index 100% rename from backend/internal/library/threadsapi/oauth_permission_test.go rename to old/backend/internal/library/threadsapi/oauth_permission_test.go diff --git a/backend/internal/library/threadsapi/oauth_test.go b/old/backend/internal/library/threadsapi/oauth_test.go similarity index 100% rename from backend/internal/library/threadsapi/oauth_test.go rename to old/backend/internal/library/threadsapi/oauth_test.go diff --git a/backend/internal/library/threadsapi/publish.go b/old/backend/internal/library/threadsapi/publish.go similarity index 62% rename from backend/internal/library/threadsapi/publish.go rename to old/backend/internal/library/threadsapi/publish.go index 1cbac06..20e89cb 100644 --- a/backend/internal/library/threadsapi/publish.go +++ b/old/backend/internal/library/threadsapi/publish.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "haixun-backend/internal/library/threadspost" ) type PublishResult struct { @@ -23,6 +22,8 @@ type PublishTextInput struct { AccessToken string Text string TopicTag string + ImageURL string + ImageURLs []string } type PublishReplyInput struct { @@ -30,80 +31,54 @@ type PublishReplyInput struct { AccessToken string ReplyToID string Text string + ImageURL string + ImageURLs []string } -// PublishText posts a new text thread via Graph API. +func collectImageURLs(single string, list []string) []string { + if len(list) > 0 { + return normalizeImageURLs(list) + } + single = strings.TrimSpace(single) + if single == "" { + return nil + } + return []string{single} +} + +// PublishText posts a new text or image 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, in.TopicTag) - 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 + return publishMedia(ctx, publishMediaInput{ + ThreadsUserID: in.ThreadsUserID, + AccessToken: in.AccessToken, + Text: in.Text, + TopicTag: in.TopicTag, + ImageURLs: collectImageURLs(in.ImageURL, in.ImageURLs), + }) } -// PublishReply posts a text reply to an existing Threads media via Graph API. +// PublishReply posts a text or image reply via Graph API. func PublishReply(ctx context.Context, in PublishReplyInput) (*PublishResult, error) { - userID := strings.TrimSpace(in.ThreadsUserID) - token := strings.TrimSpace(in.AccessToken) replyTo := strings.TrimSpace(in.ReplyToID) - text := strings.TrimSpace(in.Text) - if userID == "" || token == "" { - return nil, fmt.Errorf("threads api credentials incomplete") - } if replyTo == "" { return nil, fmt.Errorf("reply_to_id is required") } - if text == "" { - return nil, fmt.Errorf("reply text is required") - } - if err := threadspost.ValidateReply(text); err != nil { - return nil, err - } - - containerID, err := createReplyContainer(ctx, userID, token, replyTo, 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 + return publishMedia(ctx, publishMediaInput{ + ThreadsUserID: in.ThreadsUserID, + AccessToken: in.AccessToken, + Text: in.Text, + ReplyToID: replyTo, + ImageURLs: collectImageURLs(in.ImageURL, in.ImageURLs), + }) } -func createTextContainer(ctx context.Context, userID, token, text, topicTag string) (string, error) { +func urlValues(token string) url.Values { params := url.Values{} params.Set("access_token", token) - params.Set("media_type", "TEXT") - params.Set("text", text) - if tag := normalizeTopicTag(topicTag); tag != "" { - params.Set("topic_tag", tag) - } + return params +} + +func postThreadsContainer(ctx context.Context, userID string, params url.Values) (string, error) { endpoint := graphBaseURL + "/" + userID + "/threads?" + params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil) if err != nil { @@ -128,6 +103,16 @@ func createTextContainer(ctx context.Context, userID, token, text, topicTag stri return payload.ID, nil } +func createTextContainer(ctx context.Context, userID, token, text, topicTag string) (string, error) { + params := urlValues(token) + params.Set("media_type", "TEXT") + params.Set("text", text) + if tag := normalizeTopicTag(topicTag); tag != "" { + params.Set("topic_tag", tag) + } + return postThreadsContainer(ctx, userID, params) +} + func normalizeTopicTag(value string) string { tag := strings.TrimSpace(value) tag = strings.TrimLeft(tag, "#") @@ -139,35 +124,36 @@ func normalizeTopicTag(value string) string { return tag } +func createImageContainer(ctx context.Context, userID, token, text, imageURL, topicTag string) (string, error) { + params := urlValues(token) + params.Set("media_type", "IMAGE") + params.Set("image_url", imageURL) + if text != "" { + params.Set("text", text) + } + if tag := normalizeTopicTag(topicTag); tag != "" { + params.Set("topic_tag", tag) + } + return postThreadsContainer(ctx, userID, params) +} + +func createImageReplyContainer(ctx context.Context, userID, token, replyTo, text, imageURL string) (string, error) { + params := urlValues(token) + params.Set("media_type", "IMAGE") + params.Set("image_url", imageURL) + if text != "" { + params.Set("text", text) + } + params.Set("reply_to_id", replyTo) + return postThreadsContainer(ctx, userID, params) +} + func createReplyContainer(ctx context.Context, userID, token, replyTo, text string) (string, error) { - params := url.Values{} - params.Set("access_token", token) + params := urlValues(token) params.Set("media_type", "TEXT") params.Set("text", text) params.Set("reply_to_id", replyTo) - - 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 + return postThreadsContainer(ctx, userID, params) } func publishContainer(ctx context.Context, userID, token, containerID string) (string, error) { @@ -198,8 +184,11 @@ func publishContainer(ctx context.Context, userID, token, containerID string) (s return payload.ID, nil } -func waitForContainerReady(ctx context.Context, containerID, token string) error { - for attempt := 0; attempt < 12; attempt++ { +func waitForContainerReady(ctx context.Context, containerID, token string, maxAttempts int) error { + if maxAttempts <= 0 { + maxAttempts = 12 + } + for attempt := 0; attempt < maxAttempts; attempt++ { if attempt > 0 { select { case <-ctx.Done(): @@ -209,7 +198,7 @@ func waitForContainerReady(ctx context.Context, containerID, token string) error } params := url.Values{} params.Set("access_token", token) - params.Set("fields", "status") + params.Set("fields", "status,error_message") endpoint := graphBaseURL + "/" + containerID + "?" + params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { @@ -223,7 +212,8 @@ func waitForContainerReady(ctx context.Context, containerID, token string) error return parseAPIError(body, status) } var payload struct { - Status string `json:"status"` + Status string `json:"status"` + ErrorMessage string `json:"error_message"` } if err := json.Unmarshal(body, &payload); err != nil { return err @@ -232,12 +222,51 @@ func waitForContainerReady(ctx context.Context, containerID, token string) error case "FINISHED": return nil case "ERROR", "EXPIRED": + detail := strings.TrimSpace(payload.ErrorMessage) + if detail != "" { + return fmt.Errorf("threads container %s: %s", strings.ToLower(payload.Status), detail) + } return fmt.Errorf("threads container status %s", payload.Status) } } return fmt.Errorf("threads container not ready in time") } +func verifyReplyTarget(ctx context.Context, replyTo, token string) error { + replyTo = strings.TrimSpace(replyTo) + if replyTo == "" { + return fmt.Errorf("reply_to_id is required") + } + if !IsNumericMediaID(replyTo) { + return fmt.Errorf("reply_to_id 必須是 Threads 數字 media id,目前為 %q", replyTo) + } + params := url.Values{} + params.Set("access_token", token) + params.Set("fields", "id,is_reply,permalink") + endpoint := graphBaseURL + "/" + url.PathEscape(replyTo) + "?" + params.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return err + } + body, status, err := doRequest(req) + if err != nil { + return err + } + if status != http.StatusOK { + return fmt.Errorf("無法讀取回覆目標 %s:%w", replyTo, parseAPIError(body, status)) + } + var payload struct { + ID string `json:"id"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return fmt.Errorf("parse reply target: %w", err) + } + if strings.TrimSpace(payload.ID) == "" { + return fmt.Errorf("回覆目標 %s 不存在或無權限讀取", replyTo) + } + return nil +} + func fetchPermalink(ctx context.Context, mediaID, token string) (string, error) { params := url.Values{} params.Set("access_token", token) diff --git a/old/backend/internal/library/threadsapi/publish_media.go b/old/backend/internal/library/threadsapi/publish_media.go new file mode 100644 index 0000000..e620594 --- /dev/null +++ b/old/backend/internal/library/threadsapi/publish_media.go @@ -0,0 +1,155 @@ +package threadsapi + +import ( + "context" + "fmt" + "strings" + + "haixun-backend/internal/library/threadspost" +) + +const maxCarouselItems = 20 + +type publishMediaInput struct { + ThreadsUserID string + AccessToken string + Text string + TopicTag string + ReplyToID string + ImageURLs []string +} + +func normalizeImageURLs(urls []string) []string { + out := make([]string, 0, len(urls)) + seen := make(map[string]struct{}, len(urls)) + for _, raw := range urls { + url := strings.TrimSpace(raw) + if url == "" { + continue + } + if _, ok := seen[url]; ok { + continue + } + seen[url] = struct{}{} + out = append(out, url) + if len(out) >= maxCarouselItems { + break + } + } + return out +} + +func publishMedia(ctx context.Context, in publishMediaInput) (*PublishResult, error) { + userID := strings.TrimSpace(in.ThreadsUserID) + token := strings.TrimSpace(in.AccessToken) + text := strings.TrimSpace(in.Text) + replyTo := strings.TrimSpace(in.ReplyToID) + imageURLs := normalizeImageURLs(in.ImageURLs) + + if userID == "" || token == "" { + return nil, fmt.Errorf("threads api credentials incomplete") + } + if replyTo == "" && text == "" && len(imageURLs) == 0 { + return nil, fmt.Errorf("post text or image is required") + } + if replyTo != "" && text == "" && len(imageURLs) == 0 { + return nil, fmt.Errorf("reply text or image is required") + } + if replyTo != "" && len(imageURLs) > 1 { + return nil, fmt.Errorf("Threads API 目前不支援多圖回覆,請只附一張圖") + } + if replyTo != "" { + if err := verifyReplyTarget(ctx, replyTo, token); err != nil { + return nil, err + } + } + if replyTo == "" { + if err := threadspost.ValidatePublishContent(text, len(imageURLs) > 0); err != nil { + return nil, err + } + } else if err := threadspost.ValidateReplyContent(text, len(imageURLs) > 0); err != nil { + return nil, err + } + + var containerID string + var err error + switch len(imageURLs) { + case 0: + if replyTo != "" { + containerID, err = createReplyContainer(ctx, userID, token, replyTo, text) + } else { + containerID, err = createTextContainer(ctx, userID, token, text, in.TopicTag) + } + case 1: + if replyTo == "" { + containerID, err = createImageContainer(ctx, userID, token, text, imageURLs[0], in.TopicTag) + } else { + containerID, err = createImageReplyContainer(ctx, userID, token, replyTo, text, imageURLs[0]) + } + default: + containerID, err = createCarouselContainer(ctx, userID, token, text, in.TopicTag, replyTo, imageURLs) + } + if err != nil { + return nil, err + } + + waitAttempts := containerWaitAttempts(len(imageURLs)) + if err := waitForContainerReady(ctx, containerID, token, waitAttempts); 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 +} + +func containerWaitAttempts(imageCount int) int { + if imageCount == 0 { + return 12 + } + if imageCount == 1 { + return 30 + } + return 30 + imageCount*2 +} + +func createCarouselContainer(ctx context.Context, userID, token, text, topicTag, replyTo string, imageURLs []string) (string, error) { + if len(imageURLs) < 2 { + return "", fmt.Errorf("carousel requires at least two images") + } + childIDs := make([]string, 0, len(imageURLs)) + for _, imageURL := range imageURLs { + childID, err := createCarouselItemContainer(ctx, userID, token, imageURL) + if err != nil { + return "", err + } + if err := waitForContainerReady(ctx, childID, token, 30); err != nil { + return "", err + } + childIDs = append(childIDs, childID) + } + + params := urlValues(token) + params.Set("media_type", "CAROUSEL") + params.Set("children", strings.Join(childIDs, ",")) + if text != "" { + params.Set("text", text) + } + if tag := normalizeTopicTag(topicTag); tag != "" { + params.Set("topic_tag", tag) + } + if replyTo != "" { + params.Set("reply_to_id", replyTo) + } + return postThreadsContainer(ctx, userID, params) +} + +func createCarouselItemContainer(ctx context.Context, userID, token, imageURL string) (string, error) { + params := urlValues(token) + params.Set("is_carousel_item", "true") + params.Set("media_type", "IMAGE") + params.Set("image_url", imageURL) + return postThreadsContainer(ctx, userID, params) +} \ No newline at end of file diff --git a/backend/internal/library/threadsapi/replies.go b/old/backend/internal/library/threadsapi/replies.go similarity index 100% rename from backend/internal/library/threadsapi/replies.go rename to old/backend/internal/library/threadsapi/replies.go diff --git a/backend/internal/library/threadsapi/replies_test.go b/old/backend/internal/library/threadsapi/replies_test.go similarity index 100% rename from backend/internal/library/threadsapi/replies_test.go rename to old/backend/internal/library/threadsapi/replies_test.go diff --git a/backend/internal/library/threadsapi/resolve_media.go b/old/backend/internal/library/threadsapi/resolve_media.go similarity index 100% rename from backend/internal/library/threadsapi/resolve_media.go rename to old/backend/internal/library/threadsapi/resolve_media.go diff --git a/backend/internal/library/threadsapi/resolve_media_hint_test.go b/old/backend/internal/library/threadsapi/resolve_media_hint_test.go similarity index 100% rename from backend/internal/library/threadsapi/resolve_media_hint_test.go rename to old/backend/internal/library/threadsapi/resolve_media_hint_test.go diff --git a/backend/internal/library/threadsapi/resolve_media_test.go b/old/backend/internal/library/threadsapi/resolve_media_test.go similarity index 100% rename from backend/internal/library/threadsapi/resolve_media_test.go rename to old/backend/internal/library/threadsapi/resolve_media_test.go diff --git a/backend/internal/library/threadsapi/scopes.go b/old/backend/internal/library/threadsapi/scopes.go similarity index 100% rename from backend/internal/library/threadsapi/scopes.go rename to old/backend/internal/library/threadsapi/scopes.go diff --git a/backend/internal/library/threadsapi/url.go b/old/backend/internal/library/threadsapi/url.go similarity index 100% rename from backend/internal/library/threadsapi/url.go rename to old/backend/internal/library/threadsapi/url.go diff --git a/backend/internal/library/threadspost/format.go b/old/backend/internal/library/threadspost/format.go similarity index 100% rename from backend/internal/library/threadspost/format.go rename to old/backend/internal/library/threadspost/format.go diff --git a/backend/internal/library/threadspost/format_test.go b/old/backend/internal/library/threadspost/format_test.go similarity index 100% rename from backend/internal/library/threadspost/format_test.go rename to old/backend/internal/library/threadspost/format_test.go diff --git a/backend/internal/library/threadspost/limits.go b/old/backend/internal/library/threadspost/limits.go similarity index 83% rename from backend/internal/library/threadspost/limits.go rename to old/backend/internal/library/threadspost/limits.go index 24e11cb..33424d6 100644 --- a/backend/internal/library/threadspost/limits.go +++ b/old/backend/internal/library/threadspost/limits.go @@ -60,6 +60,26 @@ func PublishBand(text string) LengthBand { } } +func ValidatePublishContent(text string, hasImage bool) error { + if strings.TrimSpace(text) == "" && !hasImage { + return fmt.Errorf("貼文需有文字或圖片") + } + if strings.TrimSpace(text) == "" { + return nil + } + return ValidatePublish(text) +} + +func ValidateReplyContent(text string, hasImage bool) error { + if strings.TrimSpace(text) == "" && !hasImage { + return fmt.Errorf("回覆需有文字或圖片") + } + if strings.TrimSpace(text) == "" { + return nil + } + return ValidateReply(text) +} + func ValidatePublish(text string) error { if strings.TrimSpace(text) == "" { return fmt.Errorf("貼文內文不可為空") diff --git a/backend/internal/library/threadspost/limits_test.go b/old/backend/internal/library/threadspost/limits_test.go similarity index 100% rename from backend/internal/library/threadspost/limits_test.go rename to old/backend/internal/library/threadspost/limits_test.go diff --git a/backend/internal/library/threadspost/mimic.go b/old/backend/internal/library/threadspost/mimic.go similarity index 100% rename from backend/internal/library/threadspost/mimic.go rename to old/backend/internal/library/threadspost/mimic.go diff --git a/backend/internal/library/threadspost/mimic_test.go b/old/backend/internal/library/threadspost/mimic_test.go similarity index 100% rename from backend/internal/library/threadspost/mimic_test.go rename to old/backend/internal/library/threadspost/mimic_test.go diff --git a/backend/internal/library/validate/validate.go b/old/backend/internal/library/validate/validate.go similarity index 100% rename from backend/internal/library/validate/validate.go rename to old/backend/internal/library/validate/validate.go diff --git a/backend/internal/library/viral/analyze_viral.go b/old/backend/internal/library/viral/analyze_viral.go similarity index 100% rename from backend/internal/library/viral/analyze_viral.go rename to old/backend/internal/library/viral/analyze_viral.go diff --git a/backend/internal/library/viral/analyze_viral_test.go b/old/backend/internal/library/viral/analyze_viral_test.go similarity index 100% rename from backend/internal/library/viral/analyze_viral_test.go rename to old/backend/internal/library/viral/analyze_viral_test.go diff --git a/backend/internal/library/viral/audience.go b/old/backend/internal/library/viral/audience.go similarity index 100% rename from backend/internal/library/viral/audience.go rename to old/backend/internal/library/viral/audience.go diff --git a/backend/internal/library/viral/discover.go b/old/backend/internal/library/viral/discover.go similarity index 100% rename from backend/internal/library/viral/discover.go rename to old/backend/internal/library/viral/discover.go diff --git a/backend/internal/library/viral/discover_accounts.go b/old/backend/internal/library/viral/discover_accounts.go similarity index 100% rename from backend/internal/library/viral/discover_accounts.go rename to old/backend/internal/library/viral/discover_accounts.go diff --git a/backend/internal/library/viral/discover_accounts_test.go b/old/backend/internal/library/viral/discover_accounts_test.go similarity index 100% rename from backend/internal/library/viral/discover_accounts_test.go rename to old/backend/internal/library/viral/discover_accounts_test.go diff --git a/backend/internal/library/viral/discover_budget_test.go b/old/backend/internal/library/viral/discover_budget_test.go similarity index 100% rename from backend/internal/library/viral/discover_budget_test.go rename to old/backend/internal/library/viral/discover_budget_test.go diff --git a/backend/internal/library/viral/discover_graceful_test.go b/old/backend/internal/library/viral/discover_graceful_test.go similarity index 100% rename from backend/internal/library/viral/discover_graceful_test.go rename to old/backend/internal/library/viral/discover_graceful_test.go diff --git a/backend/internal/library/viral/enrich_accounts.go b/old/backend/internal/library/viral/enrich_accounts.go similarity index 100% rename from backend/internal/library/viral/enrich_accounts.go rename to old/backend/internal/library/viral/enrich_accounts.go diff --git a/backend/internal/library/viral/flexible_json.go b/old/backend/internal/library/viral/flexible_json.go similarity index 100% rename from backend/internal/library/viral/flexible_json.go rename to old/backend/internal/library/viral/flexible_json.go diff --git a/backend/internal/library/viral/free_trends.go b/old/backend/internal/library/viral/free_trends.go similarity index 100% rename from backend/internal/library/viral/free_trends.go rename to old/backend/internal/library/viral/free_trends.go diff --git a/backend/internal/library/viral/known_accounts.go b/old/backend/internal/library/viral/known_accounts.go similarity index 100% rename from backend/internal/library/viral/known_accounts.go rename to old/backend/internal/library/viral/known_accounts.go diff --git a/backend/internal/library/viral/merge_accounts.go b/old/backend/internal/library/viral/merge_accounts.go similarity index 100% rename from backend/internal/library/viral/merge_accounts.go rename to old/backend/internal/library/viral/merge_accounts.go diff --git a/backend/internal/library/viral/mission_inspire.go b/old/backend/internal/library/viral/mission_inspire.go similarity index 100% rename from backend/internal/library/viral/mission_inspire.go rename to old/backend/internal/library/viral/mission_inspire.go diff --git a/backend/internal/library/viral/mission_inspire_collect.go b/old/backend/internal/library/viral/mission_inspire_collect.go similarity index 100% rename from backend/internal/library/viral/mission_inspire_collect.go rename to old/backend/internal/library/viral/mission_inspire_collect.go diff --git a/backend/internal/library/viral/mission_inspire_test.go b/old/backend/internal/library/viral/mission_inspire_test.go similarity index 100% rename from backend/internal/library/viral/mission_inspire_test.go rename to old/backend/internal/library/viral/mission_inspire_test.go diff --git a/backend/internal/library/viral/mission_research_map.go b/old/backend/internal/library/viral/mission_research_map.go similarity index 100% rename from backend/internal/library/viral/mission_research_map.go rename to old/backend/internal/library/viral/mission_research_map.go diff --git a/backend/internal/library/viral/mission_research_map_test.go b/old/backend/internal/library/viral/mission_research_map_test.go similarity index 100% rename from backend/internal/library/viral/mission_research_map_test.go rename to old/backend/internal/library/viral/mission_research_map_test.go diff --git a/backend/internal/library/viral/mission_topic.go b/old/backend/internal/library/viral/mission_topic.go similarity index 100% rename from backend/internal/library/viral/mission_topic.go rename to old/backend/internal/library/viral/mission_topic.go diff --git a/backend/internal/library/viral/quality.go b/old/backend/internal/library/viral/quality.go similarity index 100% rename from backend/internal/library/viral/quality.go rename to old/backend/internal/library/viral/quality.go diff --git a/backend/internal/library/viral/quality_test.go b/old/backend/internal/library/viral/quality_test.go similarity index 100% rename from backend/internal/library/viral/quality_test.go rename to old/backend/internal/library/viral/quality_test.go diff --git a/backend/internal/library/viral/reference_accounts.go b/old/backend/internal/library/viral/reference_accounts.go similarity index 100% rename from backend/internal/library/viral/reference_accounts.go rename to old/backend/internal/library/viral/reference_accounts.go diff --git a/backend/internal/library/viral/reference_accounts_fallback_test.go b/old/backend/internal/library/viral/reference_accounts_fallback_test.go similarity index 100% rename from backend/internal/library/viral/reference_accounts_fallback_test.go rename to old/backend/internal/library/viral/reference_accounts_fallback_test.go diff --git a/backend/internal/library/viral/reference_accounts_test.go b/old/backend/internal/library/viral/reference_accounts_test.go similarity index 100% rename from backend/internal/library/viral/reference_accounts_test.go rename to old/backend/internal/library/viral/reference_accounts_test.go diff --git a/backend/internal/library/viral/replicate.go b/old/backend/internal/library/viral/replicate.go similarity index 100% rename from backend/internal/library/viral/replicate.go rename to old/backend/internal/library/viral/replicate.go diff --git a/backend/internal/library/viral/research_map.go b/old/backend/internal/library/viral/research_map.go similarity index 100% rename from backend/internal/library/viral/research_map.go rename to old/backend/internal/library/viral/research_map.go diff --git a/backend/internal/library/viral/score.go b/old/backend/internal/library/viral/score.go similarity index 100% rename from backend/internal/library/viral/score.go rename to old/backend/internal/library/viral/score.go diff --git a/backend/internal/library/viral/search_tags.go b/old/backend/internal/library/viral/search_tags.go similarity index 100% rename from backend/internal/library/viral/search_tags.go rename to old/backend/internal/library/viral/search_tags.go diff --git a/backend/internal/library/viral/search_tags_test.go b/old/backend/internal/library/viral/search_tags_test.go similarity index 100% rename from backend/internal/library/viral/search_tags_test.go rename to old/backend/internal/library/viral/search_tags_test.go diff --git a/backend/internal/library/viral/tags.go b/old/backend/internal/library/viral/tags.go similarity index 100% rename from backend/internal/library/viral/tags.go rename to old/backend/internal/library/viral/tags.go diff --git a/backend/internal/library/viral/tags_test.go b/old/backend/internal/library/viral/tags_test.go similarity index 100% rename from backend/internal/library/viral/tags_test.go rename to old/backend/internal/library/viral/tags_test.go diff --git a/backend/internal/library/webpage/digest.go b/old/backend/internal/library/webpage/digest.go similarity index 100% rename from backend/internal/library/webpage/digest.go rename to old/backend/internal/library/webpage/digest.go diff --git a/backend/internal/library/webpage/digest_test.go b/old/backend/internal/library/webpage/digest_test.go similarity index 100% rename from backend/internal/library/webpage/digest_test.go rename to old/backend/internal/library/webpage/digest_test.go diff --git a/backend/internal/library/webpage/format.go b/old/backend/internal/library/webpage/format.go similarity index 100% rename from backend/internal/library/webpage/format.go rename to old/backend/internal/library/webpage/format.go diff --git a/backend/internal/library/websearch/client.go b/old/backend/internal/library/websearch/client.go similarity index 100% rename from backend/internal/library/websearch/client.go rename to old/backend/internal/library/websearch/client.go diff --git a/backend/internal/logic/ai/actor.go b/old/backend/internal/logic/ai/actor.go similarity index 100% rename from backend/internal/logic/ai/actor.go rename to old/backend/internal/logic/ai/actor.go diff --git a/backend/internal/logic/ai/chat_logic.go b/old/backend/internal/logic/ai/chat_logic.go similarity index 100% rename from backend/internal/logic/ai/chat_logic.go rename to old/backend/internal/logic/ai/chat_logic.go diff --git a/backend/internal/logic/ai/chat_stream_logic.go b/old/backend/internal/logic/ai/chat_stream_logic.go similarity index 100% rename from backend/internal/logic/ai/chat_stream_logic.go rename to old/backend/internal/logic/ai/chat_stream_logic.go diff --git a/backend/internal/logic/ai/credential.go b/old/backend/internal/logic/ai/credential.go similarity index 100% rename from backend/internal/logic/ai/credential.go rename to old/backend/internal/logic/ai/credential.go diff --git a/backend/internal/logic/ai/islander_chat_stream_logic.go b/old/backend/internal/logic/ai/islander_chat_stream_logic.go similarity index 100% rename from backend/internal/logic/ai/islander_chat_stream_logic.go rename to old/backend/internal/logic/ai/islander_chat_stream_logic.go diff --git a/backend/internal/logic/ai/list_ai_provider_models_logic.go b/old/backend/internal/logic/ai/list_ai_provider_models_logic.go similarity index 100% rename from backend/internal/logic/ai/list_ai_provider_models_logic.go rename to old/backend/internal/logic/ai/list_ai_provider_models_logic.go diff --git a/backend/internal/logic/ai/list_ai_providers_logic.go b/old/backend/internal/logic/ai/list_ai_providers_logic.go similarity index 100% rename from backend/internal/logic/ai/list_ai_providers_logic.go rename to old/backend/internal/logic/ai/list_ai_providers_logic.go diff --git a/backend/internal/logic/ai/mapper.go b/old/backend/internal/logic/ai/mapper.go similarity index 100% rename from backend/internal/logic/ai/mapper.go rename to old/backend/internal/logic/ai/mapper.go diff --git a/old/backend/internal/logic/auth/email_verification.go b/old/backend/internal/logic/auth/email_verification.go new file mode 100644 index 0000000..89314b3 --- /dev/null +++ b/old/backend/internal/logic/auth/email_verification.go @@ -0,0 +1,78 @@ +package auth + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "time" + + "haixun-backend/internal/library/clock" + "haixun-backend/internal/model/member/domain/entity" + "haixun-backend/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +const emailVerificationTTL = 15 * time.Minute +const emailVerificationResendCooldown = 60 * time.Second + +func newVerifyCode() (string, error) { + n, err := rand.Int(rand.Reader, big.NewInt(1000000)) + if err != nil { + return "", err + } + return fmt.Sprintf("%06d", n.Int64()), nil +} + +func requiresNativeEmailVerification(member *entity.Member) bool { + if member == nil { + return false + } + return member.Origin == "" || member.Origin == entity.OriginNative +} + +func issueAndSendEmailVerification(ctx context.Context, svcCtx *svc.ServiceContext, member *entity.Member) error { + verifyCode, err := newVerifyCode() + if err != nil { + return err + } + expiresAt := clock.NowUnixNano() + int64(emailVerificationTTL) + if err := svcCtx.Member.SetEmailVerificationCode(ctx, member.TenantID, member.UID, verifyCode, expiresAt); err != nil { + return err + } + if err := sendEmailVerification(ctx, svcCtx, member.Email, verifyCode); err != nil { + logx.WithContext(ctx).Errorf("send verification email failed: uid=%s err=%v", member.UID, err) + } + return nil +} + +func sendEmailVerification(ctx context.Context, svcCtx *svc.ServiceContext, email, verifyCode string) error { + if svcCtx.Mailer == nil { + return nil + } + body := fmt.Sprintf("你的巡樓 Console 驗證碼是:%s\n\n此驗證碼 15 分鐘內有效。", verifyCode) + if err := svcCtx.Mailer.Send(ctx, email, "巡樓 Console 帳號驗證碼", body); err != nil { + logx.WithContext(ctx).Errorf("send verification email failed: email=%s err=%v", email, err) + return err + } + return nil +} + +func maybeSendLoginVerificationEmail(ctx context.Context, svcCtx *svc.ServiceContext, member *entity.Member) { + if member == nil || !requiresNativeEmailVerification(member) || member.EmailVerified { + return + } + if member.EmailVerifyCode != "" && member.EmailVerifyExpiresAt > clock.NowUnixNano() { + return + } + _ = issueAndSendEmailVerification(ctx, svcCtx, member) +} + +func emailVerificationSentRecently(member *entity.Member) bool { + if member == nil || member.EmailVerifyExpiresAt <= 0 { + return false + } + sentAt := member.EmailVerifyExpiresAt - int64(emailVerificationTTL) + return clock.NowUnixNano()-sentAt < int64(emailVerificationResendCooldown) +} \ No newline at end of file diff --git a/backend/internal/logic/auth/login_logic.go b/old/backend/internal/logic/auth/login_logic.go similarity index 72% rename from backend/internal/logic/auth/login_logic.go rename to old/backend/internal/logic/auth/login_logic.go index 2cfc217..8816674 100644 --- a/backend/internal/logic/auth/login_logic.go +++ b/old/backend/internal/logic/auth/login_logic.go @@ -18,10 +18,14 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic } func (l *LoginLogic) Login(req *types.AuthLoginReq) (*types.AuthTokenData, error) { - _, token, err := l.svcCtx.Member.Login(l.ctx, memberusecase.LoginRequest{ + member, token, err := l.svcCtx.Member.Login(l.ctx, memberusecase.LoginRequest{ TenantID: req.TenantID, Email: req.Email, Password: req.Password, }) - return toAuthTokenData(token), err + if err != nil { + return nil, err + } + maybeSendLoginVerificationEmail(l.ctx, l.svcCtx, member) + return toAuthTokenData(token), nil } diff --git a/backend/internal/logic/auth/logout_logic.go b/old/backend/internal/logic/auth/logout_logic.go similarity index 100% rename from backend/internal/logic/auth/logout_logic.go rename to old/backend/internal/logic/auth/logout_logic.go diff --git a/backend/internal/logic/auth/mapper.go b/old/backend/internal/logic/auth/mapper.go similarity index 100% rename from backend/internal/logic/auth/mapper.go rename to old/backend/internal/logic/auth/mapper.go diff --git a/backend/internal/logic/auth/refresh_logic.go b/old/backend/internal/logic/auth/refresh_logic.go similarity index 100% rename from backend/internal/logic/auth/refresh_logic.go rename to old/backend/internal/logic/auth/refresh_logic.go diff --git a/backend/internal/logic/auth/register_logic.go b/old/backend/internal/logic/auth/register_logic.go similarity index 50% rename from backend/internal/logic/auth/register_logic.go rename to old/backend/internal/logic/auth/register_logic.go index 8859384..d4891eb 100644 --- a/backend/internal/logic/auth/register_logic.go +++ b/old/backend/internal/logic/auth/register_logic.go @@ -2,18 +2,11 @@ package auth import ( "context" - "crypto/rand" - "fmt" - "math/big" - "time" - "haixun-backend/internal/library/clock" "haixun-backend/internal/logic/authz" memberusecase "haixun-backend/internal/model/member/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" - - "github.com/zeromicro/go-zero/core/logx" ) type RegisterLogic struct { @@ -39,27 +32,8 @@ func (l *RegisterLogic) Register(req *types.AuthRegisterReq) (*types.AuthTokenDa if err != nil { return nil, err } - verifyCode, err := newVerifyCode() - if err != nil { + if err := issueAndSendEmailVerification(l.ctx, l.svcCtx, member); err != nil { return nil, err } - expiresAt := clock.NowUnixNano() + int64(15*time.Minute) - if err := l.svcCtx.Member.SetEmailVerificationCode(l.ctx, member.TenantID, member.UID, verifyCode, expiresAt); err != nil { - return nil, err - } - if l.svcCtx.Mailer != nil { - body := fmt.Sprintf("你的巡樓 Console 驗證碼是:%s\n\n此驗證碼 15 分鐘內有效。", verifyCode) - if err := l.svcCtx.Mailer.Send(l.ctx, member.Email, "巡樓 Console 帳號驗證碼", body); err != nil { - logx.WithContext(l.ctx).Errorf("send verification email failed: uid=%s err=%v", member.UID, err) - } - } return toAuthTokenData(token), nil } - -func newVerifyCode() (string, error) { - n, err := rand.Int(rand.Reader, big.NewInt(1000000)) - if err != nil { - return "", err - } - return fmt.Sprintf("%06d", n.Int64()), nil -} diff --git a/old/backend/internal/logic/auth/resend_verification_email_logic.go b/old/backend/internal/logic/auth/resend_verification_email_logic.go new file mode 100644 index 0000000..8af2482 --- /dev/null +++ b/old/backend/internal/logic/auth/resend_verification_email_logic.go @@ -0,0 +1,44 @@ +package auth + +import ( + "context" + + "haixun-backend/internal/library/authctx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ResendVerificationEmailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewResendVerificationEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResendVerificationEmailLogic { + return &ResendVerificationEmailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResendVerificationEmailLogic) ResendVerificationEmail() (*types.AuthResendVerificationData, error) { + actor, ok := authctx.ActorFromContext(l.ctx) + if !ok { + return nil, app.For(code.Auth).AuthUnauthorized("missing actor") + } + if l.svcCtx.Mailer == nil { + return nil, app.For(code.Auth).SysNotImplemented("smtp mailer is not configured") + } + err := l.svcCtx.Member.ResendEmailVerification(l.ctx, actor.TenantID, actor.UID, func(email, verifyCode string) error { + return sendEmailVerification(l.ctx, l.svcCtx, email, verifyCode) + }) + if err != nil { + return nil, err + } + return &types.AuthResendVerificationData{Sent: true}, nil +} \ No newline at end of file diff --git a/old/backend/internal/logic/auth/verify_email_logic.go b/old/backend/internal/logic/auth/verify_email_logic.go new file mode 100644 index 0000000..ed56bb0 --- /dev/null +++ b/old/backend/internal/logic/auth/verify_email_logic.go @@ -0,0 +1,39 @@ +package auth + +import ( + "context" + + "haixun-backend/internal/library/authctx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VerifyEmailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewVerifyEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyEmailLogic { + return &VerifyEmailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *VerifyEmailLogic) VerifyEmail(req *types.AuthVerifyEmailReq) (*types.AuthVerifyEmailData, error) { + actor, ok := authctx.ActorFromContext(l.ctx) + if !ok { + return nil, app.For(code.Auth).AuthUnauthorized("missing actor") + } + member, err := l.svcCtx.Member.VerifyEmail(l.ctx, actor.TenantID, actor.UID, req.Code) + if err != nil { + return nil, err + } + return &types.AuthVerifyEmailData{EmailVerified: member.EmailVerified}, nil +} \ No newline at end of file diff --git a/backend/internal/logic/authz/admin.go b/old/backend/internal/logic/authz/admin.go similarity index 100% rename from backend/internal/logic/authz/admin.go rename to old/backend/internal/logic/authz/admin.go diff --git a/backend/internal/logic/brand/actor.go b/old/backend/internal/logic/brand/actor.go similarity index 100% rename from backend/internal/logic/brand/actor.go rename to old/backend/internal/logic/brand/actor.go diff --git a/backend/internal/logic/brand/create_brand_logic.go b/old/backend/internal/logic/brand/create_brand_logic.go similarity index 100% rename from backend/internal/logic/brand/create_brand_logic.go rename to old/backend/internal/logic/brand/create_brand_logic.go diff --git a/backend/internal/logic/brand/create_brand_product_logic.go b/old/backend/internal/logic/brand/create_brand_product_logic.go similarity index 100% rename from backend/internal/logic/brand/create_brand_product_logic.go rename to old/backend/internal/logic/brand/create_brand_product_logic.go diff --git a/backend/internal/logic/brand/delete_brand_logic.go b/old/backend/internal/logic/brand/delete_brand_logic.go similarity index 100% rename from backend/internal/logic/brand/delete_brand_logic.go rename to old/backend/internal/logic/brand/delete_brand_logic.go diff --git a/backend/internal/logic/brand/delete_brand_product_logic.go b/old/backend/internal/logic/brand/delete_brand_product_logic.go similarity index 100% rename from backend/internal/logic/brand/delete_brand_product_logic.go rename to old/backend/internal/logic/brand/delete_brand_product_logic.go diff --git a/backend/internal/logic/brand/expand_knowledge_graph_logic.go b/old/backend/internal/logic/brand/expand_knowledge_graph_logic.go similarity index 100% rename from backend/internal/logic/brand/expand_knowledge_graph_logic.go rename to old/backend/internal/logic/brand/expand_knowledge_graph_logic.go diff --git a/backend/internal/logic/brand/generate_brand_content_matrix_logic.go b/old/backend/internal/logic/brand/generate_brand_content_matrix_logic.go similarity index 100% rename from backend/internal/logic/brand/generate_brand_content_matrix_logic.go rename to old/backend/internal/logic/brand/generate_brand_content_matrix_logic.go diff --git a/backend/internal/logic/brand/generate_outreach_drafts_logic.go b/old/backend/internal/logic/brand/generate_outreach_drafts_logic.go similarity index 100% rename from backend/internal/logic/brand/generate_outreach_drafts_logic.go rename to old/backend/internal/logic/brand/generate_outreach_drafts_logic.go diff --git a/backend/internal/logic/brand/get_brand_content_matrix_logic.go b/old/backend/internal/logic/brand/get_brand_content_matrix_logic.go similarity index 100% rename from backend/internal/logic/brand/get_brand_content_matrix_logic.go rename to old/backend/internal/logic/brand/get_brand_content_matrix_logic.go diff --git a/backend/internal/logic/brand/get_brand_logic.go b/old/backend/internal/logic/brand/get_brand_logic.go similarity index 100% rename from backend/internal/logic/brand/get_brand_logic.go rename to old/backend/internal/logic/brand/get_brand_logic.go diff --git a/backend/internal/logic/brand/get_brand_scan_schedule_logic.go b/old/backend/internal/logic/brand/get_brand_scan_schedule_logic.go similarity index 100% rename from backend/internal/logic/brand/get_brand_scan_schedule_logic.go rename to old/backend/internal/logic/brand/get_brand_scan_schedule_logic.go diff --git a/backend/internal/logic/brand/get_knowledge_graph_logic.go b/old/backend/internal/logic/brand/get_knowledge_graph_logic.go similarity index 100% rename from backend/internal/logic/brand/get_knowledge_graph_logic.go rename to old/backend/internal/logic/brand/get_knowledge_graph_logic.go diff --git a/backend/internal/logic/brand/list_brand_products_logic.go b/old/backend/internal/logic/brand/list_brand_products_logic.go similarity index 100% rename from backend/internal/logic/brand/list_brand_products_logic.go rename to old/backend/internal/logic/brand/list_brand_products_logic.go diff --git a/backend/internal/logic/brand/list_brand_scan_posts_logic.go b/old/backend/internal/logic/brand/list_brand_scan_posts_logic.go similarity index 100% rename from backend/internal/logic/brand/list_brand_scan_posts_logic.go rename to old/backend/internal/logic/brand/list_brand_scan_posts_logic.go diff --git a/backend/internal/logic/brand/list_brands_logic.go b/old/backend/internal/logic/brand/list_brands_logic.go similarity index 100% rename from backend/internal/logic/brand/list_brands_logic.go rename to old/backend/internal/logic/brand/list_brands_logic.go diff --git a/backend/internal/logic/brand/mapper.go b/old/backend/internal/logic/brand/mapper.go similarity index 100% rename from backend/internal/logic/brand/mapper.go rename to old/backend/internal/logic/brand/mapper.go diff --git a/backend/internal/logic/brand/patch_knowledge_graph_nodes_logic.go b/old/backend/internal/logic/brand/patch_knowledge_graph_nodes_logic.go similarity index 100% rename from backend/internal/logic/brand/patch_knowledge_graph_nodes_logic.go rename to old/backend/internal/logic/brand/patch_knowledge_graph_nodes_logic.go diff --git a/backend/internal/logic/brand/patch_scan_post_outreach_logic.go b/old/backend/internal/logic/brand/patch_scan_post_outreach_logic.go similarity index 100% rename from backend/internal/logic/brand/patch_scan_post_outreach_logic.go rename to old/backend/internal/logic/brand/patch_scan_post_outreach_logic.go diff --git a/backend/internal/logic/brand/publish_outreach_draft_logic.go b/old/backend/internal/logic/brand/publish_outreach_draft_logic.go similarity index 100% rename from backend/internal/logic/brand/publish_outreach_draft_logic.go rename to old/backend/internal/logic/brand/publish_outreach_draft_logic.go diff --git a/backend/internal/logic/brand/start_brand_scan_job_logic.go b/old/backend/internal/logic/brand/start_brand_scan_job_logic.go similarity index 100% rename from backend/internal/logic/brand/start_brand_scan_job_logic.go rename to old/backend/internal/logic/brand/start_brand_scan_job_logic.go diff --git a/backend/internal/logic/brand/update_brand_logic.go b/old/backend/internal/logic/brand/update_brand_logic.go similarity index 100% rename from backend/internal/logic/brand/update_brand_logic.go rename to old/backend/internal/logic/brand/update_brand_logic.go diff --git a/backend/internal/logic/brand/update_brand_product_logic.go b/old/backend/internal/logic/brand/update_brand_product_logic.go similarity index 100% rename from backend/internal/logic/brand/update_brand_product_logic.go rename to old/backend/internal/logic/brand/update_brand_product_logic.go diff --git a/backend/internal/logic/brand/update_outreach_draft_logic.go b/old/backend/internal/logic/brand/update_outreach_draft_logic.go similarity index 100% rename from backend/internal/logic/brand/update_outreach_draft_logic.go rename to old/backend/internal/logic/brand/update_outreach_draft_logic.go diff --git a/backend/internal/logic/brand/upsert_brand_scan_schedule_logic.go b/old/backend/internal/logic/brand/upsert_brand_scan_schedule_logic.go similarity index 100% rename from backend/internal/logic/brand/upsert_brand_scan_schedule_logic.go rename to old/backend/internal/logic/brand/upsert_brand_scan_schedule_logic.go diff --git a/backend/internal/logic/copy_mission/actor.go b/old/backend/internal/logic/copy_mission/actor.go similarity index 100% rename from backend/internal/logic/copy_mission/actor.go rename to old/backend/internal/logic/copy_mission/actor.go diff --git a/backend/internal/logic/copy_mission/inspire_copy_mission_logic.go b/old/backend/internal/logic/copy_mission/inspire_copy_mission_logic.go similarity index 100% rename from backend/internal/logic/copy_mission/inspire_copy_mission_logic.go rename to old/backend/internal/logic/copy_mission/inspire_copy_mission_logic.go diff --git a/backend/internal/logic/job/ack_worker_job_cancel_logic.go b/old/backend/internal/logic/job/ack_worker_job_cancel_logic.go similarity index 100% rename from backend/internal/logic/job/ack_worker_job_cancel_logic.go rename to old/backend/internal/logic/job/ack_worker_job_cancel_logic.go diff --git a/backend/internal/logic/job/actor.go b/old/backend/internal/logic/job/actor.go similarity index 100% rename from backend/internal/logic/job/actor.go rename to old/backend/internal/logic/job/actor.go diff --git a/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go b/old/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go similarity index 100% rename from backend/internal/logic/job/analyze_style8_d_from_worker_logic.go rename to old/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go diff --git a/backend/internal/logic/job/cancel_job_logic.go b/old/backend/internal/logic/job/cancel_job_logic.go similarity index 100% rename from backend/internal/logic/job/cancel_job_logic.go rename to old/backend/internal/logic/job/cancel_job_logic.go diff --git a/backend/internal/logic/job/check_worker_job_cancel_logic.go b/old/backend/internal/logic/job/check_worker_job_cancel_logic.go similarity index 100% rename from backend/internal/logic/job/check_worker_job_cancel_logic.go rename to old/backend/internal/logic/job/check_worker_job_cancel_logic.go diff --git a/backend/internal/logic/job/claim_worker_job_logic.go b/old/backend/internal/logic/job/claim_worker_job_logic.go similarity index 100% rename from backend/internal/logic/job/claim_worker_job_logic.go rename to old/backend/internal/logic/job/claim_worker_job_logic.go diff --git a/backend/internal/logic/job/complete_worker_job_logic.go b/old/backend/internal/logic/job/complete_worker_job_logic.go similarity index 100% rename from backend/internal/logic/job/complete_worker_job_logic.go rename to old/backend/internal/logic/job/complete_worker_job_logic.go diff --git a/backend/internal/logic/job/create_job_logic.go b/old/backend/internal/logic/job/create_job_logic.go similarity index 100% rename from backend/internal/logic/job/create_job_logic.go rename to old/backend/internal/logic/job/create_job_logic.go diff --git a/backend/internal/logic/job/create_job_schedule_logic.go b/old/backend/internal/logic/job/create_job_schedule_logic.go similarity index 100% rename from backend/internal/logic/job/create_job_schedule_logic.go rename to old/backend/internal/logic/job/create_job_schedule_logic.go diff --git a/backend/internal/logic/job/delete_job_schedule_logic.go b/old/backend/internal/logic/job/delete_job_schedule_logic.go similarity index 100% rename from backend/internal/logic/job/delete_job_schedule_logic.go rename to old/backend/internal/logic/job/delete_job_schedule_logic.go diff --git a/backend/internal/logic/job/disable_job_schedule_logic.go b/old/backend/internal/logic/job/disable_job_schedule_logic.go similarity index 100% rename from backend/internal/logic/job/disable_job_schedule_logic.go rename to old/backend/internal/logic/job/disable_job_schedule_logic.go diff --git a/backend/internal/logic/job/enable_job_schedule_logic.go b/old/backend/internal/logic/job/enable_job_schedule_logic.go similarity index 100% rename from backend/internal/logic/job/enable_job_schedule_logic.go rename to old/backend/internal/logic/job/enable_job_schedule_logic.go diff --git a/backend/internal/logic/job/fail_worker_job_logic.go b/old/backend/internal/logic/job/fail_worker_job_logic.go similarity index 100% rename from backend/internal/logic/job/fail_worker_job_logic.go rename to old/backend/internal/logic/job/fail_worker_job_logic.go diff --git a/backend/internal/logic/job/get_job_logic.go b/old/backend/internal/logic/job/get_job_logic.go similarity index 100% rename from backend/internal/logic/job/get_job_logic.go rename to old/backend/internal/logic/job/get_job_logic.go diff --git a/backend/internal/logic/job/get_job_template_logic.go b/old/backend/internal/logic/job/get_job_template_logic.go similarity index 100% rename from backend/internal/logic/job/get_job_template_logic.go rename to old/backend/internal/logic/job/get_job_template_logic.go diff --git a/backend/internal/logic/job/get_worker_threads_account_session_logic.go b/old/backend/internal/logic/job/get_worker_threads_account_session_logic.go similarity index 100% rename from backend/internal/logic/job/get_worker_threads_account_session_logic.go rename to old/backend/internal/logic/job/get_worker_threads_account_session_logic.go diff --git a/backend/internal/logic/job/list_job_events_logic.go b/old/backend/internal/logic/job/list_job_events_logic.go similarity index 100% rename from backend/internal/logic/job/list_job_events_logic.go rename to old/backend/internal/logic/job/list_job_events_logic.go diff --git a/backend/internal/logic/job/list_job_schedules_logic.go b/old/backend/internal/logic/job/list_job_schedules_logic.go similarity index 100% rename from backend/internal/logic/job/list_job_schedules_logic.go rename to old/backend/internal/logic/job/list_job_schedules_logic.go diff --git a/backend/internal/logic/job/list_job_templates_logic.go b/old/backend/internal/logic/job/list_job_templates_logic.go similarity index 100% rename from backend/internal/logic/job/list_job_templates_logic.go rename to old/backend/internal/logic/job/list_job_templates_logic.go diff --git a/backend/internal/logic/job/list_jobs_logic.go b/old/backend/internal/logic/job/list_jobs_logic.go similarity index 100% rename from backend/internal/logic/job/list_jobs_logic.go rename to old/backend/internal/logic/job/list_jobs_logic.go diff --git a/backend/internal/logic/job/mapper.go b/old/backend/internal/logic/job/mapper.go similarity index 100% rename from backend/internal/logic/job/mapper.go rename to old/backend/internal/logic/job/mapper.go diff --git a/backend/internal/logic/job/ownership.go b/old/backend/internal/logic/job/ownership.go similarity index 100% rename from backend/internal/logic/job/ownership.go rename to old/backend/internal/logic/job/ownership.go diff --git a/backend/internal/logic/job/ownership_test.go b/old/backend/internal/logic/job/ownership_test.go similarity index 100% rename from backend/internal/logic/job/ownership_test.go rename to old/backend/internal/logic/job/ownership_test.go diff --git a/backend/internal/logic/job/refresh_worker_job_lock_logic.go b/old/backend/internal/logic/job/refresh_worker_job_lock_logic.go similarity index 100% rename from backend/internal/logic/job/refresh_worker_job_lock_logic.go rename to old/backend/internal/logic/job/refresh_worker_job_lock_logic.go diff --git a/backend/internal/logic/job/retry_job_logic.go b/old/backend/internal/logic/job/retry_job_logic.go similarity index 100% rename from backend/internal/logic/job/retry_job_logic.go rename to old/backend/internal/logic/job/retry_job_logic.go diff --git a/backend/internal/logic/job/store_persona_style_profile_from_worker_logic.go b/old/backend/internal/logic/job/store_persona_style_profile_from_worker_logic.go similarity index 100% rename from backend/internal/logic/job/store_persona_style_profile_from_worker_logic.go rename to old/backend/internal/logic/job/store_persona_style_profile_from_worker_logic.go diff --git a/backend/internal/logic/job/update_job_schedule_logic.go b/old/backend/internal/logic/job/update_job_schedule_logic.go similarity index 100% rename from backend/internal/logic/job/update_job_schedule_logic.go rename to old/backend/internal/logic/job/update_job_schedule_logic.go diff --git a/backend/internal/logic/job/update_worker_job_progress_logic.go b/old/backend/internal/logic/job/update_worker_job_progress_logic.go similarity index 100% rename from backend/internal/logic/job/update_worker_job_progress_logic.go rename to old/backend/internal/logic/job/update_worker_job_progress_logic.go diff --git a/backend/internal/logic/job/upsert_job_template_logic.go b/old/backend/internal/logic/job/upsert_job_template_logic.go similarity index 100% rename from backend/internal/logic/job/upsert_job_template_logic.go rename to old/backend/internal/logic/job/upsert_job_template_logic.go diff --git a/backend/internal/logic/job/worker_helpers.go b/old/backend/internal/logic/job/worker_helpers.go similarity index 100% rename from backend/internal/logic/job/worker_helpers.go rename to old/backend/internal/logic/job/worker_helpers.go diff --git a/backend/internal/logic/member/ai_settings_mapper.go b/old/backend/internal/logic/member/ai_settings_mapper.go similarity index 100% rename from backend/internal/logic/member/ai_settings_mapper.go rename to old/backend/internal/logic/member/ai_settings_mapper.go diff --git a/old/backend/internal/logic/member/avatar_ref.go b/old/backend/internal/logic/member/avatar_ref.go new file mode 100644 index 0000000..88b5089 --- /dev/null +++ b/old/backend/internal/logic/member/avatar_ref.go @@ -0,0 +1,47 @@ +package member + +import ( + "haixun-backend/internal/library/storage" + "haixun-backend/internal/model/member/domain/entity" + "haixun-backend/internal/types" +) + +func normalizeAvatarRef(bucket, ref string) string { + return storage.NormalizeObjectKey(bucket, ref) +} + +func optionalNormalizedAvatar(bucket, value string) *string { + if value == "" { + return nil + } + normalized := normalizeAvatarRef(bucket, value) + return &normalized +} + +func optionalNormalizedAvatarPtr(bucket string, value *string) *string { + if value == nil { + return nil + } + return optionalNormalizedAvatar(bucket, *value) +} + +func toMemberMeDataWithAvatar(member *entity.Member, bucket string) *types.MemberMeData { + data := toMemberMeData(member) + if data == nil || bucket == "" { + return data + } + data.Avatar = normalizeAvatarRef(bucket, data.Avatar) + return data +} + +func toMemberMeDataListWithAvatar(members []*entity.Member, bucket string) []types.MemberMeData { + list := make([]types.MemberMeData, 0, len(members)) + for _, item := range members { + data := toMemberMeDataWithAvatar(item, bucket) + if data == nil { + continue + } + list = append(list, *data) + } + return list +} diff --git a/backend/internal/logic/member/get_member_ai_settings_logic.go b/old/backend/internal/logic/member/get_member_ai_settings_logic.go similarity index 100% rename from backend/internal/logic/member/get_member_ai_settings_logic.go rename to old/backend/internal/logic/member/get_member_ai_settings_logic.go diff --git a/backend/internal/logic/member/get_member_capabilities_logic.go b/old/backend/internal/logic/member/get_member_capabilities_logic.go similarity index 100% rename from backend/internal/logic/member/get_member_capabilities_logic.go rename to old/backend/internal/logic/member/get_member_capabilities_logic.go diff --git a/backend/internal/logic/member/get_member_me_logic.go b/old/backend/internal/logic/member/get_member_me_logic.go similarity index 90% rename from backend/internal/logic/member/get_member_me_logic.go rename to old/backend/internal/logic/member/get_member_me_logic.go index 7dc61bd..c4eae76 100644 --- a/backend/internal/logic/member/get_member_me_logic.go +++ b/old/backend/internal/logic/member/get_member_me_logic.go @@ -28,5 +28,5 @@ func (l *GetMemberMeLogic) GetMemberMe() (*types.MemberMeData, error) { if err != nil { return nil, err } - return toMemberMeData(member), nil + return toMemberMeDataWithAvatar(member, l.svcCtx.Config.Storage.S3.Bucket), nil } diff --git a/backend/internal/logic/member/get_member_placement_settings_logic.go b/old/backend/internal/logic/member/get_member_placement_settings_logic.go similarity index 100% rename from backend/internal/logic/member/get_member_placement_settings_logic.go rename to old/backend/internal/logic/member/get_member_placement_settings_logic.go diff --git a/backend/internal/logic/member/list_member_ai_provider_models_logic.go b/old/backend/internal/logic/member/list_member_ai_provider_models_logic.go similarity index 100% rename from backend/internal/logic/member/list_member_ai_provider_models_logic.go rename to old/backend/internal/logic/member/list_member_ai_provider_models_logic.go diff --git a/backend/internal/logic/member/list_members_logic.go b/old/backend/internal/logic/member/list_members_logic.go similarity index 93% rename from backend/internal/logic/member/list_members_logic.go rename to old/backend/internal/logic/member/list_members_logic.go index 10044b9..a0d7903 100644 --- a/backend/internal/logic/member/list_members_logic.go +++ b/old/backend/internal/logic/member/list_members_logic.go @@ -51,6 +51,6 @@ func (l *ListMembersLogic) ListMembers(req *types.ListMembersReq) (resp *types.L PageSize: result.PageSize, TotalPages: result.TotalPages, }, - List: toMemberMeDataList(result.List), + List: toMemberMeDataListWithAvatar(result.List, l.svcCtx.Config.Storage.S3.Bucket), }, nil } diff --git a/backend/internal/logic/member/mapper.go b/old/backend/internal/logic/member/mapper.go similarity index 100% rename from backend/internal/logic/member/mapper.go rename to old/backend/internal/logic/member/mapper.go diff --git a/backend/internal/logic/member/update_member_ai_settings_logic.go b/old/backend/internal/logic/member/update_member_ai_settings_logic.go similarity index 100% rename from backend/internal/logic/member/update_member_ai_settings_logic.go rename to old/backend/internal/logic/member/update_member_ai_settings_logic.go diff --git a/backend/internal/logic/member/update_member_me_logic.go b/old/backend/internal/logic/member/update_member_me_logic.go similarity index 88% rename from backend/internal/logic/member/update_member_me_logic.go rename to old/backend/internal/logic/member/update_member_me_logic.go index d06ac4d..c8fd889 100644 --- a/backend/internal/logic/member/update_member_me_logic.go +++ b/old/backend/internal/logic/member/update_member_me_logic.go @@ -25,11 +25,12 @@ func (l *UpdateMemberMeLogic) UpdateMemberMe(req *types.UpdateMemberMeReq) (*typ if !ok { return nil, app.For(code.Auth).AuthUnauthorized("missing actor") } + bucket := l.svcCtx.Config.Storage.S3.Bucket member, err := l.svcCtx.Member.UpdateProfile(l.ctx, memberusecase.UpdateProfileRequest{ TenantID: actor.TenantID, UID: actor.UID, DisplayName: optionalString(req.DisplayName), - Avatar: optionalString(req.Avatar), + Avatar: optionalNormalizedAvatar(bucket, req.Avatar), Language: optionalString(req.Language), Currency: optionalString(req.Currency), Phone: optionalString(req.Phone), @@ -37,7 +38,7 @@ func (l *UpdateMemberMeLogic) UpdateMemberMe(req *types.UpdateMemberMeReq) (*typ if err != nil { return nil, err } - return toMemberMeData(member), nil + return toMemberMeDataWithAvatar(member, bucket), nil } func optionalString(value string) *string { diff --git a/backend/internal/logic/member/update_member_password_logic.go b/old/backend/internal/logic/member/update_member_password_logic.go similarity index 100% rename from backend/internal/logic/member/update_member_password_logic.go rename to old/backend/internal/logic/member/update_member_password_logic.go diff --git a/backend/internal/logic/member/update_member_placement_settings_logic.go b/old/backend/internal/logic/member/update_member_placement_settings_logic.go similarity index 100% rename from backend/internal/logic/member/update_member_placement_settings_logic.go rename to old/backend/internal/logic/member/update_member_placement_settings_logic.go diff --git a/backend/internal/logic/member/update_member_profile_logic.go b/old/backend/internal/logic/member/update_member_profile_logic.go similarity index 90% rename from backend/internal/logic/member/update_member_profile_logic.go rename to old/backend/internal/logic/member/update_member_profile_logic.go index 5651579..c3befaf 100644 --- a/backend/internal/logic/member/update_member_profile_logic.go +++ b/old/backend/internal/logic/member/update_member_profile_logic.go @@ -36,11 +36,12 @@ func (l *UpdateMemberProfileLogic) UpdateMemberProfile(req *types.UpdateMemberPr if err != nil { return nil, err } + bucket := l.svcCtx.Config.Storage.S3.Bucket member, err := l.svcCtx.Member.UpdateProfile(l.ctx, memberusecase.UpdateProfileRequest{ TenantID: tenantID, UID: req.UID, DisplayName: req.DisplayName, - Avatar: req.Avatar, + Avatar: optionalNormalizedAvatarPtr(bucket, req.Avatar), Language: req.Language, Currency: req.Currency, Phone: req.Phone, @@ -54,5 +55,5 @@ func (l *UpdateMemberProfileLogic) UpdateMemberProfile(req *types.UpdateMemberPr if err != nil { return nil, err } - return toMemberMeData(member), nil + return toMemberMeDataWithAvatar(member, bucket), nil } diff --git a/backend/internal/logic/member/update_member_roles_logic.go b/old/backend/internal/logic/member/update_member_roles_logic.go similarity index 100% rename from backend/internal/logic/member/update_member_roles_logic.go rename to old/backend/internal/logic/member/update_member_roles_logic.go diff --git a/backend/internal/logic/member/upload_member_avatar_logic.go b/old/backend/internal/logic/member/upload_member_avatar_logic.go similarity index 89% rename from backend/internal/logic/member/upload_member_avatar_logic.go rename to old/backend/internal/logic/member/upload_member_avatar_logic.go index fb661f8..dcd70cc 100644 --- a/backend/internal/logic/member/upload_member_avatar_logic.go +++ b/old/backend/internal/logic/member/upload_member_avatar_logic.go @@ -48,14 +48,14 @@ func (l *UploadMemberAvatarLogic) UploadMemberAvatar(filename, contentType strin return nil, app.For(code.Member).InputInvalidFormat("avatar file must be an image") } key := "avatars/" + tenantID + "/" + uid + ext - url, err := l.svcCtx.AvatarStorage.Upload(l.ctx, key, contentType, body) + storedKey, err := l.svcCtx.AvatarStorage.Upload(l.ctx, key, contentType, body) if err != nil { return nil, app.For(code.Member).SysInternal("upload avatar failed").WithCause(err) } - if _, err := l.svcCtx.Member.UpdateProfile(l.ctx, memberusecase.UpdateProfileRequest{TenantID: tenantID, UID: uid, Avatar: &url}); err != nil { + if _, err := l.svcCtx.Member.UpdateProfile(l.ctx, memberusecase.UpdateProfileRequest{TenantID: tenantID, UID: uid, Avatar: &storedKey}); err != nil { return nil, err } - return &types.UploadAvatarData{Avatar: url}, nil + return &types.UploadAvatarData{Avatar: storedKey}, nil } func allowedAvatarExt(ext string) bool { diff --git a/backend/internal/logic/normal/health_logic.go b/old/backend/internal/logic/normal/health_logic.go similarity index 100% rename from backend/internal/logic/normal/health_logic.go rename to old/backend/internal/logic/normal/health_logic.go diff --git a/backend/internal/logic/permission/get_me_permissions_logic.go b/old/backend/internal/logic/permission/get_me_permissions_logic.go similarity index 100% rename from backend/internal/logic/permission/get_me_permissions_logic.go rename to old/backend/internal/logic/permission/get_me_permissions_logic.go diff --git a/backend/internal/logic/permission/get_permission_catalog_logic.go b/old/backend/internal/logic/permission/get_permission_catalog_logic.go similarity index 100% rename from backend/internal/logic/permission/get_permission_catalog_logic.go rename to old/backend/internal/logic/permission/get_permission_catalog_logic.go diff --git a/backend/internal/logic/permission/mapper.go b/old/backend/internal/logic/permission/mapper.go similarity index 100% rename from backend/internal/logic/permission/mapper.go rename to old/backend/internal/logic/permission/mapper.go diff --git a/backend/internal/logic/persona/actor.go b/old/backend/internal/logic/persona/actor.go similarity index 100% rename from backend/internal/logic/persona/actor.go rename to old/backend/internal/logic/persona/actor.go diff --git a/backend/internal/logic/persona/content_ops_mapper.go b/old/backend/internal/logic/persona/content_ops_mapper.go similarity index 100% rename from backend/internal/logic/persona/content_ops_mapper.go rename to old/backend/internal/logic/persona/content_ops_mapper.go diff --git a/backend/internal/logic/persona/copy_draft_mapper.go b/old/backend/internal/logic/persona/copy_draft_mapper.go similarity index 100% rename from backend/internal/logic/persona/copy_draft_mapper.go rename to old/backend/internal/logic/persona/copy_draft_mapper.go diff --git a/backend/internal/logic/persona/create_content_plan_logic.go b/old/backend/internal/logic/persona/create_content_plan_logic.go similarity index 100% rename from backend/internal/logic/persona/create_content_plan_logic.go rename to old/backend/internal/logic/persona/create_content_plan_logic.go diff --git a/backend/internal/logic/persona/create_feedback_event_logic.go b/old/backend/internal/logic/persona/create_feedback_event_logic.go similarity index 100% rename from backend/internal/logic/persona/create_feedback_event_logic.go rename to old/backend/internal/logic/persona/create_feedback_event_logic.go diff --git a/backend/internal/logic/persona/create_formula_pool_logic.go b/old/backend/internal/logic/persona/create_formula_pool_logic.go similarity index 100% rename from backend/internal/logic/persona/create_formula_pool_logic.go rename to old/backend/internal/logic/persona/create_formula_pool_logic.go diff --git a/backend/internal/logic/persona/create_knowledge_source_logic.go b/old/backend/internal/logic/persona/create_knowledge_source_logic.go similarity index 100% rename from backend/internal/logic/persona/create_knowledge_source_logic.go rename to old/backend/internal/logic/persona/create_knowledge_source_logic.go diff --git a/backend/internal/logic/persona/create_persona_logic.go b/old/backend/internal/logic/persona/create_persona_logic.go similarity index 100% rename from backend/internal/logic/persona/create_persona_logic.go rename to old/backend/internal/logic/persona/create_persona_logic.go diff --git a/backend/internal/logic/persona/delete_persona_copy_draft_logic.go b/old/backend/internal/logic/persona/delete_persona_copy_draft_logic.go similarity index 100% rename from backend/internal/logic/persona/delete_persona_copy_draft_logic.go rename to old/backend/internal/logic/persona/delete_persona_copy_draft_logic.go diff --git a/backend/internal/logic/persona/delete_persona_logic.go b/old/backend/internal/logic/persona/delete_persona_logic.go similarity index 100% rename from backend/internal/logic/persona/delete_persona_logic.go rename to old/backend/internal/logic/persona/delete_persona_logic.go diff --git a/backend/internal/logic/persona/delete_style_preset_logic.go b/old/backend/internal/logic/persona/delete_style_preset_logic.go similarity index 100% rename from backend/internal/logic/persona/delete_style_preset_logic.go rename to old/backend/internal/logic/persona/delete_style_preset_logic.go diff --git a/backend/internal/logic/persona/generate_from_content_formula_logic.go b/old/backend/internal/logic/persona/generate_from_content_formula_logic.go similarity index 100% rename from backend/internal/logic/persona/generate_from_content_formula_logic.go rename to old/backend/internal/logic/persona/generate_from_content_formula_logic.go diff --git a/backend/internal/logic/persona/generate_persona_copy_draft_logic.go b/old/backend/internal/logic/persona/generate_persona_copy_draft_logic.go similarity index 100% rename from backend/internal/logic/persona/generate_persona_copy_draft_logic.go rename to old/backend/internal/logic/persona/generate_persona_copy_draft_logic.go diff --git a/backend/internal/logic/persona/generate_persona_topic_matrix_logic.go b/old/backend/internal/logic/persona/generate_persona_topic_matrix_logic.go similarity index 100% rename from backend/internal/logic/persona/generate_persona_topic_matrix_logic.go rename to old/backend/internal/logic/persona/generate_persona_topic_matrix_logic.go diff --git a/backend/internal/logic/persona/get_persona_logic.go b/old/backend/internal/logic/persona/get_persona_logic.go similarity index 100% rename from backend/internal/logic/persona/get_persona_logic.go rename to old/backend/internal/logic/persona/get_persona_logic.go diff --git a/backend/internal/logic/persona/list_content_plans_logic.go b/old/backend/internal/logic/persona/list_content_plans_logic.go similarity index 100% rename from backend/internal/logic/persona/list_content_plans_logic.go rename to old/backend/internal/logic/persona/list_content_plans_logic.go diff --git a/backend/internal/logic/persona/list_formula_pools_logic.go b/old/backend/internal/logic/persona/list_formula_pools_logic.go similarity index 100% rename from backend/internal/logic/persona/list_formula_pools_logic.go rename to old/backend/internal/logic/persona/list_formula_pools_logic.go diff --git a/backend/internal/logic/persona/list_knowledge_chunks_logic.go b/old/backend/internal/logic/persona/list_knowledge_chunks_logic.go similarity index 100% rename from backend/internal/logic/persona/list_knowledge_chunks_logic.go rename to old/backend/internal/logic/persona/list_knowledge_chunks_logic.go diff --git a/backend/internal/logic/persona/list_knowledge_sources_logic.go b/old/backend/internal/logic/persona/list_knowledge_sources_logic.go similarity index 100% rename from backend/internal/logic/persona/list_knowledge_sources_logic.go rename to old/backend/internal/logic/persona/list_knowledge_sources_logic.go diff --git a/backend/internal/logic/persona/list_persona_content_inbox_logic.go b/old/backend/internal/logic/persona/list_persona_content_inbox_logic.go similarity index 100% rename from backend/internal/logic/persona/list_persona_content_inbox_logic.go rename to old/backend/internal/logic/persona/list_persona_content_inbox_logic.go diff --git a/backend/internal/logic/persona/list_persona_copy_drafts_logic.go b/old/backend/internal/logic/persona/list_persona_copy_drafts_logic.go similarity index 100% rename from backend/internal/logic/persona/list_persona_copy_drafts_logic.go rename to old/backend/internal/logic/persona/list_persona_copy_drafts_logic.go diff --git a/backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go b/old/backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go similarity index 100% rename from backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go rename to old/backend/internal/logic/persona/list_persona_viral_scan_posts_logic.go diff --git a/backend/internal/logic/persona/list_personas_logic.go b/old/backend/internal/logic/persona/list_personas_logic.go similarity index 100% rename from backend/internal/logic/persona/list_personas_logic.go rename to old/backend/internal/logic/persona/list_personas_logic.go diff --git a/backend/internal/logic/persona/list_style_presets_logic.go b/old/backend/internal/logic/persona/list_style_presets_logic.go similarity index 100% rename from backend/internal/logic/persona/list_style_presets_logic.go rename to old/backend/internal/logic/persona/list_style_presets_logic.go diff --git a/backend/internal/logic/persona/list_topic_candidates_logic.go b/old/backend/internal/logic/persona/list_topic_candidates_logic.go similarity index 100% rename from backend/internal/logic/persona/list_topic_candidates_logic.go rename to old/backend/internal/logic/persona/list_topic_candidates_logic.go diff --git a/backend/internal/logic/persona/mapper.go b/old/backend/internal/logic/persona/mapper.go similarity index 100% rename from backend/internal/logic/persona/mapper.go rename to old/backend/internal/logic/persona/mapper.go diff --git a/backend/internal/logic/persona/prune_persona_copy_drafts_logic.go b/old/backend/internal/logic/persona/prune_persona_copy_drafts_logic.go similarity index 100% rename from backend/internal/logic/persona/prune_persona_copy_drafts_logic.go rename to old/backend/internal/logic/persona/prune_persona_copy_drafts_logic.go diff --git a/backend/internal/logic/persona/publish_persona_copy_draft_logic.go b/old/backend/internal/logic/persona/publish_persona_copy_draft_logic.go similarity index 100% rename from backend/internal/logic/persona/publish_persona_copy_draft_logic.go rename to old/backend/internal/logic/persona/publish_persona_copy_draft_logic.go diff --git a/backend/internal/logic/persona/schedule_persona_copy_draft_logic.go b/old/backend/internal/logic/persona/schedule_persona_copy_draft_logic.go similarity index 100% rename from backend/internal/logic/persona/schedule_persona_copy_draft_logic.go rename to old/backend/internal/logic/persona/schedule_persona_copy_draft_logic.go diff --git a/backend/internal/logic/persona/schedule_persona_drafts_logic.go b/old/backend/internal/logic/persona/schedule_persona_drafts_logic.go similarity index 100% rename from backend/internal/logic/persona/schedule_persona_drafts_logic.go rename to old/backend/internal/logic/persona/schedule_persona_drafts_logic.go diff --git a/backend/internal/logic/persona/start_persona_formula_draft_job_logic.go b/old/backend/internal/logic/persona/start_persona_formula_draft_job_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_formula_draft_job_logic.go rename to old/backend/internal/logic/persona/start_persona_formula_draft_job_logic.go diff --git a/backend/internal/logic/persona/start_persona_rewrite_draft_job_logic.go b/old/backend/internal/logic/persona/start_persona_rewrite_draft_job_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_rewrite_draft_job_logic.go rename to old/backend/internal/logic/persona/start_persona_rewrite_draft_job_logic.go diff --git a/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go b/old/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go rename to old/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go diff --git a/backend/internal/logic/persona/start_persona_style_analysis_logic.go b/old/backend/internal/logic/persona/start_persona_style_analysis_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_style_analysis_logic.go rename to old/backend/internal/logic/persona/start_persona_style_analysis_logic.go diff --git a/backend/internal/logic/persona/start_persona_topic_matrix_job_logic.go b/old/backend/internal/logic/persona/start_persona_topic_matrix_job_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_topic_matrix_job_logic.go rename to old/backend/internal/logic/persona/start_persona_topic_matrix_job_logic.go diff --git a/backend/internal/logic/persona/start_persona_viral_scan_job_logic.go b/old/backend/internal/logic/persona/start_persona_viral_scan_job_logic.go similarity index 100% rename from backend/internal/logic/persona/start_persona_viral_scan_job_logic.go rename to old/backend/internal/logic/persona/start_persona_viral_scan_job_logic.go diff --git a/backend/internal/logic/persona/update_content_plan_logic.go b/old/backend/internal/logic/persona/update_content_plan_logic.go similarity index 100% rename from backend/internal/logic/persona/update_content_plan_logic.go rename to old/backend/internal/logic/persona/update_content_plan_logic.go diff --git a/backend/internal/logic/persona/update_persona_copy_draft_logic.go b/old/backend/internal/logic/persona/update_persona_copy_draft_logic.go similarity index 100% rename from backend/internal/logic/persona/update_persona_copy_draft_logic.go rename to old/backend/internal/logic/persona/update_persona_copy_draft_logic.go diff --git a/backend/internal/logic/persona/update_persona_logic.go b/old/backend/internal/logic/persona/update_persona_logic.go similarity index 100% rename from backend/internal/logic/persona/update_persona_logic.go rename to old/backend/internal/logic/persona/update_persona_logic.go diff --git a/backend/internal/logic/persona/upsert_style_preset_logic.go b/old/backend/internal/logic/persona/upsert_style_preset_logic.go similarity index 100% rename from backend/internal/logic/persona/upsert_style_preset_logic.go rename to old/backend/internal/logic/persona/upsert_style_preset_logic.go diff --git a/backend/internal/logic/placement_topic/actor.go b/old/backend/internal/logic/placement_topic/actor.go similarity index 100% rename from backend/internal/logic/placement_topic/actor.go rename to old/backend/internal/logic/placement_topic/actor.go diff --git a/backend/internal/logic/placement_topic/batch_delete_placement_topic_scan_posts_logic.go b/old/backend/internal/logic/placement_topic/batch_delete_placement_topic_scan_posts_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/batch_delete_placement_topic_scan_posts_logic.go rename to old/backend/internal/logic/placement_topic/batch_delete_placement_topic_scan_posts_logic.go diff --git a/backend/internal/logic/placement_topic/create_placement_topic_logic.go b/old/backend/internal/logic/placement_topic/create_placement_topic_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/create_placement_topic_logic.go rename to old/backend/internal/logic/placement_topic/create_placement_topic_logic.go diff --git a/backend/internal/logic/placement_topic/delete_placement_topic_logic.go b/old/backend/internal/logic/placement_topic/delete_placement_topic_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/delete_placement_topic_logic.go rename to old/backend/internal/logic/placement_topic/delete_placement_topic_logic.go diff --git a/backend/internal/logic/placement_topic/delete_placement_topic_scan_post_logic.go b/old/backend/internal/logic/placement_topic/delete_placement_topic_scan_post_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/delete_placement_topic_scan_post_logic.go rename to old/backend/internal/logic/placement_topic/delete_placement_topic_scan_post_logic.go diff --git a/backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go b/old/backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go rename to old/backend/internal/logic/placement_topic/expand_placement_topic_graph_logic.go diff --git a/backend/internal/logic/placement_topic/generate_placement_topic_content_matrix_logic.go b/old/backend/internal/logic/placement_topic/generate_placement_topic_content_matrix_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/generate_placement_topic_content_matrix_logic.go rename to old/backend/internal/logic/placement_topic/generate_placement_topic_content_matrix_logic.go diff --git a/backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go b/old/backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go rename to old/backend/internal/logic/placement_topic/generate_placement_topic_outreach_drafts_logic.go diff --git a/backend/internal/logic/placement_topic/get_placement_topic_content_matrix_logic.go b/old/backend/internal/logic/placement_topic/get_placement_topic_content_matrix_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/get_placement_topic_content_matrix_logic.go rename to old/backend/internal/logic/placement_topic/get_placement_topic_content_matrix_logic.go diff --git a/backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go b/old/backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go rename to old/backend/internal/logic/placement_topic/get_placement_topic_graph_logic.go diff --git a/backend/internal/logic/placement_topic/get_placement_topic_logic.go b/old/backend/internal/logic/placement_topic/get_placement_topic_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/get_placement_topic_logic.go rename to old/backend/internal/logic/placement_topic/get_placement_topic_logic.go diff --git a/backend/internal/logic/placement_topic/get_placement_topic_scan_schedule_logic.go b/old/backend/internal/logic/placement_topic/get_placement_topic_scan_schedule_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/get_placement_topic_scan_schedule_logic.go rename to old/backend/internal/logic/placement_topic/get_placement_topic_scan_schedule_logic.go diff --git a/backend/internal/logic/placement_topic/list_placement_topic_scan_posts_logic.go b/old/backend/internal/logic/placement_topic/list_placement_topic_scan_posts_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/list_placement_topic_scan_posts_logic.go rename to old/backend/internal/logic/placement_topic/list_placement_topic_scan_posts_logic.go diff --git a/backend/internal/logic/placement_topic/list_placement_topics_logic.go b/old/backend/internal/logic/placement_topic/list_placement_topics_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/list_placement_topics_logic.go rename to old/backend/internal/logic/placement_topic/list_placement_topics_logic.go diff --git a/backend/internal/logic/placement_topic/mapper.go b/old/backend/internal/logic/placement_topic/mapper.go similarity index 100% rename from backend/internal/logic/placement_topic/mapper.go rename to old/backend/internal/logic/placement_topic/mapper.go diff --git a/backend/internal/logic/placement_topic/patch_placement_topic_graph_nodes_logic.go b/old/backend/internal/logic/placement_topic/patch_placement_topic_graph_nodes_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/patch_placement_topic_graph_nodes_logic.go rename to old/backend/internal/logic/placement_topic/patch_placement_topic_graph_nodes_logic.go diff --git a/backend/internal/logic/placement_topic/patch_placement_topic_scan_post_outreach_logic.go b/old/backend/internal/logic/placement_topic/patch_placement_topic_scan_post_outreach_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/patch_placement_topic_scan_post_outreach_logic.go rename to old/backend/internal/logic/placement_topic/patch_placement_topic_scan_post_outreach_logic.go diff --git a/backend/internal/logic/placement_topic/publish_placement_topic_outreach_draft_logic.go b/old/backend/internal/logic/placement_topic/publish_placement_topic_outreach_draft_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/publish_placement_topic_outreach_draft_logic.go rename to old/backend/internal/logic/placement_topic/publish_placement_topic_outreach_draft_logic.go diff --git a/backend/internal/logic/placement_topic/scope.go b/old/backend/internal/logic/placement_topic/scope.go similarity index 100% rename from backend/internal/logic/placement_topic/scope.go rename to old/backend/internal/logic/placement_topic/scope.go diff --git a/backend/internal/logic/placement_topic/start_placement_topic_outreach_draft_job_logic.go b/old/backend/internal/logic/placement_topic/start_placement_topic_outreach_draft_job_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/start_placement_topic_outreach_draft_job_logic.go rename to old/backend/internal/logic/placement_topic/start_placement_topic_outreach_draft_job_logic.go diff --git a/backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go b/old/backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go rename to old/backend/internal/logic/placement_topic/start_placement_topic_scan_job_logic.go diff --git a/backend/internal/logic/placement_topic/update_placement_topic_logic.go b/old/backend/internal/logic/placement_topic/update_placement_topic_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/update_placement_topic_logic.go rename to old/backend/internal/logic/placement_topic/update_placement_topic_logic.go diff --git a/backend/internal/logic/placement_topic/update_placement_topic_outreach_draft_logic.go b/old/backend/internal/logic/placement_topic/update_placement_topic_outreach_draft_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/update_placement_topic_outreach_draft_logic.go rename to old/backend/internal/logic/placement_topic/update_placement_topic_outreach_draft_logic.go diff --git a/backend/internal/logic/placement_topic/upsert_placement_topic_scan_schedule_logic.go b/old/backend/internal/logic/placement_topic/upsert_placement_topic_scan_schedule_logic.go similarity index 100% rename from backend/internal/logic/placement_topic/upsert_placement_topic_scan_schedule_logic.go rename to old/backend/internal/logic/placement_topic/upsert_placement_topic_scan_schedule_logic.go diff --git a/backend/internal/logic/setting/authz.go b/old/backend/internal/logic/setting/authz.go similarity index 100% rename from backend/internal/logic/setting/authz.go rename to old/backend/internal/logic/setting/authz.go diff --git a/backend/internal/logic/setting/delete_setting_logic.go b/old/backend/internal/logic/setting/delete_setting_logic.go similarity index 100% rename from backend/internal/logic/setting/delete_setting_logic.go rename to old/backend/internal/logic/setting/delete_setting_logic.go diff --git a/backend/internal/logic/setting/get_setting_logic.go b/old/backend/internal/logic/setting/get_setting_logic.go similarity index 100% rename from backend/internal/logic/setting/get_setting_logic.go rename to old/backend/internal/logic/setting/get_setting_logic.go diff --git a/backend/internal/logic/setting/list_settings_logic.go b/old/backend/internal/logic/setting/list_settings_logic.go similarity index 100% rename from backend/internal/logic/setting/list_settings_logic.go rename to old/backend/internal/logic/setting/list_settings_logic.go diff --git a/backend/internal/logic/setting/mapper.go b/old/backend/internal/logic/setting/mapper.go similarity index 100% rename from backend/internal/logic/setting/mapper.go rename to old/backend/internal/logic/setting/mapper.go diff --git a/backend/internal/logic/setting/upsert_setting_logic.go b/old/backend/internal/logic/setting/upsert_setting_logic.go similarity index 100% rename from backend/internal/logic/setting/upsert_setting_logic.go rename to old/backend/internal/logic/setting/upsert_setting_logic.go diff --git a/backend/internal/logic/threads_account/activate_threads_account_logic.go b/old/backend/internal/logic/threads_account/activate_threads_account_logic.go similarity index 100% rename from backend/internal/logic/threads_account/activate_threads_account_logic.go rename to old/backend/internal/logic/threads_account/activate_threads_account_logic.go diff --git a/backend/internal/logic/threads_account/actor.go b/old/backend/internal/logic/threads_account/actor.go similarity index 100% rename from backend/internal/logic/threads_account/actor.go rename to old/backend/internal/logic/threads_account/actor.go diff --git a/backend/internal/logic/threads_account/analyze_content_formula_logic.go b/old/backend/internal/logic/threads_account/analyze_content_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/analyze_content_formula_logic.go rename to old/backend/internal/logic/threads_account/analyze_content_formula_logic.go diff --git a/backend/internal/logic/threads_account/cancel_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/cancel_publish_queue_item_logic.go similarity index 100% rename from backend/internal/logic/threads_account/cancel_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/cancel_publish_queue_item_logic.go diff --git a/backend/internal/logic/threads_account/content_formula_mapper.go b/old/backend/internal/logic/threads_account/content_formula_mapper.go similarity index 100% rename from backend/internal/logic/threads_account/content_formula_mapper.go rename to old/backend/internal/logic/threads_account/content_formula_mapper.go diff --git a/backend/internal/logic/threads_account/create_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/create_publish_queue_item_logic.go similarity index 94% rename from backend/internal/logic/threads_account/create_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/create_publish_queue_item_logic.go index 96fdfef..74458bf 100644 --- a/backend/internal/logic/threads_account/create_publish_queue_item_logic.go +++ b/old/backend/internal/logic/threads_account/create_publish_queue_item_logic.go @@ -27,6 +27,8 @@ func (l *CreatePublishQueueItemLogic) CreatePublishQueueItem(req *types.CreatePu OwnerUID: uid, AccountID: req.ID, Text: req.Text, + ImageKey: req.ImageKey, + ImageKeys: req.ImageKeys, TopicTag: req.TopicTag, ScheduledAt: req.ScheduledAt, }) diff --git a/backend/internal/logic/threads_account/create_threads_account_logic.go b/old/backend/internal/logic/threads_account/create_threads_account_logic.go similarity index 100% rename from backend/internal/logic/threads_account/create_threads_account_logic.go rename to old/backend/internal/logic/threads_account/create_threads_account_logic.go diff --git a/backend/internal/logic/threads_account/delete_content_formula_logic.go b/old/backend/internal/logic/threads_account/delete_content_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/delete_content_formula_logic.go rename to old/backend/internal/logic/threads_account/delete_content_formula_logic.go diff --git a/backend/internal/logic/threads_account/delete_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/delete_publish_queue_item_logic.go similarity index 100% rename from backend/internal/logic/threads_account/delete_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/delete_publish_queue_item_logic.go diff --git a/backend/internal/logic/threads_account/delete_threads_account_logic.go b/old/backend/internal/logic/threads_account/delete_threads_account_logic.go similarity index 100% rename from backend/internal/logic/threads_account/delete_threads_account_logic.go rename to old/backend/internal/logic/threads_account/delete_threads_account_logic.go diff --git a/backend/internal/logic/threads_account/disconnect_threads_api_logic.go b/old/backend/internal/logic/threads_account/disconnect_threads_api_logic.go similarity index 100% rename from backend/internal/logic/threads_account/disconnect_threads_api_logic.go rename to old/backend/internal/logic/threads_account/disconnect_threads_api_logic.go diff --git a/backend/internal/logic/threads_account/generate_own_post_formula_logic.go b/old/backend/internal/logic/threads_account/generate_own_post_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/generate_own_post_formula_logic.go rename to old/backend/internal/logic/threads_account/generate_own_post_formula_logic.go diff --git a/backend/internal/logic/threads_account/generate_own_post_reply_draft_logic.go b/old/backend/internal/logic/threads_account/generate_own_post_reply_draft_logic.go similarity index 100% rename from backend/internal/logic/threads_account/generate_own_post_reply_draft_logic.go rename to old/backend/internal/logic/threads_account/generate_own_post_reply_draft_logic.go diff --git a/backend/internal/logic/threads_account/get_content_formula_logic.go b/old/backend/internal/logic/threads_account/get_content_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_content_formula_logic.go rename to old/backend/internal/logic/threads_account/get_content_formula_logic.go diff --git a/backend/internal/logic/threads_account/get_publish_dashboard_summary_logic.go b/old/backend/internal/logic/threads_account/get_publish_dashboard_summary_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_publish_dashboard_summary_logic.go rename to old/backend/internal/logic/threads_account/get_publish_dashboard_summary_logic.go diff --git a/backend/internal/logic/threads_account/get_publish_guard_policy_logic.go b/old/backend/internal/logic/threads_account/get_publish_guard_policy_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_publish_guard_policy_logic.go rename to old/backend/internal/logic/threads_account/get_publish_guard_policy_logic.go diff --git a/backend/internal/logic/threads_account/get_publish_inventory_policy_logic.go b/old/backend/internal/logic/threads_account/get_publish_inventory_policy_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_publish_inventory_policy_logic.go rename to old/backend/internal/logic/threads_account/get_publish_inventory_policy_logic.go diff --git a/backend/internal/logic/threads_account/get_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/get_publish_queue_item_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/get_publish_queue_item_logic.go diff --git a/backend/internal/logic/threads_account/get_publish_slot_insights_logic.go b/old/backend/internal/logic/threads_account/get_publish_slot_insights_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_publish_slot_insights_logic.go rename to old/backend/internal/logic/threads_account/get_publish_slot_insights_logic.go diff --git a/backend/internal/logic/threads_account/get_threads_account_ai_settings_logic.go b/old/backend/internal/logic/threads_account/get_threads_account_ai_settings_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_threads_account_ai_settings_logic.go rename to old/backend/internal/logic/threads_account/get_threads_account_ai_settings_logic.go diff --git a/backend/internal/logic/threads_account/get_threads_account_connection_logic.go b/old/backend/internal/logic/threads_account/get_threads_account_connection_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_threads_account_connection_logic.go rename to old/backend/internal/logic/threads_account/get_threads_account_connection_logic.go diff --git a/backend/internal/logic/threads_account/get_threads_account_logic.go b/old/backend/internal/logic/threads_account/get_threads_account_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_threads_account_logic.go rename to old/backend/internal/logic/threads_account/get_threads_account_logic.go diff --git a/backend/internal/logic/threads_account/get_threads_diagnostics_logic.go b/old/backend/internal/logic/threads_account/get_threads_diagnostics_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_threads_diagnostics_logic.go rename to old/backend/internal/logic/threads_account/get_threads_diagnostics_logic.go diff --git a/backend/internal/logic/threads_account/get_threads_oauth_config_logic.go b/old/backend/internal/logic/threads_account/get_threads_oauth_config_logic.go similarity index 100% rename from backend/internal/logic/threads_account/get_threads_oauth_config_logic.go rename to old/backend/internal/logic/threads_account/get_threads_oauth_config_logic.go diff --git a/backend/internal/logic/threads_account/import_own_post_to_content_formula_logic.go b/old/backend/internal/logic/threads_account/import_own_post_to_content_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/import_own_post_to_content_formula_logic.go rename to old/backend/internal/logic/threads_account/import_own_post_to_content_formula_logic.go diff --git a/backend/internal/logic/threads_account/import_threads_account_session_logic.go b/old/backend/internal/logic/threads_account/import_threads_account_session_logic.go similarity index 100% rename from backend/internal/logic/threads_account/import_threads_account_session_logic.go rename to old/backend/internal/logic/threads_account/import_threads_account_session_logic.go diff --git a/backend/internal/logic/threads_account/list_content_formulas_logic.go b/old/backend/internal/logic/threads_account/list_content_formulas_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_content_formulas_logic.go rename to old/backend/internal/logic/threads_account/list_content_formulas_logic.go diff --git a/backend/internal/logic/threads_account/list_mention_inbox_logic.go b/old/backend/internal/logic/threads_account/list_mention_inbox_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_mention_inbox_logic.go rename to old/backend/internal/logic/threads_account/list_mention_inbox_logic.go diff --git a/backend/internal/logic/threads_account/list_own_post_formulas_logic.go b/old/backend/internal/logic/threads_account/list_own_post_formulas_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_own_post_formulas_logic.go rename to old/backend/internal/logic/threads_account/list_own_post_formulas_logic.go diff --git a/backend/internal/logic/threads_account/list_publish_alerts_logic.go b/old/backend/internal/logic/threads_account/list_publish_alerts_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_publish_alerts_logic.go rename to old/backend/internal/logic/threads_account/list_publish_alerts_logic.go diff --git a/backend/internal/logic/threads_account/list_publish_queue_events_logic.go b/old/backend/internal/logic/threads_account/list_publish_queue_events_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_publish_queue_events_logic.go rename to old/backend/internal/logic/threads_account/list_publish_queue_events_logic.go diff --git a/backend/internal/logic/threads_account/list_publish_queue_items_logic.go b/old/backend/internal/logic/threads_account/list_publish_queue_items_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_publish_queue_items_logic.go rename to old/backend/internal/logic/threads_account/list_publish_queue_items_logic.go diff --git a/backend/internal/logic/threads_account/list_publish_queue_range_logic.go b/old/backend/internal/logic/threads_account/list_publish_queue_range_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_publish_queue_range_logic.go rename to old/backend/internal/logic/threads_account/list_publish_queue_range_logic.go diff --git a/backend/internal/logic/threads_account/list_threads_account_ai_provider_models_logic.go b/old/backend/internal/logic/threads_account/list_threads_account_ai_provider_models_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_threads_account_ai_provider_models_logic.go rename to old/backend/internal/logic/threads_account/list_threads_account_ai_provider_models_logic.go diff --git a/backend/internal/logic/threads_account/list_threads_accounts_logic.go b/old/backend/internal/logic/threads_account/list_threads_accounts_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_threads_accounts_logic.go rename to old/backend/internal/logic/threads_account/list_threads_accounts_logic.go diff --git a/backend/internal/logic/threads_account/list_threads_oauth_logs_logic.go b/old/backend/internal/logic/threads_account/list_threads_oauth_logs_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_threads_oauth_logs_logic.go rename to old/backend/internal/logic/threads_account/list_threads_oauth_logs_logic.go diff --git a/backend/internal/logic/threads_account/list_threads_post_performance_logic.go b/old/backend/internal/logic/threads_account/list_threads_post_performance_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_threads_post_performance_logic.go rename to old/backend/internal/logic/threads_account/list_threads_post_performance_logic.go diff --git a/backend/internal/logic/threads_account/list_threads_publish_health_logic.go b/old/backend/internal/logic/threads_account/list_threads_publish_health_logic.go similarity index 100% rename from backend/internal/logic/threads_account/list_threads_publish_health_logic.go rename to old/backend/internal/logic/threads_account/list_threads_publish_health_logic.go diff --git a/backend/internal/logic/threads_account/mapper.go b/old/backend/internal/logic/threads_account/mapper.go similarity index 99% rename from backend/internal/logic/threads_account/mapper.go rename to old/backend/internal/logic/threads_account/mapper.go index f83071d..e986a51 100644 --- a/backend/internal/logic/threads_account/mapper.go +++ b/old/backend/internal/logic/threads_account/mapper.go @@ -108,6 +108,8 @@ func toPublishQueueItemData(item *pqdomain.QueueItemSummary) *types.PublishQueue CopyMissionID: item.CopyMissionID, CopyDraftID: item.CopyDraftID, Text: item.Text, + ImageKey: item.ImageKey, + ImageKeys: item.ImageKeys, TopicTag: item.TopicTag, ScheduledAt: item.ScheduledAt, Status: item.Status, diff --git a/backend/internal/logic/threads_account/mention_inbox_mapper.go b/old/backend/internal/logic/threads_account/mention_inbox_mapper.go similarity index 100% rename from backend/internal/logic/threads_account/mention_inbox_mapper.go rename to old/backend/internal/logic/threads_account/mention_inbox_mapper.go diff --git a/backend/internal/logic/threads_account/own_post_formula_mapper.go b/old/backend/internal/logic/threads_account/own_post_formula_mapper.go similarity index 100% rename from backend/internal/logic/threads_account/own_post_formula_mapper.go rename to old/backend/internal/logic/threads_account/own_post_formula_mapper.go diff --git a/backend/internal/logic/threads_account/patch_content_formula_logic.go b/old/backend/internal/logic/threads_account/patch_content_formula_logic.go similarity index 100% rename from backend/internal/logic/threads_account/patch_content_formula_logic.go rename to old/backend/internal/logic/threads_account/patch_content_formula_logic.go diff --git a/backend/internal/logic/threads_account/patch_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/patch_publish_queue_item_logic.go similarity index 94% rename from backend/internal/logic/threads_account/patch_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/patch_publish_queue_item_logic.go index 3e70636..6bbd710 100644 --- a/backend/internal/logic/threads_account/patch_publish_queue_item_logic.go +++ b/old/backend/internal/logic/threads_account/patch_publish_queue_item_logic.go @@ -28,6 +28,8 @@ func (l *PatchPublishQueueItemLogic) PatchPublishQueueItem(req *types.PatchPubli AccountID: req.ID, QueueID: req.QID, Text: req.Text, + ImageKey: req.ImageKey, + ImageKeys: req.ImageKeys, TopicTag: req.TopicTag, ScheduledAt: req.ScheduledAt, }) diff --git a/old/backend/internal/logic/threads_account/publish_attachment.go b/old/backend/internal/logic/threads_account/publish_attachment.go new file mode 100644 index 0000000..c681406 --- /dev/null +++ b/old/backend/internal/logic/threads_account/publish_attachment.go @@ -0,0 +1,55 @@ +package threads_account + +import ( + "context" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/storage" + "haixun-backend/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +func resolvePublishImageURLs(svcCtx *svc.ServiceContext, single string, list []string) ([]string, []string, error) { + keys := storage.NormalizeImageKeys(single, list) + if len(keys) == 0 { + return nil, nil, nil + } + resolver, ok := svcCtx.AvatarStorage.(storage.PublicURLResolver) + if !ok || resolver == nil { + return nil, nil, app.For(code.ThreadsAccount).SysNotImplemented("image storage is not configured") + } + urls := make([]string, 0, len(keys)) + for _, key := range keys { + if err := storage.ValidatePublishAttachmentKey(key); err != nil { + return nil, nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + url, err := resolver.ResolvePublicURL(key) + if err != nil { + return nil, nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + if err := storage.ValidateThreadsFetchableURL(url); err != nil { + return nil, nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + urls = append(urls, url) + } + return urls, keys, nil +} + +func purgePublishAttachments(ctx context.Context, svcCtx *svc.ServiceContext, imageKeys []string) { + deleter, ok := svcCtx.AvatarStorage.(storage.ObjectDeleter) + if !ok || deleter == nil { + return + } + for _, key := range imageKeys { + key = strings.TrimSpace(key) + if key == "" { + continue + } + if err := storage.PurgePublishAttachment(ctx, deleter, key); err != nil { + logx.WithContext(ctx).Errorf("purge publish attachment %s: %v", key, err) + } + } +} \ No newline at end of file diff --git a/backend/internal/logic/threads_account/publish_mention_reply_logic.go b/old/backend/internal/logic/threads_account/publish_mention_reply_logic.go similarity index 76% rename from backend/internal/logic/threads_account/publish_mention_reply_logic.go rename to old/backend/internal/logic/threads_account/publish_mention_reply_logic.go index 67b1797..2be218e 100644 --- a/backend/internal/logic/threads_account/publish_mention_reply_logic.go +++ b/old/backend/internal/logic/threads_account/publish_mention_reply_logic.go @@ -24,17 +24,27 @@ func NewPublishMentionReplyLogic(ctx context.Context, svcCtx *svc.ServiceContext } } -func (l *PublishMentionReplyLogic) PublishMentionReply(req *types.PublishMentionReplyHandlerReq) (*types.PublishMentionReplyData, error) { +func (l *PublishMentionReplyLogic) PublishMentionReply( + req *types.PublishMentionReplyHandlerReq, + image *ReplyImageFile, +) (*types.PublishMentionReplyData, error) { tenantID, uid, err := actorFrom(l.ctx) if err != nil { return nil, err } + imageURLs, cleanup, err := replyImageURLs(l.svcCtx, image) + if err != nil { + return nil, err + } + defer cleanup() + result, err := l.svcCtx.MentionInbox.PublishReply(l.ctx, mentioninboxdomain.PublishReplyRequest{ TenantID: tenantID, OwnerUID: uid, AccountID: req.ID, MediaID: req.MediaID, Text: req.Text, + ImageURLs: imageURLs, }) if err != nil { return nil, err diff --git a/old/backend/internal/logic/threads_account/publish_threads_reply_logic.go b/old/backend/internal/logic/threads_account/publish_threads_reply_logic.go new file mode 100644 index 0000000..e22b913 --- /dev/null +++ b/old/backend/internal/logic/threads_account/publish_threads_reply_logic.go @@ -0,0 +1,87 @@ +package threads_account + +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" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type PublishThreadsReplyLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPublishThreadsReplyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishThreadsReplyLogic { + return &PublishThreadsReplyLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PublishThreadsReplyLogic) PublishThreadsReply( + req *types.PublishThreadsReplyHandlerReq, + image *ReplyImageFile, +) (*types.PublishThreadsReplyData, error) { + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + replyToID := strings.TrimSpace(req.ReplyToID) + text := strings.TrimSpace(req.Text) + if replyToID == "" { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("reply_to_id is required") + } + imageURLs, cleanup, err := replyImageURLs(l.svcCtx, image) + if err != nil { + return nil, err + } + defer cleanup() + if text == "" && len(imageURLs) == 0 { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("text or image is required") + } + if err := threadspost.ValidateReplyContent(text, len(imageURLs) > 0); err != nil { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + + account, err := l.svcCtx.ThreadsAccount.Get(l.ctx, tenantID, uid, req.ID) + if err != nil { + return nil, err + } + token, err := l.svcCtx.ThreadsAccount.ResolveAccountAPIAccessToken(l.ctx, tenantID, uid, req.ID) + if err != nil { + return nil, err + } + userID := strings.TrimSpace(account.ThreadsUserID) + if userID == "" { + if profile, perr := libthreads.GetMeProfile(l.ctx, token); perr == nil && profile != nil { + userID = profile.ID.String() + } + } + + result, err := libthreads.PublishReply(l.ctx, libthreads.PublishReplyInput{ + ThreadsUserID: userID, + AccessToken: token, + ReplyToID: replyToID, + Text: text, + ImageURLs: imageURLs, + }) + if err != nil { + return nil, err + } + out := &types.PublishThreadsReplyData{} + if result != nil { + out.MediaID = strings.TrimSpace(result.MediaID) + out.Permalink = strings.TrimSpace(result.Permalink) + } + return out, nil +} \ No newline at end of file diff --git a/old/backend/internal/logic/threads_account/reply_image.go b/old/backend/internal/logic/threads_account/reply_image.go new file mode 100644 index 0000000..6a7509e --- /dev/null +++ b/old/backend/internal/logic/threads_account/reply_image.go @@ -0,0 +1,88 @@ +package threads_account + +import ( + "io" + "path/filepath" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/storage" + "haixun-backend/internal/svc" +) + +const maxReplyImageBytes = 8 << 20 + +type ReplyImageFile struct { + Filename string + ContentType string + Body io.Reader +} + +func stageReplyImageURL(svcCtx *svc.ServiceContext, file *ReplyImageFile) (imageURL string, cleanup func(), err error) { + cleanup = func() {} + if file == nil || file.Body == nil { + return "", cleanup, nil + } + ext := strings.ToLower(filepath.Ext(strings.TrimSpace(file.Filename))) + if !allowedPublishAttachmentExt(ext) { + return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("image must be jpg, png, webp, or gif") + } + contentType := strings.TrimSpace(file.ContentType) + if contentType != "" && !strings.HasPrefix(strings.ToLower(contentType), "image/") { + return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("file must be an image") + } + if contentType == "" { + switch ext { + case ".jpg", ".jpeg": + contentType = "image/jpeg" + case ".png": + contentType = "image/png" + case ".webp": + contentType = "image/webp" + case ".gif": + contentType = "image/gif" + default: + contentType = "application/octet-stream" + } + } + + limited := io.LimitReader(file.Body, maxReplyImageBytes+1) + data, readErr := io.ReadAll(limited) + if readErr != nil { + return "", cleanup, app.For(code.ThreadsAccount).SysInternal("read image failed").WithCause(readErr) + } + if len(data) == 0 { + return "", cleanup, app.For(code.ThreadsAccount).InputMissingRequired("image file is required") + } + if len(data) > maxReplyImageBytes { + return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("image must be smaller than 8MB") + } + + key, regErr := storage.DefaultEphemeralAssetStore().Register(data, contentType, 0) + if regErr != nil { + return "", cleanup, app.For(code.ThreadsAccount).SysInternal("stage image failed").WithCause(regErr) + } + base := strings.TrimSpace(svcCtx.Config.Storage.S3.PublicBaseURL) + imageURL, urlErr := storage.BuildEphemeralPublicURL(base, key) + if urlErr != nil { + storage.DefaultEphemeralAssetStore().Delete(key) + return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat(urlErr.Error()) + } + if err := storage.ValidateThreadsFetchableURL(imageURL); err != nil { + storage.DefaultEphemeralAssetStore().Delete(key) + return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + cleanup = func() { + storage.DefaultEphemeralAssetStore().Delete(key) + } + return imageURL, cleanup, nil +} + +func replyImageURLs(svcCtx *svc.ServiceContext, file *ReplyImageFile) ([]string, func(), error) { + imageURL, cleanup, err := stageReplyImageURL(svcCtx, file) + if err != nil || imageURL == "" { + return nil, cleanup, err + } + return []string{imageURL}, cleanup, nil +} \ No newline at end of file diff --git a/backend/internal/logic/threads_account/resume_publish_guard_logic.go b/old/backend/internal/logic/threads_account/resume_publish_guard_logic.go similarity index 100% rename from backend/internal/logic/threads_account/resume_publish_guard_logic.go rename to old/backend/internal/logic/threads_account/resume_publish_guard_logic.go diff --git a/backend/internal/logic/threads_account/retry_publish_queue_item_logic.go b/old/backend/internal/logic/threads_account/retry_publish_queue_item_logic.go similarity index 100% rename from backend/internal/logic/threads_account/retry_publish_queue_item_logic.go rename to old/backend/internal/logic/threads_account/retry_publish_queue_item_logic.go diff --git a/backend/internal/logic/threads_account/run_threads_api_playground_logic.go b/old/backend/internal/logic/threads_account/run_threads_api_playground_logic.go similarity index 75% rename from backend/internal/logic/threads_account/run_threads_api_playground_logic.go rename to old/backend/internal/logic/threads_account/run_threads_api_playground_logic.go index af9017f..9042868 100644 --- a/backend/internal/logic/threads_account/run_threads_api_playground_logic.go +++ b/old/backend/internal/logic/threads_account/run_threads_api_playground_logic.go @@ -2,6 +2,8 @@ package threads_account import ( "context" + "strings" + domusecase "haixun-backend/internal/model/threads_account/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" @@ -24,16 +26,26 @@ func (l *RunThreadsAPIPlaygroundLogic) RunThreadsAPIPlayground( return nil, err } body := req.RunThreadsAPIPlaygroundReq + imageURLs, imageKeys, err := resolvePublishImageURLs(l.svcCtx, body.ImageKey, body.ImageKeys) + if err != nil { + return nil, err + } result, err := l.svcCtx.ThreadsAccount.RunAPIPlayground(l.ctx, tenantID, uid, req.ID, domusecase.APIPlaygroundRequest{ Action: body.Action, Query: body.Query, Username: body.Username, MediaID: body.MediaID, ReplyID: body.ReplyID, ReplyToID: body.ReplyToID, - Text: body.Text, Permalink: body.Permalink, HintText: body.HintText, SkipPrime: body.SkipPrime, + Text: body.Text, ImageURLs: imageURLs, Permalink: body.Permalink, HintText: body.HintText, SkipPrime: body.SkipPrime, LocationQuery: body.LocationQuery, Limit: body.Limit, Hide: body.Hide, SearchType: body.SearchType, }) if err != nil { return nil, err } + if result.Ok && len(imageKeys) > 0 { + switch strings.TrimSpace(strings.ToLower(body.Action)) { + case "publish_text": + purgePublishAttachments(l.ctx, l.svcCtx, imageKeys) + } + } data := "" if len(result.Data) > 0 { data = string(result.Data) diff --git a/backend/internal/logic/threads_account/run_threads_api_smoke_test_logic.go b/old/backend/internal/logic/threads_account/run_threads_api_smoke_test_logic.go similarity index 100% rename from backend/internal/logic/threads_account/run_threads_api_smoke_test_logic.go rename to old/backend/internal/logic/threads_account/run_threads_api_smoke_test_logic.go diff --git a/backend/internal/logic/threads_account/search_content_formula_posts_logic.go b/old/backend/internal/logic/threads_account/search_content_formula_posts_logic.go similarity index 100% rename from backend/internal/logic/threads_account/search_content_formula_posts_logic.go rename to old/backend/internal/logic/threads_account/search_content_formula_posts_logic.go diff --git a/backend/internal/logic/threads_account/start_threads_oauth_logic.go b/old/backend/internal/logic/threads_account/start_threads_oauth_logic.go similarity index 100% rename from backend/internal/logic/threads_account/start_threads_oauth_logic.go rename to old/backend/internal/logic/threads_account/start_threads_oauth_logic.go diff --git a/backend/internal/logic/threads_account/sync_mention_inbox_logic.go b/old/backend/internal/logic/threads_account/sync_mention_inbox_logic.go similarity index 100% rename from backend/internal/logic/threads_account/sync_mention_inbox_logic.go rename to old/backend/internal/logic/threads_account/sync_mention_inbox_logic.go diff --git a/backend/internal/logic/threads_account/threads_oauth_callback_logic.go b/old/backend/internal/logic/threads_account/threads_oauth_callback_logic.go similarity index 100% rename from backend/internal/logic/threads_account/threads_oauth_callback_logic.go rename to old/backend/internal/logic/threads_account/threads_oauth_callback_logic.go diff --git a/backend/internal/logic/threads_account/update_threads_account_ai_settings_logic.go b/old/backend/internal/logic/threads_account/update_threads_account_ai_settings_logic.go similarity index 100% rename from backend/internal/logic/threads_account/update_threads_account_ai_settings_logic.go rename to old/backend/internal/logic/threads_account/update_threads_account_ai_settings_logic.go diff --git a/backend/internal/logic/threads_account/update_threads_account_connection_logic.go b/old/backend/internal/logic/threads_account/update_threads_account_connection_logic.go similarity index 100% rename from backend/internal/logic/threads_account/update_threads_account_connection_logic.go rename to old/backend/internal/logic/threads_account/update_threads_account_connection_logic.go diff --git a/backend/internal/logic/threads_account/update_threads_account_logic.go b/old/backend/internal/logic/threads_account/update_threads_account_logic.go similarity index 100% rename from backend/internal/logic/threads_account/update_threads_account_logic.go rename to old/backend/internal/logic/threads_account/update_threads_account_logic.go diff --git a/old/backend/internal/logic/threads_account/upload_publish_attachment_logic.go b/old/backend/internal/logic/threads_account/upload_publish_attachment_logic.go new file mode 100644 index 0000000..6e41985 --- /dev/null +++ b/old/backend/internal/logic/threads_account/upload_publish_attachment_logic.go @@ -0,0 +1,82 @@ +package threads_account + +import ( + "context" + "fmt" + "io" + "path/filepath" + "strings" + + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/library/storage" + "haixun-backend/internal/svc" + "haixun-backend/internal/types" + + "github.com/google/uuid" + "github.com/zeromicro/go-zero/core/logx" +) + +type UploadPublishAttachmentLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUploadPublishAttachmentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadPublishAttachmentLogic { + return &UploadPublishAttachmentLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UploadPublishAttachmentLogic) UploadPublishAttachment(accountID, filename, contentType string, body io.Reader) (*types.UploadPublishAttachmentData, error) { + if l.svcCtx.AvatarStorage == nil { + return nil, app.For(code.ThreadsAccount).SysNotImplemented("image storage is not configured") + } + tenantID, uid, err := actorFrom(l.ctx) + if err != nil { + return nil, err + } + accountID = strings.TrimSpace(accountID) + if accountID == "" { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("account_id is required") + } + if _, err := l.svcCtx.ThreadsAccount.Get(l.ctx, tenantID, uid, accountID); err != nil { + return nil, err + } + ext := strings.ToLower(filepath.Ext(filename)) + if !allowedPublishAttachmentExt(ext) { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat("image must be jpg, png, webp, or gif") + } + if contentType != "" && !strings.HasPrefix(strings.ToLower(contentType), "image/") { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat("file must be an image") + } + key := fmt.Sprintf("%s%s/%s/%s%s", storage.PublishAttachmentKeyPrefix, tenantID, accountID, uuid.NewString(), ext) + storedKey, err := l.svcCtx.AvatarStorage.Upload(l.ctx, key, contentType, body) + if err != nil { + return nil, app.For(code.ThreadsAccount).SysInternal("upload image failed").WithCause(err) + } + urls, _, err := resolvePublishImageURLs(l.svcCtx, storedKey, nil) + if err != nil { + return nil, err + } + publicURL := "" + if len(urls) > 0 { + publicURL = urls[0] + } + return &types.UploadPublishAttachmentData{ + AttachmentKey: storedKey, + PublicURL: publicURL, + }, nil +} + +func allowedPublishAttachmentExt(ext string) bool { + switch ext { + case ".jpg", ".jpeg", ".png", ".webp", ".gif": + return true + default: + return false + } +} \ No newline at end of file diff --git a/backend/internal/logic/threads_account/upsert_publish_guard_policy_logic.go b/old/backend/internal/logic/threads_account/upsert_publish_guard_policy_logic.go similarity index 100% rename from backend/internal/logic/threads_account/upsert_publish_guard_policy_logic.go rename to old/backend/internal/logic/threads_account/upsert_publish_guard_policy_logic.go diff --git a/backend/internal/logic/threads_account/upsert_publish_inventory_policy_logic.go b/old/backend/internal/logic/threads_account/upsert_publish_inventory_policy_logic.go similarity index 100% rename from backend/internal/logic/threads_account/upsert_publish_inventory_policy_logic.go rename to old/backend/internal/logic/threads_account/upsert_publish_inventory_policy_logic.go diff --git a/backend/internal/middleware/auth.go b/old/backend/internal/middleware/auth.go similarity index 100% rename from backend/internal/middleware/auth.go rename to old/backend/internal/middleware/auth.go diff --git a/backend/internal/middleware/auth_test.go b/old/backend/internal/middleware/auth_test.go similarity index 100% rename from backend/internal/middleware/auth_test.go rename to old/backend/internal/middleware/auth_test.go diff --git a/backend/internal/middleware/authjwt_middleware.go b/old/backend/internal/middleware/authjwt_middleware.go similarity index 100% rename from backend/internal/middleware/authjwt_middleware.go rename to old/backend/internal/middleware/authjwt_middleware.go diff --git a/old/backend/internal/middleware/emailverified_middleware.go b/old/backend/internal/middleware/emailverified_middleware.go new file mode 100644 index 0000000..5dc746f --- /dev/null +++ b/old/backend/internal/middleware/emailverified_middleware.go @@ -0,0 +1,77 @@ +package middleware + +import ( + "net/http" + + "haixun-backend/internal/library/authctx" + app "haixun-backend/internal/library/errors" + "haixun-backend/internal/library/errors/code" + "haixun-backend/internal/model/member/domain/entity" + memberdomain "haixun-backend/internal/model/member/domain/usecase" + "haixun-backend/internal/response" +) + +var emailVerificationBypass = map[string]map[string]struct{}{ + http.MethodGet: { + "/api/v1/members/me": {}, + }, + http.MethodPost: { + "/api/v1/auth/logout": {}, + "/api/v1/auth/verify-email": {}, + "/api/v1/auth/resend-verification-email": {}, + }, +} + +// EmailVerifiedMiddleware blocks native members who have not verified email yet. +// Third-party (oauth) origins skip this gate. +type EmailVerifiedMiddleware struct { + members memberdomain.UseCase +} + +func NewEmailVerifiedMiddleware(members memberdomain.UseCase) *EmailVerifiedMiddleware { + return &EmailVerifiedMiddleware{members: members} +} + +func (m *EmailVerifiedMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + actor, ok := authctx.ActorFromContext(r.Context()) + if !ok { + next(w, r) + return + } + if emailVerificationBypassed(r.Method, r.URL.Path) { + next(w, r) + return + } + if m.members == nil { + response.Write(r.Context(), w, nil, app.For(code.Auth).SysNotImplemented("member usecase is not configured")) + return + } + member, err := m.members.GetByUID(r.Context(), actor.TenantID, actor.UID) + if err != nil { + response.Write(r.Context(), w, nil, err) + return + } + if !requiresNativeEmailVerification(member) || member.EmailVerified { + next(w, r) + return + } + response.Write(r.Context(), w, nil, app.For(code.Auth).AuthForbidden("email verification required")) + } +} + +func emailVerificationBypassed(method, path string) bool { + routes, ok := emailVerificationBypass[method] + if !ok { + return false + } + _, ok = routes[path] + return ok +} + +func requiresNativeEmailVerification(member *entity.Member) bool { + if member == nil { + return false + } + return member.Origin == "" || member.Origin == entity.OriginNative +} \ No newline at end of file diff --git a/old/backend/internal/middleware/emailverified_middleware_test.go b/old/backend/internal/middleware/emailverified_middleware_test.go new file mode 100644 index 0000000..f7d44e6 --- /dev/null +++ b/old/backend/internal/middleware/emailverified_middleware_test.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net/http" + "testing" + + "haixun-backend/internal/model/member/domain/entity" +) + +func TestEmailVerificationBypassed(t *testing.T) { + if !emailVerificationBypassed(http.MethodPost, "/api/v1/auth/verify-email") { + t.Fatal("verify-email should bypass") + } + if emailVerificationBypassed(http.MethodGet, "/api/v1/settings") { + t.Fatal("settings should not bypass") + } +} + +func TestRequiresNativeEmailVerification(t *testing.T) { + if !requiresNativeEmailVerification(&entity.Member{Origin: entity.OriginNative}) { + t.Fatal("native should require verification") + } + if requiresNativeEmailVerification(&entity.Member{Origin: entity.OriginOAuth, EmailVerified: false}) { + t.Fatal("oauth should skip verification") + } +} \ No newline at end of file diff --git a/backend/internal/middleware/memberauth_middleware.go b/old/backend/internal/middleware/memberauth_middleware.go similarity index 100% rename from backend/internal/middleware/memberauth_middleware.go rename to old/backend/internal/middleware/memberauth_middleware.go diff --git a/backend/internal/middleware/permissionrbac_middleware.go b/old/backend/internal/middleware/permissionrbac_middleware.go similarity index 100% rename from backend/internal/middleware/permissionrbac_middleware.go rename to old/backend/internal/middleware/permissionrbac_middleware.go diff --git a/backend/internal/middleware/workersecret_middleware.go b/old/backend/internal/middleware/workersecret_middleware.go similarity index 100% rename from backend/internal/middleware/workersecret_middleware.go rename to old/backend/internal/middleware/workersecret_middleware.go diff --git a/backend/internal/model/ai/domain/enum/provider.go b/old/backend/internal/model/ai/domain/enum/provider.go similarity index 100% rename from backend/internal/model/ai/domain/enum/provider.go rename to old/backend/internal/model/ai/domain/enum/provider.go diff --git a/backend/internal/model/ai/domain/usecase/ai.go b/old/backend/internal/model/ai/domain/usecase/ai.go similarity index 100% rename from backend/internal/model/ai/domain/usecase/ai.go rename to old/backend/internal/model/ai/domain/usecase/ai.go diff --git a/backend/internal/model/ai/provider/openai_compatible.go b/old/backend/internal/model/ai/provider/openai_compatible.go similarity index 100% rename from backend/internal/model/ai/provider/openai_compatible.go rename to old/backend/internal/model/ai/provider/openai_compatible.go diff --git a/backend/internal/model/ai/provider/openai_compatible_test.go b/old/backend/internal/model/ai/provider/openai_compatible_test.go similarity index 100% rename from backend/internal/model/ai/provider/openai_compatible_test.go rename to old/backend/internal/model/ai/provider/openai_compatible_test.go diff --git a/backend/internal/model/ai/usecase/provider_map.go b/old/backend/internal/model/ai/usecase/provider_map.go similarity index 100% rename from backend/internal/model/ai/usecase/provider_map.go rename to old/backend/internal/model/ai/usecase/provider_map.go diff --git a/backend/internal/model/ai/usecase/usecase.go b/old/backend/internal/model/ai/usecase/usecase.go similarity index 100% rename from backend/internal/model/ai/usecase/usecase.go rename to old/backend/internal/model/ai/usecase/usecase.go diff --git a/backend/internal/model/auth/domain/repository/token_revoke.go b/old/backend/internal/model/auth/domain/repository/token_revoke.go similarity index 100% rename from backend/internal/model/auth/domain/repository/token_revoke.go rename to old/backend/internal/model/auth/domain/repository/token_revoke.go diff --git a/backend/internal/model/auth/domain/usecase/token.go b/old/backend/internal/model/auth/domain/usecase/token.go similarity index 100% rename from backend/internal/model/auth/domain/usecase/token.go rename to old/backend/internal/model/auth/domain/usecase/token.go diff --git a/backend/internal/model/auth/repository/token_revoke_redis.go b/old/backend/internal/model/auth/repository/token_revoke_redis.go similarity index 100% rename from backend/internal/model/auth/repository/token_revoke_redis.go rename to old/backend/internal/model/auth/repository/token_revoke_redis.go diff --git a/backend/internal/model/auth/usecase/token.go b/old/backend/internal/model/auth/usecase/token.go similarity index 100% rename from backend/internal/model/auth/usecase/token.go rename to old/backend/internal/model/auth/usecase/token.go diff --git a/backend/internal/model/brand/domain/entity/brand.go b/old/backend/internal/model/brand/domain/entity/brand.go similarity index 100% rename from backend/internal/model/brand/domain/entity/brand.go rename to old/backend/internal/model/brand/domain/entity/brand.go diff --git a/backend/internal/model/brand/domain/entity/product.go b/old/backend/internal/model/brand/domain/entity/product.go similarity index 100% rename from backend/internal/model/brand/domain/entity/product.go rename to old/backend/internal/model/brand/domain/entity/product.go diff --git a/backend/internal/model/brand/domain/entity/research_map.go b/old/backend/internal/model/brand/domain/entity/research_map.go similarity index 100% rename from backend/internal/model/brand/domain/entity/research_map.go rename to old/backend/internal/model/brand/domain/entity/research_map.go diff --git a/backend/internal/model/brand/domain/repository/repository.go b/old/backend/internal/model/brand/domain/repository/repository.go similarity index 100% rename from backend/internal/model/brand/domain/repository/repository.go rename to old/backend/internal/model/brand/domain/repository/repository.go diff --git a/backend/internal/model/brand/domain/usecase/usecase.go b/old/backend/internal/model/brand/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/brand/domain/usecase/usecase.go rename to old/backend/internal/model/brand/domain/usecase/usecase.go diff --git a/backend/internal/model/brand/repository/mongo.go b/old/backend/internal/model/brand/repository/mongo.go similarity index 100% rename from backend/internal/model/brand/repository/mongo.go rename to old/backend/internal/model/brand/repository/mongo.go diff --git a/backend/internal/model/brand/usecase/products.go b/old/backend/internal/model/brand/usecase/products.go similarity index 100% rename from backend/internal/model/brand/usecase/products.go rename to old/backend/internal/model/brand/usecase/products.go diff --git a/backend/internal/model/brand/usecase/products_placement_url_test.go b/old/backend/internal/model/brand/usecase/products_placement_url_test.go similarity index 100% rename from backend/internal/model/brand/usecase/products_placement_url_test.go rename to old/backend/internal/model/brand/usecase/products_placement_url_test.go diff --git a/backend/internal/model/brand/usecase/usecase.go b/old/backend/internal/model/brand/usecase/usecase.go similarity index 100% rename from backend/internal/model/brand/usecase/usecase.go rename to old/backend/internal/model/brand/usecase/usecase.go diff --git a/backend/internal/model/content_formula/domain/entity/formula.go b/old/backend/internal/model/content_formula/domain/entity/formula.go similarity index 100% rename from backend/internal/model/content_formula/domain/entity/formula.go rename to old/backend/internal/model/content_formula/domain/entity/formula.go diff --git a/backend/internal/model/content_formula/domain/repository/repository.go b/old/backend/internal/model/content_formula/domain/repository/repository.go similarity index 100% rename from backend/internal/model/content_formula/domain/repository/repository.go rename to old/backend/internal/model/content_formula/domain/repository/repository.go diff --git a/backend/internal/model/content_formula/domain/usecase/usecase.go b/old/backend/internal/model/content_formula/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_formula/domain/usecase/usecase.go rename to old/backend/internal/model/content_formula/domain/usecase/usecase.go diff --git a/backend/internal/model/content_formula/repository/mongo.go b/old/backend/internal/model/content_formula/repository/mongo.go similarity index 100% rename from backend/internal/model/content_formula/repository/mongo.go rename to old/backend/internal/model/content_formula/repository/mongo.go diff --git a/backend/internal/model/content_formula/usecase/usecase.go b/old/backend/internal/model/content_formula/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_formula/usecase/usecase.go rename to old/backend/internal/model/content_formula/usecase/usecase.go diff --git a/backend/internal/model/content_matrix/domain/entity/matrix.go b/old/backend/internal/model/content_matrix/domain/entity/matrix.go similarity index 100% rename from backend/internal/model/content_matrix/domain/entity/matrix.go rename to old/backend/internal/model/content_matrix/domain/entity/matrix.go diff --git a/backend/internal/model/content_matrix/domain/repository/repository.go b/old/backend/internal/model/content_matrix/domain/repository/repository.go similarity index 100% rename from backend/internal/model/content_matrix/domain/repository/repository.go rename to old/backend/internal/model/content_matrix/domain/repository/repository.go diff --git a/backend/internal/model/content_matrix/domain/usecase/usecase.go b/old/backend/internal/model/content_matrix/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_matrix/domain/usecase/usecase.go rename to old/backend/internal/model/content_matrix/domain/usecase/usecase.go diff --git a/backend/internal/model/content_matrix/repository/mongo.go b/old/backend/internal/model/content_matrix/repository/mongo.go similarity index 100% rename from backend/internal/model/content_matrix/repository/mongo.go rename to old/backend/internal/model/content_matrix/repository/mongo.go diff --git a/backend/internal/model/content_matrix/usecase/usecase.go b/old/backend/internal/model/content_matrix/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_matrix/usecase/usecase.go rename to old/backend/internal/model/content_matrix/usecase/usecase.go diff --git a/backend/internal/model/content_ops/domain/entity/entity.go b/old/backend/internal/model/content_ops/domain/entity/entity.go similarity index 100% rename from backend/internal/model/content_ops/domain/entity/entity.go rename to old/backend/internal/model/content_ops/domain/entity/entity.go diff --git a/backend/internal/model/content_ops/domain/repository/repository.go b/old/backend/internal/model/content_ops/domain/repository/repository.go similarity index 100% rename from backend/internal/model/content_ops/domain/repository/repository.go rename to old/backend/internal/model/content_ops/domain/repository/repository.go diff --git a/backend/internal/model/content_ops/domain/usecase/usecase.go b/old/backend/internal/model/content_ops/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_ops/domain/usecase/usecase.go rename to old/backend/internal/model/content_ops/domain/usecase/usecase.go diff --git a/backend/internal/model/content_ops/repository/mongo.go b/old/backend/internal/model/content_ops/repository/mongo.go similarity index 100% rename from backend/internal/model/content_ops/repository/mongo.go rename to old/backend/internal/model/content_ops/repository/mongo.go diff --git a/backend/internal/model/content_ops/usecase/usecase.go b/old/backend/internal/model/content_ops/usecase/usecase.go similarity index 100% rename from backend/internal/model/content_ops/usecase/usecase.go rename to old/backend/internal/model/content_ops/usecase/usecase.go diff --git a/backend/internal/model/copy_draft/domain/entity/draft.go b/old/backend/internal/model/copy_draft/domain/entity/draft.go similarity index 100% rename from backend/internal/model/copy_draft/domain/entity/draft.go rename to old/backend/internal/model/copy_draft/domain/entity/draft.go diff --git a/backend/internal/model/copy_draft/domain/repository/repository.go b/old/backend/internal/model/copy_draft/domain/repository/repository.go similarity index 100% rename from backend/internal/model/copy_draft/domain/repository/repository.go rename to old/backend/internal/model/copy_draft/domain/repository/repository.go diff --git a/backend/internal/model/copy_draft/domain/usecase/usecase.go b/old/backend/internal/model/copy_draft/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/copy_draft/domain/usecase/usecase.go rename to old/backend/internal/model/copy_draft/domain/usecase/usecase.go diff --git a/backend/internal/model/copy_draft/repository/mongo.go b/old/backend/internal/model/copy_draft/repository/mongo.go similarity index 100% rename from backend/internal/model/copy_draft/repository/mongo.go rename to old/backend/internal/model/copy_draft/repository/mongo.go diff --git a/backend/internal/model/copy_draft/usecase/usecase.go b/old/backend/internal/model/copy_draft/usecase/usecase.go similarity index 100% rename from backend/internal/model/copy_draft/usecase/usecase.go rename to old/backend/internal/model/copy_draft/usecase/usecase.go diff --git a/backend/internal/model/copy_mission/domain/entity/mission.go b/old/backend/internal/model/copy_mission/domain/entity/mission.go similarity index 100% rename from backend/internal/model/copy_mission/domain/entity/mission.go rename to old/backend/internal/model/copy_mission/domain/entity/mission.go diff --git a/backend/internal/model/copy_mission/domain/repository/repository.go b/old/backend/internal/model/copy_mission/domain/repository/repository.go similarity index 100% rename from backend/internal/model/copy_mission/domain/repository/repository.go rename to old/backend/internal/model/copy_mission/domain/repository/repository.go diff --git a/backend/internal/model/copy_mission/domain/usecase/usecase.go b/old/backend/internal/model/copy_mission/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/copy_mission/domain/usecase/usecase.go rename to old/backend/internal/model/copy_mission/domain/usecase/usecase.go diff --git a/backend/internal/model/copy_mission/repository/mongo.go b/old/backend/internal/model/copy_mission/repository/mongo.go similarity index 100% rename from backend/internal/model/copy_mission/repository/mongo.go rename to old/backend/internal/model/copy_mission/repository/mongo.go diff --git a/backend/internal/model/copy_mission/usecase/usecase.go b/old/backend/internal/model/copy_mission/usecase/usecase.go similarity index 100% rename from backend/internal/model/copy_mission/usecase/usecase.go rename to old/backend/internal/model/copy_mission/usecase/usecase.go diff --git a/backend/internal/model/job/cron/next_run.go b/old/backend/internal/model/job/cron/next_run.go similarity index 100% rename from backend/internal/model/job/cron/next_run.go rename to old/backend/internal/model/job/cron/next_run.go diff --git a/backend/internal/model/job/cron/next_run_test.go b/old/backend/internal/model/job/cron/next_run_test.go similarity index 100% rename from backend/internal/model/job/cron/next_run_test.go rename to old/backend/internal/model/job/cron/next_run_test.go diff --git a/backend/internal/model/job/domain/entity/event.go b/old/backend/internal/model/job/domain/entity/event.go similarity index 100% rename from backend/internal/model/job/domain/entity/event.go rename to old/backend/internal/model/job/domain/entity/event.go diff --git a/backend/internal/model/job/domain/entity/run.go b/old/backend/internal/model/job/domain/entity/run.go similarity index 100% rename from backend/internal/model/job/domain/entity/run.go rename to old/backend/internal/model/job/domain/entity/run.go diff --git a/backend/internal/model/job/domain/entity/schedule.go b/old/backend/internal/model/job/domain/entity/schedule.go similarity index 100% rename from backend/internal/model/job/domain/entity/schedule.go rename to old/backend/internal/model/job/domain/entity/schedule.go diff --git a/backend/internal/model/job/domain/entity/template.go b/old/backend/internal/model/job/domain/entity/template.go similarity index 100% rename from backend/internal/model/job/domain/entity/template.go rename to old/backend/internal/model/job/domain/entity/template.go diff --git a/backend/internal/model/job/domain/enum/status.go b/old/backend/internal/model/job/domain/enum/status.go similarity index 100% rename from backend/internal/model/job/domain/enum/status.go rename to old/backend/internal/model/job/domain/enum/status.go diff --git a/backend/internal/model/job/domain/repository/event.go b/old/backend/internal/model/job/domain/repository/event.go similarity index 100% rename from backend/internal/model/job/domain/repository/event.go rename to old/backend/internal/model/job/domain/repository/event.go diff --git a/backend/internal/model/job/domain/repository/queue.go b/old/backend/internal/model/job/domain/repository/queue.go similarity index 100% rename from backend/internal/model/job/domain/repository/queue.go rename to old/backend/internal/model/job/domain/repository/queue.go diff --git a/backend/internal/model/job/domain/repository/run.go b/old/backend/internal/model/job/domain/repository/run.go similarity index 100% rename from backend/internal/model/job/domain/repository/run.go rename to old/backend/internal/model/job/domain/repository/run.go diff --git a/backend/internal/model/job/domain/repository/schedule.go b/old/backend/internal/model/job/domain/repository/schedule.go similarity index 100% rename from backend/internal/model/job/domain/repository/schedule.go rename to old/backend/internal/model/job/domain/repository/schedule.go diff --git a/backend/internal/model/job/domain/repository/template.go b/old/backend/internal/model/job/domain/repository/template.go similarity index 100% rename from backend/internal/model/job/domain/repository/template.go rename to old/backend/internal/model/job/domain/repository/template.go diff --git a/backend/internal/model/job/domain/usecase/job.go b/old/backend/internal/model/job/domain/usecase/job.go similarity index 93% rename from backend/internal/model/job/domain/usecase/job.go rename to old/backend/internal/model/job/domain/usecase/job.go index ccb3b88..9efd7db 100644 --- a/backend/internal/model/job/domain/usecase/job.go +++ b/old/backend/internal/model/job/domain/usecase/job.go @@ -96,17 +96,12 @@ type UseCase interface { EnsureExpandGraphTemplate(ctx context.Context) error EnsurePlacementScanTemplate(ctx context.Context) error EnsureScanViralTemplate(ctx context.Context) error - EnsureAnalyzeCopyMissionTemplate(ctx context.Context) error - EnsureExpandCopyMissionGraphTemplate(ctx context.Context) error - EnsureGenerateCopyMatrixTemplate(ctx context.Context) error - EnsureGenerateCopyDraftTemplate(ctx context.Context) error EnsureGenerateOutreachDraftTemplate(ctx context.Context) error EnsureGenerateTopicMatrixTemplate(ctx context.Context) error EnsureGenerateFormulaDraftTemplate(ctx context.Context) error EnsureGenerateRewriteDraftTemplate(ctx context.Context) error EnsureRefreshThreadsTokenTemplate(ctx context.Context) error EnsurePublishAnalyticsTemplate(ctx context.Context) error - EnsureRefillPublishInventoryTemplate(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/backend/internal/model/job/repository/mongo_event.go b/old/backend/internal/model/job/repository/mongo_event.go similarity index 100% rename from backend/internal/model/job/repository/mongo_event.go rename to old/backend/internal/model/job/repository/mongo_event.go diff --git a/backend/internal/model/job/repository/mongo_run.go b/old/backend/internal/model/job/repository/mongo_run.go similarity index 100% rename from backend/internal/model/job/repository/mongo_run.go rename to old/backend/internal/model/job/repository/mongo_run.go diff --git a/backend/internal/model/job/repository/mongo_schedule.go b/old/backend/internal/model/job/repository/mongo_schedule.go similarity index 100% rename from backend/internal/model/job/repository/mongo_schedule.go rename to old/backend/internal/model/job/repository/mongo_schedule.go diff --git a/backend/internal/model/job/repository/mongo_template.go b/old/backend/internal/model/job/repository/mongo_template.go similarity index 100% rename from backend/internal/model/job/repository/mongo_template.go rename to old/backend/internal/model/job/repository/mongo_template.go diff --git a/backend/internal/model/job/repository/redis_queue.go b/old/backend/internal/model/job/repository/redis_queue.go similarity index 100% rename from backend/internal/model/job/repository/redis_queue.go rename to old/backend/internal/model/job/repository/redis_queue.go diff --git a/backend/internal/model/job/repository/redis_queue_test.go b/old/backend/internal/model/job/repository/redis_queue_test.go similarity index 100% rename from backend/internal/model/job/repository/redis_queue_test.go rename to old/backend/internal/model/job/repository/redis_queue_test.go diff --git a/backend/internal/model/job/resume/resume.go b/old/backend/internal/model/job/resume/resume.go similarity index 100% rename from backend/internal/model/job/resume/resume.go rename to old/backend/internal/model/job/resume/resume.go diff --git a/backend/internal/model/job/resume/resume_test.go b/old/backend/internal/model/job/resume/resume_test.go similarity index 100% rename from backend/internal/model/job/resume/resume_test.go rename to old/backend/internal/model/job/resume/resume_test.go diff --git a/backend/internal/model/job/usecase/cancel_test.go b/old/backend/internal/model/job/usecase/cancel_test.go similarity index 100% rename from backend/internal/model/job/usecase/cancel_test.go rename to old/backend/internal/model/job/usecase/cancel_test.go diff --git a/backend/internal/model/job/usecase/dedupe_test.go b/old/backend/internal/model/job/usecase/dedupe_test.go similarity index 100% rename from backend/internal/model/job/usecase/dedupe_test.go rename to old/backend/internal/model/job/usecase/dedupe_test.go diff --git a/backend/internal/model/job/usecase/enqueue_run_test.go b/old/backend/internal/model/job/usecase/enqueue_run_test.go similarity index 100% rename from backend/internal/model/job/usecase/enqueue_run_test.go rename to old/backend/internal/model/job/usecase/enqueue_run_test.go diff --git a/backend/internal/model/job/usecase/helpers.go b/old/backend/internal/model/job/usecase/helpers.go similarity index 100% rename from backend/internal/model/job/usecase/helpers.go rename to old/backend/internal/model/job/usecase/helpers.go diff --git a/backend/internal/model/job/usecase/maintenance.go b/old/backend/internal/model/job/usecase/maintenance.go similarity index 100% rename from backend/internal/model/job/usecase/maintenance.go rename to old/backend/internal/model/job/usecase/maintenance.go diff --git a/backend/internal/model/job/usecase/maintenance_stuck_test.go b/old/backend/internal/model/job/usecase/maintenance_stuck_test.go similarity index 100% rename from backend/internal/model/job/usecase/maintenance_stuck_test.go rename to old/backend/internal/model/job/usecase/maintenance_stuck_test.go diff --git a/backend/internal/model/job/usecase/maintenance_test.go b/old/backend/internal/model/job/usecase/maintenance_test.go similarity index 100% rename from backend/internal/model/job/usecase/maintenance_test.go rename to old/backend/internal/model/job/usecase/maintenance_test.go diff --git a/backend/internal/model/job/usecase/refresh_lock_test.go b/old/backend/internal/model/job/usecase/refresh_lock_test.go similarity index 100% rename from backend/internal/model/job/usecase/refresh_lock_test.go rename to old/backend/internal/model/job/usecase/refresh_lock_test.go diff --git a/backend/internal/model/job/usecase/retry_max_attempts_test.go b/old/backend/internal/model/job/usecase/retry_max_attempts_test.go similarity index 100% rename from backend/internal/model/job/usecase/retry_max_attempts_test.go rename to old/backend/internal/model/job/usecase/retry_max_attempts_test.go diff --git a/backend/internal/model/job/usecase/retry_resume_test.go b/old/backend/internal/model/job/usecase/retry_resume_test.go similarity index 100% rename from backend/internal/model/job/usecase/retry_resume_test.go rename to old/backend/internal/model/job/usecase/retry_resume_test.go diff --git a/backend/internal/model/job/usecase/retry_test.go b/old/backend/internal/model/job/usecase/retry_test.go similarity index 100% rename from backend/internal/model/job/usecase/retry_test.go rename to old/backend/internal/model/job/usecase/retry_test.go diff --git a/backend/internal/model/job/usecase/schedule.go b/old/backend/internal/model/job/usecase/schedule.go similarity index 100% rename from backend/internal/model/job/usecase/schedule.go rename to old/backend/internal/model/job/usecase/schedule.go diff --git a/backend/internal/model/job/usecase/schedule_payload.go b/old/backend/internal/model/job/usecase/schedule_payload.go similarity index 100% rename from backend/internal/model/job/usecase/schedule_payload.go rename to old/backend/internal/model/job/usecase/schedule_payload.go diff --git a/backend/internal/model/job/usecase/schedule_payload_test.go b/old/backend/internal/model/job/usecase/schedule_payload_test.go similarity index 100% rename from backend/internal/model/job/usecase/schedule_payload_test.go rename to old/backend/internal/model/job/usecase/schedule_payload_test.go diff --git a/backend/internal/model/job/usecase/schedule_test.go b/old/backend/internal/model/job/usecase/schedule_test.go similarity index 100% rename from backend/internal/model/job/usecase/schedule_test.go rename to old/backend/internal/model/job/usecase/schedule_test.go diff --git a/backend/internal/model/job/usecase/step_resume.go b/old/backend/internal/model/job/usecase/step_resume.go similarity index 100% rename from backend/internal/model/job/usecase/step_resume.go rename to old/backend/internal/model/job/usecase/step_resume.go diff --git a/backend/internal/model/job/usecase/template.go b/old/backend/internal/model/job/usecase/template.go similarity index 100% rename from backend/internal/model/job/usecase/template.go rename to old/backend/internal/model/job/usecase/template.go diff --git a/backend/internal/model/job/usecase/template_test.go b/old/backend/internal/model/job/usecase/template_test.go similarity index 100% rename from backend/internal/model/job/usecase/template_test.go rename to old/backend/internal/model/job/usecase/template_test.go diff --git a/backend/internal/model/job/usecase/test_mocks.go b/old/backend/internal/model/job/usecase/test_mocks.go similarity index 100% rename from backend/internal/model/job/usecase/test_mocks.go rename to old/backend/internal/model/job/usecase/test_mocks.go diff --git a/backend/internal/model/job/usecase/usecase.go b/old/backend/internal/model/job/usecase/usecase.go similarity index 80% rename from backend/internal/model/job/usecase/usecase.go rename to old/backend/internal/model/job/usecase/usecase.go index 75cb93a..d6aa762 100644 --- a/backend/internal/model/job/usecase/usecase.go +++ b/old/backend/internal/model/job/usecase/usecase.go @@ -14,38 +14,28 @@ import ( ) const ( - demoTemplateType = "demo_long_task" - style8DTemplateType = "style-8d" - expandGraphTemplateType = "expand-graph" - placementScanTemplateType = "placement-scan" - scanViralTemplateType = "scan-viral" - analyzeCopyMissionTemplateType = "analyze-copy-mission" - expandCopyMissionGraphTemplateType = "expand-copy-mission-graph" - generateCopyMatrixTemplateType = "generate-copy-matrix" - generateCopyDraftTemplateType = "generate-copy-draft" - generateOutreachDraftTemplateType = "generate-outreach-draft" - generateTopicMatrixTemplateType = "generate-topic-matrix" - generateFormulaDraftTemplateType = "generate-formula-draft" - generateRewriteDraftTemplateType = "generate-rewrite-draft" - refreshThreadsTokenTemplateType = "refresh-threads-token" - publishAnalyticsTemplateType = "publish-analytics" - refillPublishInventoryTemplateType = "refill-publish-inventory" - workerTypeDemo = "go-demo" - workerTypeExpandGraph = "go-expand-graph" - workerTypePlacementScan = "go-placement-scan" - workerTypeScanViral = "go-scan-viral" - workerTypeAnalyzeCopyMission = "go-analyze-copy-mission" - workerTypeExpandCopyMissionGraph = "go-expand-copy-mission-graph" - workerTypeGenerateCopyMatrix = "go-generate-copy-matrix" - workerTypeGenerateCopyDraft = "go-generate-copy-draft" - workerTypeGenerateOutreachDraft = "go-generate-outreach-draft" - workerTypeGenerateTopicMatrix = "go-generate-topic-matrix" - workerTypeGenerateFormulaDraft = "go-generate-formula-draft" - workerTypeGenerateRewriteDraft = "go-generate-rewrite-draft" - workerTypeRefreshThreadsToken = "go-refresh-threads-token" - workerTypePublishAnalytics = "go-publish-analytics" - workerTypeRefillPublishInventory = "go-refill-publish-inventory" - style8DWorkerType = "node-style-8d" + demoTemplateType = "demo_long_task" + style8DTemplateType = "style-8d" + expandGraphTemplateType = "expand-graph" + placementScanTemplateType = "placement-scan" + scanViralTemplateType = "scan-viral" + generateOutreachDraftTemplateType = "generate-outreach-draft" + generateTopicMatrixTemplateType = "generate-topic-matrix" + generateFormulaDraftTemplateType = "generate-formula-draft" + generateRewriteDraftTemplateType = "generate-rewrite-draft" + refreshThreadsTokenTemplateType = "refresh-threads-token" + publishAnalyticsTemplateType = "publish-analytics" + workerTypeDemo = "go-demo" + workerTypeExpandGraph = "go-expand-graph" + workerTypePlacementScan = "go-placement-scan" + workerTypeScanViral = "go-scan-viral" + workerTypeGenerateOutreachDraft = "go-generate-outreach-draft" + workerTypeGenerateTopicMatrix = "go-generate-topic-matrix" + workerTypeGenerateFormulaDraft = "go-generate-formula-draft" + workerTypeGenerateRewriteDraft = "go-generate-rewrite-draft" + workerTypeRefreshThreadsToken = "go-refresh-threads-token" + workerTypePublishAnalytics = "go-publish-analytics" + style8DWorkerType = "node-style-8d" ) type UseCase = domusecase.UseCase @@ -107,26 +97,6 @@ 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) EnsureExpandCopyMissionGraphTemplate(ctx context.Context) error { - _, err := u.templates.Upsert(ctx, expandCopyMissionGraphTemplate()) - 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 (u *jobUseCase) EnsureGenerateOutreachDraftTemplate(ctx context.Context) error { _, err := u.templates.Upsert(ctx, generateOutreachDraftTemplate()) return err @@ -157,89 +127,6 @@ func (u *jobUseCase) EnsurePublishAnalyticsTemplate(ctx context.Context) error { return err } -func (u *jobUseCase) EnsureRefillPublishInventoryTemplate(ctx context.Context) error { - _, err := u.templates.Upsert(ctx, refillPublishInventoryTemplate()) - return err -} - -func expandCopyMissionGraphTemplate() *entity.Template { - return &entity.Template{ - Type: expandCopyMissionGraphTemplateType, - Version: 1, - Name: "Expand Copy Mission Knowledge Graph", - Description: "Brave/LLM knowledge graph for copy ninja missions", - Enabled: true, - Repeatable: true, - ConcurrencyPolicy: string(enum.ConcurrencyRejectSameScope), - DedupeKeys: []string{"scope_id", "seed_query"}, - TimeoutSeconds: 600, - CancelPolicy: entity.CancelPolicy{ - Supported: true, - Mode: "cooperative", - GraceSeconds: 30, - }, - RetryPolicy: entity.RetryPolicy{ - MaxAttempts: 1, - BackoffSeconds: []int{}, - }, - Steps: []entity.TemplateStep{ - {ID: "expand_copy_knowledge", Name: "Expand copy mission knowledge", WorkerType: workerTypeExpandCopyMissionGraph, TimeoutSeconds: 600, Cancelable: true}, - }, - } -} - -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: workerTypeAnalyzeCopyMission, 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: workerTypeGenerateCopyMatrix, TimeoutSeconds: 600, Cancelable: true}, - }, - } -} - func generateTopicMatrixTemplate() *entity.Template { return &entity.Template{ Type: generateTopicMatrixTemplateType, @@ -344,32 +231,6 @@ func generateOutreachDraftTemplate() *entity.Template { } } -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: workerTypeGenerateCopyDraft, TimeoutSeconds: 600, Cancelable: true}, - }, - } -} - func refreshThreadsTokenTemplate() *entity.Template { return &entity.Template{ Type: refreshThreadsTokenTemplateType, @@ -418,32 +279,6 @@ func publishAnalyticsTemplate() *entity.Template { } } -func refillPublishInventoryTemplate() *entity.Template { - return &entity.Template{ - Type: refillPublishInventoryTemplateType, - Version: 1, - Name: "Refill Publish Inventory", - Description: "Generate and schedule drafts when Threads publish inventory is low", - Enabled: true, - Repeatable: true, - ConcurrencyPolicy: string(enum.ConcurrencyRejectSameScope), - DedupeKeys: []string{"account_id"}, - TimeoutSeconds: 900, - CancelPolicy: entity.CancelPolicy{ - Supported: true, - Mode: "cooperative", - GraceSeconds: 30, - }, - RetryPolicy: entity.RetryPolicy{ - MaxAttempts: 1, - BackoffSeconds: []int{}, - }, - Steps: []entity.TemplateStep{ - {ID: "refill_publish_inventory", Name: "Refill publish inventory", WorkerType: workerTypeRefillPublishInventory, TimeoutSeconds: 900, Cancelable: true}, - }, - } -} - func scanViralTemplate() *entity.Template { return &entity.Template{ Type: scanViralTemplateType, diff --git a/backend/internal/model/knowledge_graph/domain/entity/graph.go b/old/backend/internal/model/knowledge_graph/domain/entity/graph.go similarity index 100% rename from backend/internal/model/knowledge_graph/domain/entity/graph.go rename to old/backend/internal/model/knowledge_graph/domain/entity/graph.go diff --git a/backend/internal/model/knowledge_graph/domain/repository/repository.go b/old/backend/internal/model/knowledge_graph/domain/repository/repository.go similarity index 100% rename from backend/internal/model/knowledge_graph/domain/repository/repository.go rename to old/backend/internal/model/knowledge_graph/domain/repository/repository.go diff --git a/backend/internal/model/knowledge_graph/domain/usecase/usecase.go b/old/backend/internal/model/knowledge_graph/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/knowledge_graph/domain/usecase/usecase.go rename to old/backend/internal/model/knowledge_graph/domain/usecase/usecase.go diff --git a/backend/internal/model/knowledge_graph/repository/mongo.go b/old/backend/internal/model/knowledge_graph/repository/mongo.go similarity index 100% rename from backend/internal/model/knowledge_graph/repository/mongo.go rename to old/backend/internal/model/knowledge_graph/repository/mongo.go diff --git a/backend/internal/model/knowledge_graph/usecase/topic_graph.go b/old/backend/internal/model/knowledge_graph/usecase/topic_graph.go similarity index 100% rename from backend/internal/model/knowledge_graph/usecase/topic_graph.go rename to old/backend/internal/model/knowledge_graph/usecase/topic_graph.go diff --git a/backend/internal/model/knowledge_graph/usecase/usecase.go b/old/backend/internal/model/knowledge_graph/usecase/usecase.go similarity index 100% rename from backend/internal/model/knowledge_graph/usecase/usecase.go rename to old/backend/internal/model/knowledge_graph/usecase/usecase.go diff --git a/backend/internal/model/member/domain/entity/member.go b/old/backend/internal/model/member/domain/entity/member.go similarity index 93% rename from backend/internal/model/member/domain/entity/member.go rename to old/backend/internal/model/member/domain/entity/member.go index fd3e13e..3da6507 100644 --- a/backend/internal/model/member/domain/entity/member.go +++ b/old/backend/internal/model/member/domain/entity/member.go @@ -16,6 +16,8 @@ type Origin string const ( OriginNative Origin = "native" + // OriginOAuth is reserved for future third-party sign-in; those accounts skip native email verification. + OriginOAuth Origin = "oauth" ) type Member struct { diff --git a/backend/internal/model/member/domain/repository/member.go b/old/backend/internal/model/member/domain/repository/member.go similarity index 93% rename from backend/internal/model/member/domain/repository/member.go rename to old/backend/internal/model/member/domain/repository/member.go index 3d13ee2..089f975 100644 --- a/backend/internal/model/member/domain/repository/member.go +++ b/old/backend/internal/model/member/domain/repository/member.go @@ -30,5 +30,6 @@ type Repository interface { SetRoles(ctx context.Context, tenantID, uid string, roles []string) error SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID string) error SetEmailVerificationCode(ctx context.Context, tenantID, uid, code string, expiresAt int64) error + ConfirmEmailVerification(ctx context.Context, tenantID, uid, code string) (*entity.Member, error) UpdatePassword(ctx context.Context, tenantID, uid, passwordHash string) error } diff --git a/backend/internal/model/member/domain/usecase/member.go b/old/backend/internal/model/member/domain/usecase/member.go similarity index 90% rename from backend/internal/model/member/domain/usecase/member.go rename to old/backend/internal/model/member/domain/usecase/member.go index 00940b0..27e54a2 100644 --- a/backend/internal/model/member/domain/usecase/member.go +++ b/old/backend/internal/model/member/domain/usecase/member.go @@ -67,5 +67,7 @@ type UseCase interface { UpdatePassword(ctx context.Context, tenantID, uid, password string) error SetRoles(ctx context.Context, tenantID, uid string, roles []string) error SetEmailVerificationCode(ctx context.Context, tenantID, uid, code string, expiresAt int64) error + VerifyEmail(ctx context.Context, tenantID, uid, code string) (*entity.Member, error) + ResendEmailVerification(ctx context.Context, tenantID, uid string, send func(email, code string) error) error SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID string) error } diff --git a/backend/internal/model/member/repository/mongo.go b/old/backend/internal/model/member/repository/mongo.go similarity index 86% rename from backend/internal/model/member/repository/mongo.go rename to old/backend/internal/model/member/repository/mongo.go index e4ace3f..a4efa55 100644 --- a/backend/internal/model/member/repository/mongo.go +++ b/old/backend/internal/model/member/repository/mongo.go @@ -219,6 +219,47 @@ func (r *mongoRepository) UpdatePassword(ctx context.Context, tenantID, uid, pas return nil } +func (r *mongoRepository) ConfirmEmailVerification(ctx context.Context, tenantID, uid, verifyCode string) (*entity.Member, error) { + if r.collection == nil { + return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") + } + member, err := r.FindByUID(ctx, tenantID, uid) + if err != nil { + return nil, err + } + if member.EmailVerified { + return member, nil + } + normalizedCode := strings.TrimSpace(verifyCode) + if normalizedCode == "" || member.EmailVerifyCode == "" || member.EmailVerifyCode != normalizedCode { + return nil, app.For(code.Auth).AuthUnauthorized("invalid verification code") + } + if member.EmailVerifyExpiresAt > 0 && clock.NowUnixNano() > member.EmailVerifyExpiresAt { + return nil, app.For(code.Auth).AuthUnauthorized("verification code expired") + } + res, err := r.collection.UpdateOne( + ctx, + bson.M{"tenant_id": tenantID, "uid": uid, "email_verify_code": member.EmailVerifyCode}, + bson.M{ + "$set": bson.M{ + "email_verified": true, + "update_at": clock.NowUnixNano(), + }, + "$unset": bson.M{ + "email_verify_code": "", + "email_verify_expires_at": "", + }, + }, + ) + if err != nil { + return nil, err + } + if res.MatchedCount == 0 { + return nil, app.For(code.Auth).AuthUnauthorized("invalid verification code") + } + return r.FindByUID(ctx, tenantID, uid) +} + func (r *mongoRepository) SetEmailVerificationCode(ctx context.Context, tenantID, uid, verifyCode string, expiresAt int64) error { if r.collection == nil { return app.For(code.Member).DBUnavailable("Mongo is not configured") diff --git a/backend/internal/model/member/usecase/usecase.go b/old/backend/internal/model/member/usecase/usecase.go similarity index 77% rename from backend/internal/model/member/usecase/usecase.go rename to old/backend/internal/model/member/usecase/usecase.go index 86273ce..336a46c 100644 --- a/backend/internal/model/member/usecase/usecase.go +++ b/old/backend/internal/model/member/usecase/usecase.go @@ -2,8 +2,13 @@ package usecase import ( "context" + "crypto/rand" + "fmt" + "math/big" "strings" + "time" + "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" authusecase "haixun-backend/internal/model/auth/domain/usecase" @@ -127,6 +132,74 @@ func (u *memberUseCase) SetEmailVerificationCode(ctx context.Context, tenantID, return u.repo.SetEmailVerificationCode(ctx, tenantID, uid, strings.TrimSpace(verifyCode), expiresAt) } +func (u *memberUseCase) VerifyEmail(ctx context.Context, tenantID, uid, verifyCode string) (*entity.Member, error) { + if tenantID == "" || uid == "" || strings.TrimSpace(verifyCode) == "" { + return nil, app.For(code.Auth).InputMissingRequired("tenant_id, uid, and code are required") + } + member, err := u.repo.FindByUID(ctx, tenantID, uid) + if err != nil { + return nil, err + } + if member.Origin != "" && member.Origin != entity.OriginNative { + if member.EmailVerified { + return member, nil + } + return u.repo.UpdateProfile(ctx, tenantID, uid, domrepo.ProfileUpdate{ + EmailVerified: boolPtr(true), + }) + } + return u.repo.ConfirmEmailVerification(ctx, tenantID, uid, strings.TrimSpace(verifyCode)) +} + +func (u *memberUseCase) ResendEmailVerification(ctx context.Context, tenantID, uid string, send func(email, code string) error) error { + if tenantID == "" || uid == "" { + return app.For(code.Auth).InputMissingRequired("tenant_id and uid are required") + } + if send == nil { + return app.For(code.Auth).SysNotImplemented("email sender is not configured") + } + member, err := u.repo.FindByUID(ctx, tenantID, uid) + if err != nil { + return err + } + if member.Origin != "" && member.Origin != entity.OriginNative { + return nil + } + if member.EmailVerified { + return nil + } + if member.EmailVerifyExpiresAt > 0 { + sentAt := member.EmailVerifyExpiresAt - int64(15*time.Minute) + if clock.NowUnixNano()-sentAt < int64(60*time.Second) { + return app.For(code.Auth).ResInvalidState("verification email was sent recently; please wait before resending") + } + } + verifyCode, err := generateVerifyCode() + if err != nil { + return err + } + expiresAt := clock.NowUnixNano() + int64(15*time.Minute) + if err := u.repo.SetEmailVerificationCode(ctx, tenantID, uid, verifyCode, expiresAt); err != nil { + return err + } + if err := send(member.Email, verifyCode); err != nil { + return app.For(code.Auth).SvcThirdParty("驗證碼已產生但寄信失敗,請確認 SMTP 或 Mailpit 是否啟動").WithCause(err) + } + return nil +} + +func boolPtr(value bool) *bool { + return &value +} + +func generateVerifyCode() (string, error) { + n, err := rand.Int(rand.Reader, big.NewInt(1000000)) + if err != nil { + return "", err + } + return fmt.Sprintf("%06d", n.Int64()), nil +} + func (u *memberUseCase) UpdatePassword(ctx context.Context, tenantID, uid, password string) error { if tenantID == "" || uid == "" || password == "" { return app.For(code.Member).InputMissingRequired("tenant_id, uid, and password are required") diff --git a/backend/internal/model/mention_inbox/domain/entity/mention.go b/old/backend/internal/model/mention_inbox/domain/entity/mention.go similarity index 100% rename from backend/internal/model/mention_inbox/domain/entity/mention.go rename to old/backend/internal/model/mention_inbox/domain/entity/mention.go diff --git a/backend/internal/model/mention_inbox/domain/repository/repository.go b/old/backend/internal/model/mention_inbox/domain/repository/repository.go similarity index 100% rename from backend/internal/model/mention_inbox/domain/repository/repository.go rename to old/backend/internal/model/mention_inbox/domain/repository/repository.go diff --git a/backend/internal/model/mention_inbox/domain/usecase/usecase.go b/old/backend/internal/model/mention_inbox/domain/usecase/usecase.go similarity index 97% rename from backend/internal/model/mention_inbox/domain/usecase/usecase.go rename to old/backend/internal/model/mention_inbox/domain/usecase/usecase.go index a5a8be4..8e6387c 100644 --- a/backend/internal/model/mention_inbox/domain/usecase/usecase.go +++ b/old/backend/internal/model/mention_inbox/domain/usecase/usecase.go @@ -63,7 +63,8 @@ type PublishReplyRequest struct { OwnerUID string AccountID string MediaID string - Text string + Text string + ImageURLs []string } type PublishReplyResult struct { diff --git a/backend/internal/model/mention_inbox/repository/mongo.go b/old/backend/internal/model/mention_inbox/repository/mongo.go similarity index 100% rename from backend/internal/model/mention_inbox/repository/mongo.go rename to old/backend/internal/model/mention_inbox/repository/mongo.go diff --git a/backend/internal/model/mention_inbox/usecase/usecase.go b/old/backend/internal/model/mention_inbox/usecase/usecase.go similarity index 96% rename from backend/internal/model/mention_inbox/usecase/usecase.go rename to old/backend/internal/model/mention_inbox/usecase/usecase.go index 8ae69c0..61c8329 100644 --- a/backend/internal/model/mention_inbox/usecase/usecase.go +++ b/old/backend/internal/model/mention_inbox/usecase/usecase.go @@ -8,6 +8,7 @@ import ( app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libthreads "haixun-backend/internal/library/threadsapi" + "haixun-backend/internal/library/threadspost" "haixun-backend/internal/model/mention_inbox/domain/entity" domrepo "haixun-backend/internal/model/mention_inbox/domain/repository" domusecase "haixun-backend/internal/model/mention_inbox/domain/usecase" @@ -168,14 +169,18 @@ func (u *mentionInboxUseCase) PublishReply(ctx context.Context, req domusecase.P accountID := strings.TrimSpace(req.AccountID) mediaID := strings.TrimSpace(req.MediaID) text := strings.TrimSpace(req.Text) + imageURLs := req.ImageURLs if tenantID == "" || ownerUID == "" { return nil, app.For(code.Auth).AuthUnauthorized("missing actor") } if accountID == "" || mediaID == "" { return nil, app.For(code.ThreadsAccount).InputMissingRequired("account_id and media_id are required") } - if text == "" { - return nil, app.For(code.ThreadsAccount).InputMissingRequired("text is required") + if text == "" && len(imageURLs) == 0 { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("text or image_keys is required") + } + if err := threadspost.ValidateReplyContent(text, len(imageURLs) > 0); err != nil { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) } item, err := u.repo.Get(ctx, tenantID, ownerUID, accountID, mediaID) @@ -213,6 +218,7 @@ func (u *mentionInboxUseCase) PublishReply(ctx context.Context, req domusecase.P AccessToken: token, ReplyToID: mediaID, Text: text, + ImageURLs: imageURLs, }) if err != nil { return nil, err diff --git a/backend/internal/model/outreach_draft/domain/entity/draft.go b/old/backend/internal/model/outreach_draft/domain/entity/draft.go similarity index 100% rename from backend/internal/model/outreach_draft/domain/entity/draft.go rename to old/backend/internal/model/outreach_draft/domain/entity/draft.go diff --git a/backend/internal/model/outreach_draft/domain/repository/repository.go b/old/backend/internal/model/outreach_draft/domain/repository/repository.go similarity index 100% rename from backend/internal/model/outreach_draft/domain/repository/repository.go rename to old/backend/internal/model/outreach_draft/domain/repository/repository.go diff --git a/backend/internal/model/outreach_draft/domain/usecase/usecase.go b/old/backend/internal/model/outreach_draft/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/outreach_draft/domain/usecase/usecase.go rename to old/backend/internal/model/outreach_draft/domain/usecase/usecase.go diff --git a/backend/internal/model/outreach_draft/repository/mongo.go b/old/backend/internal/model/outreach_draft/repository/mongo.go similarity index 100% rename from backend/internal/model/outreach_draft/repository/mongo.go rename to old/backend/internal/model/outreach_draft/repository/mongo.go diff --git a/backend/internal/model/outreach_draft/usecase/errors.go b/old/backend/internal/model/outreach_draft/usecase/errors.go similarity index 100% rename from backend/internal/model/outreach_draft/usecase/errors.go rename to old/backend/internal/model/outreach_draft/usecase/errors.go diff --git a/backend/internal/model/outreach_draft/usecase/usecase.go b/old/backend/internal/model/outreach_draft/usecase/usecase.go similarity index 100% rename from backend/internal/model/outreach_draft/usecase/usecase.go rename to old/backend/internal/model/outreach_draft/usecase/usecase.go diff --git a/backend/internal/model/own_post_formula/domain/entity/review.go b/old/backend/internal/model/own_post_formula/domain/entity/review.go similarity index 100% rename from backend/internal/model/own_post_formula/domain/entity/review.go rename to old/backend/internal/model/own_post_formula/domain/entity/review.go diff --git a/backend/internal/model/own_post_formula/domain/repository/repository.go b/old/backend/internal/model/own_post_formula/domain/repository/repository.go similarity index 100% rename from backend/internal/model/own_post_formula/domain/repository/repository.go rename to old/backend/internal/model/own_post_formula/domain/repository/repository.go diff --git a/backend/internal/model/own_post_formula/repository/mongo.go b/old/backend/internal/model/own_post_formula/repository/mongo.go similarity index 100% rename from backend/internal/model/own_post_formula/repository/mongo.go rename to old/backend/internal/model/own_post_formula/repository/mongo.go diff --git a/backend/internal/model/own_post_formula/usecase/usecase.go b/old/backend/internal/model/own_post_formula/usecase/usecase.go similarity index 100% rename from backend/internal/model/own_post_formula/usecase/usecase.go rename to old/backend/internal/model/own_post_formula/usecase/usecase.go diff --git a/backend/internal/model/permission/domain/entity/permission.go b/old/backend/internal/model/permission/domain/entity/permission.go similarity index 100% rename from backend/internal/model/permission/domain/entity/permission.go rename to old/backend/internal/model/permission/domain/entity/permission.go diff --git a/backend/internal/model/permission/domain/repository/permission.go b/old/backend/internal/model/permission/domain/repository/permission.go similarity index 100% rename from backend/internal/model/permission/domain/repository/permission.go rename to old/backend/internal/model/permission/domain/repository/permission.go diff --git a/backend/internal/model/permission/domain/usecase/permission.go b/old/backend/internal/model/permission/domain/usecase/permission.go similarity index 100% rename from backend/internal/model/permission/domain/usecase/permission.go rename to old/backend/internal/model/permission/domain/usecase/permission.go diff --git a/backend/internal/model/permission/repository/mongo_permission.go b/old/backend/internal/model/permission/repository/mongo_permission.go similarity index 100% rename from backend/internal/model/permission/repository/mongo_permission.go rename to old/backend/internal/model/permission/repository/mongo_permission.go diff --git a/backend/internal/model/permission/repository/mongo_role_permission.go b/old/backend/internal/model/permission/repository/mongo_role_permission.go similarity index 100% rename from backend/internal/model/permission/repository/mongo_role_permission.go rename to old/backend/internal/model/permission/repository/mongo_role_permission.go diff --git a/backend/internal/model/permission/repository/object_id.go b/old/backend/internal/model/permission/repository/object_id.go similarity index 100% rename from backend/internal/model/permission/repository/object_id.go rename to old/backend/internal/model/permission/repository/object_id.go diff --git a/backend/internal/model/permission/usecase/usecase.go b/old/backend/internal/model/permission/usecase/usecase.go similarity index 96% rename from backend/internal/model/permission/usecase/usecase.go rename to old/backend/internal/model/permission/usecase/usecase.go index 99decdd..b82f6e9 100644 --- a/backend/internal/model/permission/usecase/usecase.go +++ b/old/backend/internal/model/permission/usecase/usecase.go @@ -203,6 +203,8 @@ func mergePermissions(base []*entity.Permission, extras []*entity.Permission) [] func defaultUserPermissionNames() []string { return []string{ "auth.logout", + "auth.verify_email", + "auth.resend_verification_email", "member.me.read", "member.me.update", "member.avatar.update", @@ -267,6 +269,8 @@ func defaultPermissions() []*entity.Permission { allMethods := "GET|POST|PUT|PATCH|DELETE" return []*entity.Permission{ {Name: "auth.logout", HTTPMethods: "POST", HTTPPath: "/api/v1/auth/logout", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, + {Name: "auth.verify_email", HTTPMethods: "POST", HTTPPath: "/api/v1/auth/verify-email", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, + {Name: "auth.resend_verification_email", HTTPMethods: "POST", HTTPPath: "/api/v1/auth/resend-verification-email", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.me.read", HTTPMethods: "GET", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.me.update", HTTPMethods: "PATCH", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, diff --git a/backend/internal/model/permission/usecase/usecase_test.go b/old/backend/internal/model/permission/usecase/usecase_test.go similarity index 100% rename from backend/internal/model/permission/usecase/usecase_test.go rename to old/backend/internal/model/permission/usecase/usecase_test.go diff --git a/backend/internal/model/persona/domain/entity/copy_research_map.go b/old/backend/internal/model/persona/domain/entity/copy_research_map.go similarity index 100% rename from backend/internal/model/persona/domain/entity/copy_research_map.go rename to old/backend/internal/model/persona/domain/entity/copy_research_map.go diff --git a/backend/internal/model/persona/domain/entity/persona.go b/old/backend/internal/model/persona/domain/entity/persona.go similarity index 100% rename from backend/internal/model/persona/domain/entity/persona.go rename to old/backend/internal/model/persona/domain/entity/persona.go diff --git a/backend/internal/model/persona/domain/repository/repository.go b/old/backend/internal/model/persona/domain/repository/repository.go similarity index 100% rename from backend/internal/model/persona/domain/repository/repository.go rename to old/backend/internal/model/persona/domain/repository/repository.go diff --git a/backend/internal/model/persona/domain/usecase/usecase.go b/old/backend/internal/model/persona/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/persona/domain/usecase/usecase.go rename to old/backend/internal/model/persona/domain/usecase/usecase.go diff --git a/backend/internal/model/persona/repository/mongo.go b/old/backend/internal/model/persona/repository/mongo.go similarity index 100% rename from backend/internal/model/persona/repository/mongo.go rename to old/backend/internal/model/persona/repository/mongo.go diff --git a/backend/internal/model/persona/usecase/usecase.go b/old/backend/internal/model/persona/usecase/usecase.go similarity index 100% rename from backend/internal/model/persona/usecase/usecase.go rename to old/backend/internal/model/persona/usecase/usecase.go diff --git a/backend/internal/model/placement/usecase/settings.go b/old/backend/internal/model/placement/usecase/settings.go similarity index 100% rename from backend/internal/model/placement/usecase/settings.go rename to old/backend/internal/model/placement/usecase/settings.go diff --git a/backend/internal/model/placement/usecase/settings_dev_mode_test.go b/old/backend/internal/model/placement/usecase/settings_dev_mode_test.go similarity index 100% rename from backend/internal/model/placement/usecase/settings_dev_mode_test.go rename to old/backend/internal/model/placement/usecase/settings_dev_mode_test.go diff --git a/backend/internal/model/placement_topic/domain/entity/topic.go b/old/backend/internal/model/placement_topic/domain/entity/topic.go similarity index 100% rename from backend/internal/model/placement_topic/domain/entity/topic.go rename to old/backend/internal/model/placement_topic/domain/entity/topic.go diff --git a/backend/internal/model/placement_topic/domain/repository/repository.go b/old/backend/internal/model/placement_topic/domain/repository/repository.go similarity index 100% rename from backend/internal/model/placement_topic/domain/repository/repository.go rename to old/backend/internal/model/placement_topic/domain/repository/repository.go diff --git a/backend/internal/model/placement_topic/domain/usecase/usecase.go b/old/backend/internal/model/placement_topic/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/placement_topic/domain/usecase/usecase.go rename to old/backend/internal/model/placement_topic/domain/usecase/usecase.go diff --git a/backend/internal/model/placement_topic/repository/mongo.go b/old/backend/internal/model/placement_topic/repository/mongo.go similarity index 100% rename from backend/internal/model/placement_topic/repository/mongo.go rename to old/backend/internal/model/placement_topic/repository/mongo.go diff --git a/backend/internal/model/placement_topic/usecase/usecase.go b/old/backend/internal/model/placement_topic/usecase/usecase.go similarity index 100% rename from backend/internal/model/placement_topic/usecase/usecase.go rename to old/backend/internal/model/placement_topic/usecase/usecase.go diff --git a/backend/internal/model/publish_analytics/domain/entity/analytics.go b/old/backend/internal/model/publish_analytics/domain/entity/analytics.go similarity index 100% rename from backend/internal/model/publish_analytics/domain/entity/analytics.go rename to old/backend/internal/model/publish_analytics/domain/entity/analytics.go diff --git a/backend/internal/model/publish_analytics/domain/repository/repository.go b/old/backend/internal/model/publish_analytics/domain/repository/repository.go similarity index 100% rename from backend/internal/model/publish_analytics/domain/repository/repository.go rename to old/backend/internal/model/publish_analytics/domain/repository/repository.go diff --git a/backend/internal/model/publish_analytics/domain/usecase/usecase.go b/old/backend/internal/model/publish_analytics/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_analytics/domain/usecase/usecase.go rename to old/backend/internal/model/publish_analytics/domain/usecase/usecase.go diff --git a/backend/internal/model/publish_analytics/repository/mongo.go b/old/backend/internal/model/publish_analytics/repository/mongo.go similarity index 100% rename from backend/internal/model/publish_analytics/repository/mongo.go rename to old/backend/internal/model/publish_analytics/repository/mongo.go diff --git a/backend/internal/model/publish_analytics/usecase/schedule.go b/old/backend/internal/model/publish_analytics/usecase/schedule.go similarity index 100% rename from backend/internal/model/publish_analytics/usecase/schedule.go rename to old/backend/internal/model/publish_analytics/usecase/schedule.go diff --git a/backend/internal/model/publish_analytics/usecase/usecase.go b/old/backend/internal/model/publish_analytics/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_analytics/usecase/usecase.go rename to old/backend/internal/model/publish_analytics/usecase/usecase.go diff --git a/backend/internal/model/publish_guard/domain/entity/policy.go b/old/backend/internal/model/publish_guard/domain/entity/policy.go similarity index 100% rename from backend/internal/model/publish_guard/domain/entity/policy.go rename to old/backend/internal/model/publish_guard/domain/entity/policy.go diff --git a/backend/internal/model/publish_guard/domain/repository/repository.go b/old/backend/internal/model/publish_guard/domain/repository/repository.go similarity index 100% rename from backend/internal/model/publish_guard/domain/repository/repository.go rename to old/backend/internal/model/publish_guard/domain/repository/repository.go diff --git a/backend/internal/model/publish_guard/domain/usecase/usecase.go b/old/backend/internal/model/publish_guard/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_guard/domain/usecase/usecase.go rename to old/backend/internal/model/publish_guard/domain/usecase/usecase.go diff --git a/backend/internal/model/publish_guard/repository/mongo.go b/old/backend/internal/model/publish_guard/repository/mongo.go similarity index 100% rename from backend/internal/model/publish_guard/repository/mongo.go rename to old/backend/internal/model/publish_guard/repository/mongo.go diff --git a/backend/internal/model/publish_guard/usecase/usecase.go b/old/backend/internal/model/publish_guard/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_guard/usecase/usecase.go rename to old/backend/internal/model/publish_guard/usecase/usecase.go diff --git a/backend/internal/model/publish_guard/usecase/usecase_test.go b/old/backend/internal/model/publish_guard/usecase/usecase_test.go similarity index 100% rename from backend/internal/model/publish_guard/usecase/usecase_test.go rename to old/backend/internal/model/publish_guard/usecase/usecase_test.go diff --git a/backend/internal/model/publish_inventory/domain/entity/policy.go b/old/backend/internal/model/publish_inventory/domain/entity/policy.go similarity index 100% rename from backend/internal/model/publish_inventory/domain/entity/policy.go rename to old/backend/internal/model/publish_inventory/domain/entity/policy.go diff --git a/backend/internal/model/publish_inventory/domain/repository/repository.go b/old/backend/internal/model/publish_inventory/domain/repository/repository.go similarity index 100% rename from backend/internal/model/publish_inventory/domain/repository/repository.go rename to old/backend/internal/model/publish_inventory/domain/repository/repository.go diff --git a/backend/internal/model/publish_inventory/domain/usecase/usecase.go b/old/backend/internal/model/publish_inventory/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_inventory/domain/usecase/usecase.go rename to old/backend/internal/model/publish_inventory/domain/usecase/usecase.go diff --git a/backend/internal/model/publish_inventory/repository/mongo.go b/old/backend/internal/model/publish_inventory/repository/mongo.go similarity index 100% rename from backend/internal/model/publish_inventory/repository/mongo.go rename to old/backend/internal/model/publish_inventory/repository/mongo.go diff --git a/backend/internal/model/publish_inventory/usecase/usecase.go b/old/backend/internal/model/publish_inventory/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_inventory/usecase/usecase.go rename to old/backend/internal/model/publish_inventory/usecase/usecase.go diff --git a/backend/internal/model/publish_queue/domain/entity/queue_item.go b/old/backend/internal/model/publish_queue/domain/entity/queue_item.go similarity index 92% rename from backend/internal/model/publish_queue/domain/entity/queue_item.go rename to old/backend/internal/model/publish_queue/domain/entity/queue_item.go index 295d93c..7c80791 100644 --- a/backend/internal/model/publish_queue/domain/entity/queue_item.go +++ b/old/backend/internal/model/publish_queue/domain/entity/queue_item.go @@ -20,6 +20,8 @@ type QueueItem struct { CopyMissionID string `bson:"copy_mission_id,omitempty"` CopyDraftID string `bson:"copy_draft_id,omitempty"` Text string `bson:"text"` + ImageKey string `bson:"image_key,omitempty"` + ImageKeys []string `bson:"image_keys,omitempty"` TopicTag string `bson:"topic_tag,omitempty"` ScheduledAt int64 `bson:"scheduled_at"` Status string `bson:"status"` diff --git a/backend/internal/model/publish_queue/domain/repository/repository.go b/old/backend/internal/model/publish_queue/domain/repository/repository.go similarity index 100% rename from backend/internal/model/publish_queue/domain/repository/repository.go rename to old/backend/internal/model/publish_queue/domain/repository/repository.go diff --git a/backend/internal/model/publish_queue/domain/usecase/usecase.go b/old/backend/internal/model/publish_queue/domain/usecase/usecase.go similarity index 95% rename from backend/internal/model/publish_queue/domain/usecase/usecase.go rename to old/backend/internal/model/publish_queue/domain/usecase/usecase.go index 7487ada..1927644 100644 --- a/backend/internal/model/publish_queue/domain/usecase/usecase.go +++ b/old/backend/internal/model/publish_queue/domain/usecase/usecase.go @@ -9,6 +9,8 @@ type QueueItemSummary struct { CopyMissionID string CopyDraftID string Text string + ImageKey string + ImageKeys []string TopicTag string ScheduledAt int64 Status string @@ -33,6 +35,8 @@ type CreateRequest struct { CopyMissionID string CopyDraftID string Text string + ImageKey string + ImageKeys []string TopicTag string ScheduledAt int64 } @@ -43,6 +47,8 @@ type UpdateRequest struct { AccountID string QueueID string Text *string + ImageKey *string + ImageKeys *[]string TopicTag *string ScheduledAt *int64 } @@ -124,6 +130,8 @@ type RecordPublishedRequest struct { CopyMissionID string CopyDraftID string Text string + ImageKey string + ImageKeys []string TopicTag string MediaID string Permalink string diff --git a/backend/internal/model/publish_queue/repository/mongo.go b/old/backend/internal/model/publish_queue/repository/mongo.go similarity index 100% rename from backend/internal/model/publish_queue/repository/mongo.go rename to old/backend/internal/model/publish_queue/repository/mongo.go diff --git a/backend/internal/model/publish_queue/usecase/usecase.go b/old/backend/internal/model/publish_queue/usecase/usecase.go similarity index 87% rename from backend/internal/model/publish_queue/usecase/usecase.go rename to old/backend/internal/model/publish_queue/usecase/usecase.go index a09798a..387235e 100644 --- a/backend/internal/model/publish_queue/usecase/usecase.go +++ b/old/backend/internal/model/publish_queue/usecase/usecase.go @@ -2,6 +2,7 @@ package usecase import ( "context" + "fmt" "strings" "time" @@ -9,6 +10,7 @@ import ( app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libthreads "haixun-backend/internal/library/threadsapi" + "haixun-backend/internal/library/storage" "haixun-backend/internal/library/threadspost" copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" padomain "haixun-backend/internal/model/publish_analytics/domain/usecase" @@ -30,6 +32,7 @@ type publishQueueUseCase struct { guard guarddomain.UseCase events eventdomain.UseCase copyDraft copydraftdomain.UseCase + attachmentStore storage.PublishAttachmentStore } func NewUseCase( @@ -39,6 +42,7 @@ func NewUseCase( guard guarddomain.UseCase, events eventdomain.UseCase, copyDraft copydraftdomain.UseCase, + attachmentStore storage.PublishAttachmentStore, ) domusecase.UseCase { return &publishQueueUseCase{ repo: repo, @@ -47,6 +51,7 @@ func NewUseCase( guard: guard, events: events, copyDraft: copyDraft, + attachmentStore: attachmentStore, } } @@ -55,16 +60,22 @@ func (u *publishQueueUseCase) Create(ctx context.Context, req domusecase.CreateR ownerUID := strings.TrimSpace(req.OwnerUID) accountID := strings.TrimSpace(req.AccountID) text := strings.TrimSpace(req.Text) + imageKeys := storage.NormalizeImageKeys(req.ImageKey, req.ImageKeys) if tenantID == "" || ownerUID == "" { return nil, app.For(code.Auth).AuthUnauthorized("missing actor") } if accountID == "" { return nil, app.For(code.ThreadsAccount).InputMissingRequired("account_id is required") } - if text == "" { - return nil, app.For(code.ThreadsAccount).InputMissingRequired("text is required") + if text == "" && len(imageKeys) == 0 { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("text or image_keys is required") } - if err := threadspost.ValidatePublish(text); err != nil { + for _, imageKey := range imageKeys { + if err := storage.ValidatePublishAttachmentKey(imageKey); err != nil { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + } + if err := threadspost.ValidatePublishContent(text, len(imageKeys) > 0); err != nil { return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) } if _, err := u.threadsAccount.Get(ctx, tenantID, ownerUID, accountID); err != nil { @@ -86,6 +97,7 @@ func (u *publishQueueUseCase) Create(ctx context.Context, req domusecase.CreateR CopyMissionID: strings.TrimSpace(req.CopyMissionID), CopyDraftID: strings.TrimSpace(req.CopyDraftID), Text: text, + ImageKeys: imageKeys, TopicTag: normalizeTopicTag(req.TopicTag), ScheduledAt: scheduledAt, Status: entity.StatusScheduled, @@ -270,14 +282,33 @@ func (u *publishQueueUseCase) Update(ctx context.Context, req domusecase.UpdateR return nil, app.For(code.ThreadsAccount).ResInvalidState("僅排程中的項目可修改") } if req.Text != nil { - text := strings.TrimSpace(*req.Text) - if text == "" { - return nil, app.For(code.ThreadsAccount).InputMissingRequired("text is required") + item.Text = strings.TrimSpace(*req.Text) + } + if req.ImageKeys != nil || req.ImageKey != nil { + single := "" + list := queueItemImageKeys(*item) + if req.ImageKey != nil { + single = *req.ImageKey } - if err := threadspost.ValidatePublish(text); err != nil { - return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + if req.ImageKeys != nil { + list = *req.ImageKeys + single = "" } - item.Text = text + newKeys := storage.NormalizeImageKeys(single, list) + for _, imageKey := range newKeys { + if err := storage.ValidatePublishAttachmentKey(imageKey); err != nil { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) + } + } + u.purgeRemovedAttachments(ctx, queueItemImageKeys(*item), newKeys) + item.ImageKeys = newKeys + item.ImageKey = "" + } + if strings.TrimSpace(item.Text) == "" && len(queueItemImageKeys(*item)) == 0 { + return nil, app.For(code.ThreadsAccount).InputMissingRequired("text or image_keys is required") + } + if err := threadspost.ValidatePublishContent(item.Text, len(queueItemImageKeys(*item)) > 0); err != nil { + return nil, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error()) } if req.ScheduledAt != nil && *req.ScheduledAt > 0 { item.ScheduledAt = *req.ScheduledAt @@ -365,6 +396,7 @@ func (u *publishQueueUseCase) Delete(ctx context.Context, tenantID, ownerUID, ac _, err = u.Cancel(ctx, tenantID, ownerUID, accountID, queueID) return err default: + u.purgePublishAttachments(ctx, queueItemImageKeys(*item)) return u.repo.Delete(ctx, tenantID, ownerUID, accountID, queueID) } } @@ -430,26 +462,35 @@ func (u *publishQueueUseCase) dispatchOne(ctx context.Context, item *entity.Queu return u.markFailed(ctx, claimed, "Threads API 未連線") } + imageURLs, err := u.resolveImageURLs(queueItemImageKeys(*claimed)) + if err != nil { + return u.markFailed(ctx, claimed, err.Error()) + } result, err := libthreads.PublishText(ctx, libthreads.PublishTextInput{ ThreadsUserID: userID, AccessToken: token, Text: claimed.Text, TopicTag: claimed.TopicTag, + ImageURLs: imageURLs, }) if err != nil { return u.markFailed(ctx, claimed, "發文失敗:"+err.Error()) } now := clock.NowUnixNano() + attachmentKeys := queueItemImageKeys(*claimed) claimed.Status = entity.StatusPublished claimed.MediaID = result.MediaID claimed.Permalink = result.Permalink claimed.PublishedAt = now claimed.UpdateAt = now claimed.ErrorMessage = "" + claimed.ImageKey = "" + claimed.ImageKeys = nil if _, err := u.repo.UpdateIfStatus(ctx, claimed, []string{entity.StatusPublishing}); err != nil { return err } + u.purgePublishAttachments(ctx, attachmentKeys) u.recordEvent(ctx, claimed, evententity.EventPublished, entity.StatusPublishing, entity.StatusPublished, "Threads API 發布完成") if claimed.CopyDraftID != "" && u.copyDraft != nil { _, _ = u.copyDraft.MarkPublished(ctx, copydraftdomain.MarkPublishedRequest{ @@ -738,6 +779,14 @@ func firstNonEmpty(values ...string) string { return "" } +func firstImageKey(item entity.QueueItem) string { + keys := queueItemImageKeys(item) + if len(keys) == 0 { + return "" + } + return keys[0] +} + func requireActor(tenantID, ownerUID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Auth).AuthUnauthorized("missing actor") @@ -745,6 +794,54 @@ func requireActor(tenantID, ownerUID string) error { return nil } +func queueItemImageKeys(item entity.QueueItem) []string { + if len(item.ImageKeys) > 0 { + return item.ImageKeys + } + if key := strings.TrimSpace(item.ImageKey); key != "" { + return []string{key} + } + return nil +} + +func (u *publishQueueUseCase) resolveImageURLs(imageKeys []string) ([]string, error) { + imageKeys = storage.NormalizeImageKeys("", imageKeys) + if len(imageKeys) == 0 { + return nil, nil + } + if u.attachmentStore == nil { + return nil, fmt.Errorf("圖片儲存未設定,無法發布附圖貼文") + } + urls := make([]string, 0, len(imageKeys)) + for _, imageKey := range imageKeys { + url, err := u.attachmentStore.ResolvePublicURL(imageKey) + if err != nil { + return nil, fmt.Errorf("圖片網址無法解析:%w", err) + } + urls = append(urls, url) + } + return urls, nil +} + +func (u *publishQueueUseCase) purgePublishAttachments(ctx context.Context, imageKeys []string) { + storage.PurgePublishAttachments(ctx, u.attachmentStore, imageKeys) +} + +func (u *publishQueueUseCase) purgeRemovedAttachments(ctx context.Context, oldKeys, newKeys []string) { + oldSet := make(map[string]struct{}, len(oldKeys)) + for _, key := range oldKeys { + oldSet[key] = struct{}{} + } + for _, key := range newKeys { + delete(oldSet, key) + } + removed := make([]string, 0, len(oldSet)) + for key := range oldSet { + removed = append(removed, key) + } + u.purgePublishAttachments(ctx, removed) +} + func toSummary(item entity.QueueItem) domusecase.QueueItemSummary { return domusecase.QueueItemSummary{ ID: item.ID, @@ -753,6 +850,8 @@ func toSummary(item entity.QueueItem) domusecase.QueueItemSummary { CopyMissionID: item.CopyMissionID, CopyDraftID: item.CopyDraftID, Text: item.Text, + ImageKey: firstImageKey(item), + ImageKeys: queueItemImageKeys(item), TopicTag: item.TopicTag, ScheduledAt: item.ScheduledAt, Status: item.Status, diff --git a/backend/internal/model/publish_queue_event/domain/entity/event.go b/old/backend/internal/model/publish_queue_event/domain/entity/event.go similarity index 100% rename from backend/internal/model/publish_queue_event/domain/entity/event.go rename to old/backend/internal/model/publish_queue_event/domain/entity/event.go diff --git a/backend/internal/model/publish_queue_event/domain/repository/repository.go b/old/backend/internal/model/publish_queue_event/domain/repository/repository.go similarity index 100% rename from backend/internal/model/publish_queue_event/domain/repository/repository.go rename to old/backend/internal/model/publish_queue_event/domain/repository/repository.go diff --git a/backend/internal/model/publish_queue_event/domain/usecase/usecase.go b/old/backend/internal/model/publish_queue_event/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_queue_event/domain/usecase/usecase.go rename to old/backend/internal/model/publish_queue_event/domain/usecase/usecase.go diff --git a/backend/internal/model/publish_queue_event/repository/mongo.go b/old/backend/internal/model/publish_queue_event/repository/mongo.go similarity index 100% rename from backend/internal/model/publish_queue_event/repository/mongo.go rename to old/backend/internal/model/publish_queue_event/repository/mongo.go diff --git a/backend/internal/model/publish_queue_event/usecase/usecase.go b/old/backend/internal/model/publish_queue_event/usecase/usecase.go similarity index 100% rename from backend/internal/model/publish_queue_event/usecase/usecase.go rename to old/backend/internal/model/publish_queue_event/usecase/usecase.go diff --git a/backend/internal/model/scan_post/domain/entity/outreach_patch.go b/old/backend/internal/model/scan_post/domain/entity/outreach_patch.go similarity index 100% rename from backend/internal/model/scan_post/domain/entity/outreach_patch.go rename to old/backend/internal/model/scan_post/domain/entity/outreach_patch.go diff --git a/backend/internal/model/scan_post/domain/entity/post.go b/old/backend/internal/model/scan_post/domain/entity/post.go similarity index 100% rename from backend/internal/model/scan_post/domain/entity/post.go rename to old/backend/internal/model/scan_post/domain/entity/post.go diff --git a/backend/internal/model/scan_post/domain/repository/repository.go b/old/backend/internal/model/scan_post/domain/repository/repository.go similarity index 100% rename from backend/internal/model/scan_post/domain/repository/repository.go rename to old/backend/internal/model/scan_post/domain/repository/repository.go diff --git a/backend/internal/model/scan_post/domain/usecase/usecase.go b/old/backend/internal/model/scan_post/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/scan_post/domain/usecase/usecase.go rename to old/backend/internal/model/scan_post/domain/usecase/usecase.go diff --git a/backend/internal/model/scan_post/repository/mongo.go b/old/backend/internal/model/scan_post/repository/mongo.go similarity index 100% rename from backend/internal/model/scan_post/repository/mongo.go rename to old/backend/internal/model/scan_post/repository/mongo.go diff --git a/backend/internal/model/scan_post/repository/mongo_priority_filter_test.go b/old/backend/internal/model/scan_post/repository/mongo_priority_filter_test.go similarity index 100% rename from backend/internal/model/scan_post/repository/mongo_priority_filter_test.go rename to old/backend/internal/model/scan_post/repository/mongo_priority_filter_test.go diff --git a/backend/internal/model/scan_post/repository/mongo_recent7d_filter_test.go b/old/backend/internal/model/scan_post/repository/mongo_recent7d_filter_test.go similarity index 100% rename from backend/internal/model/scan_post/repository/mongo_recent7d_filter_test.go rename to old/backend/internal/model/scan_post/repository/mongo_recent7d_filter_test.go diff --git a/backend/internal/model/scan_post/repository/mongo_topic_scope_test.go b/old/backend/internal/model/scan_post/repository/mongo_topic_scope_test.go similarity index 100% rename from backend/internal/model/scan_post/repository/mongo_topic_scope_test.go rename to old/backend/internal/model/scan_post/repository/mongo_topic_scope_test.go diff --git a/backend/internal/model/scan_post/usecase/errors.go b/old/backend/internal/model/scan_post/usecase/errors.go similarity index 100% rename from backend/internal/model/scan_post/usecase/errors.go rename to old/backend/internal/model/scan_post/usecase/errors.go diff --git a/backend/internal/model/scan_post/usecase/usecase.go b/old/backend/internal/model/scan_post/usecase/usecase.go similarity index 100% rename from backend/internal/model/scan_post/usecase/usecase.go rename to old/backend/internal/model/scan_post/usecase/usecase.go diff --git a/backend/internal/model/setting/domain/entity/setting.go b/old/backend/internal/model/setting/domain/entity/setting.go similarity index 100% rename from backend/internal/model/setting/domain/entity/setting.go rename to old/backend/internal/model/setting/domain/entity/setting.go diff --git a/backend/internal/model/setting/domain/repository/setting.go b/old/backend/internal/model/setting/domain/repository/setting.go similarity index 100% rename from backend/internal/model/setting/domain/repository/setting.go rename to old/backend/internal/model/setting/domain/repository/setting.go diff --git a/backend/internal/model/setting/domain/usecase/setting.go b/old/backend/internal/model/setting/domain/usecase/setting.go similarity index 100% rename from backend/internal/model/setting/domain/usecase/setting.go rename to old/backend/internal/model/setting/domain/usecase/setting.go diff --git a/backend/internal/model/setting/repository/mongo.go b/old/backend/internal/model/setting/repository/mongo.go similarity index 100% rename from backend/internal/model/setting/repository/mongo.go rename to old/backend/internal/model/setting/repository/mongo.go diff --git a/backend/internal/model/setting/usecase/usecase.go b/old/backend/internal/model/setting/usecase/usecase.go similarity index 100% rename from backend/internal/model/setting/usecase/usecase.go rename to old/backend/internal/model/setting/usecase/usecase.go diff --git a/backend/internal/model/style_preset/domain/entity/preset.go b/old/backend/internal/model/style_preset/domain/entity/preset.go similarity index 100% rename from backend/internal/model/style_preset/domain/entity/preset.go rename to old/backend/internal/model/style_preset/domain/entity/preset.go diff --git a/backend/internal/model/style_preset/domain/repository/repository.go b/old/backend/internal/model/style_preset/domain/repository/repository.go similarity index 100% rename from backend/internal/model/style_preset/domain/repository/repository.go rename to old/backend/internal/model/style_preset/domain/repository/repository.go diff --git a/backend/internal/model/style_preset/domain/usecase/usecase.go b/old/backend/internal/model/style_preset/domain/usecase/usecase.go similarity index 100% rename from backend/internal/model/style_preset/domain/usecase/usecase.go rename to old/backend/internal/model/style_preset/domain/usecase/usecase.go diff --git a/backend/internal/model/style_preset/repository/mongo.go b/old/backend/internal/model/style_preset/repository/mongo.go similarity index 100% rename from backend/internal/model/style_preset/repository/mongo.go rename to old/backend/internal/model/style_preset/repository/mongo.go diff --git a/backend/internal/model/style_preset/usecase/usecase.go b/old/backend/internal/model/style_preset/usecase/usecase.go similarity index 100% rename from backend/internal/model/style_preset/usecase/usecase.go rename to old/backend/internal/model/style_preset/usecase/usecase.go diff --git a/backend/internal/model/threads_account/domain/entity/account.go b/old/backend/internal/model/threads_account/domain/entity/account.go similarity index 100% rename from backend/internal/model/threads_account/domain/entity/account.go rename to old/backend/internal/model/threads_account/domain/entity/account.go diff --git a/backend/internal/model/threads_account/domain/entity/secrets.go b/old/backend/internal/model/threads_account/domain/entity/secrets.go similarity index 100% rename from backend/internal/model/threads_account/domain/entity/secrets.go rename to old/backend/internal/model/threads_account/domain/entity/secrets.go diff --git a/backend/internal/model/threads_account/domain/repository/repository.go b/old/backend/internal/model/threads_account/domain/repository/repository.go similarity index 100% rename from backend/internal/model/threads_account/domain/repository/repository.go rename to old/backend/internal/model/threads_account/domain/repository/repository.go diff --git a/backend/internal/model/threads_account/domain/usecase/usecase.go b/old/backend/internal/model/threads_account/domain/usecase/usecase.go similarity index 99% rename from backend/internal/model/threads_account/domain/usecase/usecase.go rename to old/backend/internal/model/threads_account/domain/usecase/usecase.go index b5d814f..4b13a39 100644 --- a/backend/internal/model/threads_account/domain/usecase/usecase.go +++ b/old/backend/internal/model/threads_account/domain/usecase/usecase.go @@ -186,6 +186,8 @@ type APIPlaygroundRequest struct { ReplyID string ReplyToID string Text string + ImageURL string + ImageURLs []string Permalink string HintText string SkipPrime bool diff --git a/backend/internal/model/threads_account/repository/mongo.go b/old/backend/internal/model/threads_account/repository/mongo.go similarity index 100% rename from backend/internal/model/threads_account/repository/mongo.go rename to old/backend/internal/model/threads_account/repository/mongo.go diff --git a/backend/internal/model/threads_account/repository/oauth_state_redis.go b/old/backend/internal/model/threads_account/repository/oauth_state_redis.go similarity index 100% rename from backend/internal/model/threads_account/repository/oauth_state_redis.go rename to old/backend/internal/model/threads_account/repository/oauth_state_redis.go diff --git a/backend/internal/model/threads_account/repository/secrets_mongo.go b/old/backend/internal/model/threads_account/repository/secrets_mongo.go similarity index 100% rename from backend/internal/model/threads_account/repository/secrets_mongo.go rename to old/backend/internal/model/threads_account/repository/secrets_mongo.go diff --git a/backend/internal/model/threads_account/usecase/ai_credentials.go b/old/backend/internal/model/threads_account/usecase/ai_credentials.go similarity index 100% rename from backend/internal/model/threads_account/usecase/ai_credentials.go rename to old/backend/internal/model/threads_account/usecase/ai_credentials.go diff --git a/backend/internal/model/threads_account/usecase/api_playground.go b/old/backend/internal/model/threads_account/usecase/api_playground.go similarity index 93% rename from backend/internal/model/threads_account/usecase/api_playground.go rename to old/backend/internal/model/threads_account/usecase/api_playground.go index c1f0883..3175448 100644 --- a/backend/internal/model/threads_account/usecase/api_playground.go +++ b/old/backend/internal/model/threads_account/usecase/api_playground.go @@ -138,11 +138,12 @@ func (u *threadsAccountUseCase) RunAPIPlayground( } case "publish_text": text := strings.TrimSpace(req.Text) - if userID == "" || text == "" { - runErr = fmt.Errorf("threads_user_id 與 text 為必填") + imageURLs := req.ImageURLs + if userID == "" || (text == "" && len(imageURLs) == 0) { + runErr = fmt.Errorf("threads_user_id 與 text 或 image 為必填") } else { payload, runErr = libthreads.PublishText(ctx, libthreads.PublishTextInput{ - ThreadsUserID: userID, AccessToken: token, Text: text, + ThreadsUserID: userID, AccessToken: token, Text: text, ImageURLs: imageURLs, }) } case "prime_reply_target": @@ -167,9 +168,10 @@ func (u *threadsAccountUseCase) RunAPIPlayground( } case "publish_reply": text := strings.TrimSpace(req.Text) + imageURLs := req.ImageURLs replyTo := strings.TrimSpace(req.ReplyToID) - if userID == "" || text == "" || replyTo == "" { - runErr = fmt.Errorf("threads_user_id、reply_to_id、text 為必填") + if userID == "" || replyTo == "" || (text == "" && len(imageURLs) == 0) { + runErr = fmt.Errorf("threads_user_id、reply_to_id、text 或 image 為必填") } else { permalink := strings.TrimSpace(req.Permalink) if permalink != "" && !req.SkipPrime { @@ -187,7 +189,7 @@ func (u *threadsAccountUseCase) RunAPIPlayground( } if runErr == nil { payload, runErr = libthreads.PublishReply(ctx, libthreads.PublishReplyInput{ - ThreadsUserID: userID, AccessToken: token, ReplyToID: replyTo, Text: text, + ThreadsUserID: userID, AccessToken: token, ReplyToID: replyTo, Text: text, ImageURLs: imageURLs, }) } } diff --git a/backend/internal/model/threads_account/usecase/api_smoke.go b/old/backend/internal/model/threads_account/usecase/api_smoke.go similarity index 100% rename from backend/internal/model/threads_account/usecase/api_smoke.go rename to old/backend/internal/model/threads_account/usecase/api_smoke.go diff --git a/backend/internal/model/threads_account/usecase/connection_prefs_test.go b/old/backend/internal/model/threads_account/usecase/connection_prefs_test.go similarity index 100% rename from backend/internal/model/threads_account/usecase/connection_prefs_test.go rename to old/backend/internal/model/threads_account/usecase/connection_prefs_test.go diff --git a/backend/internal/model/threads_account/usecase/known_accounts.go b/old/backend/internal/model/threads_account/usecase/known_accounts.go similarity index 100% rename from backend/internal/model/threads_account/usecase/known_accounts.go rename to old/backend/internal/model/threads_account/usecase/known_accounts.go diff --git a/backend/internal/model/threads_account/usecase/oauth.go b/old/backend/internal/model/threads_account/usecase/oauth.go similarity index 100% rename from backend/internal/model/threads_account/usecase/oauth.go rename to old/backend/internal/model/threads_account/usecase/oauth.go diff --git a/backend/internal/model/threads_account/usecase/oauth_debug_log.go b/old/backend/internal/model/threads_account/usecase/oauth_debug_log.go similarity index 100% rename from backend/internal/model/threads_account/usecase/oauth_debug_log.go rename to old/backend/internal/model/threads_account/usecase/oauth_debug_log.go diff --git a/backend/internal/model/threads_account/usecase/placement_context.go b/old/backend/internal/model/threads_account/usecase/placement_context.go similarity index 100% rename from backend/internal/model/threads_account/usecase/placement_context.go rename to old/backend/internal/model/threads_account/usecase/placement_context.go diff --git a/backend/internal/model/threads_account/usecase/placement_context_test.go b/old/backend/internal/model/threads_account/usecase/placement_context_test.go similarity index 100% rename from backend/internal/model/threads_account/usecase/placement_context_test.go rename to old/backend/internal/model/threads_account/usecase/placement_context_test.go diff --git a/backend/internal/model/threads_account/usecase/post_performance.go b/old/backend/internal/model/threads_account/usecase/post_performance.go similarity index 100% rename from backend/internal/model/threads_account/usecase/post_performance.go rename to old/backend/internal/model/threads_account/usecase/post_performance.go diff --git a/backend/internal/model/threads_account/usecase/publish_credentials.go b/old/backend/internal/model/threads_account/usecase/publish_credentials.go similarity index 100% rename from backend/internal/model/threads_account/usecase/publish_credentials.go rename to old/backend/internal/model/threads_account/usecase/publish_credentials.go diff --git a/backend/internal/model/threads_account/usecase/token_refresh.go b/old/backend/internal/model/threads_account/usecase/token_refresh.go similarity index 100% rename from backend/internal/model/threads_account/usecase/token_refresh.go rename to old/backend/internal/model/threads_account/usecase/token_refresh.go diff --git a/backend/internal/model/threads_account/usecase/usecase.go b/old/backend/internal/model/threads_account/usecase/usecase.go similarity index 100% rename from backend/internal/model/threads_account/usecase/usecase.go rename to old/backend/internal/model/threads_account/usecase/usecase.go diff --git a/backend/internal/response/request.go b/old/backend/internal/response/request.go similarity index 100% rename from backend/internal/response/request.go rename to old/backend/internal/response/request.go diff --git a/backend/internal/response/response.go b/old/backend/internal/response/response.go similarity index 100% rename from backend/internal/response/response.go rename to old/backend/internal/response/response.go diff --git a/backend/internal/svc/job_workers.go b/old/backend/internal/svc/job_workers.go similarity index 100% rename from backend/internal/svc/job_workers.go rename to old/backend/internal/svc/job_workers.go diff --git a/backend/internal/svc/service_context.go b/old/backend/internal/svc/service_context.go similarity index 97% rename from backend/internal/svc/service_context.go rename to old/backend/internal/svc/service_context.go index c9234f2..d7de766 100644 --- a/backend/internal/svc/service_context.go +++ b/old/backend/internal/svc/service_context.go @@ -39,9 +39,6 @@ import ( 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" - crmcontactdomain "haixun-backend/internal/model/crm_contact/domain/usecase" - crmcontactrepo "haixun-backend/internal/model/crm_contact/repository" - crmcontactusecase "haixun-backend/internal/model/crm_contact/usecase" jobrepo "haixun-backend/internal/model/job/repository" jobusecase "haixun-backend/internal/model/job/usecase" kgdomain "haixun-backend/internal/model/knowledge_graph/domain/usecase" @@ -125,7 +122,6 @@ type ServiceContext struct { ContentOps contentopsdomain.UseCase CopyDraft copydraftdomain.UseCase ThreadsAccount threadsaccountdomain.UseCase - CrmContact crmcontactdomain.UseCase PublishAnalytics publishanalyticsdomain.UseCase PublishQueue publishqueuedomain.UseCase PublishInventory publishinventorydomain.UseCase @@ -396,18 +392,16 @@ func NewServiceContext(c config.Config) *ServiceContext { placementUseCase := placementusecase.NewUseCase(settingUseCase, secretsCipher) - crmContactRepository := crmcontactrepo.NewMongoRepository(mongoClient.Database()) - if err := crmContactRepository.EnsureIndexes(ctx); err != nil { - panic(err) - } - crmContactUseCase := crmcontactusecase.NewUseCase(crmContactRepository) - publishAnalyticsUseCase := publishanalyticsusecase.NewUseCase(publishAnalyticsRepository, jobUseCase) publishQueueEventUseCase := publishqueueeventusecase.NewUseCase(publishQueueEventRepository) publishInventoryUseCase := publishinventoryusecase.NewUseCase(publishInventoryRepository, threadsAccountUseCase) publishGuardUseCase := publishguardusecase.NewUseCase(publishGuardRepository, threadsAccountUseCase) stylePresetUseCase := stylepresetusecase.NewUseCase(stylePresetRepository, personaUseCase) + var publishAttachmentStore storage.PublishAttachmentStore + if avatarStorage != nil { + publishAttachmentStore = avatarStorage + } publishQueueUseCase := publishqueueusecase.NewUseCase( publishQueueRepository, threadsAccountUseCase, @@ -415,6 +409,7 @@ func NewServiceContext(c config.Config) *ServiceContext { publishGuardUseCase, publishQueueEventUseCase, copyDraftUseCase, + publishAttachmentStore, ) sc := &ServiceContext{ @@ -440,7 +435,6 @@ func NewServiceContext(c config.Config) *ServiceContext { ContentOps: contentOpsUseCase, CopyDraft: copyDraftUseCase, ThreadsAccount: threadsAccountUseCase, - CrmContact: crmContactUseCase, PublishAnalytics: publishAnalyticsUseCase, PublishQueue: publishQueueUseCase, PublishInventory: publishInventoryUseCase, @@ -613,10 +607,11 @@ func NewServiceContext(c config.Config) *ServiceContext { // 兩者組合後掛在受保護的 route group 上(routes.go 以 AuthJWT / MemberAuth 引用)。 authJWT := middleware.NewAuthJWTMiddleware(sc.AuthToken, sc.Config.Auth).Handle memberAuth := middleware.NewMemberAuthMiddleware(sc.AuthToken, sc.Config.Auth).Handle + emailVerified := middleware.NewEmailVerifiedMiddleware(sc.Member).Handle rbac := middleware.NewPermissionRBACMiddleware(sc.Member, sc.Permission).Handle - sc.AuthJWT = func(next http.HandlerFunc) http.HandlerFunc { return authJWT(rbac(next)) } - sc.MemberAuth = func(next http.HandlerFunc) http.HandlerFunc { return memberAuth(rbac(next)) } + sc.AuthJWT = func(next http.HandlerFunc) http.HandlerFunc { return authJWT(emailVerified(rbac(next))) } + sc.MemberAuth = func(next http.HandlerFunc) http.HandlerFunc { return memberAuth(emailVerified(rbac(next))) } sc.WorkerSecret = middleware.NewWorkerSecretMiddleware(sc.Config.InternalWorker).Handle return sc diff --git a/backend/internal/types/types.go b/old/backend/internal/types/types.go similarity index 96% rename from backend/internal/types/types.go rename to old/backend/internal/types/types.go index 2234964..395006a 100644 --- a/backend/internal/types/types.go +++ b/old/backend/internal/types/types.go @@ -117,6 +117,10 @@ type AuthRegisterReq struct { Language string `json:"language,optional"` } +type AuthResendVerificationData struct { + Sent bool `json:"sent"` +} + type AuthTokenData struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` @@ -125,6 +129,14 @@ type AuthTokenData struct { TokenType string `json:"token_type"` } +type AuthVerifyEmailData struct { + EmailVerified bool `json:"email_verified"` +} + +type AuthVerifyEmailReq struct { + Code string `json:"code" validate:"required,len=6"` +} + type BatchDeletePlacementTopicScanPostsData struct { DeletedCount int `json:"deleted_count"` } @@ -634,9 +646,11 @@ type CreatePublishQueueHandlerReq struct { } type CreatePublishQueueReq struct { - Text string `json:"text" validate:"required"` - TopicTag string `json:"topic_tag,optional"` - ScheduledAt int64 `json:"scheduled_at,optional"` + Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` + TopicTag string `json:"topic_tag,optional"` + ScheduledAt int64 `json:"scheduled_at,optional"` } type CreateThreadsAccountReq struct { @@ -1546,9 +1560,11 @@ type PatchPublishQueueHandlerReq struct { } type PatchPublishQueueReq struct { - Text *string `json:"text,optional"` - TopicTag *string `json:"topic_tag,optional"` - ScheduledAt *int64 `json:"scheduled_at,optional"` + Text *string `json:"text,optional"` + ImageKey *string `json:"image_key,optional"` + ImageKeys *[]string `json:"image_keys,optional"` + TopicTag *string `json:"topic_tag,optional"` + ScheduledAt *int64 `json:"scheduled_at,optional"` } type PatchScanPostOutreachHandlerReq struct { @@ -1727,6 +1743,17 @@ type PublishMentionReplyData struct { Permalink string `json:"permalink,omitempty"` } +type PublishThreadsReplyData struct { + MediaID string `json:"media_id,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +type PublishThreadsReplyHandlerReq struct { + ID string `path:"id" validate:"required"` + ReplyToID string `form:"reply_to_id" validate:"required"` + Text string `form:"text,optional"` +} + type PublishMentionReplyHandlerReq struct { PublishMentionReplyPath PublishMentionReplyReq @@ -1738,7 +1765,9 @@ type PublishMentionReplyPath struct { } type PublishMentionReplyReq struct { - Text string `json:"text"` + Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` } type PublishOutreachDraftData struct { @@ -1781,26 +1810,28 @@ type PublishQueueEventsData struct { } type PublishQueueItemData struct { - ID string `json:"id"` - AccountID string `json:"account_id"` - PersonaID string `json:"persona_id,omitempty"` - CopyMissionID string `json:"copy_mission_id,omitempty"` - CopyDraftID string `json:"copy_draft_id,omitempty"` - Text string `json:"text"` - TopicTag string `json:"topic_tag,omitempty"` - ScheduledAt int64 `json:"scheduled_at"` - Status string `json:"status"` - MediaID string `json:"media_id,optional"` - Permalink string `json:"permalink,optional"` - PublishedAt int64 `json:"published_at,optional"` - ErrorMessage string `json:"error_message,optional"` - RetryCount int `json:"retry_count,omitempty"` - LastAttemptAt int64 `json:"last_attempt_at,omitempty"` - NextAttemptAt int64 `json:"next_attempt_at,omitempty"` - MissedAt int64 `json:"missed_at,omitempty"` - PausedReason string `json:"paused_reason,omitempty"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` + ID string `json:"id"` + AccountID string `json:"account_id"` + PersonaID string `json:"persona_id,omitempty"` + CopyMissionID string `json:"copy_mission_id,omitempty"` + CopyDraftID string `json:"copy_draft_id,omitempty"` + Text string `json:"text"` + ImageKey string `json:"image_key,omitempty"` + ImageKeys []string `json:"image_keys,omitempty"` + TopicTag string `json:"topic_tag,omitempty"` + ScheduledAt int64 `json:"scheduled_at"` + Status string `json:"status"` + MediaID string `json:"media_id,optional"` + Permalink string `json:"permalink,optional"` + PublishedAt int64 `json:"published_at,optional"` + ErrorMessage string `json:"error_message,optional"` + RetryCount int `json:"retry_count,omitempty"` + LastAttemptAt int64 `json:"last_attempt_at,omitempty"` + NextAttemptAt int64 `json:"next_attempt_at,omitempty"` + MissedAt int64 `json:"missed_at,omitempty"` + PausedReason string `json:"paused_reason,omitempty"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` } type PublishQueueItemPath struct { @@ -1880,20 +1911,22 @@ type RunThreadsAPIPlaygroundHandlerReq struct { } type RunThreadsAPIPlaygroundReq struct { - Action string `json:"action" validate:"required"` - Query string `json:"query,optional"` - Username string `json:"username,optional"` - MediaID string `json:"media_id,optional"` - ReplyID string `json:"reply_id,optional"` - ReplyToID string `json:"reply_to_id,optional"` - Text string `json:"text,optional"` - Permalink string `json:"permalink,optional"` - HintText string `json:"hint_text,optional"` - SkipPrime bool `json:"skip_prime,optional"` - LocationQuery string `json:"location_query,optional"` - Limit int `json:"limit,optional"` - Hide bool `json:"hide,optional"` - SearchType string `json:"search_type,optional"` + Action string `json:"action" validate:"required"` + Query string `json:"query,optional"` + Username string `json:"username,optional"` + MediaID string `json:"media_id,optional"` + ReplyID string `json:"reply_id,optional"` + ReplyToID string `json:"reply_to_id,optional"` + Text string `json:"text,optional"` + ImageKey string `json:"image_key,optional"` + ImageKeys []string `json:"image_keys,optional"` + Permalink string `json:"permalink,optional"` + HintText string `json:"hint_text,optional"` + SkipPrime bool `json:"skip_prime,optional"` + LocationQuery string `json:"location_query,optional"` + Limit int `json:"limit,optional"` + Hide bool `json:"hide,optional"` + SearchType string `json:"search_type,optional"` } type RunThreadsAPISmokeTestData struct { @@ -2763,6 +2796,11 @@ type UploadAvatarData struct { Avatar string `json:"avatar"` } +type UploadPublishAttachmentData struct { + AttachmentKey string `json:"attachment_key"` + PublicURL string `json:"public_url"` +} + type UpsertBrandScanScheduleHandlerReq struct { BrandPath UpsertBrandScanScheduleReq diff --git a/backend/internal/worker/job/crawler_search.go b/old/backend/internal/worker/job/crawler_search.go similarity index 100% rename from backend/internal/worker/job/crawler_search.go rename to old/backend/internal/worker/job/crawler_search.go diff --git a/backend/internal/worker/job/expand_graph.go b/old/backend/internal/worker/job/expand_graph.go similarity index 100% rename from backend/internal/worker/job/expand_graph.go rename to old/backend/internal/worker/job/expand_graph.go diff --git a/backend/internal/worker/job/generate_formula_draft.go b/old/backend/internal/worker/job/generate_formula_draft.go similarity index 100% rename from backend/internal/worker/job/generate_formula_draft.go rename to old/backend/internal/worker/job/generate_formula_draft.go diff --git a/backend/internal/worker/job/generate_outreach_draft.go b/old/backend/internal/worker/job/generate_outreach_draft.go similarity index 100% rename from backend/internal/worker/job/generate_outreach_draft.go rename to old/backend/internal/worker/job/generate_outreach_draft.go diff --git a/backend/internal/worker/job/generate_rewrite_draft.go b/old/backend/internal/worker/job/generate_rewrite_draft.go similarity index 100% rename from backend/internal/worker/job/generate_rewrite_draft.go rename to old/backend/internal/worker/job/generate_rewrite_draft.go diff --git a/backend/internal/worker/job/generate_topic_matrix.go b/old/backend/internal/worker/job/generate_topic_matrix.go similarity index 100% rename from backend/internal/worker/job/generate_topic_matrix.go rename to old/backend/internal/worker/job/generate_topic_matrix.go diff --git a/backend/internal/worker/job/payload_actor.go b/old/backend/internal/worker/job/payload_actor.go similarity index 100% rename from backend/internal/worker/job/payload_actor.go rename to old/backend/internal/worker/job/payload_actor.go diff --git a/backend/internal/worker/job/payload_helpers.go b/old/backend/internal/worker/job/payload_helpers.go similarity index 100% rename from backend/internal/worker/job/payload_helpers.go rename to old/backend/internal/worker/job/payload_helpers.go diff --git a/backend/internal/worker/job/placement_scope.go b/old/backend/internal/worker/job/placement_scope.go similarity index 100% rename from backend/internal/worker/job/placement_scope.go rename to old/backend/internal/worker/job/placement_scope.go diff --git a/backend/internal/worker/job/publish_analytics.go b/old/backend/internal/worker/job/publish_analytics.go similarity index 100% rename from backend/internal/worker/job/publish_analytics.go rename to old/backend/internal/worker/job/publish_analytics.go diff --git a/backend/internal/worker/job/publish_queue_sweeper.go b/old/backend/internal/worker/job/publish_queue_sweeper.go similarity index 100% rename from backend/internal/worker/job/publish_queue_sweeper.go rename to old/backend/internal/worker/job/publish_queue_sweeper.go diff --git a/backend/internal/worker/job/reaper.go b/old/backend/internal/worker/job/reaper.go similarity index 100% rename from backend/internal/worker/job/reaper.go rename to old/backend/internal/worker/job/reaper.go diff --git a/backend/internal/worker/job/refresh_threads_token.go b/old/backend/internal/worker/job/refresh_threads_token.go similarity index 100% rename from backend/internal/worker/job/refresh_threads_token.go rename to old/backend/internal/worker/job/refresh_threads_token.go diff --git a/backend/internal/worker/job/runner.go b/old/backend/internal/worker/job/runner.go similarity index 100% rename from backend/internal/worker/job/runner.go rename to old/backend/internal/worker/job/runner.go diff --git a/backend/internal/worker/job/runner_test.go b/old/backend/internal/worker/job/runner_test.go similarity index 100% rename from backend/internal/worker/job/runner_test.go rename to old/backend/internal/worker/job/runner_test.go diff --git a/backend/internal/worker/job/scan_placement.go b/old/backend/internal/worker/job/scan_placement.go similarity index 100% rename from backend/internal/worker/job/scan_placement.go rename to old/backend/internal/worker/job/scan_placement.go diff --git a/backend/internal/worker/job/scan_viral.go b/old/backend/internal/worker/job/scan_viral.go similarity index 100% rename from backend/internal/worker/job/scan_viral.go rename to old/backend/internal/worker/job/scan_viral.go diff --git a/backend/internal/worker/job/scheduler.go b/old/backend/internal/worker/job/scheduler.go similarity index 100% rename from backend/internal/worker/job/scheduler.go rename to old/backend/internal/worker/job/scheduler.go diff --git a/backend/internal/worker/job/threads_token_sweeper.go b/old/backend/internal/worker/job/threads_token_sweeper.go similarity index 100% rename from backend/internal/worker/job/threads_token_sweeper.go rename to old/backend/internal/worker/job/threads_token_sweeper.go diff --git a/backend/web/Dockerfile b/old/backend/web/Dockerfile similarity index 100% rename from backend/web/Dockerfile rename to old/backend/web/Dockerfile diff --git a/backend/web/index.html b/old/backend/web/index.html similarity index 100% rename from backend/web/index.html rename to old/backend/web/index.html diff --git a/backend/web/package-lock.json b/old/backend/web/package-lock.json similarity index 100% rename from backend/web/package-lock.json rename to old/backend/web/package-lock.json diff --git a/backend/web/package.json b/old/backend/web/package.json similarity index 100% rename from backend/web/package.json rename to old/backend/web/package.json diff --git a/backend/web/src/App.tsx b/old/backend/web/src/App.tsx similarity index 96% rename from backend/web/src/App.tsx rename to old/backend/web/src/App.tsx index 740e807..cdff08a 100644 --- a/backend/web/src/App.tsx +++ b/old/backend/web/src/App.tsx @@ -4,6 +4,8 @@ import { useAuth } from "./auth/AuthContext"; import { Layout } from "./components/Layout"; import { Button, Card, PageTitle } from "./components/ui"; import { AuthPage } from "./pages/AuthPage"; +import { EmailVerifyPage } from "./pages/EmailVerifyPage"; +import { needsNativeEmailVerification } from "./lib/emailVerification"; import { AccountsPage } from "./pages/AccountsPage"; import { DashboardPage } from "./pages/DashboardPage"; import { GapsPage } from "./pages/GapsPage"; @@ -88,6 +90,10 @@ export function App() { return ; } + if (needsNativeEmailVerification(member)) { + return ; + } + const effectivePage = page === "register" && !member.roles?.includes("admin") ? "dashboard" : page; return ( diff --git a/backend/web/src/api/client.ts b/old/backend/web/src/api/client.ts similarity index 85% rename from backend/web/src/api/client.ts rename to old/backend/web/src/api/client.ts index 0a8fad1..e80f3de 100644 --- a/backend/web/src/api/client.ts +++ b/old/backend/web/src/api/client.ts @@ -62,8 +62,11 @@ type RequestOptions = { providerToken?: string; headers?: Record; retryOnUnauthorized?: boolean; + timeoutMs?: number; }; +const defaultRequestTimeoutMs = 30_000; + function isTransientGatewayError(status: number) { return status === 502 || status === 503 || status === 504; } @@ -97,7 +100,20 @@ export async function apiRequest(path: string, options: RequestOptions = {}): let envelope: ApiEnvelope | null = null; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { - response = await fetch(path, buildRequestInit(options)); + try { + response = await fetchWithTimeout(path, buildRequestInit(options), options.timeoutMs ?? defaultRequestTimeoutMs); + } catch (err) { + if (err instanceof ApiError) { + throw err; + } + if (err instanceof DOMException && err.name === "AbortError") { + throw new ApiError("後端服務回應逾時,請確認 API gateway 是否已啟動。", 504); + } + if (err instanceof TypeError) { + throw err; + } + throw new ApiError("後端服務暫時無法連線,請稍後再試。", 502); + } envelope = await readApiEnvelope(response); if (!isProxyUnavailable(response, envelope) || attempt === maxAttempts - 1) { break; @@ -134,6 +150,16 @@ export async function apiRequest(path: string, options: RequestOptions = {}): return envelope.data as T; } +async function fetchWithTimeout(path: string, init: RequestInit, timeoutMs: number) { + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(path, { ...init, signal: controller.signal }); + } finally { + window.clearTimeout(timer); + } +} + function buildRequestInit(options: RequestOptions): RequestInit { const headers: Record = { Accept: "application/json", diff --git a/backend/web/src/api/haixun.ts b/old/backend/web/src/api/haixun.ts similarity index 96% rename from backend/web/src/api/haixun.ts rename to old/backend/web/src/api/haixun.ts index ec580d2..bfaabe2 100644 --- a/backend/web/src/api/haixun.ts +++ b/old/backend/web/src/api/haixun.ts @@ -839,6 +839,10 @@ export type SettingValue = { export const api = { capabilities: () => apiRequest("/api/v1/members/me/capabilities", { auth: true }), memberMe: () => apiRequest("/api/v1/members/me", { auth: true }), + verifyEmail: (code: string) => + apiRequest<{ email_verified: boolean }>("/api/v1/auth/verify-email", { method: "POST", body: { code }, auth: true }), + resendVerificationEmail: () => + apiRequest<{ sent: boolean }>("/api/v1/auth/resend-verification-email", { method: "POST", auth: true }), updateMemberMe: (body: Record) => apiRequest("/api/v1/members/me", { method: "PATCH", body, auth: true }), uploadMemberAvatar: (file: File) => { const form = new FormData(); @@ -910,15 +914,37 @@ export const api = { `/api/v1/threads-accounts/${accountId}/mention-inbox?page=${page}&pageSize=${pageSize}`, { auth: true } ), - publishMentionReply: (accountId: string, mediaId: string, text: string) => - apiRequest<{ media_id?: string; permalink?: string }>( + uploadPublishAttachment: (accountId: string, file: File) => { + const form = new FormData(); + form.append("image", file); + return apiRequest<{ attachment_key: string; public_url: string }>( + `/api/v1/threads-accounts/${accountId}/publish-attachments`, + { method: "POST", body: form, auth: true } + ); + }, + publishMentionReply: (accountId: string, mediaId: string, body: { text?: string; image?: File }) => { + const form = new FormData(); + if (body.text?.trim()) form.append("text", body.text.trim()); + if (body.image) form.append("image", body.image); + return apiRequest<{ media_id?: string; permalink?: string }>( `/api/v1/threads-accounts/${accountId}/mention-inbox/${encodeURIComponent(mediaId)}/reply`, - { method: "POST", body: { text }, auth: true } - ), + { method: "POST", body: form, auth: true, timeoutMs: 120_000 } + ); + }, + publishThreadsReply: (accountId: string, body: { reply_to_id: string; text?: string; image?: File }) => { + const form = new FormData(); + form.append("reply_to_id", body.reply_to_id); + if (body.text?.trim()) form.append("text", body.text.trim()); + if (body.image) form.append("image", body.image); + return apiRequest<{ media_id?: string; permalink?: string }>( + `/api/v1/threads-accounts/${accountId}/publish-reply`, + { method: "POST", body: form, auth: true, timeoutMs: 120_000 } + ); + }, publishQueue: (accountId: string, status = "", page = 1) => apiRequest(`/api/v1/threads-accounts/${accountId}/publish-queue?page=${page}&pageSize=20${status ? `&status=${status}` : ""}`, { auth: true }), - createPublishQueue: (accountId: string, body: { text: string; topic_tag?: string; scheduled_at?: number }) => + createPublishQueue: (accountId: string, body: { text?: string; image_key?: string; image_keys?: string[]; topic_tag?: string; scheduled_at?: number }) => apiRequest(`/api/v1/threads-accounts/${accountId}/publish-queue`, { method: "POST", body, auth: true }), patchPublishQueue: (accountId: string, qid: string, body: { text?: string; topic_tag?: string; scheduled_at?: number }) => apiRequest(`/api/v1/threads-accounts/${accountId}/publish-queue/${qid}`, { method: "PATCH", body, auth: true }), diff --git a/backend/web/src/auth/AuthContext.tsx b/old/backend/web/src/auth/AuthContext.tsx similarity index 90% rename from backend/web/src/auth/AuthContext.tsx rename to old/backend/web/src/auth/AuthContext.tsx index fcc8c34..241af26 100644 --- a/backend/web/src/auth/AuthContext.tsx +++ b/old/backend/web/src/auth/AuthContext.tsx @@ -5,6 +5,8 @@ export type MemberMe = { tenant_id: string; uid: string; email: string; + email_verified: boolean; + origin?: string; display_name?: string; status: string; roles?: string[]; @@ -29,7 +31,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [member, setMember] = useState(null); const [loading, setLoading] = useState(true); - const reloadMember = useCallback(async () => { + const reloadMember = useCallback(async (opts?: { throwOnError?: boolean }) => { if (!getAccessToken()) { setMember(null); setLoading(false); @@ -39,9 +41,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { const data = await apiRequest("/api/v1/members/me", { auth: true }); setMember(data); - } catch { + } catch (err) { clearTokens(); setMember(null); + if (opts?.throwOnError) { + throw err; + } } finally { setLoading(false); } @@ -63,7 +68,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { body: { tenant_id: tenantId, email, password } }); setTokens(data.access_token, data.refresh_token); - await reloadMember(); + await reloadMember({ throwOnError: true }); }, [reloadMember] ); diff --git a/backend/web/src/components/AcIcon.tsx b/old/backend/web/src/components/AcIcon.tsx similarity index 100% rename from backend/web/src/components/AcIcon.tsx rename to old/backend/web/src/components/AcIcon.tsx diff --git a/backend/web/src/components/AuthDecor.tsx b/old/backend/web/src/components/AuthDecor.tsx similarity index 100% rename from backend/web/src/components/AuthDecor.tsx rename to old/backend/web/src/components/AuthDecor.tsx diff --git a/backend/web/src/components/AuthShell.tsx b/old/backend/web/src/components/AuthShell.tsx similarity index 100% rename from backend/web/src/components/AuthShell.tsx rename to old/backend/web/src/components/AuthShell.tsx diff --git a/backend/web/src/components/DevToolsPanel.tsx b/old/backend/web/src/components/DevToolsPanel.tsx similarity index 100% rename from backend/web/src/components/DevToolsPanel.tsx rename to old/backend/web/src/components/DevToolsPanel.tsx diff --git a/backend/web/src/components/ExtensionInstallCard.tsx b/old/backend/web/src/components/ExtensionInstallCard.tsx similarity index 100% rename from backend/web/src/components/ExtensionInstallCard.tsx rename to old/backend/web/src/components/ExtensionInstallCard.tsx diff --git a/backend/web/src/components/HoverHelpTip.tsx b/old/backend/web/src/components/HoverHelpTip.tsx similarity index 100% rename from backend/web/src/components/HoverHelpTip.tsx rename to old/backend/web/src/components/HoverHelpTip.tsx diff --git a/backend/web/src/components/JobMonitor.tsx b/old/backend/web/src/components/JobMonitor.tsx similarity index 100% rename from backend/web/src/components/JobMonitor.tsx rename to old/backend/web/src/components/JobMonitor.tsx diff --git a/backend/web/src/components/Layout.tsx b/old/backend/web/src/components/Layout.tsx similarity index 100% rename from backend/web/src/components/Layout.tsx rename to old/backend/web/src/components/Layout.tsx diff --git a/backend/web/src/components/MentionContextBlock.tsx b/old/backend/web/src/components/MentionContextBlock.tsx similarity index 100% rename from backend/web/src/components/MentionContextBlock.tsx rename to old/backend/web/src/components/MentionContextBlock.tsx diff --git a/backend/web/src/components/MentionProfileLookup.tsx b/old/backend/web/src/components/MentionProfileLookup.tsx similarity index 100% rename from backend/web/src/components/MentionProfileLookup.tsx rename to old/backend/web/src/components/MentionProfileLookup.tsx diff --git a/old/backend/web/src/components/PublishImageAttach.tsx b/old/backend/web/src/components/PublishImageAttach.tsx new file mode 100644 index 0000000..ed9e62c --- /dev/null +++ b/old/backend/web/src/components/PublishImageAttach.tsx @@ -0,0 +1,102 @@ +import { useRef, useState } from "react"; +import { api } from "../api/haixun"; +import { resolveAssetUrl } from "../lib/assetUrl"; +import { Button, ErrorText } from "./ui"; + +const defaultMaxImages = 20; + +type Props = { + accountId: string; + value?: string[]; + onChange: (keys: string[]) => void; + disabled?: boolean; + maxImages?: number; + hint?: string; +}; + +export function PublishImageAttach({ + accountId, + value = [], + onChange, + disabled, + maxImages = defaultMaxImages, + hint +}: Props) { + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(""); + const inputRef = useRef(null); + + async function handleFiles(files?: FileList | File[]) { + if (!files || !accountId) return; + const list = Array.from(files).slice(0, maxImages - value.length); + if (!list.length) return; + + setUploading(true); + setError(""); + const uploaded: string[] = []; + try { + for (const file of list) { + const result = await api.uploadPublishAttachment(accountId, file); + uploaded.push(result.attachment_key); + } + onChange([...value, ...uploaded].slice(0, maxImages)); + } catch (err) { + if (uploaded.length) { + onChange([...value, ...uploaded].slice(0, maxImages)); + } + setError(err instanceof Error ? err.message : "圖片上傳失敗"); + } finally { + setUploading(false); + } + } + + function removeAt(index: number) { + onChange(value.filter((_, i) => i !== index)); + } + + return ( +
      + {value.length > 0 ? ( +
      + {value.map((key, index) => ( +
      + {`附件 + +
      + ))} +
      + ) : null} + {value.length < maxImages ? ( +
      + 1} + hidden + onChange={(event) => { + void handleFiles(event.target.files || undefined); + event.target.value = ""; + }} + /> + + {value.length > 0 ? ( + 已選 {value.length} 張(最多 {maxImages} 張) + ) : hint ? ( + {hint} + ) : null} +
      + ) : null} + +
      + ); +} \ No newline at end of file diff --git a/old/backend/web/src/components/PublishReplyFeedback.tsx b/old/backend/web/src/components/PublishReplyFeedback.tsx new file mode 100644 index 0000000..9777fb9 --- /dev/null +++ b/old/backend/web/src/components/PublishReplyFeedback.tsx @@ -0,0 +1,43 @@ +import type { PublishReplyFeedback as PublishReplyFeedbackState } from "../lib/publishReplyFeedback"; + +type Props = { + feedback?: PublishReplyFeedbackState | null; + publishing?: boolean; +}; + +export function PublishReplyFeedback({ feedback, publishing }: Props) { + if (publishing) { + return ( +
      + 發送中 +

      正在透過 Threads API 發送回覆,含圖片時可能需 20~40 秒…

      +
      + ); + } + if (!feedback) return null; + + return ( +
      + + {feedback.kind === "success" ? "發送成功" : feedback.kind === "error" ? "發送失敗" : "處理中"} + +

      {feedback.message}

      + {feedback.mediaId && !feedback.permalink ? ( +

      + media_id: {feedback.mediaId} +

      + ) : null} + {feedback.permalink ? ( + + 查看你剛發的回覆 + + ) : null} +
      + ); +} \ No newline at end of file diff --git a/old/backend/web/src/components/ReplyImageAttach.tsx b/old/backend/web/src/components/ReplyImageAttach.tsx new file mode 100644 index 0000000..8146678 --- /dev/null +++ b/old/backend/web/src/components/ReplyImageAttach.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef, useState } from "react"; +import { Button } from "./ui"; + +type Props = { + value?: File[]; + onChange: (files: File[]) => void; + disabled?: boolean; + maxImages?: number; + hint?: string; +}; + +export function ReplyImageAttach({ + value = [], + onChange, + disabled, + maxImages = 1, + hint = "選圖後發送時才轉給 Threads,不會存入資料庫" +}: Props) { + const [previewURLs, setPreviewURLs] = useState([]); + const inputRef = useRef(null); + + useEffect(() => { + const urls = value.map((file) => URL.createObjectURL(file)); + setPreviewURLs(urls); + return () => { + urls.forEach((url) => URL.revokeObjectURL(url)); + }; + }, [value]); + + function handleFiles(files?: FileList | File[]) { + if (!files) return; + const list = Array.from(files).slice(0, maxImages); + onChange(list); + } + + function removeAt(index: number) { + onChange(value.filter((_, i) => i !== index)); + } + + return ( +
      + {previewURLs.length > 0 ? ( +
      + {previewURLs.map((src, index) => ( +
      + {`附件 + +
      + ))} +
      + ) : null} + {value.length < maxImages ? ( +
      + 1} + hidden + onChange={(event) => { + handleFiles(event.target.files || undefined); + event.target.value = ""; + }} + /> + + + {value.length > 0 ? `已選 ${value.length} 張(最多 ${maxImages} 張)` : hint} + +
      + ) : null} +
      + ); +} \ No newline at end of file diff --git a/backend/web/src/components/ThemeToggle.tsx b/old/backend/web/src/components/ThemeToggle.tsx similarity index 100% rename from backend/web/src/components/ThemeToggle.tsx rename to old/backend/web/src/components/ThemeToggle.tsx diff --git a/backend/web/src/components/ui.tsx b/old/backend/web/src/components/ui.tsx similarity index 100% rename from backend/web/src/components/ui.tsx rename to old/backend/web/src/components/ui.tsx diff --git a/backend/web/src/index.css b/old/backend/web/src/index.css similarity index 99% rename from backend/web/src/index.css rename to old/backend/web/src/index.css index 94365a1..87eff5b 100644 --- a/backend/web/src/index.css +++ b/old/backend/web/src/index.css @@ -2337,6 +2337,34 @@ td strong { object-fit: cover; } +.hx-publish-attach { + display: grid; + gap: 0.5rem; + margin-top: 0.65rem; +} + +.hx-publish-attach__grid { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.hx-publish-attach__preview { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.35rem; +} + +.hx-publish-attach__preview img { + width: min(100%, 10rem); + aspect-ratio: 1; + object-fit: cover; + border-radius: var(--radius-sm); + border: 1px solid var(--hx-line); + background: var(--hx-surface); +} + .hx-media-wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(120px, 100%), 1fr)); diff --git a/backend/web/src/lib/acAssets.ts b/old/backend/web/src/lib/acAssets.ts similarity index 100% rename from backend/web/src/lib/acAssets.ts rename to old/backend/web/src/lib/acAssets.ts diff --git a/backend/web/src/lib/activeAccount.ts b/old/backend/web/src/lib/activeAccount.ts similarity index 100% rename from backend/web/src/lib/activeAccount.ts rename to old/backend/web/src/lib/activeAccount.ts diff --git a/old/backend/web/src/lib/assetUrl.ts b/old/backend/web/src/lib/assetUrl.ts new file mode 100644 index 0000000..6bd68e6 --- /dev/null +++ b/old/backend/web/src/lib/assetUrl.ts @@ -0,0 +1,49 @@ +const defaultAssetBucket = "haixun-assets"; +const internalAssetHosts = new Set(["127.0.0.1", "localhost"]); + +function isInternalAssetHost(hostname: string) { + return internalAssetHosts.has(hostname); +} + +function publicObjectPath(bucket: string, key: string) { + const normalizedBucket = bucket.replace(/^\/+|\/+$/g, ""); + const normalizedKey = key.replace(/^\/+|\/+$/g, ""); + if (!normalizedBucket || !normalizedKey) { + return ""; + } + return `/${normalizedBucket}/${normalizedKey}`; +} + +/** + * Turn stored avatar/object refs into a browser-loadable same-origin URL. + * + * Storage contract: + * - Mongo stores S3 object keys only, e.g. avatars/default/{uid}.png + * - nginx / Vite proxy /haixun-assets/ → MinIO + * - Legacy full URLs (127.0.0.1:9000, old domains) are still accepted + */ +export function resolveAssetUrl(ref: string, bucket = defaultAssetBucket): string { + const trimmed = ref.trim(); + if (!trimmed) { + return ""; + } + if (trimmed.startsWith("/")) { + return trimmed; + } + if (trimmed.startsWith("avatars/") || trimmed.startsWith("publish-attachments/") || trimmed.startsWith(`${bucket}/`)) { + const key = trimmed.startsWith(`${bucket}/`) ? trimmed.slice(bucket.length + 1) : trimmed; + return publicObjectPath(bucket, key); + } + try { + const parsed = new URL(trimmed, window.location.origin); + if (parsed.pathname.startsWith(`/${bucket}/`)) { + if (!isInternalAssetHost(parsed.hostname)) { + return trimmed; + } + return `${parsed.pathname}${parsed.search}`; + } + } catch { + return trimmed; + } + return trimmed; +} \ No newline at end of file diff --git a/old/backend/web/src/lib/emailVerification.ts b/old/backend/web/src/lib/emailVerification.ts new file mode 100644 index 0000000..2a2fcc1 --- /dev/null +++ b/old/backend/web/src/lib/emailVerification.ts @@ -0,0 +1,7 @@ +import type { MemberMe } from "../auth/AuthContext"; + +export function needsNativeEmailVerification(member: MemberMe | null | undefined) { + if (!member) return false; + if (member.origin && member.origin !== "native") return false; + return !member.email_verified; +} \ No newline at end of file diff --git a/backend/web/src/lib/extensionInstall.ts b/old/backend/web/src/lib/extensionInstall.ts similarity index 100% rename from backend/web/src/lib/extensionInstall.ts rename to old/backend/web/src/lib/extensionInstall.ts diff --git a/backend/web/src/lib/extensionSync.ts b/old/backend/web/src/lib/extensionSync.ts similarity index 100% rename from backend/web/src/lib/extensionSync.ts rename to old/backend/web/src/lib/extensionSync.ts diff --git a/backend/web/src/lib/format.ts b/old/backend/web/src/lib/format.ts similarity index 100% rename from backend/web/src/lib/format.ts rename to old/backend/web/src/lib/format.ts diff --git a/backend/web/src/lib/formulaCache.ts b/old/backend/web/src/lib/formulaCache.ts similarity index 100% rename from backend/web/src/lib/formulaCache.ts rename to old/backend/web/src/lib/formulaCache.ts diff --git a/backend/web/src/lib/jobActions.ts b/old/backend/web/src/lib/jobActions.ts similarity index 100% rename from backend/web/src/lib/jobActions.ts rename to old/backend/web/src/lib/jobActions.ts diff --git a/backend/web/src/lib/jobEvents.ts b/old/backend/web/src/lib/jobEvents.ts similarity index 100% rename from backend/web/src/lib/jobEvents.ts rename to old/backend/web/src/lib/jobEvents.ts diff --git a/backend/web/src/lib/jobLabels.ts b/old/backend/web/src/lib/jobLabels.ts similarity index 89% rename from backend/web/src/lib/jobLabels.ts rename to old/backend/web/src/lib/jobLabels.ts index 3667a77..dfb67dc 100644 --- a/backend/web/src/lib/jobLabels.ts +++ b/old/backend/web/src/lib/jobLabels.ts @@ -3,17 +3,12 @@ const templateLabels: Record = { "generate-formula-draft": "公式草稿產文", "generate-rewrite-draft": "貼文改寫", "generate-outreach-draft": "獲客草稿", - "generate-copy-matrix": "文案矩陣", - "generate-copy-draft": "仿寫草稿", - "analyze-copy-mission": "文案任務分析", - "expand-copy-mission-graph": "文案任務圖譜", "expand-graph": "知識圖譜擴展", "placement-scan": "海巡掃描", "scan-viral": "爆款掃描", "style-8d": "8D 語調分析", "refresh-threads-token": "Threads Token 更新", "publish-analytics": "發文成效分析", - "refill-publish-inventory": "發文庫存補貨", demo_long_task: "示範長任務" }; diff --git a/backend/web/src/lib/jobStatus.ts b/old/backend/web/src/lib/jobStatus.ts similarity index 100% rename from backend/web/src/lib/jobStatus.ts rename to old/backend/web/src/lib/jobStatus.ts diff --git a/backend/web/src/lib/mentionContext.ts b/old/backend/web/src/lib/mentionContext.ts similarity index 100% rename from backend/web/src/lib/mentionContext.ts rename to old/backend/web/src/lib/mentionContext.ts diff --git a/backend/web/src/lib/mentionInbox.ts b/old/backend/web/src/lib/mentionInbox.ts similarity index 100% rename from backend/web/src/lib/mentionInbox.ts rename to old/backend/web/src/lib/mentionInbox.ts diff --git a/backend/web/src/lib/mentionInboxCache.ts b/old/backend/web/src/lib/mentionInboxCache.ts similarity index 100% rename from backend/web/src/lib/mentionInboxCache.ts rename to old/backend/web/src/lib/mentionInboxCache.ts diff --git a/backend/web/src/lib/mentionLookup.ts b/old/backend/web/src/lib/mentionLookup.ts similarity index 100% rename from backend/web/src/lib/mentionLookup.ts rename to old/backend/web/src/lib/mentionLookup.ts diff --git a/backend/web/src/lib/patrolPersona.ts b/old/backend/web/src/lib/patrolPersona.ts similarity index 100% rename from backend/web/src/lib/patrolPersona.ts rename to old/backend/web/src/lib/patrolPersona.ts diff --git a/backend/web/src/lib/patrolPlan.ts b/old/backend/web/src/lib/patrolPlan.ts similarity index 100% rename from backend/web/src/lib/patrolPlan.ts rename to old/backend/web/src/lib/patrolPlan.ts diff --git a/backend/web/src/lib/postScore.ts b/old/backend/web/src/lib/postScore.ts similarity index 100% rename from backend/web/src/lib/postScore.ts rename to old/backend/web/src/lib/postScore.ts diff --git a/backend/web/src/lib/postStats.ts b/old/backend/web/src/lib/postStats.ts similarity index 100% rename from backend/web/src/lib/postStats.ts rename to old/backend/web/src/lib/postStats.ts diff --git a/backend/web/src/lib/publishPageCache.ts b/old/backend/web/src/lib/publishPageCache.ts similarity index 100% rename from backend/web/src/lib/publishPageCache.ts rename to old/backend/web/src/lib/publishPageCache.ts diff --git a/backend/web/src/lib/publishPersona.ts b/old/backend/web/src/lib/publishPersona.ts similarity index 100% rename from backend/web/src/lib/publishPersona.ts rename to old/backend/web/src/lib/publishPersona.ts diff --git a/old/backend/web/src/lib/publishReplyFeedback.ts b/old/backend/web/src/lib/publishReplyFeedback.ts new file mode 100644 index 0000000..c06ddb3 --- /dev/null +++ b/old/backend/web/src/lib/publishReplyFeedback.ts @@ -0,0 +1,84 @@ +import type { ThreadsPlaygroundResult } from "../api/haixun"; +import { parsePublishReplyResult, type PublishReplyResult } from "./mentionInbox"; + +export type PublishReplyFeedback = { + kind: "success" | "error" | "info"; + message: string; + permalink?: string; + mediaId?: string; +}; + +export function enhancePublishErrorMessage(message: string): string { + const msg = message.trim() || "發送回覆失敗"; + if (/unknown error occurred/i.test(msg)) { + return `${msg}。常見原因:① 附圖網址 Threads 抓不到(請設 HAIXUN_STORAGE_S3_PUBLIC_BASE_URL=https://你的網域/api/v1/public/assets)② reply_to_id 無效 ③ 多圖回覆(請改一張)④ Token 權限不足`; + } + if (/502|503|504|逾時|timeout/i.test(msg)) { + return `${msg}(多半是 API gateway 未啟動或請求逾時;請確認 dev-all 有在跑)`; + } + if (/container|image_url|image url|公開|public/i.test(msg)) { + return `${msg}(附圖時請確認 HAIXUN_STORAGE_S3_PUBLIC_BASE_URL 是 Threads 能存取的公開網址,不能用 127.0.0.1)`; + } + if (/prime|keyword_search|註冊目標/i.test(msg)) { + return `${msg}(回覆他人貼文需先註冊目標;@ 提及請重新同步後再發送)`; + } + if (/permission|scope|授權/i.test(msg)) { + return `${msg}(請到 Threads 帳號頁跑診斷,確認 threads_manage_replies 等權限已開)`; + } + return msg; +} + +function missingMediaIdFeedback(): PublishReplyFeedback { + return { + kind: "error", + message: + "Threads API 回傳成功但沒有 media_id,回覆可能未真正發布。請到 Threads 原文確認;附圖請檢查公開圖片網址設定。" + }; +} + +export function feedbackFromPlaygroundResult(result: ThreadsPlaygroundResult): PublishReplyFeedback { + if (!result.ok) { + return { + kind: "error", + message: enhancePublishErrorMessage(result.message || "發送回覆失敗") + }; + } + const parsed = parsePublishReplyResult(result.data); + if (!parsed?.media_id?.trim()) { + return missingMediaIdFeedback(); + } + const mediaId = parsed.media_id.trim(); + const permalink = parsed.permalink?.trim(); + return { + kind: "success", + mediaId, + permalink, + message: permalink + ? "回覆已成功發布到 Threads。若原文還沒看到,等 30 秒後重新載入留言或點下方連結。" + : `回覆已發布(ID: ${mediaId})。Threads 有時需幾十秒才顯示,請重新載入留言確認。` + }; +} + +export function feedbackFromMentionReply(published: PublishReplyResult | null | undefined): PublishReplyFeedback { + const mediaId = published?.media_id?.trim(); + if (!mediaId) { + return missingMediaIdFeedback(); + } + const permalink = published?.permalink?.trim(); + return { + kind: "success", + mediaId, + permalink, + message: permalink + ? "回覆已成功發布到 Threads。若原文還沒看到,等 30 秒後重新同步提及或點下方連結。" + : `回覆已發布(ID: ${mediaId})。Threads 有時需幾十秒才顯示,請到原文串確認。` + }; +} + +export function feedbackFromThrownError(err: unknown): PublishReplyFeedback { + const raw = err instanceof Error ? err.message : "發送回覆失敗"; + return { + kind: "error", + message: enhancePublishErrorMessage(raw) + }; +} \ No newline at end of file diff --git a/backend/web/src/lib/replyMedia.ts b/old/backend/web/src/lib/replyMedia.ts similarity index 100% rename from backend/web/src/lib/replyMedia.ts rename to old/backend/web/src/lib/replyMedia.ts diff --git a/backend/web/src/lib/replyTree.ts b/old/backend/web/src/lib/replyTree.ts similarity index 100% rename from backend/web/src/lib/replyTree.ts rename to old/backend/web/src/lib/replyTree.ts diff --git a/backend/web/src/lib/safeStorage.ts b/old/backend/web/src/lib/safeStorage.ts similarity index 100% rename from backend/web/src/lib/safeStorage.ts rename to old/backend/web/src/lib/safeStorage.ts diff --git a/backend/web/src/lib/threadsOAuth.ts b/old/backend/web/src/lib/threadsOAuth.ts similarity index 100% rename from backend/web/src/lib/threadsOAuth.ts rename to old/backend/web/src/lib/threadsOAuth.ts diff --git a/backend/web/src/main.tsx b/old/backend/web/src/main.tsx similarity index 100% rename from backend/web/src/main.tsx rename to old/backend/web/src/main.tsx diff --git a/backend/web/src/pages/AccountsPage.tsx b/old/backend/web/src/pages/AccountsPage.tsx similarity index 100% rename from backend/web/src/pages/AccountsPage.tsx rename to old/backend/web/src/pages/AccountsPage.tsx diff --git a/backend/web/src/pages/AuthPage.tsx b/old/backend/web/src/pages/AuthPage.tsx similarity index 85% rename from backend/web/src/pages/AuthPage.tsx rename to old/backend/web/src/pages/AuthPage.tsx index 63d5222..08aaad7 100644 --- a/backend/web/src/pages/AuthPage.tsx +++ b/old/backend/web/src/pages/AuthPage.tsx @@ -56,9 +56,17 @@ function toAuthErrorMessage(error: unknown) { return "連不上伺服器,請確認後端服務是否已啟動。"; } if (error instanceof ApiError) { + if (error.status === 504 || error.message.includes("逾時")) { + return "後端回應逾時,請確認 API gateway 是否已啟動,或稍後再試。"; + } const message = error.message.toLowerCase(); if (error.status === 401 || message.includes("invalid email or password")) { - return "Email 或密碼不正確,請再確認一次。"; + return message.includes("invalid email or password") + ? "Email 或密碼不正確,請再確認一次。" + : "登入成功但無法載入會員資料,請重新整理後再試一次。"; + } + if (error.status === 403 || message.includes("permission denied")) { + return "帳號權限不足,無法進入巡樓 Console,請聯絡管理員。"; } if (message.includes("already") || message.includes("duplicate")) { return "這個 Email 已經註冊過,請改用登入。"; diff --git a/backend/web/src/pages/BrandsPage.tsx b/old/backend/web/src/pages/BrandsPage.tsx similarity index 100% rename from backend/web/src/pages/BrandsPage.tsx rename to old/backend/web/src/pages/BrandsPage.tsx diff --git a/backend/web/src/pages/DashboardPage.tsx b/old/backend/web/src/pages/DashboardPage.tsx similarity index 100% rename from backend/web/src/pages/DashboardPage.tsx rename to old/backend/web/src/pages/DashboardPage.tsx diff --git a/old/backend/web/src/pages/EmailVerifyPage.tsx b/old/backend/web/src/pages/EmailVerifyPage.tsx new file mode 100644 index 0000000..b4878e4 --- /dev/null +++ b/old/backend/web/src/pages/EmailVerifyPage.tsx @@ -0,0 +1,115 @@ +import { FormEvent, useEffect, useState } from "react"; +import { ApiError } from "../api/client"; +import { api } from "../api/haixun"; +import { useAuth } from "../auth/AuthContext"; +import { AuthShell } from "../components/AuthShell"; +import { Button, Field, Input } from "../components/ui"; + +const resendCooldownSec = 60; + +export function EmailVerifyPage() { + const { member, logout, reloadMember } = useAuth(); + const [code, setCode] = useState(""); + const [error, setError] = useState(""); + const [message, setMessage] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [resending, setResending] = useState(false); + const [cooldown, setCooldown] = useState(0); + + useEffect(() => { + if (cooldown <= 0) return; + const timer = window.setInterval(() => { + setCooldown((value) => (value > 0 ? value - 1 : 0)); + }, 1000); + return () => window.clearInterval(timer); + }, [cooldown]); + + async function submit(event: FormEvent) { + event.preventDefault(); + setError(""); + setMessage(""); + setSubmitting(true); + try { + await api.verifyEmail(code.trim()); + await reloadMember(); + setMessage("Email 已驗證,正在進入巡樓 Console…"); + } catch (err) { + setError(toVerifyErrorMessage(err)); + } finally { + setSubmitting(false); + } + } + + async function resend() { + if (cooldown > 0) return; + setError(""); + setMessage(""); + setResending(true); + try { + await api.resendVerificationEmail(); + setMessage(`驗證碼已寄到 ${member?.email || "你的信箱"},15 分鐘內有效。`); + setCooldown(resendCooldownSec); + } catch (err) { + setError(toVerifyErrorMessage(err)); + } finally { + setResending(false); + } + } + + return ( + +
      +

      + 請先驗證 {member?.email},才能使用巡樓 Console。若沒收到信,請按下方「重新寄送驗證碼」,並到 Mailpit(:8025)查看。 +

      + + setCode(e.target.value.replace(/\D/g, "").slice(0, 6))} + inputMode="numeric" + autoComplete="one-time-code" + placeholder="000000" + required + minLength={6} + maxLength={6} + /> + + {error ?

      {error}

      : null} + {message ?

      {message}

      : null} +
      + + + +
      +
      +
      + ); +} + +function toVerifyErrorMessage(error: unknown) { + if (error instanceof ApiError) { + const message = error.message.toLowerCase(); + if (message.includes("expired")) { + return "驗證碼已過期,請按「重新寄送驗證碼」。"; + } + if (message.includes("invalid verification code")) { + return "驗證碼不正確,請再確認一次。"; + } + if (message.includes("recently")) { + return "剛剛才寄過驗證碼,請稍等 1 分鐘再重寄。"; + } + if (message.includes("smtp") || message.includes("mailer")) { + return "郵件服務尚未設定,請聯絡管理員。"; + } + if (error.status === 504 || message.includes("逾時")) { + return "後端回應逾時,請稍後再試。"; + } + } + return "驗證失敗,請稍後再試一次。"; +} \ No newline at end of file diff --git a/backend/web/src/pages/GapsPage.tsx b/old/backend/web/src/pages/GapsPage.tsx similarity index 100% rename from backend/web/src/pages/GapsPage.tsx rename to old/backend/web/src/pages/GapsPage.tsx diff --git a/backend/web/src/pages/JobsPage.tsx b/old/backend/web/src/pages/JobsPage.tsx similarity index 100% rename from backend/web/src/pages/JobsPage.tsx rename to old/backend/web/src/pages/JobsPage.tsx diff --git a/backend/web/src/pages/MissionsPage.tsx b/old/backend/web/src/pages/MissionsPage.tsx similarity index 98% rename from backend/web/src/pages/MissionsPage.tsx rename to old/backend/web/src/pages/MissionsPage.tsx index a90ca3a..602471d 100644 --- a/backend/web/src/pages/MissionsPage.tsx +++ b/old/backend/web/src/pages/MissionsPage.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { api, ContentInboxItem, CopyMissionInspiration, Persona, ThreadsAccount } from "../api/haixun"; +import { PublishImageAttach } from "../components/PublishImageAttach"; import { Badge, Button, Card, ErrorText, Field, Input, PageTitle, Select, Textarea } from "../components/ui"; import { formatNano, inputDateTimeToNano, nanoToInputDateTime, shortText } from "../lib/format"; import { notifyJobCreated, requestJobsRefresh } from "../lib/jobEvents"; @@ -364,21 +365,24 @@ function ModeTabs({ activeMode, onChange }: { activeMode: Mode; onChange: (mode: function DirectPostCard({ accountId, onDone, onError }: { accountId: string; onDone: (message: string) => void; onError: (error: unknown) => void }) { const [text, setText] = useState(""); + const [imageKeys, setImageKeys] = useState([]); const [topicTag, setTopicTag] = useState(""); const [scheduledAt, setScheduledAt] = useState(""); const [submitting, setSubmitting] = useState(false); async function submit() { - if (!accountId || !text.trim()) return; + if (!accountId || (!text.trim() && imageKeys.length === 0)) return; setSubmitting(true); onError(undefined); try { await api.createPublishQueue(accountId, { - text: text.trim(), + text: text.trim() || undefined, + image_keys: imageKeys.length ? imageKeys : undefined, topic_tag: normalizeTopicTag(topicTag) || undefined, scheduled_at: inputDateTimeToNano(scheduledAt) || Date.now() * 1_000_000 }); setText(""); + setImageKeys([]); setTopicTag(""); setScheduledAt(""); onDone(scheduledAt ? "已排入發文佇列。" : "已送進發文佇列,系統會盡快發出。"); @@ -393,7 +397,8 @@ function DirectPostCard({ accountId, onDone, onError }: { accountId: string; onD
      -