feat: add public multi-user runtime protections - #1
Conversation
📝 WalkthroughWalkthrough本次变更加入邮件验证码注册、用户 token 配额账本、RAG 图片附件处理、HTML 脚本语法校验、沙箱隔离收紧,以及可选 LiteLLM Compose 部署配置和相关文档。 Changes多用户认证与配额
RAG 图片与可视化校验
沙箱与可选 LiteLLM 部署
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant AgenticChatPipeline
participant LlamaIndexPipeline
participant RuntimeData
User->>AgenticChatPipeline: submit query
AgenticChatPipeline->>LlamaIndexPipeline: retrieve sources
LlamaIndexPipeline-->>AgenticChatPipeline: sources with image_paths
AgenticChatPipeline->>RuntimeData: validate and read images
RuntimeData-->>AgenticChatPipeline: image bytes
AgenticChatPipeline-->>User: multimodal LLM message
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/app/(auth)/register/page.tsx (1)
106-119: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win改邮箱/口令后未重置
codeSent,且重发冷却硬编码 60 秒。
- 验证码挑战在后端与"邮箱 + 当次口令哈希"绑定。用户已发码后修改邮箱或口令,
codeSent仍为true,提交会命中后端 400「验证码无效」,用户无法从提示中理解原因。setCountdown(60)与后端可配置的verification_resend_cooldown_seconds(30~3600)不一致,冷却被调大时前端会提前放开按钮并撞 429。🔁 建议在关键字段变更时清空挑战状态
+ function resetChallenge() { + setCodeSent(false); + setCode(""); + setNotice(""); + } + @@ value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + resetChallenge(); + }}Also applies to: 171-186
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/app/`(auth)/register/page.tsx around lines 106 - 119, 更新注册表单中的邮箱和口令变更处理(包括 email 输入的 setEmail 以及对应的口令更新逻辑):每次关键字段变化时清空 codeSent,并重置相关验证码挑战状态,避免继续提交旧验证码。将重发冷却的 setCountdown(60) 改为使用后端 verification_resend_cooldown_seconds 配置值,确保按钮状态与服务端冷却时间一致。
🧹 Nitpick comments (17)
tests/services/config/test_runtime_settings.py (1)
125-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win补齐邮箱验证相关环境变量的断言。
当前仅断言了自注册、是否需要邮箱验证和验证码 TTL;新增的冷却时间、最大尝试次数以及邮箱/IP 每小时限流配置仍未覆盖。建议同时断言这些键,避免
render_environment()的映射回归漏测。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/services/config/test_runtime_settings.py` around lines 125 - 127, Extend the environment assertions in the runtime settings test alongside DEEPTUTOR_EMAIL_VERIFICATION_REQUIRED and DEEPTUTOR_VERIFICATION_CODE_TTL_MINUTES to cover the new verification cooldown, maximum-attempts, and email/IP hourly rate-limit keys, asserting each rendered value matches its configured setting.docker-compose.litellm.yml (1)
13-13: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win不要在部署 overlay 中使用
main-latest。生产部署应固定到经过审核的版本或 immutable digest,否则镜像内容会随上游变更,导致不可复现部署和未经验证的运行时升级。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docker-compose.litellm.yml` at line 13, 更新 docker-compose.litellm.yml 中的 LITELLM_IMAGE 默认值,移除可变的 main-latest 标签,改用经过审核的固定版本标签或 immutable digest;保留通过环境变量覆盖镜像的能力。deeptutor/services/rag/pipelines/llamaindex/document_loader.py (2)
83-91: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win回退分支的
image_paths未做resolve(),与按页分支不一致。
_page_image_paths_from_blocks写入的是str(resolved),而这里直接用str(source.path)。若asset_dir是相对路径,下游_attach_rag_images的resolve()会基于进程 CWD 解析,可能落到运行时数据目录之外而被静默丢弃。建议统一写入绝对路径。♻️ 统一为绝对路径
extra_metadata={ - "image_paths": [str(source.path) for source in extracted_images], + "image_paths": [str(source.path.resolve()) for source in extracted_images], },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/services/rag/pipelines/llamaindex/document_loader.py` around lines 83 - 91, Update the fallback branch in the document-loading flow to store resolved absolute image paths in the `image_paths` metadata, matching `_page_image_paths_from_blocks`. Resolve each `source.path` before converting it to a string, while leaving the existing `_append_if_nonempty` behavior unchanged.
445-458: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value建议无条件排除
image_paths,并避免两个字段共享同一列表对象。按页分支在无图页会写入
"image_paths": [],此时未加入排除列表,空值仍会进入 embed/LLM 元数据文本。另外同一 list 实例同时传给两个excluded_*参数,若上游对其做原地修改会相互影响。♻️ 建议调整
- excluded_metadata_keys = [] - if metadata.get("image_paths"): - # Keep paths available for post-retrieval Vision attachments, - # but do not serialize long cache paths into the text used by - # LlamaIndex's embed/LLM metadata token budget. - excluded_metadata_keys.append("image_paths") + # Keep paths available for post-retrieval Vision attachments, but + # do not serialize long cache paths into the text used by + # LlamaIndex's embed/LLM metadata token budget. + excluded = ["image_paths"] if "image_paths" in metadata else [] documents.append( Document( text=text, metadata=metadata, - excluded_embed_metadata_keys=excluded_metadata_keys, - excluded_llm_metadata_keys=excluded_metadata_keys, + excluded_embed_metadata_keys=list(excluded), + excluded_llm_metadata_keys=list(excluded),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/services/rag/pipelines/llamaindex/document_loader.py` around lines 445 - 458, Update the Document construction in the document-loading flow to always exclude the "image_paths" metadata key, including when its value is an empty list. Provide separate list instances to excluded_embed_metadata_keys and excluded_llm_metadata_keys so mutations to one cannot affect the other.tests/agents/chat/test_rag_image_attachments.py (1)
11-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value建议补充上限与后缀白名单的用例。
当前覆盖了去重与目录逃逸两条主路径,但
RAG_IMAGE_MAX_ATTACHMENTS截断、RAG_IMAGE_MAX_BYTES超限跳过、以及非图片后缀(如.svg/🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/agents/chat/test_rag_image_attachments.py` around lines 11 - 66, 在现有 RAG 图片附件测试中补充用例,覆盖 RAG_IMAGE_MAX_ATTACHMENTS 达到上限时截断、超过 RAG_IMAGE_MAX_BYTES 的文件被跳过,以及 .svg、.pdf 等非白名单后缀被拒绝;分别断言返回的新增附件数量和 context.attachments 内容符合预期。web/components/visualize/VisualizationViewer.tsx (1)
172-182: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value用整段 HTML 作
key,并在每次onLoad重置高度会引入跳动。
key={prepared}把完整 HTML 字符串放进 React 的 key 比较路径,内容较大时开销无谓;srcDoc变化本身已会重新加载文档,如需强制重建可改用短哈希。另外setHeight(560)会在每次加载后把高度打回默认值,随后才被 iframe 上报的真实高度覆盖,用户可见一次高度跳变。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/components/visualize/VisualizationViewer.tsx` around lines 172 - 182, Update the iframe rendering in VisualizationViewer so key no longer uses the full prepared HTML string; use a short stable hash only if remounting is required, otherwise remove the key. In the onLoad handler, stop unconditionally resetting height to 560, while preserving load-error clearing and subsequent iframe-reported height behavior.deeptutor/agents/visualize/utils.py (1)
62-86: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win将 HTML JavaScript 同步校验放到异步请求路径之外
validate_visualization在VisualizeCapability.run的reviewing阶段被调用,其中 HTML 路径会为每个非空<script>执行一次同步subprocess.run(["node", "--check"], timeout=3)。恶意生成的多脚本 HTML 会阻塞事件循环最多3s × 脚本数。建议包一层asyncio.to_thread,并对校验脚本总数与总耗时设置上限;subprocess这里固定 argv + stdin 输入,不属于典型subprocess-from-requestCWE-78。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/agents/visualize/utils.py` around lines 62 - 86, Update _validate_inline_javascript and its validate_visualization call path so Node syntax checks run via asyncio.to_thread rather than blocking the event loop. Enforce limits on the total number of scripts checked and the aggregate validation duration, while preserving the current error-detail behavior and fixed argv/stdin subprocess invocation.Source: Linters/SAST tools
deeptutor/services/auth.py (1)
295-307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win每次令牌校验都会同步读取
users.json。
decode_token处于所有已认证请求的热路径上,新增的_load_users()会在事件循环线程里做磁盘 JSON 解析(并可能触发_write_users归一化回写)。禁用/撤销校验本身是对的,建议为用户存储加一层按 mtime 失效的短 TTL 缓存。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/services/auth.py` around lines 295 - 307, 为 decode_token 中的用户状态校验增加基于 users.json mtime 的短 TTL 缓存,避免每次认证请求都同步读取和解析文件。更新 _load_users 或其调用路径,使缓存仅在文件 mtime 变化或 TTL 过期时重新加载,并保留 disabled 与 email_verified 校验逻辑及必要的 _write_users 归一化行为。deeptutor/api/routers/auth.py (2)
556-560: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
email_verification_required=False会让注册整体返回 503,字段等价于"关闭注册"。配置项名字暗示"是否强制邮箱验证",实际取
False时两个端点都不可用,管理员可能误以为能得到免验证注册。建议在该配置的文档/UI 说明中明确它只允许为True,或直接改用self_registration_enabled单一开关表达。Also applies to: 630-634
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/api/routers/auth.py` around lines 556 - 560, 明确 email_verification_required 配置在相关注册端点中的实际语义:更新配置文档/UI,说明设为 False 会禁用公开注册并返回 503,而不是提供免验证注册;同时确保 startLine 556 和 630 附近的说明保持一致。不要改变现有注册禁用行为。
584-591: 🔒 Security & Privacy | 🔵 Trivial | ⚖️ Poor tradeoff响应耗时差可用于账号枚举。
已注册地址走的是零成本分支,未注册地址要执行 bcrypt 哈希并等待 SMTP 投递,两者响应时间量级差异明显,抵消了统一文案带来的防枚举效果。建议把发信改为后台任务(返回不等待投递),或对两条分支施加统一的最小响应时间。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/api/routers/auth.py` around lines 584 - 591, 调整注册处理流程,避免已注册与未注册邮箱因 bcrypt 和 SMTP 投递产生明显响应时差:让 send_verification_email 通过后台任务执行而不阻塞响应,并在包含 check_registration_rate_limit、hash_password 和 issue_challenge 的完整流程上施加统一的最小响应时间,确保两条分支都满足该耗时下限。deeptutor/services/email_verification.py (2)
276-293: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win过期挑战(含 bcrypt 口令哈希)只在下一次签发时才被清理。
长期没有新注册请求时,
pending_registrations中已过期的记录会无限期保留口令哈希。建议在consume_challenge(或一个轻量的定期清理入口)中同样执行DELETE FROM pending_registrations WHERE expires_at <= ?,缩短敏感数据留存窗口。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/services/email_verification.py` around lines 276 - 293, 在 consume_challenge 中加入基于当前时间执行的 pending_registrations 过期记录清理,复用现有的 expires_at 条件和参数化查询;确保即使没有新的签发请求,过期挑战及其口令哈希也会被及时删除,并保持现有挑战消费流程不变。
387-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffSMTP 配置绕过了集中式运行时设置,且 TLS/SSL 冲突被静默当作"未配置"。
其余部署参数统一由
RuntimeSettingsService归一化并导出(该模块文档明确"进程环境覆盖集中在此处理"),这里却直接读os.getenv,管理端无法查看/校验 SMTP 状态。此外configured在use_tls and use_ssl时返回False,路由只会回 503「暂不可用」,运维看不到真实原因——建议至少在这种冲突配置下打一条 error 日志。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/services/email_verification.py` around lines 387 - 414, 更新 smtp_config,使 SMTP 配置通过 RuntimeSettingsService 的集中式运行时设置读取和归一化,而不是直接调用 os.getenv;保留现有端口、超时和布尔值校验行为。针对 use_tls 与 use_ssl 同时启用的冲突配置,在配置校验或 configured 判定路径中记录明确的 error 日志,并保留现有不可用状态。tests/api/test_auth_registration.py (1)
113-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value建议改用
monkeypatch并补一个 429 用例。fixture 已经用
monkeypatch打桩了send_verification_email,这里再手工赋值/恢复会与之叠加(恢复的是桩而非真实实现),换成monkeypatch.setattr更清晰。另外冷却/限流映射到 429 +Retry-After是这条链路的关键分支,目前没有路由级断言。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/api/test_auth_registration.py` around lines 113 - 127, Update the existing-address test around auth_router.request_registration_code to use the pytest monkeypatch fixture’s set attribute instead of manually assigning and restoring verification_module.send_verification_email. Add a route-level test covering the cooldown/rate-limit branch, asserting HTTP 429 and the expected Retry-After header.deeptutor/multi_user/token_quota.py (2)
293-298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
booked表达式恒等于actual,条件分支可以删掉。
max(requested, actual)仅在actual > requested时求值,此时结果就是actual,因此整个三元表达式等价于booked = actual。保留冗余分支会让读者误以为存在别的分支语义。♻️ 建议简化
- requested = int(reservation["requested_tokens"]) - actual = max(0, int(actual_tokens)) - # The reservation includes an upper bound. If a provider reports a - # larger value than that bound, book the larger value so accounting - # never silently undercounts a call. - booked = max(requested, actual) if actual > requested else actual + requested = int(reservation["requested_tokens"]) + # Book exactly what the provider reported, even when it exceeds the + # reserved upper bound, so accounting never undercounts a call. + booked = max(0, int(actual_tokens))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/multi_user/token_quota.py` around lines 293 - 298, 简化 token 配额预订逻辑:在处理 requested_tokens 和 actual_tokens 的代码中,将 booked 的条件表达式直接改为使用 actual,保留现有的非负值处理及其余会计行为不变。
127-162: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win每次预留/结算都会新建连接并重跑建表脚本。
_connect()在每次reserve与finalize中执行PRAGMA journal_mode = WAL和完整的executescriptDDL,也就是每次 LLM 调用至少两次连接建立 + 两次 DDL。journal_mode切换需要短暂的数据库级锁,在并发请求下会加剧BEGIN IMMEDIATE的竞争。建议把 schema 初始化与 WAL 设置收敛为进程内一次性动作(用_schema_lock保护的_initialized标记),连接创建仍保持每次新建。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/multi_user/token_quota.py` around lines 127 - 162, Update _connect so schema initialization and PRAGMA journal_mode=WAL run only once per process, guarded by _schema_lock and a shared _initialized flag; keep creating a fresh SQLite connection for each call and retain per-connection settings such as row_factory and busy_timeout. Ensure concurrent callers cannot skip initialization, while preserving the existing unavailable-error handling.tests/multi_user/test_token_quota.py (1)
77-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win建议补充流式中断与失败路径的用例。
当前流式用例只覆盖“完整迭代到
StopAsyncIteration”。请补充:消费者提前break后未close()、以及流中途抛异常这两种情形,断言reserved_tokens已归还且consumed_tokens不为 0——这正是本 PR 中最容易漏账的两条路径(详见deeptutor/core/agentic/client.py与deeptutor/services/llm/factory.py的相关评论)。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/multi_user/test_token_quota.py` around lines 77 - 134, 在现有 test_stream_reservation_is_finalized_after_iteration 测试附近补充两个流式场景:消费者提前 break 且不调用 close(),以及 FakeStream 迭代过程中抛出异常;分别验证 token_usage 中 reserved_tokens 已归还为 0,并确认 consumed_tokens 已记录为非 0。复用现有 FakeCompletions、as_user 和临时数据库设置,覆盖 _TokenQuotaCompletions 的中断与失败清理路径。deeptutor/multi_user/grants.py (1)
83-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value建议复用
token_quota._normalize_limit而非重复夹断逻辑。上限
10_000_000_000已在deeptutor/multi_user/token_quota.py中定义为MAX_QUOTA_TOKENS,两处独立维护容易漂移。♻️ 建议重构
-from .token_quota import default_token_quota +from .token_quota import _normalize_limit as normalize_quota_limit, default_token_quotaraw_quota = payload.get("token_quota") if isinstance(raw_quota, dict): quota_defaults = default_token_quota() - normalized_quota: dict[str, int] = {} - for key, default in quota_defaults.items(): - try: - value = int(raw_quota.get(key, default)) - except (TypeError, ValueError): - value = default - normalized_quota[key] = max(0, min(value, 10_000_000_000)) - base["token_quota"] = normalized_quota + base["token_quota"] = { + key: normalize_quota_limit(raw_quota.get(key, default)) + for key, default in quota_defaults.items() + }注:若采纳,建议在
token_quota.py中将该函数改为公开名(如normalize_limit)以避免跨模块引用私有符号。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deeptutor/multi_user/grants.py` around lines 83 - 93, Update the token quota normalization in the payload handling flow to reuse token_quota’s existing _normalize_limit logic instead of duplicating the inline clamp and hard-coded maximum. Expose it as normalize_limit if needed for cross-module use, and apply it when assigning normalized_quota values while preserving the current type conversion and fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@deeptutor/agents/chat/agentic_pipeline.py`:
- Around line 481-516: 将附件数量限制从单次调用调整为整轮对话总量:在 _attach_rag_images 中结合
context.attachments 当前数量计算剩余容量,使本轮新增附件与已有附件合计不超过
RAG_IMAGE_MAX_ATTACHMENTS。保留现有去重、文件校验及 context.attachments.extend
行为,并确保达到总上限后不再添加附件。
In `@deeptutor/agents/visualize/utils.py`:
- Around line 68-86: Update the inline script validation flow around the script
extraction loop to parse each script’s type attribute, skip non-JavaScript types
such as application/json and text/template, validate classic scripts with the
existing Node check, and validate module scripts with --check and
--input-type=module. Preserve the current timeout, error handling, and
diagnostic behavior for supported JavaScript scripts.
In `@deeptutor/core/agentic/client.py`:
- Around line 139-147: Update _wrap_token_quota to always return a
_TokenQuotaClient instead of reading current_user_quota_policy during client
construction. Move the current-user quota decision to the
chat.completions.create interception path, where reserve_current_user_tokens()
is invoked only when the active request has a bounded non-admin policy, while
preserving unrestricted behavior otherwise.
- Around line 212-232: Update _TokenQuotaStream to proxy unhandled attributes,
such as response, to the wrapped _stream, and implement the async
context-manager protocol with __aenter__ and __aexit__. Preserve the existing
iteration and close behavior while ensuring async with usage delegates correctly
to the underlying stream.
In `@deeptutor/multi_user/token_quota.py`:
- Around line 182-276: Update TokenQuotaManager.reserve in
deeptutor/multi_user/token_quota.py (lines 182-276) to reclaim timed-out active
reservations before checking availability: mark them expired and decrement
reserved_tokens for their daily and monthly usage rows. Update _TokenQuotaStream
in deeptutor/core/agentic/client.py (lines 234-258) to add __aexit__ and ensure
object destruction or incomplete iteration invokes _finalize, covering consumers
that exit early or break; both sites require direct changes.
- Around line 262-267: 修复 reserve 与 finalize 中捕获 OSError/sqlite3.Error 的处理,确保
BEGIN IMMEDIATE 失败且事务未开启时,ROLLBACK 不会覆盖原始异常。更新两处 connection.execute("ROLLBACK")
调用,使回滚失败被安全忽略或仅在事务已开启时执行,并继续将原始异常包装为 TokenQuotaUnavailable;TokenQuotaExceeded
分支的现有行为保持不变。
In `@deeptutor/services/auth.py`:
- Around line 397-402: 用户查找未兼容历史大小写混合的邮箱键。抽取共用的规范化用户查找函数,先执行精确键匹配,未命中时遍历用户键并按
casefold() 比较;在 deeptutor/services/auth.py 的 authenticate(397-402)和
deeptutor/api/routers/auth.py 的
get_user_info(email)(573-581)中复用该函数,确保登录与账号存在性检查使用一致的查找逻辑。
In `@deeptutor/services/email_verification.py`:
- Around line 434-440: Update the email content in the verification-message flow
to use the configured verification_max_attempts value instead of the hardcoded 5
in both the Chinese and English attempt-limit text. Reuse the existing runtime
settings access used by the surrounding email verification logic, while
preserving the current message formatting and remaining_minutes behavior.
In `@deeptutor/services/llm/factory.py`:
- Around line 574-583: Update the streaming exception/finalization flow around
quota_lease.release() and the finally block so each lease is settled exactly
once in finally using the observed usage total, falling back to
quota_reservation when unavailable. Remove the exception-path release call and
preserve the existing error mapping and queue termination behavior.
In `@deeptutor/services/rag/pipelines/llamaindex/document_loader.py`:
- Around line 174-184: Update _page_image_paths_from_blocks to resolve relative
image paths using root / candidate first, preserving subdirectories such as
images/, then validate containment and file type; only fall back to root /
candidate.name when that lookup fails. In
tests/services/rag/test_llamaindex_document_loader.py lines 146-176, place
figure-1.png under asset_dir/images/ and retain assertions covering the nested
MinerU layout.
In `@docker-compose.ghcr.yml`:
- Around line 55-63: 为包含 DEEPTUTOR_SMTP_* 环境变量的服务更新 volumes 配置,像主 Compose 一样挂载
./data:/app/data,确保 data/system、data/users 和 data/partners 等完整数据树在容器重建后持久化。
In `@docker-compose.litellm.yml`:
- Around line 20-25: Update the LITELLM_DATABASE_URL and LITELLM_MASTER_KEY
entries in the Compose environment configuration to use required-variable
expansion with an appropriate required message, so deployment fails immediately
when either value is missing; leave the optional upstream configuration defaults
unchanged.
In `@ops/litellm/README.md`:
- Around line 10-23: Update the README instructions around virtual-key
generation and admin LLM profile configuration so the shared admin profile uses
a service/admin-scoped key without a user_id. Keep user-scoped keys tied to
individual user requests or grants, and explicitly preserve the requirement that
the master key is never exposed in user grants or browser responses.
In `@tests/agents/visualize/test_utils.py`:
- Around line 1-22: Update the tests around validate_visualization to detect
whether the node executable is available and explicitly skip the
inline-JavaScript validation tests when it is missing, while retaining their
existing assertions when Node is installed. Use the test framework’s skip
mechanism and apply it to both
test_html_validation_rejects_inline_javascript_syntax_errors and
test_html_validation_accepts_valid_inline_javascript.
In `@web/components/visualize/VisualizationViewer.tsx`:
- Around line 183-189: Replace the iframe onError-based failure handling in
VisualizationViewer with a timeout-driven bridge health check after onLoad: wait
for the expected dt:visualize-height or error report, then set loadError to the
existing “Failed to load visualization” message when no valid report arrives.
Update the related load/error state and cleanup logic while preserving the
current failure UI.
In `@web/features/multi-user/components/GrantEditor.tsx`:
- Around line 290-300: 更新 setTokenQuota 以单独处理空输入,避免 Number("") 将配额立即设为
0(无限制);空值应保留当前 token_quota[key](或使用 NaN 表示未完成输入),非空值继续执行现有的有限数值、非负及向下取整逻辑。
In `@web/locales/zh/app.json`:
- Line 2227: Update the Chinese translation value for the “Failed to load
visualization” localization key to use the established “可视化” terminology instead
of “演示”, while preserving the existing “加载失败” meaning.
---
Outside diff comments:
In `@web/app/`(auth)/register/page.tsx:
- Around line 106-119: 更新注册表单中的邮箱和口令变更处理(包括 email 输入的 setEmail
以及对应的口令更新逻辑):每次关键字段变化时清空 codeSent,并重置相关验证码挑战状态,避免继续提交旧验证码。将重发冷却的
setCountdown(60) 改为使用后端 verification_resend_cooldown_seconds
配置值,确保按钮状态与服务端冷却时间一致。
---
Nitpick comments:
In `@deeptutor/agents/visualize/utils.py`:
- Around line 62-86: Update _validate_inline_javascript and its
validate_visualization call path so Node syntax checks run via asyncio.to_thread
rather than blocking the event loop. Enforce limits on the total number of
scripts checked and the aggregate validation duration, while preserving the
current error-detail behavior and fixed argv/stdin subprocess invocation.
In `@deeptutor/api/routers/auth.py`:
- Around line 556-560: 明确 email_verification_required
配置在相关注册端点中的实际语义:更新配置文档/UI,说明设为 False 会禁用公开注册并返回 503,而不是提供免验证注册;同时确保 startLine
556 和 630 附近的说明保持一致。不要改变现有注册禁用行为。
- Around line 584-591: 调整注册处理流程,避免已注册与未注册邮箱因 bcrypt 和 SMTP 投递产生明显响应时差:让
send_verification_email 通过后台任务执行而不阻塞响应,并在包含
check_registration_rate_limit、hash_password 和 issue_challenge
的完整流程上施加统一的最小响应时间,确保两条分支都满足该耗时下限。
In `@deeptutor/multi_user/grants.py`:
- Around line 83-93: Update the token quota normalization in the payload
handling flow to reuse token_quota’s existing _normalize_limit logic instead of
duplicating the inline clamp and hard-coded maximum. Expose it as
normalize_limit if needed for cross-module use, and apply it when assigning
normalized_quota values while preserving the current type conversion and
fallback behavior.
In `@deeptutor/multi_user/token_quota.py`:
- Around line 293-298: 简化 token 配额预订逻辑:在处理 requested_tokens 和 actual_tokens
的代码中,将 booked 的条件表达式直接改为使用 actual,保留现有的非负值处理及其余会计行为不变。
- Around line 127-162: Update _connect so schema initialization and PRAGMA
journal_mode=WAL run only once per process, guarded by _schema_lock and a shared
_initialized flag; keep creating a fresh SQLite connection for each call and
retain per-connection settings such as row_factory and busy_timeout. Ensure
concurrent callers cannot skip initialization, while preserving the existing
unavailable-error handling.
In `@deeptutor/services/auth.py`:
- Around line 295-307: 为 decode_token 中的用户状态校验增加基于 users.json mtime 的短 TTL
缓存,避免每次认证请求都同步读取和解析文件。更新 _load_users 或其调用路径,使缓存仅在文件 mtime 变化或 TTL 过期时重新加载,并保留
disabled 与 email_verified 校验逻辑及必要的 _write_users 归一化行为。
In `@deeptutor/services/email_verification.py`:
- Around line 276-293: 在 consume_challenge 中加入基于当前时间执行的 pending_registrations
过期记录清理,复用现有的 expires_at 条件和参数化查询;确保即使没有新的签发请求,过期挑战及其口令哈希也会被及时删除,并保持现有挑战消费流程不变。
- Around line 387-414: 更新 smtp_config,使 SMTP 配置通过 RuntimeSettingsService
的集中式运行时设置读取和归一化,而不是直接调用 os.getenv;保留现有端口、超时和布尔值校验行为。针对 use_tls 与 use_ssl
同时启用的冲突配置,在配置校验或 configured 判定路径中记录明确的 error 日志,并保留现有不可用状态。
In `@deeptutor/services/rag/pipelines/llamaindex/document_loader.py`:
- Around line 83-91: Update the fallback branch in the document-loading flow to
store resolved absolute image paths in the `image_paths` metadata, matching
`_page_image_paths_from_blocks`. Resolve each `source.path` before converting it
to a string, while leaving the existing `_append_if_nonempty` behavior
unchanged.
- Around line 445-458: Update the Document construction in the document-loading
flow to always exclude the "image_paths" metadata key, including when its value
is an empty list. Provide separate list instances to
excluded_embed_metadata_keys and excluded_llm_metadata_keys so mutations to one
cannot affect the other.
In `@docker-compose.litellm.yml`:
- Line 13: 更新 docker-compose.litellm.yml 中的 LITELLM_IMAGE 默认值,移除可变的 main-latest
标签,改用经过审核的固定版本标签或 immutable digest;保留通过环境变量覆盖镜像的能力。
In `@tests/agents/chat/test_rag_image_attachments.py`:
- Around line 11-66: 在现有 RAG 图片附件测试中补充用例,覆盖 RAG_IMAGE_MAX_ATTACHMENTS 达到上限时截断、超过
RAG_IMAGE_MAX_BYTES 的文件被跳过,以及 .svg、.pdf 等非白名单后缀被拒绝;分别断言返回的新增附件数量和
context.attachments 内容符合预期。
In `@tests/api/test_auth_registration.py`:
- Around line 113-127: Update the existing-address test around
auth_router.request_registration_code to use the pytest monkeypatch fixture’s
set attribute instead of manually assigning and restoring
verification_module.send_verification_email. Add a route-level test covering the
cooldown/rate-limit branch, asserting HTTP 429 and the expected Retry-After
header.
In `@tests/multi_user/test_token_quota.py`:
- Around line 77-134: 在现有 test_stream_reservation_is_finalized_after_iteration
测试附近补充两个流式场景:消费者提前 break 且不调用 close(),以及 FakeStream 迭代过程中抛出异常;分别验证 token_usage 中
reserved_tokens 已归还为 0,并确认 consumed_tokens 已记录为非 0。复用现有 FakeCompletions、as_user
和临时数据库设置,覆盖 _TokenQuotaCompletions 的中断与失败清理路径。
In `@tests/services/config/test_runtime_settings.py`:
- Around line 125-127: Extend the environment assertions in the runtime settings
test alongside DEEPTUTOR_EMAIL_VERIFICATION_REQUIRED and
DEEPTUTOR_VERIFICATION_CODE_TTL_MINUTES to cover the new verification cooldown,
maximum-attempts, and email/IP hourly rate-limit keys, asserting each rendered
value matches its configured setting.
In `@web/components/visualize/VisualizationViewer.tsx`:
- Around line 172-182: Update the iframe rendering in VisualizationViewer so key
no longer uses the full prepared HTML string; use a short stable hash only if
remounting is required, otherwise remove the key. In the onLoad handler, stop
unconditionally resetting height to 560, while preserving load-error clearing
and subsequent iframe-reported height behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 931d9141-025f-47cf-9dc8-006cd4ac5a14
📒 Files selected for processing (47)
.env.exampleCONTAINERIZATION.mdDockerfile.runnerREADME.mdcompose.litellm.yamlcompose.yamldeeptutor/agents/chat/agent_loop.pydeeptutor/agents/chat/agentic_pipeline.pydeeptutor/agents/visualize/prompts/en/code_generator_agent.yamldeeptutor/agents/visualize/prompts/zh/code_generator_agent.yamldeeptutor/agents/visualize/utils.pydeeptutor/api/routers/auth.pydeeptutor/core/agentic/client.pydeeptutor/multi_user/grants.pydeeptutor/multi_user/identity.pydeeptutor/multi_user/models.pydeeptutor/multi_user/token_quota.pydeeptutor/services/auth.pydeeptutor/services/config/runtime_settings.pydeeptutor/services/email_verification.pydeeptutor/services/llm/factory.pydeeptutor/services/rag/pipelines/llamaindex/document_loader.pydeeptutor/services/rag/pipelines/llamaindex/pipeline.pydeeptutor/services/sandbox/runner/server.pydeeptutor/services/sandbox/service.pydocker-compose.ghcr.ymldocker-compose.litellm.ymldocker-compose.ymlops/litellm/README.mdops/litellm/config.yamltests/agents/chat/test_rag_image_attachments.pytests/agents/visualize/test_utils.pytests/api/test_auth_registration.pytests/multi_user/test_token_quota.pytests/multi_user/test_tool_access.pytests/services/config/test_runtime_settings.pytests/services/rag/test_llamaindex_document_loader.pytests/services/sandbox/test_sandbox.pytests/services/test_email_verification.pyweb/app/(auth)/login/page.tsxweb/app/(auth)/register/page.tsxweb/components/visualize/VisualizationViewer.tsxweb/features/multi-user/components/GrantEditor.tsxweb/features/multi-user/types.tsweb/lib/auth.tsweb/locales/en/app.jsonweb/locales/zh/app.json
| for raw_path in candidate_paths: | ||
| if len(added) >= RAG_IMAGE_MAX_ATTACHMENTS: | ||
| break | ||
| try: | ||
| path = Path(raw_path).expanduser().resolve() | ||
| if not path.is_relative_to(runtime_root): | ||
| logger.warning("skipping RAG image outside runtime data root: %s", path) | ||
| continue | ||
| if path.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}: | ||
| continue | ||
| if not path.is_file() or path.stat().st_size > RAG_IMAGE_MAX_BYTES: | ||
| logger.warning("skipping missing or oversized RAG image: %s", path) | ||
| continue | ||
| path_key = str(path) | ||
| if path_key in known_paths: | ||
| continue | ||
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") | ||
| mime_type = mimetypes.guess_type(path.name)[0] or "image/png" | ||
| attachment_id = "rag-image-" + hashlib.sha256(path_key.encode()).hexdigest()[:16] | ||
| added.append( | ||
| Attachment( | ||
| type="image", | ||
| base64=encoded, | ||
| filename=path.name, | ||
| mime_type=mime_type, | ||
| id=attachment_id, | ||
| ) | ||
| ) | ||
| known_paths.add(path_key) | ||
| except (OSError, ValueError) as exc: | ||
| logger.warning("failed to load retrieved RAG image %s: %s", raw_path, exc) | ||
|
|
||
| if added: | ||
| context.attachments.extend(added) | ||
| context.metadata["_rag_image_paths"] = sorted(known_paths) | ||
| return added |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
每轮上限只约束单次调用,跨轮附件可无界累积。
RAG_IMAGE_MAX_ATTACHMENTS 只限制单次 _attach_rag_images 的新增数量,而 agent_loop.py 在每一轮工具分发后都会调用一次。若模型持续检索到新页面图片,context.attachments 每轮最多再增 6 张(每张上限 8MB),并且默认路径 _prepare_messages_with_attachments(messages, context) 会把 context.attachments 全量注入,造成请求体、内存与 token 成本随轮数膨胀。建议以整轮对话的总附件数为封顶。
♻️ 建议按整轮总量封顶
added: list[Attachment] = []
+ existing_rag_images = len(known_paths)
candidate_paths: list[str] = []
@@
for raw_path in candidate_paths:
- if len(added) >= RAG_IMAGE_MAX_ATTACHMENTS:
+ if existing_rag_images + len(added) >= RAG_IMAGE_MAX_ATTACHMENTS:
break📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for raw_path in candidate_paths: | |
| if len(added) >= RAG_IMAGE_MAX_ATTACHMENTS: | |
| break | |
| try: | |
| path = Path(raw_path).expanduser().resolve() | |
| if not path.is_relative_to(runtime_root): | |
| logger.warning("skipping RAG image outside runtime data root: %s", path) | |
| continue | |
| if path.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}: | |
| continue | |
| if not path.is_file() or path.stat().st_size > RAG_IMAGE_MAX_BYTES: | |
| logger.warning("skipping missing or oversized RAG image: %s", path) | |
| continue | |
| path_key = str(path) | |
| if path_key in known_paths: | |
| continue | |
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") | |
| mime_type = mimetypes.guess_type(path.name)[0] or "image/png" | |
| attachment_id = "rag-image-" + hashlib.sha256(path_key.encode()).hexdigest()[:16] | |
| added.append( | |
| Attachment( | |
| type="image", | |
| base64=encoded, | |
| filename=path.name, | |
| mime_type=mime_type, | |
| id=attachment_id, | |
| ) | |
| ) | |
| known_paths.add(path_key) | |
| except (OSError, ValueError) as exc: | |
| logger.warning("failed to load retrieved RAG image %s: %s", raw_path, exc) | |
| if added: | |
| context.attachments.extend(added) | |
| context.metadata["_rag_image_paths"] = sorted(known_paths) | |
| return added | |
| added: list[Attachment] = [] | |
| existing_rag_images = len(known_paths) | |
| candidate_paths: list[str] = [] | |
| for raw_path in candidate_paths: | |
| if existing_rag_images + len(added) >= RAG_IMAGE_MAX_ATTACHMENTS: | |
| break | |
| try: | |
| path = Path(raw_path).expanduser().resolve() | |
| if not path.is_relative_to(runtime_root): | |
| logger.warning("skipping RAG image outside runtime data root: %s", path) | |
| continue | |
| if path.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}: | |
| continue | |
| if not path.is_file() or path.stat().st_size > RAG_IMAGE_MAX_BYTES: | |
| logger.warning("skipping missing or oversized RAG image: %s", path) | |
| continue | |
| path_key = str(path) | |
| if path_key in known_paths: | |
| continue | |
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") | |
| mime_type = mimetypes.guess_type(path.name)[0] or "image/png" | |
| attachment_id = "rag-image-" + hashlib.sha256(path_key.encode()).hexdigest()[:16] | |
| added.append( | |
| Attachment( | |
| type="image", | |
| base64=encoded, | |
| filename=path.name, | |
| mime_type=mime_type, | |
| id=attachment_id, | |
| ) | |
| ) | |
| known_paths.add(path_key) | |
| except (OSError, ValueError) as exc: | |
| logger.warning("failed to load retrieved RAG image %s: %s", raw_path, exc) | |
| if added: | |
| context.attachments.extend(added) | |
| context.metadata["_rag_image_paths"] = sorted(known_paths) | |
| return added |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@deeptutor/agents/chat/agentic_pipeline.py` around lines 481 - 516,
将附件数量限制从单次调用调整为整轮对话总量:在 _attach_rag_images 中结合 context.attachments
当前数量计算剩余容量,使本轮新增附件与已有附件合计不超过 RAG_IMAGE_MAX_ATTACHMENTS。保留现有去重、文件校验及
context.attachments.extend 行为,并确保达到总上限后不再添加附件。
| scripts = re.findall(r"<script(?:\s[^>]*)?>([\s\S]*?)</script>", html, re.IGNORECASE) | ||
| for script in scripts: | ||
| if not script.strip(): | ||
| continue | ||
| try: | ||
| result = subprocess.run( | ||
| [node, "--check"], | ||
| input=script, | ||
| text=True, | ||
| capture_output=True, | ||
| timeout=3, | ||
| check=False, | ||
| ) | ||
| except (OSError, subprocess.TimeoutExpired): | ||
| return True, "" | ||
| if result.returncode != 0: | ||
| detail = (result.stderr or result.stdout or "JavaScript syntax error").strip() | ||
| return False, f"Inline JavaScript syntax error: {detail[-500:]}" | ||
| return True, "" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
node --check read from stdin --input-type module support
💡 Result:
Node.js supports checking syntax from stdin using the --check (or -c) flag by passing - as the filename [1][2]. Regarding the combination of --check with --input-type=module: 1. The --check flag performs a syntax check without executing the code [1][2]. 2. While --input-type=module is used to instruct Node.js to interpret input (from --eval or stdin) as an ES module, this flag is specifically designed for execution [3][4]. 3. The --check flag and --input-type flag are generally intended to be used for different purposes (syntax validation vs. execution configuration) [1][3]. 4. Notably, TypeScript documentation explicitly states that "TypeScript syntax is unsupported in... --check" [5]. If you attempt to use them together, the syntax check will generally process the input based on the default behavior for stdin, as --check typically exits before the execution-related module type configuration fully applies to module-specific runtime features [1][3]. In practice, if you need to validate code that relies on ES module syntax, you should typically use a linting tool or a dedicated parser rather than relying on --check with --input-type=module, as --check is a basic syntax validator [2].
Citations:
- 1: https://nodejs.org/api/cli.html
- 2: https://www.javaspring.net/blog/syntax-check-for-javascript-using-command/
- 3: https://nodejs.org/dist/latest/docs/api/cli.html
- 4: https://beta.docs.nodejs.org/cli.html
- 5: https://nodejs.org/docs/latest/api/typescript.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== node availability/version =="
if command -v node >/dev/null 2>&1; then
node --version
else
echo "node not available"
fi
echo "== locate target file =="
fd -a 'utils.py$' . | sed 's#^\./##' | grep -E 'deeptutor/agents/visualize/utils.py|utils.py' || true
echo "== target snippet =="
if [ -f deeptutor/agents/visualize/utils.py ]; then
nl -ba deeptutor/agents/visualize/utils.py | sed -n '1,120p'
else
echo "target file not found"
fi
echo "== node --check inputs =="
node_exists=$(command -v node || true)
if [ -n "${node_exists}" ]; then
cat >/tmp/node_check_probe.js <<'JS'
const {execFileSync} = require('node:child_process');
const inputs = [
['classic', 'import x from "x";'],
['classic', 'export default 1;'],
['module', 'import x from "x";'],
['module', 'export default 1;'],
['application/json', '{"ok": true}'],
['text/template', '<div>foo</div>'],
];
for (const [type, code] of inputs) {
let cmd = ['--check'];
if (type === 'module') cmd.push('--input-type=module');
let exit = 1; let stderr = '', stdout = '';
try {
const out = execFileSync(process.argv[0], [...cmd], {input: code, encoding: 'utf8'});
exit = 0; stdout = out.stdout || ''; stderr = out.stderr || '';
} catch (e) {
stderr += (e.stderr || '') + (e.stdout || '');
exit = e.status || 1;
}
console.log(JSON.stringify({type, code, exit, stderr: stderr.substring(0, 300), stdout: stdout.substring(0, 200)}));
}
JS
node /tmp/node_check_probe.js
fiRepository: hubo1989/DeepTutor
Length of output: 857
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file lines 1-130 =="
awk '{printf "%5d\t%s\n", NR, $0}' deeptutor/agents/visualize/utils.py | sed -n '1,130p'
echo "== Node syntax probe =="
node - <<'JS'
const {execFileSync} = require('node:child_process');
const inputs = [
['classic', 'import x from "x";'],
['classic', 'export default 1;'],
['module', 'import x from "x";'],
['module', 'export default 1;'],
['application/json', '{"ok": true}'],
['text/template', '<div>foo</div>'],
];
for (const [type, code] of inputs) {
let cmd = ['--check'];
if (type === 'module') cmd.push('--input-type=module');
let exit = 1; let stderr = '', stdout = '';
try {
const out = execFileSync(process.argv[0], [...cmd], {input: code, encoding: 'utf8'});
exit = 0; stdout = out.stdout || ''; stderr = out.stderr || '';
} catch (e) {
stderr += (e.stderr || '') + (e.stdout || '');
exit = e.status || 1;
}
console.log(`${type}\t${exit}\t${stderr.substring(0, 200).replace(/\n/g, ' ')}`);
}
JS
echo "== related tests/use mentions =="
rg -n "check_js_syntax|JavaScript syntax error|node --check|type=\"module\"|application/json|text/template|<script" deeptutor tests || trueRepository: hubo1989/DeepTutor
Length of output: 12710
🌐 Web query:
Node.js CLI --check stdin input-type=module documentation checkSyntax stdin
💡 Result:
In Node.js, the --check (or -c) command-line flag is used to syntax check a script without executing it [1][2]. When you wish to perform this check on code provided via standard input (stdin), you can use the - flag as the filename argument [3][4]. To verify the syntax of code piped from stdin as an ES module, you can combine the --check flag with the --input-type=module flag [4][5]. Example command structure: echo "import { EventEmitter } from 'events'; export const x = 1;" | node --check --input-type=module - Key Points: - The --check flag parses the code and checks for syntax errors, exiting with a non-zero status code if errors are found, without running the actual code [1][3][6]. - The - argument tells Node.js to read the script from stdin [3][4]. - The --input-type=module flag forces Node.js to interpret the incoming stdin content as an ES module [4][5]. This is useful because it ensures that features like import and export are correctly parsed during the syntax check [7][5]. - Without --input-type=module, Node.js may attempt to detect the module system, or default to CommonJS, which could lead to syntax errors if ES module syntax is present [4][7][5].
Citations:
- 1: https://nodejs.org/dist/latest/docs/api/cli.html
- 2: https://nodejs.org/api/cli.html
- 3: https://www.javaspring.net/blog/syntax-check-for-javascript-using-command/
- 4: https://nodejs.org/docs/latest-v24.x/api/cli.html
- 5: https://nodejs.org/api/cli.md
- 6: nodejs/node@3209a8ebf3
- 7: https://nodejs.org/api/esm.html
区分 JS 与非 JS/模块的 <script> 块。
当前正则会把所有 </script> 都送入 node --check,导致 <script type="module"> 中的 import/export,以及 <script type="application/json">、type="text/template" 等非 JS 内容被误判为语法错误。建议解析 type 属性:跳过非标准脚本;对经典脚本使用 --check,对 module 使用 --check --input-type=module。
🧰 Tools
🪛 ast-grep (0.45.0)
[error] 72-79: Command coming from incoming request
Context: subprocess.run(
[node, "--check"],
input=script,
text=True,
capture_output=True,
timeout=3,
check=False,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@deeptutor/agents/visualize/utils.py` around lines 68 - 86, Update the inline
script validation flow around the script extraction loop to parse each script’s
type attribute, skip non-JavaScript types such as application/json and
text/template, validate classic scripts with the existing Node check, and
validate module scripts with --check and --input-type=module. Preserve the
current timeout, error handling, and diagnostic behavior for supported
JavaScript scripts.
| def _wrap_token_quota(client: Any) -> Any: | ||
| """Wrap every generated client so all capability calls share one gate.""" | ||
| from deeptutor.multi_user.token_quota import current_user_quota_policy | ||
|
|
||
| # Preserve the native SDK/adapter shape for the single-user and admin | ||
| # paths. Only a bounded non-admin grant needs the interception layer. | ||
| if current_user_quota_policy() is None: | ||
| return client | ||
| return _TokenQuotaClient(client) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) build_openai_client 的返回值是否被缓存/跨请求复用
rg -nP -C6 '\bbuild_openai_client\s*\(' --type=py
# 2) get_current_user 在无上下文时的行为
ast-grep run --pattern 'def get_current_user($$$) { $$$ }' --lang python deeptutor/multi_user/context.py
rg -nP -C8 'def get_current_user' deeptutor/multi_user/context.pyRepository: hubo1989/DeepTutor
Length of output: 156
🏁 Script executed:
#!/bin/bash
set +e
echo "== candidate files =="
fd -a 'client.py|token_quota.py|context.py|agentic_pipeline.py' . | sed 's#^\./##'
echo "== build_openai_client references =="
rg -n -C 8 'build_openai_client' . || true
echo "== get_current_user refs =="
rg -n -C 8 'get_current_user|current_user_quota_policy|_wrap_token_quota|_TokenQuotaClient|_TokenQuotaStream' . || trueRepository: hubo1989/DeepTutor
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deeptutor/core/agentic/client.py relevant section =="
sed -n '1,290p' deeptutor/core/agentic/client.py | cat -n
echo "== deeptutor/agents/chat/agentic_pipeline.py _client_config/usages relevant section =="
sed -n '1,100p' deeptutor/agents/chat/agentic_pipeline.py | cat -n
sed -n '310,335p' deeptutor/agents/chat/agentic_pipeline.py | cat -n
sed -n '1330,1350p' deeptutor/agents/chat/agentic_pipeline.py | cat -n
echo "== deeptutor/multi_user/token_quota.py relevant section =="
sed -n '1,320p' deeptutor/multi_user/token_quota.py | cat -n
echo "== deeptutor/multi_user/context.py relevant section =="
sed -n '1,180p' deeptutor/multi_user/context.py | cat -n
echo "== focused references in production code =="
rg -n '\brun\(|async with|build_openai_client\(|self\._client_config|_client_config\s*=' --type=py | rg 'deeptutor/(agents|capabilities)/|test_[^/]+\.py' || trueRepository: hubo1989/DeepTutor
Length of output: 34411
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== current_user_quota_policy definitions/usages =="
rg -n -C 12 'def current_user_quota_policy|current_user_quota_policy\(' --type=py | rg 'deeptutor/(multi_user|core|agents|capabilities)/|test_|\.md$' || true
echo "== local_admin_user/CurrentUser definitions =="
sed -n '1,220p' deeptutor/multi_user/models.py | cat -n
echo "== pipeline/config allocation surrounding _client_config =="
rg -n -C 12 'AgenticChatPipeline\(|AgenticResearch|Question|llm_config|_client_config|client_config|build_openai_client' deeptutor --type=py | rg 'deeptutor/(agents|capabilities|core|services|runtime)/' | head -n 240
echo "== async context manager/Stream access usages =="
rg -n -C 4 'async with .*chat\.completions|chat\.completions\.create\(|\.response|\.text_stream|\.close\(\)|aclose\(' --type=py | rg 'deeptutor/(agents|capabilities|core)/|test_' | head -n 240 || trueRepository: hubo1989/DeepTutor
Length of output: 207
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== all Python refs: current_user_quota_policy/get_current_user/build_openai_client =="
rg -n -C 5 'current_user_quota_policy|get_current_user\(|get_current_user_or_none|build_openai_client|_client_config\s*=' --type=py | head -n 300
echo "== files mentioning quota/current-user context =="
rg -l 'quota|quota_policy|current_user|local_admin_user' . --glob '*.py' | sed -n '1,120p'
echo "== inspect token_quota remainder =="
wc -l deeptutor/multi_user/token_quota.py
sed -n '320,520p' deeptutor/multi_user/token_quota.py | cat -nRepository: hubo1989/DeepTutor
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== current_user_quota_policy literal across all files =="
rg -n -C 8 'current_user_quota_policy' . || true
echo "== current_user_quota_policy as variable/attribute across all files =="
rg -n -C 5 'current_user_quota|user_quota_policy' . || true
echo "== inspect token_quota.py line count and remaining relevant section =="
wc -l deeptutor/multi_user/token_quota.py
sed -n '320,620p' deeptutor/multi_user/token_quota.py | cat -n
echo "== inspect client imports/top-level =="
sed -n '1,160p' deeptutor/core/agentic/client.py | cat -n
echo "== inspect token_quota imports/top-level =="
sed -n '1,80p' deeptutor/multi_user/token_quota.py | cat -nRepository: hubo1989/DeepTutor
Length of output: 20655
配额门控不要依赖构建时刻的用户上下文。
build_openai_client() 在 deeptutor/agents/chat/agentic_pipeline.py:1345 这种路径中直接由 self._client_config 构建,而 _wrap_token_quota 会立即读取 current_user_quota_policy()。一旦构建发生在 admin/无上下文环境,受限用户后续复用该 pipeline/adapter 就会绕过拦截;如果在请求上下文外抛出异常还会让客户端构建失败。通常应总是返回 _TokenQuotaClient,在 chat.completions.create 时再按当前用户决定是否 reserve_current_user_tokens()。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@deeptutor/core/agentic/client.py` around lines 139 - 147, Update
_wrap_token_quota to always return a _TokenQuotaClient instead of reading
current_user_quota_policy during client construction. Move the current-user
quota decision to the chat.completions.create interception path, where
reserve_current_user_tokens() is invoked only when the active request has a
bounded non-admin policy, while preserving unrestricted behavior otherwise.
| class _TokenQuotaStream: | ||
| def __init__( | ||
| self, | ||
| stream: Any, | ||
| *, | ||
| lease: Any, | ||
| requested_tokens: int, | ||
| prompt_estimate: int, | ||
| output_estimate: int, | ||
| ) -> None: | ||
| self._stream = stream | ||
| self._lease = lease | ||
| self._requested_tokens = requested_tokens | ||
| self._prompt_estimate = prompt_estimate | ||
| self._output_estimate = output_estimate | ||
| self._usage_total = 0 | ||
| self._output_chars = 0 | ||
| self._finalized = False | ||
|
|
||
| def __aiter__(self) -> "_TokenQuotaStream": | ||
| return self |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 检查是否存在以 async with / .response 等方式消费 chat.completions.create(stream=True) 的调用点
rg -nP -C6 'async with .*completions\.create' --type=py
rg -nP -C6 'completions\.create\(' --type=py | rg -n 'stream'Repository: hubo1989/DeepTutor
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -u
echo "== files =="
git ls-files | sed -n '1,120p'
echo "== client outline =="
ast-grep outline deeptutor/core/agentic/client.py || true
echo "== relevant client section =="
sed -n '1,340p' deeptutor/core/agentic/client.py | cat -n
echo "== search TokenQuota and wrapper usages =="
rg -n -C5 'TokenQuota|TokenQuotaStream|_TokenQuotaStream|quota' deeptutor/core/agentic/client.py --type=py
echo "== search OpenAI completions/create stream usage =="
rg -n -C4 'chat\.completions|openai\.chat|create\(|stream\s*=\s*True|AsyncStream|AsyncClient|AsyncOpenAI|response|\.close\('\ --type=py | sed -n '1,240p'Repository: hubo1989/DeepTutor
Length of output: 25898
🏁 Script executed:
#!/bin/bash
set -u
echo "== rest of client.py =="
sed -n '340,560p' deeptutor/core/agentic/client.py | cat -n
echo "== all create/usage/response/stream references in repo =="
rg -n -C3 '\b(create\(|stream|AsyncStream|\.response|\.close\(|chat\.completions|completions\.create)\b' --type=py | sed -n '1,260p'
echo "== dependency/version for openai =="
git ls-files | rg -n 'pyproject\.toml|requirements.*txt|poetry\.lock|uv\.lock|ruff\.toml' | sed -n '1,80p'
for f in $(git ls-files | rg 'pyproject\.toml|requirements.*txt|poetry\.lock|uv\.lock|ruff\.toml'); do
echo "--- $f"
rg -n 'openai|httpx|pydantic' "$f"
doneRepository: hubo1989/DeepTutor
Length of output: 9321
🌐 Web query:
OpenAI Python AsyncStream stream.response close async with __aenter__ __aexit__
💡 Result:
In the OpenAI Python library, the AsyncStream class (and related streaming wrappers like AsyncChatCompletionStream) is designed to manage its own lifecycle [1][2]. It implements the asynchronous context manager protocol (aenter and aexit) to ensure that the underlying network connection is closed properly [1][2]. When you use an async stream with the async with syntax, aexit automatically calls the close method, which in turn calls await response.aclose on the underlying HTTP response [1][2]. This ensures the connection is released and returned to the connection pool [1][2]. Key technical details: - Automatic Cleanup: If you read the stream to completion (e.g., iterating through all chunks until the end), the library generally manages closing the response [1][2]. However, if you break out of the loop early, the aexit handler (or your explicit call to close/aclose) is essential to prevent resource leaks [1][3]. - Context Manager: Using async with is the recommended pattern [1][2]. async with client.chat.completions.create(...) as stream: async for chunk in stream:... - Manual Closing: The AsyncStream class provides a close method [1][2]. Recent versions of the library also include an aclose method to comply with standard PEP 525 async generator cleanup conventions, ensuring compatibility with various instrumentation libraries [4][5]. - Important Note on Connection Pooling: In older versions of the library, failing to close the stream explicitly could lead to connection pool exhaustion [6]. While modern versions handle this more robustly via the context manager and the finally blocks inside the iterator logic, using the async with context manager remains the safest and most idiomatic way to handle streaming responses [1][2].
Citations:
- 1: https://github.com/openai/openai-python/blob/main/src/openai/_streaming.py
- 2: https://github.com/openai/openai-python/blob/main/src/openai/lib/streaming/chat/_completions.py
- 3: **Streaming: connection force-closed (TCP FIN) after
[DONE]SSE event because chunked terminator is not drained — regression from6132922c** openai/openai-python#3440 - 4: fix: add aclose() method to AsyncStream for PEP 525 compliance openai/openai-python#2880
- 5: AsyncStream missing aclose() causes AttributeError in streaming structured output openai/openai-python#2853
- 6: The connection is not returned to the httpx pool when using a stream openai/openai-python#763
🌐 Web query:
site:github.com/openai/openai-python AsyncStream __getattr__ response close aclose
💡 Result:
The AttributeError: 'AsyncStream' object has no attribute 'aclose' typically arises when using the OpenAI Python SDK because the AsyncStream class previously exposed only a close method and lacked the aclose method required by the standard Python asynchronous cleanup convention (PEP 525) [1][2]. This issue frequently occurred when instrumentation libraries or custom wrappers attempted to close the stream using await stream.aclose, which is the standard expected interface for asynchronous context managers and iterators [1][2]. To resolve this: 1. Update your OpenAI Python SDK: This issue was addressed in recent versions of the library by adding aclose as an alias for close on the AsyncStream class to ensure compliance with PEP 525 [1][2]. 2. Verify Compatibility: If you are encountering this error on an older version, upgrading to the latest version of the openai package is the recommended fix [2]. In the internal implementation of the SDK, AsyncStream manages the underlying network response [3][4]. When an asynchronous context manager (such as AsyncChatCompletionStreamManager) is exited, it calls the stream's close method, which in turn calls aclose on the underlying response object to properly release the connection [1][3][5]. Adding the aclose method to AsyncStream allows external tools that expect this standard async interface to function correctly without triggering an AttributeError [1][2].
Citations:
- 1: AsyncStream missing aclose() causes AttributeError in streaming structured output openai/openai-python#2853
- 2: fix: add aclose() method to AsyncStream for PEP 525 compliance openai/openai-python#2880
- 3: https://github.com/openai/openai-python/blob/main/src/openai/_streaming.py
- 4: https://github.com/openai/openai-python/blob/722d3fff/src/openai/_streaming.py
- 5: https://github.com/openai/openai-python/blob/f16fbbd2/src/openai/lib/streaming/chat/_completions.py
为 _TokenQuotaStream 补齐底层流对象代理和上下文管理协议。
多用户配额路径会把原生 AsyncChatCompletionStream 替换为包装对象,但包装只实现了 __aiter__/__anext__ 和 close();外层按 async with ... 或者访问 .response 等原生成员消费流时会直接变成 AttributeError。补齐属性代理以及 __aenter__/__aexit__,避免受限用户路径下非遍历式流消费失效。
🛡️ 示例补丁
def __aiter__(self) -> "_TokenQuotaStream":
return self
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._stream, name)
+
+ async def __aenter__(self) -> "_TokenQuotaStream":
+ return self
+
+ async def __aexit__(self, *exc_info: Any) -> None:
+ await self.close()</ details>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class _TokenQuotaStream: | |
| def __init__( | |
| self, | |
| stream: Any, | |
| *, | |
| lease: Any, | |
| requested_tokens: int, | |
| prompt_estimate: int, | |
| output_estimate: int, | |
| ) -> None: | |
| self._stream = stream | |
| self._lease = lease | |
| self._requested_tokens = requested_tokens | |
| self._prompt_estimate = prompt_estimate | |
| self._output_estimate = output_estimate | |
| self._usage_total = 0 | |
| self._output_chars = 0 | |
| self._finalized = False | |
| def __aiter__(self) -> "_TokenQuotaStream": | |
| return self | |
| class _TokenQuotaStream: | |
| def __init__( | |
| self, | |
| stream: Any, | |
| *, | |
| lease: Any, | |
| requested_tokens: int, | |
| prompt_estimate: int, | |
| output_estimate: int, | |
| ) -> None: | |
| self._stream = stream | |
| self._lease = lease | |
| self._requested_tokens = requested_tokens | |
| self._prompt_estimate = prompt_estimate | |
| self._output_estimate = output_estimate | |
| self._usage_total = 0 | |
| self._output_chars = 0 | |
| self._finalized = False | |
| def __aiter__(self) -> "_TokenQuotaStream": | |
| return self | |
| def __getattr__(self, name: str) -> Any: | |
| return getattr(self._stream, name) | |
| async def __aenter__(self) -> "_TokenQuotaStream": | |
| return self | |
| async def __aexit__(self, *exc_info: Any) -> None: | |
| await self.close() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@deeptutor/core/agentic/client.py` around lines 212 - 232, Update
_TokenQuotaStream to proxy unhandled attributes, such as response, to the
wrapped _stream, and implement the async context-manager protocol with
__aenter__ and __aexit__. Preserve the existing iteration and close behavior
while ensuring async with usage delegates correctly to the underlying stream.
| def reserve( | ||
| self, | ||
| user_id: str, | ||
| requested_tokens: int, | ||
| policy: TokenQuotaPolicy, | ||
| *, | ||
| prompt_tokens_estimate: int = 0, | ||
| output_tokens_estimate: int = 0, | ||
| ) -> TokenQuotaLease | None: | ||
| requested = max(0, int(requested_tokens)) | ||
| policy = policy.normalized() | ||
| if requested == 0 or not policy.bounded: | ||
| return None | ||
|
|
||
| daily_key, monthly_key = _period_keys() | ||
| now = datetime.now(timezone.utc).isoformat() | ||
| reservation_id = uuid.uuid4().hex | ||
| connection = self._connect() | ||
| try: | ||
| connection.execute("BEGIN IMMEDIATE") | ||
| self._ensure_period_row( | ||
| connection, | ||
| user_id=user_id, | ||
| period="daily", | ||
| period_key=daily_key, | ||
| now=now, | ||
| ) | ||
| self._ensure_period_row( | ||
| connection, | ||
| user_id=user_id, | ||
| period="monthly", | ||
| period_key=monthly_key, | ||
| now=now, | ||
| ) | ||
| rows = { | ||
| row["period"]: row | ||
| for row in connection.execute( | ||
| """ | ||
| SELECT period, consumed_tokens, reserved_tokens | ||
| FROM token_usage | ||
| WHERE user_id = ? AND period_key IN (?, ?) | ||
| """, | ||
| (user_id, daily_key, monthly_key), | ||
| ) | ||
| } | ||
| for period, limit in ( | ||
| ("daily", policy.daily_tokens), | ||
| ("monthly", policy.monthly_tokens), | ||
| ): | ||
| if limit <= 0: | ||
| continue | ||
| row = rows[period] | ||
| used = int(row["consumed_tokens"]) + int(row["reserved_tokens"]) | ||
| if used + requested > limit: | ||
| raise TokenQuotaExceeded( | ||
| period=period, | ||
| limit=limit, | ||
| used=used, | ||
| requested=requested, | ||
| ) | ||
|
|
||
| for period, period_key in (("daily", daily_key), ("monthly", monthly_key)): | ||
| connection.execute( | ||
| """ | ||
| UPDATE token_usage | ||
| SET reserved_tokens = reserved_tokens + ?, updated_at = ? | ||
| WHERE user_id = ? AND period = ? AND period_key = ? | ||
| """, | ||
| (requested, now, user_id, period, period_key), | ||
| ) | ||
| connection.execute( | ||
| """ | ||
| INSERT INTO token_reservations | ||
| (reservation_id, user_id, daily_key, monthly_key, | ||
| requested_tokens, state, created_at) | ||
| VALUES (?, ?, ?, ?, ?, 'active', ?) | ||
| """, | ||
| (reservation_id, user_id, daily_key, monthly_key, requested, now), | ||
| ) | ||
| connection.execute("COMMIT") | ||
| except TokenQuotaExceeded: | ||
| connection.execute("ROLLBACK") | ||
| raise | ||
| except (OSError, sqlite3.Error) as exc: | ||
| connection.execute("ROLLBACK") | ||
| raise TokenQuotaUnavailable(f"token quota reservation failed: {exc}") from exc | ||
| finally: | ||
| connection.close() | ||
| return TokenQuotaLease( | ||
| self, | ||
| reservation_id, | ||
| requested, | ||
| max(0, int(prompt_tokens_estimate)), | ||
| max(0, int(output_tokens_estimate)), | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
租约生命周期完全依赖调用方显式结束,缺少兜底回收,用户额度会被静默锁死。 账本只在 finalize/release 时归还 reserved_tokens,而流式包装只在迭代到底、异常或显式 close() 时才结算——任何提前退出或进程中断都会留下永久 active 的预留。
deeptutor/multi_user/token_quota.py#L182-L276:在reserve内顺带回收超时的active预留(标记为expired并从对应周期行的reserved_tokens中扣减),作为所有调用路径的兜底。deeptutor/core/agentic/client.py#L234-L258:为_TokenQuotaStream增加__aexit__并在对象销毁/未完成迭代时确保_finalize被调用,避免消费者break后租约悬挂。
📍 Affects 2 files
deeptutor/multi_user/token_quota.py#L182-L276(this comment)deeptutor/core/agentic/client.py#L234-L258
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@deeptutor/multi_user/token_quota.py` around lines 182 - 276, Update
TokenQuotaManager.reserve in deeptutor/multi_user/token_quota.py (lines 182-276)
to reclaim timed-out active reservations before checking availability: mark them
expired and decrement reserved_tokens for their daily and monthly usage rows.
Update _TokenQuotaStream in deeptutor/core/agentic/client.py (lines 234-258) to
add __aexit__ and ensure object destruction or incomplete iteration invokes
_finalize, covering consumers that exit early or break; both sites require
direct changes.
| After the proxy is healthy, create a virtual key from the proxy network using | ||
| the LiteLLM master key. For a user-scoped key, use the DeepTutor user id as | ||
| `user_id` and keep the model name at `deeptutor-default`: | ||
|
|
||
| ```bash | ||
| curl -sS -X POST http://litellm:4000/key/generate \ | ||
| -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{"models":["deeptutor-default"],"user_id":"u_<user-id>","max_budget":1,"budget_duration":"30d"}' | ||
| ``` | ||
|
|
||
| Put the returned virtual key in the admin LLM profile, with base URL | ||
| `http://litellm:4000/v1` and model `deeptutor-default`. Do not put a master key | ||
| in a user grant or browser response. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files =="
git ls-files | sed -n '1,200p'
echo "== target file =="
if [ -f ops/litellm/README.md ]; then
nl -ba ops/litellm/README.md | sed -n '1,120p'
else
echo "ops/litellm/README.md not found"
fi
echo "== search for litellm/admin/profile/user-scoped =="
rg -n "LITELLM|litellm|master key|virtual key|admin LLM|LLM profile|profile|user_id|deeptutor-default" . -S --glob '!**/.git/**' | sed -n '1,240p'Repository: hubo1989/DeepTutor
Length of output: 8186
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file with line numbers =="
awk '{printf "%5d\t%s\n", NR, $0}' ops/litellm/README.md | sed -n '1,80p'
echo "== relevant usages around LiteLLM/admin/profile =="
python3 - <<'PY'
from pathlib import Path
import re
terms = ["LITELLM","litellm","master.key","virtual.key","litellm:4000","deeptutor-default","user_id"]
for p in Path(".").rglob("*"):
if ".git" in p.parts or p.is_dir():
continue
try:
data = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
if any(t in data for t in terms):
lines = data.splitlines()
hits = [(i+1, line.strip()) for i,line in enumerate(lines) if any(t in line for t in terms)]
if hits:
print(f"\n--- {p} ---")
for row in hits[:12]:
print(f"{row[0]:5d}\t{row[1]}")
PY
echo "== outline admin settings files =="
for f in $(git ls-files | rg "(^|/)settings|(^|/)admin|(^|/)profile|(^|/)grant|config|model" 2>/dev/null | head -200); do
echo "--- $f ---"
head -80 "$f"
doneRepository: hubo1989/DeepTutor
Length of output: 31013
🌐 Web query:
LiteLLM key/generate user_id virtual key budget spend attribution admin profile usage
💡 Result:
In LiteLLM, proper budget and spend attribution for virtual keys is achieved by explicitly associating keys with a user_id [1][2][3]. Key Points on user_id and Budget Attribution: - Auto-Attribution and Security: To prevent "orphan" keys (where spend is not attributed to any user and budget limits are bypassed), LiteLLM auto-assigns the caller's user_id to new keys generated by non-admin users if a user_id is not explicitly provided [4]. Proxy admins retain the ability to generate keys with any or no user_id [4]. - Spend Tracking: Spend for a virtual key is automatically tracked in the LiteLLM_VerificationTokenTable [2]. If the key has an attached user_id or team_id, the spend is also reflected in the LiteLLM_UserTable or LiteLLM_TeamTable, respectively [2][5]. - Budget Enforcement: When a key is associated with a user_id, LiteLLM enforces that user's personal budget alongside any team or key-level budgets [1][6]. Requests are blocked if any budget level on the path (key, user, or team) is exceeded [6]. - Admin Usage Tracking: Admins can track and filter usage by user_id using the Admin UI or dedicated endpoints [3][7][8]. The Admin UI allows filtering spend data and activity logs by specific users [7][8]. - Important Warning: End-users providing their own user parameter in request bodies can inadvertently lead to spend being attributed to that self-declared user ID rather than the owner of the API key [3]. To ensure accurate billing and limit enforcement for backend services, you must explicitly set the user_id when generating the API key for that user and consistently use that key for their requests [3]. For management tasks, such as generating keys or viewing analytics, LiteLLM uses role-based access controls [6]. Non-admin users are restricted to viewing data only for their own user_id [7][9]. Admin-scoped keys or sessions have visibility into global usage and are required to perform administrative actions like elevating budgets [6][7][9].
Citations:
- 1: https://docs.litellm.ai/docs/proxy/users
- 2: https://docs.litellm.ai/docs/proxy/virtual_keys
- 3: https://docs.litellm.ai/docs/proxy/cost_tracking
- 4: fix(proxy): auto-assign caller's user_id on /key/generate to prevent budget bypass BerriAI/litellm#22688
- 5: https://github.com/BerriAI/litellm/blob/main/docs/my-website/docs/proxy/virtual_keys.md
- 6: https://docs.litellm.ai/docs/proxy/access_control
- 7: [Feature] UI - Usage: Allow Filtering by User BerriAI/litellm#21351
- 8: https://docs.litellm.ai/docs/proxy/customer_usage
- 9: https://github.com/BerriAI/litellm/blob/e15b37a1/litellm/proxy/management_endpoints/common_utils.py
不要在共享 admin LLM profile 中复用带 user_id 的 user-scoped key。
user_id 会把 key 的 spend/budget 归属到指定的 DeepTutor 用户;生成后直接放入 admin profile 时,如果该 profile 被不同用户/ grant 按相同模型使用,后续 LiteLLM 请求会按 key 附着的 user_id 记账,导致计费和预算限制不再跟随实际操作用户。改为生成 service/admin key,或让用户请求自行选择对应 user_id 的 key。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ops/litellm/README.md` around lines 10 - 23, Update the README instructions
around virtual-key generation and admin LLM profile configuration so the shared
admin profile uses a service/admin-scoped key without a user_id. Keep
user-scoped keys tied to individual user requests or grants, and explicitly
preserve the requirement that the master key is never exposed in user grants or
browser responses.
| from deeptutor.agents.visualize.utils import validate_visualization | ||
|
|
||
|
|
||
| def test_html_validation_rejects_inline_javascript_syntax_errors() -> None: | ||
| html = """<!doctype html><html><body><script> | ||
| document.body.innerHTML = '<p>A'B</p>'; | ||
| </script></body></html>""" | ||
|
|
||
| ok, error = validate_visualization(html, "html") | ||
|
|
||
| assert not ok | ||
| assert "Inline JavaScript syntax error" in error | ||
|
|
||
|
|
||
| def test_html_validation_accepts_valid_inline_javascript() -> None: | ||
| html = """<!doctype html><html><body><script> | ||
| document.body.innerHTML = \"<p>A'B</p>\"; | ||
| </script></body></html>""" | ||
|
|
||
| ok, error = validate_visualization(html, "html") | ||
|
|
||
| assert ok, error |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
测试隐式依赖本机 node,缺失时第一个用例会失败。
_validate_inline_javascript 在 shutil.which("node") 为空时直接返回 (True, ""),于是 assert not ok 在没有 Node 的环境(多数纯 Python CI 镜像)中必然失败。请显式跳过。
💚 建议的修复
+import shutil
+
+import pytest
+
from deeptutor.agents.visualize.utils import validate_visualization
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None,
+ reason="inline JavaScript validation requires the node binary",
+)
+
def test_html_validation_rejects_inline_javascript_syntax_errors() -> None:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| from deeptutor.agents.visualize.utils import validate_visualization | |
| def test_html_validation_rejects_inline_javascript_syntax_errors() -> None: | |
| html = """<!doctype html><html><body><script> | |
| document.body.innerHTML = '<p>A'B</p>'; | |
| </script></body></html>""" | |
| ok, error = validate_visualization(html, "html") | |
| assert not ok | |
| assert "Inline JavaScript syntax error" in error | |
| def test_html_validation_accepts_valid_inline_javascript() -> None: | |
| html = """<!doctype html><html><body><script> | |
| document.body.innerHTML = \"<p>A'B</p>\"; | |
| </script></body></html>""" | |
| ok, error = validate_visualization(html, "html") | |
| assert ok, error | |
| import shutil | |
| import pytest | |
| from deeptutor.agents.visualize.utils import validate_visualization | |
| pytestmark = pytest.mark.skipif( | |
| shutil.which("node") is None, | |
| reason="inline JavaScript validation requires the node binary", | |
| ) | |
| def test_html_validation_rejects_inline_javascript_syntax_errors() -> None: | |
| html = """<!doctype html><html><body><script> | |
| document.body.innerHTML = '<p>A'B</p>'; | |
| </script></body></html>""" | |
| ok, error = validate_visualization(html, "html") | |
| assert not ok | |
| assert "Inline JavaScript syntax error" in error | |
| def test_html_validation_accepts_valid_inline_javascript() -> None: | |
| html = """<!doctype html><html><body><script> | |
| document.body.innerHTML = \"<p>A'B</p>\"; | |
| </script></body></html>""" | |
| ok, error = validate_visualization(html, "html") | |
| assert ok, error |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/agents/visualize/test_utils.py` around lines 1 - 22, Update the tests
around validate_visualization to detect whether the node executable is available
and explicitly skip the inline-JavaScript validation tests when it is missing,
while retaining their existing assertions when Node is installed. Use the test
framework’s skip mechanism and apply it to both
test_html_validation_rejects_inline_javascript_syntax_errors and
test_html_validation_accepts_valid_inline_javascript.
| onError={() => setLoadError(t("Failed to load visualization"))} | ||
| /> | ||
| {loadError && ( | ||
| <div className="mt-2 rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-600 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-400"> | ||
| {loadError} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 确认新增文案已进入两份 locale
rg -n 'Failed to load visualization' web/localesRepository: hubo1989/DeepTutor
Length of output: 324
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'VisualizationViewer.tsx' . | sed 's#^\./##'
echo "== outline =="
ast-grep outline web/components/visualize/VisualizationViewer.tsx --view compact 2>/dev/null || true
echo "== relevant lines =="
cat -n web/components/visualize/VisualizationViewer.tsx | sed -n '1,260p'
echo "== visualization postMessage usages =="
rg -n "dt:visualize-height|visualize-height|onError|srcDoc|iframe.*height|setLoadError|loadError" web -SRepository: hubo1989/DeepTutor
Length of output: 18761
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== iframe-html preview around height handler =="
cat -n web/lib/iframe-html.ts | sed -n '1,140p'
echo "== prepareIframeHtml definition/usage context =="
rg -n "function prepareIframeHtml|const prepareIframeHtml|export .*prepareIframeHtml|prepareIframeHtml\\(" web/lib/iframe-html.ts web/components/visualize/VisualizationViewer.tsx -S
echo "== relevant visualization UI code tail =="
cat -n web/components/visualize/VisualizationViewer.tsx | sed -n '260,480p'
echo "== iframe error/onload handler docs via local source if available =="
rg -n "onLoad|onError|srcDoc|iframe" web/components/visualize/VisualizationViewer.tsxRepository: hubo1989/DeepTutor
Length of output: 17098
将 iframe 失败状态改为基于 bridge 的超时判定。
HtmlRenderer 使用 srcDoc 内联渲染,srcDoc 的解析失败只会走 onLoad,onError 基本不会触发;脚本抛出异常或页面高度上报缺失时 loadError 也不会进入失败 UI。Failed to load visualization 文案已存在,需要把这行 UI 改为 onLoad 后期待 dt:visualize-height/错误上报的超时逻辑。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/components/visualize/VisualizationViewer.tsx` around lines 183 - 189,
Replace the iframe onError-based failure handling in VisualizationViewer with a
timeout-driven bridge health check after onLoad: wait for the expected
dt:visualize-height or error report, then set loadError to the existing “Failed
to load visualization” message when no valid report arrives. Update the related
load/error state and cleanup logic while preserving the current failure UI.
| function setTokenQuota( | ||
| key: "daily_tokens" | "monthly_tokens", | ||
| value: string, | ||
| ) { | ||
| const parsed = Number(value); | ||
| const nextValue = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0; | ||
| setGrant((current) => ({ | ||
| ...current, | ||
| token_quota: { ...current.token_quota, [key]: nextValue }, | ||
| })); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
清空输入框会被解释为 0,即“无限制”。
Number("") 返回 0,所以用户为了改数字而清空输入框的瞬间,配额就变成了 0(按本区块说明即“无限制”)。若此时点击保存,受限用户会拿到无上限额度。建议对空值保留原值(或用 NaN 显式区分)。
🛡️ 建议修复
function setTokenQuota(
key: "daily_tokens" | "monthly_tokens",
value: string,
) {
- const parsed = Number(value);
- const nextValue = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
- setGrant((current) => ({
- ...current,
- token_quota: { ...current.token_quota, [key]: nextValue },
- }));
+ const trimmed = value.trim();
+ const parsed = Number(trimmed);
+ setGrant((current) => {
+ if (trimmed === "" || !Number.isFinite(parsed)) return current;
+ return {
+ ...current,
+ token_quota: {
+ ...current.token_quota,
+ [key]: Math.max(0, Math.floor(parsed)),
+ },
+ };
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function setTokenQuota( | |
| key: "daily_tokens" | "monthly_tokens", | |
| value: string, | |
| ) { | |
| const parsed = Number(value); | |
| const nextValue = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0; | |
| setGrant((current) => ({ | |
| ...current, | |
| token_quota: { ...current.token_quota, [key]: nextValue }, | |
| })); | |
| } | |
| function setTokenQuota( | |
| key: "daily_tokens" | "monthly_tokens", | |
| value: string, | |
| ) { | |
| const trimmed = value.trim(); | |
| const parsed = Number(trimmed); | |
| setGrant((current) => { | |
| if (trimmed === "" || !Number.isFinite(parsed)) return current; | |
| return { | |
| ...current, | |
| token_quota: { | |
| ...current.token_quota, | |
| [key]: Math.max(0, Math.floor(parsed)), | |
| }, | |
| }; | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/features/multi-user/components/GrantEditor.tsx` around lines 290 - 300,
更新 setTokenQuota 以单独处理空输入,避免 Number("") 将配额立即设为 0(无限制);空值应保留当前
token_quota[key](或使用 NaN 表示未完成输入),非空值继续执行现有的有限数值、非负及向下取整逻辑。
| "Failed to load GeoGebra": "加载 GeoGebra 失败", | ||
| "Loading GeoGebra...": "正在加载 GeoGebra...", | ||
| "HTML visualization": "HTML 可视化", | ||
| "Failed to load visualization": "演示加载失败", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
译文与可视化模块术语不一致。
visualization 在其他文案中通常译为“可视化”,此处译作“演示加载失败”会让用户困惑。
🌐 建议调整
- "Failed to load visualization": "演示加载失败",
+ "Failed to load visualization": "可视化加载失败",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "Failed to load visualization": "演示加载失败", | |
| "Failed to load visualization": "可视化加载失败", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/locales/zh/app.json` at line 2227, Update the Chinese translation value
for the “Failed to load visualization” localization key to use the established
“可视化” terminology instead of “演示”, while preserving the existing “加载失败” meaning.
Summary
Validation
python-telegram-botdependencyThe local
教学产物/directory was intentionally left out of this PR because it is a local teaching artifact, not part of the runtime change.Summary by CodeRabbit
新功能
改进