338 Commits

Author SHA1 Message Date
caoxiaozhu
7ced9d93bc chore(rules): 更新公司通信费报销规则表 2026-06-30 11:41:09 +08:00
caoxiaozhu
876cf342ac docs(dev-log): 记录多 task task2 时序缺陷 bug 修复 2026-06-30 11:40:46 +08:00
caoxiaozhu
43779f8f2c fix(web): 多 task 串行推进 task2 不再被预览生成时提前触发
onPreviewReadyForNextTask 在 task1 申请核对表刚生成、用户还没操作时就
提前拉起 task2,与用户后续在 task1 上的保存草稿/提交操作互相打架,导致
task2 完全无反应。移除该提前推进回调,统一由 onApplicationActionCompleted
在 task1 真正完成后再推进 task2。

- useWorkbenchAiApplicationPreviewFlow: 删除预览生成时的提前推进分支
- usePersonalWorkbenchAiMode: startModelPlannedApplicationPreview 不再传 onPreviewReadyForNextTask
- useWorkbenchAiActionRouter: 低置信确认按钮分支同步删除该回调
- 新增时序回归测试:预览生成不提前推进、保存草稿后才推进
- 更新两处源码正则断言为 doesNotMatch
2026-06-30 11:40:31 +08:00
caoxiaozhu
08f023243e fix(web): 多 task 草稿自动保存后继续推进下一 task
useWorkbenchAiApplicationPreviewFlow 在自动保存草稿分支透传 onApplicationActionCompleted,
确保草稿/提交完成后仍能按 remaining tasks 推进,修复多 task 报销场景后续步骤不启动。
更新 application-context-submit/intent-planner-model 测试,补充 bug 日志与开发日志。
2026-06-29 20:17:36 +08:00
caoxiaozhu
6bdaeed6d4 chore: 忽略 .zcode 本地目录并更新规则表与开发日志
- .gitignore 新增 .zcode/(ZCode 工具本地配置,不入库)
- 更新交通/通信/差旅等财务规则表
- 补充 2026-06-25/26 开发日志(chat UI SaaS 化、主题企业 AI 风格、bug 日志)
2026-06-26 22:42:46 +08:00
caoxiaozhu
d5a8f84703 refactor(web): 应用外壳/差旅详情/报销创建视图适配主题与多 task
- AppShellRouteView/useAppShell 适配主题皮肤与会话入口
- TravelRequestDetailView/travelRequestDetailSetup 差旅详情适配,travel-request-detail-view.css 调整
- TravelReimbursementCreateView/useTravelReimbursementCreateViewLifecycle 创建视图适配
- 更新 app-shell-financial-assistant-entry/travel-request-detail-risk-advice 测试
2026-06-26 22:42:29 +08:00
caoxiaozhu
c4b5fcc067 feat(web): AI 工作台多 task 串行推进与会话适配
- useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiActionRouter/useWorkbenchAiCommandIntents 支持 task1 完成后自动推进 task2,确认按钮直接拉起申请预览,草稿/提交成功后继续推进下一 task
- workbenchAiIntentPlannerModel/workbenchAiMessageModel/workbenchAiCommandIntentModel 适配多 task 意图规划与消息结构
- aiApplicationPreviewActions/aiApplicationPrecheckModel/aiExpenseDraftModel/aiWorkbenchConversationStore 草稿与会话存储适配
- PersonalWorkbenchAiMode 与样式适配,更新 preview-actions/expense-draft/conversation-store/fast-preview/action-router/command-intent/intent-planner 测试
2026-06-26 22:42:23 +08:00
caoxiaozhu
5753899eb3 feat(web): 主题皮肤系统与 LLM 设置面板重构
- useThemeSkin 重构主题皮肤应用逻辑,支持企业 AI 风格主题切换
- settingsModelHelper 新增主题与模型表字段映射,useSettings 适配
- LlmSettingsPanel/SettingsView 面板重构,支持多模型行编辑与主题区块
- settings-view.css 适配主题样式,新增 settings-theme-section 测试,更新 settings-llm-section 测试
2026-06-26 22:42:00 +08:00
caoxiaozhu
9c3fa80d22 feat(server): 设置持久化新增 LLM 模型表与主题字段
- SettingsLlmForm 新增 models 列表(SettingsModelRow:slot/provider/url/apiKey/modelId/type),支持多模型行持久化
- settings 服务读写模型表与主题相关字段,更新 test_settings_persistence 测试
2026-06-26 22:41:40 +08:00
caoxiaozhu
43c3ff860c fix(web): steward plan 确认按钮直接拉起申请预览,不丢失 remaining tasks
根因:steward plan 的'确认创建申请单'按钮 action_type 是
ASSISTANT_SCOPE_ACTION_SWITCH,handleInlineSuggestedAction 没有匹配分支,
落到 startInlineConversation 重新发起对话,steward_remaining_tasks 完全丢失。

修复:当 payload 有 steward_current_task + session_type=application +
task_type=expense_application 时,直接调 startAiApplicationPreviewFromAction
(会透传 steward_remaining_tasks 到申请预览 message),不走 startInlineConversation。

这样保存草稿成功后,targetMessage 上有 stewardRemainingTasks,
buildApplicationPreviewNextTaskAction 能生成'继续处理费用报销'按钮。
2026-06-26 11:40:44 +08:00
caoxiaozhu
3e4b1e1597 fix(web): 保存草稿/提交成功后也推进到下一个 task
之前多 task 串行推进只在 steward action 执行链(executeInlineStewardAction)
生效,但保存草稿走的是 application preview 流(executeInlineApplicationPreviewAction),
成功后只显示'查看单据',不会提示继续下一个报销 task。

- startAiApplicationPreviewFromAction 透传 steward_remaining_tasks 到 preview message
- startAiApplicationPreview 把 stewardRemainingTasks 存到 message 上
- executeInlineApplicationPreviewAction 成功后:检查 targetMessage.stewardRemainingTasks,
  有剩余 task 则追加'继续处理费用报销'按钮(复用 steward_confirm_flow 分支)
- 新增 buildApplicationPreviewNextTaskAction 辅助函数

现在保存草稿/提交申请成功后,用户会看到:[查看单据] [继续处理费用报销]
2026-06-26 11:21:16 +08:00
caoxiaozhu
3a5664c4da feat(web): 多 task 串行推进 - task1 完成后自动展示 task2 确认按钮
场景:'出差并且报销招待费'→LLM 拆出 2 个 task→先做完出差申请→
完成后自动展示'继续处理费用报销'按钮→用户确认后推进到报销流程。

- 新增 buildNextTaskSuggestedAction:从 actionPayload.steward_remaining_tasks
  取第一个剩余 task,根据 task_type 生成推进按钮(steward_confirm_flow)
- executeInlineStewardAction 成功后:除'查看单据'外,追加'继续处理下一个 task'按钮
- 用户点击推进按钮复用 handleInlineSuggestedAction 的 steward_confirm_flow 分支
- 前端 28 passed 无回归
2026-06-26 10:51:43 +08:00
caoxiaozhu
d139a63e64 refactor(server): 意图识别改 LLM 驱动,规则只做闲聊拦截+resume 兜底
规则不再判断'这是哪个业务场景'——那交给 LLM function call。
规则只保留两个不可替代职责:闲聊拦截(省 LLM 成本)、resume 确定性兜底。

- gate_classify 简化:删掉规则匹配门(94 词 CHOICE 匹配)和 ambiguous 提前判断
- 新增 _is_lightweight_off_topic:只拦 greeting+meaningless,不依赖业务关键词
- HANDLER_ONLY 改为 LLM 输出驱动:LLM 返回 query_travel_standard 后转 handler
- 图拓扑简化:gate_classify 只输出 off_topic|resume|model_intent
- 验证:76 passed;复合场景'出差并且报销招待费'→LLM 返回 2 task
2026-06-26 10:19:04 +08:00
caoxiaozhu
8a2ae6eb75 fix(server): gate_classify 复用 _classify_irrelevant_input 修复 off_topic 误杀
回归问题:P1.3 重构时 gate_classify 的 off_topic 门用了 scene_registry
的 35 个 signal_keywords,丢掉了 legacy 的 73 个关键词(城市名/时间词/金额词/
交通词等),导致'下周去上海''昨天打车30块'等正常业务输入被误判 off_topic,
根本进不了 LLM。

修复:gate_classify 的 off_topic 门改用成熟的 _classify_irrelevant_input
(94 词 + registry 信号词 + greeting/meaningless 细分),scene_registry 的
signal_keywords 只用于规则匹配门(CHOICE 路由)。删除残缺的 _matches_any_signal。

验证:76 passed;实测'下周去上海'→llm_function_call、'昨天打车30块'→
llm_function_call、'你好'→off_topic(正确拦截)。
2026-06-25 16:06:52 +08:00
caoxiaozhu
992cf71fa1 refactor(server): Phase 1 图拓扑重构 - LangGraph 成为唯一编排者
P1.3-P1.7:把 endpoint 补丁搬进图节点,门控收敛到 gate_classify 单一决策点。

- StewardGraphState 扩展:recent_history/steward_state/gate_decision/gate_scene_id/conversation_id
- 新增 5 个图节点:load_context(读历史+state)/gate_classify(统一门控)/execute_scene_handler/resume_recent_task/pending_flow wrapper
- 图拓扑从 5 节点重构为 10 节点:load_context → gate_classify → {off_topic/handler_only/resume/ambiguous/model_intent} → attach_action_steps
- gate_classify 四步裁决:resume门 → off_topic门 → 规则匹配门 → LLM门
- resume 门控优先于 off_topic,避免'再提交'被误判闲聊
- schema 放宽 planning_source/next_action Literal → str,支持 scene_handler:*/context_resume/answer_only
- endpoint 按 planner 类型分发 build_plan(LangGraph 接 db,legacy 不接)
- 76 passed + 4 场景端到端验证(出差申请/再提交/查差旅标准/闲聊)
2026-06-25 15:44:20 +08:00
caoxiaozhu
54356ba81a refactor(server): scene 注册表骨架 + 统一门控管道设计文档
Phase 1 P1.1-P1.2:为后端门控收口提供声明式场景注册基础设施。

- 新建 scenes/ 目录:gate_rules(GateRule/SceneRoute 枚举)、scene_descriptor(SceneDescriptor dataclass)、scene_registry(SceneRegistry 单例)
- 3 个场景迁入 descriptor:expense_application / reimbursement / query_travel_standard
- __init__.py 的 bootstrap_scenes 在 import 时注册 + 运行时绑定 handler/builder/executor(解决循环 import)
- 查询场景 priority=50 优先于 MODEL_ONLY 场景,确保规则匹配先于 LLM
- 落地 UNIFIED_GATE_PIPELINE.md 架构文档:目标架构 / 验收标准(接入 O(1))/ 3 阶段迁移路径
- 76 passed,scene 注册表未破坏现有代码;与 intent_registry 暂时并存,P1.3-P1.8 会统一迁移
2026-06-25 15:09:16 +08:00
caoxiaozhu
e9d7c56d5b feat(server): 会话上下文保留(LLM 历史 + 确定性兜底双保险)
解决用户删除草稿后说'再提交'丢失上下文的问题:

- steward.py 新增 _inject_recent_conversation_history:build_plan 前读最近 10 条对话注入 context_json
- steward_intent_agent.py 的 _build_messages 把 recent_history 暴露给模型,system prompt 加确认类话术引导
- 新建 steward_context_resume.py:should_resume_recent_task 检测'再提交'类话术 + state 有可恢复 flow,attach_resumed_task 从 state 恢复 task
- 两个 plan 入口(/plans 和 /plans/stream)都已接入双保险
- 后端 67 passed,端到端验证'上海出差→再提交'成功恢复 task
2026-06-25 15:08:56 +08:00
caoxiaozhu
2ebc2756bf fix(server): 兼容模型 tasks 输出为 JSON 字符串与 flow_id 误填
- StewardModelPlanBuilder 解析 tasks 时兼容模型把数组序列化为字符串的情况,先反序列化;JSON 截断/语法不完整时用正则抢救 task_type/requested_action/ontology_fields 等关键字段
- task_type 未命中意图时尝试 flow_id→task_type 映射还原,避免模型把 flow_id(如 travel_application)误填为 task_type 导致正确意图被丢弃
2026-06-25 12:25:18 +08:00
caoxiaozhu
606a88c805 chore: stewardPlanModel 适配注册表动作结构并更新规则表与日志
- stewardPlanModel 适配新的意图注册表动作步骤结构
- 更新交通/通信/差旅等财务规则表,补 2026-06-25 work-log
2026-06-25 11:50:11 +08:00
caoxiaozhu
eaada4bc57 refactor(server): steward 意图改用声明式注册表编排
- 新增 steward_intent_registry,IntentDescriptor 统一描述意图的识别关键词、动作步骤构建、字段白名单与副作用集合,替代分散的 if/else
- 新增 steward_intent_bootstrap 注册 expense_application 等意图;新增 steward_query_executors 提供差旅标准查询的无副作用执行与城市/席别标签化输出
- action_contracts/action_executor/graph_planner/intent_agent/model_plan_builder/planner_extraction/fallback 适配注册表,识别与执行分发自动从注册表取数
- 新增 intent_registry/query_executors 测试,更新 intent_agent 测试
2026-06-25 11:50:02 +08:00
caoxiaozhu
d321005044 chore: 更新公司通信费规则表与 AI 意图规划/work-log 文档 2026-06-25 10:56:00 +08:00
caoxiaozhu
59353308a2 feat(web): AI 意图规划置信度阈值与动作策略细化
- workbenchAiIntentPlannerModel 新增 WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD 与 isLowConfidenceTravelApplicationPlan,shouldRequestWorkbenchAiIntentPlan 增加业务关键词前置过滤
- resolveExecutableTravelApplicationPlan 区分 requestedSubmit 与提交确认(submitRequiresConfirmation),autoSubmit 不再直接置真
- workbenchIntentActionPolicy 改用 policyDecision 路由(need_confirmation/query_candidates),透传 riskLevel/requiresSelection/requiresConfirmation
- workbenchIntentFrameModel 补充 query 动作识别,usePersonalWorkbenchAiMode/useWorkbenchAiActionRouter/useWorkbenchAiApplicationPreviewFlow 接入低置信度与确认流程
- 更新 intent-planner-model/intent-frame-model/application-gate-model/fast-preview 测试
2026-06-25 10:55:49 +08:00
caoxiaozhu
6b0756a55f chore(logs): split agent change log outputs 2026-06-25 09:35:18 +08:00
caoxiaozhu
4d8a606cd6 chore(skills): date-scope development docs 2026-06-25 09:25:28 +08:00
caoxiaozhu
23f7de6cbf chore(skills): add development docs writer 2026-06-25 09:19:37 +08:00
caoxiaozhu
a12c4bea64 chore: 更新公司通信费规则表、work-log 与 superpowers specs 文档 2026-06-24 22:59:10 +08:00
caoxiaozhu
e5b03c6601 feat(web): 文档查询意图补充风险过滤与 X 天前范围
- aiDocumentQueryIntent 新增风险等级过滤(无/高/中/低/有风险)与 N 天前日期范围解析
- aiDocumentQueryModel/aiConversationHtmlRenderer 渲染适配风险过滤标签
- useWorkbenchAiDocumentQueryFlow/aiWorkbenchConversationStore 会话流转适配命令意图
- 更新 ai-document-query-model/ai-conversation-html-renderer/assistant-session-draft-delete 测试
2026-06-24 22:59:05 +08:00
caoxiaozhu
3eb78d343a feat(web): AI 工作台命令意图解析与动作策略
- 新增 workbenchIntentFrameModel,解析删除/审批/查询/咨询政策等意图帧,含风险等级、目标模式(当前上下文/筛选候选)与日期归一化
- 新增 workbenchIntentActionPolicy,按意图帧路由下一步(直通/拦截批量/上下文确认/查询候选)
- 新增 workbenchAiCommandIntentModel 与 useWorkbenchAiCommandIntents,识别草稿删除意图并解析最近草稿载荷生成引导
- usePersonalWorkbenchAiMode 接入命令意图处理,personal-workbench-ai-mode.css 适配
- 新增 command-intent-model/intent-frame-model 测试
2026-06-24 22:58:59 +08:00
caoxiaozhu
a0f6d9f702 chore: 更新 .env.example、财务规则表与 AI 意图规划开发文档 2026-06-24 21:59:15 +08:00
caoxiaozhu
bb681aa1f3 chore(repo): 移除误跟踪的 egg-info 构建产物并新增忽略规则
server/src/x_financial_server.egg-info/ 为 uv/pip 构建产物,此前被误跟踪。
本次 git rm --cached 移除跟踪(本地保留),.gitignore 新增 *.egg-info/ 规则避免后续误入库。
2026-06-24 21:59:09 +08:00
caoxiaozhu
bc560145a4 feat(web): AI 工作台意图规划与规划思考模型
- 新增 workbenchAiIntentPlannerModel,基于 LLM function_call 解析建单/草稿/提交意图,区分 model 与 rule_fallback 来源
- 新增 workbenchAiPlanningThinkingModel 合并规划思考事件流,按 eventId 去重合并
- application gate/preview 模型接入意图规划,usePersonalWorkbenchAiMode/useWorkbenchAiStewardFlow/useWorkbenchAiActionRouter 链路适配,支持上下文提交
- steward 服务与 stewardPlanModel 适配新动作结构,receipt-folder-view 微调样式
- 新增 intent-planner-model/application-context-submit/steward-actions-service 测试,更新 gate-model/action-router/plan-message-copy/fast-preview 测试
2026-06-24 21:58:46 +08:00
caoxiaozhu
5311c99d69 refactor(server): steward 决策链路改用 LangGraph 编排
- 新增 StewardGraphPlannerService,用 LangGraph 状态图编排意图识别→流程判断→模型/规则分支→兜底,替代原 planner 内线性调用
- 新增 StewardGraphRuntimeService 编排运行时决策与槽位决策;StewardActionContracts/Executor 统一动作合约与执行
- steward_intent_agent/application_fact_resolver/runtime_chat 适配图执行器,config 暴露图相关开关
- pyproject/uv.lock 新增 langgraph 依赖
- 新增 graph_planner/graph_runtime/action_executor 测试,更新 intent_agent/planner/fact_resolver/runtime_chat/reimbursement 测试
2026-06-24 21:58:35 +08:00
caoxiaozhu
545b31d32f chore(env): docker-compose 端口与服务配置微调并更新规则表与日志
- docker-compose(.full).yml 与 start.sh 微调端口/服务配置
- AGENTS.md 同步更新协作规范
- 更新交通/通信/差旅等财务规则表,补 2026-06-24 work-log
2026-06-24 12:36:03 +08:00
caoxiaozhu
8417a9f542 feat(web): 设置中心缓存管理与文件预览资产工具
- 新增 documentPreviewAssets 工具,统一从 URL/Blob/File 推断预览类型(image/pdf/file/unsupported)
- SettingsView/SettingsView.js/settingsModelHelper 新增系统缓存管理区块,调用 /settings/cache/clear 并展示清理结果;useSettings/services 适配
- WorkbenchAiFilePreviewDialog/useWorkbenchAiFilePreview 接入预览资产工具,workbenchAiComposerModel 调整文件处理
- ReceiptFolder/LogDetailView/DigitalEmployeeWorkRecords/travelReimbursementAttachmentModel 配套适配
- 新增 settings-cache-management-section 测试,更新 settings-llm/rendering/receipt-folder-view/composer-components/attachment-association 测试
2026-06-24 12:35:59 +08:00
caoxiaozhu
9a5ed0e94a feat(server): 系统缓存清理接口与 OCR 文本层兜底增强
- 新增 system_cache 模块与 POST /settings/cache/clear,管理员可一键清理 OCR 结果/运行时配置/模型失败冷却/知识库索引/地点语义等进程内缓存
- 各服务暴露 clear_*_cache 方法(ocr/runtime_settings/runtime_chat/knowledge/application_location_semantic),SettingsCacheClearRead 汇总清理项
- OCR 转图片失败时尝试用 PDF 文本层兜底构建识别文档(有效字符≥8),并写结果缓存;OcrService 暴露 clear_result_cache
- receipt_folder 车票过滤补充身份证号关键词,附件文档/操作/展示模块同步适配
- 新增 system_cache_endpoints 测试,更新 openapi_schema/ocr/receipt_folder/attachment_association_jobs 测试
2026-06-24 12:35:51 +08:00
caoxiaozhu
50d2dc579a style(web): 微调 AI 工作台样式 2026-06-24 10:43:08 +08:00
caoxiaozhu
f9553a6a1a docs: 清理过期计划文档并补全 work-log 与开发/用户文档
- 删除已落地的 improvement-roadmap、superpowers 计划与 ui-mockups 参考稿,删除早期 work-log(2026-05-06~08)
- 新增 2026-05-23 起的 work-log 与 attachment-association-background-job、reimbursement-draft-action-branching 等开发文档及用户文档
- docker-compose(.full).yml 微调服务配置
2026-06-24 10:42:57 +08:00
caoxiaozhu
ee730aa31c feat(web): AI 工作台文件预览/附件关联任务与草稿分支
- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览
- 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示
- 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿
- PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善
- DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配
- 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
2026-06-24 10:42:50 +08:00
caoxiaozhu
0264a4b5b4 refactor(server): user_agent/steward/ocr 等服务重构并适配关联任务
- user_agent 拆分 application/locations/knowledge/response/review 四个子模块,接入申请位置语义与关联草稿分支
- steward planner/runtime/slot/plan_builder 决策链路重构,travel_reimbursement_calculator/orchestrator_expense_query 适配
- ocr/document_preview/document_intelligence/receipt_folder 复用预览与资产缓存,expense_claim_draft_flow/application_handoff 适配
- pyproject.toml 新增依赖,paddleocr bootstrap 脚本与 server_start.sh 调整
- 更新差旅/交通/通信等财务规则表,同步 document_intelligence/ocr/receipt_folder/user_agent 等测试
2026-06-24 10:42:24 +08:00
caoxiaozhu
332f77389d feat(server): 新增附件关联/关联报销草稿后台任务与申请位置语义
- attachment_association_jobs:从票据夹批量关联附件到报销单,识别城市/日期并创建明细项,内存态 job 跟踪
- linked_reimbursement_draft_jobs:基于申请单异步生成关联报销草稿,调用 Orchestrator 编排,区分 succeeded/failed 终态
- application_location_semantics:抽取差旅出发/到达城市、判断具体地址/业务动作等位置语义,供申请单校验复用
- router 注册两个 job 端点,新增对应 job/语义单元测试
2026-06-24 10:42:05 +08:00
caoxiaozhu
d4ff79f326 chore(agent): 新增变更日志/checkpoint skill 与 post-commit hook
- AGENTS.md 新增变更日志 Skill 规范:变更后增量更新 document/work-log,更新前先做 Git 拉取检查
- 新增 .codex/skills 下 agent-change-log、git-checkpoint-commit 两个 skill
- 新增 .githooks/post-commit 与 tools/agent-change-log 自动化脚本,提供提交后最低限度日志追加
2026-06-24 10:41:56 +08:00
caoxiaozhu
93212600eb chore(skills): add git checkpoint commit loop 2026-06-24 09:47:05 +08:00
caoxiaozhu
73966b3a7b refactor: consolidate finance workflow modules 2026-06-23 11:21:18 +08:00
caoxiaozhu
1f40ce3df3 fix(web): 孤儿申请预览消息触发重新生成可编辑表格
useWorkbenchAiApplicationPreviewFlow 识别仅含表格文案但缺 applicationPreview 载荷的孤儿消息,触发重新生成可编辑表格并提示用户,避免提交动作静默失败。
2026-06-23 09:43:29 +08:00
caoxiaozhu
f17098aa58 chore(repo): 移除误跟踪的 expense_claims 运行时票据文件
server/storage/expense_claims/ 下 30 个用户上传票据/预览图/meta 此前在 gitignore 规则生效前被误提交,运行时会持续产生内容变更。
本次 git rm --cached 移除版本跟踪(本地文件保留),配合已有 server/storage/expense_claims/ 忽略规则,避免后续误入库。
2026-06-23 09:43:07 +08:00
caoxiaozhu
8094333e3b style(web): 通知中心列表行布局重构与时间标签格式化
- TopBar 通知行改为 row-main/row-head/row-foot 结构化布局,标题加粗、分类改为 pill、箭头随标题右侧
- formatNotificationTime 改名 formatNotificationTimeLabel,新增 ISO/短日期直通匹配与超长截断兜底
- 更新 sidebar-document-unread-dot 测试
2026-06-23 09:42:56 +08:00
caoxiaozhu
0122f3b250 chore(rules): 更新交通/通信/差旅/出差等财务规则表 2026-06-23 09:42:43 +08:00
caoxiaozhu
dc4cad2baa chore(env): WEB_PORT 统一回退为 5173 并烟雾检查改为可开关
- .env.example/docker-compose(.full).yml WEB_PORT 默认值 5273→5173(Vite 默认),CORS_ORIGINS 同步
- docker-compose 注入 WEB_PORT/SERVER_PORT 环境变量,健康检查端口随之更新
- start.sh 新增 SERVER_SMOKE_CHECK_ENABLED 开关,默认关闭烟雾检查,仅健康探测
- web/web_start.sh 适配端口
- .gitignore 补充忽略 tmp/ 运行时目录
2026-06-23 09:42:40 +08:00
caoxiaozhu
e725b7f19c feat(web): 票据夹资产缓存接入与 AI 工作台附件流程完善
- ReceiptFolderView 删除票据后提示已关联附件副本保留,接入 useToast;fetchReceiptFolderAsset 加 no-store 避免预览缓存
- PersonalWorkbenchAiMode 附件区/对话气泡适配资产缓存,personal-workbench-ai-mode.css 调整布局
- usePersonalWorkbenchAiMode/useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiAttachmentAssociationFlow/useWorkbenchAiStewardFlow 完善附件草稿选择与关联流程
- travelRequestDetailSmartEntryRecognition 智能识别增强,AppShellRouteView/PersonalWorkbenchView/useApplicationPreviewEditor/useTravelReimbursementSubmitComposer 等配套适配
- 新增 expense-attachment-draft-selection、receipt-folder-asset-cache、travel-request-detail-smart-entry-recognition 测试,更新 attachment-association-confirmation、expense-application-fast-preview、workbench-ai-mode-switch 测试
2026-06-23 09:42:13 +08:00
caoxiaozhu
84a8998e59 feat(server): 票据文件夹资产缓存与文档预览统一生成
- 新增 document_preview 模块,DocumentPreviewAssets 统一处理 data URL 解码、pdftoppm PNG 预览生成(poppler-data 编码)、renderer_id 标识
- receipt_folder 服务复用预览生成,缓存票据资产并提供清理;删除票据时保留已关联报销单的附件副本
- document_intelligence 新增票据预览/资产缓存接入与字段提取增强;ocr 抽取复用预览工具,附件分析/文档/操作/展示四个子模块同步适配
- receipt_folder 端点补充资产缓存头,补/扩 document_intelligence、ocr_endpoints、ocr_service、receipt_folder_service、reimbursement_endpoints 测试,新增 attachment_analysis 回归测试
2026-06-23 09:42:00 +08:00
caoxiaozhu
bc743adef3 chore(rules): 更新公司通信费报销规则表 2026-06-22 15:56:13 +08:00
caoxiaozhu
ded8b39ccb feat(web): 申请单预览编辑器增强与报销流程细节适配
- useApplicationPreviewEditor 扩展字段编辑与校验,useTravelReimbursementApplicationPreviewDateEditor 微调日期处理
- travelReimbursementExpenseQueryModel/reimbursements 服务/expenseApplicationPreview 适配工号/邮箱字段与关联动作
- useWorkbenchAiApplicationPreviewFlow/usePersonalWorkbenchAiMode 接入关联门控后的预览流转
- TravelReimbursementCreateView 调整入口,TravelReimbursementMessageItem 适配
- 新增 expense-application-fast-preview 测试,更新 attachment-association-confirmation、review-drawer-switch 测试
2026-06-22 15:56:06 +08:00
caoxiaozhu
ba444a514f feat(web): 报销单新增关联申请单门控与草稿检测流程
- 新增 travelReimbursementAssociationGateModel,查询可关联申请单/草稿报销单并生成跳过/选择/单独新建动作,区分差旅费与业务招待费类型
- travelReimbursementApplicationLinkModel 补充 buildLinkedApplicationReferenceIndex/buildRequiredApplicationActions 等关联构建逻辑
- useTravelReimbursementSuggestedActions 接入 select_required_application/skip 系列动作,'我要报销'入口改为先走关联门控
- useWorkbenchAiActionRouter 新增 SKIP_REQUIRED_APPLICATION_LINK/SKIP_REIMBURSEMENT_DRAFT_CHECK 动作分发
- useWorkbenchAiExpenseFlow 暴露 startAiReimbursementAssociationGate,stewardPlanModel 待处理流程适配
- 新增 workbench-ai-action-router、workbench-ai-reimbursement-association-gate 测试并更新 guided-flow、steward-plan 测试
2026-06-22 15:55:59 +08:00
caoxiaozhu
aa965da69d feat(server): 报销单输出工号/邮箱并扩展申请人邮箱前缀匹配
- ExpenseClaimRead 新增 employee_no/employee_email 字段,ExpenseClaim 模型补对应只读属性
- expense_claim_access_policy 在姓名匹配未果时,按 candidate@% 邮箱前缀匹配 Employee.email,命中唯一记录即返回
- test_backend_pagination/test_expense_claim_service 补充工号/邮箱字段断言与邮箱前缀匹配用例
- 更新公司通信费报销规则表
2026-06-22 15:55:48 +08:00
caoxiaozhu
1b04ee1c4c fix(web): restore travel detail child component styles 2026-06-22 13:34:12 +08:00
caoxiaozhu
103f225f54 chore(rules): 更新差旅/交通/通信等财务规则表 2026-06-22 12:02:11 +08:00
caoxiaozhu
e42dedaba1 docs: 同步 Docker 编排说明并补充重构计划与 UI 参考
- README 与 docker/README 更新双 compose 文件说明:docker-compose.yml 仅主应用、docker-compose.full.yml 完整栈
- 新增 docs/superpowers/plans 800 行重构实施计划,规划前后端代码体量护栏与按职责拆分路径
- 新增 docs/ui-mockups 附件关联企业版 UI 参考稿
2026-06-22 12:02:08 +08:00
caoxiaozhu
607e127f59 chore(docker): 拆分主应用编排与完整本地栈
- 新增 docker-compose.full.yml:编排 main+postgres+qdrant+onlyoffice 完整本地开发栈,依赖与 DATABASE_URL 由 compose 注入
- docker-compose.yml 精简为仅启动主应用,ONLYOFFICE/QDRANT 相关变量改为空值占位,便于复用外部依赖
- docker-compose.postgres.yml 主应用补充 POSTGRES_*/DATABASE_URL 环境变量
2026-06-22 12:02:03 +08:00
caoxiaozhu
6d33ba5742 refactor: enforce 800 line source limits 2026-06-22 11:58:53 +08:00
caoxiaozhu
08a4fa3577 style(web): AI 工作台附件卡片样式与交互适配
- personal-workbench-ai-mode.css 新增 workbench-ai-file-card 系列样式,file-strip 改为 flex-wrap 左对齐排布卡片,移除按钮样式随卡片重构
- workbench-ai-mode-switch 测试补充附件卡片相关断言
2026-06-21 23:24:36 +08:00
caoxiaozhu
d660a961fb feat(web): AI 工作台附件改为卡片化展示并支持单项移除
- PersonalWorkbenchAiMode 附件区由计数条改为按类型图标/名称/类型标签的卡片列表,支持单项移除(removeAiModeFile),复用 buildFileIdentity 作为 key
- resolveAiComposerFileType 按 pdf/图片/表格/文档/压缩包/文件归类,分别对应图标与色调
- .gitignore 补充忽略 server/storage/receipt_folder/ 运行时票据存储目录
2026-06-21 23:24:16 +08:00
caoxiaozhu
669d22e71f feat(web): 差旅领导意见事件结构化与申请审批信息增强
- applicationApproval 新增按日期/时间/审批角色拆分格式化,buildLeaderApprovalEvents 补充 dateLabel/timeLabel/roleLabel 字段
- TravelRequestDetailView 领导意见事件改为日期+时间+审批人结构化展示,travel-request-detail-view.css 重构对应样式
- travelReimbursementAttachmentModel 微调附件标识,同步更新 application-approval-info、travel-request-detail-leader-approval、attachment-association-confirmation 测试
- 更新公司通信费报销规则表
2026-06-21 23:24:09 +08:00
caoxiaozhu
88e91a5900 feat(ocr): PDF 文本层可用时跳过 worker 调用并补装 poppler-data
- OcrService 提取 PDF 文本层后若有效字符达到阈值,直接构建文档并写入结果缓存,不再触发 OCR worker,仅无文本层时才解析 python_bin/worker_path 调用 worker
- _build_text_layer_document 复用 AggregatedOcrDocument 聚合文本层片段,_has_usable_pdf_text_layer 基于 meaningful_char_count 判定
- docker-compose 与 paddleocr bootstrap 脚本补装 poppler-data,保证 PDF 文本层抽取的中文编码正确
- 新增文本层直取与运行时依赖两项 ocr_service 单测
2026-06-21 23:23:59 +08:00
caoxiaozhu
1986b0d945 style(web): 移除顶栏 AI 快捷操作区并优化差旅领导意见事件样式
- TopBar 移除 AI 模式下的公司切换/AI 模式切换快捷操作块及 showAiModeUtilityActions 计算属性,清理 top-bar.css 对应样式
- TravelRequestDetailView 领导意见事件重构为状态/意见/审批人结构化布局,travel-request-detail-view.css 补充对应样式
- 同步更新 topbar-ai-mode-switch、ai-sidebar-rail-mode、travel-request-detail-leader-approval 测试
2026-06-21 22:56:34 +08:00
caoxiaozhu
24b5b71b0f feat(web): 差旅申请详情进度 viewer 与审批/加载态组件增强
- 新增 requestProgressViewer,申请单在直属领导审批视角下将当前步骤展示为'等待批复',travel-request-detail/app-shell/useRequests 接入
- TravelRequestApprovalDialog 增强审批交互,TableLoadingState 补充表格加载占位,ConfirmDialog 扩展确认对话框能力
- useAppShell/useRequests/AppShellRouteView 配套适配申请详情跳转与会话状态
- 同步更新 requestProgressSteps、travel-request-detail-leader-approval、assistant-session-draft-delete、documents-center-status-filter、app-shell-financial-assistant-entry、request-progress-viewer 等测试
2026-06-21 22:49:58 +08:00
caoxiaozhu
8b3495455b feat(web): AI 文档详情引用解析与查询卡片增强
- 新增 aiDocumentDetailReference,统一解析 #ai-open-document-detail / #ai-open-application-detail 引用,兼容 A/R/D 短格式与 AP-/RE-/AD- 旧格式单号,提供 isBusinessDocumentReference 判定
- aiDocumentQueryModel 文档卡片接入详情引用,按申请单/报销单生成对应 href,HTML 渲染器识别单据记录表格并生成卡片链接
- PersonalWorkbenchAiMode 处理文档详情点击跳转,卡片样式重构为结构化布局并更新背景资源
- expenseApplicationPreview 补充事由字段,同步新增/更新 ai-document-detail-reference、document-query-model、html-renderer、workbench-ai-mode 等测试
- 更新公司通信费报销规则表
2026-06-21 22:49:53 +08:00
caoxiaozhu
3b74a330a3 feat(web): AI 文档查询卡片重构与单号判定统一
- documentClassification 抽出 isApplicationDocumentNo,统一兼容 AP-/APP- 旧格式与 A+8 新格式,aiDocumentQueryModel 复用
- aiDocumentQueryModel 文档卡片改为结构化字段布局(单据类型/金额/申请人/编号/操作),新增查询范围摘要区,渲染走 HTML 信任块
- AppShellRouteView/useAppShell/useRequests/detailAlerts/riskVisibility 等差旅详情模型适配单号判定
- 同步更新 ai-document-query-model/workbench-ai-mode-switch 测试,新增 document-classification 测试
2026-06-20 22:04:37 +08:00
caoxiaozhu
8158716e23 test(server): 适配 A/R/D 紧凑单号格式
- approval_routing/service/user_agent 测试中报销单查询统一兼容 RE- 旧格式与 R+8 新格式,申请单单号断言改为短格式
- generate_claim_no 用例重命名为短前缀校验,正则改为 R[A-HJ-NP-Z2-9]{8}
- 同步更新差旅/交通/通信等财务规则表
2026-06-20 22:04:31 +08:00
caoxiaozhu
0cda750ff0 feat(web): AI 工作台会话与文档卡片渲染增强
- aiConversationHtmlRenderer 识别单据记录类表格并渲染为卡片列表,新增删除申请单详情的禁用占位链接
- aiWorkbenchConversationStore 增加草稿删除后会话链接失效处理,避免点击已删除单据跳转
- aiApplicationPreviewActions 调整提交/草稿调用路径,PersonalWorkbenchAiMode 接入新的会话存储与渲染
- ConfirmDialog/TravelRequestDeleteDialog/useAppShell/AppShellRouteView 配套适配,同步更新相关前端测试
2026-06-20 21:44:16 +08:00
caoxiaozhu
81e990ab72 feat(server): 申请单支持草稿保存并统一删除权限口径
- user_agent_application 新增草稿分支:识别'保存草稿/存草稿/先保存'等意图,复用可编辑记录更新或建草稿,提交前单据重叠仍拦截
- 草稿态返回单号与待提交提示,submit 仅在确认提交分支触发,避免草稿进入审批流
- reimbursements 删除接口文案与判定统一为系统管理员可删、申请人删自有草稿/退回单,申请单判定改用 is_application_claim_no
- 更新财务规则表与 reimbursement 端点测试
2026-06-20 21:44:12 +08:00
caoxiaozhu
47c6a4bb73 refactor(server): 单号规则收紧为 A/R/D+8 位紧凑格式
- DOCUMENT_NUMBER_PREFIXES 改为 A/R/D,新增短格式与旧格式正则并存识别,提取正则加边界锚定避免误匹配
- build_document_number 去掉时间戳段,统一生成 A+token 等紧凑单号,is_application_claim_no 兼容旧 AP-/APP- 前缀
- access_policy/status_registry/reimbursements/expense_claims/budget_support 统一复用 is_application_claim_no 判定申请单
- 同步 document_numbering 单元测试覆盖新旧两种格式
2026-06-20 21:44:06 +08:00
caoxiaozhu
96c2e1099a feat(web): 统一平台管理员判定与 AI 工作台申请预览动作接入
- authUser 抽出 resolveAuthUserAdminFlag,统一 isAdmin 解析(含 superadmin、role_codes、中英文角色名),accessControl 复用同一逻辑
- 登录态、应用外壳路由、系统状态接入统一管理员判定,LoginView 与相关 composable 配套调整
- AI 工作台申请提交改为调用新的 /application-preview-action 接口,草稿保存仍走 orchestrator;预审模型补充重叠冲突提示与阻断判断
- 同步更新 accessControl/api-request/ai 预览动作等前端测试
2026-06-20 14:42:04 +08:00
caoxiaozhu
729d833edb feat(server): 新增申请核对预览快速建单接口与平台管理员判定统一
- reimbursements 新增 POST /application-preview-action,AI 工作台表格核对后直接走 UserAgentService 建单/提交,免去通用 Orchestrator 编排
- 平台管理员判定统一抽取 PLATFORM_ADMIN_IDENTITIES 常量,identity 与 role_codes 均支持 admin/superadmin,含 header 开关
- docker-compose 镜像补装 openssh-server
- 同步更新差旅/交通/通信等财务规则表与 reimbursements 端点测试
2026-06-20 14:41:59 +08:00
caoxiaozhu
304bbe1fd4 feat(web): 工作台 AI 模式报销预审与文档查询模型拆分
- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出
- PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示
- 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试
- 新增 AI 文档卡片背景资源
2026-06-20 10:17:37 +08:00
caoxiaozhu
3d69f8501f feat(risk): 申请单阶段风险可见性细化与规则表更新
- 申请单阶段将 policy/trip/amount 类风险对申请人可见、可自行修正,画像/审批流程类仍走领导可见
- 平台风险标记与语义推断统一采用该策略,更新对应单元测试
- 风险规则中 city_mismatch 等城市匹配规则移除 expense_application 阶段,仅保留 reimbursement
- 同步更新交通/通信/差旅/出差等财务规则表
2026-06-20 10:17:18 +08:00
caoxiaozhu
4d04f4e7af chore(gitignore): 忽略 .env 防止敏感配置入库
将 .env 从版本库移除(本地文件保留),并补充 .env.local 等本地变体忽略规则,
避免数据库密码、密钥等敏感配置进入 git 历史。
2026-06-18 22:14:53 +08:00
caoxiaozhu
3131112952 style(web): 调整 AI 模式用户消息气泡布局与引用图标
- 用户消息气泡的操作区与时间右对齐
- 引用按钮图标由 mdi-reply 改为 mdi-format-quote-open
2026-06-18 22:13:09 +08:00
caoxiaozhu
a2f67af13e docs: 新增 X-Financial 改进路线图 2026-06-18 22:12:38 +08:00
caoxiaozhu
0cde1f8990 feat(web): 工作台 AI 模式与差旅/风险建议交互优化
- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源
- 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore
  及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿
