From 0a1c30f6695b466ee671c106f8edaaed9359ab87 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 5 Dec 2025 21:54:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86AI=E5=9B=9E=E5=A4=8D=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/Network/KBStreamFetcher.m | 7 ++ .../View/Function/KBStreamOverlayView.m | 2 +- CustomKeyboard/View/KBStreamTextView.m | 117 ++++++++++++++---- 3 files changed, 102 insertions(+), 24 deletions(-) diff --git a/CustomKeyboard/Network/KBStreamFetcher.m b/CustomKeyboard/Network/KBStreamFetcher.m index 822b05e..9a1a636 100644 --- a/CustomKeyboard/Network/KBStreamFetcher.m +++ b/CustomKeyboard/Network/KBStreamFetcher.m @@ -288,6 +288,13 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) { - (void)emitChunk:(NSString *)rawText { if (rawText.length == 0) return; + // 调试:在任何处理之前打印后端“原始文本分片”,便于对照排查 + if (self.loggingEnabled) { + NSLog(@"[KBStream] RAW chunk#%ld len=%lu text=\"%@\"", + (long)(self.emittedChunkCount + 1), + (unsigned long)rawText.length, + KBPrintableSnippet(rawText, 160)); + } NSString *text = rawText; // 0) 规范化换行与段起始:去掉位于片段开头的 \r/\n;将 "\n\t"、"\r\n\t"、"\r\t" 归一为 "\t" text = [text stringByReplacingOccurrencesOfString:@"\r\n\t" withString:@"\t"]; diff --git a/CustomKeyboard/View/Function/KBStreamOverlayView.m b/CustomKeyboard/View/Function/KBStreamOverlayView.m index 9261753..03f7c7c 100644 --- a/CustomKeyboard/View/Function/KBStreamOverlayView.m +++ b/CustomKeyboard/View/Function/KBStreamOverlayView.m @@ -51,7 +51,7 @@ del.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.35]; del.layer.cornerRadius = 14; del.layer.masksToBounds = YES; del.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; - [del setTitle:KBLocalized(@"Delete") forState:UIControlStateNormal]; + [del setTitle:KBLocalized(@"common_back") forState:UIControlStateNormal]; [del setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [del addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside]; _closeButton = del; diff --git a/CustomKeyboard/View/KBStreamTextView.m b/CustomKeyboard/View/KBStreamTextView.m index caf3f6c..0f50d1a 100644 --- a/CustomKeyboard/View/KBStreamTextView.m +++ b/CustomKeyboard/View/KBStreamTextView.m @@ -16,6 +16,8 @@ @property (nonatomic, strong) UIScrollView *scrollView; // 已创建的标签集合(顺序即显示顺序) @property (nonatomic, strong) NSMutableArray *labels; +// 每一段对应的“原始文本”(不含前缀编号),与 labels 一一对应 +@property (nonatomic, strong) NSMutableArray *segmentTexts; // 文本缓冲:保存尚未遇到分隔符的尾部文本 @property (nonatomic, copy) NSString *buffer; @@ -40,14 +42,12 @@ - (void)commonInit { _delimiter = @"\t"; _labelFont = [UIFont systemFontOfSize:16.0]; - if (@available(iOS 13.0, *)) { - _labelTextColor = [UIColor labelColor]; - } else { - _labelTextColor = [UIColor blackColor]; - } + // 统一使用主题色 #02BEAC 作为段落文字颜色 + _labelTextColor = [UIColor colorWithRed:2.0/255.0 green:190.0/255.0 blue:172.0/255.0 alpha:1.0]; _contentHorizontalPadding = 12.0; _interItemSpacing = 5.0; // 标签之间的垂直间距 5pt _labels = [NSMutableArray array]; + _segmentTexts = [NSMutableArray array]; _buffer = @""; _shouldTrimSegments = YES; @@ -95,7 +95,9 @@ static inline NSString *KBTrimRight(NSString *s) { if (self.delimiter.length == 0) { [self ensureCurrentLabelExists]; self.buffer = [self.buffer stringByAppendingString:text]; - self.labels.lastObject.text = self.buffer; + // 更新当前段落的展示文本(带编号前缀) + NSUInteger idx = (self.labels.count > 0) ? (self.labels.count - 1) : 0; + [self setContent:self.buffer forSegmentAtIndex:idx]; [self layoutLabelsForCurrentWidth]; [self scrollToBottomIfNeeded]; return; @@ -109,23 +111,31 @@ static inline NSString *KBTrimRight(NSString *s) { // 1) 先把第一段拼接到当前缓冲并实时显示 self.buffer = [self.buffer stringByAppendingString:parts.firstObject ?: @""]; - self.labels.lastObject.text = self.buffer; // 不裁剪,保留实时输入 + // 不裁剪,保留实时输入;更新当前段落展示(附带编号前缀) + NSUInteger currentIndex = (self.labels.count > 0) ? (self.labels.count - 1) : 0; + [self setContent:self.buffer forSegmentAtIndex:currentIndex]; [self layoutLabelsForCurrentWidth]; // 2) 处理每一个分隔符:完成当前段(仅裁剪“末尾空白”)并新建空标签,然后填入下一段的内容 for (NSUInteger i = 1; i < parts.count; i++) { // a) 完成当前段:对外观进行最终裁剪(若开启) - UILabel *current = self.labels.lastObject; - if (self.shouldTrimSegments) { current.text = KBTrimRight(current.text ?: @""); } + NSString *currentText = (self.segmentTexts.count > currentIndex) + ? (self.segmentTexts[currentIndex] ?: @"") + : (self.buffer ?: @""); + if (self.shouldTrimSegments) { + currentText = KBTrimRight(currentText ?: @""); + } + [self setContent:currentText forSegmentAtIndex:currentIndex]; [self layoutLabelsForCurrentWidth]; // b) 新建一个空标签,代表下一个段(即刻创建,哪怕下一段当前为空) [self createEmptyLabelAsNewSegment]; + currentIndex = (self.labels.count > 0) ? (self.labels.count - 1) : 0; // c) 将该分隔符之后的这段文本作为新段的初始内容(仍旧实时显示,不裁剪) - NSString *piece = parts[i]; - self.buffer = piece ?: @""; - self.labels.lastObject.text = self.buffer; + NSString *piece = parts[i] ?: @""; + self.buffer = piece; + [self setContent:self.buffer forSegmentAtIndex:currentIndex]; [self layoutLabelsForCurrentWidth]; } @@ -137,6 +147,7 @@ static inline NSString *KBTrimRight(NSString *s) { [lbl removeFromSuperview]; } [self.labels removeAllObjects]; + [self.segmentTexts removeAllObjects]; self.buffer = @""; self.scrollView.contentSize = CGSizeMake(self.bounds.size.width, 0); } @@ -151,7 +162,9 @@ static inline NSString *KBTrimRight(NSString *s) { label.font = self.labelFont; label.textColor = self.labelTextColor; label.userInteractionEnabled = YES; // 允许点击 - label.text = text; + NSUInteger idx = self.labels.count; + NSString *content = text ?: @""; + label.text = [self displayTextForSegmentIndex:idx content:content]; // 添加点击手势 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)]; @@ -159,6 +172,13 @@ static inline NSString *KBTrimRight(NSString *s) { [self.scrollView addSubview:label]; [self.labels addObject:label]; + // 维护原始文本数组,与 labels 一一对应 + if (self.segmentTexts.count <= idx) { + while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; } + [self.segmentTexts addObject:content]; + } else { + self.segmentTexts[idx] = content; + } // 依据当前宽度立即布局 [self layoutLabelsForCurrentWidth]; @@ -182,12 +202,21 @@ static inline NSString *KBTrimRight(NSString *s) { label.font = self.labelFont; label.textColor = self.labelTextColor; label.userInteractionEnabled = YES; - label.text = @""; + NSUInteger idx = self.labels.count; + // 初始为空内容,仅展示编号前缀(例如“1: ”) + label.text = [self displayTextForSegmentIndex:idx content:@""]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)]; [label addGestureRecognizer:tap]; [self.scrollView addSubview:label]; [self.labels addObject:label]; + // 保证 segmentTexts 与 labels 对齐 + if (self.segmentTexts.count <= idx) { + while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; } + [self.segmentTexts addObject:@""]; + } else { + self.segmentTexts[idx] = @""; + } self.buffer = @""; [self layoutLabelsForCurrentWidth]; @@ -201,13 +230,16 @@ static inline NSString *KBTrimRight(NSString *s) { }); return; } - UILabel *current = self.labels.lastObject; - if (!current) { return; } + if (self.labels.count == 0) { return; } + NSUInteger idx = self.labels.count - 1; + NSString *content = (self.segmentTexts.count > idx) + ? (self.segmentTexts[idx] ?: @"") + : (self.buffer ?: @""); if (self.shouldTrimSegments) { - NSString *trimmed = KBTrimRight(current.text ?: @""); - current.text = trimmed; - self.buffer = trimmed; + content = KBTrimRight(content ?: @""); } + [self setContent:content forSegmentAtIndex:idx]; + self.buffer = content; // 无论是否裁剪,都重新布局并滚动到底部,避免最后一段显示不全 [self layoutLabelsForCurrentWidth]; [self scrollToBottomIfNeeded]; @@ -271,9 +303,12 @@ static inline NSString *KBTrimRight(NSString *s) { UILabel *label = (UILabel *)tap.view; if (![label isKindOfClass:[UILabel class]]) { return; } NSInteger index = [self.labels indexOfObject:label]; - NSString *text = label.text ?: @""; + // 对外与插入宿主输入框时使用“原始内容”(不含编号前缀) + NSString *rawText = (index != NSNotFound && index < (NSInteger)self.segmentTexts.count) + ? (self.segmentTexts[(NSUInteger)index] ?: @"") + : (label.text ?: @""); if (index != NSNotFound && self.onLabelTap) { - self.onLabelTap(index, text); + self.onLabelTap(index, rawText); } // 将文本发送到当前宿主应用的输入框(搜索框/TextView 等) @@ -281,10 +316,46 @@ static inline NSString *KBTrimRight(NSString *s) { UIInputViewController *ivc = KBFindInputViewController(self); if (ivc) { id proxy = ivc.textDocumentProxy; - if (text.length > 0 && [proxy conformsToProtocol:@protocol(UITextDocumentProxy)]) { - [proxy insertText:text]; + if (rawText.length > 0 && [proxy conformsToProtocol:@protocol(UITextDocumentProxy)]) { + [proxy insertText:rawText]; } } } +#pragma mark - Segment Helpers + +// 根据段索引与原始内容生成展示文本(带 1 起始的编号前缀) +- (NSString *)displayTextForSegmentIndex:(NSUInteger)index content:(NSString *)content { + NSInteger displayIndex = (NSInteger)index + 1; + // 统一把内部的换行折叠为单个空格,避免同一段里出现“断行”的视觉效果 + NSString *body = content ?: @""; + body = [body stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"]; + // 将连续换行压缩为单个换行 + while ([body containsString:@"\n\n"]) { + body = [body stringByReplacingOccurrencesOfString:@"\n\n" withString:@"\n"]; + } + // 最终把所有换行替换成空格 + body = [body stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + + if (body.length > 0) { + return [NSString stringWithFormat:@"%ld: %@", (long)displayIndex, body]; + } else { + return [NSString stringWithFormat:@"%ld: ", (long)displayIndex]; + } +} + +// 统一更新某一段的“原始内容”和对应 label 的展示文本 +- (void)setContent:(NSString *)content forSegmentAtIndex:(NSUInteger)idx { + if (idx >= self.labels.count) { return; } + NSString *body = content ?: @""; + if (self.segmentTexts.count <= idx) { + while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; } + [self.segmentTexts addObject:body]; + } else { + self.segmentTexts[idx] = body; + } + UILabel *label = self.labels[idx]; + label.text = [self displayTextForSegmentIndex:idx content:body]; +} + @end