From bebe76fa11a0f3d3dd865d32869762755d324a08 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 15 Sep 2026 17:47:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(split):=20=E5=88=86=E6=9E=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=9C=BA=E6=A2=B0=E6=8B=86=E8=A7=A3=E6=9C=BA=E5=88=B6?= =?UTF-8?q?(2026-09-15=E7=94=A8=E6=88=B7=E8=A6=81=E6=B1=82:=E5=A4=A7?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=BF=85=E9=A1=BB=E6=8B=86=E5=B0=8F)?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=AD=A3=E6=96=87=E8=B6=85bid=5Fsplit=5Fchar?= =?UTF-8?q?s(=E9=BB=98=E8=AE=A46=E4=B8=87=E5=AD=97,params=E5=8F=AF?= =?UTF-8?q?=E9=85=8D)=E6=97=B6dispatch=E9=A6=96=E8=B7=91/A=E5=9D=97?= =?UTF-8?q?=E9=87=8D=E5=81=9A/awaiting=5Fredo=E9=87=8D=E5=81=9A=E4=B8=89?= =?UTF-8?q?=E6=B4=BE=E5=8F=91=E7=82=B9=E8=87=AA=E5=8A=A8=E6=8B=86N?= =?UTF-8?q?=E4=B8=AA=E5=88=86=E6=AE=B5=E5=AD=90=E4=BB=BB=E5=8A=A1=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C(char=5Frange=E4=BA=92=E4=B8=8D=E9=87=8D=E5=8F=A0+roun?= =?UTF-8?q?d=E8=BD=AE=E6=A0=87=E8=AE=B0+is=5Fsubtask);=E9=85=8D=E5=A5=97:Q?= =?UTF-8?q?C=E6=B4=BE=E5=8F=91=E5=8A=A0'=E7=BB=B4=E5=BA=A6=E5=9C=A8?= =?UTF-8?q?=E5=8A=9E=E4=B8=8D=E5=AE=A1'=E9=97=A8=E7=A6=81=E9=98=B2?= =?UTF-8?q?=E5=AE=A1=E5=8D=8A=E6=88=90=E5=93=81;=E8=BD=AE=E6=AC=A1?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=94=B9round=E5=8E=BB=E9=87=8D(=5Fdim=5Frou?= =?UTF-8?q?nd=5Fcount,=E9=98=B2=E6=8B=86=E8=A7=A3=E8=BD=AE=E8=AF=AF?= =?UTF-8?q?=E7=AE=97N=E8=BD=AE);=E5=88=86=E6=9E=90=E5=91=98=E6=8A=80?= =?UTF-8?q?=E8=83=BDchar=5Frange=E7=A1=AC=E8=A7=84=E5=88=99=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E6=9C=AC=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_bidding/bid_common.py | 4 + pipeline_bidding/bid_flow.py | 123 +++++++++++++++++- .../bid_orchestration_capability.py | 22 +++- wwwroot/agent/index.ui | 4 +- wwwroot/api/bid_task_io.dspy | 39 +++--- wwwroot/bid_task_tree_popup.ui | 2 +- wwwroot/index.ui | 20 +-- 7 files changed, 177 insertions(+), 37 deletions(-) diff --git a/pipeline_bidding/bid_common.py b/pipeline_bidding/bid_common.py index 2ec4bee..df5aab9 100644 --- a/pipeline_bidding/bid_common.py +++ b/pipeline_bidding/bid_common.py @@ -125,6 +125,9 @@ DEFAULT_PARAMS = { "bid_chapter_min_words": "800", # 技术标章节最低字数(低于即评审必退) "bid_chapter_min_words_biz": "500", # 商务标章节最低字数 "bid_write_concurrency": "4", # 章节编写并发上限(单轮最多同时派发的编写任务数) + "bid_split_chars": "60000", # 分析任务机械拆解阈值:招标文件正文超此字符数, + # 分析维度自动拆为分段子任务(2026-09-15 用户要求: + # 大任务必须拆解,不能一个任务跑几十分钟) } @@ -271,6 +274,7 @@ async def get_thresholds(sor): "chapter_min_words": to_int(await get_param(sor, "bid_chapter_min_words"), 800), "chapter_min_words_biz": to_int(await get_param(sor, "bid_chapter_min_words_biz"), 500), "write_concurrency": to_int(await get_param(sor, "bid_write_concurrency"), 4), + "split_chars": to_int(await get_param(sor, "bid_split_chars"), 60000), } diff --git a/pipeline_bidding/bid_flow.py b/pipeline_bidding/bid_flow.py index 123f3cb..79d3210 100644 --- a/pipeline_bidding/bid_flow.py +++ b/pipeline_bidding/bid_flow.py @@ -169,6 +169,86 @@ async def _open_dim_tasks(sor, project_id, dim): return to_int(getattr(recs[0], "c", 0) if recs else 0) +async def _tender_text_len(sor, project_id): + """项目最新招标文件正文字符数(无文件/无正文返回 0)。拆解阈值判定用。""" + recs = await sor.sqlExe( + "SELECT LENGTH(content_text) AS n FROM bid_tender_files " + "WHERE project_id=${p}$ ORDER BY created_at DESC LIMIT 1", {"p": project_id}) + await sor.sqlExe("COMMIT", {}) + if not recs: + return 0 + return to_int(getattr(recs[0], "n", 0) or 0, 0) + + +async def _dim_round_count(sor, project_id, dim, since='', redo=None): + """某维度已消耗的「轮次」数(轮 = 去重后的 params.round 个数,非任务条数)。 + + 2026-09-15 拆解机制配套:一个拆解轮会派 N 个分段子任务(同 round), + 按任务条数计轮会把一轮误算成 N 轮 → 轮次上限误判。首跑与重做统一口径: + 任务 params 带 round(拆解轮)则按 round 去重,不带 round 的老任务每条算一轮。 + redo=None 全部 / True 只重做(qc_redo) / False 只首跑(无 qc_redo)。 + """ + sql = ("SELECT params FROM pipeline_tasks WHERE tenant_id=${p}$ " + "AND pipeline_id='role_task' AND role=${r}$ " + "AND state IN ('approved','completed','qc_rejected') " + "AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.analysis_dim'))=${d}$") + p = {"p": project_id, "r": R_ANALYST, "d": dim} + if redo is True: + sql += " AND params LIKE ${kw}$" + p["kw"] = "%qc_redo%" + elif redo is False: + sql += " AND params NOT LIKE ${kw}$" + p["kw"] = "%qc_redo%" + if since: + sql += " AND updated_at >= ${since}$" + p["since"] = since + recs = await sor.sqlExe(sql, p) + await sor.sqlExe("COMMIT", {}) + rounds = set() + n_plain = 0 + for r in recs or []: + try: + pp = json.loads(getattr(r, "params", "") or "{}") + except (json.JSONDecodeError, TypeError): + pp = {} + rd = pp.get("round") if isinstance(pp, dict) else None + if rd: + rounds.add(str(rd)) + else: + n_plain += 1 + return len(rounds) + n_plain + + +async def _create_split_tasks(sor, project_id, dim, label, pname, base_params, + text_len, split_chars, round_tag, qc_imp=None): + """大文件分析维度机械拆解:按 split_chars 把正文切成 N 段,每段一个子任务。 + + 子任务 params 带 char_range=[start,end)/seg=i/seg_n=N/round=round_tag/is_subtask=1, + 分析员技能硬规则:有 char_range 只读本区间(抽取工具 offset/length 原生支持)。 + 返回创建的子任务 id 列表。N 上限 6(防极端文件拆爆并发)。 + """ + n = min(6, max(2, -(-text_len // split_chars))) # 向上取整,[2,6] + step = -(-text_len // n) + ids = [] + for i in range(n): + start = i * step + end = min(text_len, start + step) + if start >= text_len: + break + params = dict(base_params) + params.update({"char_range": [start, end], "seg": i + 1, "seg_n": n, + "round": round_tag, "is_subtask": 1}) + if qc_imp: + params["qc_improvements"] = qc_imp + tid = await create_role_task( + sor, project_id, R_ANALYST, + "%s 分析%s:%s 第%d/%d段(字符 %d-%d)" + % (pname, "重做" if qc_imp else "", label, i + 1, n, start, end), + params) + ids.append(tid) + return ids + + async def _open_orch_tasks(sor, project_id): """PM 分析编排任务在办数(2026-09-04 方案乙)。""" recs = await sor.sqlExe( @@ -685,8 +765,8 @@ async def reconcile_project(sor, project_id, project_name=""): acts.append("waiting: 分析维度「%s」任务在办" % label) continue _ft2 = _ft or await _latest_tender_file_ts(sor, project_id) - attempts = await _dim_done_count(sor, project_id, dim, since=_ft2) - n_redo_done = await _dim_redo_done_count(sor, project_id, dim, since=_ft2) + attempts = await _dim_round_count(sor, project_id, dim, since=_ft2, redo=False) + n_redo_done = await _dim_round_count(sor, project_id, dim, since=_ft2, redo=True) # QC 退回重做(携带改进意见)优先于首跑判定,不计入首跑重试上限 qc_imp = await _dim_qc_improvements(sor, project_id, dim) if qc_imp: @@ -704,6 +784,19 @@ async def reconcile_project(sor, project_id, project_name=""): await sor.sqlExe("COMMIT", {}) acts.append("escalated: 维度「%s」QC 重做轮次用尽 → 人工介入" % label) continue + # 大文件机械拆解(2026-09-15 用户要求):正文超阈值 → 重做也拆分段子任务, + # 避免单任务长读全文几十分钟;round 标记保证轮次按轮计不按任务条数计。 + _tlen = await _tender_text_len(sor, project_id) + if _tlen > th["split_chars"]: + _base = {"stage": "analysis_redo", "task_kind": "bid_analysis", + "analysis_dim": dim, "qc_redo": 1} + _ids = await _create_split_tasks( + sor, project_id, dim, label, pname, _base, _tlen, + th["split_chars"], "redo%d" % (n_redo_done + 1), qc_imp=qc_imp) + acts.append("created: 分析重做拆解 %d 个分段子任务(%s,正文 %d 字)" + % (len(_ids), label, _tlen)) + created_dims.append(dim) + continue tid = await create_role_task( sor, project_id, R_ANALYST, "%s 分析重做:%s(QC 退回,按改进意见修正)" % (pname, label), @@ -793,6 +886,18 @@ async def reconcile_project(sor, project_id, project_name=""): # 某一维度卡住只阻塞依赖它的章节,其余章节照常流转。 # 2026-09-11 流程裁剪:qc_analysis 阶段被裁 → 整段跳过(章节依赖由 _qc_passed_eff 视为全过)。 qc_pending = [] if not _stage_on(STAGE_QC) else await _qc_pending_types(sor, project_id) + # 2026-09-15 拆解配套门禁:维度有在办分析任务(含拆解分段子任务)时不派该类型 + # QC——否则 QC 审到半成品产出必退(拆解机制成立的前提,见 _create_split_tasks)。 + if qc_pending: + _kept = [] + for _qp in qc_pending: + _d = QC_TYPE_TO_DIM.get(_qp[0], "") + if _d and await _open_dim_tasks(sor, project_id, _d) > 0: + acts.append("waiting: 维度「%s」分析任务在办,%s 暂缓 QC 审核(防审半成品)" + % (_d, _qp[0])) + continue + _kept.append(_qp) + qc_pending = _kept over_types = set() if qc_pending: # 轮次用尽守卫:最新审核已达上限仍未通过且产出未更新 → 不再空转派审核任务, @@ -822,7 +927,7 @@ async def reconcile_project(sor, project_id, project_name=""): needs_redo = [(t, s, imp, rnd) for t, s, imp, rnd, _on in qc_pending if s == "awaiting_redo"] if needs_redo: redo_dims = [] - for t, _, _, _ in needs_redo: + for t, _, _, rnd in needs_redo: dim = QC_TYPE_TO_DIM.get(t, "scoring") if dim in redo_dims: continue @@ -844,6 +949,18 @@ async def reconcile_project(sor, project_id, project_name=""): await sor.sqlExe("COMMIT", {}) continue qc_imp = await _dim_qc_improvements(sor, project_id, dim) + # 大文件机械拆解(同 A 块口径):超阈值拆分段子任务重做 + _tlen2 = await _tender_text_len(sor, project_id) + if _tlen2 > th["split_chars"]: + _base2 = {"stage": "analysis_redo", "task_kind": "bid_analysis", + "analysis_dim": dim, "qc_redo": 1} + _ids2 = await _create_split_tasks( + sor, project_id, dim, dim, pname, _base2, _tlen2, + th["split_chars"], "redo%d" % (int(rnd) + 1), qc_imp=qc_imp) + redo_dims.append(dim) + acts.append("created: 分析重做拆解 %d 段(%s,产出被 QC 清空)" + % (len(_ids2), dim)) + continue await create_role_task( sor, project_id, R_ANALYST, "%s 分析重做:%s(QC 退回产出已清空,按改进意见重做)" % (pname, dim), diff --git a/pipeline_bidding/bid_orchestration_capability.py b/pipeline_bidding/bid_orchestration_capability.py index ab8f59a..699b0c1 100644 --- a/pipeline_bidding/bid_orchestration_capability.py +++ b/pipeline_bidding/bid_orchestration_capability.py @@ -95,8 +95,9 @@ async def dispatch_analysis_dim(dim, instructions="", project_id="", instructions:PM 对该维度的补充指令(异常招标文档的特殊交代/交叉核对要点)。 """ from .bid_flow import (ANALYSIS_DIMS, DIM_QC_TYPES, DIM_UPSTREAM, - R_ANALYST, _qc_passed_types, _open_dim_tasks) - from .bid_common import DIM_TO_STAGE + R_ANALYST, _qc_passed_types, _open_dim_tasks, + _tender_text_len, _create_split_tasks) + from .bid_common import DIM_TO_STAGE, get_thresholds dims = {d: lb for d, lb, _ts in ANALYSIS_DIMS} if dim not in dims: return False, "未知维度 %s(可选:%s)" % (dim, "、".join(dims)) @@ -149,6 +150,23 @@ async def dispatch_analysis_dim(dim, instructions="", project_id="", "orchestrated_by": "agent.pm"} if instructions: params["pm_instructions"] = str(instructions)[:MAX_PM_INSTRUCTIONS] + # 大文件机械拆解(2026-09-15 用户要求:大任务必须拆小):正文超阈值时 + # 直接派 N 个分段子任务(char_range 互不重叠、并行),不再派单体长任务。 + _th = await get_thresholds(sor) + _tlen = await _tender_text_len(sor, project_id) + if _tlen > _th["split_chars"]: + _ids = await _create_split_tasks( + sor, project_id, dim, label, pname, params, _tlen, + _th["split_chars"], "run1") + await sor.sqlExe("COMMIT", {}) + await record(sor, project_id, "pipeline_tasks", _ids[0] if _ids else "", + "pm_dispatch", who=who, agent_id=agent_id, + detail="PM 编排派发维度 %s 拆解 %d 段(正文 %d 字 > 阈值 %d)" + % (dim, len(_ids), _tlen, _th["split_chars"])) + await sor.sqlExe("COMMIT", {}) + return True, ("已派发维度「%s」拆解分析:正文 %d 字超阈值 %d,拆为 %d 个分段" + "子任务并行(%s)" % (label, _tlen, _th["split_chars"], + len(_ids), "、".join(_ids))) tid = await create_role_task( sor, project_id, R_ANALYST, "%s 招标文件分析:%s(PM编排,先读上游再抽取)" % (pname, label), diff --git a/wwwroot/agent/index.ui b/wwwroot/agent/index.ui index 7c1c3af..e561841 100644 --- a/wwwroot/agent/index.ui +++ b/wwwroot/agent/index.ui @@ -6,8 +6,8 @@ "widgettype": "HBox", "options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"}, "subwidgets": [ - {"widgettype": "Title2", "options": {"text": "投标产线"}}, - {"widgettype": "Text", "options": {"text": "收到招标文件(自动立项)→ 解析 → QC契合度审核 → 资质准备 → 分章节编写 → 评审 → 合成 → 评分 → 交付", "cfontsize": 0.9, "color": "#94a3b8"}}, + {"widgettype": "Title2", "options": {"otext": "投标产线", "text": "投标产线", "i18n": true}}, + {"widgettype": "Text", "options": {"otext": "收到招标文件(自动立项)→ 解析 → QC契合度审核 → 资质准备 → 分章节编写 → 评审 → 合成 → 评分 → 交付", "text": "收到招标文件(自动立项)→ 解析 → QC契合度审核 → 资质准备 → 分章节编写 → 评审 → 合成 → 评分 → 交付", "i18n": true, "cfontsize": 0.9, "color": "#94a3b8"}}, {"widgettype": "Filler"}, { "widgettype": "Button", diff --git a/wwwroot/api/bid_task_io.dspy b/wwwroot/api/bid_task_io.dspy index 1bea2e5..a7173af 100644 --- a/wwwroot/api/bid_task_io.dspy +++ b/wwwroot/api/bid_task_io.dspy @@ -24,7 +24,7 @@ _state_colors = { if not raw_id: return json.dumps({ "widgettype": "Text", - "options": {"text": "请选择左侧任务查看输入输出", "cfontsize": 0.9, + "options": {"otext": "请选择左侧任务查看输入输出", "text": "请选择左侧任务查看输入输出", "i18n": True, "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, }, ensure_ascii=False) @@ -46,14 +46,15 @@ async with DBPools().sqlorContext(dbname) as sor: task_id = getattr(rv, 'task_id', '') or '' subwidgets = [ {"widgettype": "Text", - "options": {"text": "第%s轮 %s QC 契合度审核" % ( - getattr(rv, 'round', ''), getattr(rv, 'qc_type', '') or ''), + "options": {"otext": "第${p0}轮 ${p1} QC 契合度审核", "text": "第${p0}轮 ${p1} QC 契合度审核", "i18n": True, "i18n_params": {"p0": str(getattr(rv, 'round', '')), "p1": str(getattr(rv, 'qc_type', '') or '')}, "halign": "left", "cfontsize": 1.1, "style": {"fontWeight": "bold", "color": "#1e293b"}}}, {"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"}, "subwidgets": [ {"widgettype": "Text", "options": { - "text": ("%.1f / %.1f %s" % (sc, ps, '→ 通过' if passed else '→ 未通过')), + "otext": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"), + "text": ("${p0} / ${p1} → 通过" if passed else "${p0} / ${p1} → 未通过"), + "i18n": True, "i18n_params": {"p0": "%.1f" % sc, "p1": "%.1f" % ps}, "cfontsize": 0.85, "color": "#ffffff", "padding": "1px 10px", "style": {"background": '#16a34a' if passed else '#dc2626', "borderRadius": "12px", "fontWeight": "bold"}}}, @@ -66,7 +67,7 @@ async with DBPools().sqlorContext(dbname) as sor: {"widgettype": "Filler"}, ]}, {"widgettype": "Text", - "options": {"text": "\u2500\u2500 审核说明 \u2500\u2500", "cfontsize": 0.85, + "options": {"otext": "\u2500\u2500 审核说明 \u2500\u2500", "text": "\u2500\u2500 审核说明 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}}, {"widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "minHeight": "100px", @@ -79,7 +80,7 @@ async with DBPools().sqlorContext(dbname) as sor: imp = getattr(rv, 'improvement', '') or '' if imp: subwidgets.append({"widgettype": "Text", - "options": {"text": "\u2500\u2500 改进意见 \u2500\u2500", "cfontsize": 0.85, + "options": {"otext": "\u2500\u2500 改进意见 \u2500\u2500", "text": "\u2500\u2500 改进意见 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#b45309", "marginTop": "8px"}}}) subwidgets.append({"widgettype": "VScrollPanel", "options": {"css": "filler", "width": "100%", "minHeight": "100px", @@ -88,7 +89,7 @@ async with DBPools().sqlorContext(dbname) as sor: "subwidgets": [{"widgettype": "MdWidget", "options": {"mdtext": imp, "width": "100%"}}]}) subwidgets.append({"widgettype": "Text", - "options": {"text": "(以下为该轮审核所属任务详情)", "cfontsize": 0.75, + "options": {"otext": "(以下为该轮审核所属任务详情)", "text": "(以下为该轮审核所属任务详情)", "i18n": True, "cfontsize": 0.75, "color": "#94a3b8", "marginTop": "8px"}}) return json.dumps({ "widgettype": "VBox", @@ -118,10 +119,10 @@ async with DBPools().sqlorContext(dbname) as sor: "style": {"background": _state_colors.get(st, '#64748b'), "borderRadius": "12px", "fontWeight": "bold"}}}, {"widgettype": "Text", "options": { - "text": "开始: " + str(getattr(s, 'started_at', '') or '')[:16], + "otext": "开始: ${p0}", "text": "开始: ${p0}", "i18n": True, "i18n_params": {"p0": str(str(getattr(s, 'started_at', '') or '')[:16])}, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Text", "options": { - "text": "结束: " + str(getattr(s, 'completed_at', '') or '')[:16], + "otext": "结束: ${p0}", "text": "结束: ${p0}", "i18n": True, "i18n_params": {"p0": str(str(getattr(s, 'completed_at', '') or '')[:16])}, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Filler"}, ]}, @@ -131,7 +132,7 @@ async with DBPools().sqlorContext(dbname) as sor: "options": {"text": "错误: " + str(getattr(s, 'error_msg', ''))[:500], "cfontsize": 0.8, "color": "#dc2626", "wrap": True, "halign": "left"}}) subwidgets.append({"widgettype": "Text", - "options": {"text": "(以下为所属任务详情)", "cfontsize": 0.75, + "options": {"otext": "(以下为所属任务详情)", "text": "(以下为所属任务详情)", "i18n": True, "cfontsize": 0.75, "color": "#94a3b8", "marginTop": "8px"}}) return json.dumps({ "widgettype": "VBox", @@ -176,7 +177,7 @@ async with DBPools().sqlorContext(dbname) as sor: if not recs: return json.dumps({ "widgettype": "Text", - "options": {"text": "任务不存在", "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, + "options": {"otext": "任务不存在", "text": "任务不存在", "i18n": True, "cfontsize": 0.9, "color": "#94a3b8", "padding": "16px"}, }, ensure_ascii=False) task = recs[0] @@ -230,13 +231,13 @@ async with DBPools().sqlorContext(dbname) as sor: {"widgettype": "Text", "options": {"text": state, "cfontsize": 0.75, "color": "#ffffff", "padding": "1px 10px", "style": {"background": sc, "borderRadius": "12px", "fontWeight": "bold"}}}, - {"widgettype": "Text", "options": {"text": "角色: " + role, + {"widgettype": "Text", "options": {"otext": "角色: ${p0}", "text": "角色: ${p0}", "i18n": True, "i18n_params": {"p0": str(role)}, "cfontsize": 0.8, "color": "#64748b"}}, ] if stage: - info_row.append({"widgettype": "Text", "options": {"text": "阶段: " + stage + ((" / " + dim) if dim else ""), + info_row.append({"widgettype": "Text", "options": {"otext": "阶段: ${p0}${p1}", "text": "阶段: ${p0}${p1}", "i18n": True, "i18n_params": {"p0": str(stage), "p1": str((" / " + dim) if dim else "")}, "cfontsize": 0.75, "color": "#94a3b8"}}) - info_row.append({"widgettype": "Text", "options": {"text": "创建: " + created_at + " | 更新: " + updated_at, + info_row.append({"widgettype": "Text", "options": {"otext": "创建: ${p0} | 更新: ${p1}", "text": "创建: ${p0} | 更新: ${p1}", "i18n": True, "i18n_params": {"p0": str(created_at), "p1": str(updated_at)}, "cfontsize": 0.72, "color": "#94a3b8"}}) info_row.append({"widgettype": "Filler"}) subwidgets.append({"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"}, "subwidgets": info_row}) @@ -251,7 +252,7 @@ async with DBPools().sqlorContext(dbname) as sor: # 输入 subwidgets.append({ "widgettype": "Text", - "options": {"text": "\u2500\u2500 输入 \u2500\u2500", "cfontsize": 0.85, + "options": {"otext": "\u2500\u2500 输入 \u2500\u2500", "text": "\u2500\u2500 输入 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#3b82f6", "marginTop": "8px"}}, }) if description: @@ -268,13 +269,13 @@ async with DBPools().sqlorContext(dbname) as sor: }) if not description and not qc_imp: subwidgets.append({"widgettype": "Text", - "options": {"text": "(无任务描述)", "cfontsize": 0.8, "color": "#94a3b8"}}) + "options": {"otext": "(无任务描述)", "text": "(无任务描述)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}}) # 执行步骤 if steps: subwidgets.append({ "widgettype": "Text", - "options": {"text": "\u2500\u2500 执行步骤 \u2500\u2500", "cfontsize": 0.85, + "options": {"otext": "\u2500\u2500 执行步骤 \u2500\u2500", "text": "\u2500\u2500 执行步骤 \u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}, }) for s in steps: @@ -303,12 +304,12 @@ async with DBPools().sqlorContext(dbname) as sor: # 输出(交付件) subwidgets.append({ "widgettype": "Text", - "options": {"text": "\u2500\u2500 输出(交付件)\u2500\u2500", "cfontsize": 0.85, + "options": {"otext": "\u2500\u2500 输出(交付件)\u2500\u2500", "text": "\u2500\u2500 输出(交付件)\u2500\u2500", "i18n": True, "cfontsize": 0.85, "style": {"fontWeight": "bold", "color": "#16a34a", "marginTop": "8px"}}, }) if not dels: subwidgets.append({"widgettype": "Text", - "options": {"text": "(暂无交付件)", "cfontsize": 0.8, "color": "#94a3b8"}}) + "options": {"otext": "(暂无交付件)", "text": "(暂无交付件)", "i18n": True, "cfontsize": 0.8, "color": "#94a3b8"}}) else: from pipeline_service.task_io_render import deliverable_card for d in dels: diff --git a/wwwroot/bid_task_tree_popup.ui b/wwwroot/bid_task_tree_popup.ui index c416fbd..a57f061 100644 --- a/wwwroot/bid_task_tree_popup.ui +++ b/wwwroot/bid_task_tree_popup.ui @@ -4,7 +4,7 @@ "subwidgets": [ { "widgettype": "Text", - "options": {"text": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。", + "options": {"otext": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。", "text": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。", "i18n": true, "cfontsize": 0.8, "color": "#94a3b8", "padding": "4px 12px"} }, { diff --git a/wwwroot/index.ui b/wwwroot/index.ui index c20f9d5..e1e7835 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -6,8 +6,8 @@ "widgettype": "HBox", "options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"}, "subwidgets": [ - {"widgettype": "Title2", "options": {"text": "投标产线"}}, - {"widgettype": "Text", "options": {"text": "上传招标文件 → 解析 → QC契合度审核 → 编写 → 评审 → 评分 → 交付(招标信息采集/审批在商机产线)", "cfontsize": 0.9, "color": "#94a3b8"}}, + {"widgettype": "Title2", "options": {"otext": "投标产线", "text": "投标产线", "i18n": true}}, + {"widgettype": "Text", "options": {"otext": "上传招标文件 → 解析 → QC契合度审核 → 编写 → 评审 → 评分 → 交付(招标信息采集/审批在商机产线)", "text": "上传招标文件 → 解析 → QC契合度审核 → 编写 → 评审 → 评分 → 交付(招标信息采集/审批在商机产线)", "i18n": true, "cfontsize": 0.9, "color": "#94a3b8"}}, {"widgettype": "Filler"}, { "widgettype": "Button", @@ -44,20 +44,20 @@ "widgettype": "VBox", "options": {"width": "100%", "padding": "16px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "8px", "spacing": "8px"}, "subwidgets": [ - {"widgettype": "Title3", "options": {"text": "使用流程"}}, - {"widgettype": "Text", "options": {"text": "1. 立项:投标驾驶舱(侧栏 产线管理→投标产线)说「创建投标项目」,设定参与人员", "cfontsize": 0.95}}, - {"widgettype": "Text", "options": {"text": "2. 上传招标文件:按待办任务上传招标文件(含答疑/补遗),上传后自动进入解析", "cfontsize": 0.95}}, - {"widgettype": "Text", "options": {"text": "3. 解析与QC:自动抽取评分项/得分规则/资质/投标文件要求/章节骨架,QC 对照原文按10分制审核契合度,高于9.5分放行,否则退回重做", "cfontsize": 0.95}}, - {"widgettype": "Text", "options": {"text": "4. 编写与评审:资质准备(知识库匹配+人工文件清单)→ 一章一任务自动编写与评审循环", "cfontsize": 0.95}}, - {"widgettype": "Text", "options": {"text": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "cfontsize": 0.95}} + {"widgettype": "Title3", "options": {"otext": "使用流程", "text": "使用流程", "i18n": true}}, + {"widgettype": "Text", "options": {"otext": "1. 立项:投标驾驶舱(侧栏 产线管理→投标产线)说「创建投标项目」,设定参与人员", "text": "1. 立项:投标驾驶舱(侧栏 产线管理→投标产线)说「创建投标项目」,设定参与人员", "i18n": true, "cfontsize": 0.95}}, + {"widgettype": "Text", "options": {"otext": "2. 上传招标文件:按待办任务上传招标文件(含答疑/补遗),上传后自动进入解析", "text": "2. 上传招标文件:按待办任务上传招标文件(含答疑/补遗),上传后自动进入解析", "i18n": true, "cfontsize": 0.95}}, + {"widgettype": "Text", "options": {"otext": "3. 解析与QC:自动抽取评分项/得分规则/资质/投标文件要求/章节骨架,QC 对照原文按10分制审核契合度,高于9.5分放行,否则退回重做", "text": "3. 解析与QC:自动抽取评分项/得分规则/资质/投标文件要求/章节骨架,QC 对照原文按10分制审核契合度,高于9.5分放行,否则退回重做", "i18n": true, "cfontsize": 0.95}}, + {"widgettype": "Text", "options": {"otext": "4. 编写与评审:资质准备(知识库匹配+人工文件清单)→ 一章一任务自动编写与评审循环", "text": "4. 编写与评审:资质准备(知识库匹配+人工文件清单)→ 一章一任务自动编写与评审循环", "i18n": true, "cfontsize": 0.95}}, + {"widgettype": "Text", "options": {"otext": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "text": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "i18n": true, "cfontsize": 0.95}} ] }, { "widgettype": "VBox", "options": {"width": "100%", "padding": "16px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "8px", "spacing": "8px"}, "subwidgets": [ - {"widgettype": "Title3", "options": {"text": "投标项目数据"}}, - {"widgettype": "Text", "options": {"text": "项目内数据(评分项/章节/标书/资质/评审记录)请打开对应投标项目后,从驾驶舱上方功能菜单进入,或从侧栏菜单进入后在页面里按项目筛选。", "cfontsize": 0.95}}, + {"widgettype": "Title3", "options": {"otext": "投标项目数据", "text": "投标项目数据", "i18n": true}}, + {"widgettype": "Text", "options": {"otext": "项目内数据(评分项/章节/标书/资质/评审记录)请打开对应投标项目后,从驾驶舱上方功能菜单进入,或从侧栏菜单进入后在页面里按项目筛选。", "text": "项目内数据(评分项/章节/标书/资质/评审记录)请打开对应投标项目后,从驾驶舱上方功能菜单进入,或从侧栏菜单进入后在页面里按项目筛选。", "i18n": true, "cfontsize": 0.95}}, { "widgettype": "HBox", "options": {"gap": "8px"},