echotools 2.0.0__tar.gz → 2.3.0__tar.gz
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.
- {echotools-2.0.0 → echotools-2.3.0}/PKG-INFO +1 -1
- {echotools-2.0.0 → echotools-2.3.0}/pyproject.toml +1 -1
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/__init__.py +20 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/_version.py +1 -1
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/inject.py +1 -1
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/registry.py +8 -1
- echotools-2.3.0/src/echotools/io/__init__.py +37 -0
- echotools-2.3.0/src/echotools/io/printstream.py +387 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/protocol/base.py +10 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/__init__.py +5 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/local.py +88 -14
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/session.py +65 -1
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/ssh.py +1 -0
- echotools-2.3.0/src/echotools/terminal/tmux.py +105 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/middleware.py +2 -6
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/SOURCES.txt +3 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_fncall.py +13 -20
- echotools-2.3.0/tests/test_printstream.py +339 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_protocols.py +6 -6
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_web.py +3 -3
- echotools-2.0.0/src/echotools/io/__init__.py +0 -11
- {echotools-2.0.0 → echotools-2.3.0}/README.md +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/setup.cfg +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/list_cache.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/memory_cache.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/base.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/center.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/loader.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/merge.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/candidate.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/dispatcher.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/proxy_selector.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/selector.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/usage.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/base.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/classify.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/common.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/http.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/bus.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/event.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/files/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/files/file_util.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/stream.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/history.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/templates.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/protocols/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/protocols/entml.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/coercion.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/loop_detect.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/normalization.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/ids/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/ids/generator.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/io/io_utils.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/keys.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/manager.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/updater.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/logger/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/logger/manager.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/network/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/network/http_utils.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/base.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/discovery.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/registry.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/process/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/process/port.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/protocol/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/proxy/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/proxy/manager.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/py.typed +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/retry/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/retry/retry.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/runtime/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/runtime/collector.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/scheduler/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/scheduler/scheduler.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/sdk/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/sdk/facade.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/conpty.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/sanitize.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/context.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/span.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/tracer.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/translate.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/watcher/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/watcher/file_watcher.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/__init__.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/application.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/broker.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/app.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/file-zone/file-zone.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/input-box.css +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/input-box.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/motion-kit/motion-kit.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/send-button/send-button.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/sortable-list/sortable-list.css +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/sortable-list/sortable-list.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/style.css +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/text-input/text-input.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/voice-input/voice-input.js +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/stats.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/utils.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_cache_retry.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_completion.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_config.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_config_extended.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_coverage_core.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch_extended.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch_race.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_errors.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_errors_common.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_events.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_events_event.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_facade.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_ids.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_imports.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_io.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_keys.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_lifecycle.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_logger.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_merge.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_plugin.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_proxy.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_proxy_selector.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_runtime.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_scheduler.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_tracing.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_usage.py +0 -0
- {echotools-2.0.0 → echotools-2.3.0}/tests/test_web_broker.py +0 -0
|
@@ -80,6 +80,16 @@ __all__ = [
|
|
|
80
80
|
"TerminalCallback",
|
|
81
81
|
"LocalTerminal",
|
|
82
82
|
"SSHTerminal",
|
|
83
|
+
"PrintStream",
|
|
84
|
+
"print_stream",
|
|
85
|
+
"start_print_stream",
|
|
86
|
+
"stop_print_stream",
|
|
87
|
+
"flush_print_stream",
|
|
88
|
+
"get_buffer_size",
|
|
89
|
+
"get_queue_length",
|
|
90
|
+
"is_print_stream_running",
|
|
91
|
+
"set_print_speed",
|
|
92
|
+
"configure_print_stream",
|
|
83
93
|
"inject_fncall",
|
|
84
94
|
"FncallStreamParser",
|
|
85
95
|
"detect_tool_loop",
|
|
@@ -161,6 +171,16 @@ _EXPORTS: dict[str, tuple[str, str]] = {
|
|
|
161
171
|
"TerminalCallback": ("echotools.terminal.session", "TerminalCallback"),
|
|
162
172
|
"LocalTerminal": ("echotools.terminal.local", "LocalTerminal"),
|
|
163
173
|
"SSHTerminal": ("echotools.terminal.ssh", "SSHTerminal"),
|
|
174
|
+
"PrintStream": ("echotools.io.printstream", "PrintStream"),
|
|
175
|
+
"print_stream": ("echotools.io.printstream", "print_stream"),
|
|
176
|
+
"start_print_stream": ("echotools.io.printstream", "start_print_stream"),
|
|
177
|
+
"stop_print_stream": ("echotools.io.printstream", "stop_print_stream"),
|
|
178
|
+
"flush_print_stream": ("echotools.io.printstream", "flush_print_stream"),
|
|
179
|
+
"get_buffer_size": ("echotools.io.printstream", "get_buffer_size"),
|
|
180
|
+
"get_queue_length": ("echotools.io.printstream", "get_queue_length"),
|
|
181
|
+
"is_print_stream_running": ("echotools.io.printstream", "is_print_stream_running"),
|
|
182
|
+
"set_print_speed": ("echotools.io.printstream", "set_print_speed"),
|
|
183
|
+
"configure_print_stream": ("echotools.io.printstream", "configure_print_stream"),
|
|
164
184
|
"inject_fncall": ("echotools.fncall.prompt.inject", "inject_fncall"),
|
|
165
185
|
"FncallStreamParser": ("echotools.fncall.parsers.stream", "FncallStreamParser"),
|
|
166
186
|
"detect_tool_loop": ("echotools.fncall.shared.loop_detect", "detect_tool_loop"),
|
|
@@ -71,7 +71,7 @@ def inject_fncall(
|
|
|
71
71
|
if loop_detection_threshold > 0:
|
|
72
72
|
result = detect_tool_loop(normalized, loop_detection_threshold)
|
|
73
73
|
if result.is_looping:
|
|
74
|
-
logger.
|
|
74
|
+
logger.debug(
|
|
75
75
|
"检测到工具循环(%d 次)", result.repeat_count
|
|
76
76
|
)
|
|
77
77
|
loop_warning = result.suggestion
|
|
@@ -11,7 +11,7 @@ from echotools.protocol.base import (
|
|
|
11
11
|
get_protocol_by_id,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
__all__ = ["get_protocol", "list_protocols"]
|
|
14
|
+
__all__ = ["get_protocol", "list_protocols", "set_custom_protocol_factory", "clear_custom_protocol_factory"]
|
|
15
15
|
|
|
16
16
|
logger = get_logger(__name__)
|
|
17
17
|
|
|
@@ -29,6 +29,13 @@ def set_custom_protocol_factory(factory) -> None:
|
|
|
29
29
|
_custom_factory = factory
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def clear_custom_protocol_factory() -> None:
|
|
33
|
+
"""清除 custom 协议工厂与缓存实例(插件 on_unload 时调用)。"""
|
|
34
|
+
global _custom_factory, _custom_instance
|
|
35
|
+
_custom_factory = None
|
|
36
|
+
_custom_instance = None
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
def _get_custom_protocol(
|
|
33
40
|
prompt_en: str = "", prompt_zh: str = ""
|
|
34
41
|
) -> ToolProtocol:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""io 模块导出。"""
|
|
4
|
+
|
|
5
|
+
from echotools.io.io_utils import (
|
|
6
|
+
atomic_write_text,
|
|
7
|
+
ensure_directory,
|
|
8
|
+
read_text_if_exists,
|
|
9
|
+
)
|
|
10
|
+
from echotools.io.printstream import (
|
|
11
|
+
PrintStream,
|
|
12
|
+
configure_print_stream,
|
|
13
|
+
flush_print_stream,
|
|
14
|
+
get_buffer_size,
|
|
15
|
+
get_queue_length,
|
|
16
|
+
is_print_stream_running,
|
|
17
|
+
print_stream,
|
|
18
|
+
set_print_speed,
|
|
19
|
+
start_print_stream,
|
|
20
|
+
stop_print_stream,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"atomic_write_text",
|
|
25
|
+
"ensure_directory",
|
|
26
|
+
"read_text_if_exists",
|
|
27
|
+
"PrintStream",
|
|
28
|
+
"print_stream",
|
|
29
|
+
"start_print_stream",
|
|
30
|
+
"stop_print_stream",
|
|
31
|
+
"flush_print_stream",
|
|
32
|
+
"get_buffer_size",
|
|
33
|
+
"get_queue_length",
|
|
34
|
+
"is_print_stream_running",
|
|
35
|
+
"set_print_speed",
|
|
36
|
+
"configure_print_stream",
|
|
37
|
+
]
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Dynamic speed print stream system for controlled console output.
|
|
2
|
+
|
|
3
|
+
This module provides a PrintStream class that implements dynamic speed
|
|
4
|
+
printing with ordered queue management, suitable for controlled output
|
|
5
|
+
of large text blocks with adaptive speed based on queue depth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
import math
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from collections import deque
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PrintStream:
|
|
20
|
+
"""Dynamic speed print stream system with ordered queue.
|
|
21
|
+
|
|
22
|
+
This class manages a queue of text blocks and outputs them with
|
|
23
|
+
adaptive speed based on the current queue depth. The output speed
|
|
24
|
+
increases as more text is pending, providing smooth, controlled output.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
min_speed: Minimum output speed in characters per second.
|
|
28
|
+
max_speed: Maximum output speed in characters per second.
|
|
29
|
+
decay_factor: Controls how quickly speed adapts to queue depth.
|
|
30
|
+
smoothing_factor: Controls speed change smoothing (0.0-1.0).
|
|
31
|
+
current_speed: Current output speed in characters per second.
|
|
32
|
+
accumulated_chars: Fractional characters pending output.
|
|
33
|
+
total_pending_chars: Total characters waiting to be output.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
min_speed: float = 5.0,
|
|
39
|
+
max_speed: float = 100.0,
|
|
40
|
+
decay_factor: float = 20.0,
|
|
41
|
+
smoothing_factor: float = 0.8,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Initialize the print stream.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
min_speed: Minimum output speed (characters/second).
|
|
47
|
+
max_speed: Maximum output speed (characters/second).
|
|
48
|
+
decay_factor: Controls speed adaptation curve.
|
|
49
|
+
smoothing_factor: Controls speed change smoothing.
|
|
50
|
+
"""
|
|
51
|
+
# Queue management
|
|
52
|
+
self._text_queue: deque[str] = deque()
|
|
53
|
+
self._current_text: str = ""
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
# Thread management
|
|
57
|
+
self._running = False
|
|
58
|
+
self._started = False
|
|
59
|
+
self._output_thread: Optional[threading.Thread] = None
|
|
60
|
+
|
|
61
|
+
# Speed control parameters
|
|
62
|
+
self.min_speed = max(1.0, min_speed)
|
|
63
|
+
self.max_speed = max(self.min_speed, max_speed)
|
|
64
|
+
self.decay_factor = max(1.0, decay_factor)
|
|
65
|
+
self.smoothing_factor = max(0.1, min(0.99, smoothing_factor))
|
|
66
|
+
self.current_speed = self.min_speed
|
|
67
|
+
self.accumulated_chars = 0.0
|
|
68
|
+
|
|
69
|
+
# Statistics
|
|
70
|
+
self.total_pending_chars = 0
|
|
71
|
+
|
|
72
|
+
def start(self) -> None:
|
|
73
|
+
"""Start the print stream system."""
|
|
74
|
+
if not self._running and not self._started:
|
|
75
|
+
self._running = True
|
|
76
|
+
self._started = True
|
|
77
|
+
self._output_thread = threading.Thread(
|
|
78
|
+
target=self._output_processor, daemon=True
|
|
79
|
+
)
|
|
80
|
+
self._output_thread.start()
|
|
81
|
+
|
|
82
|
+
def stop(self) -> None:
|
|
83
|
+
"""Stop the print stream system.
|
|
84
|
+
|
|
85
|
+
Waits for all pending output to complete before stopping.
|
|
86
|
+
"""
|
|
87
|
+
if self._running:
|
|
88
|
+
self._running = False
|
|
89
|
+
# Wait for all content to be output
|
|
90
|
+
max_wait = 10.0
|
|
91
|
+
start_time = time.time()
|
|
92
|
+
while (self._current_text or self._text_queue) and (
|
|
93
|
+
time.time() - start_time
|
|
94
|
+
) < max_wait:
|
|
95
|
+
time.sleep(0.1)
|
|
96
|
+
|
|
97
|
+
if self._output_thread and self._output_thread.is_alive():
|
|
98
|
+
self._output_thread.join(timeout=1)
|
|
99
|
+
|
|
100
|
+
def add_to_buffer(self, text: str) -> None:
|
|
101
|
+
"""Add text to the output queue.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
text: Text to add to the queue.
|
|
105
|
+
"""
|
|
106
|
+
if not self._running:
|
|
107
|
+
self.start()
|
|
108
|
+
|
|
109
|
+
with self._lock:
|
|
110
|
+
self._text_queue.append(str(text))
|
|
111
|
+
self.total_pending_chars += len(str(text))
|
|
112
|
+
|
|
113
|
+
def flush_remaining(self) -> None:
|
|
114
|
+
"""Immediately output all remaining content."""
|
|
115
|
+
with self._lock:
|
|
116
|
+
# Output current text being processed
|
|
117
|
+
if self._current_text:
|
|
118
|
+
sys.stdout.write(self._current_text)
|
|
119
|
+
sys.stdout.flush()
|
|
120
|
+
self._current_text = ""
|
|
121
|
+
|
|
122
|
+
# Output all text in queue
|
|
123
|
+
while self._text_queue:
|
|
124
|
+
text = self._text_queue.popleft()
|
|
125
|
+
sys.stdout.write(text)
|
|
126
|
+
sys.stdout.flush()
|
|
127
|
+
|
|
128
|
+
self.total_pending_chars = 0
|
|
129
|
+
self.accumulated_chars = 0.0
|
|
130
|
+
|
|
131
|
+
def _calculate_dynamic_speed(self, buffer_length: int) -> float:
|
|
132
|
+
"""Calculate dynamic output speed based on buffer length.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
buffer_length: Number of pending characters.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Calculated speed in characters per second.
|
|
139
|
+
"""
|
|
140
|
+
if buffer_length <= 0:
|
|
141
|
+
return self.min_speed
|
|
142
|
+
|
|
143
|
+
# Combined exponential and logarithmic function
|
|
144
|
+
exp_component = 1 - math.exp(-buffer_length / self.decay_factor)
|
|
145
|
+
log_component = math.log(1 + buffer_length) / math.log(
|
|
146
|
+
1 + self.decay_factor
|
|
147
|
+
)
|
|
148
|
+
combined_factor = (
|
|
149
|
+
2 * exp_component * log_component / (exp_component + log_component + 1e-6)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Calculate target speed
|
|
153
|
+
target_speed = self.min_speed + (self.max_speed - self.min_speed) * combined_factor
|
|
154
|
+
|
|
155
|
+
# Smooth speed changes
|
|
156
|
+
smooth_speed = (
|
|
157
|
+
self.smoothing_factor * self.current_speed
|
|
158
|
+
+ (1 - self.smoothing_factor) * target_speed
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self.current_speed = smooth_speed
|
|
162
|
+
return smooth_speed
|
|
163
|
+
|
|
164
|
+
def _output_processor(self) -> None:
|
|
165
|
+
"""Background thread for processing output queue."""
|
|
166
|
+
last_update_time = time.time()
|
|
167
|
+
|
|
168
|
+
while self._running or self._current_text or self._text_queue:
|
|
169
|
+
try:
|
|
170
|
+
current_time = time.time()
|
|
171
|
+
time_delta = current_time - last_update_time
|
|
172
|
+
last_update_time = current_time
|
|
173
|
+
|
|
174
|
+
with self._lock:
|
|
175
|
+
# If no current text, get next from queue
|
|
176
|
+
if not self._current_text and self._text_queue:
|
|
177
|
+
self._current_text = self._text_queue.popleft()
|
|
178
|
+
|
|
179
|
+
# If there's text to output
|
|
180
|
+
if self._current_text:
|
|
181
|
+
# Calculate dynamic speed based on total pending chars
|
|
182
|
+
dynamic_speed = self._calculate_dynamic_speed(
|
|
183
|
+
self.total_pending_chars
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Calculate characters to output this iteration
|
|
187
|
+
chars_to_output = dynamic_speed * time_delta + self.accumulated_chars
|
|
188
|
+
actual_chars = int(chars_to_output)
|
|
189
|
+
self.accumulated_chars = chars_to_output - actual_chars
|
|
190
|
+
|
|
191
|
+
# Output characters
|
|
192
|
+
if actual_chars > 0:
|
|
193
|
+
chars_to_print = min(actual_chars, len(self._current_text))
|
|
194
|
+
to_print = self._current_text[:chars_to_print]
|
|
195
|
+
self._current_text = self._current_text[chars_to_print:]
|
|
196
|
+
|
|
197
|
+
# Update total pending chars
|
|
198
|
+
self.total_pending_chars = max(
|
|
199
|
+
0, self.total_pending_chars - chars_to_print
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Output to console
|
|
203
|
+
sys.stdout.write(to_print)
|
|
204
|
+
sys.stdout.flush()
|
|
205
|
+
|
|
206
|
+
# Brief sleep to control update frequency
|
|
207
|
+
time.sleep(0.02) # 50Hz update rate
|
|
208
|
+
|
|
209
|
+
except Exception:
|
|
210
|
+
# Error handling: output remaining content directly
|
|
211
|
+
if self._current_text:
|
|
212
|
+
sys.stdout.write(self._current_text)
|
|
213
|
+
sys.stdout.flush()
|
|
214
|
+
self._current_text = ""
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def buffer_size(self) -> int:
|
|
218
|
+
"""Get total characters waiting to be output.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Number of pending characters.
|
|
222
|
+
"""
|
|
223
|
+
with self._lock:
|
|
224
|
+
queue_chars = sum(len(text) for text in self._text_queue)
|
|
225
|
+
return len(self._current_text) + queue_chars
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def is_running(self) -> bool:
|
|
229
|
+
"""Check if the system is running.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if running, False otherwise.
|
|
233
|
+
"""
|
|
234
|
+
return self._running
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def queue_length(self) -> int:
|
|
238
|
+
"""Get number of text blocks in queue.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Number of queued text blocks.
|
|
242
|
+
"""
|
|
243
|
+
with self._lock:
|
|
244
|
+
return len(self._text_queue)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Global instance
|
|
248
|
+
_global_print_stream = PrintStream()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def print_stream(
|
|
252
|
+
*args: Any,
|
|
253
|
+
sep: str = " ",
|
|
254
|
+
end: str = "\n",
|
|
255
|
+
flush: bool = False,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Dynamic speed print function.
|
|
258
|
+
|
|
259
|
+
This function provides a drop-in replacement for the built-in print()
|
|
260
|
+
function with dynamic speed output based on queue depth.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
*args: Content to print.
|
|
264
|
+
sep: Separator between arguments (default: space).
|
|
265
|
+
end: End character (default: newline).
|
|
266
|
+
flush: If True, output immediately without queuing.
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
# Ensure system is started
|
|
270
|
+
if not _global_print_stream.is_running:
|
|
271
|
+
_global_print_stream.start()
|
|
272
|
+
|
|
273
|
+
# Combine output content
|
|
274
|
+
text = sep.join(str(arg) for arg in args) + end
|
|
275
|
+
|
|
276
|
+
if flush:
|
|
277
|
+
# Output immediately
|
|
278
|
+
sys.stdout.write(text)
|
|
279
|
+
sys.stdout.flush()
|
|
280
|
+
else:
|
|
281
|
+
# Add to queue
|
|
282
|
+
_global_print_stream.add_to_buffer(text)
|
|
283
|
+
|
|
284
|
+
except Exception:
|
|
285
|
+
# Fallback to standard print on error
|
|
286
|
+
print(*args, sep=sep, end=end)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def start_print_stream() -> None:
|
|
290
|
+
"""Manually start the print stream system."""
|
|
291
|
+
_global_print_stream.start()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def stop_print_stream() -> None:
|
|
295
|
+
"""Stop the print stream system."""
|
|
296
|
+
_global_print_stream.stop()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def flush_print_stream() -> None:
|
|
300
|
+
"""Immediately output all buffered content."""
|
|
301
|
+
_global_print_stream.flush_remaining()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_buffer_size() -> int:
|
|
305
|
+
"""Get current buffer size.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Number of pending characters.
|
|
309
|
+
"""
|
|
310
|
+
return _global_print_stream.buffer_size
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_queue_length() -> int:
|
|
314
|
+
"""Get number of text blocks in queue.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Number of queued text blocks.
|
|
318
|
+
"""
|
|
319
|
+
return _global_print_stream.queue_length
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def is_print_stream_running() -> bool:
|
|
323
|
+
"""Check if the print stream system is running.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if running, False otherwise.
|
|
327
|
+
"""
|
|
328
|
+
return _global_print_stream.is_running
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def set_print_speed(min_speed: float = 5.0, max_speed: float = 50.0) -> None:
|
|
332
|
+
"""Set print speed range.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
min_speed: Minimum print speed (characters/second).
|
|
336
|
+
max_speed: Maximum print speed (characters/second).
|
|
337
|
+
"""
|
|
338
|
+
_global_print_stream.min_speed = max(1.0, min_speed)
|
|
339
|
+
_global_print_stream.max_speed = max(_global_print_stream.min_speed, max_speed)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def configure_print_stream(
|
|
343
|
+
min_speed: float = 5.0,
|
|
344
|
+
max_speed: float = 50.0,
|
|
345
|
+
decay_factor: float = 20.0,
|
|
346
|
+
smoothing_factor: float = 0.8,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Configure print stream system parameters.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
min_speed: Minimum print speed.
|
|
352
|
+
max_speed: Maximum print speed.
|
|
353
|
+
decay_factor: Decay factor for speed adaptation.
|
|
354
|
+
smoothing_factor: Smoothing factor for speed changes.
|
|
355
|
+
"""
|
|
356
|
+
_global_print_stream.min_speed = max(1.0, min_speed)
|
|
357
|
+
_global_print_stream.max_speed = max(_global_print_stream.min_speed, max_speed)
|
|
358
|
+
_global_print_stream.decay_factor = max(1.0, decay_factor)
|
|
359
|
+
_global_print_stream.smoothing_factor = max(0.1, min(0.99, smoothing_factor))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _cleanup() -> None:
|
|
363
|
+
"""Cleanup function registered with atexit."""
|
|
364
|
+
try:
|
|
365
|
+
_global_print_stream.flush_remaining()
|
|
366
|
+
_global_print_stream.stop()
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Register cleanup function
|
|
372
|
+
atexit.register(_cleanup)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Module exports
|
|
376
|
+
__all__ = [
|
|
377
|
+
"PrintStream",
|
|
378
|
+
"print_stream",
|
|
379
|
+
"start_print_stream",
|
|
380
|
+
"stop_print_stream",
|
|
381
|
+
"flush_print_stream",
|
|
382
|
+
"get_buffer_size",
|
|
383
|
+
"get_queue_length",
|
|
384
|
+
"is_print_stream_running",
|
|
385
|
+
"set_print_speed",
|
|
386
|
+
"configure_print_stream",
|
|
387
|
+
]
|
|
@@ -10,6 +10,7 @@ from echotools.logger.manager import get_logger
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"ToolProtocol",
|
|
12
12
|
"register_protocol",
|
|
13
|
+
"unregister_protocol",
|
|
13
14
|
"get_protocol_by_id",
|
|
14
15
|
"list_protocols",
|
|
15
16
|
"VALID_PROTOCOL_IDS",
|
|
@@ -104,6 +105,15 @@ def register_protocol(protocol: ToolProtocol) -> None:
|
|
|
104
105
|
logger.debug("已注册协议: %s", protocol.id)
|
|
105
106
|
|
|
106
107
|
|
|
108
|
+
def unregister_protocol(protocol_id: str) -> bool:
|
|
109
|
+
"""注销协议实现;内置 entml 等 Host 保留协议不应移除。"""
|
|
110
|
+
if protocol_id not in _PROTOCOL_REGISTRY:
|
|
111
|
+
return False
|
|
112
|
+
del _PROTOCOL_REGISTRY[protocol_id]
|
|
113
|
+
logger.debug("已注销协议: %s", protocol_id)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
107
117
|
def get_protocol_by_id(protocol_id: str) -> ToolProtocol:
|
|
108
118
|
"""按 ID 获取协议。"""
|
|
109
119
|
if protocol_id not in _PROTOCOL_REGISTRY:
|
|
@@ -14,17 +14,22 @@ from .sanitize import sanitize_terminal_history_chunk
|
|
|
14
14
|
from .session import (
|
|
15
15
|
MAX_HISTORY_LINES,
|
|
16
16
|
MAX_OFFLINE_BUFFER_BYTES,
|
|
17
|
+
MAX_SEQ_RING_BYTES,
|
|
17
18
|
TerminalCallback,
|
|
18
19
|
TerminalSession,
|
|
19
20
|
)
|
|
20
21
|
from .ssh import SSHTerminal
|
|
22
|
+
from .tmux import TmuxTerminal, tmux_available
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
23
25
|
"TerminalSession",
|
|
24
26
|
"TerminalCallback",
|
|
25
27
|
"LocalTerminal",
|
|
26
28
|
"SSHTerminal",
|
|
29
|
+
"TmuxTerminal",
|
|
30
|
+
"tmux_available",
|
|
27
31
|
"MAX_OFFLINE_BUFFER_BYTES",
|
|
32
|
+
"MAX_SEQ_RING_BYTES",
|
|
28
33
|
"MAX_HISTORY_LINES",
|
|
29
34
|
"sanitize_terminal_history_chunk",
|
|
30
35
|
]
|