Commit Graph

9 Commits

Author SHA1 Message Date
weicw-dshsa 1b61fe5586 feat(ai): 自由模式转创意助手,失败日志补失败节点与上游 error(issue 17 / 18)
自由模式(issue 18):
- docType 为空时不调「内容创作与宣传助手」——它开始节点的 doc_type 是必填 select,
  空值直接被上游判 failed(error="doc_type is required in input form")。改为转
  「AI 创意助手」,只发它唯一的必填入参 user_content。
- ManuscriptGenInputs 增加 userContent;doc_type / Number 仍无条件带键(空串能过
  Dify 的必填校验,缺键不行)。
- 用例:AiManuscriptFreeModeRouteTest、AiManuscriptInputsContractTest。

失败日志(issue 17):
- 稿件流程、大纲流程、executeWorkflowForResult 三处失败日志补 status + failedNode +
  upstreamError + elapsed。失败信息拆在 node_finished 与 workflow_finished 两类事件,
  入参校验类失败只有后者,只取节点事件等于没取。
- 补 event=error 与「静默断流」(没有 workflow_finished / message_end)的日志,
  两者都带 taskId,可与请求对上。
- 日志级用 error 而非 warn:生产 com.gxwebsoft 只开 ERROR。
- 用例:AiWorkflowFailureDetailTest。
2026-10-21 12:40:05 +08:00
weicw-dshsa 21e49c6a1a fix(ai): WebSocket 推送静默化,并补重试与静默推送用例(issue 11 / 12)
- WebSocketServer.sendMessage 里两行 System.out.println 删除。该方法被**每个 SSE
  事件**调用一次(大纲单次生成实测 3062 个事件,即一次请求刷 6000+ 行),而且绕过
  日志框架——既不受 logging.level 控制(生产把 com.gxwebsoft 压到 ERROR 也拦不住),
  也不进日志文件,真故障时需要的几行会被顶掉。日志改到低频的 onOpen / onClose;
  同时把 onOpen 里静默吞掉的握手回应异常改成 WARN(那是「连上了却收不到内容」的
  第一现场)。
- 新增用例:AiCreativeRetryTest、AiCreativeRetryIntegrationTest(issue 11,
  后者用本地 HTTP server 扮演 Dify,断言瞬时故障确实重跑一次、凭证失效只请求一次)、
  WebSocketServerSilentPushTest(issue 12,捕获 System.out 断言推送路径零输出)。
2026-10-13 20:00:01 +08:00
weicw-dshsa f54e28cffc fix(ai): 失败态在返回体里带 status=failed,前端才能区分失败文案与正文(issue 10)
上游工作流 failed 时,后端把 reply 置为失败文案("生成失败,请稍后重试")
并照常走成功路径返回(HTTP 200 / code=0),而返回体里**没有 status**:

    data = {"answer":"生成失败,请稍后重试", "conversationId":"…", "messageId":"…"}

两端前端的 isFailedPayload(读 payload.status / payload.data.status)因此
永远落空,只能把失败文案当正文渲染(实测三个 admin 页面都会这样)。
修 issue 09 时上游每次都 failed,这条是必现的。

改动:
- buildManuscriptResult 增加 failed 参数(保留三参重载,等价 failed=false,
  以免破坏 issue 08 既有测试的调用形态),失败时 put("status","failed")。
- 新增 buildActivityOutlineResult(同构),activityOutlineGen 收尾改用之。
- 两个方法都在外层作用域记录 workflowFailed:失败分支里有 continue,
  不能用分支内的局部布尔。

刻意不改的东西:
- 失败仍走 HTTP 200 / code=0。改成 code!=0 会与既有约定冲突、影响所有调用方
  的 catch 分支,本条只加一个字段即可让两端现有判定生效。
- answer 键在失败态下依然存在(issue 08 契约),只是现在有了 status 可区分。

测试:新增 AiWorkflowFailureHttpStatusTest 8/8,含反向用例——
成功态**不得**带 status(否则 isFailedPayload 会误伤正常结果),
以及「成功态正文恰好是失败文案」时唯一判据是 status。
全量 210 跑 0 失败(2 个 ServerContainer not available 为预存问题)。

验证:用桩上游(probe/stub-dify-failure.py,回 workflow_finished{status:failed})
起第二个后端实例,实测两条链路的返回体都带 status='failed' 且
answer 仍为可读文案,前端 isFailedPayload 判定为 true —— 全链路闭合。
2026-10-13 02:12:46 +08:00
weicw-dshsa fbc9774e19 fix(ai): 大纲链路补齐上游必填变量,资金归一化为数值(issue 09)
活动策划大纲当时必然失败,界面只显示「生成失败,请稍后重试」。
两条根因(单变量对照实测确证):

1. Dify 开始节点 event_funding 是 number,而实体与两端前端都是字符串,
   小程序问答助手页默认值还是中文「待定」→ must be a valid number,0.2s 即 failed。