- 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局
- 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
2026-06-18 22:12:24 +08:00
caoxiaozhu
a6674a1e76 feat(steward): off_topic 场景细分与引导回复
- 将业务无关输入细分为 greeting / meaningless / off_business 三类场景
- 新增 StewardOffTopicAgent,用 function calling 生成管家语气引导回复
- steward endpoint 与 user_agent_application 串联 off_topic 引导话术
- 补充 planner 与 user agent 的 off_topic 覆盖测试
2026-06-18 22:12:10 +08:00
caoxiaozhu
127d603e7d feat(ontology): 仅放行财务业务相关问题的信号校验
- 新增 _has_supported_business_signal,在加载目录前拦截非财务问题并抛错
- 同步重构 ontology 服务测试覆盖业务信号判定分支
2026-06-18 22:12:00 +08:00
caoxiaozhu
3f17619e0c fix(auth): 登录目录就绪幂等化与并发控制
- employee/settings/user_session_metrics 的 ensure_*_ready 改为按 bind 缓存 + 锁,
  避免每次登录重复建表与并发场景下的竞态
- auth 登录链路先查员工再降级触发目录就绪,并吞掉查询期 SQLAlchemy 异常
- 默认管理员账号由 superadmin 迁移为 admin,兼容历史账号回填
- 补充登录降级与设置持久化相关测试
2026-06-18 22:11:53 +08:00
caoxiaozhu
59ba76c74a feat(startup): 服务端启动 bootstrap 与缓存预热
- 新增 STARTUP_BOOTSTRAP_ENABLED / STARTUP_CACHE_WARMUP_ENABLED 配置开关
- lifespan 拆分 bootstrap 步骤并后台线程预热缓存,失败可降级继续启动
- server_start.sh / web_start.sh 扩展 SERVER_PORT、启动与调度开关的 env 覆盖
- bootstrap_paddleocr_mobile.sh 改用 python3 并补充 poppler-utils 依赖
- 补充启动 bootstrap 与 env 覆盖优先级测试
2026-06-18 22:11:37 +08:00
caoxiaozhu
35372c6661 feat(rules): 更新差旅与通信费用等财务规则表 2026-06-18 22:11:13 +08:00
caoxiaozhu
38653fa365 chore(storage): 清理用户历史报销票据附件
移除测试期残留的 receipt_folder 附件与预览文件,后续通过归档目录维护。
2026-06-18 22:11:10 +08:00
caoxiaozhu
c28e99b714 chore(gitignore): 忽略 .nezha/ 与 .omo/ 本地工具目录 2026-06-18 22:11:06 +08:00
caoxiaozhu
43432534d8 feat(steward): 前端支持 off_topic 与引导话术
- assistantSessionScope.js:新增 ASSISTANT_SCOPE_ACTION_FILL_COMPOSER 常量
- assistantSuggestedActionPrefill.js:识别 fill_composer 与 payload.fill_text
- stewardPlanModel.js:normalizeStewardPlan 透传 suggestedPrompts;
  buildStewardPlanMessageText / buildStewardSuggestedActions
  新增 off_topic 分支,按钮填充输入框不提交
- useStewardPlanFlow.js:isPendingStewardActionMessage 排除 off_topic
- steward-plan-off-topic.test.mjs:覆盖 normalize/文案/按钮/兼容路径
2026-06-18 14:15:30 +08:00
caoxiaozhu
cce19e4c40 feat(steward): 拦截业务无关输入返回 off_topic 计划
- schemas/steward.py:StewardPlanResponse 新增 suggested_prompts 字段
- steward_planner.py:新增 STEWARD_BUSINESS_SIGNAL_KEYWORDS 与
  _is_business_irrelevant_input 守卫,在 build_plan 入口前置;
  新增 _build_off_topic_plan 构造 plan_status=off_topic 的引导计划
- steward_intent_agent.py:system prompt 追加业务无关约束
- test_steward_planner.py:覆盖 123/你好/纯标点走 off_topic,
  并验证正常业务输入不受守卫影响
2026-06-18 14:15:20 +08:00
caoxiaozhu
b8915a29c0 chore(storage): 归档用户报销票据附件 2026-06-17 14:39:41 +08:00
caoxiaozhu
4199feb681 test: 同步报销审批流与预算分析测试
- 新增预算审批合并、风险标记去重与占位条目校验用例
- 补充预算分析对当前审核人与财务的可见性断言
- 调整单据删除权限测试以匹配 admin 限制
2026-06-17 14:39:26 +08:00
caoxiaozhu
0fac8b615f feat(web): 优化差旅详情、风险建议卡片与文档中心交互
- 拆分阶段风险建议卡片样式到独立文件
- 完善差旅申请审批对话框与详情视图交互
- 调整文档中心列表共享样式与状态筛选
- 同步应用外壳、视图初始化与系统状态 composables
2026-06-17 14:39:12 +08:00
caoxiaozhu
a3e5295915 feat(rules): 更新财务差旅与通信费用规则表 2026-06-17 14:38:51 +08:00
caoxiaozhu
1f4681f486 feat(claim): 重构报销审批流并收敛风险标记
- 直属领导兼任部门 P8 预算审批人时合并预算审批,直接流转至财务审批
- 预算超过警戒值时强制要求预算管理者填写审批意见
- 新增风险标记去重工具,消除各审核阶段重复风险卡片
- 新增工作流修复 Mixin,纠正重复预算审批阶段的历史数据
- 收紧单据删除权限至 admin,放宽预算分析可见范围至当前审核人
- 提交校验放宽已上传票据条目的 OCR 字段缺失并忽略尾部占位条目
2026-06-17 14:38:07 +08:00
caoxiaozhu
09a66c72cb chore: 将 web 端口由 5173 调整为 5273 2026-06-17 14:37:50 +08:00
caoxiaozhu
0d525fa64c chore: 忽略 .codex-temp 工具临时目录 2026-06-17 14:34:36 +08:00
caoxiaozhu
470f343b29 fix(expense): narrow travel route risk indicators 2026-06-17 09:36:24 +08:00
caoxiaozhu
9f7b8b46a3 Refine travel reimbursement steward flow
Align planner, runtime rules, and policy assets so travel guidance
matches the updated reimbursement workflow.
2026-06-15 22:55:18 +08:00
caoxiaozhu
792741709a fix(claim): align risk advice with expense rows 2026-06-15 20:53:48 +08:00
caoxiaozhu
5747e85acf fix(risk): restore upload-time rule center review 2026-06-15 20:20:55 +08:00
Codex
8b952c9a26 refactor(travel): split reimbursement create workflow
完整修改内容:

- 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。
- 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。
- 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。
- 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。
- 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。

验证:

- node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块
- npm --prefix web run build
- node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs

说明:

- 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。
- 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。
- TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:53:23 +00:00
336fee9d93 chore: 忽略 .superpowers 工具缓存目录 2026-06-12 09:43:53 +08:00
caoxiaozhu
25724c354f feat: 同步报销流程与工作台改动 2026-06-09 08:32:00 +00:00
caoxiaozhu
e124e4bbcb feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
2026-06-06 17:19:07 +08:00
caoxiaozhu
f60cebadb8 feat: 小财管家意图规划与报销提交编排增强
- 完善管家意图识别、模型计划构建与规划器调度
- 重构差旅报销提交编排器与管家计划流程前端交互
- 优化报销消息项样式与文档中心视图
- 新增小财管家与附件上传风险前置复核设计文档
- 补充管家规划器与文档中心测试覆盖
2026-06-04 14:25:14 +08:00
caoxiaozhu
1cbf3fee44 feat: 报销预审会话状态管理与工作台交互增强
- 新增差旅报销会话状态管理与对话模型重构
- 增强风险观测服务与运行时聊天上下文作用域
- 优化工作台图标资源、助理意图识别与摘要工具
- 完善报销创建视图样式与差旅详情页标准调整交互
- 补充风险观测、运行时聊天与报销端点测试覆盖
2026-06-04 11:03:29 +08:00
caoxiaozhu
87da5df91b feat: 风险可见性控制与差旅详情页交互优化
- 新增风险可见性工具函数与风险日趋势图表组件
- 优化差旅请求详情页费用模型与视图交互
- 完善顶部导航栏样式与应用壳路由逻辑
- 补充风险可见性、风险看板与差旅详情测试覆盖
2026-06-03 22:15:45 +08:00
caoxiaozhu
75d5c178e1 feat(workbench): persist topbar notification state 2026-06-03 21:43:35 +08:00
caoxiaozhu
b9826a1985 fix: keep adjusted risks visible to reviewers 2026-06-03 19:14:40 +08:00
caoxiaozhu
0f8bc4071a fix: preserve reviewer risk notice after standard adjustment 2026-06-03 19:10:29 +08:00
caoxiaozhu
cb36d78fa2 fix: 优化顶部导航栏布局与工作台摘要展示并清理旧票据数据 2026-06-03 17:40:52 +08:00
caoxiaozhu
8e2477587f fix: handle risk explanation standard adjustment 2026-06-03 17:31:40 +08:00
caoxiaozhu
67b81a1bd8 fix(workbench): replay profile radar animation 2026-06-03 17:31:12 +08:00
caoxiaozhu
9c24a852e7 fix(workbench): remount expense stats chart on reopen 2026-06-03 17:22:48 +08:00
caoxiaozhu
95956afbc6 fix(notifications): refine bell notification center 2026-06-03 17:16:09 +08:00
caoxiaozhu
c73178b65d fix(documents): move unread notice into bell 2026-06-03 17:05:34 +08:00
caoxiaozhu
8c2f301d85 fix(documents): sort newest rows first 2026-06-03 16:52:49 +08:00
caoxiaozhu
4717ee6086 fix(documents): refine unread badges and mark all read 2026-06-03 16:46:13 +08:00
caoxiaozhu
513ff909f9 fix: remove manual expense detail add action 2026-06-03 16:44:06 +08:00
caoxiaozhu
92198549f6 fix: require explicit transport mode for applications 2026-06-03 16:36:02 +08:00
caoxiaozhu
59d3bf0f00 fix(auth): keep admin out of personal workbench 2026-06-03 16:31:27 +08:00
caoxiaozhu
04f0951b3d fix: restrict application linking for reimbursement drafts 2026-06-03 16:28:09 +08:00
caoxiaozhu
8887cf5a27 fix(workbench): stretch profile tag card 2026-06-03 15:53:30 +08:00
caoxiaozhu
34457f9c3e feat: 本体字段治理与风险规则模板执行器重构
- 新增本体字段注册表与字段治理审计脚本
- 重构风险规则模板执行器、DSL 验证与清单分类器
- 完善票据夹服务与差旅请求详情页交互
- 优化趋势图表与总览页数据展示
- 增强报销平台风险分级与模拟公司筛选
- 补充本体字段、风险规则生成与票据夹服务测试覆盖
2026-06-03 15:46:56 +08:00
caoxiaozhu
e12b140508 fix(workbench): show single expense distribution chart 2026-06-03 15:46:51 +08:00
caoxiaozhu
18d716bc6b feat(workbench): show expense distribution as donut chart 2026-06-03 15:31:09 +08:00
caoxiaozhu
74d488adfa fix(workbench): center progress expense type 2026-06-03 15:21:38 +08:00
caoxiaozhu
31052d0b98 feat(workbench): keep progress detail return context 2026-06-03 15:14:44 +08:00
caoxiaozhu
20cb60e247 feat(workbench): add expense stats detail modal 2026-06-03 14:59:55 +08:00
caoxiaozhu
3130c42d76 feat(workbench): separate stale progress items 2026-06-03 12:38:17 +08:00
caoxiaozhu
6fc5e66ea1 feat(workbench): show progress update time first 2026-06-03 12:28:21 +08:00
caoxiaozhu
27dd2f0a0d feat(dashboard): reorganize budget and risk cards 2026-06-03 10:47:11 +08:00
caoxiaozhu
faa39e6c06 test(dashboard): cover shared loading overlay 2026-06-03 09:45:06 +08:00
caoxiaozhu
d060f89d30 style(dashboard): reuse shared loading overlay 2026-06-03 09:43:36 +08:00
caoxiaozhu
0d6327a990 feat(dashboard): polish risk and digital employee boards 2026-06-03 09:41:32 +08:00
caoxiaozhu
15006a05a7 feat: 数字员工财务报告体系与定时提醒及看板快照调度
- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
2026-06-03 09:25:23 +08:00
caoxiaozhu
0c74b4ab4a feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选
- 引入 expense_claim_status_registry 统一报销状态流转
- 完善报销草稿流程、Item Sync 与本体解析器
- 优化总览页趋势图、分页组件与请求进度步骤
- 增强报销申请快速预览、本体工具与详情展示
- 新增半年报销模拟数据种子脚本与状态审计工具
- 补充财务看板、报销状态注册与模拟数据测试覆盖
2026-06-02 16:22:59 +08:00
caoxiaozhu
ca691f3ee0 feat: 优化差旅报销预审流程与个人工作台 UI 体系
- 完善 user_agent_application 申请差旅报销预审槽位与消息组装
- 增强预算助理报告与风险建议卡片交互
- 重构登录页视觉样式与移动端响应式适配
- 优化个人工作台、文档中心、政策中心、员工管理等页面布局
- 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型
- 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
2026-06-02 14:01:51 +08:00
caoxiaozhu
92444e7eae feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
2026-06-01 17:07:14 +08:00
caoxiaozhu
7989f3a159 feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
2026-05-30 15:46:51 +08:00
caoxiaozhu
4c59941ec6 feat: 新增票据夹模块并优化 OCR 与员工画像服务
后端新增票据夹端点、数据模型和服务模块,优化 OCR 端点
Schema 和附件操作逻辑,完善员工行为画像服务和辅助函数,
前端新增票据夹视图和服务层,优化文档中心样式和侧边栏导
航,完善员工画像详情弹窗和权限控制,补充单元测试。
2026-05-29 14:51:18 +08:00
caoxiaozhu
678f64d772 feat: 统一后端分页查询与前端服务层适配
后端新增通用分页模块,为报销单、员工、预算、agent 资产等
端点统一接入分页参数和游标查询,优化 repository 层分页实
现,前端服务层适配分页响应结构,完善预算图表和全局样式,
优化侧边栏和企业选择器组件,引入 Element Plus 插件注册。
2026-05-29 14:11:06 +08:00
caoxiaozhu
e080105f9f feat(ui): finalize shared shells and loading states 2026-05-29 13:17:39 +08:00
caoxiaozhu
64cc76c970 refactor(audit): reuse list shells and split models 2026-05-29 10:13:49 +08:00
caoxiaozhu
99e90798d2 refactor(audit): split list detail flows 2026-05-29 09:44:03 +08:00
caoxiaozhu
064eeb614f refactor(ui): introduce shared list detail shells 2026-05-28 22:49:58 +08:00
caoxiaozhu
b383244a29 chore: backup workspace before list detail shell refactor 2026-05-28 22:33:53 +08:00
caoxiaozhu
e384318046 feat: 引入 ECharts 统一图表并完善员工画像标签分页
后端优化员工行为画像服务和辅助函数,完善系统设置模型和
配置持久化,前端引入 ECharts 替换所有图表组件实现统一
渲染,新增员工画像标签分页器和数字员工工作记录组件,优
化工作台响应式布局和登录页过渡动画,完善预算中心和数字
员工页面样式细节。
2026-05-28 16:24:59 +08:00
caoxiaozhu
8a4a777be7 feat: 新增员工行为画像算法与费用风险标签体系
后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
2026-05-28 12:09:49 +08:00
caoxiaozhu
04cd6d0f81 feat: 新增数字员工管理页面与工作台首页重构
后端优化 agent 资产种子初始化和常量配置,前端新增数字员工
视图和调度对话框组件,重构个人工作台首页布局和洞察面板,
完善审计页面数字员工详情和运行时模型,优化侧边栏导航和图
标配置,新增工作台摘要和工作台数据模块,补充单元测试。
2026-05-28 09:30:34 +08:00
caoxiaozhu
d4d5d40569 feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像
算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常
量和明细同步,更新差旅报销规则电子表格,前端新增预算分析
组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧
边栏和顶栏样式,补充单元测试。
2026-05-27 17:31:27 +08:00
caoxiaozhu
cbb98f4469 feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态
流转,前端完善退回对话框和审批交互组件,新增报销申请关联
模型,优化文档中心行数据和审批收件箱工具函数,增强引导
流程和会话模型,补充单元测试覆盖。
2026-05-27 14:35:17 +08:00
caoxiaozhu
7d32eae74e feat: 新增预算助手报告组件并优化报销交互细节
新增预算助手报告视图模型和组件,优化报销洞察面板和消息项
样式细节,完善预算中心页面布局和文档中心视图,增强报销创
建会话管理和提交编排器,调整 Vite 构建配置,补充单元测试。
2026-05-27 12:27:17 +08:00
caoxiaozhu
b1a9c8a194 fix: 优化报销创建页面样式与洞察面板交互
修复侧边栏和审计视图样式细节,完善差旅报销洞察面板和消息
组件布局,优化报销创建页面会话管理和流程状态持久化,增强
申请预览工具函数和导航图标,补充单元测试。
2026-05-27 10:32:08 +08:00
caoxiaozhu
2dcc72102d style: 全局 UI 主题皮肤重构与样式模块化
引入 Element Plus 主题定制和主题皮肤 composable,将全局
样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等),
统一色彩变量和间距规范,重构所有视图和组件样式以适配新
主题系统,优化图表和知识图谱组件视觉表现,提取审计和差
旅报销相关子组件。
2026-05-27 09:17:57 +08:00
caoxiaozhu
df49103f23 feat: 完善预算中心图表与确认对话框交互
后端预算服务增加汇总查询和辅助计算,前端预算中心优化趋
势图组件和数据展示,增强确认对话框通用性和样式,完善预
算编辑对话框布局,补充预算端点单元测试。
2026-05-26 20:07:56 +08:00
caoxiaozhu
e7bef0883d feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额
查询,清理旧生成规则文件并替换为按严重等级分类的差旅风
险规则库,优化认证权限和报销单访问策略,新增财务规则目
录和演示数据构建脚本,前端预算中心增加对话框交互,完善
审计页面运行时模型和元数据展示,补充单元测试。
2026-05-26 17:29:35 +08:00
caoxiaozhu
e1e515ecae feat: 新增预算中心本体与风险规则评分回填
后端新增预算本体解析模块和风险规则评分回填服务,优化规则
生成本体对齐和提示词构建,增强费用类型关键词和本体验证,
完善报销查询和审计接口,前端预算中心页面增加对话框和本
体工具函数,重构审计页面元数据和视图模型,补充单元测试。
2026-05-26 12:16:20 +08:00
caoxiaozhu
0e861d8fa6 feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
2026-05-26 09:15:14 +08:00
caoxiaozhu
d0e946cf47 feat: 完善文档中心与报销申请交互及侧边栏重构
后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
2026-05-25 13:35:39 +08:00
caoxiaozhu
50b1c3f9a9 feat: 增强规则资产管理与审计页面运行时调试
后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险
规则生成模板执行和员工数据模型字段,知识库 RAG 增强本
地回退和文档提取能力,清理旧风险规则文件统一由生成引擎
管理,前端审计页面增加运行时调试面板和规则资产编辑交互,
补充单元测试覆盖。
2026-05-24 21:44:17 +08:00
caoxiaozhu
575f093c74 feat: 新增风险规则生成引擎与知识图谱可视化
后端新增风险规则自动生成和模板执行服务,支持从规则资产
批量生成并持久化风险规则文件;知识库入库日志增强图谱
查询和本地 RAG 回退,前端审计页面增加风险规则模型和流
程图组件,知识入库面板拆分为图谱可视化子组件,报销创
建页面增加引导式流程模型,更新知识库索引数据。
2026-05-23 19:54:42 +08:00
caoxiaozhu
5b388d08c0 feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优
化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件
和 Hermes 员工同步子面板并重构样式,新增日志详情组件和
知识入库日志模型,补充单元测试覆盖。
2026-05-22 23:47:28 +08:00
caoxiaozhu
88ff04bef8 feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
2026-05-22 16:00:19 +08:00
caoxiaozhu
1f15699013 feat(mobile): track mobile app scaffold 2026-05-22 12:41:45 +08:00
caoxiaozhu
222ba0bfdc refactor(server): split oversized backend services 2026-05-22 10:42:31 +08:00
caoxiaozhu
2e57702638 docs: add agent code size standards 2026-05-22 10:42:19 +08:00
caoxiaozhu
5fe3b201d9 feat: 重构报销单服务并完善前端提交与审核交互
重构 expense_claims 服务模块结构并优化差旅票据审核逻辑,
增强用户代理服务的票据类型识别,前端报销创建页面拆分为
附件模型和会话模型模块,重构提交编排器和草稿关联确认流
程,更新知识库索引,补充单元测试。
2026-05-22 08:58:59 +08:00
caoxiaozhu
f6f787ff38 refactor(frontend): split large reimbursement and audit modules 2026-05-21 23:53:03 +08:00
caoxiaozhu
2908dda024 fix(reimbursement): harden assistant draft and claim cleanup 2026-05-21 23:52:34 +08:00
caoxiaozhu
e701fa01da feat: 增强差旅报销审核流程与票据智能推理
优化本体解析和编排器的差旅场景处理能力,完善报销单草稿
保存和费用明细同步逻辑,前端报销创建页面增加行程推理和
票据审核交互,新增助手会话快照工具函数,补充单元测试。
2026-05-21 16:09:47 +08:00
caoxiaozhu
f28d7e6d16 feat: 完善差旅票据行程提取与费用明细回填逻辑
增强文档智能识别的票据场景关键词和字段提取能力,优化
会话关联草稿报销单的解析路径,修复费用明细合并和票据
去重边界问题,前端改进报销创建和审批详情交互,补充单
元测试覆盖。
2026-05-21 14:24:51 +08:00
caoxiaozhu
b183b0bd5e feat: 细化差旅票据费用明细分类并自动计算出差补贴
将差旅费用明细拆分为火车票、机票、住宿票、乘车等细分类
型,根据票据字段自动生成行程/事由描述,结合规则引擎自
动计算出差补贴金额,前端适配费用明细编辑和差旅票据审
核交互,补充单元测试覆盖。
2026-05-21 10:57:06 +08:00
caoxiaozhu
8f65661809 feat: 增加差旅报销标准测算和财务终审流程
新增差旅报销测算接口及 Spreadsheet 规则解析,审批流程拆分
直属领导审批与财务终审两阶段并细分权限,修复 PDF 文本层
缺失时自动回退 OCR,提交后清理关联会话,前端适配审批流
交互并补充单元测试。
2026-05-21 09:28:33 +08:00
caoxiaozhu
002bf4f756 feat: 完善报销单审批流程及退回原因追踪
新增直属领导审批通过接口和审批待办列表查询,报销单退回
支持原因码分类和审批环节标记,优化票据附件去重和路径
回退查找,前端新增退回原因对话框、审批收件箱和工作台
图标组件,补充工具函数和单元测试覆盖。
2026-05-20 21:00:47 +08:00
caoxiaozhu
f8b25a7ccc fix: 修复员工服务、报销单审批及前端交互细节
- 修复员工创建时组织架构关联与邮箱校验逻辑
- 修复报销单API端点参数及预审流程调用
- 优化审批中心、差旅详情等前端页面交互
- 更新侧边栏导航与请求视图模型
- 补充员工服务与报销单相关测试用例
2026-05-20 14:32:35 +08:00
caoxiaozhu
d7e98a58b9 feat: 增强员工管理与报销单全流程功能
- 新增员工Excel导入服务(employee_spreadsheet)及导入/导出API端点
- 员工服务增加批量创建、邮箱唯一校验、组织架构关联等能力
- 报销单提交补充身份回填、部门信息透传及预审结果展示优化
- 认证流程增加部门信息(departmentName)并在schema中同步扩展
- 用户Agent服务增加部门关联与报销单回填逻辑
- 前端员工管理页面全面重构,新增导入导出、搜索过滤、分页等功能
- 前端审批中心、审计、差旅报销等视图交互与样式优化
- 新增TableLoadingState共享组件及员工导入测试用例
2026-05-20 14:21:56 +08:00
caoxiaozhu
57957d11a0 feat: 重构报销单AI预审流程并添加平台风险规则引擎
- 将AI验审改为AI预审,高风险不再拦截而是随单流转给审批人复核
- 新增平台风险规则评估引擎,支持事由过短、票据异常、重复发票等多种评估器
- 用户上下文增加部门信息(department_name),认证流程同步关联组织架构
- 规则scenario_json改为中文标签(差旅/费用科目),统一场景分类
- 新增orchestrator审核流程测试用例
- 前端更新审计视图、差旅报销等相关页面
2026-05-20 09:36:01 +08:00
caoxiaozhu
2574bc81d1 chore: 更新个人工作台和差旅报销相关功能 2026-05-19 17:24:13 +00:00
caoxiaozhu
54ffef66d3 feat: 添加风险规则及 agent assets 功能增强 2026-05-19 16:19:03 +00:00
caoxiaozhu
d460ee0fe7 fix(agent): 修复规则中心表格版本和修改记录
补齐规则资产 JSON 读写接口和前端调用,修复 AuditView 导入缺失。

