• ERP /ai/fieldOptions 已上线(feature/ai @5d4ff83),其返回的 assign 声明了
    选中一行后还应带出哪些列。前端在选中时按 assign 收集该行对应值随提交回传,
    buildCreate/buildQuote 用 applyAssign 合并:只填用户没填过的列、且必须是目标表
    真实存在的非系统列——用户已写的备注不会被客户档案的备注覆盖(ERP 回执 4.1 提醒)。
    
    修复 ERP 指出的实际缺陷:我们建的 10 张报价 sCustomerName 全空,而原生单会填。
    实测:assign 提交后 sCustomerName/sCustomerNo 落入载荷;用户已填字段不被覆盖。
    zichun authored
     
    Browse Code »
  • 用户在 FK 选择器里点中某一行后,前端把该行 id 随保存回传(form/submit 与
    preview/save 均加 boundIds:字段技术名→id),normalize 有 id 时直接绑定、
    不按名称反查——候选的权限/租户/级联判定以 ERP 的下拉配置为准。
    未经选择、仅凭名称(AI 预填或手打)的路径不变,仍保持同名 fail-closed。
    幻影列硬闸在 id 直绑路径上同样生效。
    
    回归锁 +2(FormRenderNormalizeTest 8 例),46 单测绿。
    实测:同名产品不给 id → 报同名拒绝;给 id → 精确绑定、ERP 执行 success+200,
    落库 BJD202607092 的客户/产品 id 与点中行一致。
    zichun authored
     
    Browse Code »
  • ErpClient.fieldOptions 调 /ai/fieldOptions(契约见 docs/erp-tasks-field-options.md):
    ERP 按其控件配置给候选,带行级数据权限、级联过滤(产品限定在已选客户名下)、
    联动回填映射——这三样本地直查库都没有。404/异常/mode=none 一律回落到
    FormResolverService.fkOptionPage,5 分钟退避避免反复撞未上线的端点。
    
    前端 FK 选择器改传 (formId, 字段, ctx):ctx = 表单上已绑定记录的 id,供级联过滤;
    ERP 报 need_context 时提示'请先选择客户'而不是给空列表。预览卡新增 formId 与
    fk 字段的 boundId,使修改单据时也有初始级联上下文。
    zichun authored
     
    Browse Code »
  • - create 载荷 sFormId 改写模块 id(与 ERP 原生同构;表单 id 无
      SysBillNoSettings 编号规则,执行期重生成单号必撞已用号)
    - ai_op_queue 停写 sUserId(ERP 执行器已切读 sMakePerson;列暂
      不可删——ERP myTodos 仍 COALESCE 引用)
    - saaslocal 开启 erp.dry-run.enabled(ERP /business/checkBusinessData
      已上线,保存前真实校验链 validate-only)
    
    联合 e2e 实测:建报价 BJD202607091(sFormId=模块id、主从落库)→
    审核 success+200 → 重复审核 fail+409 → 改字段 success+200 →
    取消 fail+450 → 幂等 success+201;dry-run 保存时真实调用(ERP 日志
    时刻对齐)且零落库。
    zichun authored
     
    Browse Code »
  • …/bill-no instead of asking (restores ext S1 12/12)
    zichun authored
     
    Browse Code »
  • …expired key; preview save name-keyed + empty=abandon-change; previewId atomic claim; transactional batch queue + column truncation; digest zone token-budgeted; window-orphan tool_result trim; convId length cap; readFormData authoritative moduleId + /form/options permission gate (security audit); WRITE_CLAIM state-statement lookahead
    zichun authored
     
    Browse Code »
  • …e shared kernel + PreviewService (previewId Redis binding, save-side re-validation, state legality checks) + queue-only OpService (sMakePerson dual-write, status=confirmed); retire ProposeWriteTool/OpController/AuditService/ErpClient writes; preview card frontend; skills/prompt/bench sync; D: checkBusinessData stub behind erp.dry-run.enabled
    zichun authored
     
    Browse Code »
  • …-budget projection (conservative estimator, in-turn demotion, pre-send self-check, prompt_eval_count calibration)
    zichun authored
     
    Browse Code »

  • …to file line 1; bench: simulate production anti-fab retry guard
    zichun authored
     
    Browse Code »
  • - single ReAct entry for all free text: 7 fixed tools (useSkill + 6);
      flow knowledge lives in 5 skill files (resources/skills/*.md, first
      line = brief for prompt index; xly.skills.dir overrides for hot reload)
    - useSkill returns skill body and logs skill_active; projection pins the
      active skill full text on the flow card until confirm/cancel/new skill
    - system prompt from template prompts/system.txt (domain map + skill
      index); bench reads the same files to avoid replica drift
    - delete IntentService/Intent/SlotFillService/LlmJsonClient/StateService/
      OkHttpUtil and route()/handleCreate/handleWrite/deriveWriteAction/
      ground/withState; dimension-split regex migrated into FormCollectTool
      prefill assist (model provably fails dimension splitting)
    - anti-fabrication guards now unconditional (zero-tools+digits retry,
      WRITE_CLAIM check); invariant layer and button endpoints untouched
    - bench_ext: --arch new (same-source prompt/skills) and --fifty
      (bench50 cases judged on full trajectory for both arches)
    zichun authored
     
    Browse Code »
  • …p+CAS, tenant enforcement
    
    Audit findings 1-5 confirmed real; root causes were (a) identity self-reported by
    the client, (b) every failure path defaulting open.
    
    - ERP /ai/whoami (saas-8s+ @c8e0057 follow-up) resolves token → real user/tenant/type;
      AuthzService.resolveIdentity is now the single identity entry point. Client-supplied
      userid/brandsid/usertype removed from all request bodies and from chat.html.
    - dev-login is a real switch (erp.dev-login.enabled, default false) and its ERP creds
      no longer have built-in admin/666666 defaults; blank token in production → 401,
      never a silent fall back to the dev (sysadmin) account.
    - conversations/op/form endpoints require login; conversation ids are namespaced by
      user id and ownership-checked (403 otherwise); /op/pending no longer returns sPayload.
    - op confirm/cancel check the proposer, and confirm claims the draft via CAS so
      concurrent/repeat confirms cannot execute twice; bill numbers are regenerated at
      execution time instead of replaying the propose-time snapshot.
    - FK options take the tenant from the introspected identity and return empty rather
      than dropping the sBrandsId predicate.
    - secrets moved to env vars (DB_URL/DB_USERNAME/DB_PASSWORD/REDIS_*/LLM_*/ERP_BASEURL);
      allowMultiQueries=false. NOTE: the previously committed credentials must be rotated.
    - ids interpolated into ERP URLs are validated (safeId) to stop query/path injection.
    - update path rejects system columns, resolves FK names, and coerces by column type;
      numeric coercion now rejects unparseable input instead of writing 0/truncating, and
      proposal summaries show the value that will actually be written.
    - ResponseStatusException keeps its 401/403 status instead of being wrapped as 200.
    - anti-fabrication guard stays on when the intent gate itself fails.
    zichun authored
     
    Browse Code »
  • - drop jQuery + page-in-page card; collapsible sidebar, single scroll area,
      light minimal style, markdown tables (markdown-it)
    - collectForm: grid layout, required flags, typed controls, enum dropdown,
      FK secondary picker modal (multi-column list / search / paging / select)
    - form submit posts structured fields to /api/agent/form/submit (no NL re-encoding);
      legacy FORM_SUBMIT_MARK route removed
    - askUser card: option chips + always-available free-text input
    - history replays from ledger endpoint; localStorage history dropped
    zichun authored
     
    Browse Code »

  • - delete queryData (QueryTool, sqlChatModel bean, llm.sql-model, jsqlparser dep)
    - delete kgSearch (findForms stays), delete loadSkill (SkillTool/SkillService)
    - single system prompt (domain map + 业务常识) replaces 3 ToolScope versions; ToolScope removed
    - intent gate reduced to 查询/新增/操作已有单据/其他 with class definitions only;
      write action derived from utterance in router
    - dedup: shared locateRecord() in ProposeWriteTool; name-field/label lookups
      consolidated into FormResolverService
    zichun authored
     
    Browse Code »
  • - Replace langchain4j-ollama with langchain4j-open-ai; AgentFactory/
      QueryTool depend on ChatModel/StreamingChatModel interfaces
    - OllamaJsonClient -> LlmJsonClient: raw /v1/chat/completions with
      response_format json_schema; single OkHttpClient instance
    - Thinking off at all 3 call sites via reasoning_effort=none (the only
      switch that works on /v1; think:false and /no_think are ignored)
    - Drop client-side length params (num_ctx/num_predict/max_tokens) —
      server-side OLLAMA_CONTEXT_LENGTH=16384 on xlyllm covers them
    - Unified tracing: TracingChatModelListener.record() shared by listener
      callbacks and LlmJsonClient; per-request model name from ctx; sql
      model now has the listener too (was untraced and mislabeled)
    - Config keys langchain4j.ollama.* -> llm.{base-url,api-key,chat-model,
      sql-model}; both models = qwen3.6-27b-iq3:latest (tools+thinking+
      vision, verified: streaming tool-calls / json_schema / reasoning off)
    
    Verified end-to-end on :8199: intent gate traced (340/28 tokens), agent
    tool loop, correct answers; warm 1-4s, cold load ~96s (KEEP_ALIVE=30s).
    zichun authored
     
    Browse Code »
  • P4 retirement + code slim-down (~18.5k lines deleted), on top of the
    intent-gate WIP (docs §17/§18 + agent/tool refinements):
    
    - Delete old 8-scene multi-agent stack: XlyErpService, SceneSelector/
      Chati/ErpAi/DynamicTableNl2Sql agents, DynamicToolProvider (62 meta
      tools), Scene/ToolMeta/ParamRule entities+mappers+startup caches
    - Delete whole milvus/tts/ocr packages, /api/tts + /api/ocr endpoints,
      tts.html, python stream_server.py; strip dead TTS JS from chat.html
      (playByIndex/handleNormalResponse had no callers)
    - Three-pass orphan sweep: 17 dead utils, 16 dead entities, dead
      constants/exceptions, RedisService+RedisConfig, duplicate
      JacksonConfig, OperableChatMemoryProvider, old prompt generators;
      fold PageController into MvcConfig; trim OkHttpUtil 495→39 lines
    - pom: ~35→13 deps (drop mybatis/JPA/webflux/tika/pdfbox/poi/jieba/
      jsoup/gson/fastjson2/springdoc/mapstruct/pagehelper/pinyin4j/
      spring-retry/jnr-ffi/hutool/ocr/milvus/embeddings; add starter-jdbc);
      war 200M+→48M
    - application.yml: drop milvus/tts/ocr/tesseract/mybatis blocks;
      GlobalExceptionHandler returns plain {code,message} JSON
    - docs: mark P4 retirement done in agent-architecture §13/§18
    
    Verified: mvn clean package OK; boot smoke on :8199 (Started 0.9s,
    /chat 200, health db UP). Old /api/tts & /api/ocr now 404 by design.
    zichun authored
     
    Browse Code »

  • Add a deterministic §5 intent stage before ReAct: extract {intent, form,
    entities, missing slots} via constrained JSON, then expose only the 3-5
    tools relevant to that intent instead of all 12.
    
    New:
    - agent/Intent, agent/ToolScope: intent + visible-tool-set model
    - service/IntentService: constrained-JSON intent/entity extraction
    - service/OllamaJsonClient: JSON-mode Ollama calls
    - service/SlotFillService: slot extraction/prefill of known values
    
    Wire through AgentFactory, SystemPromptService, QueryTool,
    ProposeWriteTool, ErpClient, AgentChatController, OpController, chat.html.
    zichun authored
     
    Browse Code »

  • User: form rendered every field as a plain text box; no type (dropdown/date/number), and
    FK fields didn't pull options from their source tables.
    
    - FormCollect schema now carries per-field type: fkselect | select | number | date | text
      (+ options for select, fkTable for fkselect, hint for placeholder).
      Types inferred from column data type for generic forms; curated for quote.
    - New GET /api/agent/form/options?table=&q=&brandsid= -> FK options from the source table
      (elecustomer/eleproduct/elematerials...), whitelisted to tables that appear as sFkTable in the
      field dict (rejects arbitrary tables e.g. gdslogininfo), tenant-filtered + LIMIT.
    - chat.html renderFormCollect renders by type: fkselect = searchable <input list=datalist> that
      fetches options on focus/typing; select = <select>; number = <input type=number>; date picker; text.
    - Fix: tolerant label matching (strip parentheticals) so '多数量' maps whether or not the LLM keeps
      the '(逗号分隔)' hint -> multi-qty rows now reliably created.
    
    Verified via agent: quote form shows 客户/产品/物料=fkselect, 单位/印刷/颜色/单双面=select,
    尺寸/数量/单价/系数=number; options endpoint returns real names (客户 q=Little -> Little Antelope);
    non-FK table rejected; full write incl 多数量 (BJD202607086 -> 2000/4000/6000).
    zichun authored
     
    Browse Code »
  • …en, prod injects live token)
    zichun authored
     
    Browse Dir »
  • - ErpClient.execStaging(token, opId): POST /ai/execStaging/{opId} (user-token身份)
    - OpController: erp.exec-staging.enabled -> delegate confirm to ERP executor; default false keeps tested direct path
    - application-saaslocal.yml: erp.exec-staging.enabled=false (documented)
    zichun authored
     
    Browse Dir »
  • …at UI (question/form_collect/token)
    
    - QueryTool: enforce table allowlist (viw_* + form data-sources + field-dict tables) via jsqlparser TablesNamesFinder -> blocks NL2SQL reading gdslogininfo/sysjurisdiction/ai_op_queue etc; single-table tenant predicate injection (sBrandsId); brand hint in prompt
    - TracingChatModelListener: config-gated Langfuse ingestion export (generation span) via JDK HttpClient, no new deps; docker-compose.langfuse.yml self-host
    - chat.html: send identity+token in chat req; render question(options) + form_collect(rich fields); forward Authorization on confirm/cancel
    - application-saaslocal.yml: langfuse config (off by default)
    zichun authored
     
    Browse Dir »
  • …ormCollect + Skills + KgSearch
    
    - AgentIdentity + AgentFactory: build agent per request with token+form-allowlist carried in tool instances (per-call context; robust vs LC4j callback threading)
    - ErpClient: token-aware read/write/examine; user-token expiry does NOT silently re-login as dev-admin (no privilege escalation)
    - AuthzService: devIdentity()/userIdentity() resolve granted-form set (sAuthsId)
    - proposeExamine tool + OpController examine dispatch + forward user Authorization on confirm
    - InteractionTool.askUser (structured options), FormCollectTool.collectForm (real gdsconfigformslave schema)
    - SkillService + SkillTool.loadSkill + ai_skill digest in system prompt
    - KgQueryTool.kgSearch: L2 neighbors/flow + L3 field->table
    - tools ErpReadTool/ProposeWriteTool/QueryTool/FormCollectTool now per-request (not @Component)
    zichun authored
     
    Browse File »

  • - ai_op_queue: staging table for AI write ops (draft|confirmed|executed|failed|cancelled)
    - ProposeWriteTool.proposeUpdate(entity, record, field, newValue): self-resolves the entity master table (excludes viw_* report views), resolves field via 字段字典, locates the unique record + old value, stages a DRAFT to ai_op_queue — DOES NOT execute; returns a proposal
    - ErpClient.updateForm: executes via ERP addUpdateDelBusinessData with the frontend-compatible payload {data:[{sTable,name:master,column:[{handleType:update,sId,field:value}]}]}
    - OpController: deterministic (non-LLM) POST /op/{id}/confirm (executes + writes back status) / cancel / GET pending
    - AgentChatController.onToolExecuted: on proposeUpdate, attach conversation + push write_proposal SSE
    - chat.html: confirm/cancel card + result echo
    - system prompt: proposeUpdate flow; never claim a write is done before confirm
    
    Verified end-to-end: '把必胜客简称改成必胜客中国' -> proposal card -> confirm -> ERP update -> DB changed; cancel -> DB unchanged. Nothing executes without explicit user confirm.
    zichun authored
     
    Browse File »
  • - ErpReadTool: optional keyword -> LIKE filter on the form's resolved name field (viw_kg_field_dict, *Name); keyword doc clarified (only for locating a named record, empty for counts)
    - ErpClient.readForm: accepts filterField/filterValue -> bFilter
    - KgQueryTool.findForms: rank by bAiTool(curated) + table + flow-connectivity + name length so the top hit is usually the canonical form
    - SystemPromptService: hard 'always Simplified Chinese, no other language' rule (kills qwen language drift); prefer top form; read directly instead of over-asking
    - AgentConfig: qwen3 think(false)/returnThinking(false) -> fast + disciplined tool-calling, no leaked chain-of-thought
    - application-saaslocal.yml: chat model -> qwen3:14b
    
    Verified: no more foreign-language leaks; '多少客户'->92; keyword lookup finds 必胜客; proc-backed stock forms readable; greeting 2s, form lookup ~6s. Known limit: which form/columns get picked for a specific-field-of-specific-record query still varies (deferred KgSearch).
    zichun authored
     
    Browse Dir »
  • - RedisChatMemoryStore (ChatMemoryStore): persist per-conversation messages to Redis (chat:mem:{convId}, 30d TTL) -> survives restart; agent memory now Redis-backed via MessageWindowChatMemory.builder().chatMemoryStore(...)
    - ConversationService: per-user conversation metadata (chat:convs:{userId} hash), title from first message, list/create/delete/history
    - ConversationController: GET/POST/DELETE /api/agent/conversations + GET /{id}/messages
    - AgentChatController: touch conversation on each turn
    - chat.html: conversation sidebar (list/switch/new/delete), load history on switch, refresh on send
    
    Verified: multi-turn memory recalls facts; after full restart, conversation list + history + memory all persist (agent still recalls prior-turn facts).
    zichun authored
     
    Browse File »
  • - ErpClient: thin HTTP client to ERP xlyEntry; dev-login (/checklogin admin) mints+caches a real session token, auto re-login on session expiry (code=-2); reads getBusinessDataByFormcustomId (read-only params only, never bUpdate)
    - ErpReadTool.readFormData(formId,moduleId): reads a form's real rows, renders Chinese column labels from viw_kg_field_dict, first 10 rows + total count
    - wired into the agent (tools = findForms + readFormData); system prompt now describes the findForms->readFormData flow (still read-only, writes still 'under development')
    - application-saaslocal.yml: erp.baseurl -> local xlyEntry :8697 + dev-login creds
    
    Verified: '我们有多少客户' -> agent finds the form, reads 92 real customers, renders a clean Chinese table (name/category/salesperson/tax).
    zichun authored
     
    Browse Dir »
  • - ReActAgent: AiServices streaming tool-loop (replaces 8-scene SceneSelector path)
    - AgentConfig: single agent = streaming Ollama(qwen2.5:14b) + KgQueryTool + per-conversation memory + L1-domain system prompt
    - SystemPromptService: injects L1 domain map (viw_kg_domain) into system prompt; degrades gracefully if KG views missing
    - KgQueryTool.findForms: read-only form-catalog lookup over local viw_ai_useful_forms (global metadata, no tenant) — interim formId resolver
    - AgentChatController: POST /api/agent/chat SSE stream ({type:token|reset|done|error}); reset drops pre-tool-call narration
    - chat.html: consume the new SSE endpoint (streamed), per-page conversationId, drop TTS-borrow path
    - application-saaslocal.yml: add to worktree (was master-only untracked -> profile silently hit remote DB)
    
    Verified running (saaslocal, :8099): streaming chat, tool loop returns real forms, write requests correctly report 'under development'.
    zichun authored
     
    Browse File »