xg-cli 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xg/__init__.py +3 -0
- xg/__main__.py +3 -0
- xg/adaptive/__init__.py +27 -0
- xg/adaptive/calibrate.py +191 -0
- xg/adaptive/feedback.py +170 -0
- xg/adaptive/learned_rules.py +284 -0
- xg/adaptive/signals.py +125 -0
- xg/adaptive/store.py +164 -0
- xg/agent/__init__.py +0 -0
- xg/agent/plan.py +631 -0
- xg/agent/react.py +268 -0
- xg/agent/team.py +1793 -0
- xg/assets/router.lgb +0 -0
- xg/assets/router_semantics.json +21278 -0
- xg/assets/router_semantics.onnx +0 -0
- xg/cli/__init__.py +0 -0
- xg/cli/app.py +1372 -0
- xg/cli/commands.py +921 -0
- xg/cli/completion.py +740 -0
- xg/cli/help.py +185 -0
- xg/cli/train.py +200 -0
- xg/config/__init__.py +0 -0
- xg/config/env_writer.py +121 -0
- xg/config/manager.py +446 -0
- xg/config/mcp.py +229 -0
- xg/config/provider_service.py +267 -0
- xg/config/providers.py +46 -0
- xg/config/settings.py +258 -0
- xg/config/skills.py +100 -0
- xg/config/smart_router_service.py +121 -0
- xg/config/web.py +140 -0
- xg/input_history/__init__.py +7 -0
- xg/input_history/models.py +28 -0
- xg/input_history/persistence.py +126 -0
- xg/input_history/policy.py +39 -0
- xg/input_history/prompt_toolkit.py +36 -0
- xg/input_history/store.py +118 -0
- xg/llm/__init__.py +0 -0
- xg/llm/client.py +49 -0
- xg/llm/factory.py +40 -0
- xg/llm/openai_compat.py +275 -0
- xg/llm/types.py +98 -0
- xg/mcp/__init__.py +4 -0
- xg/mcp/http.py +192 -0
- xg/mcp/manager.py +726 -0
- xg/mcp/models.py +86 -0
- xg/mcp/protocol.py +62 -0
- xg/mcp/resources.py +72 -0
- xg/mcp/schema.py +137 -0
- xg/mcp/stdio.py +210 -0
- xg/mcp/transport.py +66 -0
- xg/memory/__init__.py +15 -0
- xg/memory/context.py +327 -0
- xg/memory/manager.py +111 -0
- xg/memory/models.py +41 -0
- xg/memory/project.py +187 -0
- xg/memory/store.py +144 -0
- xg/router/__init__.py +124 -0
- xg/router/features.py +66 -0
- xg/router/keywords.py +50 -0
- xg/router/ml_router.py +178 -0
- xg/router/model_tiers.py +73 -0
- xg/router/postprocess.py +167 -0
- xg/router/rule_router.py +77 -0
- xg/router/semantic.py +138 -0
- xg/safety/__init__.py +0 -0
- xg/safety/audit.py +96 -0
- xg/safety/guards.py +106 -0
- xg/safety/hitl.py +73 -0
- xg/skill/__init__.py +9 -0
- xg/skill/errors.py +45 -0
- xg/skill/loader.py +42 -0
- xg/skill/models.py +57 -0
- xg/skill/parser.py +93 -0
- xg/skill/policy.py +40 -0
- xg/skill/prompt.py +45 -0
- xg/skill/registry.py +169 -0
- xg/tool/__init__.py +0 -0
- xg/tool/builtin.py +356 -0
- xg/tool/registry.py +228 -0
- xg/tui/__init__.py +34 -0
- xg/tui/app.py +612 -0
- xg/tui/controller.py +1296 -0
- xg/tui/diagrams/__init__.py +22 -0
- xg/tui/diagrams/layout.py +110 -0
- xg/tui/diagrams/markdown.py +39 -0
- xg/tui/diagrams/model.py +36 -0
- xg/tui/diagrams/parser.py +119 -0
- xg/tui/diagrams/renderer.py +551 -0
- xg/tui/i18n.py +169 -0
- xg/tui/messages.py +45 -0
- xg/tui/plan_renderables.py +147 -0
- xg/tui/reducer.py +1029 -0
- xg/tui/renderables.py +252 -0
- xg/tui/state.py +240 -0
- xg/tui/theme.tcss +208 -0
- xg/tui/widgets/__init__.py +1 -0
- xg/tui/widgets/action_card.py +94 -0
- xg/tui/widgets/agent_group_card.py +39 -0
- xg/tui/widgets/approval_modal.py +59 -0
- xg/tui/widgets/collapsible_card.py +40 -0
- xg/tui/widgets/command_suggestions.py +128 -0
- xg/tui/widgets/composer.py +151 -0
- xg/tui/widgets/config_panel.py +198 -0
- xg/tui/widgets/confirm_modal.py +31 -0
- xg/tui/widgets/footer.py +9 -0
- xg/tui/widgets/header.py +118 -0
- xg/tui/widgets/inspector.py +378 -0
- xg/tui/widgets/plan_modal.py +53 -0
- xg/tui/widgets/provider_form.py +141 -0
- xg/tui/widgets/queue_status.py +30 -0
- xg/tui/widgets/smart_router_form.py +95 -0
- xg/tui/widgets/transcript.py +354 -0
- xg/tui/workers.py +14 -0
- xg/web/__init__.py +21 -0
- xg/web/errors.py +57 -0
- xg/web/extract.py +127 -0
- xg/web/fetch.py +106 -0
- xg/web/markdown.py +77 -0
- xg/web/models.py +91 -0
- xg/web/providers.py +79 -0
- xg/web/search.py +118 -0
- xg/web/searxng.py +27 -0
- xg/web/serpapi.py +29 -0
- xg/web/url_policy.py +110 -0
- xg/web/zhipu.py +27 -0
- xg_cli-1.0.dist-info/METADATA +284 -0
- xg_cli-1.0.dist-info/RECORD +130 -0
- xg_cli-1.0.dist-info/WHEEL +4 -0
- xg_cli-1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Dependency-free Mermaid flowchart support for the TUI."""
|
|
2
|
+
|
|
3
|
+
from xg.tui.diagrams.markdown import MermaidBlock, split_mermaid_blocks
|
|
4
|
+
from xg.tui.diagrams.layout import FlowchartLayout, LayoutNode, layout_flowchart
|
|
5
|
+
from xg.tui.diagrams.model import FlowchartEdge, FlowchartModel, FlowchartNode
|
|
6
|
+
from xg.tui.diagrams.parser import FlowchartParseError, parse_flowchart
|
|
7
|
+
from xg.tui.diagrams.renderer import DiagramRender, render_flowchart
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FlowchartEdge",
|
|
11
|
+
"FlowchartLayout",
|
|
12
|
+
"FlowchartModel",
|
|
13
|
+
"FlowchartNode",
|
|
14
|
+
"DiagramRender",
|
|
15
|
+
"FlowchartParseError",
|
|
16
|
+
"LayoutNode",
|
|
17
|
+
"MermaidBlock",
|
|
18
|
+
"parse_flowchart",
|
|
19
|
+
"layout_flowchart",
|
|
20
|
+
"render_flowchart",
|
|
21
|
+
"split_mermaid_blocks",
|
|
22
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Stable, bounded layout for the supported Mermaid flowchart model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from xg.tui.diagrams.model import FlowchartModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class LayoutNode:
|
|
12
|
+
"""A node position in a rank/row layout."""
|
|
13
|
+
|
|
14
|
+
node_id: str
|
|
15
|
+
rank: int
|
|
16
|
+
order: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class FlowchartLayout:
|
|
21
|
+
"""Deterministic placement metadata used by the text renderer."""
|
|
22
|
+
|
|
23
|
+
nodes: tuple[LayoutNode, ...]
|
|
24
|
+
ranks: tuple[tuple[str, ...], ...]
|
|
25
|
+
cyclic: bool = False
|
|
26
|
+
|
|
27
|
+
def position(self, node_id: str) -> LayoutNode:
|
|
28
|
+
for node in self.nodes:
|
|
29
|
+
if node.node_id == node_id:
|
|
30
|
+
return node
|
|
31
|
+
raise KeyError(node_id)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def layout_flowchart(
|
|
35
|
+
model: FlowchartModel,
|
|
36
|
+
*,
|
|
37
|
+
max_iterations: int | None = None,
|
|
38
|
+
rank_by_node: dict[str, int] | None = None,
|
|
39
|
+
) -> FlowchartLayout:
|
|
40
|
+
"""Assign nodes to stable layers without an unbounded graph traversal.
|
|
41
|
+
|
|
42
|
+
DAGs use the longest known predecessor rank. For cyclic graphs the same
|
|
43
|
+
relaxation is capped at ``node_count`` iterations and the remaining nodes
|
|
44
|
+
keep their stable source order. This is deliberately small and
|
|
45
|
+
predictable rather than a full Mermaid-compatible layout engine.
|
|
46
|
+
"""
|
|
47
|
+
node_ids = [node.id for node in model.nodes]
|
|
48
|
+
index = {node_id: i for i, node_id in enumerate(node_ids)}
|
|
49
|
+
incoming: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
|
|
50
|
+
for edge in model.edges:
|
|
51
|
+
if edge.source in incoming and edge.target in incoming:
|
|
52
|
+
incoming[edge.target].append(edge.source)
|
|
53
|
+
|
|
54
|
+
limit = max_iterations if max_iterations is not None else max(1, len(node_ids))
|
|
55
|
+
ranks = {node_id: 0 for node_id in node_ids}
|
|
56
|
+
if rank_by_node is not None:
|
|
57
|
+
# Callers such as Plan already have a business-level batch partition.
|
|
58
|
+
# Honor it while retaining a safe default for omitted nodes.
|
|
59
|
+
ranks.update({node_id: rank for node_id, rank in rank_by_node.items() if node_id in ranks and rank >= 0})
|
|
60
|
+
grouped: dict[int, list[str]] = {}
|
|
61
|
+
for node_id in node_ids:
|
|
62
|
+
grouped.setdefault(ranks[node_id], []).append(node_id)
|
|
63
|
+
ordered_ranks = sorted(grouped)
|
|
64
|
+
if model.direction in ("BT", "RL"):
|
|
65
|
+
ordered_ranks.reverse()
|
|
66
|
+
rank_lists = [tuple(grouped[rank]) for rank in ordered_ranks]
|
|
67
|
+
positions = tuple(
|
|
68
|
+
LayoutNode(node_id=node_id, rank=rank_no, order=order)
|
|
69
|
+
for rank_no, rank_nodes in enumerate(rank_lists)
|
|
70
|
+
for order, node_id in enumerate(rank_nodes)
|
|
71
|
+
)
|
|
72
|
+
return FlowchartLayout(nodes=positions, ranks=tuple(rank_lists), cyclic=False)
|
|
73
|
+
|
|
74
|
+
changed = False
|
|
75
|
+
for _ in range(limit):
|
|
76
|
+
changed = False
|
|
77
|
+
for node_id in node_ids:
|
|
78
|
+
predecessors = incoming[node_id]
|
|
79
|
+
if not predecessors:
|
|
80
|
+
continue
|
|
81
|
+
candidate = max(ranks[pred] + 1 for pred in predecessors)
|
|
82
|
+
if candidate > ranks[node_id]:
|
|
83
|
+
ranks[node_id] = candidate
|
|
84
|
+
changed = True
|
|
85
|
+
if not changed:
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
cyclic = changed
|
|
89
|
+
# A cycle can make the relaxation increase every rank on every pass. Keep
|
|
90
|
+
# it bounded and collapse the inflated ranks to a useful stable layout.
|
|
91
|
+
if cyclic:
|
|
92
|
+
ranks = {node_id: 0 for node_id in node_ids}
|
|
93
|
+
for node_id in node_ids:
|
|
94
|
+
predecessors = [p for p in incoming[node_id] if index[p] < index[node_id]]
|
|
95
|
+
ranks[node_id] = min(1, max((ranks[p] + 1 for p in predecessors), default=0))
|
|
96
|
+
|
|
97
|
+
grouped: dict[int, list[str]] = {}
|
|
98
|
+
for node_id in node_ids:
|
|
99
|
+
grouped.setdefault(ranks[node_id], []).append(node_id)
|
|
100
|
+
ordered_ranks = sorted(grouped)
|
|
101
|
+
if model.direction in ("BT", "RL"):
|
|
102
|
+
ordered_ranks.reverse()
|
|
103
|
+
|
|
104
|
+
rank_lists = [tuple(grouped[rank]) for rank in ordered_ranks]
|
|
105
|
+
positions = tuple(
|
|
106
|
+
LayoutNode(node_id=node_id, rank=rank_no, order=order)
|
|
107
|
+
for rank_no, rank_nodes in enumerate(rank_lists)
|
|
108
|
+
for order, node_id in enumerate(rank_nodes)
|
|
109
|
+
)
|
|
110
|
+
return FlowchartLayout(nodes=positions, ranks=tuple(rank_lists), cyclic=cyclic)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Recognize complete Mermaid fenced blocks without parsing Markdown globally."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class MermaidBlock:
|
|
11
|
+
source: str
|
|
12
|
+
start: int
|
|
13
|
+
end: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_FENCE_RE = re.compile(
|
|
17
|
+
r"```[ \t]*mermaid[ \t]*\r?\n(?P<body>.*?)(?P<fence>```)[ \t]*(?:\r?\n|$)",
|
|
18
|
+
re.IGNORECASE | re.DOTALL,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def split_mermaid_blocks(text: str) -> list[tuple[str, MermaidBlock | None]]:
|
|
23
|
+
"""Split text into ``("text", None)`` and complete Mermaid blocks.
|
|
24
|
+
|
|
25
|
+
An unclosed fence is deliberately returned as ordinary text. This keeps
|
|
26
|
+
streaming assistant output visible as source until the fence is complete.
|
|
27
|
+
"""
|
|
28
|
+
parts: list[tuple[str, MermaidBlock | None]] = []
|
|
29
|
+
cursor = 0
|
|
30
|
+
for match in _FENCE_RE.finditer(text):
|
|
31
|
+
if match.start() > cursor:
|
|
32
|
+
parts.append((text[cursor : match.start()], None))
|
|
33
|
+
parts.append((match.group("body"), MermaidBlock(
|
|
34
|
+
source=match.group("body"), start=match.start(), end=match.end()
|
|
35
|
+
)))
|
|
36
|
+
cursor = match.end()
|
|
37
|
+
if cursor < len(text):
|
|
38
|
+
parts.append((text[cursor:], None))
|
|
39
|
+
return parts or [(text, None)]
|
xg/tui/diagrams/model.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Pure data structures for the supported Mermaid flowchart subset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Direction = Literal["TD", "TB", "BT", "LR", "RL"]
|
|
10
|
+
NodeShape = Literal["rect", "round", "diamond", "circle"]
|
|
11
|
+
EdgeStyle = Literal["solid", "dotted", "thick"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FlowchartNode:
|
|
16
|
+
id: str
|
|
17
|
+
label: str
|
|
18
|
+
shape: NodeShape = "rect"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class FlowchartEdge:
|
|
23
|
+
source: str
|
|
24
|
+
target: str
|
|
25
|
+
label: str = ""
|
|
26
|
+
style: EdgeStyle = "solid"
|
|
27
|
+
arrow: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FlowchartModel:
|
|
32
|
+
direction: Direction = "TD"
|
|
33
|
+
nodes: list[FlowchartNode] = field(default_factory=list)
|
|
34
|
+
edges: list[FlowchartEdge] = field(default_factory=list)
|
|
35
|
+
source: str = ""
|
|
36
|
+
warnings: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Small, safe parser for the common Mermaid flowchart syntax."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from xg.tui.diagrams.model import FlowchartEdge, FlowchartModel, FlowchartNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlowchartParseError(ValueError):
|
|
12
|
+
"""The source is not a supported flowchart subset."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_HEADER_RE = re.compile(r"^(?:flowchart|graph)(?:\s+(TD|TB|BT|LR|RL))?\s*$", re.IGNORECASE)
|
|
16
|
+
_NODE_RE = re.compile(
|
|
17
|
+
r"\s*(?P<id>[A-Za-z_][\w-]*)"
|
|
18
|
+
r"(?P<body>\(\([^\n()]*\)\)|\[[^\n\]]*\]|\([^\n()]*\)|\{[^\n}]*\}|>[^\n\]]*\])?"
|
|
19
|
+
)
|
|
20
|
+
_PIPE_LABEL_RE = re.compile(r"\|(?P<label>[^|]*)\|")
|
|
21
|
+
_EDGE_OPS = ("-.->", "==>", "-->", "---")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _clean_label(value: str) -> str:
|
|
25
|
+
value = html.unescape(value.replace("<br/>", "\n").replace("<br>", "\n"))
|
|
26
|
+
value = re.sub(r"<[^>]+>", "", value)
|
|
27
|
+
return value.strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _node_parts(node_id: str, body: str | None) -> tuple[str, str]:
|
|
31
|
+
if not body:
|
|
32
|
+
return node_id, "rect"
|
|
33
|
+
if body.startswith("(("):
|
|
34
|
+
return _clean_label(body[2:-2]) or node_id, "circle"
|
|
35
|
+
if body.startswith("["):
|
|
36
|
+
return _clean_label(body[1:-1]) or node_id, "rect"
|
|
37
|
+
if body.startswith("("):
|
|
38
|
+
return _clean_label(body[1:-1]) or node_id, "round"
|
|
39
|
+
if body.startswith("{"):
|
|
40
|
+
return _clean_label(body[1:-1]) or node_id, "diamond"
|
|
41
|
+
if body.startswith(">"): # Mermaid flag shape: >label]
|
|
42
|
+
return _clean_label(body[1:-1]) or node_id, "rect"
|
|
43
|
+
return node_id, "rect"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_flowchart(source: str, *, max_nodes: int = 80, max_edges: int = 160, max_label_chars: int = 120) -> FlowchartModel:
|
|
47
|
+
"""Parse a bounded subset of ``flowchart`` / ``graph`` Mermaid syntax."""
|
|
48
|
+
lines = [line.strip() for line in source.splitlines() if line.strip() and not line.lstrip().startswith("%%")]
|
|
49
|
+
if not lines:
|
|
50
|
+
raise FlowchartParseError("flowchart 内容为空")
|
|
51
|
+
header = _HEADER_RE.fullmatch(lines[0])
|
|
52
|
+
if not header:
|
|
53
|
+
raise FlowchartParseError("只支持 flowchart/graph 语法及 TD/TB/BT/LR/RL 方向")
|
|
54
|
+
direction = (header.group(1) or "TD").upper()
|
|
55
|
+
model = FlowchartModel(direction=direction, source=source)
|
|
56
|
+
nodes: dict[str, FlowchartNode] = {}
|
|
57
|
+
|
|
58
|
+
def ensure_node(node_id: str, body: str | None) -> None:
|
|
59
|
+
label, shape = _node_parts(node_id, body)
|
|
60
|
+
if len(label) > max_label_chars:
|
|
61
|
+
label = label[:max_label_chars] + "…"
|
|
62
|
+
model.warnings.append(f"节点 {node_id} 标签已截断")
|
|
63
|
+
existing = nodes.get(node_id)
|
|
64
|
+
if existing is None:
|
|
65
|
+
if len(nodes) >= max_nodes:
|
|
66
|
+
raise FlowchartParseError(f"节点数超过上限 {max_nodes}")
|
|
67
|
+
nodes[node_id] = FlowchartNode(id=node_id, label=label, shape=shape) # type: ignore[arg-type]
|
|
68
|
+
elif body:
|
|
69
|
+
existing.label = label
|
|
70
|
+
existing.shape = shape # type: ignore[assignment]
|
|
71
|
+
|
|
72
|
+
def parse_node(text: str, pos: int) -> tuple[str, str | None, int]:
|
|
73
|
+
match = _NODE_RE.match(text, pos)
|
|
74
|
+
if not match:
|
|
75
|
+
raise FlowchartParseError(f"无法解析节点:{text[pos:].strip()[:80]}")
|
|
76
|
+
return match.group("id"), match.group("body"), match.end()
|
|
77
|
+
|
|
78
|
+
for line_no, line in enumerate(lines[1:], 2):
|
|
79
|
+
if line.lower().startswith("subgraph") or line.lower().startswith("end"):
|
|
80
|
+
raise FlowchartParseError(f"第 {line_no} 行不支持 subgraph")
|
|
81
|
+
for statement in (part.strip() for part in line.split(";") if part.strip()):
|
|
82
|
+
pos = 0
|
|
83
|
+
source_id, source_body, pos = parse_node(statement, pos)
|
|
84
|
+
ensure_node(source_id, source_body)
|
|
85
|
+
while pos < len(statement):
|
|
86
|
+
rest = statement[pos:].lstrip()
|
|
87
|
+
pos = len(statement) - len(rest)
|
|
88
|
+
label = ""
|
|
89
|
+
style = "solid"
|
|
90
|
+
op = next((candidate for candidate in _EDGE_OPS if rest.startswith(candidate)), None)
|
|
91
|
+
if op is not None:
|
|
92
|
+
pos += len(op)
|
|
93
|
+
style = "dotted" if op == "-.->" else "thick" if op == "==>" else "solid"
|
|
94
|
+
label_match = _PIPE_LABEL_RE.match(statement, pos)
|
|
95
|
+
if label_match:
|
|
96
|
+
label = _clean_label(label_match.group("label"))
|
|
97
|
+
pos = label_match.end()
|
|
98
|
+
elif rest.startswith("--"):
|
|
99
|
+
# Support the readable form: A -- label --> B.
|
|
100
|
+
end = rest.find("-->", 2)
|
|
101
|
+
if end < 0:
|
|
102
|
+
raise FlowchartParseError(f"第 {line_no} 行的连线不完整")
|
|
103
|
+
label = _clean_label(rest[2:end])
|
|
104
|
+
pos += end + 3
|
|
105
|
+
else:
|
|
106
|
+
raise FlowchartParseError(f"第 {line_no} 行存在不支持的语法:{rest[:80]}")
|
|
107
|
+
target_id, target_body, pos = parse_node(statement, pos)
|
|
108
|
+
ensure_node(target_id, target_body)
|
|
109
|
+
if len(model.edges) >= max_edges:
|
|
110
|
+
raise FlowchartParseError(f"连线数超过上限 {max_edges}")
|
|
111
|
+
model.edges.append(FlowchartEdge(
|
|
112
|
+
source=source_id, target=target_id, label=label,
|
|
113
|
+
style=style, arrow=True, # type: ignore[arg-type]
|
|
114
|
+
))
|
|
115
|
+
source_id = target_id
|
|
116
|
+
model.nodes = list(nodes.values())
|
|
117
|
+
if not model.nodes:
|
|
118
|
+
raise FlowchartParseError("没有找到节点")
|
|
119
|
+
return model
|