Excel 在线编辑改为比对所有页签并生成最近修改记录,版本快照统一保存到 rules/finance-rules/.versions。

隔离规则表测试存储,避免测试或旧入口写入真实规则目录与 storage/agent_assets。
2026-05-19 15:41:53 +00:00
caoxiaozhu
9472813739 refactor: 重构 AuditView 和 TravelReimbursementCreateView 相关代码
- 优化 agent_assets、agent_foundation、user_agent 服务层结构
- 更新 AuditView 视图和脚本
- 更新 TravelReimbursementCreateView 脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 20:23:58 +08:00
caoxiaozhu
dc007f948a feat(rules): 添加公司通信费报销规则 Excel 文件并更新差旅费规则表 2026-05-18 10:06:23 +00:00
caoxiaozhu
9db663e81f feat(AuditView): 扩展场景标签配置
- 新增通信费报销(communication_expense)和费用标准(expense_standard)场景标签
2026-05-18 10:02:04 +00:00
caoxiaozhu
813ac81950 feat(finance): 添加公司通信费报销规则
- 新增通信费报销规则代码和文件名常量
- 在初始化数据中创建公司通信费报销规则资产
- 添加对应的版本和审核记录
- 标记为 v1.0.0 版本并审核通过
2026-05-18 10:01:40 +00:00
caoxiaozhu
9902a3b968 test: 添加 OnlyOffice 回调摘要测试 2026-05-18 09:45:05 +00:00
caoxiaozhu
29df4eee3b test: 添加规则服务与电子表格变更记录的测试用例
- AgentAssetService 测试覆盖版本创建、送审、审核通过等流程
- 新增电子表格变更记录的前端测试
2026-05-18 09:44:04 +00:00
caoxiaozhu
5106d286a1 feat(agent_assets): 添加规则版本送审时的命名副本创建逻辑
当提交的版本与当前工作版本不同时,自动创建命名副本:
- 新增 _create_named_working_copy_for_review 方法处理送审时的版本复制
- 支持将工作版本快照复制为指定版本进行送审
- 新增 AgentAssetSpreadsheetChangeRecordRead schema
- API 端点新增 /rules/{id}/spreadsheet-versions/{version}/change-records 接口
2026-05-18 09:42:23 +00:00
caoxiaozhu
64ec27949f feat(AuditView): 删除差旅费报销规则详情的审核按钮并优化显示
- 删除详情中间区域的提交审核、审核通过、驳回版本三个按钮
- 删除底部 footer 的提交审核、审核通过、驳回版本、正式上线四个按钮
- 将下载 Excel 按钮改为下载表格
- 简化电子表格编辑器的 meta 信息显示
2026-05-18 09:39:41 +00:00
caoxiaozhu
8814fe7dfa chore: 更新配置和构建脚本 2026-05-18 02:53:06 +00:00
caoxiaozhu
9b97f456cf test: 添加资产和运行服务测试用例 2026-05-18 02:52:13 +00:00
caoxiaozhu
9d90bf5299 feat: 更新前端UI,增强审计和日志视图功能 2026-05-18 02:51:25 +00:00
caoxiaozhu
35a3783481 feat: 更新 user_agent 服务,增强用户代理功能 2026-05-18 02:50:32 +00:00
caoxiaozhu
4414ffb34c feat: 增强知识库功能,优化索引和RAG检索 2026-05-18 02:49:39 +00:00
caoxiaozhu
55e0591a5e feat: 增强 agent_assets 功能,支持更多资产操作 2026-05-18 02:48:51 +00:00
caoxiaozhu
68f663f2f4 feat: 重构知识库系统,移除Hermes集成,增强RAG和同步功能
主要变更:
- 移除Hermes智能体及相关回调服务
- 新增知识库RAG、同步、调度、规范化和索引任务服务
- 重构orchestrator服务,增强运行时聊天功能
- 更新前端聊天、政策制度、设置等页面样式和逻辑
- 更新expense_claims和document_intelligence服务
- 删除llm_wiki相关服务和测试文件
- 更新docker-compose配置和启动脚本
2026-05-17 08:38:41 +00:00
caoxiaozhu
212c935308 feat: 集成Hermes智能体系统,增强聊天和差旅报销功能 2026-05-16 06:14:08 +00:00
caoxiaozhu
763afa0ee2 chore(server): 更新知识库索引文件,包含文档元数据和知识候选摘要信息 2026-05-15 09:38:04 +00:00
caoxiaozhu
72ea05ae0d feat(web): 更新应用外壳路由视图和政策制度页面,增强前端路由嵌套和页面展示能力 2026-05-15 09:37:42 +00:00
caoxiaozhu
02f54ea208 feat(web): 新增日志详情和日志列表页面,包含完整的视图组件和业务脚本逻辑 2026-05-15 09:37:24 +00:00
caoxiaozhu
94adb82acc feat(web): 更新路由配置和图标数据定义,优化前端路由导航和UI图标管理 2026-05-15 09:36:33 +00:00
caoxiaozhu
1bab7c22d7 feat(web): 新增系统日志服务代理和智能体资源服务,添加访问控制工具函数,增强前端服务层能力 2026-05-15 09:36:16 +00:00
caoxiaozhu
891cecb4a8 feat(web): 新增导航组合式函数,更新应用外壳组合式函数,增强前端路由和状态管理 2026-05-15 09:36:32 +00:00
caoxiaozhu
3a3000cacd feat(web): 更新侧边栏和顶部导航组件,新增日志趋势图表组件,增强前端展示能力 2026-05-15 09:36:17 +00:00
caoxiaozhu
e9735f1606 style(web): 更新应用样式和政策制度页面样式,新增日志详情和日志列表页面样式 2026-05-15 09:35:58 +00:00
caoxiaozhu
7066707492 docs: 添加日志管理界面截图,用于文档说明 2026-05-15 09:35:03 +00:00
caoxiaozhu
11435468f1 feat(server): 新增智能体运行服务,优化知识库索引存储结构,增强运行时状态管理 2026-05-15 09:34:21 +00:00
caoxiaozhu
6793b6f832 feat(server): 重构知识库服务和路由配置,优化LLM维基知识管理接口,增强知识检索能力 2026-05-15 09:33:59 +00:00
caoxiaozhu
7a3feb14a0 docs: 更新Hermes MVP开发计划文档,优化第六天智能体开发任务描述 2026-05-15 09:33:18 +00:00
caoxiaozhu
1d5d009bc7 feat(server): 新增系统日志服务模块,包含API端点、schema定义和服务实现,用于系统操作日志记录和查询 2026-05-15 09:33:20 +00:00
caoxiaozhu
8691385a8e feat(server): 更新LLM维基服务实现,新增知识库索引存储支持 2026-05-15 06:59:12 +00:00
caoxiaozhu
5b4e2b5d84 feat(server): 更新知识库schema定义和服务实现,优化索引管理逻辑 2026-05-15 06:58:39 +00:00
caoxiaozhu
4f3556a38b docs: 新增agent开发文档和风险评估文档 2026-05-15 06:58:21 +00:00
caoxiaozhu
45abd36430 feat(server): 新增费用规则运行时服务和系统赫尔墨斯服务,增强报销规则执行和系统监控能力 2026-05-15 06:58:03 +00:00
caoxiaozhu
ea339d883a feat(server): 新增LLM维基服务,实现大语言模型知识库管理和查询功能 2026-05-15 06:57:45 +00:00
caoxiaozhu
244b3a58f7 feat(web): 更新审批中心、审计、政策制度页面及对应的业务脚本,增强前端交互逻辑 2026-05-15 06:57:07 +00:00
caoxiaozhu
344ac126b3 feat(web): 更新个人工作台组件和应用外壳组合式函数,新增请求数据组合式函数 2026-05-15 06:56:51 +00:00
caoxiaozhu
d8d0415bf4 style(web): 更新审批中心、审计、政策制度页面样式,增强页面布局和视觉效果 2026-05-15 06:56:39 +00:00
caoxiaozhu
511337df95 feat(server): 更新用户代理服务,新增编排器服务测试用例,完善服务编排能力 2026-05-15 06:56:26 +00:00
caoxiaozhu
c9cc0b0641 feat(server): 扩展智能体基础服务,新增端点测试和资产服务测试用例 2026-05-15 06:56:14 +00:00
caoxiaozhu
68a448a551 feat(server): 优化费用报销服务,改进报销单创建和数据校验逻辑,增强单元测试覆盖 2026-05-15 06:56:00 +00:00
caoxiaozhu
4b1dae7ebc feat(server): 扩展知识库服务,添加knowledge API端点和schema定义,前端新增knowledge服务模块 2026-05-15 06:56:17 +00:00
caoxiaozhu
7209c75ad8 style(web): 更新差旅报销创建页面样式和业务脚本,增强前端交互和状态管理 2026-05-14 15:43:10 +00:00
caoxiaozhu
98f68c47b0 feat(web): 更新侧边栏组件和个人工作台组件,增强导航和业务展示功能 2026-05-14 15:43:28 +00:00
caoxiaozhu
056d6dbe22 feat(web): 新增前端编排器和报销单服务模块,封装对应的API调用方法 2026-05-14 15:42:47 +00:00
caoxiaozhu
910c959829 feat(server): 新增编排器和报销单API端点,扩展智能体对话服务接口 2026-05-14 15:42:39 +00:00
caoxiaozhu
c99a423f6a feat(server): 扩展文档智能识别服务,新增Azure Document Intelligence集成和测试用例 2026-05-14 15:42:29 +00:00
caoxiaozhu
e21f0d82e9 feat(server): 重构报销单服务,优化费用报销流程和数据校验逻辑,包含schema定义和服务实现 2026-05-14 15:42:45 +00:00
caoxiaozhu
ad16358e71 feat(server): 更新用户代理服务架构,增强用户行为追踪和会话管理功能,包含schema、service和单元测试 2026-05-14 15:42:33 +00:00
caoxiaozhu
fad583ee7c feat(server): 新增编排器和报销单API接口,扩展服务编排和报销管理端点 2026-05-14 15:42:22 +00:00
caoxiaozhu
af98acceb3 feat(web): 优化应用外壳组合式函数和差旅报销创建脚本,增强前端状态管理 2026-05-14 12:34:43 +00:00
caoxiaozhu
3bc7668f6c feat(server): 优化费用报销服务,增强报销单数据校验和状态流转逻辑 2026-05-14 12:34:54 +00:00
caoxiaozhu
32a43cf6bb feat(server): 新增编排器服务,实现服务间流程编排和任务调度功能 2026-05-14 12:34:12 +00:00
caoxiaozhu
dbf6c36c65 feat(server): 重构用户代理服务模块,优化用户行为跟踪和代理逻辑 2026-05-14 12:33:58 +00:00
caoxiaozhu
f36e1bbee7 docs: 添加测试流程配置图 2026-05-14 09:34:32 +00:00
caoxiaozhu
1cbf790fcf feat(web): 更新请求列表、差旅报销创建、差旅请求详情页面及对应的业务脚本逻辑 2026-05-14 09:33:23 +00:00
caoxiaozhu
f9f91380ad style(web): 更新审批中心、审计、员工管理、请求列表、差旅报销创建、差旅请求详情等页面的样式文件 2026-05-14 09:33:03 +00:00
caoxiaozhu
bac3f00ae4 feat(server): 新增运行时聊天服务和用户代理服务,支持智能对话和用户行为分析 2026-05-14 09:32:49 +00:00
caoxiaozhu
b0fef46fc6 feat(server): 重构费用报销服务,优化报销单创建和审批流程逻辑 2026-05-14 09:32:36 +00:00
caoxiaozhu
8b39f48dec feat(server): 新增文档智能识别服务,扩展OCR接口支持 Azure Document Intelligence 2026-05-14 09:32:15 +00:00
caoxiaozhu
8adeefe4a9 feat(web): 更新TableEmptyState空状态组件和RequestsView脚本,增强表格空状态展示和请求列表逻辑 2026-05-14 07:16:22 +00:00
caoxiaozhu
7df62edb78 style(web): 更新审批中心、审计、员工管理、请求列表页面的样式文件,增加内边距和调整布局间距 2026-05-14 07:16:35 +00:00
caoxiaozhu
4a72b977ba feat(web): update travel request and reimbursement views 2026-05-14 07:10:46 +00:00
caoxiaozhu
476d5fdf93 feat(web): update TopBar component and useAppShell composable 2026-05-14 07:10:31 +00:00
caoxiaozhu
64ea1bc5fd style(web): update travel views styles 2026-05-14 07:10:20 +00:00
caoxiaozhu
f53c343cd3 feat(web): update views
- AppShellRouteView.vue: update app shell route view
- PoliciesView.vue: update policies view
- TravelReimbursementCreateView.vue: update travel reimbursement create view
2026-05-14 03:15:22 +00:00
caoxiaozhu
f65ddb52f9 feat(web): update useAppShell composable
- useAppShell.js: update app shell composable
2026-05-14 03:15:04 +00:00
caoxiaozhu
12da82409e feat(web): update SidebarRail component
- SidebarRail.vue: update sidebar rail component
2026-05-14 03:14:15 +00:00
caoxiaozhu
87b6a6c21d style(web): update app and policies styles
- app.css: update app-level styles
- policies-view.css: update policies view styles
2026-05-14 03:12:15 +00:00
caoxiaozhu
00b72c3d43 style(web): update app styles and AppShellRouteView
- app.css: update app-level styles
- AppShellRouteView.vue: update app shell route view
2026-05-14 03:00:56 +00:00
caoxiaozhu
b9cb6d9253 feat(web): update composables and utils
- useAppShell.js: update app shell composable
- useNavigation.js: update navigation composable
- utils/accessControl.js: update access control utility
2026-05-14 02:59:54 +00:00
caoxiaozhu
736cc6b52b feat(web): update employee management view
- EmployeeManagementView.vue: update employee management view component
- scripts/EmployeeManagementView.js: update employee management view logic
2026-05-14 02:58:55 +00:00
caoxiaozhu
feacf2765d refactor(web): update service clients
- services/agentAssets.js: update agent assets service client
- services/api.js: update API service client
- services/employees.js: update employees service client
2026-05-14 02:58:35 +00:00
caoxiaozhu
3c28cab288 test(backend): update auth and employee service tests
- tests/test_auth_service.py: update auth service tests
- tests/test_employee_service.py: update employee service tests
2026-05-14 02:57:00 +00:00
caoxiaozhu
53c060de97 refactor(backend): update employees endpoint and service
- endpoints/employees.py: update employees API endpoint
- services/employee.py: update employee service
2026-05-14 02:55:58 +00:00
caoxiaozhu
c0401dbd0d refactor(web): update API service, view script and add tests
Frontend:
- services/api.js: update API service client
- views/scripts/EmployeeManagementView.js: update employee management view script
- tests/api-request.test.mjs: update API request tests

Scripts:
- create_employee.sh: add employee creation script
2026-05-14 02:25:15 +00:00
caoxiaozhu
3965c1ec42 test(backend): update employee service tests
- tests/test_employee_service.py: update employee service tests
2026-05-14 02:23:33 +00:00
caoxiaozhu
1b371ad7bb refactor(backend): update employee schema
- schemas/employee.py: update employee data schemas
2026-05-14 02:21:45 +00:00
caoxiaozhu
89f1bd613d test(backend): update orchestrator service tests
- tests/test_orchestrator_service.py: update orchestrator service tests
2026-05-13 15:43:56 +00:00
caoxiaozhu
98792c7584 refactor(backend): update expense claims service and tests
- services/expense_claims.py: update expense claims service
- tests/test_expense_claim_service.py: update expense claim service tests
2026-05-13 15:42:25 +00:00
caoxiaozhu
edb484e2f6 refactor: update orchestrator service and travel form view
- services/orchestrator.py: update orchestrator service
- views/TravelReimbursementCreateView.vue: update travel form view
- views/scripts/TravelReimbursementCreateView.js: update travel form script
2026-05-13 15:40:41 +00:00
caoxiaozhu
f804a23239 feat(web): update useRequests composable
- useRequests.js: update requests composable
2026-05-13 15:38:59 +00:00
caoxiaozhu
ada0eb40ca feat(web): update TopBar component
- TopBar.vue: update top bar component
2026-05-13 15:37:32 +00:00
caoxiaozhu
63c94da216 style(web): update travel reimbursement create view styles
- travel-reimbursement-create-view.css: update travel form styles
2026-05-13 15:35:53 +00:00
caoxiaozhu
68a3907920 refactor(backend): update expense claims service and tests
- services/expense_claims.py: update expense claims service
- tests/test_orchestrator_service.py: update orchestrator service tests
2026-05-13 15:33:35 +00:00
caoxiaozhu
44b2838a12 refactor(backend): update service layers
- services/agent_conversations.py: update agent conversations service
- services/agent_foundation.py: update agent foundation service
- services/orchestrator.py: update orchestrator service
- services/user_agent.py: update user agent service
2026-05-13 15:31:04 +00:00
caoxiaozhu
de51ed2e9f chore(backend): update config and user agent schema
- core/config.py: update application configuration
- schemas/user_agent.py: update user agent data schemas
2026-05-13 15:29:25 +00:00
caoxiaozhu
14379c4e59 style(web): update travel reimbursement create view styles
- travel-reimbursement-create-view.css: update travel form styles
2026-05-13 13:20:24 +00:00
caoxiaozhu
bc7aff8858 refactor(backend): update user agent service and tests
- services/user_agent.py: update user agent service
- tests/test_orchestrator_service.py: update orchestrator tests
2026-05-13 13:18:05 +00:00
caoxiaozhu
da7684a6bb refactor(web): update view scripts
- TravelReimbursementCreateView.js: update travel form script
- TravelRequestDetailView.js: update travel detail script
2026-05-13 13:16:11 +00:00
caoxiaozhu
c6a599fcec feat(web): update views
- AppShellRouteView.vue: update app shell route view
- TravelReimbursementCreateView.vue: update travel form view
- TravelRequestDetailView.vue: update travel request detail view
2026-05-13 13:14:48 +00:00
caoxiaozhu
778d203443 feat(web): update composable and service
- useNavigation.js: update navigation composable
- services/orchestrator.js: update orchestrator service client
2026-05-13 13:14:17 +00:00
caoxiaozhu
eec4efe207 feat(web): update components
- PersonalWorkbench.vue: update personal workbench component
- SidebarRail.vue: update sidebar rail component
- TopBar.vue: update top bar component
- ConfirmDialog.vue: update confirm dialog component
2026-05-13 13:12:28 +00:00
caoxiaozhu
97b0851e26 style(web): update travel request styles
- travel-reimbursement-create-view.css: update travel form styles
- travel-request-detail-view.css: update travel detail styles
2026-05-13 13:10:25 +00:00
caoxiaozhu
a6526c5159 test(backend): update orchestrator service tests
- test_orchestrator_service.py: update orchestrator service tests
2026-05-13 13:08:22 +00:00
caoxiaozhu
70cff69b7f refactor(backend): update orchestrator endpoint and services
- endpoints/orchestrator.py: update orchestrator API endpoint
- services/agent_conversations.py: update agent conversations service
- services/orchestrator.py: update orchestrator service
- services/user_agent.py: update user agent service
2026-05-13 13:06:52 +00:00
caoxiaozhu
0f7bd43ce3 test(web): update travel request detail tests and styles
- test_reimbursement_endpoints.py: update reimbursement endpoint tests
- travel-request-detail-view.css: update travel request detail styles
- TravelRequestDetailView.vue: update travel request detail view
2026-05-13 06:56:30 +00:00
caoxiaozhu
755f935c9d refactor(web): update composables, utils and scripts
- useRequests.js: update requests composable
- utils/requestViewModel.js: update request view model utility
- scripts/TravelRequestDetailView.js: update travel detail view script
2026-05-13 06:55:23 +00:00
caoxiaozhu
999872a060 refactor(backend): update financial record model, schema and expense claims
- models/financial_record.py: update financial record model
- schemas/reimbursement.py: update reimbursement schema
- services/expense_claims.py: update expense claims service
2026-05-13 06:54:27 +00:00
caoxiaozhu
9459476d82 feat(web): add TableEmptyState component
- components/shared/TableEmptyState.vue: add shared empty state component
2026-05-13 06:53:02 +00:00
caoxiaozhu
151787ada2 refactor(web): update view scripts
- AuditView.js: update audit view logic
- EmployeeManagementView.js: update employee management logic
- RequestsView.js: update requests view logic
- TravelRequestDetailView.js: update travel detail view logic
2026-05-13 06:52:30 +00:00
caoxiaozhu
fcaed5b2ec feat(web): update views
- AuditView.vue: update audit view
- EmployeeManagementView.vue: update employee management view
- RequestsView.vue: update requests view
- TravelRequestDetailView.vue: update travel request detail view
2026-05-13 06:51:12 +00:00
caoxiaozhu
b637a2bf08 refactor(web): update reimbursements service
- services/reimbursements.js: update reimbursement API service
2026-05-13 06:49:58 +00:00
caoxiaozhu
6c8947f40f feat(web): update composables
- useAppShell.js: update app shell composable
- useRequests.js: update requests composable
2026-05-13 06:48:27 +00:00
caoxiaozhu
8bc919386b style(web): update travel request detail view styles
- travel-request-detail-view.css: update travel request detail styles
2026-05-13 06:47:46 +00:00
caoxiaozhu
13df8fc9dc test(backend): update and add service tests
Updated tests:
- test_ontology_service.py: update ontology service tests

