From 7d9c0894f81a3ba62c1d67b16068f774392490e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Thu, 2 Jul 2026 11:17:52 +0800 Subject: [PATCH 1/3] fix: limit roundtable chain rail height --- .../src/roundtable-planning/pages/RoundtablePlanningPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 633964e5..908fcabe 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -3067,7 +3067,7 @@ function RoundtableEntryRail({ {chains.length === 0 ? (

暂无公共业务链

) : ( -
+
{chains.map((chain) => { const selected = selectedChainId === chain.id; @@ -3078,7 +3078,7 @@ function RoundtableEntryRail({
Date: Fri, 3 Jul 2026 14:56:35 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=8A=80=E8=83=BD=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills-data/xdfx-full-flow-skill-guide.md | 4 ++ .../xdfx-single-agent-full-flow/SKILL.md | 4 +- .../scripts/xdfx_flow.py | 48 +++++++++++++------ skills-data/xdfx-split-controller/SKILL.md | 4 +- .../scripts/xdfx_flow.py | 48 +++++++++++++------ skills-data/xdfx-split-flow-skill-guide.md | 5 +- .../xdfx-step1-audience/scripts/xdfx_flow.py | 48 +++++++++++++------ .../xdfx-step2-pathway/scripts/xdfx_flow.py | 48 +++++++++++++------ .../scripts/xdfx_flow.py | 48 +++++++++++++------ .../scripts/xdfx_flow.py | 48 +++++++++++++------ .../xdfx-step5-audience-strategy/SKILL.md | 2 +- .../scripts/xdfx_flow.py | 48 +++++++++++++------ .../xdfx-step6-pathway-strategy/SKILL.md | 2 +- .../scripts/xdfx_flow.py | 48 +++++++++++++------ 14 files changed, 287 insertions(+), 118 deletions(-) diff --git a/skills-data/xdfx-full-flow-skill-guide.md b/skills-data/xdfx-full-flow-skill-guide.md index 024233fe..62555388 100644 --- a/skills-data/xdfx-full-flow-skill-guide.md +++ b/skills-data/xdfx-full-flow-skill-guide.md @@ -12,6 +12,8 @@ 6. 让用户选择一个或多个关键传播节点生成策略并入库;受众和节点不能在同一步交叉。 7. 同步前端步骤状态,保证原 `item-aiceh` 页面步骤条可继续识别。 +策略入库归属由步骤强制决定:Step5 固定写“目标受众账号”和 `designId + strategyId` 关系,Step6 固定写“关键传播节点账号”和 `pathwayId + strategyId` 关系,不用模型返回的 `accountType` 判断归属。 + ## 技能位置 ```text @@ -146,6 +148,8 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl 策略信息形式/模态会按前端 `modelType` 字典保存为 value 逗号串,例如模型输出“图片,视频”时,脚本先查 `sys/sys-dict-data` 再写入 `task-action-message.modelType`,不会直接保存中文文字。 +策略关系表写入时,Step5 只写受众 `designId`,Step6 只写传播节点 `pathwayId`;即使模型把 `账号类型/accountType` 生成反了,脚本也会按当前步骤强制纠正,避免受众策略和传播节点策略串写。 + ## 输出文件 执行后查看: diff --git a/skills-data/xdfx-single-agent-full-flow/SKILL.md b/skills-data/xdfx-single-agent-full-flow/SKILL.md index 4f8151ce..7ecee90a 100644 --- a/skills-data/xdfx-single-agent-full-flow/SKILL.md +++ b/skills-data/xdfx-single-agent-full-flow/SKILL.md @@ -73,6 +73,8 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl Step5 只能选择受众生成策略;Step6 只能选择关键传播节点生成策略。不要在同一步里同时传 `--select-strategy-audiences` 和 `--select-strategy-pathways`,受众和关键传播节点不能交叉生成策略。 +策略归属必须由当前步骤强制决定,不能相信模型返回的 `账号类型/accountType`:Step5 固定按“目标受众账号”保存,只写 `designId + strategyId` 关系;Step6 固定按“关键传播节点账号”保存,只写 `pathwayId + strategyId` 关系,严禁把受众策略写到关键传播节点或反向串写。 + 脚本每次调用接口前还会输出 `操作反馈 {...}`,包含当前操作、HTTP 方法和 URL。智能体必须用简短中文告诉用户“正在做什么、调用哪个接口”。 每次保存接口成功拿到返回 ID 后会输出 `入库反馈 {...}`,包含 `入库对象类型`、`入库对象名称`、`返回ID` 和 `接口路径`。智能体必须把它转述成“已入库 xxx 受众 / xxx 脆弱点 / xxx 策略”等明确日志。 @@ -106,7 +108,7 @@ python scripts/xdfx_flow.py --mode full --input xdfx-input.json --dry-run - 每个被选中的受众/传播节点默认生成最多 3 个策略,可用 `--max-strategies` 调整。 - 每个保存接口必须拿到返回 `id` 才进入下游。 - 保存后会同步前端 `task/task-action.step` 步骤状态;如只想入库不更新步骤条,追加 `--no-step-status`。 -- 策略保存会同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`,兼容策略表格/编排页面读取。 +- 策略保存会同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`,兼容策略表格/编排页面读取;Step5 只写受众 `designId` 关系,Step6 只写传播节点 `pathwayId` 关系,归属不使用模型返回的 `accountType` 判断。 - 策略 `信息形式/模态` 入库前会读取 `sys/sys-dict-data` 的 `modelType` 字典,把模型返回的“图片,视频/文本,图片”等文字转成前端需要的 value 逗号串;字典读取失败时使用内置兜底映射。 - Step1 的受众目标抽取结果里,`用户编号列表` / `编号id` / `id` 必须对应上一轮 `zhanghao` 账号数组的索引值;脚本会按这个索引依次找到账号并保存受众账号关系。 - 模型流式 `{"answer":"..."}` 会自动拼接。 diff --git a/skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py b/skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py +++ b/skills-data/xdfx-single-agent-full-flow/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-split-controller/SKILL.md b/skills-data/xdfx-split-controller/SKILL.md index e2af1e50..fb2d02f3 100644 --- a/skills-data/xdfx-split-controller/SKILL.md +++ b/skills-data/xdfx-split-controller/SKILL.md @@ -58,6 +58,8 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl Step5 只能选择受众生成策略;Step6 只能选择关键传播节点生成策略。总控不能在同一步混传 `--select-strategy-audiences` 和 `--select-strategy-pathways`。 +策略归属必须由当前步骤强制决定,不能相信模型返回的 `账号类型/accountType`:Step5 固定按“目标受众账号”保存,只写 `designId + strategyId` 关系;Step6 固定按“关键传播节点账号”保存,只写 `pathwayId + strategyId` 关系,严禁把受众策略写到关键传播节点或反向串写。 + 每次保存接口成功拿到返回 ID 后会输出 `入库反馈 {...}`。总控必须告诉用户已入库的对象类型、名称和返回 ID,例如“已入库关键传播节点:xxx,pathwayAccountId=123”。 如果接口或模型处理报错,会立即输出 `错误反馈 {...}`。总控必须立刻把失败阶段、URL、状态码、请求摘要和响应摘要告诉用户。 @@ -95,7 +97,7 @@ python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow- - Step4/Step6 的传播节点下游分析默认从已入库传播节点中选择前 2 个继续;可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。 - Step3-Step6 对上述选中的每个受众/传播节点默认生成最多 3 个脆弱点或策略。 - 各步骤会同步前端 `task/task-action.step`;需要禁用时每条命令追加 `--no-step-status`。 -- 策略步骤会同步写入 channel、narrate、message、策略与受众/传播节点关系表,保证原前端策略表格/编排页可继续读取。 +- 策略步骤会同步写入 channel、narrate、message、策略与受众/传播节点关系表,保证原前端策略表格/编排页可继续读取;Step5 只写受众 `designId` 关系,Step6 只写传播节点 `pathwayId` 关系,归属不使用模型返回的 `accountType` 判断。 - 策略步骤保存 `信息形式/模态` 时,必须使用脚本读取 `sys/sys-dict-data` 的 `modelType` 字典并把文字转成 value 逗号串,不能把“图片,视频”等文字直接写入 `task-action-message.modelType`。 - 每个步骤完成都会输出 `步骤反馈`,总控必须逐步转述给用户。 - 每次操作和接口调用都会输出 `操作反馈`,总控必须逐条转述当前操作。 diff --git a/skills-data/xdfx-split-controller/scripts/xdfx_flow.py b/skills-data/xdfx-split-controller/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-split-controller/scripts/xdfx_flow.py +++ b/skills-data/xdfx-split-controller/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-split-flow-skill-guide.md b/skills-data/xdfx-split-flow-skill-guide.md index d75eef97..a68c15cc 100644 --- a/skills-data/xdfx-split-flow-skill-guide.md +++ b/skills-data/xdfx-split-flow-skill-guide.md @@ -141,6 +141,7 @@ python scripts/xdfx_run_full_steps.py --input xdfx-input.json --state xdfx-flow- - Step4/Step6 的传播节点下游分析默认从已入库传播节点中选择前 2 个继续;可用 `--downstream-audiences` / `--downstream-pathways` 调成 1 或 2。 - Step3-Step6 对上述选中的每个受众/传播节点默认生成最多 3 个脆弱点或策略。 - Step5/Step6 的策略模态必须按前端 `modelType` 字典保存为 value 逗号串;脚本会自动把“图片,视频/文本,图片”等文字转成 `task-action-message.modelType` 需要的值。 +- Step5/Step6 的策略归属由步骤强制决定:Step5 固定写“目标受众账号”和 `designId + strategyId` 关系,Step6 固定写“关键传播节点账号”和 `pathwayId + strategyId` 关系,不用模型返回的 `accountType` 判断归属。 总控反馈用户时建议使用这种格式: @@ -233,7 +234,7 @@ pathwayVulns - 输出 `用户协助`,等待用户选择一个或多个受众;带 `--select-strategy-audiences` 重跑后才生成并保存这些受众的策略。 - 本步骤只能选择受众,不能选择或混入关键传播节点。 - 保存策略主表。 -- 同步写入策略 channel、narrate、message 和策略-受众关系。 +- 同步写入策略 channel、narrate、message 和策略-受众关系;只写 `designId + strategyId`,即使模型返回关键传播节点账号类型也要按受众策略保存。 - 同步前端 `step-analysis-strategy`、`step-analysis-strategy-gantt` 状态。 输出写入: @@ -252,7 +253,7 @@ audienceStrategies - 输出 `用户协助`,等待用户选择一个或多个关键传播节点;带 `--select-strategy-pathways` 重跑后才生成并保存这些传播节点的策略。 - 本步骤只能选择关键传播节点,不能选择或混入受众。 - 保存策略主表。 -- 同步写入策略 channel、narrate、message 和策略-传播节点关系。 +- 同步写入策略 channel、narrate、message 和策略-传播节点关系;只写 `pathwayId + strategyId`,即使模型返回受众账号类型也要按传播节点策略保存。 - 同步前端 `step-analysis-strategy`、`step-analysis-strategy-gantt` 状态。 输出写入: diff --git a/skills-data/xdfx-step1-audience/scripts/xdfx_flow.py b/skills-data/xdfx-step1-audience/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step1-audience/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step1-audience/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-step2-pathway/scripts/xdfx_flow.py b/skills-data/xdfx-step2-pathway/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step2-pathway/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step2-pathway/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-step3-audience-vulns/scripts/xdfx_flow.py b/skills-data/xdfx-step3-audience-vulns/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step3-audience-vulns/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step3-audience-vulns/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-step4-pathway-vulns/scripts/xdfx_flow.py b/skills-data/xdfx-step4-pathway-vulns/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step4-pathway-vulns/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step4-pathway-vulns/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-step5-audience-strategy/SKILL.md b/skills-data/xdfx-step5-audience-strategy/SKILL.md index 1abba8a8..2826e2e3 100644 --- a/skills-data/xdfx-step5-audience-strategy/SKILL.md +++ b/skills-data/xdfx-step5-audience-strategy/SKILL.md @@ -24,7 +24,7 @@ python scripts/xdfx_flow.py --mode step5 --input xdfx-input.json --state xdfx-fl - 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。 - 自动解析 `发布人设N / 账号类型N / 信息标题N` 等字段。 - 调用策略保存接口。 -- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`。 +- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`;本步骤强制按“目标受众账号”保存,只写 `designId + strategyId` 受众关系,不能使用模型返回的 `账号类型/accountType` 判断归属。 - 保存 `task-action-message.modelType` 前会读取前端 `modelType` 字典并把“图片,视频/文本,图片”等模态文字转成 value 逗号串,避免策略表格回显为空。 - 同步前端步骤状态 `step-analysis-strategy` 与 `step-analysis-strategy-gantt`。 - 保存成功后写入 `audienceStrategies`。 diff --git a/skills-data/xdfx-step5-audience-strategy/scripts/xdfx_flow.py b/skills-data/xdfx-step5-audience-strategy/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step5-audience-strategy/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step5-audience-strategy/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") diff --git a/skills-data/xdfx-step6-pathway-strategy/SKILL.md b/skills-data/xdfx-step6-pathway-strategy/SKILL.md index 476d5436..d949d598 100644 --- a/skills-data/xdfx-step6-pathway-strategy/SKILL.md +++ b/skills-data/xdfx-step6-pathway-strategy/SKILL.md @@ -24,7 +24,7 @@ python scripts/xdfx_flow.py --mode step6 --input xdfx-input.json --state xdfx-fl - 流式模型接口首次空响应、模型无响应或超时类返回会自动再调用一次。 - 自动解析 `发布人设N / 账号类型N / 信息标题N` 等字段。 - 调用策略保存接口。 -- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`。 +- 同步写入 `task-action-channel`、`base-narrate`、`task-action-message`、`task-action-design-strategy-relation`;本步骤强制按“关键传播节点账号”保存,只写 `pathwayId + strategyId` 传播节点关系,不能使用模型返回的 `账号类型/accountType` 判断归属。 - 保存 `task-action-message.modelType` 前会读取前端 `modelType` 字典并把“图片,视频/文本,图片”等模态文字转成 value 逗号串,避免策略表格回显为空。 - 同步前端步骤状态 `step-analysis-strategy` 与 `step-analysis-strategy-gantt`。 - 保存成功后写入 `pathwayStrategies`。 diff --git a/skills-data/xdfx-step6-pathway-strategy/scripts/xdfx_flow.py b/skills-data/xdfx-step6-pathway-strategy/scripts/xdfx_flow.py index d385f95b..88d4e692 100644 --- a/skills-data/xdfx-step6-pathway-strategy/scripts/xdfx_flow.py +++ b/skills-data/xdfx-step6-pathway-strategy/scripts/xdfx_flow.py @@ -2093,6 +2093,15 @@ def strategy_language(value: Any) -> int: return as_int(value) or 3 +STRATEGY_ACCOUNT_TYPE_AUDIENCE = "目标受众账号" +STRATEGY_ACCOUNT_TYPE_PATHWAY = "关键传播节点账号" + + +def strategy_account_type(*, audience: bool) -> str: + """Return the fixed account type for the current strategy step.""" + return STRATEGY_ACCOUNT_TYPE_AUDIENCE if audience else STRATEGY_ACCOUNT_TYPE_PATHWAY + + def save_strategy_details( ctx: FlowContext, state: dict[str, Any], @@ -2112,15 +2121,7 @@ def save_strategy_details( platform = strategy_platform( first_present(strategy.get("platform"), state.get("platform"), platform_label(item.get("account") or {})) ) - account_type = str( - first_present( - strategy.get("accountType"), - item.get("accountType"), - item.get("type"), - "目标受众账号" if audience else "关键传播节点账号", - ) - or "" - ).strip() + account_type = strategy_account_type(audience=audience) persona = strategy.get("persona") or "专业议题分析者" persona_size = chat_number(strategy.get("personaSize"), 3) responding_count = chat_number(first_present(strategy.get("respondingCount"), strategy.get("数量")), 10) @@ -2164,8 +2165,18 @@ def save_strategy_details( stage + "/save-message", ) - design_ids = [item.get("designId")] if audience else item.get("designIds") or [] - pathway_ids = [] if audience else [item.get("pathwayAccountId")] + # Strategy ownership must never be inferred from model text or mixed + # relations: Step5 writes only audience designId, Step6 writes only pathwayId. + if audience: + design_ids = [item.get("designId")] + pathway_ids: list[Any] = [] + if not any(design_ids): + record_error(state, stage, "受众策略缺少 designId,无法写入受众策略关系", request=item) + else: + design_ids = [] + pathway_ids = [first_present(item.get("pathwayAccountId"), item.get("pathwayId"))] + if not any(pathway_ids): + record_error(state, stage, "关键传播节点策略缺少 pathwayId,无法写入传播节点策略关系", request=item) for design_id in [x for x in design_ids if x]: save_with_id(ctx, state, "save_strategy_relation", {"designId": design_id, "strategyId": strategy_id}, stage + "/save-strategy-relation") for pathway_id in [x for x in pathway_ids if x]: @@ -2901,7 +2912,16 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool strategies = extract_strategies(data, ctx.max_strategies) for strategy in strategies: target = (target_names[0] if target_names else "") or (item.get("accountName") if not audience else "") - account_type = strategy.get("accountType") or ("目标受众账号" if audience else "关键传播节点账号") + account_type = strategy_account_type(audience=audience) + returned_account_type = str(first_present(strategy.get("accountType"), item.get("accountType"), item.get("type"), "") or "").strip() + if returned_account_type and returned_account_type != account_type: + record_event( + state, + stage, + f"模型返回账号类型“{returned_account_type}”与当前步骤不一致,已强制按“{account_type}”入库", + {"strategy": strategy, "item": item}, + ) + strategy_for_save = {**strategy, "accountType": account_type} topic = f"利用\"{strategy.get('persona') or '专业议题分析者'}\"人设,传播\"{strategy.get('title')}\"主题" if not strategy.get("title"): record_error(state, stage, "策略缺少信息标题,跳过", request=strategy, response=data) @@ -2922,9 +2942,9 @@ def run_strategy_step(ctx: FlowContext, state: dict[str, Any], *, audience: bool if strategy_id is not None: # Frontend strategy pages read these child tables, so a strategy # is not complete until this helper succeeds as far as possible. - save_strategy_details(ctx, state, strategy_id, item, strategy, topic, stage, audience=audience) + save_strategy_details(ctx, state, strategy_id, item, strategy_for_save, topic, stage, audience=audience) existing_topics.add(topic) - state[output_key].append({**strategy, "id": strategy_id, "targetAudience": target, "accountType": account_type, "topic": topic, "saveResponse": resp}) + state[output_key].append({**strategy_for_save, "id": strategy_id, "targetAudience": target, "topic": topic, "saveResponse": resp}) if state[output_key]: mark_step_status(ctx, state, ["step-analysis-strategy", "step-analysis-strategy-gantt"], stage + "/step-status") record_event(state, stage, "完成") From f3511e5cd08df4042de31f5285813b9e8777710f Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Fri, 3 Jul 2026 16:35:52 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/.env.network | 4 +- .../workspace/agents/agent-chat-sidebar.tsx | 49 +- .../workspace/agents/agent-module-sidebar.tsx | 49 +- .../workspace/appearance-controls.tsx | 42 +- .../workspace/general-qa-sidebar.tsx | 49 +- .../settings/appearance-settings-page.tsx | 624 ++++++++++++++++-- frontend-web/src/core/system-settings/api.ts | 20 + .../src/open-canvas/pages/AIWritingPage.tsx | 22 +- frontend-web/src/pages/ChatPage.tsx | 2 + frontend-web/src/pages/ChatsPage.tsx | 2 + frontend-web/src/pages/ScheduledTasksPage.tsx | 2 + frontend-web/src/pages/WorkspaceRoutes.tsx | 19 +- .../src/strategy-components/Header.tsx | 7 +- .../app/gateway/routers/system_settings.py | 67 ++ .../deerflow/config/system_settings.py | 59 ++ 15 files changed, 933 insertions(+), 84 deletions(-) diff --git a/frontend-web/.env.network b/frontend-web/.env.network index ae615eb8..d81d70e5 100644 --- a/frontend-web/.env.network +++ b/frontend-web/.env.network @@ -14,8 +14,8 @@ VITE_MOCK_SQ_REPORT=false VITE_MOCK_COP_TASK_DETAIL=false # Gateway / DeerFlow API -VITE_BACKEND_BASE_URL=http://47.88.25.99:7001 -VITE_LANGGRAPH_BASE_URL=http://47.88.25.99:7001/api +VITE_BACKEND_BASE_URL=http://10.19.200.5:2026 +VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx index 65b504f0..d0bf5763 100644 --- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx @@ -260,37 +260,68 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) key={agent.id} href={href} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx index b9e48c2a..4cf1da8e 100644 --- a/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx @@ -219,33 +219,64 @@ export function AgentModuleSidebar({ } }} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/appearance-controls.tsx b/frontend-web/src/components/workspace/appearance-controls.tsx index 40c50efb..c11534b0 100644 --- a/frontend-web/src/components/workspace/appearance-controls.tsx +++ b/frontend-web/src/components/workspace/appearance-controls.tsx @@ -1,6 +1,6 @@ "use client"; -import { Check, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; +import { Check, ExternalLink, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -14,8 +14,9 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useTheme, type ThemeType } from "@/contexts/ThemeContext"; -import { useLayoutMode } from "@/core/system-settings/hooks"; +import { useAppearanceSettings, useLayoutMode } from "@/core/system-settings/hooks"; import { cn } from "@/lib/utils"; +import { buildLightAppUrl } from "@/strategy-components/lib/light-app"; /** * 换肤 / 界面模式 顶栏控件。深色顶栏(Header)与简洁模式页面级顶栏(ChatPage 等)共用, @@ -39,6 +40,43 @@ const triggerToneClass = (tone: AppearanceControlTone) => ? "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]" : "text-muted-foreground hover:bg-muted hover:text-foreground"; +export function CompactShortcutButtons({ tone }: { tone: AppearanceControlTone }) { + const { data: appearance } = useAppearanceSettings(); + const buttons = (appearance?.compact_shortcut_buttons ?? []) + .filter((button) => button.enabled && button.label.trim() && button.url.trim()) + .sort((a, b) => a.sort_order - b.sort_order); + + if (buttons.length === 0) return null; + + return ( + <> + {buttons.map((button) => ( + + ))} + + ); +} + export function ThemeSwitcher({ tone }: { tone: AppearanceControlTone }) { const { theme, setTheme } = useTheme(); const [open, setOpen] = useState(false); diff --git a/frontend-web/src/components/workspace/general-qa-sidebar.tsx b/frontend-web/src/components/workspace/general-qa-sidebar.tsx index b06e4123..4937b8b1 100644 --- a/frontend-web/src/components/workspace/general-qa-sidebar.tsx +++ b/frontend-web/src/components/workspace/general-qa-sidebar.tsx @@ -349,33 +349,64 @@ export function GeneralQaSidebar() { type="button" onClick={() => handleSelectAgent(agent.id)} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 text-left transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 text-left transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx index d94f8db1..162d710b 100644 --- a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx @@ -1,16 +1,30 @@ "use client"; -import { DropletIcon, LayoutGridIcon, LayoutIcon, ListIcon, MoonIcon, PanelLeftIcon, SunIcon } from "lucide-react"; -import { useMemo, useState, type ComponentType, type SVGProps } from "react"; +import { + ArrowDown, + ArrowUp, + DropletIcon, + ExternalLink, + LayoutGridIcon, + LayoutIcon, + ListIcon, + MoonIcon, + PanelLeftIcon, + Plus, + SunIcon, + Trash2, +} from "lucide-react"; +import { useEffect, useMemo, useState, type ComponentType, type SVGProps } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { useTheme } from "@/contexts/ThemeContext"; -import { FONT_SCALE_OPTIONS } from "@/components/font-scale"; -import { Button } from "@/components/ui/button"; -import { - Select, +import { FONT_SCALE_OPTIONS } from "@/components/font-scale"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, SelectContent, SelectItem, SelectTrigger, @@ -25,14 +39,24 @@ import { isIsolatedSession, setIsolatedSession } from "@/core/embed/embed-sessio import { enUS, isLocale, zhCN, type Locale } from "@/core/i18n"; import { useI18n } from "@/core/i18n/hooks"; import { useLocalSettings } from "@/core/settings"; -import { - useAppearanceSettings, +import { + useAppearanceSettings, useCitationDisplaySettings, useLayoutMode, useUpdateAppearanceSettings, useUpdateCitationDisplaySettings, -} from "@/core/system-settings/hooks"; -import { cn } from "@/lib/utils"; +} from "@/core/system-settings/hooks"; +import type { + CompactShortcutButton, + CompactShortcutRouteParam, +} from "@/core/system-settings/api"; +import { cn } from "@/lib/utils"; +import { + APP_THEME_COLORS, + LOGIN_SOURCE_OPTIONS, + parseThemeMap, + stringifyThemeMap, +} from "@/strategy-components/lib/light-app"; import PrivateSquareSettingsPage from "@/pages/PrivateSquareSettingsPage"; import PublishSquaresSettingsPage from "@/pages/PublishSquaresSettingsPage"; @@ -41,10 +65,101 @@ import SkillCompressionSettingsPage from "@/pages/SkillCompressionSettingsPage"; import { SettingsSection } from "./settings-section"; -const languageOptions: { value: Locale; label: string }[] = [ - { value: "en-US", label: enUS.locale.localName }, - { value: "zh-CN", label: zhCN.locale.localName }, -]; +const languageOptions: { value: Locale; label: string }[] = [ + { value: "en-US", label: enUS.locale.localName }, + { value: "zh-CN", label: zhCN.locale.localName }, +]; + +const DEFAULT_SHORTCUT_THEME_MAP = "red=red;blue=blue;dark-blue=dark-blue"; + +function makeShortcutId() { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `shortcut-${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +function newShortcutParam(): CompactShortcutRouteParam { + return { + key: "access_token", + paramType: "login", + source: "access_token", + customValue: "", + themeValue: DEFAULT_SHORTCUT_THEME_MAP, + }; +} + +function newShortcutButton(): CompactShortcutButton { + return { + id: makeShortcutId(), + label: "快捷入口", + url: "", + route_params: [newShortcutParam()], + enabled: true, + sort_order: 0, + }; +} + +function normalizeShortcutDraft( + buttons: CompactShortcutButton[] | undefined, +): CompactShortcutButton[] { + const source = buttons?.length ? buttons : [newShortcutButton()]; + return [...source] + .sort((a, b) => a.sort_order - b.sort_order) + .map((button, index) => ({ + ...button, + id: button.id || makeShortcutId(), + label: button.label || "快捷入口", + url: button.url || "", + route_params: (button.route_params?.length + ? button.route_params + : [newShortcutParam()] + ).map((param) => ({ + key: param.key || "", + paramType: param.paramType || "login", + source: param.source || "access_token", + customValue: param.customValue ?? "", + themeValue: param.themeValue || DEFAULT_SHORTCUT_THEME_MAP, + })), + enabled: button.enabled ?? true, + sort_order: index, + })); +} + +function cleanShortcutParams( + params: CompactShortcutRouteParam[], +): CompactShortcutRouteParam[] { + return params + .filter((param) => param.key.trim()) + .map((param) => { + const base: CompactShortcutRouteParam = { + key: param.key.trim(), + paramType: param.paramType, + }; + if (param.paramType === "theme") { + base.themeValue = stringifyThemeMap(parseThemeMap(param.themeValue)); + return base; + } + base.source = param.source || "access_token"; + if (base.source === "other") { + base.customValue = (param.customValue ?? "").trim(); + } + return base; + }); +} + +function cleanShortcutButtons( + buttons: CompactShortcutButton[], +): CompactShortcutButton[] { + return buttons.map((button, index) => ({ + id: button.id || makeShortcutId(), + label: button.label.trim() || "快捷入口", + url: button.url.trim(), + route_params: cleanShortcutParams(button.route_params), + enabled: button.enabled, + sort_order: index, + })); +} export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const { t, locale, changeLocale } = useI18n(); @@ -52,10 +167,11 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const navigate = useNavigate(); const currentTheme = theme; const [settings, setSettings] = useLocalSettings(); - const isAdmin = getStoredAuth()?.system_role === "admin"; - const { data: appearance } = useAppearanceSettings(); - const updateAppearance = useUpdateAppearanceSettings(); - const { data: citationDisplay } = useCitationDisplaySettings(); + const isAdmin = getStoredAuth()?.system_role === "admin"; + const { data: appearance } = useAppearanceSettings(); + const updateAppearance = useUpdateAppearanceSettings(); + const [shortcutDraft, setShortcutDraft] = useState([]); + const { data: citationDisplay } = useCitationDisplaySettings(); const updateCitationDisplay = useUpdateCitationDisplaySettings(); const globalDefault = appearance?.default_theme ?? "light"; const globalSkillView = appearance?.default_skill_view ?? "list"; @@ -63,8 +179,14 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const globalLayoutMode = appearance?.default_layout_mode ?? "full"; // 有效界面模式:用户自己选过则以其为准,否则跟随管理员系统默认(再否则会话级回退)。 const [effectiveLayoutMode, setLayoutMode] = useLayoutMode(); - const cleanupWords = - citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; + const cleanupWords = + citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; + + useEffect(() => { + setShortcutDraft( + normalizeShortcutDraft(appearance?.compact_shortcut_buttons), + ); + }, [appearance?.compact_shortcut_buttons]); // 独立登录会话(本标签页):开关切换鉴权存储 localStorage ↔ sessionStorage(见 scoped-storage)。 const [isolatedSession, setIsolatedSessionState] = useState(() => isIsolatedSession()); @@ -115,7 +237,7 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } }; - const handleSetDefaultView = async ( + const handleSetDefaultView = async ( kind: "skill" | "agent", value: "card" | "list", ) => { @@ -134,7 +256,103 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败"); } - }; + }; + + const patchShortcut = ( + id: string, + patch: Partial, + ) => { + setShortcutDraft((items) => + items.map((item) => (item.id === id ? { ...item, ...patch } : item)), + ); + }; + + const patchShortcutParam = ( + id: string, + index: number, + patch: Partial, + ) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: item.route_params.map((param, paramIndex) => + paramIndex === index ? { ...param, ...patch } : param, + ), + } + : item, + ), + ); + }; + + const moveShortcut = (id: string, direction: -1 | 1) => { + setShortcutDraft((items) => { + const index = items.findIndex((item) => item.id === id); + const target = index + direction; + if (index < 0 || target < 0 || target >= items.length) return items; + const next = [...items]; + [next[index], next[target]] = [next[target]!, next[index]!]; + return next.map((item, order) => ({ ...item, sort_order: order })); + }); + }; + + const addShortcutParam = (id: string) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: [...item.route_params, newShortcutParam()], + } + : item, + ), + ); + }; + + const removeShortcutParam = (id: string, index: number) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: item.route_params.filter((_, paramIndex) => paramIndex !== index), + } + : item, + ), + ); + }; + + const handleSaveShortcuts = async () => { + for (const item of shortcutDraft) { + if (item.enabled && !item.url.trim()) { + toast.error(`请填写「${item.label.trim() || "快捷入口"}」的跳转链接,或先关闭该按钮`); + return; + } + for (const param of item.route_params) { + if (!param.key.trim()) { + toast.error("路由参数键名不可为空"); + return; + } + if ( + param.paramType === "login" && + param.source === "other" && + !param.customValue?.trim() + ) { + toast.error("登录参数自定义值不可为空"); + return; + } + } + } + try { + await updateAppearance.mutateAsync({ + compact_shortcut_buttons: cleanShortcutButtons(shortcutDraft), + }); + toast.success("顶部快捷按钮已保存"); + } catch (e) { + toast.error(e instanceof Error ? e.message : "保存失败"); + } + }; const themeOptions = useMemo( () => [ @@ -224,11 +442,11 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
- {isAdmin && ( - <> - - + +
@@ -257,13 +475,47 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { {globalLayoutMode === "full" ? "全量模式" : "简洁模式"}

-
- - )} - - {isAdmin && ( - <> - +
+ + )} + + {isAdmin && ( + <> + + + + setShortcutDraft((items) => [ + ...items, + { ...newShortcutButton(), sort_order: items.length }, + ]) + } + onPatch={patchShortcut} + onMove={moveShortcut} + onRemove={(id) => + setShortcutDraft((items) => + items + .filter((item) => item.id !== id) + .map((item, index) => ({ ...item, sort_order: index })), + ) + } + onAddParam={addShortcutParam} + onPatchParam={patchShortcutParam} + onRemoveParam={removeShortcutParam} + onSave={handleSaveShortcuts} + /> + + + )} + + {isAdmin && ( + <> + void }) { )}
- ); -} - -function ThemePreviewCard({ - icon: Icon, - label, + ); +} + +function CompactShortcutSettings({ + buttons, + saving, + onAdd, + onPatch, + onMove, + onRemove, + onAddParam, + onPatchParam, + onRemoveParam, + onSave, +}: { + buttons: CompactShortcutButton[]; + saving: boolean; + onAdd: () => void; + onPatch: (id: string, patch: Partial) => void; + onMove: (id: string, direction: -1 | 1) => void; + onRemove: (id: string) => void; + onAddParam: (id: string) => void; + onPatchParam: ( + id: string, + index: number, + patch: Partial, + ) => void; + onRemoveParam: (id: string, index: number) => void; + onSave: () => void; +}) { + return ( +
+
+

+ 已配置 {buttons.length} 个按钮;只有启用且填写了跳转链接的按钮会显示。 +

+
+ + +
+
+ + {buttons.length === 0 ? ( +
+ 暂无按钮,点击“添加按钮”创建一个。 +
+ ) : ( +
+ {buttons.map((button, index) => ( +
+
+
+ + onPatch(button.id, { enabled: checked }) + } + aria-label="启用快捷按钮" + /> + + {button.enabled ? "启用" : "停用"} + +
+
+
+
按钮名称
+ + onPatch(button.id, { label: event.target.value }) + } + placeholder="如:业务系统" + /> +
+
+
跳转链接
+ + onPatch(button.id, { url: event.target.value }) + } + placeholder="https://example.com/page" + className="font-mono" + /> +
+
+
+ + + +
+
+ +
+
+
+ + 路由参数 +
+ +
+ + {button.route_params.length === 0 ? ( +
+ 不追加额外参数。 +
+ ) : ( +
+ {button.route_params.map((param, paramIndex) => { + const themeMap = + param.paramType === "theme" + ? parseThemeMap(param.themeValue) + : {}; + return ( +
+
+ + onPatchParam(button.id, paramIndex, { + key: event.target.value, + }) + } + placeholder="参数名 key" + className="font-mono text-xs" + /> + + {param.paramType === "login" ? ( +
+ + {param.source === "other" && ( + + onPatchParam(button.id, paramIndex, { + customValue: event.target.value, + }) + } + placeholder="自定义值" + className="w-32 text-xs" + /> + )} +
+ ) : ( +
+ 按当前主题取值 +
+ )} + +
+ + {param.paramType === "theme" && ( +
+ {APP_THEME_COLORS.map((color) => ( + + ))} +
+ )} +
+ ); + })} +
+ )} +
+
+ ))} +
+ )} +
+ ); +} + +function ThemePreviewCard({ + icon: Icon, + label, description, active, mode, diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index dacfa808..d2c0564e 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -279,12 +279,31 @@ export async function updatePublishSquares( export type ThemeMode = "light" | "dark" | "lightblue"; export type ViewMode = "card" | "list"; export type LayoutMode = "full" | "compact"; +export type CompactShortcutParamType = "login" | "theme"; + +export interface CompactShortcutRouteParam { + key: string; + paramType: CompactShortcutParamType; + source?: string | null; + customValue?: string | null; + themeValue?: string | null; +} + +export interface CompactShortcutButton { + id: string; + label: string; + url: string; + route_params: CompactShortcutRouteParam[]; + enabled: boolean; + sort_order: number; +} export interface AppearanceSettings { default_theme: ThemeMode; default_skill_view: ViewMode; default_agent_view: ViewMode; default_layout_mode: LayoutMode; + compact_shortcut_buttons: CompactShortcutButton[]; } export interface AppearanceUpdate { @@ -292,6 +311,7 @@ export interface AppearanceUpdate { default_skill_view?: ViewMode; default_agent_view?: ViewMode; default_layout_mode?: LayoutMode; + compact_shortcut_buttons?: CompactShortcutButton[]; } export async function loadAppearanceSettings(): Promise { diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 2cd88cd4..eefd2bf0 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -450,7 +450,7 @@ function AIWritingBody({ {showSidebar && (
@@ -465,11 +465,11 @@ function AIWritingBody({ onClick={() => setSidebarCollapsed(false)} title="展开侧栏" aria-label="展开侧栏" - className="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700" + className="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]" > - +