ot-cli 0.2.1__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.
- openterminal/__init__.py +3 -0
- openterminal/agent/__init__.py +39 -0
- openterminal/agent/backend.py +180 -0
- openterminal/agent/build.py +115 -0
- openterminal/agent/middleware.py +111 -0
- openterminal/agent/model.py +41 -0
- openterminal/agent/prompt.py +63 -0
- openterminal/agent/runner.py +340 -0
- openterminal/agent/sysprobe.py +164 -0
- openterminal/agent/token_est.py +49 -0
- openterminal/app.py +52 -0
- openterminal/approval.py +7 -0
- openterminal/backend.py +3 -0
- openterminal/banner.py +7 -0
- openterminal/cli.py +426 -0
- openterminal/cmdset.py +7 -0
- openterminal/config.py +322 -0
- openterminal/connections/__init__.py +20 -0
- openterminal/connections/db.py +240 -0
- openterminal/connections/models.py +289 -0
- openterminal/connections/secrets.py +68 -0
- openterminal/connections_db.py +6 -0
- openterminal/core/__init__.py +41 -0
- openterminal/core/ai_flow.py +781 -0
- openterminal/core/auth.py +499 -0
- openterminal/core/cmdset_runner.py +352 -0
- openterminal/core/exec_events.py +650 -0
- openterminal/core/history.py +79 -0
- openterminal/core/hook_gate.py +415 -0
- openterminal/core/messages.py +193 -0
- openterminal/core/nested_ssh.py +178 -0
- openterminal/core/pipeline.py +1187 -0
- openterminal/core/pump.py +190 -0
- openterminal/core/transcript.py +26 -0
- openterminal/frontend/__init__.py +21 -0
- openterminal/frontend/approval.py +40 -0
- openterminal/frontend/banner.py +40 -0
- openterminal/frontend/headless.py +290 -0
- openterminal/frontend/render.py +41 -0
- openterminal/frontend/taskview.py +184 -0
- openterminal/frontend/term.py +1417 -0
- openterminal/headless.py +7 -0
- openterminal/history_db.py +7 -0
- openterminal/local_pty.py +3 -0
- openterminal/local_win.py +3 -0
- openterminal/policy.py +362 -0
- openterminal/render.py +7 -0
- openterminal/secrets_store.py +5 -0
- openterminal/session/__init__.py +17 -0
- openterminal/session/base.py +328 -0
- openterminal/session/local_posix.py +184 -0
- openterminal/session/local_win.py +173 -0
- openterminal/session/ssh.py +321 -0
- openterminal/shell/__init__.py +17 -0
- openterminal/shell/cmdset.py +49 -0
- openterminal/shell/integration.py +1298 -0
- openterminal/shell_integration.py +7 -0
- openterminal/shell_session.py +8 -0
- openterminal/skills/__init__.py +45 -0
- openterminal/skills/markdown-tables/SKILL.md +247 -0
- openterminal/ssh_pty.py +6 -0
- openterminal/sysprobe.py +7 -0
- openterminal/taskview.py +7 -0
- openterminal/term_frontend.py +7 -0
- openterminal/token_est.py +3 -0
- openterminal/transcript.py +7 -0
- openterminal/web/__init__.py +1 -0
- openterminal/web/files_api.py +789 -0
- openterminal/web/frontend/index.html +37 -0
- openterminal/web/frontend/static/app.js +3632 -0
- openterminal/web/frontend/static/block_stream.js +274 -0
- openterminal/web/frontend/static/block_view.js +383 -0
- openterminal/web/frontend/static/files_panel.js +782 -0
- openterminal/web/frontend/static/md_lite.js +127 -0
- openterminal/web/frontend/static/modal.js +122 -0
- openterminal/web/frontend/static/pad_ledger.js +46 -0
- openterminal/web/frontend/static/style.css +915 -0
- openterminal/web/frontend/static/ui/ui.css +1 -0
- openterminal/web/frontend/static/ui/ui.js +6292 -0
- openterminal/web/frontend/vendor/addon-fit.js +2 -0
- openterminal/web/frontend/vendor/xterm.css +8 -0
- openterminal/web/frontend/vendor/xterm.js +8 -0
- openterminal/web/protocol.py +41 -0
- openterminal/web/server.py +322 -0
- openterminal/web/worker.py +62 -0
- ot_cli-0.2.1.dist-info/METADATA +439 -0
- ot_cli-0.2.1.dist-info/RECORD +91 -0
- ot_cli-0.2.1.dist-info/WHEEL +5 -0
- ot_cli-0.2.1.dist-info/entry_points.txt +3 -0
- ot_cli-0.2.1.dist-info/licenses/LICENSE +202 -0
- ot_cli-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""连接后命令集(Task 6 自 PipelineCore 原样搬入)。
|
|
2
|
+
|
|
3
|
+
连接就绪/重连后按目标配置免交互串行执行命令集:输出尾窗判定、
|
|
4
|
+
静默窗、交互提示应答(密码/确认/verify code)、提示符身份核对、
|
|
5
|
+
hook 门禁等待、「继续」按钮强制放行。状态**物理存 core 实例**
|
|
6
|
+
(P3 裁定延续:pump/feed_msg/close 直读 core._cs_*;CmdsetRunner
|
|
7
|
+
__init__ 以 hasattr 守卫补建,预置值不覆盖),方法体经 self.core 读写。
|
|
8
|
+
|
|
9
|
+
CMDSET_* 时限常量物理在本模块;运行期经 _impl.CMDSET_* 解析——
|
|
10
|
+
tests 从 web.worker 经双跳补丁桥落到 pipeline dict,裸读模块全局
|
|
11
|
+
会漏补丁(auth/_PROBE_TIMEOUT 缝先例);正则常量无补丁方,裸读。
|
|
12
|
+
**唯一合法 patch 落点是 wmod/cmod(web.worker / shell.cmdset 经桥)**:
|
|
13
|
+
首个补丁周期后 pipeline.__dict__ 永久持有该常量影子,直接 patch
|
|
14
|
+
本模块的 CMDSET_* 实现端看不到。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
from . import pipeline as _impl
|
|
24
|
+
from ..shell import cmdset
|
|
25
|
+
from ..session.base import _needs_password_prompt
|
|
26
|
+
from .messages import PasswordCancelled, ServerMsg, _ANSI_RE, _dbg
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- 连接后命令集时序(模块级便于测试 monkeypatch 调小)---
|
|
30
|
+
CMDSET_ECHO_TIMEOUT = 2.0 # 等命令回显封顶(秒)
|
|
31
|
+
CMDSET_QUIET = 0.3 # 输出静默判定窗口(秒)
|
|
32
|
+
CMDSET_PROMPT_TIMEOUT = 10.0 # 等交互提示封顶(秒);超时跳过该应答行
|
|
33
|
+
CMDSET_RESUME_QUIET = 1.2 # 暂停等输入:提示消失后再静默此时长才继续(秒)
|
|
34
|
+
# 静默≠跑完:ssh/sudo 类握手期会静默数秒才吐交互提示(password:),静默判定把
|
|
35
|
+
# 这段空档当「命令跑完」,下一条命令就抢跑进它的 stdin。密码类命令静默后按
|
|
36
|
+
# CMDSET_PROMPT_TIMEOUT 继续观察:见交互提示即停下等应答、见新 shell 提示符
|
|
37
|
+
# 即判定跑完,两者都到才放行。非密码类命令不观察(不引入额外延迟)。
|
|
38
|
+
_CMDSET_PROMPT_RE = re.compile(
|
|
39
|
+
r"(password|passphrase|密码|口令)\s*[::]\s*$|\[sudo\]|yes/no|verification code",
|
|
40
|
+
re.IGNORECASE)
|
|
41
|
+
# 密码类与确认类提示分开:@引用是密码,只能应答密码类——确认类(yes/no)
|
|
42
|
+
# 会回显输入,密码打上去就明文漏进终端(真机:跳转时 host key 确认先弹)。
|
|
43
|
+
# 明文应答(如 yes/y)两类都应答。
|
|
44
|
+
_CMDSET_SECRET_RE = re.compile(
|
|
45
|
+
r"(password|passphrase|密码|口令)\s*[::]\s*$|\[sudo\]", re.IGNORECASE)
|
|
46
|
+
_CMDSET_CONFIRM_RE = re.compile(r"yes/no|verification code", re.IGNORECASE)
|
|
47
|
+
# 新 shell 提示符 = 命令真跑完(含嵌套 ssh/su - 登录后的新提示符)。
|
|
48
|
+
# 密码类提示不以 $/#/%/> 结尾,故与 _CMDSET_PROMPT_RE 互斥,不会互相误判。
|
|
49
|
+
_CMDSET_DONE_RE = re.compile(r"[$#%>]\s*$")
|
|
50
|
+
# 提示符身份(user@host):换壳(sudo su - / 嵌套 ssh 登录)的唯一廉价凭据。
|
|
51
|
+
# 命令集凭它决定发下一条前要不要先补注入——hook 标记是全会话新鲜度,
|
|
52
|
+
# 分不清标记来自哪个壳(su - 后外层标记仍新鲜 → _hook_ok 假阳性)。
|
|
53
|
+
_CS_PROMPT_ID_RE = re.compile(
|
|
54
|
+
r"^[\[({]?\s*([A-Za-z0-9._-]+)@([A-Za-z0-9._-]+)")
|
|
55
|
+
CMDSET_HOOK_WAIT = 20.0 # 等他人补注入窗口关闭的封顶(秒)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CmdsetRunner:
|
|
59
|
+
def __init__(self, core) -> None:
|
|
60
|
+
self.core = core
|
|
61
|
+
for name, val in (
|
|
62
|
+
("_cs_tail", None), # 命令集监听的输出滚动缓冲(bytearray|None)
|
|
63
|
+
("_cs_pw_pending", False), # 上一条命令是密码类:发下条前先确认未停在提示
|
|
64
|
+
("_cs_prompt_id", None), # 上一条命令落点提示符的 user@host
|
|
65
|
+
("_cs_task", None), # 命令集 runner 任务
|
|
66
|
+
("_cs_resume", None), # 「继续」按钮强制放行事件(core 预建优先)
|
|
67
|
+
):
|
|
68
|
+
if not hasattr(core, name):
|
|
69
|
+
setattr(core, name, val)
|
|
70
|
+
if not hasattr(core, "_cs_resume") or core._cs_resume is None:
|
|
71
|
+
core._cs_resume = asyncio.Event()
|
|
72
|
+
|
|
73
|
+
def _spawn_cmdset(self) -> None:
|
|
74
|
+
# 重复触发(重连/就绪)先取消在跑的旧 runner:否则双 runner 交错写
|
|
75
|
+
# PTY、同抢 _pws 队列(密码会随机路由给其中一个等待者)
|
|
76
|
+
if self.core._cs_task is not None and not self.core._cs_task.done():
|
|
77
|
+
self.core._cs_task.cancel()
|
|
78
|
+
self.core._cs_task = asyncio.create_task(self._run_command_set())
|
|
79
|
+
|
|
80
|
+
# --- 连接后命令集(spec 2026-09-24):免交互串行执行 ---
|
|
81
|
+
def _cs_text(self) -> str:
|
|
82
|
+
"""尾窗文本(剥 ANSI / 归一换行)——只喂正则判定,不用于显示。
|
|
83
|
+
|
|
84
|
+
不剥就判不出 shell 提示符回来了:PowerShell/ConPTY 的提示符后面跟着
|
|
85
|
+
光标显隐、颜色复位等转义序列,行尾判提示符的正则永远匹配不上,命令集每条
|
|
86
|
+
都要白等 _impl.CMDSET_PROMPT_TIMEOUT 才放行(真机浏览器复现:提示符 3.1s
|
|
87
|
+
就回来了,第 2 条命令 13.2s 才发出去;真机「要按 Enter 才刷新状态」
|
|
88
|
+
的迟滞感同源)。回显判定同理:PSReadLine 会把命令分词上色。
|
|
89
|
+
"""
|
|
90
|
+
raw = bytes(self.core._cs_tail or b"").decode("utf-8", "replace")
|
|
91
|
+
return _ANSI_RE.sub("", raw).replace("\r\n", "\n").replace("\r", "\n")
|
|
92
|
+
|
|
93
|
+
def _cs_lastline(self) -> str:
|
|
94
|
+
return self._cs_text().rsplit("\n", 1)[-1]
|
|
95
|
+
|
|
96
|
+
async def _cs_pause_for_input(self) -> None:
|
|
97
|
+
"""命令执行后终端停在交互提示、又没有应答行接管 → 暂停等用户输入。
|
|
98
|
+
|
|
99
|
+
用户直接在终端输(密码不回显),见到 shell 提示符后自动继续;前端
|
|
100
|
+
「继续」按钮(cmdset_resume)随时强制放行。输错密码导致提示重现时
|
|
101
|
+
保持暂停。
|
|
102
|
+
|
|
103
|
+
收束信号只能是 shell 提示符,不能是「提示文本消失了」——ssh/sudo
|
|
104
|
+
读完密码先回一个裸 CRLF(尾行空、提示正则不匹配),auth 还没出结果、
|
|
105
|
+
TTY 仍关回显,此刻放行下一条命令就射进无回显窗口:字符进 tty 排队、
|
|
106
|
+
登录后被执行,但一条回显都没有(真机:sudo su - / cd /root 从没上屏,
|
|
107
|
+
界面要按 Enter 才见到提示符)。
|
|
108
|
+
"""
|
|
109
|
+
if self.core._cs_pw_pending:
|
|
110
|
+
await self._cs_watch_prompt()
|
|
111
|
+
self.core._cs_pw_pending = False
|
|
112
|
+
if not _CMDSET_PROMPT_RE.search(self._cs_lastline()):
|
|
113
|
+
return
|
|
114
|
+
self.core._cs_resume.clear()
|
|
115
|
+
paused = False
|
|
116
|
+
gone_at = None
|
|
117
|
+
while not self.core._closed:
|
|
118
|
+
line = self._cs_lastline()
|
|
119
|
+
if _CMDSET_PROMPT_RE.search(line):
|
|
120
|
+
gone_at = None
|
|
121
|
+
if not paused:
|
|
122
|
+
paused = True
|
|
123
|
+
await self.core.emit_msg(ServerMsg(
|
|
124
|
+
type="cmdset", state="paused"))
|
|
125
|
+
try:
|
|
126
|
+
await asyncio.wait_for(self.core._cs_resume.wait(), 0.2)
|
|
127
|
+
return # 提示未消失也放行:用户明示继续
|
|
128
|
+
except asyncio.TimeoutError:
|
|
129
|
+
pass
|
|
130
|
+
continue
|
|
131
|
+
if _CMDSET_DONE_RE.search(line):
|
|
132
|
+
return # shell 提示符回来:命令真跑完
|
|
133
|
+
if gone_at is None:
|
|
134
|
+
gone_at = time.monotonic()
|
|
135
|
+
try:
|
|
136
|
+
await asyncio.wait_for(self.core._cs_resume.wait(), 0.1)
|
|
137
|
+
return # 用户点继续:强制放行
|
|
138
|
+
except asyncio.TimeoutError:
|
|
139
|
+
pass
|
|
140
|
+
if time.monotonic() - gone_at > _impl.CMDSET_PROMPT_TIMEOUT:
|
|
141
|
+
return # 超时兜底:不再卡住命令集
|
|
142
|
+
|
|
143
|
+
async def _run_command_set(self) -> None:
|
|
144
|
+
lines = [l for l in (self.core._target.commands if self.core._target else [])
|
|
145
|
+
if l.strip() and not l.lstrip().startswith("#")]
|
|
146
|
+
total = len(lines)
|
|
147
|
+
try:
|
|
148
|
+
for i, line in enumerate(lines, 1):
|
|
149
|
+
if self.core._closed:
|
|
150
|
+
return
|
|
151
|
+
await self.core.emit_msg(ServerMsg(
|
|
152
|
+
type="cmdset", state="running", index=i, total=total,
|
|
153
|
+
cmd="" if line.lstrip().startswith(">")
|
|
154
|
+
else line.strip()))
|
|
155
|
+
if line.lstrip().startswith(">"):
|
|
156
|
+
await self._cs_answer(line.strip()[1:].strip())
|
|
157
|
+
else:
|
|
158
|
+
await self._cs_pause_for_input()
|
|
159
|
+
await self._cs_command(line.strip())
|
|
160
|
+
# 末条命令可能停在交互提示上(如最后一条是 ssh):先按待输入暂停,
|
|
161
|
+
# 否则状态栏报「完成」而终端还卡在密码提示
|
|
162
|
+
await self._cs_pause_for_input()
|
|
163
|
+
await self.core.emit_msg(ServerMsg(
|
|
164
|
+
type="cmdset", state="done", index=total, total=total))
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
raise
|
|
167
|
+
except ConnectionError:
|
|
168
|
+
return # 弹窗等待期间连接关闭:静默退出,close 流程已接管
|
|
169
|
+
finally:
|
|
170
|
+
self.core._cs_tail = None # runner 退出即停缓冲(pump 钩子归零)
|
|
171
|
+
self.core._cs_pw_pending = False
|
|
172
|
+
|
|
173
|
+
async def _cs_command(self, cmd: str) -> None:
|
|
174
|
+
"""命令行:写入 PTY,等回显再等输出静默(自回显起封顶 ECHO_TIMEOUT)。
|
|
175
|
+
|
|
176
|
+
不在结束时清 tail:密码提示常与命令回显同批到达(快网络),
|
|
177
|
+
后续应答行要能继承这段输出继续匹配。
|
|
178
|
+
|
|
179
|
+
发命令前先过 _cs_hook_gate:换壳后不补注入就把命令打进裸壳,
|
|
180
|
+
回显没有青色重绘(hook 不在位);补注入窗口与发送并发则更糟——
|
|
181
|
+
窗口吞掉命令回显与新壳提示符(真机:sudo su - / cd /tmp / root
|
|
182
|
+
提示符全不上屏、光标悬空),分片还会被换壳拆进两个壳装配错乱。
|
|
183
|
+
"""
|
|
184
|
+
await self._cs_wait_pw_verify()
|
|
185
|
+
await self._cs_hook_gate()
|
|
186
|
+
self.core._cs_tail = bytearray()
|
|
187
|
+
# 密码类命令:发前置标记——密码提示随输出到达时 _pw_armed 需要
|
|
188
|
+
# 已能看到它(_cs_command 尾部再置一次是给 _cs_pause_for_input 用的,
|
|
189
|
+
# 那时提示符早已被 _detect_password_prompt 处理过)
|
|
190
|
+
self.core._cs_pw_pending = _needs_password_prompt(cmd)
|
|
191
|
+
await self.core.session.send_raw((cmd + "\r").encode())
|
|
192
|
+
deadline = time.monotonic() + _impl.CMDSET_ECHO_TIMEOUT
|
|
193
|
+
while time.monotonic() < deadline and cmd not in self._cs_text():
|
|
194
|
+
await asyncio.sleep(0.02)
|
|
195
|
+
if cmd not in self._cs_text() and not self.core._cs_tail:
|
|
196
|
+
await self.core.emit_bytes(cmd.encode() + bytes([13, 10]))
|
|
197
|
+
quiet_deadline = time.monotonic() + _impl.CMDSET_ECHO_TIMEOUT
|
|
198
|
+
last = -1
|
|
199
|
+
while time.monotonic() < quiet_deadline:
|
|
200
|
+
if len(self.core._cs_tail) != last:
|
|
201
|
+
last = len(self.core._cs_tail)
|
|
202
|
+
await asyncio.sleep(_impl.CMDSET_QUIET) # 有新字节:再等一个静默窗
|
|
203
|
+
else:
|
|
204
|
+
break # 静默期满:命令可能跑完
|
|
205
|
+
# 静默≠跑完:记下这条是不是密码类,交给 _cs_pause_for_input 在发下一条
|
|
206
|
+
# 前确认它没停在交互提示上(见 _cs_watch_prompt)
|
|
207
|
+
self.core._cs_pw_pending = _needs_password_prompt(cmd)
|
|
208
|
+
|
|
209
|
+
async def _cs_wait_pw_verify(self) -> None:
|
|
210
|
+
"""密码验证窗内不注入命令:TTY 关着回显,射进去的命令被执行却不上屏。
|
|
211
|
+
|
|
212
|
+
与用户按键(_on_keys)同一道闸,只是这里等而不是排队——命令集是自动
|
|
213
|
+
推进的,排进 _pw_hold 会和「先看提示再发」的时序打架。
|
|
214
|
+
"""
|
|
215
|
+
deadline = time.monotonic() + _impl._PW_VERIFY_TIMEOUT
|
|
216
|
+
while self.core._pw_verifying and not self.core._closed:
|
|
217
|
+
if time.monotonic() > deadline:
|
|
218
|
+
return
|
|
219
|
+
await asyncio.sleep(0.05)
|
|
220
|
+
|
|
221
|
+
def _cs_prompt_identity(self) -> str | None:
|
|
222
|
+
"""当前壳身份 user@host:尾窗末个非空行优先,尾窗全空退回 _cur_prompt。
|
|
223
|
+
|
|
224
|
+
必须是「末个非空行」而不是 rsplit 出来的末行:ssh / su 登录后的提示符
|
|
225
|
+
块常以 CR 或括号粘贴模式转义收尾再补换行,归一化后末行是空串 → 身份
|
|
226
|
+
判不出 → 门禁按「同壳」放行,下一条命令打进裸壳(真机 2026-10-08:
|
|
227
|
+
sudo su - 回显白色、无青色重绘)。只认末个非空行、不向上多扫:再往上
|
|
228
|
+
就是上一个壳的提示符,拿它记账会把换壳判成没换。
|
|
229
|
+
"""
|
|
230
|
+
for line in reversed(self._cs_text().split("\n")):
|
|
231
|
+
if not line.strip():
|
|
232
|
+
continue
|
|
233
|
+
m = _CS_PROMPT_ID_RE.match(line.strip())
|
|
234
|
+
return f"{m.group(1)}@{m.group(2)}" if m else None
|
|
235
|
+
# 尾窗一个非空行都没有(命令集第一条:连接后还没有泵输出进尾窗)→ 退回
|
|
236
|
+
# 路由最近一次 B 标记捕获的提示符。否则基线永远种不下:第二条命令时
|
|
237
|
+
# base is None → ubuntu→testuser 的换壳判不出来 → sudo su - 打进裸壳、
|
|
238
|
+
# 回显白色不青(真机 2026-10-08 run4:ident= None base= None)。
|
|
239
|
+
# 只在尾窗全空时退回:末个非空行是输出文本(Last login 等)时宁返回
|
|
240
|
+
# None——_cur_prompt 只在装了 hook 的壳里更新,拿它记账会把「刚跳进
|
|
241
|
+
# 裸壳」判成同壳;None 不改基线,下一次门禁仍判得出换壳。
|
|
242
|
+
m = _CS_PROMPT_ID_RE.match(self.core._cur_prompt.strip())
|
|
243
|
+
return f"{m.group(1)}@{m.group(2)}" if m else None
|
|
244
|
+
|
|
245
|
+
async def _cs_hook_gate(self) -> None:
|
|
246
|
+
"""命令集逐条前置门禁:注入在飞等窗口关;换壳/缺 hook 先补注入。
|
|
247
|
+
|
|
248
|
+
补注入的 _suppress_live 窗口吞窗口内全部泵输出——与命令集并发时
|
|
249
|
+
正好吞掉命令回显与新壳提示符(真机 2026-10-08:sudo su - 之后整屏
|
|
250
|
+
冻结、光标悬在旧提示符);分片飞行途中换壳(su -)还会把 base64 装配
|
|
251
|
+
拆进两个壳 → eval 语法错误 → hook 永远装不进嵌套壳(命令行不青)。
|
|
252
|
+
串行化后两症同解:窗口在飞只等不发;提示符身份变了先注入完再发,
|
|
253
|
+
嵌套壳 thereby 拿到 hook,回显青色重绘与提示符落屏都恢复。
|
|
254
|
+
"""
|
|
255
|
+
deadline = time.monotonic() + _impl.CMDSET_HOOK_WAIT
|
|
256
|
+
while self.core._suppress_live and not self.core._closed:
|
|
257
|
+
if time.monotonic() > deadline:
|
|
258
|
+
return # 注入卡死:不再等,命令照发(宁显不青不吞屏)
|
|
259
|
+
await asyncio.sleep(0.05)
|
|
260
|
+
if self.core._closed:
|
|
261
|
+
return
|
|
262
|
+
ident = self._cs_prompt_identity()
|
|
263
|
+
base = self.core._cs_prompt_id
|
|
264
|
+
if ident is not None:
|
|
265
|
+
self.core._cs_prompt_id = ident
|
|
266
|
+
# 首条命令没有基线可比(base is None):不算换壳。setup 注入刚落地就
|
|
267
|
+
# 再注一遍=白等 2~4s,还把实例号推进新号段(外层帧号空间白白作废)
|
|
268
|
+
changed = base is not None and ident is not None and ident != base
|
|
269
|
+
_dbg("cs gate: ident=", ident, "base=", base, "changed=", changed,
|
|
270
|
+
"hook_ok=", self.core.hook._hook_ok(), "hook_gone=", self.core._hook_gone,
|
|
271
|
+
"lastline=", repr(self._cs_lastline()[-40:]))
|
|
272
|
+
if not changed and (self.core.hook._hook_ok() or self.core._hook_gone):
|
|
273
|
+
return # 同壳且 hook 在位(或结构性不可集成)
|
|
274
|
+
try:
|
|
275
|
+
await self.core.hook._ensure_integrated()
|
|
276
|
+
except Exception: # noqa: BLE001 - 注入失败不阻断命令集
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
async def _cs_watch_prompt(self) -> None:
|
|
280
|
+
"""上一条是密码类命令时,发下一条前先确认它没停在交互提示上。
|
|
281
|
+
|
|
282
|
+
ssh/sudo 类握手期会静默数秒才吐 password:,静默判定把这段空档当成
|
|
283
|
+
「命令跑完」,下一条命令就抢跑进它的 stdin 被当下一条密码吃掉(真机:
|
|
284
|
+
ssh 该阻塞等密码,sudo su - / cd /root 却已回显,登录后又排队执行)。
|
|
285
|
+
|
|
286
|
+
见交互提示即返回(交 _cs_pause_for_input 暂停等应答);见新 shell 提示符
|
|
287
|
+
= 命令已结束(含 ssh/su - 登录后的新提示符)立即返回,零额外延迟。
|
|
288
|
+
"""
|
|
289
|
+
if not self.core._cs_tail:
|
|
290
|
+
return # 无输出证据可判:不做无谓等待
|
|
291
|
+
deadline = time.monotonic() + _impl.CMDSET_PROMPT_TIMEOUT
|
|
292
|
+
while time.monotonic() < deadline:
|
|
293
|
+
line = self._cs_lastline()
|
|
294
|
+
if _CMDSET_PROMPT_RE.search(line):
|
|
295
|
+
return # 交互提示在前台:停下等应答
|
|
296
|
+
if _CMDSET_DONE_RE.search(line):
|
|
297
|
+
return # 新 shell 提示符:命令已结束
|
|
298
|
+
await asyncio.sleep(0.05)
|
|
299
|
+
|
|
300
|
+
async def _cs_answer(self, ref: str) -> None:
|
|
301
|
+
"""应答行:等密码类提示出现后自动输入;10s 等不到则跳过(容错)。
|
|
302
|
+
|
|
303
|
+
@引用在密钥库缺失时首次学习:弹窗询问一次(必定记住,存
|
|
304
|
+
cmdset:<引用名>),本次立即用,之后连接自动取。
|
|
305
|
+
"""
|
|
306
|
+
if ref.startswith("@"):
|
|
307
|
+
name = ref[1:].strip()
|
|
308
|
+
answer = cmdset.resolve_answer(name)
|
|
309
|
+
pat = _CMDSET_SECRET_RE # 密码应答只认密码类提示(确认类会回显)
|
|
310
|
+
if answer is None:
|
|
311
|
+
answer = await self.core.auth.ask_password(
|
|
312
|
+
f"命令集:{name} 的密码(记住后下次自动填充)",
|
|
313
|
+
auth_kind="cmdset")
|
|
314
|
+
if not answer: # 用户空提交:放弃该应答行
|
|
315
|
+
await self.core.emit_msg(ServerMsg(
|
|
316
|
+
type="status",
|
|
317
|
+
text=f"命令集:@{name} 未输入密码,已跳过自动应答"))
|
|
318
|
+
return
|
|
319
|
+
_impl.store_password(f"cmdset:{name}", None, None, answer)
|
|
320
|
+
else:
|
|
321
|
+
answer = ref
|
|
322
|
+
pat = _CMDSET_PROMPT_RE
|
|
323
|
+
if self.core._cs_tail is None:
|
|
324
|
+
# 命令集以应答行开头(前面没有命令阶段)才新建缓冲;否则继承
|
|
325
|
+
# 命令阶段的输出——提示很可能已在那里等着
|
|
326
|
+
self.core._cs_tail = bytearray()
|
|
327
|
+
try:
|
|
328
|
+
deadline = time.monotonic() + _impl.CMDSET_PROMPT_TIMEOUT
|
|
329
|
+
while time.monotonic() < deadline:
|
|
330
|
+
if self.core._closed:
|
|
331
|
+
return
|
|
332
|
+
if pat.search(self._cs_text()):
|
|
333
|
+
await self.core.session.send_raw((answer + "\r").encode())
|
|
334
|
+
# 应答是静默的(密码不回显):旧提示符还挂在尾窗里,尾窗
|
|
335
|
+
# 重置成「应答之后的输出」,下一条才判得出 shell 提示符
|
|
336
|
+
# 何时回来;并记账发下一条前先等它回来,否则抢跑进仍在关
|
|
337
|
+
# 回显的 auth 窗口(真机:sudo su - / cd /root 不上屏)
|
|
338
|
+
self.core._cs_tail = bytearray()
|
|
339
|
+
self.core._cs_pw_pending = True
|
|
340
|
+
await asyncio.sleep(_impl.CMDSET_QUIET) # 密码不回显,静默即稳
|
|
341
|
+
return
|
|
342
|
+
await asyncio.sleep(0.05)
|
|
343
|
+
await self.core.emit_msg(ServerMsg(
|
|
344
|
+
type="status", text="命令集:未检测到交互提示,已跳过自动应答"))
|
|
345
|
+
finally:
|
|
346
|
+
# 不在这清 _cs_tail:应答后下一条要靠它判 shell 提示符何时回来,
|
|
347
|
+
# 清掉就无证据、_cs_watch_prompt 直接放行→抢跑进无回显窗口。
|
|
348
|
+
# 归零由 _cs_command 开头与 runner 退出时的 finally 负责。
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
# --- 组件公开接口绑定 ---
|
|
352
|
+
spawn = _spawn_cmdset
|