New tests:
- test_expense_claim_service.py: add expense claim service tests
- test_reimbursement_endpoints.py: add reimbursement endpoint tests
2026-05-13 06:46:24 +00:00
caoxiaozhu
6317fc0ccd refactor(backend): update reimbursement and related services
- endpoints/reimbursements.py: update reimbursement API endpoint
- schemas/reimbursement.py: update reimbursement data schemas
- services/expense_claims.py: update expense claims service
- services/ontology.py: update ontology service
- services/user_agent.py: update user agent service
2026-05-13 06:45:04 +00:00
caoxiaozhu
4db5e8ec16 refactor(backend): update services and tests
- services/expense_claims.py: update expense claims service
- services/user_agent.py: update user agent service
- tests/test_orchestrator_service.py: update orchestrator service tests
- tests/test_user_agent_service.py: update user agent service tests
2026-05-13 03:39:41 +00:00
caoxiaozhu
fae9966a11 feat(web): add reimbursements service and ConfirmDialog component
- services/reimbursements.js: add reimbursement API service client
- components/shared/ConfirmDialog.vue: add shared confirm dialog component
2026-05-13 03:37:47 +00:00
caoxiaozhu
46644d429f refactor(web): update view scripts
- AuditView.js: update audit view logic
- EmployeeManagementView.js: update employee management logic
- PoliciesView.js: update policies view logic
- RequestsView.js: update requests view logic
- TravelReimbursementCreateView.js: update travel form logic
- TravelRequestDetailView.js: update travel detail view logic
2026-05-13 03:35:44 +00:00
caoxiaozhu
8b72f4e962 feat(web): update views
- AppShellRouteView.vue: update app shell route view
- AuditView.vue: update audit view
- EmployeeManagementView.vue: update employee management view
- PoliciesView.vue: update policies view
- RequestsView.vue: update requests view
- TravelReimbursementCreateView.vue: update travel form view
- TravelRequestDetailView.vue: update travel detail view
2026-05-13 03:33:11 +00:00
caoxiaozhu
473198c669 refactor(web): update data and utils
- data/requests.js: update requests data
- utils/requestViewModel.js: update request view model utility
2026-05-13 03:31:07 +00:00
caoxiaozhu
2bf133d232 feat(web): update composables
- useAppShell.js: update app shell composable
- useNavigation.js: update navigation composable
- useRequests.js: update requests composable
2026-05-13 03:29:10 +00:00
caoxiaozhu
478c2a0e09 feat(web): update layout and business components
- PersonalWorkbench.vue: update personal workbench component
- SidebarRail.vue: update sidebar rail component
- TopBar.vue: update top bar component
2026-05-13 03:27:30 +00:00
caoxiaozhu
99231f30f1 style(web): update view styles
- audit-view.css: update audit view styles
- requests-view.css: update requests view styles
- travel-reimbursement-create-view.css: update travel form styles
- travel-request-detail-view.css: update travel detail view styles
2026-05-13 03:24:49 +00:00
caoxiaozhu
cea8239370 refactor(backend): update reimbursement and expense claims
- endpoints/reimbursements.py: update reimbursement API endpoint
- schemas/reimbursement.py: update reimbursement data schemas
- services/expense_claims.py: update expense claims service logic
2026-05-13 03:22:52 +00:00
caoxiaozhu
6147b690b2 style(web): update view styles
- overview-view.css: update overview view styles
- travel-reimbursement-create-view.css: update travel form styles
2026-05-12 15:39:55 +00:00
caoxiaozhu
ce04f5aa86 feat(web): update Vue components
- App.vue: update app root component
- PersonalWorkbench.vue: update personal workbench component
2026-05-12 15:39:00 +00:00
caoxiaozhu
024999626c style(web): update app and global styles
- app.css: update app-level styles
- global.css: update global styles
2026-05-12 15:38:16 +00:00
caoxiaozhu
5c3786f179 assets(web): update image assets
- web/public/assets/header.png: update header avatar image
- web/public/assets/person.png: update person avatar image
- web/UI/对话界面.png: add chat interface image
2026-05-12 15:17:20 +00:00
caoxiaozhu
8246c0d4dd refactor(web): update travel reimbursement view script
- scripts/TravelReimbursementCreateView.js: update travel form view logic
2026-05-12 15:16:18 +00:00
caoxiaozhu
13d0cab86e feat(web): update Vue components
- App.vue: update app root component
- SidebarRail.vue: update sidebar rail component
- TravelReimbursementCreateView.vue: update travel form view
2026-05-12 15:15:43 +00:00
caoxiaozhu
a6720942fb style(web): update styles
- app.css: update app-level styles
- global.css: update global styles
- backend-unavailable-view.css: update backend unavailable view styles
- login-view.css: update login view styles
- setup-view.css: update setup view styles
- travel-reimbursement-create-view.css: update travel form styles
2026-05-12 15:14:34 +00:00
caoxiaozhu
bc593d4473 test(backend): update service tests
- test_ontology_service.py: update ontology service tests
- test_orchestrator_service.py: update orchestrator service tests
- test_user_agent_service.py: update user agent service tests
2026-05-12 15:13:57 +00:00
caoxiaozhu
4d9b071e37 refactor(backend): update service layers
- services/ontology.py: update ontology service logic
- services/user_agent.py: update user agent service logic
2026-05-12 15:12:57 +00:00
caoxiaozhu
6137bb5cdc docs(agent-week-plan): update weekly plan documents
- MASTER_TODO.md: update master todo list
- day_3_semantic_ontology_mvp.md: update semantic ontology tasks
- day_5_user_agent_mvp.md: update user agent tasks
2026-05-12 15:12:32 +00:00
caoxiaozhu
2e1b0645c0 refactor(web): refactor avatar assets to use PNG instead of inline SVG
- TravelReimbursementCreateView.js: change avatar imports to use PNG paths
- web/public/assets: add PNG avatar assets (header.png, person.png)
2026-05-12 07:25:45 +00:00
caoxiaozhu
b1e67cc99b feat(web): add SVG assets and update travel reimbursement script
- web/src/assets/header.svg: add header SVG asset
- web/src/assets/person.svg: add person SVG asset
- scripts/TravelReimbursementCreateView.js: update travel form logic
2026-05-12 07:23:29 +00:00
caoxiaozhu
4d748bcdeb feat(web): update travel reimbursement view
- travel-reimbursement-create-view.css: update form styles
- TravelReimbursementCreateView.vue: update view component
- scripts/TravelReimbursementCreateView.js: update view logic
2026-05-12 07:22:11 +00:00
caoxiaozhu
bff20d8eb3 test(backend): update service tests
- test_orchestrator_service.py: update orchestrator service tests
- test_user_agent_service.py: update user agent service tests
2026-05-12 07:20:59 +00:00
caoxiaozhu
a3d40ad9f5 refactor(backend): update service layers
- services/ontology.py: update ontology service logic
- services/orchestrator.py: update orchestrator service logic
- services/user_agent.py: update user agent service logic
2026-05-12 07:19:21 +00:00
caoxiaozhu
3ad16405a1 docs: update agent plan and week plan documents
- agent plan/04_orchestrator_and_runtime_flow.md: update orchestrator runtime flow docs
- agent week plan/day_3_semantic_ontology_mvp.md: update semantic ontology tasks
- agent week plan/day_4_orchestrator_runtime.md: update orchestrator runtime tasks
- agent week plan/day_5_user_agent_mvp.md: update user agent tasks
2026-05-12 07:18:01 +00:00
caoxiaozhu
df450cf79f refactor(server): enhance expense claims resolution logic
- expense_claims.py: add review_form_values fallback for expense type, employee name resolution
2026-05-12 06:42:50 +00:00
caoxiaozhu
4592ba3bb2 refactor(server): update expense claims service
- services/expense_claims.py: update expense claims business logic
2026-05-12 06:41:55 +00:00
caoxiaozhu
5a66e98fc8 refactor(backend): update user_agent schema and service
- schemas/user_agent.py: update user agent data schemas
- services/user_agent.py: update user agent service logic
2026-05-12 06:40:59 +00:00
caoxiaozhu
c263fc9752 feat(web): update views and services
Views:
- AppShellRouteView.vue: update route view
- SettingsView.vue: update settings view
- TravelReimbursementCreateView.vue: update travel form view
- scripts/SettingsView.js: update settings view logic
- scripts/TravelReimbursementCreateView.js: update travel form logic

Services:
- services/orchestrator.js: update orchestrator service client
2026-05-12 06:40:19 +00:00
caoxiaozhu
f6a5eeb620 feat(web): update components and composables
- PersonalWorkbench.vue: update personal workbench component
- useAppShell.js: update app shell composable
2026-05-12 06:39:26 +00:00
caoxiaozhu
c2f208da31 style(web): update view styles
- settings-view.css: update settings view styling
- travel-reimbursement-create-view.css: update travel reimbursement form styles
2026-05-12 06:38:45 +00:00
caoxiaozhu
e53c0aa5d1 test(backend): update service tests
- test_orchestrator_service.py: update orchestrator service tests
- test_settings_persistence.py: update settings persistence tests
- test_user_agent_service.py: update user agent service tests
2026-05-12 06:37:59 +00:00
caoxiaozhu
e416818ae2 feat(backend): update orchestrator endpoint
- endpoints/orchestrator.py: update orchestrator API endpoint with new features
2026-05-12 06:36:58 +00:00
caoxiaozhu
01df3452fd refactor(backend): update and add service layers
- services/ontology.py: update ontology service
- services/orchestrator.py: update orchestrator service
- services/user_agent.py: update user agent service
- services/settings.py: update settings service
- services/expense_claims.py: update expense claims service
- services/agent_conversations.py: add new agent conversations service
2026-05-12 06:36:09 +00:00
caoxiaozhu
a6a28ba865 refactor(backend): update data schemas
- schemas/orchestrator.py: update orchestrator schemas
- schemas/settings.py: update settings schemas
- schemas/user_agent.py: update user agent schemas
2026-05-12 06:35:17 +00:00
caoxiaozhu
665a744a43 feat(backend): update database models and add agent_conversation
- base.py: update database base configuration
- models/__init__.py: update models exports
- models/system_setting.py: update system setting model
- models/agent_conversation.py: add new agent conversation model
2026-05-12 06:34:36 +00:00
caoxiaozhu
8227e0ab5c docs(agent-week-plan): update weekly plan documents
- MASTER_TODO.md: update master todo list
- day_3_semantic_ontology_mvp.md: update semantic ontology tasks
- day_4_orchestrator_runtime.md: update orchestrator runtime tasks
- day_5_user_agent_mvp.md: update user agent tasks
2026-05-12 06:33:55 +00:00
caoxiaozhu
d81ac10894 chore(server): update server start script dependencies check
- server_start.sh: add multipart to dependencies check for file upload support
2026-05-12 03:07:09 +00:00
caoxiaozhu
035be110b6 feat(web): add OCR service and update travel reimbursement view
- web/src/services/ocr.js: add OCR service API client
- web/src/views/scripts/TravelReimbursementCreateView.js: update travel form script with OCR integration
2026-05-12 03:05:51 +00:00
caoxiaozhu
a3f3421ebc test(server): add OCR endpoint and service tests
New tests:
- server/tests/test_ocr_endpoints.py: OCR API endpoint tests
- server/tests/test_ocr_service.py: OCR service unit tests

Updated tests:
- server/tests/test_openapi_schema.py: update OpenAPI schema tests
- server/tests/test_orchestrator_service.py: update orchestrator service tests
2026-05-12 03:05:05 +00:00
caoxiaozhu
fb23a6976a feat(server): add OCR invoice processing functionality
New endpoints:
- server/src/app/api/v1/endpoints/ocr.py: OCR API endpoints for invoice scanning

New schemas:
- server/src/app/schemas/ocr.py: OCR request/response data schemas

New services:
- server/src/app/services/ocr.py: OCR processing business logic
- server/src/app/services/expense_claims.py: expense claims management service

Scripts:
- server/scripts/bootstrap_paddleocr_mobile.sh: PaddleOCR mobile setup script
- server/scripts/paddle_ocr_worker.py: PaddleOCR worker process
2026-05-12 03:04:10 +00:00
caoxiaozhu
ca29025063 refactor(backend): update services and register OCR router
- router.py: register ocr_router with OCR tag
- ontology.py: update ontology service logic
- orchestrator.py: update orchestrator service logic
- user_agent.py: update user agent schema and service
- schemas/user_agent.py: update user agent data schemas
2026-05-12 03:03:15 +00:00
caoxiaozhu
33826929ba chore(server): update backend configuration files
- pyproject.toml: update project dependencies and metadata
- config.py: update application configuration settings
- openapi.py: update OpenAPI documentation
2026-05-12 03:01:24 +00:00
caoxiaozhu
4792b5074f docs(agent-week-plan): update master todo and daily plan docs
- MASTER_TODO.md: update master todo list for current sprint
- day_3_semantic_ontology_mvp.md: update semantic ontology tasks
- day_4_orchestrator_runtime.md: update orchestrator runtime tasks
- day_5_user_agent_mvp.md: update user agent tasks

chore: add server/.venv-ocr312 to .gitignore
2026-05-12 03:00:54 +00:00
caoxiaozhu
434944abc3 feat(web): add ontology and orchestrator service modules
- web/src/services/ontology.js: ontology service API client
- web/src/services/orchestrator.js: orchestrator service API client
2026-05-12 01:28:38 +00:00
caoxiaozhu
93b1a5e746 feat(web): update Vue components and composables
- PersonalWorkbench.vue: update personal workbench component
- useAppShell.js: update app shell composable
- useChat.js: update chat composable with new features
- AppShellRouteView.vue: update route view
- ChatView.vue: update chat view with enhanced UI
- TravelReimbursementCreateView.vue: update travel reimbursement form
- ChatView.js: update chat view script logic
- TravelReimbursementCreateView.js: update travel form script logic
2026-05-12 01:27:49 +00:00
caoxiaozhu
e3548dfaba style(web): update chat and travel reimbursement styles
- chat-view.css: update chat view styling
- travel-reimbursement-create-view.css: update travel reimbursement form styles
2026-05-12 01:26:56 +00:00
caoxiaozhu
441e27145d refactor(backend): register ontology and orchestrator routers
- router.py: import and register ontology_router and orchestrator_router
- openapi.py: add ontology and orchestrator tags to OpenAPI documentation
- agent_runs.py: extend agent runs service with additional functionality
- test_openapi_schema.py: update OpenAPI schema tests
2026-05-12 01:26:13 +00:00
caoxiaozhu
22d47cbf2b feat(backend): add ontology and orchestrator API endpoints
New endpoints:
- server/src/app/api/v1/endpoints/ontology.py: ontology API
- server/src/app/api/v1/endpoints/orchestrator.py: orchestrator API

New schemas:
- server/src/app/schemas/ontology.py: ontology data schemas
- server/src/app/schemas/orchestrator.py: orchestrator data schemas
- server/src/app/schemas/user_agent.py: user agent data schemas

New services:
- server/src/app/services/ontology.py: ontology business logic
- server/src/app/services/orchestrator.py: orchestrator business logic
- server/src/app/services/runtime_chat.py: runtime chat service
- server/src/app/services/user_agent.py: user agent service

New tests:
- server/tests/test_ontology_service.py
- server/tests/test_orchestrator_service.py
- server/tests/test_user_agent_service.py
2026-05-12 01:24:39 +00:00
caoxiaozhu
19da459bb3 docs(agent-week-plan-html): update HTML plan documents
- Update day-1.html to day-7.html: update daily plan content
- Update index.html: refresh week plan index page
2026-05-12 01:23:33 +00:00
caoxiaozhu
ba28627f11 docs(agent-week-plan): update weekly execution plan documents
- Update 00_README.md: refresh week plan overview and structure
- Update MASTER_TODO.md: update master todo list for new week
- Update day_1_foundation_models.md: expand foundation models tasks
- Update day_2_rule_center_integration.md: add rule center integration tasks
- Update day_3_semantic_ontology_mvp.md: add semantic ontology tasks
- Update day_4_orchestrator_runtime.md: add orchestrator runtime tasks
- Update day_5_user_agent_mvp.md: add user agent tasks
- Update day_6_hermes_mvp.md: add hermes agent tasks
- Update day_7_hardening_demo_acceptance.md: add hardening tasks
2026-05-12 01:22:38 +00:00
caoxiaozhu
9b88ee2901 docs(agent-plan): update architecture docs and remove weekly_execution_details
- Update 00_README.md: refresh architecture overview
- Update 02_semantic_ontology.md: expand semantic layer design
- Update 04_orchestrator_and_runtime_flow.md: add runtime flow details
- Update 05_development_roadmap.md: refine milestone timeline
- Update 06_data_contracts_and_governance.md: add contract specifications
- Update 10_evaluation_and_testset.md: add evaluation framework
- Update 11_ocr_invoice_architecture.md: enhance OCR architecture
- Update 14_financial_document_canonical_model.md: complete model design
- Remove weekly_execution_details/: deprecated in favor of agent week plan
2026-05-12 01:20:53 +00:00
1697 changed files with 440648 additions and 34011 deletions

View File

@@ -0,0 +1,160 @@
---
name: agent-change-log
description: Use when working in X-Financial after bug fixes that need a split dev log, when maintaining document/development/YYYY-MM-DD/dev-logs/bugs, or when generating the daily 17:00 combined work-logs.med from same-day feature docs and bug logs.
---
# Agent Change Log
## Overview
This skill keeps split development logs for X-Financial. Bug fixes are recorded under the same-day `dev-logs/bugs` folder. Daily summaries combine same-day feature documents and bug records into one `work-logs.med`.
The log should sound like a careful teammate writing for tomorrow's teammate: concrete, warm, and honest.
## When To Use
- After fixing a bug.
- When a post-commit hook detects a bug-like commit.
- At the daily 17:00 summary pass.
- Before updating the log in a branch where other agents may have pushed commits.
- After verification, so the entry can include what was actually checked.
- When a failed attempt changed files, generated artifacts, or revealed a risk worth preserving.
Do not create legacy `document/work-log/YYYY-MM-DD.md` entries for new work.
## Log Location
Use the same date root as development documents:
```text
document/development/YYYY-MM-DD/
├── feature/<feature-point>/CONCEPT.md + TODO.md
├── dev-logs/bugs/<bug-slug>.md
└── work-logs.med
```
If the date folder exists, reuse it. If not, create it.
## Bug Log Structure
For bug fixes, create or update one file per bug:
```text
document/development/YYYY-MM-DD/dev-logs/bugs/<bug-slug>.md
```
The file must contain `## 修复记录` and timestamped bullets similar to the old `当日工作内容`.
Do not add `遗留问题` or `TODO` sections to bug logs.
Use the helper when possible:
```bash
python3 tools/agent-change-log/update_change_log.py \
--kind bug \
--bug-title "<bug 名称>" \
--bug-slug <bug-slug>
```
## Required Git Check
Before writing or updating a bug log, manually check Git for upstream and local-ahead commits from other agents.
Run:
```bash
date '+%Y-%m-%d %H:%M:%S %Z'
git fetch --all --prune
git status -sb
git rev-parse --abbrev-ref --symbolic-full-name @{u}
git log --oneline --decorate --max-count=20 HEAD..@{u}
git log --oneline --decorate --max-count=20 @{u}..HEAD
```
Rules:
- Treat `git fetch --all --prune` as the default safe "pull check"; it updates remote refs without merging into a dirty worktree.
- Treat `HEAD..@{u}` as upstream commits not yet in local history.
- Treat `@{u}..HEAD` as local commits not yet in upstream history; these may also come from another agent working in the same checkout.
- If the worktree is clean and the branch is only behind upstream, `git pull --ff-only` may be used to fast-forward before analysis.
- If the worktree is dirty, diverged, or likely to conflict, do not merge/rebase automatically. Record the upstream commits from `HEAD..@{u}` in the bug fix entry.
- If there is no upstream branch, record that fact in the bug fix entry and continue with local-only logging.
- When `HEAD..@{u}` has commits, summarize those commits in the bug fix entry before describing local edits. Mention commit hash, subject, and inferred impact.
- When `@{u}..HEAD` has commits that were not created in the current task, summarize them too, because another local agent may have committed without pushing yet.
- When no upstream or local-ahead commits exist, still record "Git 提交检查:未发现 upstream 新提交或本地 ahead 新提交" in the work entry.
## Bug Entry Rules
1. Get the current local time first:
```bash
date '+%Y-%m-%d %H:%M:%S %Z'
```
2. Run the required Git check and capture whether upstream or local-ahead has new commits.
3. Ensure `document/development/YYYY-MM-DD/dev-logs/bugs/` exists.
4. Create or update one `<bug-slug>.md` file for the specific bug.
5. Append a new timestamped bullet under `## 修复记录`.
6. Mention Git commits, changed files or modules, the operation, the intent, and the verification result.
7. Do not add leftover issue or TODO sections.
## Daily Summary
At 17:00 every day, generate the combined work log:
```bash
python3 tools/agent-change-log/update_change_log.py --kind summary
```
This reads:
```text
document/development/YYYY-MM-DD/feature/
document/development/YYYY-MM-DD/dev-logs/bugs/
```
Then writes:
```text
document/development/YYYY-MM-DD/work-logs.med
```
The summary should cover:
- Today's feature points from `feature/*/CONCEPT.md` and `TODO.md`.
- Today's bug fixes from `dev-logs/bugs/*.md`.
- A concise combined analysis of what changed that day.
## Entry Rules
- For each bug entry, append a new timestamped bullet under `## 修复记录`.
- Mention Git commits, changed files or modules, the operation, the intent, and the verification result.
- Do not write `遗留问题`.
- Do not write `TODO`.
- If the change is a feature rather than a bug, use the development document skill to keep `feature/<feature-point>/CONCEPT.md` and `TODO.md` current instead of writing a bug log.
## Writing Style
- Write in Simplified Chinese.
- Be specific and a little human: "我把...", "这次先...", "还需要留意..." are good.
- Keep the tone factual. Do not turn the log into a victory lap.
- Prefer concise file names and module names in prose, but include enough context to find the change.
- Work content should be detailed enough that a future agent can continue without asking "你到底改了啥?"
## Bug Entry Template
```markdown
- HH:MM记录 bug 修复:<bug 名称>。
- Git 提交检查:<git fetch 后 HEAD..upstream 与 upstream..HEAD 的结果;没有就写未发现 upstream 或本地 ahead 新提交>。
- 修改:<文件/模块><做了什么>。
- 操作:<运行了什么命令、迁移了什么状态、重启了什么服务等>。
- 验证:<测试/构建/检查结果;如果没跑,说明原因>。
- 影响:<用户可见变化或工程边界变化>。
```
## Final Response Checklist
Before saying work is complete:
- For bug fixes, today's bug log exists under `document/development/YYYY-MM-DD/dev-logs/bugs/`.
- For non-bug feature work, relevant `feature/<feature-point>` documents are current.
- Git check ran for bug logs, and upstream plus local-ahead commits were summarized or explicitly marked as absent.
- No new legacy `document/work-log/YYYY-MM-DD.md` entry was created.
- The final response mentions whether a bug log, feature document, or daily `work-logs.med` was updated.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Agent Change Log"
short_description: "Record bug logs and daily work summaries"
default_prompt: "Use $agent-change-log after an X-Financial bug fix or at 17:00 to update document/development/<date>/dev-logs/bugs or work-logs.med."

View File

