feat(toolbox): F1 拆书 teardown 落库成项目 rules
- teardown_spec.writes=["rules"](只声明 tier,入库仍经白名单 gate,不变量 #3) - toolbox_registry teardown entry 加 ingest=IngestSpec(table="rules") - routers/toolbox.py 通用 ingest dispatcher 加 _ingest_rules: BookTeardownResult(themes/archetypes/structure/hooks) 拍平为可读 rules 条目, 落项目级(无 continuity 预检,仿 _ingest_outline 无 409) - schemas/toolbox.py 加 TeardownIngestView + ToolIngestRequest.teardown - 单测:teardown ingest→rules 真落行 + 预览(generate)不写库 后端门禁绿:ruff/format/mypy clean · alembic no drift · pytest 644 passed
This commit is contained in:
@@ -31,6 +31,7 @@ from ww_core.domain.chapter_repo import ChapterDraftView, ChapterView
|
||||
from ww_core.domain.outline_write_repo import OutlineWriteView
|
||||
from ww_core.domain.project_repo import ProjectCreate
|
||||
from ww_core.domain.repositories import OutlineView
|
||||
from ww_core.domain.rule_repo import RuleWriteView
|
||||
from ww_core.domain.world_entity_repo import WorldEntityWriteView
|
||||
from ww_llm_gateway.types import LlmRequest, LlmResponse, ServedBy, Usage
|
||||
from ww_shared import AppError, ErrorCode
|
||||
@@ -94,6 +95,17 @@ class _FakeOutlineWriteRepo:
|
||||
)
|
||||
|
||||
|
||||
class _FakeRuleWriteRepo:
|
||||
"""拆书入库 rules 用的内存替身:按 (level, content) 收行(只 flush 语义)。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[dict[str, Any]] = []
|
||||
|
||||
async def create(self, project_id: uuid.UUID, *, level: str, content: str) -> RuleWriteView:
|
||||
self.rows.append({"project_id": project_id, "level": level, "content": content})
|
||||
return RuleWriteView(project_id=project_id, level=level, content=content)
|
||||
|
||||
|
||||
class _FakeOutlineReadRepo:
|
||||
"""续写/按章工具读节拍用的内存替身:缺章 → None(端点降级到空节拍)。"""
|
||||
|
||||
@@ -199,6 +211,7 @@ def _make_app(
|
||||
outline_write_repo: _FakeOutlineWriteRepo | None = None,
|
||||
chapter_repo: _FakeChapterRepo | None = None,
|
||||
outline_read_repo: _FakeOutlineReadRepo | None = None,
|
||||
rule_write_repo: _FakeRuleWriteRepo | None = None,
|
||||
memory: Any = None,
|
||||
no_creds: bool = False,
|
||||
) -> tuple[Any, FakeSession, _FakeWorldWriteRepo, _FakeOutlineWriteRepo]:
|
||||
@@ -210,6 +223,7 @@ def _make_app(
|
||||
get_outline_read_repo,
|
||||
get_outline_write_repo,
|
||||
get_project_repo,
|
||||
get_rule_write_repo,
|
||||
get_tier_gateway_builder,
|
||||
get_world_entity_write_repo,
|
||||
)
|
||||
@@ -220,6 +234,7 @@ def _make_app(
|
||||
outline_write_repo = outline_write_repo or _FakeOutlineWriteRepo()
|
||||
chapter_repo = chapter_repo or _FakeChapterRepo()
|
||||
outline_read_repo = outline_read_repo or _FakeOutlineReadRepo()
|
||||
rule_write_repo = rule_write_repo or _FakeRuleWriteRepo()
|
||||
|
||||
async def _build_ok(_tier: str) -> Any:
|
||||
return gateway
|
||||
@@ -236,6 +251,7 @@ def _make_app(
|
||||
app.dependency_overrides[get_outline_write_repo] = lambda: outline_write_repo
|
||||
app.dependency_overrides[get_chapter_repo] = lambda: chapter_repo
|
||||
app.dependency_overrides[get_outline_read_repo] = lambda: outline_read_repo
|
||||
app.dependency_overrides[get_rule_write_repo] = lambda: rule_write_repo
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
app.dependency_overrides[get_tier_gateway_builder] = lambda: (
|
||||
_build_no_creds if no_creds else _build_ok
|
||||
@@ -285,10 +301,13 @@ async def test_list_toolbox_contains_all_keys() -> None:
|
||||
"teardown",
|
||||
}
|
||||
by_key = {t["key"]: t for t in tools}
|
||||
# 4 个竞品生成器:非 legacy、纯预览(不可入库)。
|
||||
for key in ("continue", "expand", "de-ai", "teardown"):
|
||||
# 续写/扩写/降AI:非 legacy、纯预览(不可入库)。
|
||||
for key in ("continue", "expand", "de-ai"):
|
||||
assert by_key[key]["is_legacy"] is False
|
||||
assert by_key[key]["ingestable"] is False
|
||||
# F1:拆书非 legacy、可入库(落 rules)。
|
||||
assert by_key["teardown"]["is_legacy"] is False
|
||||
assert by_key["teardown"]["ingestable"] is True
|
||||
# legacy 携 legacy_route + is_legacy;新工具 ingestable 标记。
|
||||
assert by_key["worldbuilding"]["is_legacy"] is True
|
||||
assert by_key["worldbuilding"]["legacy_route"]
|
||||
@@ -622,3 +641,72 @@ async def test_ingest_outline_upserts_scenes() -> None:
|
||||
assert outline_repo.rows[0]["beats"] == ["主角觉醒", "初遇反派"]
|
||||
assert outline_repo.rows[0]["chapter_no"] == 3
|
||||
assert session.commits == 1
|
||||
|
||||
|
||||
# ---- POST .../ingest(F1:拆书落库成 rules)----
|
||||
|
||||
|
||||
def _teardown_payload() -> dict[str, Any]:
|
||||
return {
|
||||
"teardown": {
|
||||
"themes": ["逆袭", "复仇"],
|
||||
"archetypes": ["废柴主角", "腹黑反派"],
|
||||
"structure": "黄金三章立钩→铺垫→爆发",
|
||||
"hooks": ["开局即巅峰", "扮猪吃虎"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ingest_teardown_writes_rules_201() -> None:
|
||||
# F1:拆书结论拍平为可读 rules 条目落库(无 continuity 预检,仿 outline 无 409)。
|
||||
repo = FakeProjectRepo()
|
||||
pid = await _seed_project(repo)
|
||||
rule_repo = _FakeRuleWriteRepo()
|
||||
gateway = _SchemaRoutingGateway({})
|
||||
app, session, *_ = _make_app(project_repo=repo, gateway=gateway, rule_write_repo=rule_repo)
|
||||
|
||||
async with _client(app) as client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/skills/teardown/ingest", json=_teardown_payload()
|
||||
)
|
||||
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["table"] == "rules"
|
||||
assert body["rejected_tables"] == []
|
||||
# 真落行:themes/archetypes/structure/hooks 各拼成可读规则条目。
|
||||
assert len(rule_repo.rows) >= 1
|
||||
contents = "\n".join(r["content"] for r in rule_repo.rows)
|
||||
assert "逆袭" in contents
|
||||
assert "废柴主角" in contents
|
||||
assert "黄金三章立钩" in contents
|
||||
assert "开局即巅峰" in contents
|
||||
# 全部落到 project 级 rules(owner stub 单用户)。
|
||||
for r in rule_repo.rows:
|
||||
assert r["level"] == "project"
|
||||
assert r["project_id"] == pid
|
||||
assert session.commits == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teardown_generate_preview_does_not_write_rules() -> None:
|
||||
# 负向:预览(generate)只产结构化产物,绝不写 rules(不变量 #3:入库经 ingest gate)。
|
||||
repo = FakeProjectRepo()
|
||||
pid = await _seed_project(repo)
|
||||
rule_repo = _FakeRuleWriteRepo()
|
||||
parsed = BookTeardownResult(
|
||||
themes=["逆袭"], archetypes=["废柴主角"], structure="立钩", hooks=["开局巅峰"]
|
||||
)
|
||||
gateway = _CaptureGateway(parsed)
|
||||
app, _, *_ = _make_app(project_repo=repo, gateway=gateway, rule_write_repo=rule_repo)
|
||||
|
||||
async with _client(app) as client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/skills/teardown/generate",
|
||||
json={"kind": "某爆款", "text": "样本章节正文……"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["output_kind"] == "BookTeardownResult"
|
||||
assert rule_repo.rows == [] # 预览不写库
|
||||
|
||||
@@ -24,6 +24,7 @@ from ww_core.domain.chapter_repo import ChapterRepo
|
||||
from ww_core.domain.outline_write_repo import OutlineWriteRepo
|
||||
from ww_core.domain.project_repo import ProjectRepo
|
||||
from ww_core.domain.repositories import MemoryRepos, OutlineRepo
|
||||
from ww_core.domain.rule_repo import RuleWriteRepo
|
||||
from ww_core.domain.world_entity_repo import WorldEntityWriteRepo
|
||||
from ww_core.orchestrator import run_generator
|
||||
from ww_db import get_session
|
||||
@@ -37,6 +38,7 @@ from ww_api.routers.generation import _project_context, _world_context
|
||||
from ww_api.schemas.generation import IngestConflictView, WorldEntityCardView
|
||||
from ww_api.schemas.toolbox import (
|
||||
OutlineSceneIngestView,
|
||||
TeardownIngestView,
|
||||
ToolboxListResponse,
|
||||
ToolDescriptorView,
|
||||
ToolGeneratePreviewResponse,
|
||||
@@ -53,6 +55,7 @@ from ww_api.services.project_deps import (
|
||||
get_outline_read_repo,
|
||||
get_outline_write_repo,
|
||||
get_project_repo,
|
||||
get_rule_write_repo,
|
||||
get_tier_gateway_builder,
|
||||
get_world_entity_write_repo,
|
||||
)
|
||||
@@ -79,6 +82,7 @@ OutlineReadRepoDep = Annotated[OutlineRepo, Depends(get_outline_read_repo)]
|
||||
ChapterRepoDep = Annotated[ChapterRepo, Depends(get_chapter_repo)]
|
||||
WorldWriteRepoDep = Annotated[WorldEntityWriteRepo, Depends(get_world_entity_write_repo)]
|
||||
OutlineWriteRepoDep = Annotated[OutlineWriteRepo, Depends(get_outline_write_repo)]
|
||||
RuleWriteRepoDep = Annotated[RuleWriteRepo, Depends(get_rule_write_repo)]
|
||||
GatewayBuilderDep = Annotated[TierGatewayBuilder, Depends(get_tier_gateway_builder)]
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
@@ -316,6 +320,7 @@ async def ingest_with_tool(
|
||||
memory: MemoryReposDep,
|
||||
world_write_repo: WorldWriteRepoDep,
|
||||
outline_write_repo: OutlineWriteRepoDep,
|
||||
rule_write_repo: RuleWriteRepoDep,
|
||||
build_gateway: GatewayBuilderDep,
|
||||
session: SessionDep,
|
||||
) -> ToolIngestResponse:
|
||||
@@ -362,6 +367,16 @@ async def ingest_with_tool(
|
||||
request_id=request_id,
|
||||
tool_key=tool_key,
|
||||
)
|
||||
if table == "rules":
|
||||
return await _ingest_rules(
|
||||
spec,
|
||||
body=body,
|
||||
project_id=project_id,
|
||||
rule_write_repo=rule_write_repo,
|
||||
session=session,
|
||||
request_id=request_id,
|
||||
tool_key=tool_key,
|
||||
)
|
||||
# 描述符的 IngestSpec.table 已限白名单;其余表本期通用入库未实现。
|
||||
raise AppError(ErrorCode.VALIDATION, f"入库表 {table} 暂未支持通用入库", {"table": table})
|
||||
|
||||
@@ -493,3 +508,72 @@ async def _ingest_outline(
|
||||
created_count=len(created),
|
||||
)
|
||||
return ToolIngestResponse(table="outline", created=created, rejected_tables=rejected)
|
||||
|
||||
|
||||
# 拆书入库的 rules 级别(meta 规则非设定卡,落项目级;单用户原型 owner stub)。
|
||||
_TEARDOWN_RULE_LEVEL = "project"
|
||||
|
||||
|
||||
def _flatten_teardown_rules(teardown: TeardownIngestView) -> list[tuple[str, str]]:
|
||||
"""把拆书结论拍平成可读 rules 条目:返回 (label, content) 列表(确定性顺序,跳过空字段)。
|
||||
|
||||
主题/原型/钩子各一条(清单拼成可读文本);结构一条。空清单/空串的字段跳过,不落空规则。
|
||||
"""
|
||||
entries: list[tuple[str, str]] = []
|
||||
if teardown.themes:
|
||||
entries.append(("themes", f"核心主题:{';'.join(teardown.themes)}"))
|
||||
if teardown.archetypes:
|
||||
entries.append(("archetypes", f"人物原型:{';'.join(teardown.archetypes)}"))
|
||||
if teardown.structure.strip():
|
||||
entries.append(("structure", f"叙事结构:{teardown.structure.strip()}"))
|
||||
if teardown.hooks:
|
||||
entries.append(("hooks", f"钩子套路:{';'.join(teardown.hooks)}"))
|
||||
return entries
|
||||
|
||||
|
||||
async def _ingest_rules(
|
||||
spec: AgentSpec,
|
||||
*,
|
||||
body: ToolIngestRequest,
|
||||
project_id: uuid.UUID,
|
||||
rule_write_repo: RuleWriteRepo,
|
||||
session: AsyncSession,
|
||||
request_id: str | None,
|
||||
tool_key: str,
|
||||
) -> ToolIngestResponse:
|
||||
"""rules 入库(拆书):白名单 → 把拆书结论拍平为可读 rules 条目写入项目级 rules。
|
||||
|
||||
meta 规则非设定卡,无需 continuity 预检(仿 outline 无 409 路径);仍过 partition_writes
|
||||
白名单(teardown spec 声明 writes=["rules"])。无 teardown 产物 → VALIDATION。
|
||||
"""
|
||||
if body.teardown is None:
|
||||
raise AppError(ErrorCode.VALIDATION, "拆书入库需提供 teardown 产物", {"tool_key": tool_key})
|
||||
|
||||
entries = _flatten_teardown_rules(body.teardown)
|
||||
|
||||
# gate:权限白名单(teardown spec 声明 writes=["rules"])。
|
||||
allowed, rejected = partition_writes(spec, {"rules": entries})
|
||||
if rejected:
|
||||
log.warning(
|
||||
"toolbox_ingest_rejected_over_permission",
|
||||
tool_key=tool_key,
|
||||
project_id=str(project_id),
|
||||
request_id=request_id,
|
||||
rejected_tables=rejected,
|
||||
)
|
||||
|
||||
created: list[str] = []
|
||||
for label, content in allowed.get("rules", []):
|
||||
await rule_write_repo.create(project_id, level=_TEARDOWN_RULE_LEVEL, content=content)
|
||||
created.append(label)
|
||||
|
||||
await session.commit()
|
||||
log.info(
|
||||
"toolbox_ingest_done",
|
||||
tool_key=tool_key,
|
||||
project_id=str(project_id),
|
||||
request_id=request_id,
|
||||
table="rules",
|
||||
created_count=len(created),
|
||||
)
|
||||
return ToolIngestResponse(table="rules", created=created, rejected_tables=rejected)
|
||||
|
||||
@@ -105,12 +105,22 @@ class OutlineSceneIngestView(BaseModel):
|
||||
hook: str = ""
|
||||
|
||||
|
||||
class TeardownIngestView(BaseModel):
|
||||
"""拆书入库产物(贴 ww_agents.BookTeardownResult;落 rules 表)。F1。"""
|
||||
|
||||
themes: list[str] = Field(default_factory=list, description="核心主题/立意清单")
|
||||
archetypes: list[str] = Field(default_factory=list, description="人物原型/角色模板清单")
|
||||
structure: str = Field(default="", description="叙事结构概述")
|
||||
hooks: list[str] = Field(default_factory=list, description="抓人钩子/爽点套路清单")
|
||||
|
||||
|
||||
class ToolIngestRequest(BaseModel):
|
||||
"""通用入库请求:待持久化的产物 + 冲突确认。
|
||||
|
||||
仅声明了 `ingest` 的工具可用。按入库表取用对应字段:
|
||||
- world_entities(金手指/词条)→ `world_entities`(贴 WorldEntityCardView:type/name/rules);
|
||||
- outline(细纲)→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView)。
|
||||
- outline(细纲)→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView);
|
||||
- rules(拆书)→ `teardown`(贴 TeardownIngestView:themes/archetypes/structure/hooks)。
|
||||
`acknowledge_conflicts`:作者已查看并接受 continuity 预检冲突时置 true 放行(仿 accept gate)。
|
||||
"""
|
||||
|
||||
@@ -123,6 +133,9 @@ class ToolIngestRequest(BaseModel):
|
||||
scenes: list[OutlineSceneIngestView] = Field(
|
||||
default_factory=list, description="outline 入库的场景行(细纲)"
|
||||
)
|
||||
teardown: TeardownIngestView | None = Field(
|
||||
default=None, description="rules 入库的拆书结论(F1;拍平为可读规则条目)"
|
||||
)
|
||||
acknowledge_conflicts: bool = Field(
|
||||
default=False, description="作者已知悉并接受 continuity 冲突 → 放行入库"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Scope B 竞品快赢 · 4 个生成器的 spec/schema 契约测试。
|
||||
|
||||
校验每个 spec 的 name/tier/reads/writes/output_schema/scope/input_schema,
|
||||
对齐不变量 #2(只声明 tier)/ #3(4 生成器一律纯预览 writes=[])。不联网、无 DB。
|
||||
对齐不变量 #2(只声明 tier)/ #3(续写/扩写/降AI 纯预览 writes=[];拆书 F1 落库 rules)。
|
||||
不联网、无 DB。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,17 +20,25 @@ from ww_agents import (
|
||||
teardown_spec,
|
||||
)
|
||||
|
||||
# (spec, name, tier, reads, output_schema)
|
||||
# (spec, name, tier, reads, writes, output_schema)
|
||||
_COMPETITOR_SPECS = [
|
||||
(continue_spec, "continue", "writer", ["projects", "outline", "chapters"], ContinuationResult),
|
||||
(expand_spec, "expand", "writer", ["projects"], PolishResult),
|
||||
(de_ai_spec, "de-ai", "analyst", ["projects"], DeAiResult),
|
||||
(teardown_spec, "teardown", "analyst", ["projects"], BookTeardownResult),
|
||||
(
|
||||
continue_spec,
|
||||
"continue",
|
||||
"writer",
|
||||
["projects", "outline", "chapters"],
|
||||
[],
|
||||
ContinuationResult,
|
||||
),
|
||||
(expand_spec, "expand", "writer", ["projects"], [], PolishResult),
|
||||
(de_ai_spec, "de-ai", "analyst", ["projects"], [], DeAiResult),
|
||||
# F1:拆书结论可落库为项目 rules(writes=["rules"];其余三者仍纯预览)。
|
||||
(teardown_spec, "teardown", "analyst", ["projects"], ["rules"], BookTeardownResult),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("spec", "name", "tier", "reads", "output_schema"),
|
||||
("spec", "name", "tier", "reads", "writes", "output_schema"),
|
||||
_COMPETITOR_SPECS,
|
||||
)
|
||||
def test_competitor_spec_declares_expected_contract(
|
||||
@@ -37,12 +46,13 @@ def test_competitor_spec_declares_expected_contract(
|
||||
name: str,
|
||||
tier: str,
|
||||
reads: list[str],
|
||||
writes: list[str],
|
||||
output_schema: type,
|
||||
) -> None:
|
||||
assert spec.name == name
|
||||
assert spec.tier == tier
|
||||
assert spec.reads == reads
|
||||
assert spec.writes == [] # 不变量 #3:4 生成器一律纯预览
|
||||
assert spec.writes == writes # 不变量 #3:续写/扩写/降AI 纯预览;拆书落 rules
|
||||
assert spec.output_schema is output_schema
|
||||
assert spec.scope == "builtin"
|
||||
assert spec.input_schema is None # 注入材料为序列化文本,非结构化入参
|
||||
|
||||
@@ -571,8 +571,8 @@ class DeAiResult(BaseModel):
|
||||
class BookTeardownResult(BaseModel):
|
||||
"""拆书生成器结构化产出:样本作品的结构化拆解(Scope B 竞品快赢)。
|
||||
|
||||
纯预览产物——不映射任何业务表、不入库(`teardown_spec.writes=[]`,不变量 #3)。
|
||||
拆书「落库成 rules」为可选 follow-up(需 `KNOWN_TABLES` + `_ingest_rules`),本期不做。
|
||||
F1:可经 ingest 端点把结论拍平为项目 `rules` 条目落库(`teardown_spec.writes=["rules"]`);
|
||||
入库仍经白名单 gate(不变量 #3),预览(generate)不写库。
|
||||
"""
|
||||
|
||||
themes: list[str] = Field(
|
||||
|
||||
@@ -799,6 +799,6 @@ teardown_spec = AgentSpec(
|
||||
input_schema=None, # 注入材料为序列化文本(设定 + 样本 + 需求),非结构化入参
|
||||
output_schema=BookTeardownResult,
|
||||
reads=["projects"],
|
||||
writes=[], # 纯预览,不写库(不变量 #3)
|
||||
writes=["rules"], # F1:拆书结论可落库为项目 rules(入库仍经 ingest 白名单 gate,#3)
|
||||
scope="builtin",
|
||||
)
|
||||
|
||||
@@ -19,13 +19,13 @@ _NEW_KEYS = {
|
||||
"glossary",
|
||||
"opening",
|
||||
"fine-outline",
|
||||
# 竞品快赢(Scope B):续写 / 扩写 / 降AI率 / 拆书(全 preview-only)。
|
||||
# 竞品快赢(Scope B):续写/扩写/降AI率 preview-only;拆书 F1 可落库 rules。
|
||||
"continue",
|
||||
"expand",
|
||||
"de-ai",
|
||||
"teardown",
|
||||
}
|
||||
_INGEST_KEYS = {"golden-finger", "glossary", "fine-outline"}
|
||||
_INGEST_KEYS = {"golden-finger", "glossary", "fine-outline", "teardown"}
|
||||
|
||||
|
||||
def test_toolbox_has_all_tools() -> None:
|
||||
@@ -70,6 +70,9 @@ def test_ingest_tools_declare_known_table() -> None:
|
||||
assert TOOLBOX["glossary"].ingest.table == "world_entities"
|
||||
assert TOOLBOX["fine-outline"].ingest is not None
|
||||
assert TOOLBOX["fine-outline"].ingest.table == "outline"
|
||||
# F1:拆书结论落库为项目 rules。
|
||||
assert TOOLBOX["teardown"].ingest is not None
|
||||
assert TOOLBOX["teardown"].ingest.table == "rules"
|
||||
|
||||
|
||||
def test_chapter_tools_have_chapter_no_field() -> None:
|
||||
|
||||
@@ -197,7 +197,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
|
||||
ingest=IngestSpec(table="outline"),
|
||||
),
|
||||
# ---- 竞品快赢(Scope B):续写 / 扩写 / 降AI率 / 拆书(全 preview-only,writes=[])----
|
||||
# ---- 竞品快赢(Scope B):续写/扩写/降AI率 preview-only;拆书 F1 可落库 rules ----
|
||||
"continue": GeneratorTool(
|
||||
key="continue",
|
||||
title="续写生成器",
|
||||
@@ -252,6 +252,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
_SOURCE_TEXT_FIELD,
|
||||
_BRIEF_FIELD,
|
||||
],
|
||||
ingest=IngestSpec(table="rules"), # F1:拆书结论拍平为项目 rules 条目落库
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user