Skip to content

Commit 27895e9

Browse files
deepseek-assistantPlanteAmigor
authored andcommitted
feat: 聊天终端支持多行输入(Enter 换行,空行提交)
- Enter 在有内容时换行,空行时提交消息 - 修复 `</think>` 过滤初始状态(in_think 默认 False) - Backspace 删除换行符时光标正确上移 - `_total_width` 和 `_char_width` 正确处理 \n
1 parent 4178e44 commit 27895e9

3 files changed

Lines changed: 53 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ printf '问题1\n问题2\n' | ./ov-cli chat --model ./model-ov --mode pipe
183183
| `/help` | 帮助 |
184184
| `/exit` | 退出 |
185185

186+
> **多行输入**:Enter 换行,当前行空白时 Enter 提交消息。
187+
186188
**单次输出模式**`--mode once`):
187189

188190
| 参数 | 说明 |

README_EN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ Interactive terminal. Auto-detects model format (GenAI / Optimum), supports stre
181181
| `/help` | Help |
182182
| `/exit` | Exit |
183183

184+
> **Multiline input**: Enter inserts a newline; press Enter on an empty line to submit.
185+
184186
**Once mode** (`--mode once`):
185187

186188
| Option | Description |

ov_cli/chat.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Optimum 格式 (optimum-intel OVModelForVisualCausalLM): Gemma-4 等
77
"""
88

9-
import os, sys, time, json, signal, threading
9+
import os, sys, time, json, re, signal, threading
1010
import openvino as ov
1111
import openvino_genai as ov_genai
1212
from wcwidth import wcwidth, wcswidth
@@ -228,6 +228,8 @@ def run_pipe(ctx, reasoning=True, max_tokens=1024, temperature=0.7):
228228

229229
elapsed = time.time() - t0
230230
resp = str(result).strip()
231+
if not reasoning:
232+
resp = re.sub(r'</?think>', '', resp).strip()
231233
print(_json.dumps({"text": resp, "time": round(elapsed, 1)}, ensure_ascii=False), flush=True)
232234
except KeyboardInterrupt:
233235
pass
@@ -295,6 +297,8 @@ def _count_tokens(ctx, text):
295297

296298

297299
def _char_width(ch):
300+
if ch == "\n":
301+
return 0
298302
return max(wcwidth(ch), 1)
299303

300304

@@ -303,7 +307,11 @@ def _total_width(chars, start=0, end=None):
303307
end = len(chars)
304308
if start >= end:
305309
return 0
306-
return wcswidth("".join(chars[start:end]))
310+
# 只计算当前行(最后一个 \n 之后)的宽度
311+
s = "".join(chars[start:end])
312+
if "\n" in s:
313+
s = s.rsplit("\n", 1)[1]
314+
return wcswidth(s)
307315

308316

309317
def _move_cursor(delta):
@@ -356,6 +364,17 @@ def readline():
356364
if b == 4:
357365
break
358366
if b in (13, 10):
367+
# 取当前行(光标所在行,从上一个 \n 到光标)的内容
368+
before = "".join(buf[:char_pos])
369+
cur_line = before.rsplit("\n", 1)[1] if "\n" in before else before
370+
if cur_line: # 当前行有内容 → 换行继续输入
371+
buf.insert(char_pos, "\n")
372+
widths.insert(char_pos, 0)
373+
char_pos += 1
374+
_sys.stdout.write("\r\n")
375+
_sys.stdout.flush()
376+
continue
377+
# 当前行无内容 → 提交
359378
_sys.stdout.write("\r\n")
360379
_sys.stdout.flush()
361380
break
@@ -413,13 +432,24 @@ def readline():
413432
if b in (127, 8):
414433
if char_pos > 0:
415434
dw = _char_width(buf[char_pos - 1])
435+
is_nl = buf[char_pos - 1] == "\n"
416436
del buf[char_pos - 1]
417437
del widths[char_pos - 1]
418438
char_pos -= 1
419-
_move_cursor(-dw)
420-
tail = "".join(buf[char_pos:])
421-
_sys.stdout.write(tail + " ")
422-
_move_cursor(-_total_width(buf[char_pos:]) - 1)
439+
if is_nl:
440+
_sys.stdout.write("\033[A") # 光标上移一行
441+
# 清除当前行并重新绘制后续文本
442+
import shutil
443+
cols = shutil.get_terminal_size().columns
444+
_sys.stdout.write("\r\033[K")
445+
tail = "".join(buf[char_pos:])
446+
_sys.stdout.write(tail)
447+
_move_cursor(-_total_width(buf[char_pos:]))
448+
else:
449+
_move_cursor(-dw)
450+
tail = "".join(buf[char_pos:])
451+
_sys.stdout.write(tail + " ")
452+
_move_cursor(-_total_width(buf[char_pos:]) - 1)
423453
_sys.stdout.flush()
424454
continue
425455

@@ -644,6 +674,9 @@ def _prog():
644674

645675
reply_text = "".join(reply_parts)
646676

677+
if not reasoning:
678+
reply_text = re.sub(r'</?think>', '', reply_text).strip()
679+
647680
# 输出统计
648681
elapsed = time.time() - t0
649682
char_count = len(reply_text.replace(" ", ""))
@@ -1138,7 +1171,7 @@ def _show_progress():
11381171
thread.start()
11391172

11401173
thinking_filter = not reasoning
1141-
in_think = [thinking_filter]
1174+
in_think = [False] # 初始不在 think 块内,由 <think> 标签触发
11421175
reply_parts = []
11431176
_opt_first = [True]
11441177
try:
@@ -1178,6 +1211,15 @@ def _show_progress():
11781211
else:
11791212
in_think[0] = True
11801213
continue
1214+
# 过滤孤立的 </think>(没有配对的 <think>)
1215+
if '</think>' in t:
1216+
idx = t.index('</think>')
1217+
after = t[idx + 8:]
1218+
if after:
1219+
reply_parts.append(after)
1220+
sys.stdout.write(after)
1221+
sys.stdout.flush()
1222+
continue
11811223
sys.stdout.write(t)
11821224
sys.stdout.flush()
11831225
reply_parts.append(t)

0 commit comments

Comments
 (0)