@@ -0,0 +1,85 @@
---
name: git-checkpoint-commit
description: Use when a coding task finishes a verified bug fix, key feature, risky refactor checkpoint, local backup commit, checkpoint commit, 提交备份, 本地提交, or when an active session has accumulated about five meaningful edit rounds and should create a scoped local git commit without pushing.
---
# Git Checkpoint Commit
## Overview
Use this skill to keep a local git backup loop during active development. The goal is to commit verified, task-scoped progress at meaningful checkpoints without mixing unrelated user changes or pushing remotely.
## Trigger Rules
Create a local checkpoint commit when any condition is true:
- A bug fix is implemented and the targeted regression check passes.
- A key feature slice is implemented and the smallest relevant verification passes.
- A risky refactor reaches a behavior-preserving checkpoint.
- The current session has reached about five meaningful edit rounds since the last commit.
- The user asks for `提交`, `本地提交`, `备份`, `checkpoint`, `commit`, or `形成提交循环`.
Do not use this skill when the user explicitly says not to commit, when the change is exploratory and unverified, or when the only available commit would include unrelated dirty files.
## Workflow
1. Inspect the working tree with `git status --short`.
2. Identify files or hunks owned by the current task.
3. Run the smallest relevant verification first.
- In X-Financial, run backend tests inside `x-financial-main` when backend code is involved.
- Use targeted frontend tests/builds for web-only changes.
4. Commit only the task-owned files.
5. Report the commit hash and verification evidence.
6. Reset the session edit counter to zero after a successful checkpoint.
## Safety Rules
- Never commit unrelated user changes just to make the tree clean.
- Never push as part of this skill.
- Never rewrite history, amend old commits, or run destructive git commands.
- If the same file contains unrelated hunks, split the staging carefully with `git add -p` or a narrower manual patch.
- If the index already contains staged changes, inspect them first; do not mix them into a checkpoint unless they belong to the same current task.
- If verification cannot run, say why in the commit body or final report and prefer a `chore(checkpoint)` message rather than a confident `fix` or `feat`.
## Commit Style
Use normal semantic messages for completed, verified work:
- `fix(workbench): keep application preview after draft save failure`
- `feat(reimbursements): add attachment association job polling`
- `refactor(claims): split draft flow serialization`
Use checkpoint messages for interim backup points:
- `chore(checkpoint): backup attachment association flow`
- `chore(checkpoint): backup after five edit rounds`
Keep the subject concise. Add body lines only when the verification state or scope needs clarity.
## Helper Script
Use `scripts/checkpoint_commit.py` when a path-scoped local commit is enough:
```bash
python3 .codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py \
--message "chore(checkpoint): backup workbench AI flow" \
web/src/composables/workbenchAiMode/useWorkbenchAiExpenseFlow.js \
web/tests/workbench-ai-mode-switch.test.mjs
```
Preview before committing:
```bash
python3 .codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py \
--dry-run \
web/src/utils/expenseApplicationPreview.js
```
The script refuses to commit the whole tree unless `--allow-all` is passed. Use `--allow-all` only when the current task truly owns every dirty file.
## Common Mistakes
- Mistaking backup for verification. Verify first, then commit.
- Staging `.` in a dirty worktree. Stage explicit paths or hunks.
- Combining documentation cleanup, feature work, and unrelated local edits in one checkpoint.
- Continuing indefinitely after multiple verified slices. Commit once the counter reaches five meaningful edit rounds.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Git Checkpoint Commit"
short_description: "Create local backup commits at safe milestones"
default_prompt: "Use $git-checkpoint-commit to checkpoint verified task changes into a local git commit."

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Create a path-scoped local git checkpoint commit."""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
def run_git(args: list[str], cwd: Path, *, check: bool = True) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
["git", *args],
cwd=cwd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=60,
)
if check and result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip()
raise SystemExit(f"git {' '.join(args)} failed: {message}")
return result
def repo_root() -> Path:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=60,
)
if result.returncode != 0:
raise SystemExit("Not inside a git repository.")
return Path(result.stdout.strip())
def load_paths(args: argparse.Namespace) -> list[str]:
paths = list(args.paths)
if args.paths_from_file:
raw_lines = Path(args.paths_from_file).read_text(encoding="utf-8").splitlines()
paths.extend(line.strip() for line in raw_lines if line.strip() and not line.startswith("#"))
return paths
def ensure_clean_index(root: Path) -> None:
staged = run_git(["diff", "--cached", "--name-only"], root).stdout.strip()
if staged:
raise SystemExit(
"Refusing to continue because the index already has staged changes:\n"
f"{staged}\n"
"Commit or unstage them before running this checkpoint helper."
)
def status_for(root: Path, paths: list[str], allow_all: bool) -> str:
command = ["status", "--short"]
if not allow_all:
command.extend(["--", *paths])
return run_git(command, root).stdout.strip()
def infer_message(paths: list[str], allow_all: bool) -> str:
if allow_all or not paths:
return "chore(checkpoint): backup current task changes"
first_parts = [Path(item).parts[0] for item in paths if Path(item).parts]
scope = first_parts[0] if first_parts else "task"
return f"chore(checkpoint): backup {scope} changes"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="*", help="Task-owned paths to stage and commit.")
parser.add_argument("-m", "--message", help="Commit message subject/body.")
parser.add_argument("--paths-from-file", help="Read additional pathspecs from a UTF-8 text file.")
parser.add_argument("--dry-run", action="store_true", help="Show matching changes without staging.")
parser.add_argument("--allow-all", action="store_true", help="Allow committing all dirty files.")
parser.add_argument("--no-verify", action="store_true", help="Pass --no-verify to git commit.")
return parser.parse_args()
def main() -> int:
args = parse_args()
root = repo_root()
paths = load_paths(args)
if not paths and not args.allow_all:
raise SystemExit("Pass explicit paths, or use --allow-all when the current task owns all changes.")
current_status = status_for(root, paths, args.allow_all)
if not current_status:
print("No matching changes to commit.")
return 0
print(current_status)
if args.dry_run:
return 0
ensure_clean_index(root)
# 使用 -A 保留删除/重命名等变更,但只作用于明确传入的 pathspec。
if args.allow_all:
run_git(["add", "-A"], root)
else:
run_git(["add", "-A", "--", *paths], root)
staged_summary = run_git(["diff", "--cached", "--name-status"], root).stdout.strip()
if not staged_summary:
raise SystemExit("No staged changes after git add.")
message = args.message or infer_message(paths, args.allow_all)
commit_command = ["commit"]
if args.no_verify:
commit_command.append("--no-verify")
commit_command.extend(["-m", message])
commit_result = run_git(commit_command, root)
sys.stdout.write(commit_result.stdout)
commit_hash = run_git(["rev-parse", "--short", "HEAD"], root).stdout.strip()
print(f"checkpoint_commit={commit_hash}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,125 @@
---
name: write-development-docs
description: Use when working in X-Financial and the user asks to 落文档, 写开发文档, 沉淀方案, 补 concept/todo, or create/update planning documentation under document/development for a feature, refactor, page, algorithm, rule, or business capability.
---
# Write Development Docs
## 目标
把一个功能、重构、算法、页面或业务能力沉淀为项目标准开发文档。
默认落点固定为:
```text
document/development/<YYYY-MM-DD>/feature/<具体功能点目录>/
├── CONCEPT.md
└── TODO.md
```
如果用户说 `concept.md` / `todo.md`,也按仓库现有规范使用大写文件名
`CONCEPT.md``TODO.md`
## 工作流
1. 先读取当前日期,默认使用本地日期 `YYYY-MM-DD` 作为第一层目录。
2. 先阅读 `document/development` 中 2-3 组相邻或同类样例。
3. 再读取本次能力相关的代码、接口、页面、测试或历史文档。
4. 创建或更新 `CONCEPT.md`,先写清业务边界,再写方案和验收。
5. 创建或更新 `TODO.md`,每个任务都要回链到 `CONCEPT.md` 的章节。
6. 已实现或已验证的 TODO 可以勾选 `[x]`,但必须写证据。
7. 交付前检查两份文档互相一致,不额外创建 README、CHANGELOG、SUMMARY。
## 路径规则
- 第一层必须是日期目录,例如 `document/development/2026-06-25/`
- 第二层固定是 `feature/`,表示当天沉淀的功能点集合。
- 第三层是具体功能点目录,每个独立功能点一个目录。
- 每个具体功能点目录内只放 `CONCEPT.md``TODO.md` 两个核心文件。
- 如果一次请求包含多个互不依赖的功能点,拆成多个兄弟目录:
```text
document/development/2026-06-25/feature/
├── receipt-folder-ocr/
│ ├── CONCEPT.md
│ └── TODO.md
└── risk-review-nudge/
├── CONCEPT.md
└── TODO.md
```
- 如果用户明确指定日期,使用用户指定日期;否则使用当前本地日期。
- 如果是更新历史文档,先查找已有目录并原地更新,不自动迁移旧路径。
## 目录命名
- 优先复用用户指定目录名或已有目录名。
- 新增英文目录用小写 kebab-case例如 `receipt-folder`
- 新增中文目录可直接用清晰中文能力名,例如 `费用审批动态路由`
- 同一能力已有目录时更新原目录,不新建近义目录。
## CONCEPT.md 要求
可参考 `assets/CONCEPT.md` 模板。必须包含:
- 标题:`# <功能名> 概念文档`
- `更新时间YYYY-MM-DD`
- `## 功能一句话`
- `## 背景与问题`
- `## 目标与非目标`
- `## 用户与场景`
- `## 功能能力`
- `## 方案设计`
- `## 算法与公式`
- `## 测试方案`
- `## 指标与验收`
- `## 风险与开放问题`
写法要求:
- 先讲业务问题和边界,再讲技术方案。
- 目标与非目标分开写,避免需求无限扩张。
- 方案设计按前端、后端、算法/规则、数据、权限、降级策略分块;
不涉及的块明确写“当前不涉及”。
- 算法与公式必须明确“不涉及”或写出公式、变量说明和适用边界。
- 验收标准必须可验证,不写空泛口号。
## TODO.md 要求
可参考 `assets/TODO.md` 模板。必须包含:
- 标题:`# <功能名> 开发 TODO`
- `更新时间YYYY-MM-DD`
- `## 使用规则`
- 分阶段 checklist
TODO 条目规则:
- 每条用 `- [ ]``- [x]`
- 每条必须包含 `[CONCEPT: <章节名>]`
- 已完成项必须补证据,格式为 `证据:<文件、接口、命令或验证结果>`
- 没有真实证据时不得勾选 `[x]`
建议阶段:
- `## 1. 调研与边界`
- `## 2. 契约与设计`
- `## 3. 后端实现`
- `## 4. 算法/规则实现`
- `## 5. 前端实现`
- `## 6. 测试与验证`
- `## 7. 文档收尾`
## 更新既有文档
- 先读现有 `CONCEPT.md``TODO.md` 全文。
- 新需求先补 `CONCEPT.md`,再补 `TODO.md`
- 实现变化时同步更新“非目标”“风险与开放问题”“本轮实现记录”。
- 不删除历史证据;除非证据明显错误,才替换为新证据。
## 验收检查
- 新建文档路径符合 `document/development/<YYYY-MM-DD>/feature/<具体功能点目录>/`
- `CONCEPT.md``TODO.md` 都存在于同一个具体功能点目录。
- TODO 的 `[CONCEPT: ...]` 都能在 CONCEPT 中找到对应章节或语义段落。
- 已勾选项都有证据。
- 文档没有遗留模板占位符,例如 `<功能名>``<YYYY-MM-DD>``待补充`

View File

@@ -0,0 +1,4 @@
interface:
display_name: "开发文档落地"
short_description: "按日期和功能点生成 CONCEPT/TODO"
default_prompt: "Use $write-development-docs to create CONCEPT.md and TODO.md under document/development/<date>/feature/<feature-point> for an X-Financial feature."

View File

@@ -0,0 +1,132 @@
# <功能名> 概念文档
更新时间:<YYYY-MM-DD>
文档路径document/development/<YYYY-MM-DD>/feature/<具体功能点目录>/CONCEPT.md
## 功能一句话
用一句话说明这个能力解决什么问题、服务谁、交付什么结果。
## 背景与问题
- 当前现状:
- 用户痛点:
- 业务影响:
- 为什么现在需要做:
## 目标与非目标
### 目标
- [G1]
- [G2]
- [G3]
### 非目标
- [NG1] 本轮不做:
- [NG2] 本轮不改变:
- [NG3] 后续再评估:
## 用户与场景
- 目标用户:
- 使用入口:
- 核心场景:
1.
2.
3.
- 异常场景:
-
## 功能能力
- [C1] 输入能力:
- [C2] 处理能力:
- [C3] 输出能力:
- [C4] 状态与权限:
- [C5] 边界与降级:
## 方案设计
### 前端
- 页面/组件:
- 交互状态:
- 展示规则:
- 降级处理:
### 后端
- 接口/服务:
- 权限与校验:
- 持久化:
- 降级处理:
### 算法与规则
- 输入:
- 流程:
- 输出:
- 解释:
### 数据与契约
- 核心字段:
- 状态枚举:
- 兼容策略:
- 版本/审计:
## 算法与公式
当前功能不涉及显式数学公式。
如涉及公式,使用如下格式:
```text
metric = input_a + input_b
```
变量说明:
- metric
- input_a
- input_b
## 测试方案
后端:
-
前端:
-
集成:
-
手工验证:
-
## 指标与验收
- [A1] 功能验收:
- [A2] 性能指标:
- [A3] 质量指标:
- [A4] 安全/权限指标:
- [A5] 可观测性:
## 风险与开放问题
- 风险:
- 已处理依赖:
- 待确认:
- 降级策略:
## 本轮实现记录
-

View File

@@ -0,0 +1,73 @@
# <功能名> 开发 TODO
更新时间:<YYYY-MM-DD>
文档路径document/development/<YYYY-MM-DD>/feature/<具体功能点目录>/TODO.md
## 使用规则
- 每个 TODO 必须对应 `CONCEPT.md` 中的目标、能力、方案或验收点。
- 只有完成并验证后,才能把 `[ ]` 改成 `[x]`
- 勾选时在任务后补充简短证据,例如文件、接口、命令或验证结果。
- 如果需求发生变化,先更新 `CONCEPT.md`,再调整本 TODO。
## 1. 调研与边界
- [ ] [CONCEPT: 背景与问题] 阅读相关页面、接口、服务、测试和历史文档,记录当前实现事实。
证据:
- [ ] [CONCEPT: 目标与非目标] 确认本轮开发范围,写清楚不做项。
证据:
- [ ] [CONCEPT: 风险与开放问题] 标记无法立即确认的依赖、风险和假设。
证据:
## 2. 契约与设计
- [ ] [CONCEPT: 功能能力] 定义输入、输出、状态、权限和边界条件。
证据:
- [ ] [CONCEPT: 方案设计] 明确前端、后端、算法、数据的职责边界。
证据:
- [ ] [CONCEPT: 算法与公式] 补全公式、变量解释或明确当前不涉及公式。
证据:
- [ ] [CONCEPT: 指标与验收] 把验收标准转成可验证的检查点。
证据:
## 3. 后端实现
- [ ] [CONCEPT: 后端] 新增或调整 schema、service、endpoint、权限和持久化逻辑。
证据:
- [ ] [CONCEPT: 数据与契约] 保持响应结构、状态枚举和兼容策略清晰。
证据:
## 4. 算法/规则实现
- [ ] [CONCEPT: 算法与规则] 实现核心处理流程、规则判断或计算逻辑。
证据:
- [ ] [CONCEPT: 结果解释] 输出可读解释、证据链、贡献项或降级原因。
证据:
## 5. 前端实现
- [ ] [CONCEPT: 前端] 新增或调整页面、组件、服务 API 和视图模型。
证据:
- [ ] [CONCEPT: 前端] 实现加载、空态、错误态、权限态和刷新。
证据:
- [ ] [CONCEPT: 前端] 对齐现有企业后台风格,避免营销页或花哨卡片感。
证据:
## 6. 测试与验证
- [ ] [CONCEPT: 测试方案] 补充后端 service/API 定向测试,容器内运行,超时控制在 60s 内。
证据:
- [ ] [CONCEPT: 测试方案] 补充前端视图模型、路由、组件或构建验证。
证据:
- [ ] [CONCEPT: 指标与验收] 记录验证命令、结果和未覆盖风险。
证据:
## 7. 文档收尾
- [ ] [CONCEPT: 指标与验收] 回看所有验收点,确认均有实现或验证证据。
证据:
- [ ] [CONCEPT: 风险与开放问题] 更新剩余风险、后续任务和明确不做项。
证据:
- [ ] [CONCEPT: 功能一句话] 确认最终实现没有偏离原始目标。
证据:

50
.env
View File

@@ -1,50 +0,0 @@
APP_NAME=X-Financial
APP_ENV=local
APP_DEBUG=true
API_V1_PREFIX=/api/v1
SETUP_COMPLETED=true
VITE_SETUP_COMPLETED=true
COMPANY_NAME=YGSOFT
COMPANY_CODE=123
ADMIN_EMAIL='admin@admin.com'
VITE_COMPANY_NAME=YGSOFT
VITE_COMPANY_CODE=123
VITE_ADMIN_EMAIL='admin@admin.com'
# Admin login credentials are stored separately under server/.secrets/
WEB_HOST=10.10.10.122
WEB_PORT=5173
VITE_WEB_HOST=10.10.10.122
VITE_WEB_PORT=5173
SERVER_HOST=0.0.0.0
SERVER_PORT=8000
VITE_SERVER_HOST=0.0.0.0
VITE_SERVER_PORT=8000
SERVER_STARTUP_TIMEOUT=300
SERVER_BLOCKING_STARTUP_TIMEOUT=12
VITE_API_BASE_URL=/api/v1
VITE_AUTH_IDLE_TIMEOUT_MINUTES=30
ONLYOFFICE_ENABLED=true
ONLYOFFICE_PUBLIC_URL=http://10.10.10.122:8082
ONLYOFFICE_BACKEND_URL=http://main:8000
ONLYOFFICE_JWT_SECRET=change-me-onlyoffice
POSTGRES_HOST=10.10.10.189
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=root
POSTGRES_PASSWORD=8811614287327Leo
VITE_POSTGRES_HOST=10.10.10.189
VITE_POSTGRES_PORT=5432
VITE_POSTGRES_DB=postgres
VITE_POSTGRES_USER=root
DATABASE_URL='postgresql+psycopg://root:8811614287327Leo@10.10.10.189:5432/postgres'
SQLALCHEMY_ECHO=false
REDIS_URL=
VITE_REDIS_URL=
CORS_ORIGINS='["http://10.10.10.122:5173"]'

View File

@@ -30,6 +30,8 @@ ONLYOFFICE_ENABLED=false
ONLYOFFICE_PUBLIC_URL=http://127.0.0.1:8082
ONLYOFFICE_BACKEND_URL=http://127.0.0.1:8000
ONLYOFFICE_JWT_SECRET=change-me-onlyoffice
HERMES_AGENT_SHARED_TOKEN=change-me-hermes
STEWARD_AGENT_RUNTIME=langgraph
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
@@ -47,4 +49,8 @@ SQLALCHEMY_ECHO=false
REDIS_URL=
VITE_REDIS_URL=
OCR_DEVICE=
OCR_TIMEOUT_SECONDS=180
OCR_MAX_CONCURRENT_WORKERS=1
CORS_ORIGINS='["http://127.0.0.1:5173","http://localhost:5173","http://0.0.0.0:5173"]'

10
.githooks/post-commit Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
# Auto-append a minimal X-Financial agent work-log entry after each commit.
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
cd "$repo_root" || exit 0
python3 tools/agent-change-log/update_change_log.py \
--kind auto \
--event "post-commit hook" \
>/tmp/x-financial-agent-change-log-hook.log 2>&1 || true

28
.gitignore vendored
View File

@@ -7,11 +7,37 @@ web/.vite/
.omc/
.omx/
.claude/
.codex/
*.egg-info/
.codex/*
!.codex/skills/
.codex/skills/*
!.codex/skills/agent-change-log/
!.codex/skills/agent-change-log/**
!.codex/skills/git-checkpoint-commit/
!.codex/skills/git-checkpoint-commit/**
!.codex/skills/write-development-docs/
!.codex/skills/write-development-docs/**
.codex-temp/
.superpowers/
*.log
.DS_Store
Thumbs.db
__pycache__/
*.pyc
server/.venv/
server/.venv-ocr312
server/.secrets/
server/logs/
server/storage/expense_claims/
server/storage/finance_reports/
server/storage/receipt_folder/
test-results/
.codex-remote-attachments/
tmp-*.png
tmp/
.zcode/
.nezha/
.omo/
.env
.env.local
.env.*.local

1
.tmp/Yuxi Submodule

Submodule .tmp/Yuxi added at fd6803e477

View File

@@ -0,0 +1,22 @@
from typing import TYPE_CHECKING, Any
from ._version import __version__ as __version__
__all__ = ["LightRAG", "QueryParam", "__version__"]
if TYPE_CHECKING:
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
def __getattr__(name: str) -> Any:
if name in {"LightRAG", "QueryParam"}:
from .lightrag import LightRAG, QueryParam
value = {"LightRAG": LightRAG, "QueryParam": QueryParam}[name]
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__author__ = "Zirui Guo"
__url__ = "https://github.com/HKUDS/LightRAG"

View File

@@ -0,0 +1,4 @@
"""Lightweight version definitions shared by packaging and runtime code."""
__version__ = "1.4.16"
__api_version__ = "0291"

View File

@@ -0,0 +1 @@
from .._version import __api_version__ as __api_version__

View File

@@ -0,0 +1,163 @@
from datetime import datetime, timedelta, timezone
import jwt
from dotenv import load_dotenv
from fastapi import HTTPException, status
from pydantic import BaseModel
from ..utils import logger
from .config import DEFAULT_TOKEN_SECRET, global_args
from .passwords import verify_password
# use the .env that is inside the current folder
# allows to use different .env file for each lightrag instance
# the OS environment variables take precedence over the .env file
load_dotenv(dotenv_path=".env", override=False)
class TokenPayload(BaseModel):
sub: str # Username
exp: datetime # Expiration time
role: str = "user" # User role, default is regular user
metadata: dict = {} # Additional metadata
class AuthHandler:
def __init__(self):
auth_accounts = global_args.auth_accounts
self.secret = global_args.token_secret
if not self.secret:
if auth_accounts:
raise ValueError(
"TOKEN_SECRET must be explicitly set to a non-default value when AUTH_ACCOUNTS is configured."
)
self.secret = DEFAULT_TOKEN_SECRET
logger.warning(
"TOKEN_SECRET not set and AUTH_ACCOUNTS is not configured. "
"Falling back to the default guest-mode JWT secret. "
)
algorithm = global_args.jwt_algorithm
if not algorithm or algorithm.lower() == "none":
raise ValueError(
"JWT_ALGORITHM must be set to a secure algorithm (e.g. HS256). "
"The 'none' algorithm is not permitted."
)
self.algorithm = algorithm
self.expire_hours = global_args.token_expire_hours
self.guest_expire_hours = global_args.guest_token_expire_hours
self.accounts = {}
invalid_accounts = []
if auth_accounts:
for account in auth_accounts.split(","):
try:
username, password = account.split(":", 1)
if not username or not password:
raise ValueError
self.accounts[username] = password
except ValueError:
invalid_accounts.append(account)
if invalid_accounts:
invalid_entries = ", ".join(invalid_accounts)
logger.error(f"Invalid account format in AUTH_ACCOUNTS: {invalid_entries}")
raise ValueError(
"AUTH_ACCOUNTS must use comma-separated user:password pairs."
)
def verify_password(self, username: str, plain_password: str) -> bool:
"""
Verify password for a user. Supports explicit bcrypt values and plaintext.
Args:
username: Username to verify
plain_password: Plaintext password to check
Returns:
bool: True if password is correct, False otherwise
"""
if username not in self.accounts:
return False
stored_password = self.accounts[username]
return verify_password(plain_password, stored_password)
def create_token(
self,
username: str,
role: str = "user",
custom_expire_hours: int = None,
metadata: dict = None,
) -> str:
"""
Create JWT token
Args:
username: Username
role: User role, default is "user", guest is "guest"
custom_expire_hours: Custom expiration time (hours), if None use default value
metadata: Additional metadata
Returns:
str: Encoded JWT token
"""
# Choose default expiration time based on role
if custom_expire_hours is None:
if role == "guest":
expire_hours = self.guest_expire_hours
else:
expire_hours = self.expire_hours
else:
expire_hours = custom_expire_hours
expire = datetime.now(timezone.utc) + timedelta(hours=expire_hours)
# Create payload
payload = TokenPayload(
sub=username, exp=expire, role=role, metadata=metadata or {}
)
return jwt.encode(payload.model_dump(), self.secret, algorithm=self.algorithm)
def validate_token(self, token: str) -> dict:
"""
Validate JWT token
Args:
token: JWT token
Returns:
dict: Dictionary containing user information
Raises:
HTTPException: If token is invalid or expired
"""
try:
# Explicitly exclude 'none' to prevent algorithm confusion attacks
allowed_algorithms = [self.algorithm]
if "none" in (a.lower() for a in allowed_algorithms):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Insecure JWT algorithm configuration",
)
payload = jwt.decode(token, self.secret, algorithms=allowed_algorithms)
expire_timestamp = payload["exp"]
expire_time = datetime.fromtimestamp(expire_timestamp, timezone.utc)
if datetime.now(timezone.utc) > expire_time:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
)
# Return complete payload instead of just username
return {
"username": payload["sub"],
"role": payload.get("role", "user"),
"metadata": payload.get("metadata", {}),
"exp": expire_time,
}
except jwt.PyJWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
)
auth_handler = AuthHandler()

View File

@@ -0,0 +1,697 @@
"""
Configs for the LightRAG API.
"""
import os
import re
import argparse
import logging
from dotenv import load_dotenv
from lightrag.utils import get_env_value, logger
from lightrag.llm.binding_options import (
GeminiEmbeddingOptions,
GeminiLLMOptions,
OllamaEmbeddingOptions,
OllamaLLMOptions,
OpenAILLMOptions,
)
from lightrag.base import OllamaServerInfos
import sys
from lightrag.constants import (
DEFAULT_WOKERS,
DEFAULT_TIMEOUT,
DEFAULT_TOP_K,
DEFAULT_CHUNK_TOP_K,
DEFAULT_HISTORY_TURNS,
DEFAULT_MAX_ENTITY_TOKENS,
DEFAULT_MAX_RELATION_TOKENS,
DEFAULT_MAX_TOTAL_TOKENS,
DEFAULT_COSINE_THRESHOLD,
DEFAULT_RELATED_CHUNK_NUMBER,
DEFAULT_MIN_RERANK_SCORE,
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE,
DEFAULT_MAX_ASYNC,
DEFAULT_SUMMARY_MAX_TOKENS,
DEFAULT_SUMMARY_LENGTH_RECOMMENDED,
DEFAULT_SUMMARY_CONTEXT_SIZE,
DEFAULT_SUMMARY_LANGUAGE,
DEFAULT_EMBEDDING_FUNC_MAX_ASYNC,
DEFAULT_EMBEDDING_BATCH_NUM,
DEFAULT_OLLAMA_MODEL_NAME,
DEFAULT_OLLAMA_MODEL_TAG,
DEFAULT_RERANK_BINDING,
DEFAULT_ENTITY_TYPES,
)
# use the .env that is inside the current folder
# allows to use different .env file for each lightrag instance
# the OS environment variables take precedence over the .env file
load_dotenv(dotenv_path=".env", override=False)
ollama_server_infos = OllamaServerInfos()
DEFAULT_TOKEN_SECRET = "lightrag-jwt-default-secret-key!"
NO_PREFIX_SENTINEL = "NO_PREFIX"
PROVIDER_ASYMMETRIC_EMBEDDING_BINDINGS = {"gemini", "jina", "voyageai"}
PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS = {"azure_openai", "ollama", "openai"}
class DefaultRAGStorageConfig:
KV_STORAGE = "JsonKVStorage"
VECTOR_STORAGE = "NanoVectorDBStorage"
GRAPH_STORAGE = "NetworkXStorage"
DOC_STATUS_STORAGE = "JsonDocStatusStorage"
def get_default_host(binding_type: str) -> str:
default_hosts = {
"ollama": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"),
"lollms": os.getenv("LLM_BINDING_HOST", "http://localhost:9600"),
"azure_openai": os.getenv("AZURE_OPENAI_ENDPOINT", "https://api.openai.com/v1"),
"openai": os.getenv("LLM_BINDING_HOST", "https://api.openai.com/v1"),
"gemini": os.getenv(
"LLM_BINDING_HOST", "https://generativelanguage.googleapis.com"
),
}
return default_hosts.get(
binding_type, os.getenv("LLM_BINDING_HOST", "http://localhost:11434")
) # fallback to ollama if unknown
def resolve_asymmetric_embedding_opt_in(
*,
binding: str,
embedding_asymmetric: bool,
embedding_asymmetric_configured: bool,
query_prefix: str | None,
document_prefix: str | None,
query_prefix_configured: bool = False,
document_prefix_configured: bool = False,
) -> bool:
"""Resolve whether query/document-aware embedding behavior should be enabled."""
has_non_empty_prefix = bool(query_prefix or document_prefix)
has_prefix_config = query_prefix_configured or document_prefix_configured
if not embedding_asymmetric:
if has_prefix_config:
state = "false" if embedding_asymmetric_configured else "unset"
logger.warning(
f"EMBEDDING_ASYMMETRIC is {state}; "
"EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX will be ignored."
)
return False
if binding in PROVIDER_ASYMMETRIC_EMBEDDING_BINDINGS:
if has_prefix_config:
logger.warning(
f"{binding} embeddings use provider task parameters for asymmetric "
"mode; EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX will be ignored."
)
return True
if binding in PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS:
if not query_prefix_configured or not document_prefix_configured:
raise ValueError(
f"EMBEDDING_ASYMMETRIC=true for {binding} embeddings requires both "
"EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX. Use "
f"{NO_PREFIX_SENTINEL} for a side that should intentionally have no prefix."
)
if not has_non_empty_prefix:
raise ValueError(
"At least one of EMBEDDING_QUERY_PREFIX or EMBEDDING_DOCUMENT_PREFIX "
f"must be non-empty. Use {NO_PREFIX_SENTINEL} only for the side that "
"should intentionally have no prefix."
)
return True
raise ValueError(
f"EMBEDDING_ASYMMETRIC=true is not supported for {binding} embeddings."
)
def get_embedding_prefix_config(env_key: str) -> tuple[str | None, bool]:
"""Read an embedding prefix and whether it was explicitly configured."""
if env_key not in os.environ:
return None, False
value = os.environ[env_key]
if value == "None":
return None, False
if value == NO_PREFIX_SENTINEL:
return "", True
if value == "":
raise ValueError(
f"{env_key} is empty. Use {NO_PREFIX_SENTINEL} to explicitly request "
"no prefix, or remove the variable to leave it unconfigured."
)
return value, True
def validate_auth_configuration(args: argparse.Namespace) -> None:
"""Reject insecure JWT auth settings before the API starts."""
auth_accounts = (getattr(args, "auth_accounts", "") or "").strip()
token_secret = (getattr(args, "token_secret", "") or "").strip()
if auth_accounts and (not token_secret or token_secret == DEFAULT_TOKEN_SECRET):
raise ValueError(
"TOKEN_SECRET must be explicitly set to a non-default value when AUTH_ACCOUNTS is configured."
)
def parse_args() -> argparse.Namespace:
"""
Parse command line arguments with environment variable fallback
Args:
is_uvicorn_mode: Whether running under uvicorn mode
Returns:
argparse.Namespace: Parsed arguments
"""
parser = argparse.ArgumentParser(description="LightRAG API Server")
# Server configuration
parser.add_argument(
"--host",
default=get_env_value("HOST", "0.0.0.0"),
help="Server host (default: from env or 0.0.0.0)",
)
parser.add_argument(
"--port",
type=int,
default=get_env_value("PORT", 9621, int),
help="Server port (default: from env or 9621)",
)
# Directory configuration
parser.add_argument(
"--working-dir",
default=get_env_value("WORKING_DIR", "./rag_storage"),
help="Working directory for RAG storage (default: from env or ./rag_storage)",
)
parser.add_argument(
"--input-dir",
default=get_env_value("INPUT_DIR", "./inputs"),
help="Directory containing input documents (default: from env or ./inputs)",
)
parser.add_argument(
"--timeout",
default=get_env_value("TIMEOUT", DEFAULT_TIMEOUT, int, special_none=True),
type=int,
help="Timeout in seconds (useful when using slow AI). Use None for infinite timeout",
)
# RAG configuration
parser.add_argument(
"--max-async",
type=int,
default=get_env_value("MAX_ASYNC", DEFAULT_MAX_ASYNC, int),
help=f"Maximum async operations (default: from env or {DEFAULT_MAX_ASYNC})",
)
parser.add_argument(
"--summary-max-tokens",
type=int,
default=get_env_value("SUMMARY_MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS, int),
help=f"Maximum token size for entity/relation summary(default: from env or {DEFAULT_SUMMARY_MAX_TOKENS})",
)
parser.add_argument(
"--summary-context-size",
type=int,
default=get_env_value(
"SUMMARY_CONTEXT_SIZE", DEFAULT_SUMMARY_CONTEXT_SIZE, int
),
help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_CONTEXT_SIZE})",
)
parser.add_argument(
"--summary-length-recommended",
type=int,
default=get_env_value(
"SUMMARY_LENGTH_RECOMMENDED", DEFAULT_SUMMARY_LENGTH_RECOMMENDED, int
),
help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_LENGTH_RECOMMENDED})",
)
# Logging configuration
parser.add_argument(
"--log-level",
default=get_env_value("LOG_LEVEL", "INFO"),
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Logging level (default: from env or INFO)",
)
parser.add_argument(
"--verbose",
action="store_true",
default=get_env_value("VERBOSE", False, bool),
help="Enable verbose debug output(only valid for DEBUG log-level)",
)
parser.add_argument(
"--key",
type=str,
default=get_env_value("LIGHTRAG_API_KEY", None),
help="API key for authentication. This protects lightrag server against unauthorized access",
)
# Optional https parameters
parser.add_argument(
"--ssl",
action="store_true",
default=get_env_value("SSL", False, bool),
help="Enable HTTPS (default: from env or False)",
)
parser.add_argument(
"--ssl-certfile",
default=get_env_value("SSL_CERTFILE", None),
help="Path to SSL certificate file (required if --ssl is enabled)",
)
parser.add_argument(
"--ssl-keyfile",
default=get_env_value("SSL_KEYFILE", None),
help="Path to SSL private key file (required if --ssl is enabled)",
)
# Ollama model configuration
parser.add_argument(
"--simulated-model-name",
type=str,
default=get_env_value("OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME),
help="Name for the simulated Ollama model (default: from env or lightrag)",
)
parser.add_argument(
"--simulated-model-tag",
type=str,
default=get_env_value("OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG),
help="Tag for the simulated Ollama model (default: from env or latest)",
)
# Namespace
parser.add_argument(
"--workspace",
type=str,
default=get_env_value("WORKSPACE", ""),
help="Default workspace for all storage",
)
# Server workers configuration
parser.add_argument(
"--workers",
type=int,
default=get_env_value("WORKERS", DEFAULT_WOKERS, int),
help="Number of worker processes (default: from env or 1)",
)
# LLM and embedding bindings
parser.add_argument(
"--llm-binding",
type=str,
default=get_env_value("LLM_BINDING", "ollama"),
choices=[
"lollms",
"ollama",
"openai",
"openai-ollama",
"azure_openai",
"aws_bedrock",
"gemini",
],
help="LLM binding type (default: from env or ollama)",
)
parser.add_argument(
"--embedding-binding",
type=str,
default=get_env_value("EMBEDDING_BINDING", "ollama"),
choices=[
"lollms",
"ollama",
"openai",
"azure_openai",
"aws_bedrock",
"jina",
"gemini",
"voyageai",
],
help="Embedding binding type (default: from env or ollama)",
)
parser.add_argument(
"--rerank-binding",
type=str,
default=get_env_value("RERANK_BINDING", DEFAULT_RERANK_BINDING),
choices=["null", "cohere", "jina", "aliyun"],
help=f"Rerank binding type (default: from env or {DEFAULT_RERANK_BINDING})",
)
# Document loading engine configuration
parser.add_argument(
"--docling",
action="store_true",
default=False,
help="Enable DOCLING document loading engine (default: from env or DEFAULT)",
)
# Conditionally add binding-specific options (Ollama, OpenAI, Azure OpenAI, Gemini)
# This registers command line arguments (e.g., --openai-llm-temperature)
# and reads corresponding environment variables (e.g., OPENAI_LLM_TEMPERATURE)
# Determine LLM binding value consistently from command line or environment
llm_binding_value = None
if "--llm-binding" in sys.argv:
try:
idx = sys.argv.index("--llm-binding")
if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
llm_binding_value = sys.argv[idx + 1]
except IndexError:
pass
# Fall back to environment variable using same function as argparse default
if llm_binding_value is None:
llm_binding_value = get_env_value("LLM_BINDING", "ollama")
# Add LLM binding options based on determined value
if llm_binding_value == "ollama":
OllamaLLMOptions.add_args(parser)
elif llm_binding_value in ["openai", "azure_openai"]:
OpenAILLMOptions.add_args(parser)
elif llm_binding_value == "gemini":
GeminiLLMOptions.add_args(parser)
# Determine embedding binding value consistently from command line or environment
embedding_binding_value = None
if "--embedding-binding" in sys.argv:
try:
idx = sys.argv.index("--embedding-binding")
if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
embedding_binding_value = sys.argv[idx + 1]
except IndexError:
pass
# Fall back to environment variable using same function as argparse default
if embedding_binding_value is None:
embedding_binding_value = get_env_value("EMBEDDING_BINDING", "ollama")
# Add embedding binding options based on determined value
if embedding_binding_value == "ollama":
OllamaEmbeddingOptions.add_args(parser)
elif embedding_binding_value == "gemini":
GeminiEmbeddingOptions.add_args(parser)
args = parser.parse_args()
# convert relative path to absolute path
args.working_dir = os.path.abspath(args.working_dir)
args.input_dir = os.path.abspath(args.input_dir)
# Inject storage configuration from environment variables
args.kv_storage = get_env_value(
"LIGHTRAG_KV_STORAGE", DefaultRAGStorageConfig.KV_STORAGE
)
args.doc_status_storage = get_env_value(
"LIGHTRAG_DOC_STATUS_STORAGE", DefaultRAGStorageConfig.DOC_STATUS_STORAGE
)
args.graph_storage = get_env_value(
"LIGHTRAG_GRAPH_STORAGE", DefaultRAGStorageConfig.GRAPH_STORAGE
)
args.vector_storage = get_env_value(
"LIGHTRAG_VECTOR_STORAGE", DefaultRAGStorageConfig.VECTOR_STORAGE
)
# Get MAX_PARALLEL_INSERT from environment
args.max_parallel_insert = get_env_value("MAX_PARALLEL_INSERT", 2, int)
# Get MAX_GRAPH_NODES from environment
args.max_graph_nodes = get_env_value("MAX_GRAPH_NODES", 1000, int)
# Handle openai-ollama special case
if args.llm_binding == "openai-ollama":
args.llm_binding = "openai"
args.embedding_binding = "ollama"
args.llm_binding_host = get_env_value(
"LLM_BINDING_HOST", get_default_host(args.llm_binding)
)
args.embedding_binding_host = get_env_value(
"EMBEDDING_BINDING_HOST", get_default_host(args.embedding_binding)
)
args.llm_binding_api_key = get_env_value("LLM_BINDING_API_KEY", None)
args.embedding_binding_api_key = get_env_value("EMBEDDING_BINDING_API_KEY", "")
# Inject model configuration
args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest")
# EMBEDDING_MODEL defaults to None - each binding will use its own default model
# e.g., OpenAI uses "text-embedding-3-small", Jina uses "jina-embeddings-v4"
args.embedding_model = get_env_value("EMBEDDING_MODEL", None, special_none=True)
# EMBEDDING_DIM defaults to None - each binding will use its own default dimension
# Value is inherited from provider defaults via wrap_embedding_func_with_attrs decorator
args.embedding_dim = get_env_value("EMBEDDING_DIM", None, int, special_none=True)
args.embedding_send_dim = get_env_value("EMBEDDING_SEND_DIM", False, bool)
# Inject chunk configuration
args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int)
# Inject LLM cache configuration
args.enable_llm_cache_for_extract = get_env_value(
"ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool
)
args.enable_llm_cache = get_env_value("ENABLE_LLM_CACHE", True, bool)
# Set document_loading_engine from --docling flag
if args.docling:
args.document_loading_engine = "DOCLING"
else:
args.document_loading_engine = get_env_value(
"DOCUMENT_LOADING_ENGINE", "DEFAULT"
)
# PDF decryption password
args.pdf_decrypt_password = get_env_value("PDF_DECRYPT_PASSWORD", None)
# Add environment variables that were previously read directly
args.cors_origins = get_env_value("CORS_ORIGINS", "*")
args.summary_language = get_env_value("SUMMARY_LANGUAGE", DEFAULT_SUMMARY_LANGUAGE)
args.entity_types = get_env_value("ENTITY_TYPES", DEFAULT_ENTITY_TYPES, list)
args.whitelist_paths = get_env_value("WHITELIST_PATHS", "/health,/api/*")
# For JWT Auth
args.auth_accounts = get_env_value("AUTH_ACCOUNTS", "")
args.token_secret = get_env_value("TOKEN_SECRET", None)
args.token_expire_hours = get_env_value("TOKEN_EXPIRE_HOURS", 48, float)
args.guest_token_expire_hours = get_env_value("GUEST_TOKEN_EXPIRE_HOURS", 24, float)
args.jwt_algorithm = get_env_value("JWT_ALGORITHM", "HS256")
# Token auto-renewal configuration (sliding window expiration)
args.token_auto_renew = get_env_value("TOKEN_AUTO_RENEW", True, bool)
args.token_renew_threshold = get_env_value("TOKEN_RENEW_THRESHOLD", 0.5, float)
# Rerank model configuration
args.rerank_model = get_env_value("RERANK_MODEL", None)
args.rerank_binding_host = get_env_value("RERANK_BINDING_HOST", None)
args.rerank_binding_api_key = get_env_value("RERANK_BINDING_API_KEY", None)
# Note: rerank_binding is already set by argparse, no need to override from env
# Min rerank score configuration
args.min_rerank_score = get_env_value(
"MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float
)
# Query configuration
args.history_turns = get_env_value("HISTORY_TURNS", DEFAULT_HISTORY_TURNS, int)
args.top_k = get_env_value("TOP_K", DEFAULT_TOP_K, int)
args.chunk_top_k = get_env_value("CHUNK_TOP_K", DEFAULT_CHUNK_TOP_K, int)
args.max_entity_tokens = get_env_value(
"MAX_ENTITY_TOKENS", DEFAULT_MAX_ENTITY_TOKENS, int
)
args.max_relation_tokens = get_env_value(
"MAX_RELATION_TOKENS", DEFAULT_MAX_RELATION_TOKENS, int
)
args.max_total_tokens = get_env_value(
"MAX_TOTAL_TOKENS", DEFAULT_MAX_TOTAL_TOKENS, int
)
args.cosine_threshold = get_env_value(
"COSINE_THRESHOLD", DEFAULT_COSINE_THRESHOLD, float
)
args.related_chunk_number = get_env_value(
"RELATED_CHUNK_NUMBER", DEFAULT_RELATED_CHUNK_NUMBER, int
)
# Add missing environment variables for health endpoint
args.force_llm_summary_on_merge = get_env_value(
"FORCE_LLM_SUMMARY_ON_MERGE", DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, int
)
args.embedding_func_max_async = get_env_value(
"EMBEDDING_FUNC_MAX_ASYNC", DEFAULT_EMBEDDING_FUNC_MAX_ASYNC, int
)
args.embedding_batch_num = get_env_value(
"EMBEDDING_BATCH_NUM", DEFAULT_EMBEDDING_BATCH_NUM, int
)
# Embedding token limit configuration
args.embedding_token_limit = get_env_value(
"EMBEDDING_TOKEN_LIMIT", None, int, special_none=True
)
# File upload size limit (in bytes, None for unlimited)
# Default: 100MB (104857600 bytes)
args.max_upload_size = get_env_value(
"MAX_UPLOAD_SIZE", 104857600, int, special_none=True
)
# Embedding prefix configuration for context-aware embeddings. Empty prefixes
# must be explicit via NO_PREFIX so missing config is distinguishable.
(
args.embedding_document_prefix,
args.embedding_document_prefix_configured,
) = get_embedding_prefix_config("EMBEDDING_DOCUMENT_PREFIX")
(
args.embedding_query_prefix,
args.embedding_query_prefix_configured,
) = get_embedding_prefix_config("EMBEDDING_QUERY_PREFIX")
args.embedding_prefix_no_prefix_sentinel = NO_PREFIX_SENTINEL
args.embedding_prefixes_configured = (
args.embedding_document_prefix_configured
or args.embedding_query_prefix_configured
)
# Asymmetric embedding behavior toggle
args.embedding_asymmetric_configured = "EMBEDDING_ASYMMETRIC" in os.environ
args.embedding_asymmetric = get_env_value("EMBEDDING_ASYMMETRIC", False, bool)
ollama_server_infos.LIGHTRAG_NAME = args.simulated_model_name
ollama_server_infos.LIGHTRAG_TAG = args.simulated_model_tag
# Sanitize workspace: only alphanumeric characters and underscores are allowed
if args.workspace:
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", args.workspace)
if sanitized != args.workspace:
logging.warning(
f"Workspace name '{args.workspace}' contains invalid characters. "
f"It has been sanitized to '{sanitized}'. "
"Only alphanumeric characters and underscores are allowed."
)
args.workspace = sanitized
validate_auth_configuration(args)
return args
def update_uvicorn_mode_config():
# If in uvicorn mode and workers > 1, force it to 1 and log warning
if global_args.workers > 1:
original_workers = global_args.workers
global_args.workers = 1
# Log warning directly here
logging.debug(
f">> Forcing workers=1 in uvicorn mode(Ignoring workers={original_workers})"
)
# Global configuration with lazy initialization
_global_args = None
_initialized = False
def initialize_config(args=None, force=False):
"""Initialize global configuration
This function allows explicit initialization of the configuration,
which is useful for programmatic usage, testing, or embedding LightRAG
in other applications.
Args:
args: Pre-parsed argparse.Namespace or None to parse from sys.argv
force: Force re-initialization even if already initialized
Returns:
argparse.Namespace: The configured arguments
Example:
# Use parsed command line arguments (default)
initialize_config()
# Use custom configuration programmatically
custom_args = argparse.Namespace(
host='localhost',
port=8080,
working_dir='./custom_rag',
# ... other config
)
initialize_config(custom_args)
"""
global _global_args, _initialized
if _initialized and not force:
return _global_args
resolved_args = args if args is not None else parse_args()
validate_auth_configuration(resolved_args)
_global_args = resolved_args
_initialized = True
return _global_args
def get_config():
"""Get global configuration, auto-initializing if needed
Returns:
argparse.Namespace: The configured arguments
"""
if not _initialized:
initialize_config()
return _global_args
class _GlobalArgsProxy:
"""Proxy object that auto-initializes configuration on first access
This maintains backward compatibility with existing code while
allowing programmatic control over initialization timing.
The proxy fully delegates to the underlying argparse.Namespace,
including support for vars() calls which is used by binding_options
to extract provider-specific configuration options.
"""
def __getattribute__(self, name):
"""Override attribute access to support vars() and regular attribute access.
This method intercepts __dict__ access (used by vars()) and delegates
to the underlying _global_args namespace, ensuring binding options
can be properly extracted.
"""
global _initialized, _global_args
# Handle __dict__ access for vars() support
if name == "__dict__":
if not _initialized:
initialize_config()
return vars(_global_args)
# Handle class-level attributes that should come from the proxy itself
if name in ("__class__", "__repr__", "__getattribute__", "__setattr__"):
return object.__getattribute__(self, name)
# Delegate all other attribute access to the underlying namespace
if not _initialized:
initialize_config()
return getattr(_global_args, name)
def __setattr__(self, name, value):
global _initialized, _global_args
if not _initialized:
initialize_config()
setattr(_global_args, name, value)
def __repr__(self):
global _initialized, _global_args
if not _initialized:
return "<GlobalArgsProxy: Not initialized>"
return repr(_global_args)
# Create proxy instance for backward compatibility
# Existing code like `from config import global_args` continues to work
# The proxy will auto-initialize on first attribute access
global_args = _GlobalArgsProxy()

View File

@@ -0,0 +1,162 @@
# gunicorn_config.py
import os
import logging
from lightrag.kg.shared_storage import finalize_share_data
from lightrag.utils import setup_logger, get_env_value
from lightrag.constants import (
DEFAULT_LOG_MAX_BYTES,
DEFAULT_LOG_BACKUP_COUNT,
DEFAULT_LOG_FILENAME,
)
# Get log directory path from environment variable
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(os.path.join(log_dir, DEFAULT_LOG_FILENAME))
# Ensure log directory exists
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = get_env_value("LOG_MAX_BYTES", DEFAULT_LOG_MAX_BYTES, int)
log_backup_count = get_env_value("LOG_BACKUP_COUNT", DEFAULT_LOG_BACKUP_COUNT, int)
# These variables will be set by run_with_gunicorn.py
workers = None
bind = None
loglevel = None
certfile = None
keyfile = None
# Enable preload_app option
preload_app = True
# Use Uvicorn worker
worker_class = "uvicorn.workers.UvicornWorker"
# Other Gunicorn configurations
# Logging configuration
errorlog = os.getenv("ERROR_LOG", log_file_path) # Default write to lightrag.log
accesslog = os.getenv("ACCESS_LOG", log_file_path) # Default write to lightrag.log
logconfig_dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "standard",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf8",
},
},
"filters": {
"path_filter": {
"()": "lightrag.utils.LightragPathFilter",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": loglevel.upper() if loglevel else "INFO",
"propagate": False,
},
"gunicorn": {
"handlers": ["console", "file"],
"level": loglevel.upper() if loglevel else "INFO",
"propagate": False,
},
"gunicorn.error": {
"handlers": ["console", "file"],
"level": loglevel.upper() if loglevel else "INFO",
"propagate": False,
},
"gunicorn.access": {
"handlers": ["console", "file"],
"level": loglevel.upper() if loglevel else "INFO",
"propagate": False,
"filters": ["path_filter"],
},
},
}
def on_starting(server):
"""
Executed when Gunicorn starts, before forking the first worker processes
You can use this function to do more initialization tasks for all processes
"""
print("=" * 80)
print(f"GUNICORN MASTER PROCESS: on_starting jobs for {workers} worker(s)")
print(f"Process ID: {os.getpid()}")
print("=" * 80)
# Memory usage monitoring
try:
import psutil
process = psutil.Process(os.getpid())
memory_info = process.memory_info()
msg = (
f"Memory usage after initialization: {memory_info.rss / 1024 / 1024:.2f} MB"
)
print(msg)
except ImportError:
print("psutil not installed, skipping memory usage reporting")
# Log the location of the LightRAG log file
print(f"LightRAG log file: {log_file_path}\n")
print("Gunicorn initialization complete, forking workers...\n")
def on_exit(server):
"""
Executed when Gunicorn is shutting down.
This is a good place to release shared resources.
"""
print("=" * 80)
print("GUNICORN MASTER PROCESS: Shutting down")
print(f"Process ID: {os.getpid()}")
print("Finalizing shared storage...")
finalize_share_data()
print("Gunicorn shutdown complete")
print("=" * 80)
def post_fork(server, worker):
"""
Executed after a worker has been forked.
This is a good place to set up worker-specific configurations.
"""
# Set up main loggers
log_level = loglevel.upper() if loglevel else "INFO"
setup_logger("uvicorn", log_level, add_filter=False, log_file_path=log_file_path)
setup_logger(
"uvicorn.access", log_level, add_filter=True, log_file_path=log_file_path
)
setup_logger("lightrag", log_level, add_filter=True, log_file_path=log_file_path)
# Set up lightrag submodule loggers
for name in logging.root.manager.loggerDict:
if name.startswith("lightrag."):
setup_logger(name, log_level, add_filter=True, log_file_path=log_file_path)
# Disable uvicorn.error logger
uvicorn_error_logger = logging.getLogger("uvicorn.error")
uvicorn_error_logger.handlers = []
uvicorn_error_logger.setLevel(logging.CRITICAL)
uvicorn_error_logger.propagate = False

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
import bcrypt
BCRYPT_PASSWORD_PREFIX = "{bcrypt}"
def hash_password(password: str) -> str:
"""Return an AUTH_ACCOUNTS-ready bcrypt password value."""
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
return f"{BCRYPT_PASSWORD_PREFIX}{hashed}"
def verify_password(plain_password: str, stored_password: str) -> bool:
"""Verify a plaintext password against a stored password spec."""
if stored_password.startswith(BCRYPT_PASSWORD_PREFIX):
hashed_password = stored_password[len(BCRYPT_PASSWORD_PREFIX) :]
if not hashed_password:
return False
try:
return bcrypt.checkpw(
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
)
except ValueError:
return False
return stored_password == plain_password

View File

@@ -0,0 +1,10 @@
"""
This module contains all the routers for the LightRAG API.
"""
from .document_routes import router as document_router
from .query_routes import router as query_router
from .graph_routes import router as graph_router
from .ollama_api import OllamaAPI
__all__ = ["document_router", "query_router", "graph_router", "OllamaAPI"]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,688 @@
"""
This module contains all graph-related routes for the LightRAG API.
"""
from typing import Optional, Dict, Any
import traceback
from fastapi import APIRouter, Depends, Query, HTTPException
from pydantic import BaseModel, Field
from lightrag.utils import logger
from ..utils_api import get_combined_auth_dependency
router = APIRouter(tags=["graph"])
class EntityUpdateRequest(BaseModel):
entity_name: str
updated_data: Dict[str, Any]
allow_rename: bool = False
allow_merge: bool = False
class RelationUpdateRequest(BaseModel):
source_id: str
target_id: str
updated_data: Dict[str, Any]
class EntityMergeRequest(BaseModel):
entities_to_change: list[str] = Field(
...,
description="List of entity names to be merged and deleted. These are typically duplicate or misspelled entities.",
min_length=1,
examples=[["Elon Msk", "Ellon Musk"]],
)
entity_to_change_into: str = Field(
...,
description="Target entity name that will receive all relationships from the source entities. This entity will be preserved.",
min_length=1,
examples=["Elon Musk"],
)
class EntityCreateRequest(BaseModel):
entity_name: str = Field(
...,
description="Unique name for the new entity",
min_length=1,
examples=["Tesla"],
)
entity_data: Dict[str, Any] = Field(
...,
description="Dictionary containing entity properties. Common fields include 'description' and 'entity_type'.",
examples=[
{
"description": "Electric vehicle manufacturer",
"entity_type": "ORGANIZATION",
}
],
)
class RelationCreateRequest(BaseModel):
source_entity: str = Field(
...,
description="Name of the source entity. This entity must already exist in the knowledge graph.",
min_length=1,
examples=["Elon Musk"],
)
target_entity: str = Field(
...,
description="Name of the target entity. This entity must already exist in the knowledge graph.",
min_length=1,
examples=["Tesla"],
)
relation_data: Dict[str, Any] = Field(
...,
description="Dictionary containing relationship properties. Common fields include 'description', 'keywords', and 'weight'.",
examples=[
{
"description": "Elon Musk is the CEO of Tesla",
"keywords": "CEO, founder",
"weight": 1.0,
}
],
)
def create_graph_routes(rag, api_key: Optional[str] = None):
combined_auth = get_combined_auth_dependency(api_key)
@router.get("/graph/label/list", dependencies=[Depends(combined_auth)])
async def get_graph_labels():
"""
Get all graph labels
Returns:
List[str]: List of graph labels
"""
try:
return await rag.get_graph_labels()
except Exception as e:
logger.error(f"Error getting graph labels: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error getting graph labels: {str(e)}"
)
@router.get("/graph/label/popular", dependencies=[Depends(combined_auth)])
async def get_popular_labels(
limit: int = Query(
300, description="Maximum number of popular labels to return", ge=1, le=1000
),
):
"""
Get popular labels by node degree (most connected entities)
Args:
limit (int): Maximum number of labels to return (default: 300, max: 1000)
Returns:
List[str]: List of popular labels sorted by degree (highest first)
"""
try:
return await rag.chunk_entity_relation_graph.get_popular_labels(limit)
except Exception as e:
logger.error(f"Error getting popular labels: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error getting popular labels: {str(e)}"
)
@router.get("/graph/label/search", dependencies=[Depends(combined_auth)])
async def search_labels(
q: str = Query(..., description="Search query string"),
limit: int = Query(
50, description="Maximum number of search results to return", ge=1, le=100
),
):
"""
Search labels with fuzzy matching
Args:
q (str): Search query string
limit (int): Maximum number of results to return (default: 50, max: 100)
Returns:
List[str]: List of matching labels sorted by relevance
"""
try:
return await rag.chunk_entity_relation_graph.search_labels(q, limit)
except Exception as e:
logger.error(f"Error searching labels with query '{q}': {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error searching labels: {str(e)}"
)
@router.get("/graphs", dependencies=[Depends(combined_auth)])
async def get_knowledge_graph(
label: str = Query(..., description="Label to get knowledge graph for"),
max_depth: int = Query(3, description="Maximum depth of graph", ge=1),
max_nodes: int = Query(1000, description="Maximum nodes to return", ge=1),
):
"""
Retrieve a connected subgraph of nodes where the label includes the specified label.
When reducing the number of nodes, the prioritization criteria are as follows:
1. Hops(path) to the staring node take precedence
2. Followed by the degree of the nodes
Args:
label (str): Label of the starting node
max_depth (int, optional): Maximum depth of the subgraph,Defaults to 3
max_nodes: Maxiumu nodes to return
Returns:
Dict[str, List[str]]: Knowledge graph for label
"""
try:
# Log the label parameter to check for leading spaces
logger.debug(
f"get_knowledge_graph called with label: '{label}' (length: {len(label)}, repr: {repr(label)})"
)
return await rag.get_knowledge_graph(
node_label=label,
max_depth=max_depth,
max_nodes=max_nodes,
)
except Exception as e:
logger.error(f"Error getting knowledge graph for label '{label}': {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error getting knowledge graph: {str(e)}"
)
@router.get("/graph/entity/exists", dependencies=[Depends(combined_auth)])
async def check_entity_exists(
name: str = Query(..., description="Entity name to check"),
):
"""
Check if an entity with the given name exists in the knowledge graph
Args:
name (str): Name of the entity to check
Returns:
Dict[str, bool]: Dictionary with 'exists' key indicating if entity exists
"""
try:
exists = await rag.chunk_entity_relation_graph.has_node(name)
return {"exists": exists}
except Exception as e:
logger.error(f"Error checking entity existence for '{name}': {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error checking entity existence: {str(e)}"
)
@router.post("/graph/entity/edit", dependencies=[Depends(combined_auth)])
async def update_entity(request: EntityUpdateRequest):
"""
Update an entity's properties in the knowledge graph
This endpoint allows updating entity properties, including renaming entities.
When renaming to an existing entity name, the behavior depends on allow_merge:
Args:
request (EntityUpdateRequest): Request containing:
- entity_name (str): Name of the entity to update
- updated_data (Dict[str, Any]): Dictionary of properties to update
- allow_rename (bool): Whether to allow entity renaming (default: False)
- allow_merge (bool): Whether to merge into existing entity when renaming
causes name conflict (default: False)
Returns:
Dict with the following structure:
{
"status": "success",
"message": "Entity updated successfully" | "Entity merged successfully into 'target_name'",
"data": {
"entity_name": str, # Final entity name
"description": str, # Entity description
"entity_type": str, # Entity type
"source_id": str, # Source chunk IDs
... # Other entity properties
},
"operation_summary": {
"merged": bool, # Whether entity was merged into another
"merge_status": str, # "success" | "failed" | "not_attempted"
"merge_error": str | None, # Error message if merge failed
"operation_status": str, # "success" | "partial_success" | "failure"
"target_entity": str | None, # Target entity name if renaming/merging
"final_entity": str, # Final entity name after operation
"renamed": bool # Whether entity was renamed
}
}
operation_status values explained:
- "success": All operations completed successfully
* For simple updates: entity properties updated
* For renames: entity renamed successfully
* For merges: non-name updates applied AND merge completed
- "partial_success": Update succeeded but merge failed
* Non-name property updates were applied successfully
* Merge operation failed (entity not merged)
* Original entity still exists with updated properties
* Use merge_error for failure details
- "failure": Operation failed completely
* If merge_status == "failed": Merge attempted but both update and merge failed
* If merge_status == "not_attempted": Regular update failed
* No changes were applied to the entity
merge_status values explained:
- "success": Entity successfully merged into target entity
- "failed": Merge operation was attempted but failed
- "not_attempted": No merge was attempted (normal update/rename)
Behavior when renaming to an existing entity:
- If allow_merge=False: Raises ValueError with 400 status (default behavior)
- If allow_merge=True: Automatically merges the source entity into the existing target entity,
preserving all relationships and applying non-name updates first
Example Request (simple update):
POST /graph/entity/edit
{
"entity_name": "Tesla",
"updated_data": {"description": "Updated description"},
"allow_rename": false,
"allow_merge": false
}
Example Response (simple update success):
{
"status": "success",
"message": "Entity updated successfully",
"data": { ... },
"operation_summary": {
"merged": false,
"merge_status": "not_attempted",
"merge_error": null,
"operation_status": "success",
"target_entity": null,
"final_entity": "Tesla",
"renamed": false
}
}
Example Request (rename with auto-merge):
POST /graph/entity/edit
{
"entity_name": "Elon Msk",
"updated_data": {
"entity_name": "Elon Musk",
"description": "Corrected description"
},
"allow_rename": true,
"allow_merge": true
}
Example Response (merge success):
{
"status": "success",
"message": "Entity merged successfully into 'Elon Musk'",
"data": { ... },
"operation_summary": {
"merged": true,
"merge_status": "success",
"merge_error": null,
"operation_status": "success",
"target_entity": "Elon Musk",
"final_entity": "Elon Musk",
"renamed": true
}
}
Example Response (partial success - update succeeded but merge failed):
{
"status": "success",
"message": "Entity updated successfully",
"data": { ... }, # Data reflects updated "Elon Msk" entity
"operation_summary": {
"merged": false,
"merge_status": "failed",
"merge_error": "Target entity locked by another operation",
"operation_status": "partial_success",
"target_entity": "Elon Musk",
"final_entity": "Elon Msk", # Original entity still exists
"renamed": true
}
}
"""
try:
result = await rag.aedit_entity(
entity_name=request.entity_name,
updated_data=request.updated_data,
allow_rename=request.allow_rename,
allow_merge=request.allow_merge,
)
# Extract operation_summary from result, with fallback for backward compatibility
operation_summary = result.get(
"operation_summary",
{
"merged": False,
"merge_status": "not_attempted",
"merge_error": None,
"operation_status": "success",
"target_entity": None,
"final_entity": request.updated_data.get(
"entity_name", request.entity_name
),
"renamed": request.updated_data.get(
"entity_name", request.entity_name
)
!= request.entity_name,
},
)
# Separate entity data from operation_summary for clean response
entity_data = dict(result)
entity_data.pop("operation_summary", None)
# Generate appropriate response message based on merge status
response_message = (
f"Entity merged successfully into '{operation_summary['final_entity']}'"
if operation_summary.get("merged")
else "Entity updated successfully"
)
return {
"status": "success",
"message": response_message,
"data": entity_data,
"operation_summary": operation_summary,
}
except ValueError as ve:
logger.error(
f"Validation error updating entity '{request.entity_name}': {str(ve)}"
)
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(f"Error updating entity '{request.entity_name}': {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error updating entity: {str(e)}"
)
@router.post("/graph/relation/edit", dependencies=[Depends(combined_auth)])
async def update_relation(request: RelationUpdateRequest):
"""Update a relation's properties in the knowledge graph
Args:
request (RelationUpdateRequest): Request containing source ID, target ID and updated data
Returns:
Dict: Updated relation information
"""
try:
result = await rag.aedit_relation(
source_entity=request.source_id,
target_entity=request.target_id,
updated_data=request.updated_data,
)
return {
"status": "success",
"message": "Relation updated successfully",
"data": result,
}
except ValueError as ve:
logger.error(
f"Validation error updating relation between '{request.source_id}' and '{request.target_id}': {str(ve)}"
)
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(
f"Error updating relation between '{request.source_id}' and '{request.target_id}': {str(e)}"
)
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error updating relation: {str(e)}"
)
@router.post("/graph/entity/create", dependencies=[Depends(combined_auth)])
async def create_entity(request: EntityCreateRequest):
"""
Create a new entity in the knowledge graph
This endpoint creates a new entity node in the knowledge graph with the specified
properties. The system automatically generates vector embeddings for the entity
to enable semantic search and retrieval.
Request Body:
entity_name (str): Unique name identifier for the entity
entity_data (dict): Entity properties including:
- description (str): Textual description of the entity
- entity_type (str): Category/type of the entity (e.g., PERSON, ORGANIZATION, LOCATION)
- source_id (str): Related chunk_id from which the description originates
- Additional custom properties as needed
Response Schema:
{
"status": "success",
"message": "Entity 'Tesla' created successfully",
"data": {
"entity_name": "Tesla",
"description": "Electric vehicle manufacturer",
"entity_type": "ORGANIZATION",
"source_id": "chunk-123<SEP>chunk-456"
... (other entity properties)
}
}
HTTP Status Codes:
200: Entity created successfully
400: Invalid request (e.g., missing required fields, duplicate entity)
500: Internal server error
Example Request:
POST /graph/entity/create
{
"entity_name": "Tesla",
"entity_data": {
"description": "Electric vehicle manufacturer",
"entity_type": "ORGANIZATION"
}
}
"""
try:
# Use the proper acreate_entity method which handles:
# - Graph lock for concurrency
# - Vector embedding creation in entities_vdb
# - Metadata population and defaults
# - Index consistency via _edit_entity_done
result = await rag.acreate_entity(
entity_name=request.entity_name,
entity_data=request.entity_data,
)
return {
"status": "success",
"message": f"Entity '{request.entity_name}' created successfully",
"data": result,
}
except ValueError as ve:
logger.error(
f"Validation error creating entity '{request.entity_name}': {str(ve)}"
)
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(f"Error creating entity '{request.entity_name}': {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error creating entity: {str(e)}"
)
@router.post("/graph/relation/create", dependencies=[Depends(combined_auth)])
async def create_relation(request: RelationCreateRequest):
"""
Create a new relationship between two entities in the knowledge graph
This endpoint establishes an undirected relationship between two existing entities.
The provided source/target order is accepted for convenience, but the backend
stored edge is undirected and may be returned with the entities swapped.
Both entities must already exist in the knowledge graph. The system automatically
generates vector embeddings for the relationship to enable semantic search and graph traversal.
Prerequisites:
- Both source_entity and target_entity must exist in the knowledge graph
- Use /graph/entity/create to create entities first if they don't exist
Request Body:
source_entity (str): Name of the source entity (relationship origin)
target_entity (str): Name of the target entity (relationship destination)
relation_data (dict): Relationship properties including:
- description (str): Textual description of the relationship
- keywords (str): Comma-separated keywords describing the relationship type
- source_id (str): Related chunk_id from which the description originates
- weight (float): Relationship strength/importance (default: 1.0)
- Additional custom properties as needed
Response Schema:
{
"status": "success",
"message": "Relation created successfully between 'Elon Musk' and 'Tesla'",
"data": {
"src_id": "Elon Musk",
"tgt_id": "Tesla",
"description": "Elon Musk is the CEO of Tesla",
"keywords": "CEO, founder",
"source_id": "chunk-123<SEP>chunk-456"
"weight": 1.0,
... (other relationship properties)
}
}
HTTP Status Codes:
200: Relationship created successfully
400: Invalid request (e.g., missing entities, invalid data, duplicate relationship)
500: Internal server error
Example Request:
POST /graph/relation/create
{
"source_entity": "Elon Musk",
"target_entity": "Tesla",
"relation_data": {
"description": "Elon Musk is the CEO of Tesla",
"keywords": "CEO, founder",
"weight": 1.0
}
}
"""
try:
# Use the proper acreate_relation method which handles:
# - Graph lock for concurrency
# - Entity existence validation
# - Duplicate relation checks
# - Vector embedding creation in relationships_vdb
# - Index consistency via _edit_relation_done
result = await rag.acreate_relation(
source_entity=request.source_entity,
target_entity=request.target_entity,
relation_data=request.relation_data,
)
return {
"status": "success",
"message": f"Relation created successfully between '{request.source_entity}' and '{request.target_entity}'",
"data": result,
}
except ValueError as ve:
logger.error(
f"Validation error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(ve)}"
)
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(
f"Error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(e)}"
)
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error creating relation: {str(e)}"
)
@router.post("/graph/entities/merge", dependencies=[Depends(combined_auth)])
async def merge_entities(request: EntityMergeRequest):
"""
Merge multiple entities into a single entity, preserving all relationships
This endpoint consolidates duplicate or misspelled entities while preserving the entire
graph structure. It's particularly useful for cleaning up knowledge graphs after document
processing or correcting entity name variations.
What the Merge Operation Does:
1. Deletes the specified source entities from the knowledge graph
2. Transfers all relationships from source entities to the target entity
3. Intelligently merges duplicate relationships (if multiple sources have the same relationship)
4. Updates vector embeddings for accurate retrieval and search
5. Preserves the complete graph structure and connectivity
6. Maintains relationship properties and metadata
Use Cases:
- Fixing spelling errors in entity names (e.g., "Elon Msk" -> "Elon Musk")
- Consolidating duplicate entities discovered after document processing
- Merging name variations (e.g., "NY", "New York", "New York City")
- Cleaning up the knowledge graph for better query performance
- Standardizing entity names across the knowledge base
Request Body:
entities_to_change (list[str]): List of entity names to be merged and deleted
entity_to_change_into (str): Target entity that will receive all relationships
Response Schema:
{
"status": "success",
"message": "Successfully merged 2 entities into 'Elon Musk'",
"data": {
"merged_entity": "Elon Musk",
"deleted_entities": ["Elon Msk", "Ellon Musk"],
"relationships_transferred": 15,
... (merge operation details)
}
}
HTTP Status Codes:
200: Entities merged successfully
400: Invalid request (e.g., empty entity list, target entity doesn't exist)
500: Internal server error
Example Request:
POST /graph/entities/merge
{
"entities_to_change": ["Elon Msk", "Ellon Musk"],
"entity_to_change_into": "Elon Musk"
}
Note:
- The target entity (entity_to_change_into) must exist in the knowledge graph
- Source entities will be permanently deleted after the merge
- This operation cannot be undone, so verify entity names before merging
"""
try:
result = await rag.amerge_entities(
source_entities=request.entities_to_change,
target_entity=request.entity_to_change_into,
)
return {
"status": "success",
"message": f"Successfully merged {len(request.entities_to_change)} entities into '{request.entity_to_change_into}'",
"data": result,
}
except ValueError as ve:
logger.error(
f"Validation error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(ve)}"
)
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(
f"Error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(e)}"
)
logger.error(traceback.format_exc())
raise HTTPException(
status_code=500, detail=f"Error merging entities: {str(e)}"
)
return router

View File

@@ -0,0 +1,723 @@
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from typing import List, Dict, Any, Optional, Type
from lightrag.utils import logger
import time
import json
import re
from enum import Enum
from fastapi.responses import StreamingResponse
import asyncio
from lightrag import LightRAG, QueryParam
from lightrag.utils import TiktokenTokenizer
from lightrag.api.utils_api import get_combined_auth_dependency
from fastapi import Depends
# query mode according to query prefix (bypass is not LightRAG quer mode)
class SearchMode(str, Enum):
naive = "naive"
local = "local"
global_ = "global"
hybrid = "hybrid"
mix = "mix"
bypass = "bypass"
context = "context"
class OllamaMessage(BaseModel):
role: str
content: str
images: Optional[List[str]] = None
class OllamaChatRequest(BaseModel):
model: str
messages: List[OllamaMessage]
stream: bool = True
options: Optional[Dict[str, Any]] = None
system: Optional[str] = None
class OllamaChatResponse(BaseModel):
model: str
created_at: str
message: OllamaMessage
done: bool
class OllamaGenerateRequest(BaseModel):
model: str
prompt: str
system: Optional[str] = None
stream: bool = False
options: Optional[Dict[str, Any]] = None
class OllamaGenerateResponse(BaseModel):
model: str
created_at: str
response: str
done: bool
context: Optional[List[int]]
total_duration: Optional[int]
load_duration: Optional[int]
prompt_eval_count: Optional[int]
prompt_eval_duration: Optional[int]
eval_count: Optional[int]
eval_duration: Optional[int]
class OllamaVersionResponse(BaseModel):
version: str
class OllamaModelDetails(BaseModel):
parent_model: str
format: str
family: str
families: List[str]
parameter_size: str
quantization_level: str
class OllamaModel(BaseModel):
name: str
model: str
size: int
digest: str
modified_at: str
details: OllamaModelDetails
class OllamaTagResponse(BaseModel):
models: List[OllamaModel]
class OllamaRunningModelDetails(BaseModel):
parent_model: str
format: str
family: str
families: List[str]
parameter_size: str
quantization_level: str
class OllamaRunningModel(BaseModel):
name: str
model: str
size: int
digest: str
details: OllamaRunningModelDetails
expires_at: str
size_vram: int
class OllamaPsResponse(BaseModel):
models: List[OllamaRunningModel]
async def parse_request_body(
request: Request, model_class: Type[BaseModel]
) -> BaseModel:
"""
Parse request body based on Content-Type header.
Supports both application/json and application/octet-stream.
Args:
request: The FastAPI Request object
model_class: The Pydantic model class to parse the request into
Returns:
An instance of the provided model_class
"""
content_type = request.headers.get("content-type", "").lower()
try:
if content_type.startswith("application/json"):
# FastAPI already handles JSON parsing for us
body = await request.json()
elif content_type.startswith("application/octet-stream"):
# Manually parse octet-stream as JSON
body_bytes = await request.body()
body = json.loads(body_bytes.decode("utf-8"))
else:
# Try to parse as JSON for any other content type
body_bytes = await request.body()
body = json.loads(body_bytes.decode("utf-8"))
# Create an instance of the model
return model_class(**body)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON in request body")
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Error parsing request body: {str(e)}"
)
def estimate_tokens(text: str) -> int:
"""Estimate the number of tokens in text using tiktoken"""
tokens = TiktokenTokenizer().encode(text)
return len(tokens)
def parse_query_mode(query: str) -> tuple[str, SearchMode, bool, Optional[str]]:
"""Parse query prefix to determine search mode
Returns tuple of (cleaned_query, search_mode, only_need_context, user_prompt)
Examples:
- "/local[use mermaid format for diagrams] query string" -> (cleaned_query, SearchMode.local, False, "use mermaid format for diagrams")
- "/[use mermaid format for diagrams] query string" -> (cleaned_query, SearchMode.hybrid, False, "use mermaid format for diagrams")
- "/local query string" -> (cleaned_query, SearchMode.local, False, None)
"""
# Initialize user_prompt as None
user_prompt = None
# First check if there's a bracket format for user prompt
bracket_pattern = r"^/([a-z]*)\[(.*?)\](.*)"
bracket_match = re.match(bracket_pattern, query)
if bracket_match:
mode_prefix = bracket_match.group(1)
user_prompt = bracket_match.group(2)
remaining_query = bracket_match.group(3).lstrip()
# Reconstruct query, removing the bracket part
query = f"/{mode_prefix} {remaining_query}".strip()
# Unified handling of mode and only_need_context determination
mode_map = {
"/local ": (SearchMode.local, False),
"/global ": (
SearchMode.global_,
False,
), # global_ is used because 'global' is a Python keyword
"/naive ": (SearchMode.naive, False),
"/hybrid ": (SearchMode.hybrid, False),
"/mix ": (SearchMode.mix, False),
"/bypass ": (SearchMode.bypass, False),
"/context": (
SearchMode.mix,
True,
),
"/localcontext": (SearchMode.local, True),
"/globalcontext": (SearchMode.global_, True),
"/hybridcontext": (SearchMode.hybrid, True),
"/naivecontext": (SearchMode.naive, True),
"/mixcontext": (SearchMode.mix, True),
}
for prefix, (mode, only_need_context) in mode_map.items():
if query.startswith(prefix):
# After removing prefix and leading spaces
cleaned_query = query[len(prefix) :].lstrip()
return cleaned_query, mode, only_need_context, user_prompt
return query, SearchMode.mix, False, user_prompt
class OllamaAPI:
def __init__(self, rag: LightRAG, top_k: int = 60, api_key: Optional[str] = None):
self.rag = rag
self.ollama_server_infos = rag.ollama_server_infos
self.top_k = top_k
self.api_key = api_key
self.router = APIRouter(tags=["ollama"])
self.setup_routes()
def setup_routes(self):
# Create combined auth dependency for Ollama API routes
combined_auth = get_combined_auth_dependency(self.api_key)
@self.router.get("/version", dependencies=[Depends(combined_auth)])
async def get_version():
"""Get Ollama version information"""
return OllamaVersionResponse(version="0.9.3")
@self.router.get("/tags", dependencies=[Depends(combined_auth)])
async def get_tags():
"""Return available models acting as an Ollama server"""
return OllamaTagResponse(
models=[
{
"name": self.ollama_server_infos.LIGHTRAG_MODEL,
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"modified_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"size": self.ollama_server_infos.LIGHTRAG_SIZE,
"digest": self.ollama_server_infos.LIGHTRAG_DIGEST,
"details": {
"parent_model": "",
"format": "gguf",
"family": self.ollama_server_infos.LIGHTRAG_NAME,
"families": [self.ollama_server_infos.LIGHTRAG_NAME],
"parameter_size": "13B",
"quantization_level": "Q4_0",
},
}
]
)
@self.router.get("/ps", dependencies=[Depends(combined_auth)])
async def get_running_models():
"""List Running Models - returns currently running models"""
return OllamaPsResponse(
models=[
{
"name": self.ollama_server_infos.LIGHTRAG_MODEL,
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"size": self.ollama_server_infos.LIGHTRAG_SIZE,
"digest": self.ollama_server_infos.LIGHTRAG_DIGEST,
"details": {
"parent_model": "",
"format": "gguf",
"family": "llama",
"families": ["llama"],
"parameter_size": "7.2B",
"quantization_level": "Q4_0",
},
"expires_at": "2050-12-31T14:38:31.83753-07:00",
"size_vram": self.ollama_server_infos.LIGHTRAG_SIZE,
}
]
)
@self.router.post(
"/generate", dependencies=[Depends(combined_auth)], include_in_schema=True
)
async def generate(raw_request: Request):
"""Handle generate completion requests acting as an Ollama model
For compatibility purpose, the request is not processed by LightRAG,
and will be handled by underlying LLM model.
Supports both application/json and application/octet-stream Content-Types.
"""
try:
# Parse the request body manually
request = await parse_request_body(raw_request, OllamaGenerateRequest)
query = request.prompt
start_time = time.time_ns()
prompt_tokens = estimate_tokens(query)
if request.system:
self.rag.llm_model_kwargs["system_prompt"] = request.system
if request.stream:
response = await self.rag.llm_model_func(
query, stream=True, **self.rag.llm_model_kwargs
)
async def stream_generator():
first_chunk_time = None
last_chunk_time = time.time_ns()
total_response = ""
# Ensure response is an async generator
if isinstance(response, str):
# If it's a string, send in two parts
first_chunk_time = start_time
last_chunk_time = time.time_ns()
total_response = response
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": response,
"done": False,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
completion_tokens = estimate_tokens(total_response)
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": "",
"done": True,
"done_reason": "stop",
"context": [],
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
else:
try:
async for chunk in response:
if chunk:
if first_chunk_time is None:
first_chunk_time = time.time_ns()
last_chunk_time = time.time_ns()
total_response += chunk
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": chunk,
"done": False,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
except (asyncio.CancelledError, Exception) as e:
error_msg = str(e)
if isinstance(e, asyncio.CancelledError):
error_msg = "Stream was cancelled by server"
else:
error_msg = f"Provider error: {error_msg}"
logger.error(f"Stream error: {error_msg}")
# Send error message to client
error_data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": f"\n\nError: {error_msg}",
"error": f"\n\nError: {error_msg}",
"done": False,
}
yield f"{json.dumps(error_data, ensure_ascii=False)}\n"
# Send final message to close the stream
final_data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": "",
"done": True,
}
yield f"{json.dumps(final_data, ensure_ascii=False)}\n"
return
if first_chunk_time is None:
first_chunk_time = start_time
completion_tokens = estimate_tokens(total_response)
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": "",
"done": True,
"done_reason": "stop",
"context": [],
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
return
return StreamingResponse(
stream_generator(),
media_type="application/x-ndjson",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/x-ndjson",
"X-Accel-Buffering": "no", # Ensure proper handling of streaming responses in Nginx proxy
},
)
else:
first_chunk_time = time.time_ns()
response_text = await self.rag.llm_model_func(
query, stream=False, **self.rag.llm_model_kwargs
)
last_chunk_time = time.time_ns()
if not response_text:
response_text = "No response generated"
completion_tokens = estimate_tokens(str(response_text))
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
return {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"response": str(response_text),
"done": True,
"done_reason": "stop",
"context": [],
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
except Exception as e:
logger.error(f"Ollama generate error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@self.router.post(
"/chat", dependencies=[Depends(combined_auth)], include_in_schema=True
)
async def chat(raw_request: Request):
"""Process chat completion requests by acting as an Ollama model.
Routes user queries through LightRAG by selecting query mode based on query prefix.
Detects and forwards OpenWebUI session-related requests (for meta data generation task) directly to LLM.
Supports both application/json and application/octet-stream Content-Types.
"""
try:
# Parse the request body manually
request = await parse_request_body(raw_request, OllamaChatRequest)
# Get all messages
messages = request.messages
if not messages:
raise HTTPException(status_code=400, detail="No messages provided")
# Validate that the last message is from a user
if messages[-1].role != "user":
raise HTTPException(
status_code=400, detail="Last message must be from user role"
)
# Get the last message as query and previous messages as history
query = messages[-1].content
# Convert OllamaMessage objects to dictionaries
conversation_history = [
{"role": msg.role, "content": msg.content} for msg in messages[:-1]
]
# Check for query prefix
cleaned_query, mode, only_need_context, user_prompt = parse_query_mode(
query
)
start_time = time.time_ns()
prompt_tokens = estimate_tokens(cleaned_query)
param_dict = {
"mode": mode.value,
"stream": request.stream,
"only_need_context": only_need_context,
"conversation_history": conversation_history,
"top_k": self.top_k,
}
# Add user_prompt to param_dict
if user_prompt is not None:
param_dict["user_prompt"] = user_prompt
query_param = QueryParam(**param_dict)
if request.stream:
# Determine if the request is prefix with "/bypass"
if mode == SearchMode.bypass:
if request.system:
self.rag.llm_model_kwargs["system_prompt"] = request.system
response = await self.rag.llm_model_func(
cleaned_query,
stream=True,
history_messages=conversation_history,
**self.rag.llm_model_kwargs,
)
else:
response = await self.rag.aquery(
cleaned_query, param=query_param
)
async def stream_generator():
first_chunk_time = None
last_chunk_time = time.time_ns()
total_response = ""
# Ensure response is an async generator
if isinstance(response, str):
# If it's a string, send in two parts
first_chunk_time = start_time
last_chunk_time = time.time_ns()
total_response = response
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": response,
"images": None,
},
"done": False,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
completion_tokens = estimate_tokens(total_response)
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": "",
"images": None,
},
"done_reason": "stop",
"done": True,
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
else:
try:
async for chunk in response:
if chunk:
if first_chunk_time is None:
first_chunk_time = time.time_ns()
last_chunk_time = time.time_ns()
total_response += chunk
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": chunk,
"images": None,
},
"done": False,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
except (asyncio.CancelledError, Exception) as e:
error_msg = str(e)
if isinstance(e, asyncio.CancelledError):
error_msg = "Stream was cancelled by server"
else:
error_msg = f"Provider error: {error_msg}"
logger.error(f"Stream error: {error_msg}")
# Send error message to client
error_data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": f"\n\nError: {error_msg}",
"images": None,
},
"error": f"\n\nError: {error_msg}",
"done": False,
}
yield f"{json.dumps(error_data, ensure_ascii=False)}\n"
# Send final message to close the stream
final_data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": "",
"images": None,
},
"done": True,
}
yield f"{json.dumps(final_data, ensure_ascii=False)}\n"
return
if first_chunk_time is None:
first_chunk_time = start_time
completion_tokens = estimate_tokens(total_response)
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
data = {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": "",
"images": None,
},
"done_reason": "stop",
"done": True,
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
yield f"{json.dumps(data, ensure_ascii=False)}\n"
return StreamingResponse(
stream_generator(),
media_type="application/x-ndjson",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/x-ndjson",
"X-Accel-Buffering": "no", # Ensure proper handling of streaming responses in Nginx proxy
},
)
else:
first_chunk_time = time.time_ns()
# Determine if the request is prefix with "/bypass" or from Open WebUI's session title and session keyword generation task
match_result = re.search(
r"\n<chat_history>\nUSER:", cleaned_query, re.MULTILINE
)
if match_result or mode == SearchMode.bypass:
if request.system:
self.rag.llm_model_kwargs["system_prompt"] = request.system
response_text = await self.rag.llm_model_func(
cleaned_query,
stream=False,
history_messages=conversation_history,
**self.rag.llm_model_kwargs,
)
else:
response_text = await self.rag.aquery(
cleaned_query, param=query_param
)
last_chunk_time = time.time_ns()
if not response_text:
response_text = "No response generated"
completion_tokens = estimate_tokens(str(response_text))
total_time = last_chunk_time - start_time
prompt_eval_time = first_chunk_time - start_time
eval_time = last_chunk_time - first_chunk_time
return {
"model": self.ollama_server_infos.LIGHTRAG_MODEL,
"created_at": self.ollama_server_infos.LIGHTRAG_CREATED_AT,
"message": {
"role": "assistant",
"content": str(response_text),
"images": None,
},
"done_reason": "stop",
"done": True,
"total_duration": total_time,
"load_duration": 0,
"prompt_eval_count": prompt_tokens,
"prompt_eval_duration": prompt_eval_time,
"eval_count": completion_tokens,
"eval_duration": eval_time,
}
except Exception as e:
logger.error(f"Ollama chat error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python
"""
Start LightRAG server with Gunicorn
"""
import os
import sys
import platform
import pipmaster as pm
from lightrag.api.utils_api import display_splash_screen, check_env_file
from lightrag.api.config import global_args
from lightrag.utils import get_env_value
from lightrag.kg.shared_storage import initialize_share_data
from lightrag.constants import (
DEFAULT_WOKERS,
DEFAULT_TIMEOUT,
)
def check_and_install_dependencies():
"""Check and install required dependencies"""
required_packages = [
"gunicorn",
"tiktoken",
"psutil",
# Add other required packages here
]
for package in required_packages:
if not pm.is_installed(package):
print(f"Installing {package}...")
pm.install(package)
print(f"{package} installed successfully")
def main():
# Explicitly initialize configuration for Gunicorn mode
from lightrag.api.config import initialize_config
initialize_config()
# Set Gunicorn mode flag for lifespan cleanup detection
os.environ["LIGHTRAG_GUNICORN_MODE"] = "1"
# Check .env file
if not check_env_file():
sys.exit(1)
# Check DOCLING compatibility with Gunicorn multi-worker mode on macOS
if (
platform.system() == "Darwin"
and global_args.document_loading_engine == "DOCLING"
and global_args.workers > 1
):
print("\n" + "=" * 80)
print("❌ ERROR: Incompatible configuration detected!")
print("=" * 80)
print(
"\nDOCLING engine with Gunicorn multi-worker mode is not supported on macOS"
)
print("\nReason:")
print(" PyTorch (required by DOCLING) has known compatibility issues with")
print(" fork-based multiprocessing on macOS, which can cause crashes or")
print(" unexpected behavior when using Gunicorn with multiple workers.")
print("\nCurrent configuration:")
print(" - Operating System: macOS (Darwin)")
print(f" - Document Engine: {global_args.document_loading_engine}")
print(f" - Workers: {global_args.workers}")
print("\nPossible solutions:")
print(" 1. Use single worker mode:")
print(" --workers 1")
print("\n 2. Change document loading engine in .env:")
print(" DOCUMENT_LOADING_ENGINE=DEFAULT")
print("\n 3. Deploy on Linux where multi-worker mode is fully supported")
print("=" * 80 + "\n")
sys.exit(1)
# Check macOS fork safety environment variable for multi-worker mode
if (
platform.system() == "Darwin"
and global_args.workers > 1
and os.environ.get("OBJC_DISABLE_INITIALIZE_FORK_SAFETY") != "YES"
):
print("\n" + "=" * 80)
print("❌ ERROR: Missing required environment variable on macOS!")
print("=" * 80)
print("\nmacOS with Gunicorn multi-worker mode requires:")
print(" OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES")
print("\nReason:")
print(" NumPy uses macOS's Accelerate framework (Objective-C based) for")
print(" vector computations. The Objective-C runtime has fork safety checks")
print(" that will crash worker processes when embedding functions are called.")
print("\nCurrent configuration:")
print(" - Operating System: macOS (Darwin)")
print(f" - Workers: {global_args.workers}")
print(
f" - Environment Variable: {os.environ.get('OBJC_DISABLE_INITIALIZE_FORK_SAFETY', 'NOT SET')}"
)
print("\nHow to fix:")
print(" Option 1 - Set environment variable before starting (recommended):")
print(" export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES")
print(" lightrag-gunicorn --workers 2")
print("\n Option 2 - Add to your shell profile (~/.zshrc or ~/.bash_profile):")
print(" echo 'export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES' >> ~/.zshrc")
print(" source ~/.zshrc")
print("\n Option 3 - Use single worker mode (no multiprocessing):")
print(" lightrag-server --workers 1")
print("=" * 80 + "\n")
sys.exit(1)
# Check and install dependencies
check_and_install_dependencies()
# Note: Signal handlers are NOT registered here because:
# - Master cleanup already handled by gunicorn_config.on_exit()
# Display startup information
display_splash_screen(global_args)
print("🚀 Starting LightRAG with Gunicorn")
print(f"🔄 Worker management: Gunicorn (workers={global_args.workers})")
print("🔍 Preloading app: Enabled")
print("📝 Note: Using Gunicorn's preload feature for shared data initialization")
print("\n\n" + "=" * 80)
print("MAIN PROCESS INITIALIZATION")
print(f"Process ID: {os.getpid()}")
print(f"Workers setting: {global_args.workers}")
print("=" * 80 + "\n")
# Import Gunicorn's StandaloneApplication
from gunicorn.app.base import BaseApplication
# Define a custom application class that loads our config
class GunicornApp(BaseApplication):
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load_config(self):
# Define valid Gunicorn configuration options
valid_options = {
"bind",
"workers",
"worker_class",
"timeout",
"keepalive",
"preload_app",
"errorlog",
"accesslog",
"loglevel",
"certfile",
"keyfile",
"limit_request_line",
"limit_request_fields",
"limit_request_field_size",
"graceful_timeout",
"max_requests",
"max_requests_jitter",
}
# Special hooks that need to be set separately
special_hooks = {
"on_starting",
"on_reload",
"on_exit",
"pre_fork",
"post_fork",
"pre_exec",
"pre_request",
"post_request",
"worker_init",
"worker_exit",
"nworkers_changed",
"child_exit",
}
# Import and configure the gunicorn_config module
from lightrag.api import gunicorn_config
# Set configuration variables in gunicorn_config, prioritizing command line arguments
gunicorn_config.workers = (
global_args.workers
if global_args.workers
else get_env_value("WORKERS", DEFAULT_WOKERS, int)
)
# Bind configuration prioritizes command line arguments
host = (
global_args.host
if global_args.host != "0.0.0.0"
else os.getenv("HOST", "0.0.0.0")
)
port = (
global_args.port
if global_args.port != 9621
else get_env_value("PORT", 9621, int)
)
gunicorn_config.bind = f"{host}:{port}"
# Log level configuration prioritizes command line arguments
gunicorn_config.loglevel = (
global_args.log_level.lower()
if global_args.log_level
else os.getenv("LOG_LEVEL", "info")
)
# Timeout configuration prioritizes command line arguments
gunicorn_config.timeout = (
global_args.timeout + 30
if global_args.timeout is not None
else get_env_value(
"TIMEOUT", DEFAULT_TIMEOUT + 30, int, special_none=True
)
)
# Keepalive configuration
gunicorn_config.keepalive = get_env_value("KEEPALIVE", 5, int)
# SSL configuration prioritizes command line arguments
if global_args.ssl or os.getenv("SSL", "").lower() in (
"true",
"1",
"yes",
"t",
"on",
):
gunicorn_config.certfile = (
global_args.ssl_certfile
if global_args.ssl_certfile
else os.getenv("SSL_CERTFILE")
)
gunicorn_config.keyfile = (
global_args.ssl_keyfile
if global_args.ssl_keyfile
else os.getenv("SSL_KEYFILE")
)
# Set configuration options from the module
for key in dir(gunicorn_config):
if key in valid_options:
value = getattr(gunicorn_config, key)
# Skip functions like on_starting and None values
if not callable(value) and value is not None:
self.cfg.set(key, value)
# Set special hooks
elif key in special_hooks:
value = getattr(gunicorn_config, key)
if callable(value):
self.cfg.set(key, value)
if hasattr(gunicorn_config, "logconfig_dict"):
self.cfg.set(
"logconfig_dict", getattr(gunicorn_config, "logconfig_dict")
)
def load(self):
# Import the application
from lightrag.api.lightrag_server import get_application
return get_application(global_args)
# Create the application
app = GunicornApp("")
# Force workers to be an integer and greater than 1 for multi-process mode
workers_count = global_args.workers
if workers_count > 1:
# Set a flag to indicate we're in the main process
os.environ["LIGHTRAG_MAIN_PROCESS"] = "1"
initialize_share_data(workers_count)
else:
initialize_share_data(1)
# Run the application
print("\nStarting Gunicorn with direct Python API...")
app.run()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,128 @@
"""Helpers for validating startup runtime expectations from `.env`."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from dotenv import dotenv_values
_CONTAINER_RUNTIME_TARGETS = {"compose", "docker"}
@dataclass(frozen=True)
class RuntimeEnvironment:
"""Describes whether the current process is running in a container runtime."""
in_container: bool
in_docker: bool
in_kubernetes: bool
@property
def label(self) -> str:
if self.in_kubernetes:
return "Kubernetes"
if self.in_docker:
return "Docker"
return "host"
def _read_cgroup_content() -> str:
"""Best-effort read of cgroup metadata for container detection."""
for candidate in ("/proc/1/cgroup", "/proc/self/cgroup"):
try:
return Path(candidate).read_text(encoding="utf-8")
except OSError:
continue
return ""
def detect_runtime_environment(
environ: dict[str, str] | None = None,
) -> RuntimeEnvironment:
"""Detect whether the current process is running on host, Docker, or Kubernetes."""
environ = environ or os.environ
cgroup_content = _read_cgroup_content().lower()
in_kubernetes = bool(
environ.get("KUBERNETES_SERVICE_HOST")
or Path("/var/run/secrets/kubernetes.io/serviceaccount").exists()
or "kubepods" in cgroup_content
or "kubernetes" in cgroup_content
)
in_docker = bool(
Path("/.dockerenv").exists()
or Path("/run/.containerenv").exists()
or any(
marker in cgroup_content
for marker in ("docker", "containerd", "libpod", "podman")
)
)
return RuntimeEnvironment(
in_container=in_kubernetes or in_docker,
in_docker=in_docker,
in_kubernetes=in_kubernetes,
)
def load_runtime_target_from_env_file(env_path: str | Path = ".env") -> str | None:
"""Return the raw LIGHTRAG_RUNTIME_TARGET value from the `.env` file, if present."""
env_values = dotenv_values(str(env_path))
runtime_target = env_values.get("LIGHTRAG_RUNTIME_TARGET")
if runtime_target is None:
return None
return runtime_target.strip()
def validate_runtime_target(
runtime_target: str | None,
runtime_environment: RuntimeEnvironment | None = None,
) -> tuple[bool, str | None]:
"""Validate `.env` runtime target against the current runtime environment."""
if runtime_target is None:
return True, None
normalized_target = runtime_target.strip().lower()
runtime_environment = runtime_environment or detect_runtime_environment()
if normalized_target == "host":
if runtime_environment.in_container:
return (
False,
"Configuration error in .env: LIGHTRAG_RUNTIME_TARGET=host.\n"
"This value from .env requires the server process to run on the host, "
f"but the current process is running inside {runtime_environment.label}.",
)
return True, None
if normalized_target in _CONTAINER_RUNTIME_TARGETS:
if runtime_environment.in_container:
return True, None
return (
False,
f"Configuration error in .env: LIGHTRAG_RUNTIME_TARGET={runtime_target}.\n"
"This value from .env requires the server process to run inside Docker or "
"Kubernetes, but the current process is running on the host.",
)
return (
False,
f"Configuration error in .env: LIGHTRAG_RUNTIME_TARGET={runtime_target!r}.\n"
"This value from .env must be 'host' or 'compose' (alias: 'docker').",
)
def validate_runtime_target_from_env_file(
env_path: str | Path = ".env",
runtime_environment: RuntimeEnvironment | None = None,
) -> tuple[bool, str | None]:
"""Load LIGHTRAG_RUNTIME_TARGET from `.env` and validate it if declared."""
runtime_target = load_runtime_target_from_env_file(env_path)
return validate_runtime_target(runtime_target, runtime_environment)

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,451 @@
"""
Utility functions for the LightRAG API.
"""
import os
import argparse
from typing import Optional, List, Tuple
import sys
import time
import logging
from ascii_colors import ASCIIColors
from .._version import __api_version__ as api_version
from .._version import __version__ as core_version
from lightrag.constants import (
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE,
)
from lightrag.api.runtime_validation import validate_runtime_target_from_env_file
from fastapi import HTTPException, Security, Request, Response, status
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
from starlette.status import HTTP_403_FORBIDDEN
from .auth import auth_handler
from .config import ollama_server_infos, global_args, get_env_value
logger = logging.getLogger("lightrag")
# ========== Token Renewal Rate Limiting ==========
# Cache to track last renewal time per user (username as key)
# Format: {username: last_renewal_timestamp}
_token_renewal_cache: dict[str, float] = {}
_RENEWAL_MIN_INTERVAL = 60 # Minimum 60 seconds between renewals for same user
# ========== Token Renewal Path Exclusions ==========
# Paths that should NOT trigger token auto-renewal
# - /health: Health check endpoint, no login required
# - /documents/paginated: Client polls this frequently (5-30s), renewal not needed
# - /documents/pipeline_status: Client polls this very frequently (2s), renewal not needed
_TOKEN_RENEWAL_SKIP_PATHS = [
"/health",
"/documents/paginated",
"/documents/pipeline_status",
]
def check_env_file():
"""
Check if .env file exists and handle user confirmation if needed.
Returns True if should continue, False if should exit.
"""
env_path = ".env"
if not os.path.exists(env_path):
warning_msg = "Warning: Startup directory must contain .env file for multi-instance support."
ASCIIColors.yellow(warning_msg)
# Check if running in interactive terminal
if sys.stdin.isatty():
response = input("Do you want to continue? (yes/NO): ")
if response.lower() != "yes":
ASCIIColors.red("Server startup cancelled")
return False
return True
is_valid, error_message = validate_runtime_target_from_env_file(env_path)
if not is_valid:
for line in error_message.splitlines():
ASCIIColors.red(line)
return False
return True
# Get whitelist paths from global_args, only once during initialization
whitelist_paths = global_args.whitelist_paths.split(",")
# Pre-compile path matching patterns
whitelist_patterns: List[Tuple[str, bool]] = []
for path in whitelist_paths:
path = path.strip()
if path:
# If path ends with /*, match all paths with that prefix
if path.endswith("/*"):
prefix = path[:-2]
whitelist_patterns.append((prefix, True)) # (prefix, is_prefix_match)
else:
whitelist_patterns.append((path, False)) # (exact_path, is_prefix_match)
# Global authentication configuration
auth_configured = bool(auth_handler.accounts)
def get_combined_auth_dependency(api_key: Optional[str] = None):
"""
Create a combined authentication dependency that implements authentication logic
based on API key, OAuth2 token, and whitelist paths.
Args:
api_key (Optional[str]): API key for validation
Returns:
Callable: A dependency function that implements the authentication logic
"""
# Use global whitelist_patterns and auth_configured variables
# whitelist_patterns and auth_configured are already initialized at module level
# Only calculate api_key_configured as it depends on the function parameter
api_key_configured = bool(api_key)
# Create security dependencies with proper descriptions for Swagger UI
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="login", auto_error=False, description="OAuth2 Password Authentication"
)
# If API key is configured, create an API key header security
api_key_header = None
if api_key_configured:
api_key_header = APIKeyHeader(
name="X-API-Key", auto_error=False, description="API Key Authentication"
)
async def combined_dependency(
request: Request,
response: Response, # Added: needed to return new token via response header
token: str = Security(oauth2_scheme),
api_key_header_value: Optional[str] = None
if api_key_header is None
else Security(api_key_header),
):
# 1. Check if path is in whitelist
path = request.url.path
for pattern, is_prefix in whitelist_patterns:
if (is_prefix and path.startswith(pattern)) or (
not is_prefix and path == pattern
):
return # Whitelist path, allow access
# 2. Validate token first if provided in the request (Ensure 401 error if token is invalid)
if token:
try:
token_info = auth_handler.validate_token(token)
# ========== Token Auto-Renewal Logic ==========
from lightrag.api.config import global_args
from datetime import datetime, timezone
if global_args.token_auto_renew:
# Check if current path should skip token renewal
skip_renewal = any(
path == skip_path or path.startswith(skip_path + "/")
for skip_path in _TOKEN_RENEWAL_SKIP_PATHS
)
if skip_renewal:
logger.debug(f"Token auto-renewal skipped for path: {path}")
else:
try:
expire_time = token_info.get("exp")
if expire_time:
# Calculate remaining time ratio
now = datetime.now(timezone.utc)
remaining_seconds = (expire_time - now).total_seconds()
# Get original token expiration duration
role = token_info.get("role", "user")
total_hours = (
auth_handler.guest_expire_hours
if role == "guest"
else auth_handler.expire_hours
)
total_seconds = total_hours * 3600
# Issue new token if remaining time < threshold
if (
remaining_seconds
< total_seconds * global_args.token_renew_threshold
):
# ========== Rate Limiting Check ==========
username = token_info["username"]
current_time = time.time()
last_renewal = _token_renewal_cache.get(username, 0)
time_since_last_renewal = (
current_time - last_renewal
)
# Only renew if enough time has passed since last renewal
if time_since_last_renewal >= _RENEWAL_MIN_INTERVAL:
new_token = auth_handler.create_token(
username=username,
role=role,
metadata=token_info.get("metadata", {}),
)
# Return new token via response header
response.headers["X-New-Token"] = new_token
# Update renewal cache
_token_renewal_cache[username] = current_time
# Optional: log renewal
logger.info(
f"Token auto-renewed for user {username} "
f"(role: {role}, remaining: {remaining_seconds:.0f}s)"
)
else:
# Log skip due to rate limit
logger.debug(
f"Token renewal skipped for {username} "
f"(rate limit: last renewal {time_since_last_renewal:.0f}s ago)"
)
# ========== End of Rate Limiting Check ==========
except Exception as e:
# Renewal failure should not affect normal request, just log
logger.warning(f"Token auto-renew failed: {e}")
# ========== End of Token Auto-Renewal Logic ==========
# Accept guest token if no auth is configured
if not auth_configured and token_info.get("role") == "guest":
return
# Accept non-guest token if auth is configured
if auth_configured and token_info.get("role") != "guest":
return
# Token validation failed, immediately return 401 error
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token. Please login again.",
)
except HTTPException as e:
# If already a 401 error, re-raise it
if e.status_code == status.HTTP_401_UNAUTHORIZED:
raise
# For other exceptions, continue processing
# 3. Acept all request if no API protection needed
if not auth_configured and not api_key_configured:
return
# 4. Validate API key if provided and API-Key authentication is configured
if (
api_key_configured
and api_key_header_value
and api_key_header_value == api_key
):
return # API key validation successful
### Authentication failed ####
# if password authentication is configured but not provided, ensure 401 error if auth_configured
if auth_configured and not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No credentials provided. Please login.",
)
# if api key is provided but validation failed
if api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid API Key",
)
# if api_key_configured but not provided
if api_key_configured and not api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="API Key required",
)
# Otherwise: refuse access and return 403 error
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="API Key required or login authentication required.",
)
return combined_dependency
def display_splash_screen(args: argparse.Namespace) -> None:
"""
Display a colorful splash screen showing LightRAG server configuration
Args:
args: Parsed command line arguments
"""
# Banner
# Banner
top_border = "╔══════════════════════════════════════════════════════════════╗"
bottom_border = "╚══════════════════════════════════════════════════════════════╝"
width = len(top_border) - 4 # width inside the borders
line1_text = f"LightRAG Server v{core_version}/{api_version}"
line2_text = "Fast, Lightweight RAG Server Implementation"
line1 = f"{line1_text.center(width)}"
line2 = f"{line2_text.center(width)}"
banner = f"""
{top_border}
{line1}
{line2}
{bottom_border}
"""
ASCIIColors.cyan(banner)
# Server Configuration
ASCIIColors.magenta("\n📡 Server Configuration:")
ASCIIColors.white(" ├─ Host: ", end="")
ASCIIColors.yellow(f"{args.host}")
ASCIIColors.white(" ├─ Port: ", end="")
ASCIIColors.yellow(f"{args.port}")
ASCIIColors.white(" ├─ Workers: ", end="")
ASCIIColors.yellow(f"{args.workers}")
ASCIIColors.white(" ├─ Timeout: ", end="")
ASCIIColors.yellow(f"{args.timeout}")
ASCIIColors.white(" ├─ CORS Origins: ", end="")
ASCIIColors.yellow(f"{args.cors_origins}")
ASCIIColors.white(" ├─ SSL Enabled: ", end="")
ASCIIColors.yellow(f"{args.ssl}")
if args.ssl:
ASCIIColors.white(" ├─ SSL Cert: ", end="")
ASCIIColors.yellow(f"{args.ssl_certfile}")
ASCIIColors.white(" ├─ SSL Key: ", end="")
ASCIIColors.yellow(f"{args.ssl_keyfile}")
ASCIIColors.white(" ├─ Ollama Emulating Model: ", end="")
ASCIIColors.yellow(f"{ollama_server_infos.LIGHTRAG_MODEL}")
ASCIIColors.white(" ├─ Log Level: ", end="")
ASCIIColors.yellow(f"{args.log_level}")
ASCIIColors.white(" ├─ Verbose Debug: ", end="")
ASCIIColors.yellow(f"{args.verbose}")
ASCIIColors.white(" ├─ API Key: ", end="")
ASCIIColors.yellow("Set" if args.key else "Not Set")
ASCIIColors.white(" └─ JWT Auth: ", end="")
ASCIIColors.yellow("Enabled" if args.auth_accounts else "Disabled")
# Directory Configuration
ASCIIColors.magenta("\n📂 Directory Configuration:")
ASCIIColors.white(" ├─ Working Directory: ", end="")
ASCIIColors.yellow(f"{args.working_dir}")
ASCIIColors.white(" └─ Input Directory: ", end="")
ASCIIColors.yellow(f"{args.input_dir}")
# LLM Configuration
ASCIIColors.magenta("\n🤖 LLM Configuration:")
ASCIIColors.white(" ├─ Binding: ", end="")
ASCIIColors.yellow(f"{args.llm_binding}")
ASCIIColors.white(" ├─ Host: ", end="")
ASCIIColors.yellow(f"{args.llm_binding_host}")
ASCIIColors.white(" ├─ Model: ", end="")
ASCIIColors.yellow(f"{args.llm_model}")
ASCIIColors.white(" ├─ Max Async for LLM: ", end="")
ASCIIColors.yellow(f"{args.max_async}")
ASCIIColors.white(" ├─ Summary Context Size: ", end="")
ASCIIColors.yellow(f"{args.summary_context_size}")
ASCIIColors.white(" ├─ LLM Cache Enabled: ", end="")
ASCIIColors.yellow(f"{args.enable_llm_cache}")
ASCIIColors.white(" └─ LLM Cache for Extraction Enabled: ", end="")
ASCIIColors.yellow(f"{args.enable_llm_cache_for_extract}")
# Embedding Configuration
ASCIIColors.magenta("\n📊 Embedding Configuration:")
ASCIIColors.white(" ├─ Binding: ", end="")
ASCIIColors.yellow(f"{args.embedding_binding}")
ASCIIColors.white(" ├─ Host: ", end="")
ASCIIColors.yellow(f"{args.embedding_binding_host}")
ASCIIColors.white(" ├─ Model: ", end="")
ASCIIColors.yellow(f"{args.embedding_model}")
ASCIIColors.white(" ├─ Dimensions: ", end="")
ASCIIColors.yellow(f"{args.embedding_dim}")
ASCIIColors.white(" └─ Asymmetric: ", end="")
ASCIIColors.yellow(f"{args.embedding_asymmetric}")
# RAG Configuration
ASCIIColors.magenta("\n⚙️ RAG Configuration:")
ASCIIColors.white(" ├─ Summary Language: ", end="")
ASCIIColors.yellow(f"{args.summary_language}")
ASCIIColors.white(" ├─ Entity Types: ", end="")
ASCIIColors.yellow(f"{args.entity_types}")
ASCIIColors.white(" ├─ Max Parallel Insert: ", end="")
ASCIIColors.yellow(f"{args.max_parallel_insert}")
ASCIIColors.white(" ├─ Chunk Size: ", end="")
ASCIIColors.yellow(f"{args.chunk_size}")
ASCIIColors.white(" ├─ Chunk Overlap Size: ", end="")
ASCIIColors.yellow(f"{args.chunk_overlap_size}")
ASCIIColors.white(" ├─ Cosine Threshold: ", end="")
ASCIIColors.yellow(f"{args.cosine_threshold}")
ASCIIColors.white(" ├─ Top-K: ", end="")
ASCIIColors.yellow(f"{args.top_k}")
ASCIIColors.white(" └─ Force LLM Summary on Merge: ", end="")
ASCIIColors.yellow(
f"{get_env_value('FORCE_LLM_SUMMARY_ON_MERGE', DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, int)}"
)
# System Configuration
ASCIIColors.magenta("\n💾 Storage Configuration:")
ASCIIColors.white(" ├─ KV Storage: ", end="")
ASCIIColors.yellow(f"{args.kv_storage}")
ASCIIColors.white(" ├─ Vector Storage: ", end="")
ASCIIColors.yellow(f"{args.vector_storage}")
ASCIIColors.white(" ├─ Graph Storage: ", end="")
ASCIIColors.yellow(f"{args.graph_storage}")
ASCIIColors.white(" ├─ Document Status Storage: ", end="")
ASCIIColors.yellow(f"{args.doc_status_storage}")
ASCIIColors.white(" └─ Workspace: ", end="")
ASCIIColors.yellow(f"{args.workspace if args.workspace else '-'}")
# Server Status
ASCIIColors.green("\n✨ Server starting up...\n")
# Server Access Information
protocol = "https" if args.ssl else "http"
if args.host == "0.0.0.0":
ASCIIColors.magenta("\n🌐 Server Access Information:")
ASCIIColors.white(" ├─ WebUI (local): ", end="")
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}")
ASCIIColors.white(" ├─ Remote Access: ", end="")
ASCIIColors.yellow(f"{protocol}://<your-ip-address>:{args.port}")
ASCIIColors.white(" ├─ API Documentation (local): ", end="")
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs")
ASCIIColors.white(" └─ Alternative Documentation (local): ", end="")
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/redoc")
ASCIIColors.magenta("\n📝 Note:")
ASCIIColors.cyan(""" Since the server is running on 0.0.0.0:
- Use 'localhost' or '127.0.0.1' for local access
- Use your machine's IP address for remote access
- To find your IP address:
• Windows: Run 'ipconfig' in terminal
• Linux/Mac: Run 'ifconfig' or 'ip addr' in terminal
""")
else:
base_url = f"{protocol}://{args.host}:{args.port}"
ASCIIColors.magenta("\n🌐 Server Access Information:")
ASCIIColors.white(" ├─ WebUI (local): ", end="")
ASCIIColors.yellow(f"{base_url}")
ASCIIColors.white(" ├─ API Documentation: ", end="")
ASCIIColors.yellow(f"{base_url}/docs")
ASCIIColors.white(" └─ Alternative Documentation: ", end="")
ASCIIColors.yellow(f"{base_url}/redoc")
# Security Notice
if args.key:
ASCIIColors.yellow("\n⚠️ Security Notice:")
ASCIIColors.white(""" API Key authentication is enabled.
Make sure to include the X-API-Key header in all your requests.
""")
if args.auth_accounts:
ASCIIColors.yellow("\n⚠️ Security Notice:")
ASCIIColors.white(""" JWT authentication is enabled.
Make sure to login before making the request, and include the 'Authorization' in the header.
""")
# Ensure splash output flush to system log
sys.stdout.flush()