2. event_member / tel_people 是上游必填变量,实体里根本没有字段,从不发送
   → event_member is required in input form。

实施中发现的关键事实,推翻了 issue 初稿的一条建议:
「is required in input form」检查的是键是否存在,不是值是否非空 ——

  缺 event_member 键 / null  → failed: is required in input form
  "event_member": ""        → succeeded(4752 条 message)
  "event_funding": ""       → succeeded(7268 条 message)
  "event_funding": "待定"    → failed: must be a valid number

所以「干脆不发该字段」行不通(不发 = 报 required),正确做法是键始终下发、
值空串兜底。

改动:
- ActivityOutlineInputs 补 eventMember / peopleManage / telPeople 三字段
  (snake + 驼峰别名);eventFunding 保持 String 承载用户原文。
- buildActivityOutlineInputs:6 个上游必填变量无条件占位(空值走空白转空串,
  不能给 null —— null 会被上游当缺失);event_type / event_location /
  people_manage / event_meme 实测非必填,保持「为空不发」。
- 新增 normalizeEventFunding:抽出首个金额并归一化为 BigDecimal。
  两个必须先处理的坑,否则静默发出错误金额(比留空更糟):
    · 日期:'2026年4月办活动,预算5000元' 的 2026 会被当金额 → 先剔除日期
    · 单位前空格:'预算 1 万元' 只认紧邻的「万」会抽成 1 元 → \s* 容忍
  另支持 万/亿/千/百 倍率与 '3万5千' 连续降序单位相加;抽不出即降级空串。
- 前置校验抽成 validateActivityOutline,只校验主题/受众/时间三项。
  刻意不校验另外三个上游必填项 —— 上游必填不等于用户必填,加校验会把两端
  默认入参挡在门外(改前端默认值为空串后,旧校验会直接返回
  'event_funding不能为空',比原缺陷更糟)。
- buildLoadingText 加 120 字上限:agent_thought 的思维链实测可达数 KB,
  整段塞进 loading 气泡等于把思维链推给端上。
- 去掉 isDisplayableText 之后的恒假条件(空串已被排除),并注释说明
  reply.indexOf("") 恒为 0 会误判已存在,故两道判断都不能少。

配套前端:
- uniapp 问答助手页资金默认值 '待定' → ''(未提及 != 待定)
- admin 入参类型补注释与 3 个可选键

测试:新增 AiActivityOutlineContractTest 23/23,含「凡校验放行的入参必带上游
全部必填键」的不变量用例;AiManuscriptUuidLeakTest 增至 21 条,补 loading
长度上限与真实 message_end 形状。全量 202 跑 0 失败
(2 个 ServerContainer not available 为预存问题)。

验收:probe/outline-e2e.py 走真实后端,用两端前端默认入参与 issue 原文要求的
'待定' 一档,复刻 apiRequest 回落 + formatOutlineResult + parseAssistantContent
判定,29/29 PASS。
2026-10-13 00:37:15 +08:00
weicw-dshsa a8c8dd8643 feat(dify): 全部迁移到内网实例,统一为单份配置
VPN 打通后开发机可直连内网,原先「默认公网 + prod 覆盖内网」的双份配置
已无必要,且两套 Key/知识库 ID 完全不通用(实测互打一律 401),
留着只会造成误用。公网那套整体移除,回退到 git 历史取。

- application.yml:base-url 与 8 个应用 Key、全部知识库密钥/ID 改指内网;
  原默认值来自公网,其注释同步改写
- application-dev.yml / application-prod.yml:删除 dify 覆盖段。
  YAML map 是按键合并而非整体替换,保留一份与默认值相同的覆盖
  只会在将来漏改时混出「一半内网一半公网」的状态
- DifyProperties:默认 baseUrl 改内网,类注释同步

修复一个上线起就坏的接口:

- admin 端「竞赛历史」文档列表按 challenge-cup 取库,而该 ID
  b342f096-… 在**公网实例上也返回 404**(公网真实的挑战杯库是
  7c812029-…,从未登记),故该接口一直不可用。改指内网「竞赛案例」
  库(181d58e3-…,145 篇),实测已能列出文档。对外路径保持
  /challenge-cup/documents 不变,以免影响 admin 端调用

清理失效的工作流变量(内网实测已从开始节点移除):

- document-process 的 query / pdf_url,declaration 的 parse_value。
  三者配置为空串以显式丢弃 —— 直接删行会退回「按原名透传」,
  反而把 pdfUrl / parseValue 这类驼峰名发给 Dify。实体字段保留,
  不破坏 /documentProcess/upload 的入参契约(pdfUrl 仍参与
  「必须提供文件或 pdfUrl」的校验,只是不再转发)
- 注:Dify 对多传变量静默忽略,故此前不报错、功能未受影响,
  属「看着对其实无效」的配置

