feat(split): 分析任务机械拆解机制(2026-09-15用户要求:大任务必须拆小)——正文超bid_split_chars(默认6万字,params可配)时dispatch首跑/A块重做/awaiting_redo重做三派发点自动拆N个分段子任务并行(char_range互不重叠+round轮标记+is_subtask);配套:QC派发加'维度在办不审'门禁防审半成品;轮次统计改round去重(_dim_round_count,防拆解轮误算N轮);分析员技能char_range硬规则只读本段

This commit is contained in:
yumoqing 2026-09-15 17:47:27 +08:00
parent dfb7b11c5d
commit bebe76fa11
7 changed files with 177 additions and 37 deletions

View File

@ -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),
}

View File

@ -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),

View File

@ -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),

View File

@ -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",

View File

@ -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:

View File

@ -4,7 +4,7 @@
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。",
"options": {"otext": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。", "text": "左树:项目 → 阶段 → 任务 → 步骤/交付件;点击任务节点查看右侧输入输出。", "i18n": true,
"cfontsize": 0.8, "color": "#94a3b8", "padding": "4px 12px"}
},
{

View File

@ -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"},