logtap 0.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.
- logtap/__init__.py +7 -0
- logtap/cli.py +54 -0
- logtap/read/__init__.py +8 -0
- logtap/read/app.py +266 -0
- logtap/read/disguise.py +141 -0
- logtap/read/log_style_render.py +269 -0
- logtap/read/reader_core.py +305 -0
- logtap/read/storage.py +196 -0
- logtap/read/text_splitter.py +79 -0
- logtap/read/themes.py +217 -0
- logtap-0.1.0.dist-info/METADATA +194 -0
- logtap-0.1.0.dist-info/RECORD +16 -0
- logtap-0.1.0.dist-info/WHEEL +5 -0
- logtap-0.1.0.dist-info/entry_points.txt +2 -0
- logtap-0.1.0.dist-info/licenses/LICENSE +21 -0
- logtap-0.1.0.dist-info/top_level.txt +1 -0
logtap/__init__.py
ADDED
logtap/cli.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""logtap 主命令行入口,负责解析子命令并分发。
|
|
2
|
+
|
|
3
|
+
当前仅有 read 子命令;后续新增其他 logtap 功能时,在此文件追加新的
|
|
4
|
+
subparser 即可,read 相关实现始终保持在 logtap.read 包内部,不与
|
|
5
|
+
其他子命令耦合。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from logtap import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="logtap",
|
|
20
|
+
description="logtap: 跨平台终端工具箱",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--version", action="version", version=f"logtap {__version__}"
|
|
24
|
+
)
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
26
|
+
|
|
27
|
+
read_parser = subparsers.add_parser(
|
|
28
|
+
"read", help="纯终端小说(.txt)阅读器"
|
|
29
|
+
)
|
|
30
|
+
read_parser.add_argument(
|
|
31
|
+
"path",
|
|
32
|
+
nargs="?",
|
|
33
|
+
default=None,
|
|
34
|
+
help="要打开的 .txt 文件路径;留空则在当前目录选择",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return parser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
41
|
+
parser = _build_parser()
|
|
42
|
+
args = parser.parse_args(argv)
|
|
43
|
+
|
|
44
|
+
if args.command == "read":
|
|
45
|
+
from logtap.read import run as run_reader
|
|
46
|
+
|
|
47
|
+
return run_reader(args.path)
|
|
48
|
+
|
|
49
|
+
parser.print_help()
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
sys.exit(main())
|
logtap/read/__init__.py
ADDED
logtap/read/app.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Textual 主应用:简化版日志风格小说阅读器。
|
|
2
|
+
|
|
3
|
+
核心交互:
|
|
4
|
+
- w/s 翻页
|
|
5
|
+
- ↑/↓ 滚动
|
|
6
|
+
- g 跳转行号
|
|
7
|
+
- m 添加/移除书签
|
|
8
|
+
- t 切换主题
|
|
9
|
+
- q 退出保存
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from typing import List, Optional, Set
|
|
15
|
+
|
|
16
|
+
from textual.app import App, ComposeResult
|
|
17
|
+
from textual.binding import Binding
|
|
18
|
+
from textual.containers import VerticalScroll
|
|
19
|
+
from textual.reactive import reactive
|
|
20
|
+
from textual.widgets import Footer
|
|
21
|
+
|
|
22
|
+
from logtap.read import themes
|
|
23
|
+
from logtap.read.log_style_render import LogStyleText
|
|
24
|
+
from logtap.read.reader_core import Document
|
|
25
|
+
from logtap.read.storage import Storage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NovelReaderApp(App):
|
|
29
|
+
"""日志风格小说阅读器 Textual 应用(简化版)。"""
|
|
30
|
+
|
|
31
|
+
CSS = themes.LOG_THEME_CSS
|
|
32
|
+
|
|
33
|
+
BINDINGS = [
|
|
34
|
+
Binding("up,k", "scroll_up", "向上", show=True),
|
|
35
|
+
Binding("down,j", "scroll_down", "向下", show=True),
|
|
36
|
+
Binding("w", "page_up", "上一页", show=True),
|
|
37
|
+
Binding("s", "page_down", "下一页", show=True),
|
|
38
|
+
Binding("g", "goto_line", "跳转", show=True),
|
|
39
|
+
Binding("m", "toggle_bookmark", "书签", show=True),
|
|
40
|
+
Binding("d", "cycle_disguise", "伪装", show=True),
|
|
41
|
+
Binding("t", "cycle_theme", "主题", show=True),
|
|
42
|
+
Binding("q", "quit_app", "退出", show=True),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# 响应式状态
|
|
46
|
+
theme_key: reactive[str] = reactive("log")
|
|
47
|
+
disguise_mode: reactive[str] = reactive("log") # "log" / "ai" / "off"
|
|
48
|
+
current_line: reactive[int] = reactive(0)
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
novel_path: str,
|
|
53
|
+
storage: Storage,
|
|
54
|
+
**kwargs,
|
|
55
|
+
):
|
|
56
|
+
super().__init__(**kwargs)
|
|
57
|
+
self.novel_path = novel_path
|
|
58
|
+
self.storage = storage
|
|
59
|
+
self.document: Optional[Document] = None
|
|
60
|
+
self.text_widget: Optional[LogStyleText] = None
|
|
61
|
+
self.scroll_container: Optional[VerticalScroll] = None
|
|
62
|
+
self.bookmarks: Set[int] = set()
|
|
63
|
+
self._lines: List[tuple] = []
|
|
64
|
+
self._lines_per_page = 20 # 每页显示行数
|
|
65
|
+
|
|
66
|
+
def compose(self) -> ComposeResult:
|
|
67
|
+
"""构建 UI 组件树。"""
|
|
68
|
+
with VerticalScroll(id="text-container") as scroll:
|
|
69
|
+
self.scroll_container = scroll
|
|
70
|
+
self.text_widget = LogStyleText(id="log-text")
|
|
71
|
+
yield self.text_widget
|
|
72
|
+
yield Footer()
|
|
73
|
+
|
|
74
|
+
def on_mount(self) -> None:
|
|
75
|
+
"""应用启动时加载文档并恢复进度。"""
|
|
76
|
+
try:
|
|
77
|
+
self.document = Document(self.novel_path)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.exit(message=f"文件加载失败: {e}")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# 恢复进度和书签
|
|
83
|
+
record = self.storage.get_book_record(self.novel_path)
|
|
84
|
+
self.current_line = record.progress.line_index
|
|
85
|
+
self.bookmarks = {bm.line_index for bm in record.bookmarks}
|
|
86
|
+
|
|
87
|
+
# 恢复主题
|
|
88
|
+
saved_theme = self.storage.settings.theme
|
|
89
|
+
if saved_theme in themes.THEME_KEYS:
|
|
90
|
+
self.theme_key = saved_theme
|
|
91
|
+
self.CSS = themes.get_theme_css(saved_theme)
|
|
92
|
+
|
|
93
|
+
# 加载全部文本行
|
|
94
|
+
self._load_text_lines()
|
|
95
|
+
self._render_current_page()
|
|
96
|
+
|
|
97
|
+
# 启动定时更新状态行
|
|
98
|
+
self.set_interval(5.0, self._update_status_line)
|
|
99
|
+
|
|
100
|
+
def _load_text_lines(self) -> None:
|
|
101
|
+
"""从 Document 加载所有行,标记类型(章节/正文/空行)。
|
|
102
|
+
|
|
103
|
+
性能优化:不预加载全部内容到内存,只构建行索引。
|
|
104
|
+
实际渲染时才调用 document.get_line() 按需读取。
|
|
105
|
+
"""
|
|
106
|
+
lines = []
|
|
107
|
+
chapter_starts = {ch.start_line for ch in self.document.chapters}
|
|
108
|
+
# 只构建元数据,不读取实际内容
|
|
109
|
+
for i in range(self.document.line_count):
|
|
110
|
+
if i in chapter_starts:
|
|
111
|
+
lines.append(("chapter", i)) # 存储行号而非内容
|
|
112
|
+
else:
|
|
113
|
+
lines.append(("text", i)) # 存储行号而非内容
|
|
114
|
+
self._lines = lines
|
|
115
|
+
|
|
116
|
+
def _render_current_page(self) -> None:
|
|
117
|
+
"""渲染当前页的文本行(按需读取内容,不预加载)。"""
|
|
118
|
+
if not self.text_widget or not self.document:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
start = max(0, self.current_line)
|
|
122
|
+
end = min(len(self._lines), start + self._lines_per_page)
|
|
123
|
+
|
|
124
|
+
# 按需读取当前页的实际内容
|
|
125
|
+
page_lines = []
|
|
126
|
+
for line_type, line_index in self._lines[start:end]:
|
|
127
|
+
raw_content = self.document.get_line(line_index)
|
|
128
|
+
if line_type == "chapter":
|
|
129
|
+
page_lines.append(("chapter", raw_content))
|
|
130
|
+
elif raw_content.strip() == "":
|
|
131
|
+
page_lines.append(("empty", ""))
|
|
132
|
+
else:
|
|
133
|
+
page_lines.append(("text", raw_content))
|
|
134
|
+
|
|
135
|
+
self.text_widget.set_lines(page_lines)
|
|
136
|
+
self.text_widget.set_bookmarks(self.bookmarks)
|
|
137
|
+
self.text_widget.set_disguise_mode(self.disguise_mode) # 同步伪装模式
|
|
138
|
+
self._update_status_line()
|
|
139
|
+
|
|
140
|
+
def _update_status_line(self) -> None:
|
|
141
|
+
"""更新状态栏,显示进度/章节/时间/伪装模式。"""
|
|
142
|
+
if not self.document or not self.text_widget:
|
|
143
|
+
return
|
|
144
|
+
progress = (self.current_line / max(self.document.line_count, 1)) * 100
|
|
145
|
+
chapter = self.document.chapter_at_line(self.current_line)
|
|
146
|
+
chapter_name = chapter.title if chapter else "N/A"
|
|
147
|
+
time_str = time.strftime("%H:%M:%S")
|
|
148
|
+
|
|
149
|
+
# 伪装模式标签
|
|
150
|
+
mode_labels = {"log": "日志模式", "ai": "AI模式", "off": "关闭"}
|
|
151
|
+
mode_label = mode_labels.get(self.disguise_mode, "未知")
|
|
152
|
+
|
|
153
|
+
status = (
|
|
154
|
+
f"Progress: {progress:.1f}% | Chapter: {chapter_name} | "
|
|
155
|
+
f"Line: {self.current_line + 1}/{self.document.line_count} | "
|
|
156
|
+
f"Disguise: {mode_label} | Time: {time_str}"
|
|
157
|
+
)
|
|
158
|
+
self.text_widget.set_status(status)
|
|
159
|
+
|
|
160
|
+
# ----------------------------------------------------------------
|
|
161
|
+
# 快捷键动作
|
|
162
|
+
# ----------------------------------------------------------------
|
|
163
|
+
def action_scroll_up(self) -> None:
|
|
164
|
+
"""向上滚动一行。"""
|
|
165
|
+
if self.current_line > 0:
|
|
166
|
+
self.current_line -= 1
|
|
167
|
+
self._render_current_page()
|
|
168
|
+
|
|
169
|
+
def action_scroll_down(self) -> None:
|
|
170
|
+
"""向下滚动一行。"""
|
|
171
|
+
if self.current_line < self.document.line_count - 1:
|
|
172
|
+
self.current_line += 1
|
|
173
|
+
self._render_current_page()
|
|
174
|
+
|
|
175
|
+
def action_page_up(self) -> None:
|
|
176
|
+
"""向上翻页。"""
|
|
177
|
+
self.current_line = max(0, self.current_line - self._lines_per_page)
|
|
178
|
+
self._render_current_page()
|
|
179
|
+
|
|
180
|
+
def action_page_down(self) -> None:
|
|
181
|
+
"""向下翻页。"""
|
|
182
|
+
self.current_line = min(
|
|
183
|
+
self.document.line_count - 1,
|
|
184
|
+
self.current_line + self._lines_per_page
|
|
185
|
+
)
|
|
186
|
+
self._render_current_page()
|
|
187
|
+
|
|
188
|
+
def action_goto_line(self) -> None:
|
|
189
|
+
"""跳转到指定行号(简化:直接通知输入提示,不弹窗)。"""
|
|
190
|
+
# 移除复杂的 push_screen 弹窗,改用通知提示用户
|
|
191
|
+
self.notify("跳转功能:输入 'g数字' 如 'g100' 跳转到第100行(开发中)", severity="information")
|
|
192
|
+
|
|
193
|
+
def _do_goto(self, value: str) -> None:
|
|
194
|
+
"""执行跳转(保留接口,暂不使用)。"""
|
|
195
|
+
if value.isdigit():
|
|
196
|
+
line_num = int(value) - 1
|
|
197
|
+
if 0 <= line_num < self.document.line_count:
|
|
198
|
+
self.current_line = line_num
|
|
199
|
+
self._render_current_page()
|
|
200
|
+
self.notify(f"已跳转到行 {line_num + 1}")
|
|
201
|
+
|
|
202
|
+
def action_toggle_bookmark(self) -> None:
|
|
203
|
+
"""在当前行添加/移除书签。"""
|
|
204
|
+
if self.current_line in self.bookmarks:
|
|
205
|
+
self.bookmarks.remove(self.current_line)
|
|
206
|
+
self.storage.get_book_record(self.novel_path).bookmarks = [
|
|
207
|
+
bm for bm in self.storage.get_book_record(self.novel_path).bookmarks
|
|
208
|
+
if bm.line_index != self.current_line
|
|
209
|
+
]
|
|
210
|
+
self.storage.save()
|
|
211
|
+
self.notify(f"已移除书签 (行 {self.current_line + 1})")
|
|
212
|
+
else:
|
|
213
|
+
self.bookmarks.add(self.current_line)
|
|
214
|
+
self.storage.add_bookmark(self.novel_path, self.current_line, "")
|
|
215
|
+
self.notify(f"已添加书签 (行 {self.current_line + 1})")
|
|
216
|
+
self._render_current_page()
|
|
217
|
+
|
|
218
|
+
def action_cycle_theme(self) -> None:
|
|
219
|
+
"""循环切换主题。"""
|
|
220
|
+
self.theme_key = themes.next_theme_key(self.theme_key)
|
|
221
|
+
new_css = themes.get_theme_css(self.theme_key)
|
|
222
|
+
self.stylesheet.parse(new_css)
|
|
223
|
+
self.storage.set_theme(self.theme_key)
|
|
224
|
+
label = themes.get_theme_label(self.theme_key)
|
|
225
|
+
self.notify(f"主题: {label}")
|
|
226
|
+
|
|
227
|
+
def action_cycle_disguise(self) -> None:
|
|
228
|
+
"""循环切换伪装模式:日志 → AI → 关闭 → 日志。"""
|
|
229
|
+
modes = ["log", "ai", "off"]
|
|
230
|
+
try:
|
|
231
|
+
idx = modes.index(self.disguise_mode)
|
|
232
|
+
except ValueError:
|
|
233
|
+
idx = 0
|
|
234
|
+
self.disguise_mode = modes[(idx + 1) % len(modes)]
|
|
235
|
+
|
|
236
|
+
mode_labels = {"log": "日志模式", "ai": "AI助手模式", "off": "关闭伪装"}
|
|
237
|
+
self.notify(f"伪装: {mode_labels[self.disguise_mode]}")
|
|
238
|
+
self._render_current_page() # 重新渲染以应用新模式
|
|
239
|
+
|
|
240
|
+
def action_quit_app(self) -> None:
|
|
241
|
+
"""退出并保存进度。"""
|
|
242
|
+
if self.document:
|
|
243
|
+
self.storage.save_progress(
|
|
244
|
+
self.novel_path, self.current_line, self.document.encoding
|
|
245
|
+
)
|
|
246
|
+
self.exit()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run(path: Optional[str] = None) -> int:
|
|
250
|
+
"""logtap read 子命令入口(Textual 版本)。"""
|
|
251
|
+
if path is None:
|
|
252
|
+
print("请指定要打开的 .txt 文件路径")
|
|
253
|
+
return 1
|
|
254
|
+
|
|
255
|
+
if not os.path.exists(path):
|
|
256
|
+
print(f"文件不存在: {path}")
|
|
257
|
+
return 1
|
|
258
|
+
|
|
259
|
+
if not path.lower().endswith(".txt"):
|
|
260
|
+
print("仅支持 .txt 文本文件")
|
|
261
|
+
return 1
|
|
262
|
+
|
|
263
|
+
storage = Storage()
|
|
264
|
+
app = NovelReaderApp(novel_path=os.path.abspath(path), storage=storage)
|
|
265
|
+
app.run()
|
|
266
|
+
return 0
|
logtap/read/disguise.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""伪装模式渲染逻辑:日志模式 / AI助手模式。
|
|
2
|
+
|
|
3
|
+
核心设计:
|
|
4
|
+
- 日志模式:强制分段 + 每行时间戳 + 日志级别 + 高频噪音(40%)
|
|
5
|
+
- AI模式:连续thinking/diff块 + 段落化输出 + 高频噪音(50%)
|
|
6
|
+
- 真实感优先:模拟实际工作场景的输出特征
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
from typing import List, Tuple
|
|
13
|
+
|
|
14
|
+
# 模拟系统日志噪音库(日志模式)
|
|
15
|
+
LOG_NOISE_TEMPLATES = [
|
|
16
|
+
"[INFO] Database connection pool: 8/20 active",
|
|
17
|
+
"[DEBUG] Cache hit ratio: {:.1f}%",
|
|
18
|
+
"[INFO] Request processed in {:.2f}ms",
|
|
19
|
+
"[WARN] Memory usage: {}MB / 2048MB",
|
|
20
|
+
"[DEBUG] Session cleanup completed",
|
|
21
|
+
"[INFO] Background task scheduler running",
|
|
22
|
+
"[DEBUG] API rate limit: {}/1000 requests",
|
|
23
|
+
"[INFO] Health check passed",
|
|
24
|
+
"[WARN] Slow query detected: {}ms",
|
|
25
|
+
"[DEBUG] Connection from {}",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# 模拟AI思考/状态噪音库(AI模式)
|
|
29
|
+
AI_NOISE_TEMPLATES = [
|
|
30
|
+
"<thinking>分析需求中...</thinking>",
|
|
31
|
+
"<thinking>检查代码结构...</thinking>",
|
|
32
|
+
"# 生成实现方案",
|
|
33
|
+
"# 验证逻辑正确性",
|
|
34
|
+
"[assistant] 好的,我来帮你实现这个功能。",
|
|
35
|
+
"[user] 这段代码有什么问题?",
|
|
36
|
+
"```diff",
|
|
37
|
+
"// 重构后的实现",
|
|
38
|
+
"/* 优化性能 */",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# 随机IP地址池
|
|
42
|
+
IP_POOL = ["192.168.1.{}".format(i) for i in range(10, 50)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def generate_timestamp() -> str:
|
|
46
|
+
"""生成当前时间戳(日志格式)。"""
|
|
47
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def generate_log_noise() -> str:
|
|
51
|
+
"""生成一条随机系统日志噪音。"""
|
|
52
|
+
template = random.choice(LOG_NOISE_TEMPLATES)
|
|
53
|
+
if "{}" in template or "{:" in template:
|
|
54
|
+
# 填充随机数据
|
|
55
|
+
if "Cache hit" in template:
|
|
56
|
+
return template.format(random.uniform(75.0, 95.0))
|
|
57
|
+
elif "processed in" in template:
|
|
58
|
+
return template.format(random.uniform(5.0, 50.0))
|
|
59
|
+
elif "Memory usage" in template:
|
|
60
|
+
return template.format(random.randint(512, 1800))
|
|
61
|
+
elif "rate limit" in template:
|
|
62
|
+
return template.format(random.randint(100, 800))
|
|
63
|
+
elif "Slow query" in template:
|
|
64
|
+
return template.format(random.randint(200, 800))
|
|
65
|
+
elif "Connection from" in template:
|
|
66
|
+
return template.format(random.choice(IP_POOL))
|
|
67
|
+
return template
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_ai_noise() -> str:
|
|
71
|
+
"""生成一条随机AI助手状态/思考噪音。"""
|
|
72
|
+
return random.choice(AI_NOISE_TEMPLATES)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def should_insert_noise(probability: float = 0.4) -> bool:
|
|
76
|
+
"""根据概率决定是否插入噪音行(提升到40%)。"""
|
|
77
|
+
return random.random() < probability
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def should_add_timestamp(probability: float = 1.0) -> bool:
|
|
81
|
+
"""根据概率决定是否给正文行加时间戳(日志模式必加)。"""
|
|
82
|
+
return random.random() < probability
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def should_render_as_diff(probability: float = 0.5) -> bool:
|
|
86
|
+
"""根据概率决定是否将正文渲染成diff格式(AI模式提升到50%)。"""
|
|
87
|
+
return random.random() < probability
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_random_log_level() -> Tuple[str, str]:
|
|
91
|
+
"""随机返回一个日志级别标签及其颜色样式。
|
|
92
|
+
|
|
93
|
+
返回: (标签, Rich样式)
|
|
94
|
+
"""
|
|
95
|
+
levels = [
|
|
96
|
+
("[INFO]", "blue"),
|
|
97
|
+
("[DEBUG]", "dim cyan"),
|
|
98
|
+
("[WARN]", "yellow"),
|
|
99
|
+
("[ERROR]", "red"),
|
|
100
|
+
]
|
|
101
|
+
weights = [0.5, 0.3, 0.15, 0.05] # INFO最常见,ERROR最少
|
|
102
|
+
choice = random.choices(levels, weights=weights, k=1)[0]
|
|
103
|
+
return choice
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def wrap_as_diff_line(content: str, is_add: bool) -> Tuple[str, str]:
|
|
107
|
+
"""将正文包装成diff格式。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
content: 原始文本
|
|
111
|
+
is_add: True=新增行(+绿色), False=删除行(-红色)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
(prefix, style) 例如 ("+ ", "green") 或 ("- ", "red")
|
|
115
|
+
"""
|
|
116
|
+
if is_add:
|
|
117
|
+
return ("+ ", "green")
|
|
118
|
+
else:
|
|
119
|
+
return ("- ", "red")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def insert_ai_thinking_block(lines: List[Tuple[str, str]], index: int) -> None:
|
|
123
|
+
"""在指定位置插入一条AI思考链路噪音。
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
lines: 待渲染的行列表 [(line_type, content), ...]
|
|
127
|
+
index: 插入位置
|
|
128
|
+
"""
|
|
129
|
+
thinking = generate_ai_noise()
|
|
130
|
+
lines.insert(index, ("ai_noise", thinking))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def insert_log_noise_line(lines: List[Tuple[str, str]], index: int) -> None:
|
|
134
|
+
"""在指定位置插入一条系统日志噪音。
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
lines: 待渲染的行列表 [(line_type, content), ...]
|
|
138
|
+
index: 插入位置
|
|
139
|
+
"""
|
|
140
|
+
log_line = generate_log_noise()
|
|
141
|
+
lines.insert(index, ("log_noise", log_line))
|