测试:改断言为内网值,并新增「不残留任何公网配置」「失效变量必须
配成空串」等防回归用例;变异验证(base-url 改回公网 / 空串改回生效名)
确认用例有真实判别力。Dify 相关 44 例全绿。

Refs: .scratch/dify-probe/README.md, .scratch/ai-assistant/map.md
2026-10-12 12:37:33 +08:00
weicw-dshsa a2a83f7d9e fix(ai): 稿件/大纲链路失败态不再把标识符当正文推给端上
这三条路径在 workflow_finished 事件只 extractAnswer、从不读 status:
工作流失败时 outputs 为空、answer 可能被填成 workflow_run_id(UUID),
原样下发等于把内部标识符当正文推给端上(issue 03)。

- activityOutlineGen:失败先判定,推「生成失败,请稍后重试」(reset=true)
  再正常收尾
- executeWorkflowForResult:同上;最终结果失败时只回 status + 可读文案,
  不带 outputs / raw
- manuscriptGen:自查发现该方法不走通用执行、自带一份缺口,一并补齐
- socket 发送器增加 reset 字段:端上收到后丢弃失败前已累积的半截内容
- 新增 AiWorkflowFailureStatusTest 10 例:锁定「失败判定先于取 answer」
  的顺序契约与顶层/嵌套 status、大小写空白、成功不误判等边界

Refs: .scratch/ai-assistant/issues/03-other-links-raw-payload-fallback.md
2026-09-24 04:22:29 +08:00
weicw-dshsa c598d88506 test(ai): 补端到端形状契约用例
把 buildCreativeAssistantResult 在失败态下的**真实产出**打成一行 JSON,
供 .scratch/ai-assistant/verify-frontend-e2e.js 当输入喂给前端真实渲染函数,
验证的是「后端真实产出 → 前端真实渲染」整条链路,而非两侧各自自测。

实测产出为 {"status":"failed","answer":"生成失败,请稍后重试"},
不含 raw / workflow_id / req_id / 上游错误栈。

Refs: .scratch/ai-assistant/issues/01-creative-assistant-raw-json-bubble.md
2026-09-24 03:51:49 +08:00
weicw-dshsa 23a7f001cc fix(ai): 创意生成工作流失败时不再把原始报文吐给用户
Dify 以 workflow_finished + data.status=failed + outputs={} 收场时,后端只搬运
字段、从不读 status,把一个失败结果包成 success 下发,且把整包报文塞进 raw;
前端 formatCreativeResult 取不到业务字段就落到 text || stringifyUnknown(payload),
整段原始 JSON(含 workflow_id / task_id / req_id 与上游错误栈)进了聊天气泡。

- 新增 workflowStatus / isWorkflowFailed:失败时只回 status + 可读 answer,
  不下发 outputs / raw,不携带上游错误文本
- executeCreativeAssistant 的 workflow_finished 分支先判失败,命中则直接返回,
  不再用流式片段或 taskId 覆盖失败文案
- 失败报文顶层 answer 是 workflow_run_id(UUID),故失败文案用固定字符串,
  不取自 answer
- 新增 AiCreativeFailureResultTest:13 个用例,样本为 issue 原文的真实失败报文

Refs: .scratch/ai-assistant/issues/01-creative-assistant-raw-json-bubble.md
2026-09-24 03:44:08 +08:00
weicw-dshsa 8c994d9ed2 Dify 配置外置并切至内网实例
接口地址与各应用 API Key 原先硬编码在 AiController 与
KnowledgeBaseController 中,换环境要改代码、密钥还会散落在多处。
新增 DifyProperties 统一收敛到 dify 配置节点,并补上文档中缺失的
两个应用与知识库管理接口。

- 新增 DifyProperties,外置 base-url / 8 个应用 Key / 知识库密钥,
  默认值保持公网实例,本地开发不受影响
- 生产 profile 覆盖为内网 10.100.158.15,含全部 8 个应用 Key 与
  两个知识库的独立密钥
- 新增申报书生成、智能文档处理两个工作流接口(含文件上传版本),
  接入既有的 WebSocket 流式推送与停止机制
- KnowledgeBaseController 补齐知识库增删改查、文档管理与索引进度,
  并对文档状态动作加白名单,避免路径参数被任意拼接
- 修正 AI 内容创作的 number 变量名:Dify 侧为 Number,
  变量名大小写敏感,此前一直发送小写导致该功能缺参
- 生产下显式覆盖公网知识库 ID 的密钥:YAML 的 map 按键合并而非整体
  替换,不覆盖会残留指向公网实例的死密钥

工作流变量名取自内网实例实测(probe-dify.sh),其中申报书生成与
文档处理两组变量与公网命名习惯不一致,故以配置映射承载,改名只需
改 yml 而非改代码。

新增 18 项测试,覆盖配置绑定、profile 覆盖完整性(逐项断言 8 个
Key 均已覆盖且不残留公网 Key)与入参到 Dify inputs 的映射。
2026-09-21 19:46:49 +08:00