-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathenv.example
More file actions
952 lines (856 loc) · 51.2 KB
/
env.example
File metadata and controls
952 lines (856 loc) · 51.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
# ============================================================================
# MINDPHASE ENVIRONMENT CONFIGURATION
# ============================================================================
# Copy this file to .env and configure according to your deployment needs
# ============================================================================
# APPLICATION CONFIGURATION
# ============================================================================
HOST=0.0.0.0
PORT=9527
DEBUG=False
# Set to "development" to bypass production startup guards (COLLAB_STRICT_PROD_GUARDS).
# Leave as "production" (the default) for real deployments.
# ENVIRONMENT=development
# Optional: Set your public WAN IP for external access (auto-detected if not set)
# EXTERNAL_HOST=your-public-ip-address
# Optional: Full base URL for external access (takes precedence over EXTERNAL_HOST)
# Use this when behind a reverse proxy with HTTPS (e.g. Nginx Proxy Manager)
# Also used in the admin Schools tab invitation share text (same value as the public site URL)
# Example: https://your-domain.com (no trailing slash)
# EXTERNAL_BASE_URL=https://your-domain.com
# ============================================================================
# LLM MODELS CONFIGURATION
# ============================================================================
QWEN_API_KEY=your-qwen-api-key-here
QWEN_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
# Classification (fast) vs generation (autocomplete / node palette / inline rec); disable thinking in app code.
QWEN_MODEL_CLASSIFICATION=qwen3.6-flash
QWEN_MODEL_GENERATION=qwen3.6-flash
# DashScope Qwen-Omni-Realtime (Kitty Agent WebSocket). Vision-capable realtime id from DashScope console.
# Default voice: Cherry for Qwen3-Omni-Flash-Realtime; Tina for Qwen3.5-Omni-Realtime per Aliyun docs.
# QWEN_OMNI_MODEL=qwen3-omni-flash-realtime-2025-12-01
# QWEN_OMNI_VOICE=Cherry
# QWEN_OMNI_VAD_THRESHOLD=0.5
# QWEN_OMNI_VAD_SILENCE_MS=1200
# QWEN_OMNI_INPUT_FORMAT=pcm16
# QWEN_OMNI_OUTPUT_FORMAT=pcm24
DASHSCOPE_QPM_LIMIT=13500
DASHSCOPE_CONCURRENT_LIMIT=500
DASHSCOPE_RATE_LIMITING_ENABLED=false
ARK_API_KEY=your-ark-api-key-here
ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
ARK_DEEPSEEK_ENDPOINT=ep-20250101000000-dummy
ARK_KIMI_ENDPOINT=ep-20250101000000-dummy
ARK_DOUBAO_ENDPOINT=ep-20250101000000-dummy
# Volcengine ARK Rate Limiting (Endpoint-Specific)
# Each endpoint has independent limits per Volcengine provider
# Official limits:
# - ark-deepseek (v3.2): 15,000 RPM, 1,500,000 TPM
# - ark-kimi: 5,000 RPM, 500,000 TPM
# - ark-doubao: 30,000 RPM, 5,000,000 TPM
# Note: Each endpoint is tracked separately with endpoint-specific Redis keys
KIMI_VOLCENGINE_QPM_LIMIT=4500
KIMI_VOLCENGINE_CONCURRENT_LIMIT=500
DOUBAO_VOLCENGINE_QPM_LIMIT=27000
DOUBAO_VOLCENGINE_CONCURRENT_LIMIT=500
# Legacy ARK rate limiting (deprecated - use endpoint-specific limits above)
ARK_QPM_LIMIT=4500
ARK_CONCURRENT_LIMIT=500
ARK_RATE_LIMITING_ENABLED=false
# Load Balancing Rate Limiting (for DeepSeek)
# DeepSeek is load-balanced between Dashscope and Volcengine
# Official Dashscope limits:
# - deepseek-v3.1: 15,000 RPM, 1,200,000 TPM
# - deepseek-v3.2: 15,000 RPM, 1,500,000 TPM
# - deepseek-r1: 15,000 RPM, 1,200,000 TPM
# Volcengine ark-deepseek endpoint (v3.2): 15,000 RPM, 1,500,000 TPM (same as Dashscope)
LOAD_BALANCING_RATE_LIMITING_ENABLED=true
# DeepSeek Volcengine Route Rate Limits
# These limits are used for the Volcengine route (ark-deepseek endpoint)
# Separate from Dashscope limits since Volcengine is a different provider
DEEPSEEK_VOLCENGINE_QPM_LIMIT=13500
DEEPSEEK_VOLCENGINE_CONCURRENT_LIMIT=500
# ============================================================================
# LOAD BALANCING CONFIGURATION
# ============================================================================
# Distributes DeepSeek requests between Dashscope and Volcengine
# Only DeepSeek is load-balanced; Qwen always uses Dashscope; Kimi/Doubao always use Volcengine
LOAD_BALANCING_ENABLED=true
LOAD_BALANCING_STRATEGY=round_robin
LOAD_BALANCING_WEIGHTS=dashscope:50,volcengine:50
# ============================================================================
# UI & DISPLAY CONFIGURATION
# ============================================================================
GRAPH_LANGUAGE=zh
DEFAULT_LANGUAGE=zh
TOPIC_FONT_SIZE=18
CHAR_FONT_SIZE=14
WATERMARK_TEXT=MindGraph
# ============================================================================
# LOGGING & DEBUGGING
# ============================================================================
LOG_LEVEL=INFO
VERBOSE_LOGGING=False
# ============================================================================
# ABUSEIPDB & FAIL2BAN (OPTIONAL)
# ============================================================================
# Security: put secrets only in .env (never commit .env). Sync uses HTTPS; AbuseIPDB is
# authenticated with ABUSEIPDB_API_KEY. CrowdSec Raw IP List uses HTTP Basic Auth
# (CROWDSEC_BLOCKLIST_USERNAME / CROWDSEC_BLOCKLIST_PASSWORD); the blocklist URL is not
# public without those credentials. Optional: set full CrowdSec URL via CROWDSEC_BLOCKLIST_URL
# or build it from CROWDSEC_BLOCKLIST_INTEGRATION_ID (+ optional CROWDSEC_BLOCKLIST_API_BASE).
#
# IP reputation (AbuseIPDB API v2) and daily blacklist sync into Redis.
# Fail2ban templates live in resources/fail2ban/ — see docs/FAIL2BAN_SETUP.md
# Linux: app exits on startup if MindGraph jail templates are missing (unless disabled).
# Set false for Docker or hosts without Fail2ban. Optional: FAIL2BAN_ETC=/etc/fail2ban
# FAIL2BAN_STARTUP_CHECK=false
# All launch copy-paste commands (Redis ports, Qdrant, Celery, …):
# python -m services.infrastructure.utils.launch_commands
# APIv2 reference: https://docs.abuseipdb.com/?python#introduction
#
# Redis SET key abuseipdb:blacklist:ips holds AbuseIPDB API data, both shipped baselines,
# and CrowdSec merges (historical key name; not AbuseIPDB-only). See docs/FAIL2BAN_SETUP.md.
# Optional in-process TTL cache for SISMEMBER on that key (same canonical IP within TTL skips Redis):
# Read once at startup with warm_sismember_cache_ttl_snapshot (restart to change).
# IP_REPUTATION_SISMEMBER_CACHE_TTL_SECONDS=0
# AbuseIPDB/CrowdSec lookup flags are snapshotted once after Redis init (see ip_reputation_env_snapshot).
# Per-request vetting logs ([IP reputation] Vetting allow: ...) — default off; set true when debugging.
# IP_REPUTATION_VERBOSE_LOG=false
#
# ABUSEIPDB_ENABLED=false
# ABUSEIPDB_API_KEY=
# Optional: override API host (default https://api.abuseipdb.com/api/v2). Key stays in ABUSEIPDB_API_KEY.
# ABUSEIPDB_API_BASE=https://api.abuseipdb.com/api/v2
# Live GET /check per IP (uses daily quota). Default off — use blacklist sync + Redis only.
# ABUSEIPDB_CHECK_ENABLED=false
# ABUSEIPDB_CHECK_MIN_SCORE=80
# ABUSEIPDB_CHECK_CACHE_TTL_SECONDS=86400
# ABUSEIPDB_BLACKLIST_LOOKUP_ENABLED=true
# ABUSEIPDB_BLACKLIST_SYNC_ENABLED=true
# The in-process blacklist scheduler runs on the same clock as DB backups (BACKUP_HOUR, default 03:00 local).
# ABUSEIPDB_BLACKLIST_SYNC_INTERVAL_SECONDS is not used for that schedule; it remains for AbuseIPDB API semantics.
# Without relax, values are clamped to at least 86400s when referenced elsewhere.
# ABUSEIPDB_BLACKLIST_SYNC_RELAX_MIN_INTERVAL=true
# ABUSEIPDB_BLACKLIST_SYNC_INTERVAL_SECONDS=86400
# ABUSEIPDB_BLACKLIST_CONFIDENCE_MINIMUM=75
# Max IPs per request; AbuseIPDB caps by plan (10k / 100k / 500k). Default 10000 fits Free/Standard.
# ABUSEIPDB_BLACKLIST_LIMIT=10000
# ABUSEIPDB_REPORT_ENABLED=true
# ABUSEIPDB_REPORT_DEDUPE_TTL_SECONDS=86400
# For Fail2ban ban-action CLI only (not used by the web app):
# ABUSEIPDB_API_KEY_FILE=/etc/fail2ban/abuseipdb.conf
#
# Shipped baseline file (merged into Redis at startup and after each API blacklist sync):
# ABUSEIPDB_BASELINE_ENABLED=true
# ABUSEIPDB_BASELINE_FILE=data/abuseipdb/blacklist_baseline.txt
# Refresh file: python scripts/setup/download_abuseipdb_baseline.py
#
# CrowdSec Console blocklists (Raw IP List integration) — merged into the same Redis set.
# Create integration in Console → Blocklist → Integrations; subscribe to blocklists (e.g. Community).
# Docs: https://docs.crowdsec.net/u/integrations/rawiplist/
# CROWDSEC_BLOCKLIST_ENABLED=false
# Full URL or integration id (path segment) for .../v1/integrations/{id}/content
# CROWDSEC_BLOCKLIST_URL=https://admin.api.crowdsec.net/v1/integrations/YOUR_ID/content
# If using INTEGRATION_ID only, optional override for the integrations prefix (default CrowdSec Console API):
# CROWDSEC_BLOCKLIST_API_BASE=https://admin.api.crowdsec.net/v1/integrations
# CROWDSEC_BLOCKLIST_INTEGRATION_ID=
# CROWDSEC_BLOCKLIST_USERNAME=
# CROWDSEC_BLOCKLIST_PASSWORD=
# CROWDSEC_BLOCKLIST_SYNC_ENABLED=true
# CROWDSEC_BLOCKLIST_LOOKUP_ENABLED=true
# Scheduled network merge uses BACKUP_HOUR (default 03:00) like DB backup/COS; not CROWDSEC_BLOCKLIST_SYNC_INTERVAL_SECONDS.
# CROWDSEC_BLOCKLIST_SYNC_INTERVAL_SECONDS=86400
# Skip fetch if last successful merge was within this window (startup merge and non-scheduled callers only; forced daily sync ignores this).
# CROWDSEC_BLOCKLIST_MIN_INTERVAL_SECONDS=82800
#
# Shipped baseline (same idea as AbuseIPDB; merged at startup and after AbuseIPDB replace sync):
# CROWDSEC_BASELINE_ENABLED=true
# CROWDSEC_BASELINE_FILE=data/crowdsec/blocklist_baseline.txt
# Refresh file: python scripts/setup/download_crowdsec_baseline.py
# ============================================================================
# FEATURE FLAGS
# ============================================================================
# Core Features
FEATURE_MINDMATE=False
# Mobile /m/kitty and canvas multimodal agent: set True and restart API.
FEATURE_KITTY_AGENT=True
# Optional: process Kitty WS JSON through Pipecat PipelineTask (same behaviour; adds framework path).
# FEATURE_KITTY_PIPECAT_PIPELINE=False
# TTL (seconds) for kitty:sessionmeta:* and kitty:live_spec:* in Redis (minimum 60; default 4 hours if unset).
# KITTY_SESSION_REDIS_TTL_SECONDS=14400
# TTL (seconds) for the global per-scope WebSocket refcount key (default 4 hours if unset; refreshed on attach/detach).
# KITTY_WS_REFCOUNT_TTL_SECONDS=14400
# Refcount Lua: 1 = EVALSHA with per-process script cache (default); 0 = plain EVAL only (some Redis Cluster setups).
# KITTY_REFCOUNT_USE_EVALSHA=1
# Kitty WebSocket inbound limits (DoS / memory). Defaults match routers/features/voice/kitty_routes.py.
# KITTY_WS_MAX_TEXT_CHARS=12000
# KITTY_WS_MAX_AUDIO_B64_CHARS=65536
# KITTY_WS_IMAGE_B64_MAX_CHARS=900000
# KITTY_WS_IMAGE_RAW_MAX_BYTES=524288
# Max nesting depth for JSON object frames (start message + client loop). See utils/ws_limits.py.
# KITTY_WS_MAX_JSON_DEPTH=32
# Close Kitty / voice WebSocket when no inbound client message for this many seconds (default 300).
# Set to 0, off, or false to disable idle shutdown.
# KITTY_WS_IDLE_TIMEOUT_SECONDS=300
# Cross-worker Kitty coordination (Redis pub/sub). Default channel mg:kitty:control.
# KITTY_CONTROL_CHANNEL=mg:kitty:control
# Optional: stable pod/instance id (default hostname:pid).
# KITTY_CONTROL_INSTANCE_ID=
# HMAC shared secret for cross-worker Kitty control messages. All workers must use the same value.
# When DEBUG=False and this is unset, subscribers reject Kitty control envelopes (lock down Redis ACLs or set a secret).
# KITTY_CONTROL_SHARED_SECRET=
FEATURE_DRAG_AND_DROP=False
# Sidebar Features
FEATURE_COURSE=False # Thinking Course (思维课程) - Disabled by default
FEATURE_TEMPLATE=False # Template Resources (模板资源) - Disabled by default
FEATURE_COMMUNITY=False # Community Sharing (社区分享) - Disabled by default
FEATURE_ASKONCE=True # AskOnce (多应) - Multi-LLM chat - Enabled by default
FEATURE_SCHOOL_ZONE=False # School Zone (学校专区) - Organization sharing - Disabled by default
FEATURE_DEBATEVERSE=False # DebateVerse (论境) - US-style debate system - Disabled by default
FEATURE_KNOWLEDGE_SPACE=False # Personal Knowledge Space (RAG) - Disabled by default (requires Qdrant and Celery)
FEATURE_LIBRARY=False # Library (图书馆) - PDF viewing with danmaku comments - Disabled by default
FEATURE_MARKETS=False # Market (市场) - catalog, orders, Alipay checkout - Disabled by default
FEATURE_MINDBOT=True # MindBot — DingTalk HTTP robot → Dify (per-organization config) — enabled by default
#
# MindBot admin page — “Dify Service Online / Offline” header button (blue/red pill):
# Backend probes GET {MINDBOT_DIFY_HEALTH_BASE_URL}/parameters with Authorization: Bearer {MINDBOT_DIFY_HEALTH_API_KEY}.
# Dify’s app API has no GET /v1/health; /parameters is the lightweight check. Keys stay server-side only.
# Optional: omit MINDBOT_DIFY_HEALTH_API_KEY to show “not configured” until set.
# MINDBOT_DIFY_HEALTH_BASE_URL=https://dify.mindspringedu.com/v1
# MINDBOT_DIFY_HEALTH_API_KEY=
# MINDBOT_LOG_CALLBACK_INBOUND=1 # Log one line per callback: method, path, query, body length, sign preview (no full headers)
# MINDBOT_LOG_CALLBACK_INBOUND_FULL=1 # Verbose: client, X-Forwarded-*, all headers JSON, full body (capped)
# MINDBOT_LOG_CALLBACK_BODY_MAX=65536 # Max raw body bytes logged in FULL mode (default 65536)
# MINDBOT_LOG_CALLBACK_DEBUG=0 # Default is on: full inbound + failure dumps; set 0 to disable (may log secrets when on)
# MINDBOT_LOG_INBOUND_PROMPT_PREVIEW=1 # INFO: log truncated extracted user text after pipeline_start (privacy; dev only)
# Capacity: per-process semaphores cap in-flight Dify work (not global across workers).
# MINDBOT_DINGTALK_MAX_SKEW_MS=3600000 # Robot receive-message-1: max |now - timestamp| in ms (default 1h; aligns with Flask samples)
# MINDBOT_MAX_CONCURRENT_STREAMING=64 # Concurrent streaming Dify startup slots per process (released on first SSE event; default 64)
# MINDBOT_MAX_ACTIVE_STREAMING=128 # Active streams held end-to-end per process (default 128); used as denominator for burst utilisation math
# MINDBOT_MAX_CONCURRENT_BLOCKING=64 # Concurrent blocking Dify + outbound paths per process (default 64)
# MINDBOT_MAX_ACTIVE_BLOCKING=128 # Active blocking pipelines held end-to-end per process (default 128)
# Per-org dynamic concurrency cap (streaming). At low global utilisation the cap expands so workshops
# with 20-50 teachers are not throttled; under genuine load it contracts to the base to enforce fairness.
# MINDBOT_ORG_MAX_CONCURRENT_STREAMING=8 # Base cap per org per worker (always enforced when pool is busy; default 8)
# MINDBOT_ORG_BURST_FREE_THRESHOLD=0.5 # Burst activates when >= this fraction of MINDBOT_MAX_ACTIVE_STREAMING is free (default 0.5)
# MINDBOT_ORG_BURST_SHARE=0.4 # In burst mode, org may claim up to this fraction of free slots (default 0.4)
# MINDBOT_ORG_ABSOLUTE_MAX_STREAMING=40 # Hard ceiling per org per worker even in full burst (default 40; handles 50-teacher workshop across 4 workers)
# Per-org dynamic concurrency cap (blocking / legacy path).
# MINDBOT_ORG_MAX_CONCURRENT_BLOCKING=4 # Base cap per org per worker (default 4)
# MINDBOT_ORG_BURST_FREE_THRESHOLD_BLOCKING=0.5
# MINDBOT_ORG_BURST_SHARE_BLOCKING=0.4
# MINDBOT_ORG_ABSOLUTE_MAX_BLOCKING=16 # Hard ceiling per org per worker in burst (default 16)
# MINDBOT_ORG_STREAM_WARN_THRESHOLD=10 # Log WARNING when one org exceeds this many active streams on one worker (default 10)
# MINDBOT_REDIS_MAX_CONNECTIONS=150 # Async Redis pool for MindBot (default 150; raise with DB pool if many concurrent handlers)
# MINDBOT_OAUTH_LOCK_MAP_MAX=2048 # Max in-process LRU entries for DingTalk OAuth thundering-herd locks (per worker; bounds memory if many orgs over time)
# Memory / RSS verification (ops — not automatic): under steady load, watch process RSS 24–48h; it should plateau for a fixed tenant set.
# If RSS climbs with org onboarding only, GET /api/mindbot/admin/internal/memory-footprint (platform admin only) shows oauth_lock_map_size and dingtalk_stream_registered_clients.
# Optional: tracemalloc / py-spy on the API worker if you suspect leaks (correlate with concurrent pipelines vs idle heap).
# DingTalk Stream SDK: one WebSocket client per distinct client_id per process; recycle workers after large credential churn (see stream_client module docstring).
# MINDBOT_RATE_LIMIT_ENABLED=true # Per-org fixed-window rate limiter (default true; set false to disable)
# MINDBOT_ORG_RATE_LIMIT=200 # Max requests per org per window (default 200).
# # Multi-worker note: Redis is authoritative; during a Redis outage each worker tracks its
# # own counter so effective limit = N_workers × this value. With 4 workers set to 50 for
# # a desired hard limit of 200 req/min under Redis-down fallback.
# MINDBOT_ORG_RATE_WINDOW_SECONDS=60 # Window size in seconds (default 60)
# MINDBOT_RATE_LIMIT_MEM_MAX_KEYS=5000 # Max orgs tracked in per-process fallback counter (default 5000; oldest evicted when exceeded)
# MINDBOT_DEDUP_REQUIRE_REDIS=false # If true, return 503 when Redis is down and msgId dedup cannot run (fail closed vs duplicate processing)
# MINDBOT_CONV_GATE_ENABLED=true # Redis single-flight for first Dify bind per DingTalk chat (multi-worker); set false to disable
# MINDBOT_CONV_GATE_TTL_SECONDS=120 # TTL for mindbot:conv_gate:* lock key
# MINDBOT_CONV_GATE_POLL_MS=3000 # Max wait for peer to bind Dify conversation_id (milliseconds)
# MINDBOT_CONV_GATE_POLL_STEP_MS=50 # Poll interval when waiting for conv_key (milliseconds)
# MINDBOT_REPLY_MSGTYPE=markdown # sessionWebhook reply: text | markdown (default markdown, per DingTalk receive-message-1)
# DingTalk OpenAPI (api.dingtalk.com): token cache, media download, optional fallback when sessionWebhook fails or is absent
# MINDBOT_OPENAPI_ENABLED=true # false disables OAuth token, media fetch, and OpenAPI send fallback
# Per-org MindBot admin: optional DingTalk AI card template id + param key (markdown variable).
# Uses POST /v1.0/card/instances/createAndDeliver and PUT /v1.0/card/streaming (OpenAPI).
# App needs interactive card write + AI streaming update scopes; third-party personal apps are not supported.
# Card OpenAPI calls count against monthly quotas (see DingTalk docs).
# MINDBOT_DINGTALK_AI_CARD_PARAM_KEY_DEFAULT=content # Template variable key when org leaves param key empty
# MINDBOT_FETCH_MEDIA=true # Download picture/video/audio/file via OpenAPI and attach to Dify (needs org Client ID)
# MINDBOT_FALLBACK_OPENAPI_SEND=true # Reply via robot OpenAPI when sessionWebhook POST fails or is missing
# MINDBOT_DINGTALK_TOKEN_TTL=6800 # Redis cache TTL for access token (seconds; DingTalk tokens are ~7200s)
# MINDBOT_MAX_MEDIA_BYTES=10485760 # Max bytes when downloading media from DingTalk temporary URL
# MINDBOT_OPENAPI_FALLBACK_MSGKEY= # Optional: sampleText | sampleMarkdown | sampleImageMsg (else mirror MINDBOT_REPLY_MSGTYPE)
# MINDBOT_SESSION_WEBHOOK_ALLOW_HTTP=false # If true, allow http:// sessionWebhook URLs (default: HTTPS only)
# MINDBOT_SESSION_WEBHOOK_ALLOW_HOSTS= # Optional comma-separated hostname allowlist (empty = any public host after SSRF checks).
# # Production: pin to DingTalk-related hosts only, e.g. oapi.dingtalk.com,*.dingtalk.com
# MINDBOT_SESSION_WEBHOOK_DNS_TIMEOUT=5 # Seconds for DNS resolution during sessionWebhook URL validation
# MINDBOT_DIFY_STREAMING=true # Use Dify SSE streaming; batch deltas then send text chunks to DingTalk
# MINDBOT_STREAM_MIN_CHARS=64 # Flush a batch after this many characters (streaming)
# MINDBOT_STREAM_FLUSH_MS=400 # Or flush after this many ms with a non-empty buffer
# MINDBOT_STREAM_MAX_PARTS=40 # Max batched outbound sends per reply (rest coalesced at message_end)
# MINDBOT_STREAM_DEFER_TO_END=false # If true, accumulate SSE text until message_end then send (fewer DingTalk bubbles; better with message_replace)
# MINDBOT_AI_CARD_GROUP_USE_APPKEY=false # Group card: default imGroupOpenDeliverModel.robotCode=dingtalk_robot_code (same as 1:1); set true to use dingtalk_client_id (AppKey) for internal non-scene orgs.
# MINDBOT_AI_CARD_GROUP_ANONYMOUS_DELIVER=true # Group only: if callback has only LWCP tokens (no real userId), omit userId/recipients and deliver to openSpaceId (createAndDeliver optional fields).
# MINDBOT_AI_CARD_EXTRA_SENDER_KEYS= # Comma-separated extra JSON keys to scan for a real user id (also under extension/bizData).
# DingTalk AI card (OpenAPI): Dify sends many SSE deltas; MindBot batches (MINDBOT_STREAM_*), then each PUT /v1.0/card/streaming sends isFull=true (full markdown snapshot, not deltas).
# Each frame is sanitized and capped (~950 chars); longer replies truncate on the card unless MINDBOT_AI_CARD_APPEND_OVERFLOW_REMAINDER=true (sends overflow as one follow-up chat message).
# PUT may return HTTP 401 if the cached OAuth token expired; MindBot invalidates Redis and retries once with a fresh token.
# Admin "Test permissions" only checks OAuth + streaming endpoint (random outTrackId); it is not an end-to-end card delivery test.
# Dify-native media (image URLs, voice, file links) use separate robot OpenAPI messages, not the card body.
# MINDBOT_AI_CARD_APPEND_OVERFLOW_REMAINDER=false
# MindBot pipeline debug logs (no separate env flag): use LOG_LEVEL=DEBUG or set loggers to DEBUG in app config:
# services.mindbot.core.dify_stream — SSE batch params, buffer flush reason (min_chars | interval | coalesce | defer_*), outcome
# services.mindbot.platforms.dingtalk.ai_card — create/stream PUT sizes, OAuth 401 retry (INFO), put_ok (DEBUG)
# services.mindbot.pipeline.dify_paths — streaming branch ai_card_wiring, message_replace hook (INFO)
# clients.dify — stream_chat start (conversation id prefix, query length)
# INFO without DEBUG: Dify message_replace, DingTalk ai_card_streaming_oauth_retry, ai_card_create_ok, mindbot_pipeline_message_replace
# MINDBOT_DIFY_WORKFLOW_OUTPUT_KEY= # Chatflow: if set, read final text from workflow_finished.data.outputs[KEY] when message deltas are empty
# MINDBOT_USAGE_TRACKING=true # Persist mindbot_usage_events (DingTalk identity + optional Dify token counts); set false to disable DB writes
# MINDBOT_EDUCATION_METRICS=true # Educational research fields: group vs 1:1, inbound msg type, per-thread user-turn index (Redis); set false to skip turn counter
# OpenAPI helpers also support oapi.dingtalk.com/media/upload (image≤1MB voice≤2MB video/file≤10MB) for sampleFile/sampleAudio/sampleVideo
# Alipay (when FEATURE_MARKETS=True): unified gateway alipay.trade.page.pay + async notify
# ALIPAY_APP_ID=
# ALIPAY_APP_PRIVATE_KEY= # RSA2 private key (PEM body or PKCS#1)
# ALIPAY_ALIPAY_PUBLIC_KEY= # Alipay public key (not app cert)
# ALIPAY_SANDBOX=false # Use openapi.alipaydev.com when true
# ALIPAY_NOTIFY_BASE_URL=https://your-domain.com # Public origin; notify URL = {base}/api/markets/payments/alipay/notify
FEATURE_GEWE=False # Gewe WeChat integration (admin only) - Disabled by default
FEATURE_RAG_CHUNK_TEST=False # RAG Chunk Test - Disabled by default
FEATURE_SMART_RESPONSE=False # Smart Response (智回) - ESP32 watch teacher interface - Disabled by default
FEATURE_TEACHER_USAGE=False # Teacher Usage (教师使用度) - Admin analytics dashboard - Disabled by default
FEATURE_WORKSHOP_CHAT=False # Workshop Chat (教研坊) - School-scoped communication system - Disabled by default
FEATURE_MCP_HTTP=False # MCP Streamable HTTP at /api/mcp (Bearer mgat_ + X-MG-Account) - Disabled by default
# MCP_HTTP_INTERNAL_BASE_URL= # Optional override for loopback base URL used by MCP tools (default http://127.0.0.1:PORT)
# Comma-separated organization IDs whose members may use Workshop Chat without admin/manager (preview/testing). Empty = elevated roles only.
WORKSHOP_CHAT_PREVIEW_ORG_IDS=
# ============================================================================
# AI ASSISTANT CONFIGURATION
# ============================================================================
DIFY_API_KEY=your_dify_api_key_here
DIFY_API_URL=https://api.dify.ai/v1
# MindMate maps Dify `user` to the Bayi vendor UUID when AUTH_MODE=bayi and users.phone is a UUID (SSO); passkey and non-UUID phones use mg_user_<db_pk> (see utils/dify_mindmate_user_id.py).
DIFY_TIMEOUT=300
AI_ASSISTANT_NAME=教学设计
# ============================================================================
# GEWE WECHAT INTEGRATION CONFIGURATION (Admin Only)
# ============================================================================
# Set FEATURE_GEWE=True above to enable. When disabled, Gewe API and UI are not loaded.
GEWE_TOKEN=your_gewe_token_here
GEWE_BASE_URL=http://api.geweapi.com
GEWE_TIMEOUT=30
# Gewe Webhook Security Configuration
# IP Whitelisting: Comma-separated list of allowed IP addresses for webhook requests
# Leave empty to allow all IPs (dev mode). In production, add Gewe API server IPs.
# Example: GEWE_WEBHOOK_ALLOWED_IPS=82.157.39.177,123.45.67.89
GEWE_WEBHOOK_ALLOWED_IPS=
# Default webhook callback URL for reference
# Note: Currently uses main application port (9527). For production, consider using
# a separate port for webhooks (like xxxbot pattern) for better isolation and security.
# Example: http://123.45.67.89:9527/api/gewe/webhook
# Alternative (separate port): http://123.45.67.89:9528/api/gewe/webhook
# ============================================================================
# DATABASE CONFIGURATION
# ============================================================================
# PostgreSQL (default after migration)
# Format: postgresql://user:password@host:port/database
#
# DEFAULT VALUES (used by migration script if not set):
# - User: mindgraph_user
# - Password: mindgraph_password
# - Host: localhost
# - Port: 5432
# - Database: mindgraph
#
# The migration script (scripts/migrate_sqlite_to_postgresql.py) will use
# these defaults if DATABASE_URL is not set or if individual POSTGRESQL_*
# environment variables are not set.
DATABASE_URL=postgresql://mindgraph_user:mindgraph_password@localhost:5432/mindgraph
# Optional: override the URL used for CREATE/DROP DATABASE during PG dump merge.
# Defaults to DATABASE_URL credentials pointed at the "postgres" system database.
# Only needed if your app user lacks CREATEDB on an external PostgreSQL server.
# PG_ADMIN_URL=postgresql://postgres:superuser_password@localhost:5432/postgres
# SQLite (legacy - only used during migration from SQLite to PostgreSQL)
# Uncomment below to use SQLite temporarily (migration will run automatically)
# DATABASE_URL=sqlite:///./data/mindgraph.db
# GeoLite2-Country MMDB path (overseas education-email registration; email login CN block)
# Default when unset: data/GeoLite2-Country.mmdb relative to project root
# GEOIP_MAXMIND_COUNTRY_PATH=data/GeoLite2-Country.mmdb
# Email/password login with email: block clients GeoIP-resolved to CN unless whitelisted (admin).
# Default true. Set false for emergency disable without deploy.
# Skipped when AUTH_MODE=bayi (same stack as overseas registration GeoIP).
# EMAIL_LOGIN_CN_BLOCK_ENABLED=true
# VPN / CN transition: when enabled, users who signed in from a non-CN baseline and
# later appear from mainland China (IP / CF-IPCountry) get access sessions invalidated,
# all refresh tokens revoked, mg_geo_cn_mainland cookie stamped, and 403. HTTP and
# WebSocket (/ws/*) connections use the same rules. Geo Redis keys TTL is extended on
# same-IP requests and on token refresh so long sessions keep a stable baseline.
# Mainland 11-digit phone accounts are exempt. Default off.
# VPN_CN_KICKOUT_ENABLED=false
# Comma-separated user IDs to exempt (support / testing).
# VPN_CN_KICKOUT_ALLOWLIST_USER_IDS=
# PostgreSQL Subprocess Management (Option B - Primary)
# Application manages PostgreSQL as subprocess (like Qdrant/Celery)
# Set POSTGRESQL_MANAGED_BY_APP=false to use external/systemd PostgreSQL service
POSTGRESQL_MANAGED_BY_APP=true
# PostgreSQL Data Directory (for subprocess mode)
# Directory where PostgreSQL will store its data files
# Will be created automatically if it doesn't exist
#
# Automatic path selection (if not set):
# - WSL: Uses ~/.mindgraph/postgresql (Linux-native path)
# - Ubuntu/Debian (as root with /root/ path): Uses /var/lib/postgresql/mindgraph
# - Otherwise: Uses ./storage/postgresql (relative to project root)
POSTGRESQL_DATA_DIR=./storage/postgresql
# PostgreSQL Connection Settings
# These settings are used when POSTGRESQL_MANAGED_BY_APP=true
# They must match the values in DATABASE_URL above
#
# DEFAULT VALUES (used by migration script if not set):
# These are the default credentials that will be used if not specified.
# The migration script will use these values when constructing the connection URL.
POSTGRESQL_PORT=5432
POSTGRESQL_USER=mindgraph_user
POSTGRESQL_PASSWORD=mindgraph_password
POSTGRESQL_DATABASE=mindgraph
# Server max_connections (managed subprocess only writes this into postgresql.conf).
# Default app pools need 175 for one worker (async+sync pools + 10 reserve); raise if more workers or clients.
# For external PostgreSQL, set the same in postgresql.conf (or ALTER SYSTEM) and restart the server.
POSTGRESQL_MAX_CONNECTIONS=175
# Database Connection Pool Configuration
# Defaults: async pool 50+100, sync pool 5+10 per process (~165 connections per worker before reserve).
# Set PostgreSQL max_connections >= workers × (async+sync totals) + ~10 reserve; see config/database.py.
# DATABASE_POOL_SIZE=50
# DATABASE_MAX_OVERFLOW=100
# DATABASE_POOL_TIMEOUT=60
# Database Integrity Check Configuration (PostgreSQL)
# PostgreSQL uses connection tests (SELECT 1) on startup
# Set SKIP_INTEGRITY_CHECK=true to skip (for development/testing)
# ============================================================================
# TOKEN TRACKING CONFIGURATION
# ============================================================================
TOKEN_TRACKER_ENABLED=true
TOKEN_TRACKER_BATCH_SIZE=1000
TOKEN_TRACKER_BATCH_INTERVAL=300
TOKEN_TRACKER_MAX_BUFFER_SIZE=10000
# ============================================================================
# DIAGRAM STORAGE CONFIGURATION
# ============================================================================
# Persistent storage for user diagrams with Redis caching
DIAGRAM_CACHE_TTL=604800
DIAGRAM_SYNC_INTERVAL=300
DIAGRAM_SYNC_BATCH_SIZE=100
DIAGRAM_MAX_PER_USER=20
DIAGRAM_MAX_SPEC_SIZE_KB=500
# ============================================================================
# DATABASE BACKUP CONFIGURATION
# ============================================================================
BACKUP_ENABLED=true
BACKUP_HOUR=3
BACKUP_RETENTION_COUNT=2
BACKUP_DIR=backup
# ============================================================================
# TENCENT CLOUD OBJECT STORAGE (COS) CONFIGURATION (Optional)
# ============================================================================
# Register at: https://console.cloud.tencent.com/cos5
COS_BACKUP_ENABLED=false
COS_BUCKET=your-bucket-name-appid
COS_REGION=ap-beijing
COS_KEY_PREFIX=backups/mindgraph-CHANGE
# ============================================================================
# AUTHENTICATION & SECURITY CONFIGURATION
# ============================================================================
# NOTE: JWT_SECRET_KEY is no longer needed in .env
# It is auto-generated and stored in Redis for multi-worker safety
# Users only need to re-login if Redis is flushed (rare event)
JWT_EXPIRY_HOURS=24
# Maximum concurrent sessions per user (default: 2 devices)
# When exceeded, oldest sessions are automatically logged out
MAX_CONCURRENT_SESSIONS=2
# Auth modes: standard, enterprise, bayi
AUTH_MODE=standard
# Self-service signup (invite, SMS/email OTP for register—including /sms/verify & /email/verify peek—
# overseas email, quick-register mint & attendee signup).
# When false, those flows return 403; login and SSO unaffected. Default true.
# REGISTRATION_ENABLED=true
# Quick registration: 6-digit rotating room code (HMAC). Per-channel key material is generated at mint
# and stored in Redis with the token (room_code_secret), not in this file.
# The room code only indicates in-room presence; it does not prove phone ownership like SMS OTP.
#
# Optional tuning (defaults shown) for large rooms / shared NAT. SMS is not used on register-quick.
# Token TTL and workshop sliding refresh: DEFAULT_TTL_SECONDS in quick_register_redis; workshop mode
# refreshes EXPIRE on the token, minter index, and usage key after each successful signup until close/cap.
# Room-code guess limits (Redis): if Redis errors on the guess-throttle read, the service fails open
# (does not block) for availability; monitor Redis health.
# If metric quick_reg_token_delete_failed_after_success rises, investigate Redis and revoke the token if needed.
# Default IP window: 2 min (120s) sliding, 600 max — shared-WiFi workshop ~200-500; lower if stricter per-IP cap needed.
# QUICK_REGISTER_IP_MAX=600
# QUICK_REGISTER_IP_WINDOW=120
# QUICK_REGISTER_PHONE_MAX=15
# QUICK_REGISTER_PHONE_WINDOW=600
#
# GET /api/auth/quick-register/room-code (modal + attendee probe): optional per-IP and per-token limits.
# QUICK_REG_ROOM_GET_IP_MAX=60
# QUICK_REG_ROOM_GET_IP_WINDOW=60
# QUICK_REG_ROOM_GET_TOKEN_MAX=240
# QUICK_REG_ROOM_GET_TOKEN_WINDOW=60
# Admin login identifiers (comma-separated): SMS/phone admins and optionally Bayi passkey admins.
# ADMIN_PHONES=13812345678,550e8400-e29b-41d4-a716-446655440000,bayi@system.com
ADMIN_PHONES=
# Comma-separated database user primary keys (users.id) granting admin alongside ADMIN_PHONES above.
ADMIN_USER_IDS=
# Enterprise Mode (only if AUTH_MODE=enterprise)
# WARNING: enterprise disables JWT validation for all requests. Use only on VPN/private LAN
# or other network isolation; never expose an enterprise-mode server to the public Internet.
ENTERPRISE_DEFAULT_ORG_CODE=DEMO-001
ENTERPRISE_DEFAULT_USER_PHONE=enterprise@system.com
# Public Dashboard (passkey-protected public dashboard)
PUBLIC_DASHBOARD_PASSKEY=123456
# Dashboard Configuration
DASHBOARD_MAX_CONCURRENT_SSE_CONNECTIONS=2
DASHBOARD_SSE_POLL_INTERVAL_SECONDS=5
DASHBOARD_STATS_UPDATE_INTERVAL=10
DASHBOARD_HEARTBEAT_INTERVAL=30
DASHBOARD_STATS_CACHE_TTL=3
DASHBOARD_MAP_DATA_CACHE_TTL=20
DASHBOARD_REGISTERED_USERS_CACHE_TTL=300
DASHBOARD_TOKEN_USAGE_CACHE_TTL=60
# Bayi Mode (only if AUTH_MODE=bayi)
# 6-digit fallback login: POST /api/auth/bayi/passkey (AUTH_MODE=bayi only; not vendor SSO).
# One passkey only; admin dashboard: set ADMIN_PHONES / ADMIN_USER_IDS (e.g. bayi@system.com or users.id).
BAYI_PASSKEY=888888
BAYI_DECRYPTION_KEY=v8IT7XujLPsM7FYuDPRhPtZk
BAYI_DEFAULT_ORG_CODE=BAYI-001
# Optional: use existing org by primary key (e.g. prod 北京八一学校). When set, that row must exist; no org auto-create.
# Also used for first-time POST /api/auth/bayi/passkey users (bayi@system.com); SSO /loginByXz uses the same rule.
# BAYI_DEFAULT_ORG_ID=5
# Default display name on first Bayi SSO user create only (repeat SSO does not overwrite; users may PATCH /api/auth/profile).
# BAYI_SSO_DEFAULT_DISPLAY_NAME=八一用户
BAYI_CLOCK_SKEW_TOLERANCE=10
# Optional cleanup after deploy: rename legacy Bayi SSO display label for users under a given org.
# Example (adjust organization_id to match BAYI_DEFAULT_ORG_ID): UPDATE users SET name = '八一用户' WHERE organization_id = 5 AND name = 'Bayi User';
# Alembic data migration rev_0030 sets organizations.display_name to 北京八一学校 for id=5 (`alembic upgrade head`).
# Invitation Codes: Format AAAA-XXXXX
# Internal format: ORG_CODE:INVITATION_CODE:EXPIRY_DATE
INVITATION_CODES=DEMO-001:DEMO-A1B2C:never
# ============================================================================
# TENCENT CLOUD — SMS + SES (same SecretId / SecretKey for both products)
# ============================================================================
# One Tencent Cloud API key pair authenticates SMS, SES, and other services.
# SMS: https://console.cloud.tencent.com/smsv2
# SES: https://console.cloud.tencent.com/ses — optional TENCENT_SES_SECRET_* below
# fall back to TENCENT_SMS_SECRET_* when empty.
TENCENT_SMS_SECRET_ID=your-tencent-secret-id-here
TENCENT_SMS_SECRET_KEY=your-tencent-secret-key-here
TENCENT_SMS_SDK_APP_ID=1400000000
TENCENT_SMS_SIGN_NAME=MindGraph
TENCENT_SMS_REGION=ap-guangzhou
TENCENT_SMS_TEMPLATE_REGISTER=1234567
TENCENT_SMS_TEMPLATE_LOGIN=1234568
TENCENT_SMS_TEMPLATE_RESET_PASSWORD=1234569
TENCENT_SMS_TEMPLATE_ALERT=12345678
TENCENT_SMS_TEMPLATE_STARTUP=123456
SMS_CODE_EXPIRY_MINUTES=5
SMS_RESEND_INTERVAL_SECONDS=60
SMS_MAX_ATTEMPTS_PER_PHONE=5
SMS_MAX_ATTEMPTS_WINDOW_HOURS=1
SMS_MAX_CONCURRENT_REQUESTS=10
# POST /sms/verify — sliding window (minutes); limits brute-force on 6-digit codes
SMS_VERIFY_MAX_ATTEMPTS_PER_COMBO=25
SMS_VERIFY_WINDOW_MINUTES=15
SMS_VERIFY_MAX_ATTEMPTS_PER_IP=150
# POST /sms/send — max attempts per client IP per sliding window
SMS_SEND_WINDOW_MINUTES=15
SMS_SEND_MAX_ATTEMPTS_PER_IP=40
SMS_QPM_LIMIT=100
SMS_RATE_LIMITING_ENABLED=true
SMS_STARTUP_NOTIFICATION_ENABLED=true
# ============================================================================
# TENCENT CLOUD SES (邮件推送) — Email verification (Optional)
# ============================================================================
# Console: https://console.cloud.tencent.com/ses — use ap-hongkong for overseas.
# API host should match signing Host (default: ses.ap-hongkong.tencentcloudapi.com).
# Leave TENCENT_SES_SECRET_ID / TENCENT_SES_SECRET_KEY empty to reuse TENCENT_SMS_SECRET_*.
TENCENT_SES_SECRET_ID=
TENCENT_SES_SECRET_KEY=
TENCENT_SES_REGION=ap-hongkong
TENCENT_SES_HOST=ses.ap-hongkong.tencentcloudapi.com
# Full From line, e.g. MindSpring Support Team <noreply@yourdomain.com>
TENCENT_SES_FROM_EMAIL=
# Optional: monitored inbox for Reply-To (recommended)
TENCENT_SES_REPLY_TO=
# Single template ID for all verification-code emails (register, login OTP, reset, change email).
# Template JSON must accept CODE (e.g. {"CODE": "{{1}}"} in console) — see services/auth/ses_service.py.
TENCENT_SES_TEMPLATE_ID=123456
EMAIL_SUBJECT_REGISTER=Your verification code
EMAIL_SUBJECT_LOGIN=Your verification code
EMAIL_SUBJECT_RESET_PASSWORD=Your verification code
EMAIL_SUBJECT_CHANGE_EMAIL=Your verification code
EMAIL_CODE_EXPIRY_MINUTES=10
EMAIL_RESEND_INTERVAL_SECONDS=60
EMAIL_MAX_ATTEMPTS_PER_ADDRESS=5
EMAIL_MAX_ATTEMPTS_WINDOW_HOURS=1
EMAIL_MAX_CONCURRENT_REQUESTS=50
EMAIL_QPM_LIMIT=100
EMAIL_RATE_LIMITING_ENABLED=true
# POST /email/verify — sliding window (minutes); limits brute-force on 6-digit codes
EMAIL_VERIFY_MAX_ATTEMPTS_PER_COMBO=25
EMAIL_VERIFY_WINDOW_MINUTES=15
EMAIL_VERIFY_MAX_ATTEMPTS_PER_IP=150
# POST /email/send — max attempts per client IP per sliding window (abuse / SES cost)
EMAIL_SEND_WINDOW_MINUTES=15
EMAIL_SEND_MAX_ATTEMPTS_PER_IP=40
# Academic email: Kikobeats free-domain list — REQUIRED at runtime (import-time load in swot_academic).
# Path: data/kikobeats_free_email_domains.json (commit in repo or mount in deploy).
# pyswot from git (rse-pyswot @ main), not PyPI — see requirements.txt. Refresh: scripts/update_swot_upstream.ps1
# Upstream: https://github.com/JetBrains/swot https://github.com/kikobeats/free-email-domains
SWOT_ACADEMIC_EMAIL_REQUIRED=true
# Comma-separated purposes (e.g. register). Only enforced when REQUIRED=true.
SWOT_ACADEMIC_EMAIL_PURPOSES=register
# ============================================================================
# REDIS CONFIGURATION (REQUIRED)
# ============================================================================
# MindGraph uses PostgreSQL + Redis architecture
# Redis is REQUIRED. Application will NOT start without Redis.
#
# To enable Redis authentication (Ubuntu):
# 1. sudo nano /etc/redis/redis.conf
# 2. Find "# requirepass foobared" and change to: requirepass your-secure-redis-password
# 3. sudo systemctl restart redis-server
# 4. Update REDIS_URL below with: redis://:your-secure-redis-password@localhost:6379/0
#
# Format: redis://[:password]@host:port/db
# Examples:
# Without password: redis://localhost:6379/0
# With password: redis://:your-secure-redis-password@localhost:6379/0
# Remote server: redis://:password@redis.example.com:6379/0
REDIS_URL=redis://localhost:6379/0
# Redis connection settings (used by Celery and other services)
# Default Redis host and port (used if REDIS_URL is not set or for Celery)
REDIS_HOST=localhost
REDIS_PORT=6379
# Connection pool settings (optional)
# REDIS_MAX_CONNECTIONS=50
# REDIS_SOCKET_TIMEOUT=5
# User Auth Cache Preloading
# Pre-loads all users and organizations from database into Redis cache at startup
# Set to false to skip preloading (useful for development to speed up startup)
# Cache will still be populated on-demand when users access the system
PRELOAD_USER_AUTH_CACHE=true
# ============================================================================
# CELERY CONFIGURATION (REQUIRED)
# ============================================================================
# Celery is REQUIRED for background task processing (document processing, etc.)
# Application will NOT start without Celery worker.
#
# Celery uses Redis as broker and result backend.
# Default configuration uses Redis DB 1 for Celery (DB 0 is used for caching).
#
# Start Celery worker:
# celery -A config.celery worker --loglevel=info
#
# Or use the server launcher which starts Celery automatically:
# python main.py
# Redis database number for Celery (default: 1, DB 0 is used for application cache)
REDIS_CELERY_DB=1
# Celery broker and result backend URLs (optional, auto-constructed from REDIS_HOST/REDIS_PORT/REDIS_CELERY_DB if not set)
# CELERY_BROKER_URL=redis://localhost:6379/1
# CELERY_RESULT_BACKEND=redis://localhost:6379/1
# ============================================================================
# PROCESS MONITORING CONFIGURATION
# ============================================================================
# Process Monitor automatically monitors health of Qdrant, Celery, and Redis,
# and restarts failed subprocesses with circuit breaker protection.
# Enable process monitoring (default: true)
PROCESS_MONITOR_ENABLED=true
# Health check interval in seconds (default: 30)
PROCESS_MONITOR_INTERVAL_SECONDS=30
# Maximum restart attempts before circuit breaker opens (default: 3)
PROCESS_MONITOR_MAX_RESTARTS=3
# Time window for restart count in seconds (default: 300 = 5 minutes)
PROCESS_MONITOR_RESTART_WINDOW_SECONDS=300
# Enable circuit breaker to prevent restart loops (default: true)
PROCESS_MONITOR_CIRCUIT_BREAKER_ENABLED=true
# Enable SMS alerts for critical failures (default: true)
PROCESS_MONITOR_SMS_ALERTS_ENABLED=true
# SMS alert cooldown period in seconds (default: 600 = 10 minutes)
# Prevents spam - max 1 SMS per service per cooldown period
PROCESS_MONITOR_SMS_ALERT_COOLDOWN_SECONDS=600
# ============================================================================
# HEALTH MONITORING CONFIGURATION
# ============================================================================
# Health Monitor periodically checks /health/all endpoint and sends SMS alerts
# to admin phones when server health issues are detected.
# Enable health monitoring (default: true)
HEALTH_MONITOR_ENABLED=true
# Health check interval in seconds (default: 900 = 15 minutes)
HEALTH_MONITOR_INTERVAL_SECONDS=900
# SMS alert cooldown period in seconds (default: 1800 = 30 minutes)
# Prevents alert spam if health check keeps failing
HEALTH_MONITOR_SMS_ALERT_COOLDOWN_SECONDS=1800
# Number of consecutive failures before sending alert (default: 1)
# Set to 2+ to reduce false positives from transient failures
HEALTH_MONITOR_FAILURE_THRESHOLD=1
# Health check HTTP timeout in seconds (default: 30)
HEALTH_MONITOR_TIMEOUT_SECONDS=30
# ============================================================================
# CRITICAL ALERT CONFIGURATION
# ============================================================================
# Critical Alert Service sends SMS alerts for critical application errors
# Ensures only ONE alert per critical error scenario (no spam)
# Enable critical alerting (default: true)
CRITICAL_ALERT_ENABLED=true
# Cooldown period in seconds for runtime critical errors (default: 1800 = 30 minutes)
# Prevents duplicate alerts for the same error
CRITICAL_ALERT_COOLDOWN_SECONDS=1800
# Cooldown period in seconds for unhandled exceptions (default: 3600 = 1 hour)
# Longer cooldown to prevent spam from repeated crashes
CRITICAL_ALERT_EXCEPTION_COOLDOWN_SECONDS=3600
# ============================================================================
# KNOWLEDGE SPACE (RAG) CONFIGURATION - REQUIRED
# ============================================================================
# Vector Database: Qdrant (REQUIRED)
# Qdrant server is REQUIRED for Knowledge Space features.
# Application will exit if Qdrant is not available.
#
# Install Qdrant (Linux): sudo python3 scripts/setup/setup.py — see docs/QDRANT_SETUP.md
# Default QDRANT_HOST (change if using remote Qdrant server):
# Alternative: Use QDRANT_URL for full URL format (e.g., http://localhost:6333)
QDRANT_HOST=localhost:6333
# QDRANT_URL=http://localhost:6333
# Embedded mode storage (used when QDRANT_HOST is not set)
QDRANT_PERSIST_DIR=./storage/qdrant
QDRANT_COLLECTION_PREFIX=user_
QDRANT_COMPRESSION=SQ8
# Knowledge Space Storage
KNOWLEDGE_STORAGE_DIR=./storage/knowledge_documents
MAX_DOCUMENTS_PER_USER=5
MAX_FILE_SIZE=10485760
MAX_STORAGE_PER_USER=52428800
MAX_CHUNKS_PER_USER=1000
# Embedding Configuration
DASHSCOPE_EMBEDDING_MODEL=text-embedding-v4
DASHSCOPE_RERANK_MODEL=qwen3-rerank
EMBEDDING_BATCH_SIZE=50
# Chunking Configuration
# Options: semchunk (default, fast), mindchunk (LLM-based, semantic)
CHUNKING_ENGINE=semchunk
# Retrieval Configuration
DEFAULT_RETRIEVAL_METHOD=hybrid
HYBRID_VECTOR_WEIGHT=0.5
HYBRID_KEYWORD_WEIGHT=0.5
RERANKING_MODE=reranking_model
RERANK_SCORE_THRESHOLD=0.5
RETRIEVAL_PARALLEL_WORKERS=2
# Chunking Configuration
CHUNK_SIZE=500
CHUNK_OVERLAP=50
# ============================================================================
# EXTERNAL SERVICES CONFIGURATION
# ============================================================================
WECHAT_QR_IMAGE=
# ============================================================================
# ONLINE COLLAB (CANVAS WORKSHOP / WEBSOCKET)
# ============================================================================
# Requires Redis >= 8.0 (RedisJSON in core). Live workshop spec uses JSON.* on
# workshop:live_spec:{code}; the app refuses startup below 8.0 when collab is on
# (unless COLLAB_DISABLED=1).
#
# Emergency kill switch: new collab WebSocket handshakes close immediately with
# WebSocket code 1013 (Try Again Later) before auth — REST APIs stay up.
# COLLAB_DISABLED=0
#
# Redis 8 client-side cache invalidation for workshop key prefix (opt-in).
# COLLAB_REDIS_CLIENT_TRACKING=0
#
# Editor state in Redis: HASH+HEXPIRE (default) vs legacy JSON string (0).
# COLLAB_EDITORS_USE_HASH=1
#
# Hash-tag workshop:* keys so live_spec, snapshot_seq, tombstones share one cluster slot (enables WATCH/MULTI).
# COLLAB_REDIS_HASH_TAGS=1
#
# Fall back to PostgreSQL NOTIFY when Redis pub/sub is unavailable (0/1).
# COLLAB_PG_NOTIFY_FALLBACK=0
#
# Concurrent shards when broadcasting updates to local WebSocket queues.
# WORKSHOP_FANOUT_SHARD_CONCURRENCY=50
#
# LRU dup suppression across Redis + PostgreSQL NOTIFY duplicate frames (parsed JSON.msg_id keys).
# WORKSHOP_FANOUT_MSG_DEDUP=8192
#
# TTL (seconds 120–86400) for join resume credential minted inside ``joined`` frames.
# WORKSHOP_JOIN_RESUME_TTL_SEC=900
#
# Uvicorn WebSocket RFC ping interval/timeout seconds (unset uses 25 / 40; <=0 disables that knob).
# COLLAB_UVICORN_WS_PING_INTERVAL_SEC=25
# COLLAB_UVICORN_WS_PING_TIMEOUT_SEC=40
#
# Outbound snapshot soft limit (UTF-8 JSON bytes of the snapshot frame body). Larger diagrams get an error + metric instead of enqueue.
# COLLAB_WS_SNAPSHOT_MAX_BYTES=4194304
#
# Production requires Redis and fails startup if Redis is unavailable.
# Set COLLAB_STRICT_PROD_GUARDS=1 in production so missing security settings
# fail startup instead of silently weakening collaboration security.
# COLLAB_STRICT_PROD_GUARDS=1
#
# Comma-separated full origins allowed for canvas-collab WebSocket (CSWSH mitigation).
# Required in production when COLLAB_STRICT_PROD_GUARDS=1.
# COLLAB_WS_ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
#
# When origins are enforced, allow upgrades with no Origin header (tests / non-browser clients).
# COLLAB_WS_ALLOW_MISSING_ORIGIN=0
#
# Per-user socket caps. Endpoint cap applies to canvas-collab sockets on one worker;
# global cap applies across WebSocket endpoints when Redis global cap is enabled.
# COLLAB_WS_MAX_PER_USER_ENDPOINT=5
# COLLAB_WS_MAX_PER_USER_GLOBAL=20
#
# Re-validate JWT on open collab sockets every N seconds (0 = off).
# COLLAB_WS_JWT_REVALIDATE_SEC=180
#
# Optional Redis-accurate global collab socket cap across workers (uses same ceiling as COLLAB_WS_MAX_PER_USER_GLOBAL).
# COLLAB_WS_REDIS_GLOBAL_SOCKET_CAP=0
#
# Update merge: max seconds to wait for process / room semaphores.
# COLLAB_WS_UPDATE_SEMAPHORE_ACQUIRE_SEC=45
#
# Update merge: max concurrent Redis merges per workshop code on this worker (legacy, prefer COLLAB_ROOM_MERGE_SEM_CAP).
# COLLAB_WS_ROOM_MERGE_MAX_CONCURRENT=32
#
# Per-room merge semaphore cap: maximum concurrent in-flight Redis merge ops for a single room.
# Controls burst concurrency, NOT the number of connected participants — rooms of 200-500 teachers
# work fine at the default of 50 (50 concurrent × ~10ms/op = ~5,000 merges/s capacity per room).
# Raise to 100+ only if profiling shows the semaphore is a bottleneck under sustained bursts.
# COLLAB_ROOM_MERGE_SEM_CAP=50
#
# Process-wide backstop for total concurrent update merges across all rooms.
# Should be at least COLLAB_ROOM_MERGE_SEM_CAP × number of expected active rooms.
# COLLAB_UPDATE_GLOBAL_SEM_CAP=500
#
# Fanout pub/sub: max queued local delivery tasks before drop + metric.
# WORKSHOP_FANOUT_DELIVERY_QUEUE_MAX=8192
#
# Stop/idle-kick: Redis TTL for workshop:closing:{code} marker.
# WORKSHOP_SESSION_CLOSING_TTL_SEC=120
#
# Max participants per workshop room (default 500; cap 10000).
# WORKSHOP_MAX_PARTICIPANTS=500
#
# Maximum inbound canvas-collab WebSocket text frame size (UTF-8 bytes).
# Default 1 MiB to allow full-document replacement frames (~768 KiB) plus JSON overhead.
# COLLAB_WS_MAX_TEXT_BYTES=1048576
#
# Maximum accepted JSON nesting depth for inbound collab messages (DoS bound).
# COLLAB_WS_MAX_JSON_DEPTH=48
#
# Concurrent shutdown flushes from SCAN of live_spec keys (default 8).
# LIVE_SPEC_SHUTDOWN_FLUSH_CONCURRENCY=8
#
# Max live_spec keys sampled for stale flush detection on /health/websocket.
# LIVE_SPEC_HEALTH_STALE_SAMPLE=80
# Use SPUBLISH / sharded pubsub on Redis Cluster (see runbook redis-cluster.md).
# WORKSHOP_USE_SHARDED_PUBSUB=0
#
# Fan-out envelope origin secret. Required in production when COLLAB_STRICT_PROD_GUARDS=1.
# Set to a random string (e.g. openssl rand -hex 32) so subscribers reject envelopes
# that did not originate from this app.
# When not set, the application auto-generates an ephemeral secret at startup —
# acceptable for single-process dev; set explicitly in production so all workers share
# the same secret (otherwise cross-worker fan-out envelope validation will fail).
# COLLAB_FANOUT_ORIGIN_SECRET=
#
# Join rate-limit behaviour when Redis is unavailable (fail-open = allow, fail-closed = deny).
# Keep false in production to prevent join brute-force spikes during Redis outages.
# COLLAB_JOIN_RL_FAIL_OPEN=false