View File

@@ -0,0 +1 @@
import{H as e,R as t,S as n,U as r,b as i,c as a,f as o,n as s,r as c,u as l,v as u}from"./_createAssigner-CdflnHPZ.js";import{A as d,C as f,D as p,M as m,N as h,S as g,T as _,f as v,h as y,u as b,w as x}from"./_baseUniq-CSNgIvS9.js";var S=/\s/;function C(e){for(var t=e.length;t--&&S.test(e.charAt(t)););return t}var w=/^\s+/;function T(e){return e&&e.slice(0,C(e)+1).replace(w,``)}var E=NaN,D=/^[-+]0x[0-9a-f]+$/i,O=/^0b[01]+$/i,k=/^0o[0-7]+$/i,A=parseInt;function j(t){if(typeof t==`number`)return t;if(h(t))return E;if(e(t)){var n=typeof t.valueOf==`function`?t.valueOf():t;t=e(n)?n+``:n}if(typeof t!=`string`)return t===0?t:+t;t=T(t);var r=O.test(t);return r||k.test(t)?A(t.slice(2),r?2:8):D.test(t)?E:+t}var M=1/0,N=17976931348623157e292;function P(e){return e?(e=j(e),e===M||e===-M?(e<0?-1:1)*N:e===e?e:0):e===0?e:0}function F(e){var t=P(e),n=t%1;return t===t?n?t-n:t:0}function I(e){return e!=null&&e.length?g(e,1):[]}var L=Object.prototype,R=L.hasOwnProperty,z=c(function(e,n){e=Object(e);var r=-1,i=n.length,o=i>2?n[2]:void 0;for(o&&s(n[0],n[1],o)&&(i=1);++r<i;)for(var c=n[r],l=a(c),u=-1,d=l.length;++u<d;){var f=l[u],p=e[f];(p===void 0||t(p,L[f])&&!R.call(e,f))&&(e[f]=c[f])}return e});function B(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}function V(e){return function(t,n,r){var i=Object(t);if(!u(t)){var a=v(n,3);t=p(t),n=function(e){return a(i[e],e,i)}}var o=e(t,n,r);return o>-1?i[a?t[o]:o]:void 0}}var H=Math.max;function U(e,t,n){var r=e==null?0:e.length;if(!r)return-1;var i=n==null?0:F(n);return i<0&&(i=H(r+i,0)),d(e,v(t,3),i)}var W=V(U);function G(e,t){var n=-1,r=u(e)?Array(e.length):[];return b(e,function(e,i,a){r[++n]=t(e,i,a)}),r}function K(e,t){return(i(e)?m:G)(e,v(t,3))}var q=Object.prototype.hasOwnProperty;function J(e,t){return e!=null&&q.call(e,t)}function Y(e,t){return e!=null&&y(e,t,J)}var X=`[object String]`;function Z(e){return typeof e==`string`||!i(e)&&n(e)&&r(e)==X}function Q(t,n,r,i){if(!e(t))return t;n=_(n,t);for(var a=-1,s=n.length,c=s-1,u=t;u!=null&&++a<s;){var d=x(n[a]),f=r;if(d===`__proto__`||d===`constructor`||d===`prototype`)return t;if(a!=c){var p=u[d];f=i?i(p,d,u):void 0,f===void 0&&(f=e(p)?p:l(n[a+1])?[]:{})}o(u,d,f),u=u[d]}return t}function $(e,t,n){for(var r=-1,i=t.length,a={};++r<i;){var o=t[r],s=f(e,o);n(s,o)&&Q(a,_(o,e),s)}return a}export{G as a,z as c,P as d,K as i,I as l,Z as n,W as o,Y as r,B as s,$ as t,F as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{n as e,t}from"./path-BmDdnQs8.js";import{$ as n,Q as r,X as i,Y as a,Z as o,at as s,it as c,nt as l,ot as u,rt as d,st as f,tt as p}from"./index-Cmfh6eB3.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var s=e-n,c=t-r,d=(o?a:-a)/u(s*s+c*c),f=d*c,p=-d*s,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*u(l(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var l=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+l.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-p,_=T.apply(this,arguments)-p,v=a(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>f-1e-12)D.moveTo(h*n(g),h*s(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*n(_),m*s(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):u(m*m+h*h)),R=d(a(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*s(I)),W=o(L/h*s(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*n(A),K=h*s(A),q=m*n(N),J=m*s(N);if(R>1e-12){var Y=h*n(j),X=h*s(j),Z=m*n(M),Q=m*s(M),$;if(v<c)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/s(i((ee*ne+te*re)/(u(ee*ee+te*te)*u(ne*ne+re*re)))/2),ae=u($[0]*$[0]+$[1]*$[1]);z=d(R,(m-ae)/(ie-1)),B=d(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,r(V.y01,V.x01),r(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,r(V.y01,V.x01),r(V.y11,V.x11),!k),D.arc(0,0,h,r(V.cy+V.y11,V.cx+V.x11),r(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,r(H.y11,H.x11),r(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,r(V.y01,V.x01),r(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,r(V.y01,V.x01),r(V.y11,V.x11),!k),D.arc(0,0,m,r(V.cy+V.y11,V.cx+V.x11),r(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,r(H.y11,H.x11),r(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+l.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-c/2;return[n(t)*e,s(t)*e]},k.innerRadius=function(t){return arguments.length?(l=typeof t==`function`?t:e(+t),k):l},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};

View File

@@ -0,0 +1 @@
import"./chunk-K5T4RW27-Bdzw7m0z.js";import{n as e}from"./chunk-7N4EOEYR-Bu6dy4pK.js";export{e as createArchitectureServices};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
Array.prototype.slice;function e(e){return typeof e==`object`&&`length`in e?e:Array.from(e)}export{e as t};

Some files were not shown because too many files have changed in this diff Show More