diff --git a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json index d4b1584..9581a88 100644 --- a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json +++ b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json @@ -4,15 +4,79 @@ "idiom" : "universal", "scale" : "1x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, { "filename" : "kb_del_icon@2x.png", "idiom" : "universal", "scale" : "2x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "kb_del_icon@2x 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "切图 256@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, { "filename" : "kb_del_icon@3x.png", "idiom" : "universal", "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "kb_del_icon@3x 1.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "切图 256@3x.png", + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x 1.png b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x 1.png new file mode 100644 index 0000000..3f36532 Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x 1.png differ diff --git a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x 1.png b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x 1.png new file mode 100644 index 0000000..5cc16ee Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x 1.png differ diff --git a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@2x.png b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@2x.png new file mode 100644 index 0000000..2975a21 Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@2x.png differ diff --git a/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@3x.png b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@3x.png new file mode 100644 index 0000000..78bb147 Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@3x.png differ diff --git a/CustomKeyboard/View/KBFunctionTagCell.m b/CustomKeyboard/View/KBFunctionTagCell.m index 9950b0d..93d04d2 100644 --- a/CustomKeyboard/View/KBFunctionTagCell.m +++ b/CustomKeyboard/View/KBFunctionTagCell.m @@ -6,118 +6,125 @@ // #import "KBFunctionTagCell.h" +#import "KBFunctionView.h" #import "Masonry.h" @interface KBFunctionTagCell () -@property (nonatomic, strong) UILabel *emojiLabel; -@property (nonatomic, strong) UILabel *titleLabelInternal; -@property (nonatomic, strong) UIActivityIndicatorView *loadingView; +@property(nonatomic, strong) UILabel *emojiLabel; +@property(nonatomic, strong) UILabel *titleLabelInternal; +@property(nonatomic, strong) UIActivityIndicatorView *loadingView; @end @implementation KBFunctionTagCell - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - self.contentView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9]; - self.contentView.layer.cornerRadius = 12; - self.contentView.layer.masksToBounds = YES; + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [KBFunctionView kb_cellBackgroundColor]; + self.contentView.layer.cornerRadius = 12; + self.contentView.layer.masksToBounds = YES; - // 小菊花:默认隐藏,放在整体内容右侧偏内的位置 - [self.contentView addSubview:self.loadingView]; - [self.loadingView mas_makeConstraints:^(MASConstraintMaker *make) { - make.center.equalTo(self.contentView); - make.width.height.mas_equalTo(16); - }]; + // 小菊花:默认隐藏,放在整体内容右侧偏内的位置 + [self.contentView addSubview:self.loadingView]; + [self.loadingView mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.contentView); + make.width.height.mas_equalTo(16); + }]; - // 中心容器:将 icon + title 组合整体水平居中 - UIView *centerContainer = [[UIView alloc] init]; - centerContainer.backgroundColor = [UIColor clearColor]; - [self.contentView addSubview:centerContainer]; - [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.centerX.equalTo(self.contentView.mas_centerX); - make.centerY.equalTo(self.contentView.mas_centerY); - make.left.greaterThanOrEqualTo(self.contentView.mas_left).offset(6); - make.right.lessThanOrEqualTo(self.contentView).offset(-6); - }]; + // 中心容器:将 icon + title 组合整体水平居中 + UIView *centerContainer = [[UIView alloc] init]; + centerContainer.backgroundColor = [UIColor clearColor]; + [self.contentView addSubview:centerContainer]; + [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.contentView.mas_centerX); + make.centerY.equalTo(self.contentView.mas_centerY); + make.left.greaterThanOrEqualTo(self.contentView.mas_left).offset(6); + make.right.lessThanOrEqualTo(self.contentView).offset(-6); + }]; - [centerContainer addSubview:self.emojiLabel]; - [centerContainer addSubview:self.titleLabelInternal]; + [centerContainer addSubview:self.emojiLabel]; + [centerContainer addSubview:self.titleLabelInternal]; - [self.emojiLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(centerContainer.mas_left); - make.centerY.equalTo(centerContainer.mas_centerY); - // 留出一点余量,避免 emoji 字形在右侧被裁剪 - make.width.height.mas_equalTo(24); - }]; - [self.titleLabelInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.emojiLabel.mas_right).offset(3); - make.top.equalTo(centerContainer.mas_top); - make.bottom.equalTo(centerContainer.mas_bottom); - make.right.equalTo(centerContainer.mas_right); - }]; - } - return self; + [self.emojiLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(centerContainer.mas_left); + make.centerY.equalTo(centerContainer.mas_centerY); + // 留出一点余量,避免 emoji 字形在右侧被裁剪 + make.width.height.mas_equalTo(24); + }]; + [self.titleLabelInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.emojiLabel.mas_right).offset(3); + make.top.equalTo(centerContainer.mas_top); + make.bottom.equalTo(centerContainer.mas_bottom); + make.right.equalTo(centerContainer.mas_right); + }]; + } + return self; } -- (void)setItemModel:(KBTagItemModel *)itemModel{ - _itemModel = itemModel; - self.emojiLabel.text = itemModel.emoji; - self.titleLabelInternal.text = itemModel.characterName; +- (void)setItemModel:(KBTagItemModel *)itemModel { + _itemModel = itemModel; + self.emojiLabel.text = itemModel.emoji; + self.titleLabelInternal.text = itemModel.characterName; } #pragma mark - Lazy - (UILabel *)emojiLabel { - if (!_emojiLabel) { - _emojiLabel = [[UILabel alloc] init]; - _emojiLabel.textAlignment = NSTextAlignmentCenter; - _emojiLabel.font = [KBFont medium:20]; - _emojiLabel.adjustsFontSizeToFitWidth = YES; - - } - return _emojiLabel; + if (!_emojiLabel) { + _emojiLabel = [[UILabel alloc] init]; + _emojiLabel.textAlignment = NSTextAlignmentCenter; + _emojiLabel.font = [KBFont medium:20]; + _emojiLabel.adjustsFontSizeToFitWidth = YES; + } + return _emojiLabel; } - (UILabel *)titleLabelInternal { - if (!_titleLabelInternal) { - _titleLabelInternal = [[UILabel alloc] init]; - _titleLabelInternal.font = [KBFont medium:10]; - _titleLabelInternal.textColor = [UIColor colorWithHex:0x1B1F1A]; - // 最多两行,文本过长时末尾截断 - _titleLabelInternal.numberOfLines = 2; - _titleLabelInternal.lineBreakMode = NSLineBreakByTruncatingTail; - } - return _titleLabelInternal; + if (!_titleLabelInternal) { + _titleLabelInternal = [[UILabel alloc] init]; + _titleLabelInternal.font = [KBFont medium:10]; + _titleLabelInternal.textColor = [KBFunctionView kb_cellTextColor]; + // 最多两行,文本过长时末尾截断 + _titleLabelInternal.numberOfLines = 2; + _titleLabelInternal.lineBreakMode = NSLineBreakByTruncatingTail; + } + return _titleLabelInternal; } #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 -static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleMedium; } +static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { + return UIActivityIndicatorViewStyleMedium; +} #else -static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleGray; } +static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { + return UIActivityIndicatorViewStyleGray; +} #endif - (UIActivityIndicatorView *)loadingView { - if (!_loadingView) { - _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:KBSpinnerStyle()]; - _loadingView.hidesWhenStopped = YES; - _loadingView.color = [UIColor grayColor]; - _loadingView.hidden = YES; - } - return _loadingView; + if (!_loadingView) { + _loadingView = [[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:KBSpinnerStyle()]; + _loadingView.hidesWhenStopped = YES; + _loadingView.color = [UIColor grayColor]; + _loadingView.hidden = YES; + } + return _loadingView; } #pragma mark - Expose -- (UILabel *)titleLabel { return self.titleLabelInternal; } +- (UILabel *)titleLabel { + return self.titleLabelInternal; +} - (void)setLoading:(BOOL)loading { - if (loading) { - self.loadingView.hidden = NO; - [self.loadingView startAnimating]; - } else { - [self.loadingView stopAnimating]; - self.loadingView.hidden = YES; - } + if (loading) { + self.loadingView.hidden = NO; + [self.loadingView startAnimating]; + } else { + [self.loadingView stopAnimating]; + self.loadingView.hidden = YES; + } } @end diff --git a/CustomKeyboard/View/KBFunctionView.h b/CustomKeyboard/View/KBFunctionView.h index f403171..726ff03 100644 --- a/CustomKeyboard/View/KBFunctionView.h +++ b/CustomKeyboard/View/KBFunctionView.h @@ -6,13 +6,16 @@ // #import -@class KBFunctionBarView, KBFunctionPasteView,KBFunctionView; +@class KBFunctionBarView, KBFunctionPasteView, KBFunctionView; @protocol KBFunctionViewDelegate @optional -- (void)functionView:(KBFunctionView *_Nullable)functionView didTapToolActionAtIndex:(NSInteger)index; -- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index; -- (void)functionViewDidRequestSubscription:(KBFunctionView *_Nullable)functionView; +- (void)functionView:(KBFunctionView *_Nullable)functionView + didTapToolActionAtIndex:(NSInteger)index; +- (void)functionView:(KBFunctionView *_Nullable)functionView + didRightTapToolActionAtIndex:(NSInteger)index; +- (void)functionViewDidRequestSubscription: + (KBFunctionView *_Nullable)functionView; @end @@ -21,24 +24,33 @@ NS_ASSUME_NONNULL_BEGIN /// 整个功能面板视图:顶部Bar + 粘贴区 + 标签列表 + 右侧操作按钮 @interface KBFunctionView : UIView -@property (nonatomic, weak) id delegate; +@property(nonatomic, weak) id delegate; - -@property (nonatomic, strong, readonly) UICollectionView *collectionView; // 话术分类/标签列表 -@property (nonatomic, strong, readonly) NSArray *items; // 简单数据源(演示用) +@property(nonatomic, strong, readonly) + UICollectionView *collectionView; // 话术分类/标签列表 +@property(nonatomic, strong, readonly) + NSArray *items; // 简单数据源(演示用) // 子视图暴露,便于外部接入事件 -@property (nonatomic, strong, readonly) KBFunctionBarView *barView; -@property (nonatomic, strong, readonly) KBFunctionPasteView *pasteView; +@property(nonatomic, strong, readonly) KBFunctionBarView *barView; +@property(nonatomic, strong, readonly) KBFunctionPasteView *pasteView; -@property (nonatomic, strong, readonly) UIButton *pasteButton; // 右侧-粘贴 -@property (nonatomic, strong, readonly) UIButton *deleteButton; // 右侧-删除 -@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空 -@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送 +@property(nonatomic, strong, readonly) UIButton *pasteButton; // 右侧-粘贴 +@property(nonatomic, strong, readonly) UIButton *deleteButton; // 右侧-删除 +@property(nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空 +@property(nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送 /// 应用当前皮肤(更新背景/强调色) - (void)kb_applyTheme; +#pragma mark - Theme Colors (用于 Cell 获取暗黑模式颜色) + +/// Cell 背景色:暗黑 #707070,浅色 白色90%透明度 ++ (UIColor *)kb_cellBackgroundColor; + +/// Cell 文字颜色:暗黑 #FFFFFF,浅色 #1B1F1A ++ (UIColor *)kb_cellTextColor; + @end NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 8faf6b1..9934db6 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -6,206 +6,317 @@ // #import "KBFunctionView.h" -#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具 +#import "KBAuthManager.h" // 登录态判断(共享钥匙串) +#import "KBBackspaceLongPressHandler.h" +#import "KBBackspaceUndoManager.h" +#import "KBBizCode.h" +#import "KBFullAccessGuideView.h" +#import "KBFullAccessManager.h" #import "KBFunctionBarView.h" #import "KBFunctionPasteView.h" #import "KBFunctionTagCell.h" -#import "Masonry.h" -#import -#import "KBFullAccessGuideView.h" -#import "KBFullAccessManager.h" -#import "KBSkinManager.h" -#import "KBAuthManager.h" // 登录态判断(共享钥匙串) -#import "KBULBridgeNotification.h" // Darwin 通知常量(UL 已处理) -#import "KBHostAppLauncher.h" -#import "KBStreamTextView.h" // 流式文本视图 -#import "KBStreamOverlayView.h" // 带关闭按钮的流式层 #import "KBFunctionTagListView.h" -#import "WJXEventSource.h" -#import "KBTagItemModel.h" -#import -#import "KBBizCode.h" -#import "KBBackspaceLongPressHandler.h" -#import "KBBackspaceUndoManager.h" +#import "KBHostAppLauncher.h" #import "KBInputBufferManager.h" +#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具 +#import "KBSkinManager.h" +#import "KBStreamOverlayView.h" // 带关闭按钮的流式层 +#import "KBStreamTextView.h" // 流式文本视图 +#import "KBTagItemModel.h" +#import "KBULBridgeNotification.h" // Darwin 通知常量(UL 已处理) +#import "Masonry.h" +#import "WJXEventSource.h" +#import +#import -@interface KBFunctionView () +@interface KBFunctionView () // UI -@property (nonatomic, strong) KBFunctionBarView *barViewInternal; -@property (nonatomic, strong) KBFunctionPasteView *pasteViewInternal; -@property (nonatomic, strong) KBFunctionTagListView *tagListView; -@property (nonatomic, strong) UIView *rightButtonContainer; // 右侧竖排按钮容器 -@property (nonatomic, strong) UIButton *pasteButtonInternal; -@property (nonatomic, strong) UIButton *deleteButtonInternal; -@property (nonatomic, strong) UIButton *clearButtonInternal; -@property (nonatomic, strong) UIButton *sendButtonInternal; +@property(nonatomic, strong) KBFunctionBarView *barViewInternal; +@property(nonatomic, strong) KBFunctionPasteView *pasteViewInternal; +@property(nonatomic, strong) KBFunctionTagListView *tagListView; +@property(nonatomic, strong) UIView *rightButtonContainer; // 右侧竖排按钮容器 +@property(nonatomic, strong) UIButton *pasteButtonInternal; +@property(nonatomic, strong) UIButton *deleteButtonInternal; +@property(nonatomic, strong) UIButton *clearButtonInternal; +@property(nonatomic, strong) UIButton *sendButtonInternal; // 叠层:流式文本视图 + 关闭按钮 -@property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay; +@property(nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay; // 网络流式(封装) -@property (nonatomic, strong, nullable) WJXEventSource *eventSource; -@property (nonatomic, assign) BOOL streamHasOutput; // 是否已输出过正文(首段去首个 \t 用) -@property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // 当前显示loading的标签index -@property (nonatomic, copy, nullable) NSString *loadingTagTitle; -@property (nonatomic, assign) BOOL eventSourceDidReceiveDone; -@property (nonatomic, copy, nullable) NSString *eventSourceSplitPrefix; +@property(nonatomic, strong, nullable) WJXEventSource *eventSource; +@property(nonatomic, assign) + BOOL streamHasOutput; // 是否已输出过正文(首段去首个 \t 用) +@property(nonatomic, strong, nullable) + NSNumber *loadingTagIndex; // 当前显示loading的标签index +@property(nonatomic, copy, nullable) NSString *loadingTagTitle; +@property(nonatomic, assign) BOOL eventSourceDidReceiveDone; +@property(nonatomic, copy, nullable) NSString *eventSourceSplitPrefix; // Data //@property (nonatomic, strong) NSArray *itemsInternal; -@property (nonatomic, strong) NSMutableArray *modelArray; +@property(nonatomic, strong) NSMutableArray *modelArray; // 剪贴板自动检测 -@property (nonatomic, strong) NSTimer *pasteboardTimer; // 轮询定时器(轻量、主线程) -@property (nonatomic, assign) NSInteger lastHandledPBCount; // 上次处理过的 changeCount,避免重复弹窗 +@property(nonatomic, strong) + NSTimer *pasteboardTimer; // 轮询定时器(轻量、主线程) +@property(nonatomic, assign) + NSInteger lastHandledPBCount; // 上次处理过的 changeCount,避免重复弹窗 // UL 双路兜底 -@property (nonatomic, assign) NSUInteger kb_ulSeq; // 当前 UL 发起序号 -@property (nonatomic, assign) BOOL kb_ulHandledFlag; // 主 App 已确认处理 UL -@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; +@property(nonatomic, assign) NSUInteger kb_ulSeq; // 当前 UL 发起序号 +@property(nonatomic, assign) BOOL kb_ulHandledFlag; // 主 App 已确认处理 UL +@property(nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @end @implementation KBFunctionView - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - // 背景使用当前主题强调色 - [self kb_applyTheme]; - self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; + if (self = [super initWithFrame:frame]) { + // 背景使用当前主题强调色 + [self kb_applyTheme]; + self.backspaceHandler = + [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; - [self setupUI]; -// [self reloadDemoData]; - [self kb_reloadTagsFromSharedDefaults]; + [self setupUI]; + // [self reloadDemoData]; + [self kb_reloadTagsFromSharedDefaults]; + // 初始化剪贴板监控状态 + _lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount; - // 初始化剪贴板监控状态 - _lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount; + // 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 + // TCC/XPC 错误日志 + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(kb_fullAccessChanged) + name:KBFullAccessChangedNotification + object:nil]; - // 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志 - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil]; - - // 监听主 App 的 Darwin 确认(UL 已处理) - CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), - (__bridge const void *)(self), - KBULDarwinCallback, - (__bridge CFStringRef)KBDarwinULHandled, - NULL, - CFNotificationSuspensionBehaviorDeliverImmediately); - } - return self; + // 监听主 App 的 Darwin 确认(UL 已处理) + CFNotificationCenterAddObserver( + CFNotificationCenterGetDarwinNotifyCenter(), + (__bridge const void *)(self), KBULDarwinCallback, + (__bridge CFStringRef)KBDarwinULHandled, NULL, + CFNotificationSuspensionBehaviorDeliverImmediately); + } + return self; } #pragma mark - Data /// 从 App Group 的 NSUserDefaults 中读取真实 JSON,解析为 model + 标签文案 - (void)kb_reloadTagsFromSharedDefaults { - NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; - NSDictionary *jsonDict = [sharedDefaults objectForKey:AppGroup_MyKbJson]; - if (jsonDict != nil) { - id dataObj = jsonDict[@"data"]; - NSArray *modelList = [KBTagItemModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj]; - if (modelList.count > 0) { - self.modelArray = [NSMutableArray array]; - [self.modelArray addObjectsFromArray:modelList]; -// [self.collectionView reloadData]; - [self.tagListView setItems:self.modelArray]; - } - }else{ - NSLog(@"json❎"); + NSUserDefaults *sharedDefaults = + [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; + NSDictionary *jsonDict = [sharedDefaults objectForKey:AppGroup_MyKbJson]; + if (jsonDict != nil) { + id dataObj = jsonDict[@"data"]; + NSArray *modelList = + [KBTagItemModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj]; + if (modelList.count > 0) { + self.modelArray = [NSMutableArray array]; + [self.modelArray addObjectsFromArray:modelList]; + // [self.collectionView reloadData]; + [self.tagListView setItems:self.modelArray]; } + } else { + NSLog(@"json❎"); + } } - #pragma mark - Theme +/// 判断当前是否为暗黑模式 +- (BOOL)kb_isDarkMode { + if (@available(iOS 13.0, *)) { + return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; + } + return NO; +} + +#pragma mark - Theme Colors + +/// 整体背景色:暗黑 #323232,浅色 #D0D3DA ++ (UIColor *)kb_backgroundColor { + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return [UIColor colorWithHex:0x2B2B2B]; + } else { + return [UIColor colorWithHex:0xD0D3DA]; + } + }]; + } + return [UIColor colorWithHex:0xD0D3DA]; +} + +/// Cell 背景色:暗黑 #707070,浅色 白色90%透明度 ++ (UIColor *)kb_cellBackgroundColor { + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return [UIColor colorWithHex:0x707070]; + } else { + return [UIColor colorWithWhite:1 alpha:0.9]; + } + }]; + } + return [UIColor colorWithWhite:1 alpha:0.9]; +} + +/// Cell 文字颜色:暗黑 #FFFFFF,浅色 #1B1F1A ++ (UIColor *)kb_cellTextColor { + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return [UIColor whiteColor]; + } else { + return [UIColor colorWithHex:0x1B1F1A]; + } + }]; + } + return [UIColor colorWithHex:0x1B1F1A]; +} + +/// Clear 按钮文字颜色:暗黑白色,浅色黑色 ++ (UIColor *)kb_clearButtonTextColor { + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return [UIColor whiteColor]; + } else { + return [UIColor blackColor]; + } + }]; + } + return [UIColor blackColor]; +} + +/// 删除按钮背景色:暗黑 #707070,浅色 #B9BDC8 ++ (UIColor *)kb_deleteButtonBackgroundColor { + if (@available(iOS 13.0, *)) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return [UIColor colorWithHex:0x707070]; + } else { + return [UIColor colorWithHex:0xB9BDC8]; + } + }]; + } + return [UIColor colorWithHex:0xB9BDC8]; +} + - (void)kb_applyTheme { -// KBSkinManager *mgr = [KBSkinManager shared]; -// UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0]; -// BOOL hasImg = ([mgr currentBackgroundImage] != nil); - self.backgroundColor = [UIColor colorWithHex:0xD0D3DA]; + // 使用动态颜色设置背景 + self.backgroundColor = [KBFunctionView kb_backgroundColor]; + + // 更新按钮颜色 + self.clearButtonInternal.backgroundColor = + [KBFunctionView kb_deleteButtonBackgroundColor]; + [self.clearButtonInternal + setTitleColor:[KBFunctionView kb_clearButtonTextColor] + forState:UIControlStateNormal]; + self.deleteButtonInternal.backgroundColor = + [KBFunctionView kb_deleteButtonBackgroundColor]; + + // 刷新 TagListView 以更新 cell 颜色 + [self.tagListView.collectionView reloadData]; } - (void)dealloc { - [self stopPasteboardMonitor]; - [self kb_stopNetworkStreaming]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; - CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinULHandled, NULL); + [self stopPasteboardMonitor]; + [self kb_stopNetworkStreaming]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + CFNotificationCenterRemoveObserver( + CFNotificationCenterGetDarwinNotifyCenter(), + (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinULHandled, + NULL); } #pragma mark - UI - (void)setupUI { - // 1. 顶部 Bar - [self addSubview:self.barViewInternal]; - CGFloat barTopInset = KBFit(6.0f); - CGFloat barHeight = KBFit(52.0f); - [self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.right.equalTo(self); - make.top.equalTo(self.mas_top).offset(barTopInset); - make.height.mas_equalTo(barHeight); - }]; + // 1. 顶部 Bar + [self addSubview:self.barViewInternal]; + CGFloat barTopInset = KBFit(6.0f); + CGFloat barHeight = KBFit(52.0f); + [self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self); + make.top.equalTo(self.mas_top).offset(barTopInset); + make.height.mas_equalTo(barHeight); + }]; - // 右侧竖排按钮容器 - [self addSubview:self.rightButtonContainer]; - CGFloat rightInset = KBFit(4.0f); - CGFloat containerBottomInset = KBFit(10.0f); - CGFloat containerWidth = KBFit(60.0f); - [self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(self.mas_right).offset(-rightInset); - make.top.equalTo(self.barViewInternal.mas_bottom).offset(0); - make.bottom.equalTo(self.mas_bottom).offset(0); - make.width.mas_equalTo(containerWidth); - }]; + // 右侧竖排按钮容器 + [self addSubview:self.rightButtonContainer]; + CGFloat rightInset = KBFit(4.0f); + CGFloat containerBottomInset = KBFit(10.0f); + CGFloat containerWidth = KBFit(60.0f); + [self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.mas_right).offset(-rightInset); + make.top.equalTo(self.barViewInternal.mas_bottom).offset(0); + make.bottom.equalTo(self.mas_bottom).offset(0); + make.width.mas_equalTo(containerWidth); + }]; - // 右侧四个按钮 - [self.rightButtonContainer addSubview:self.pasteButtonInternal]; - [self.rightButtonContainer addSubview:self.deleteButtonInternal]; - [self.rightButtonContainer addSubview:self.clearButtonInternal]; - [self.rightButtonContainer addSubview:self.sendButtonInternal]; + // 右侧四个按钮 + [self.rightButtonContainer addSubview:self.pasteButtonInternal]; + [self.rightButtonContainer addSubview:self.deleteButtonInternal]; + [self.rightButtonContainer addSubview:self.clearButtonInternal]; + [self.rightButtonContainer addSubview:self.sendButtonInternal]; - // 竖向排布:容器内四个按钮等高分配,间距为 8px(按设计稿等比缩放) - CGFloat smallH = KBFit(41.0f); - CGFloat vSpace = KBFit(8.0f); - [self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.rightButtonContainer.mas_top); - make.left.right.equalTo(self.rightButtonContainer); - }]; - [self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace); - make.left.right.equalTo(self.rightButtonContainer); - make.height.equalTo(self.pasteButtonInternal); - }]; - [self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace); - make.left.right.equalTo(self.rightButtonContainer); - make.height.equalTo(self.pasteButtonInternal); - }]; - [self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace); - make.left.right.equalTo(self.rightButtonContainer); - make.height.equalTo(self.pasteButtonInternal); - make.bottom.equalTo(self.rightButtonContainer.mas_bottom); - }]; + // 竖向排布:容器内四个按钮等高分配,间距为 8px(按设计稿等比缩放) + CGFloat smallH = KBFit(41.0f); + CGFloat vSpace = KBFit(8.0f); + [self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.rightButtonContainer.mas_top); + make.left.right.equalTo(self.rightButtonContainer); + }]; + [self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace); + make.left.right.equalTo(self.rightButtonContainer); + make.height.equalTo(self.pasteButtonInternal); + }]; + [self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace); + make.left.right.equalTo(self.rightButtonContainer); + make.height.equalTo(self.pasteButtonInternal); + }]; + [self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace); + make.left.right.equalTo(self.rightButtonContainer); + make.height.equalTo(self.pasteButtonInternal); + make.bottom.equalTo(self.rightButtonContainer.mas_bottom); + }]; - // 2. 粘贴区(位于右侧按钮左侧) - [self addSubview:self.pasteViewInternal]; - [self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.mas_left).offset(vSpace); - make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace); - make.top.equalTo(self.barViewInternal.mas_bottom).offset(0); - make.height.mas_equalTo(smallH); - }]; - // 点击整个粘贴卡片按钮,行为与右侧「Paste」按钮保持一致 - [self.pasteViewInternal.pasBtn addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside]; + // 2. 粘贴区(位于右侧按钮左侧) + [self addSubview:self.pasteViewInternal]; + [self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.mas_left).offset(vSpace); + make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace); + make.top.equalTo(self.barViewInternal.mas_bottom).offset(0); + make.height.mas_equalTo(smallH); + }]; + // 点击整个粘贴卡片按钮,行为与右侧「Paste」按钮保持一致 + [self.pasteViewInternal.pasBtn addTarget:self + action:@selector(onTapPaste) + forControlEvents:UIControlEventTouchUpInside]; - // 3. Tag List View - [self addSubview:self.tagListView]; - [self.tagListView mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.pasteViewInternal); - make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace); - make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(vSpace); - make.bottom.equalTo(self.mas_bottom).offset(0); - }]; + // 3. Tag List View + [self addSubview:self.tagListView]; + [self.tagListView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.pasteViewInternal); + make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace); + make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(vSpace); + make.bottom.equalTo(self.mas_bottom).offset(0); + }]; } #pragma mark - Data @@ -228,303 +339,359 @@ // UICollectionView 逻辑已下沉至 KBFunctionTagListView - (void)kb_showStreamTextViewIfNeededWithTitle:(NSString *)title { - // 已有则不重复创建 - if (self.streamOverlay.superview) { return; } + // 已有则不重复创建 + if (self.streamOverlay.superview) { + return; + } - // 隐藏标签列表,使用同一区域展示流式文本 - self.tagListView.hidden = YES; + // 隐藏标签列表,使用同一区域展示流式文本 + self.tagListView.hidden = YES; - KBStreamOverlayView *overlay = [[KBStreamOverlayView alloc] init]; - overlay.delegate = (id)self; - [self addSubview:overlay]; - [overlay mas_makeConstraints:^(MASConstraintMaker *make) { - // 在原标签区域内展示流式文本,右侧继续保留竖排按钮栏 - CGFloat vSpace = KBFit(4.0f); - CGFloat overlayTopInset = KBFit(10.0f); - CGFloat overlayBottomInset = KBFit(10.0f); - make.left.equalTo(self.pasteViewInternal); - make.right.equalTo(self).offset(-vSpace); - make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(overlayTopInset); - make.bottom.equalTo(self.mas_bottom).offset(-overlayBottomInset); - }]; - // 仅隐藏删除/清空/发送按钮,保留“Paste”按钮可用 - self.pasteButtonInternal.hidden = NO; - self.deleteButtonInternal.hidden = YES; - self.clearButtonInternal.hidden = YES; - self.sendButtonInternal.hidden = YES; - // 适当缩小内部左右留白,进一步提升可用宽度 - overlay.textView.contentHorizontalPadding = 8.0; - self.streamOverlay = overlay; + KBStreamOverlayView *overlay = [[KBStreamOverlayView alloc] init]; + overlay.delegate = (id)self; + [self addSubview:overlay]; + [overlay mas_makeConstraints:^(MASConstraintMaker *make) { + // 在原标签区域内展示流式文本,右侧继续保留竖排按钮栏 + CGFloat vSpace = KBFit(4.0f); + CGFloat overlayTopInset = KBFit(10.0f); + CGFloat overlayBottomInset = KBFit(10.0f); + make.left.equalTo(self.pasteViewInternal); + make.right.equalTo(self).offset(-vSpace); + make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(overlayTopInset); + make.bottom.equalTo(self.mas_bottom).offset(-overlayBottomInset); + }]; + // 仅隐藏删除/清空/发送按钮,保留“Paste”按钮可用 + self.pasteButtonInternal.hidden = NO; + self.deleteButtonInternal.hidden = YES; + self.clearButtonInternal.hidden = YES; + self.sendButtonInternal.hidden = YES; + // 适当缩小内部左右留白,进一步提升可用宽度 + overlay.textView.contentHorizontalPadding = 8.0; + self.streamOverlay = overlay; - // 只创建 UI;网络在点击 cell 时启动,避免重复 start 导致首包重复 + // 只创建 UI;网络在点击 cell 时启动,避免重复 start 导致首包重复 } - (void)kb_onTapStreamDelete { - // 关闭并销毁流式视图,恢复标签列表 - [self kb_stopNetworkStreaming]; - [self.streamOverlay removeFromSuperview]; - self.streamOverlay = nil; - self.tagListView.hidden = NO; - // 恢复右侧按钮栏的全部按钮 - self.pasteButtonInternal.hidden = NO; - self.deleteButtonInternal.hidden = NO; - self.clearButtonInternal.hidden = NO; - self.sendButtonInternal.hidden = NO; + // 关闭并销毁流式视图,恢复标签列表 + [self kb_stopNetworkStreaming]; + [self.streamOverlay removeFromSuperview]; + self.streamOverlay = nil; + self.tagListView.hidden = NO; + // 恢复右侧按钮栏的全部按钮 + self.pasteButtonInternal.hidden = NO; + self.deleteButtonInternal.hidden = NO; + self.clearButtonInternal.hidden = NO; + self.sendButtonInternal.hidden = NO; } // 叠层关闭回调 - (void)streamOverlayDidTapClose:(KBStreamOverlayView *)overlay { - [self kb_onTapStreamDelete]; + [self kb_onTapStreamDelete]; } - #pragma mark - Network Streaming (WJXEventSource) - (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle { - [self kb_stopNetworkStreaming]; - if (![[KBFullAccessManager shared] hasFullAccess]) { return; } + [self kb_stopNetworkStreaming]; + if (![[KBFullAccessManager shared] hasFullAccess]) { + return; + } - NSString *apiUrl = [NSString stringWithFormat:@"%@%@", KB_BASE_URL, API_AI_TALK]; - NSURL *url = [NSURL URLWithString:apiUrl]; - if (!url) { return; } + NSString *apiUrl = + [NSString stringWithFormat:@"%@%@", KB_BASE_URL, API_AI_TALK]; + NSURL *url = [NSURL URLWithString:apiUrl]; + if (!url) { + return; + } - NSInteger characterId = 0; - if (self.loadingTagIndex != nil) { - NSInteger idx = self.loadingTagIndex.integerValue; - if (idx >= 0 && idx < self.modelArray.count) { - KBTagItemModel *model = self.modelArray[idx]; - characterId = model.characterId; - } - } - NSInteger resolvedCharacterId = (characterId > 0) ? characterId : 75; - NSString *message = seedTitle.length > 0 ? seedTitle : @"aliqua non cupidatat"; -// message = [NSString stringWithFormat:@"%@%d",message,arc4random() % 10000]; - NSDictionary *payload = @{ - @"characterId": @(resolvedCharacterId), - @"message": message - }; - NSLog(@"[KBFunction] request payload: %@", payload); - NSError *bodyError = nil; - NSData *bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&bodyError]; - if (bodyError || bodyData.length == 0) { - NSLog(@"[KBFunction] build body failed: %@", bodyError); - return; + NSInteger characterId = 0; + if (self.loadingTagIndex != nil) { + NSInteger idx = self.loadingTagIndex.integerValue; + if (idx >= 0 && idx < self.modelArray.count) { + KBTagItemModel *model = self.modelArray[idx]; + characterId = model.characterId; } + } + NSInteger resolvedCharacterId = (characterId > 0) ? characterId : 75; + NSString *message = + seedTitle.length > 0 ? seedTitle : @"aliqua non cupidatat"; + // message = [NSString stringWithFormat:@"%@%d",message,arc4random() % + // 10000]; + NSDictionary *payload = + @{@"characterId" : @(resolvedCharacterId), @"message" : message}; + NSLog(@"[KBFunction] request payload: %@", payload); + NSError *bodyError = nil; + NSData *bodyData = [NSJSONSerialization dataWithJSONObject:payload + options:0 + error:&bodyError]; + if (bodyError || bodyData.length == 0) { + NSLog(@"[KBFunction] build body failed: %@", bodyError); + return; + } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60]; - request.HTTPMethod = @"POST"; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSString *token = KBAuthManager.shared.current.accessToken ?: @""; - if (token.length > 0) { - [request setValue:token forHTTPHeaderField:@"auth-token"]; - } - request.HTTPBody = bodyData; + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60]; + request.HTTPMethod = @"POST"; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + NSString *token = KBAuthManager.shared.current.accessToken ?: @""; + if (token.length > 0) { + [request setValue:token forHTTPHeaderField:@"auth-token"]; + } + request.HTTPBody = bodyData; - self.streamHasOutput = NO; - self.eventSourceSplitPrefix = nil; - self.eventSourceDidReceiveDone = NO; + self.streamHasOutput = NO; + self.eventSourceSplitPrefix = nil; + self.eventSourceDidReceiveDone = NO; - __weak typeof(self) weakSelf = self; - WJXEventSource *source = [[WJXEventSource alloc] initWithRquest:request]; - source.ignoreRetryAction = YES; - [source addListener:^(WJXEvent * _Nonnull event) { - __strong typeof(weakSelf) self = weakSelf; if (!self) return; + __weak typeof(self) weakSelf = self; + WJXEventSource *source = [[WJXEventSource alloc] initWithRquest:request]; + source.ignoreRetryAction = YES; + [source + addListener:^(WJXEvent *_Nonnull event) { + __strong typeof(weakSelf) self = weakSelf; + if (!self) + return; [self kb_handleEventSourceMessage:event]; - } forEvent:WJXEventNameMessage queue:NSOperationQueue.mainQueue]; - [source addListener:^(WJXEvent * _Nonnull event) { - __strong typeof(weakSelf) self = weakSelf; if (!self) return; + } + forEvent:WJXEventNameMessage + queue:NSOperationQueue.mainQueue]; + [source + addListener:^(WJXEvent *_Nonnull event) { + __strong typeof(weakSelf) self = weakSelf; + if (!self) + return; [self kb_handleEventSourceError:event.error]; - } forEvent:WJXEventNameError queue:NSOperationQueue.mainQueue]; - self.eventSource = source; - [self.eventSource open]; + } + forEvent:WJXEventNameError + queue:NSOperationQueue.mainQueue]; + self.eventSource = source; + [self.eventSource open]; } - (void)kb_stopNetworkStreaming { - [self.eventSource close]; - self.eventSource = nil; - self.eventSourceSplitPrefix = nil; - self.eventSourceDidReceiveDone = NO; - self.streamHasOutput = NO; + [self.eventSource close]; + self.eventSource = nil; + self.eventSourceSplitPrefix = nil; + self.eventSourceDidReceiveDone = NO; + self.streamHasOutput = NO; } - (void)kb_handleEventSourceMessage:(WJXEvent *)event { - if (event.data.length == 0) { return; } - NSLog(@"[KBStream] SSE raw payload: %@", event.data); - NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding]; - if (!jsonData) { return; } - NSError *error = nil; - NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (error || ![payload isKindOfClass:[NSDictionary class]]) { return; } - if ([self kb_handleBizErrorIfNeeded:payload]) { return; } - NSString *type = payload[@"type"]; - if (![type isKindOfClass:[NSString class]]) { return; } + if (event.data.length == 0) { + return; + } + NSLog(@"[KBStream] SSE raw payload: %@", event.data); + NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding]; + if (!jsonData) { + return; + } + NSError *error = nil; + NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:jsonData + options:0 + error:&error]; + if (error || ![payload isKindOfClass:[NSDictionary class]]) { + return; + } + if ([self kb_handleBizErrorIfNeeded:payload]) { + return; + } + NSString *type = payload[@"type"]; + if (![type isKindOfClass:[NSString class]]) { + return; + } - if ([type isEqualToString:@"llm_chunk"]) { - NSString *chunk = [self kb_normalizedLLMChunkString:payload[@"data"]]; - if (chunk.length > 0) { - [self kb_appendChunkToStreamView:chunk]; - } - return; + if ([type isEqualToString:@"llm_chunk"]) { + NSString *chunk = [self kb_normalizedLLMChunkString:payload[@"data"]]; + if (chunk.length > 0) { + [self kb_appendChunkToStreamView:chunk]; } - if ([type isEqualToString:@"search_result"]) { - NSString *text = [self kb_formattedSearchResultString:payload[@"data"]]; - if (text.length > 0) { - [self kb_appendChunkToStreamView:text]; - } - return; - } - if ([type isEqualToString:@"done"]) { - self.eventSourceDidReceiveDone = YES; - [self kb_finishEventSourceWithError:nil]; - return; + return; + } + if ([type isEqualToString:@"search_result"]) { + NSString *text = [self kb_formattedSearchResultString:payload[@"data"]]; + if (text.length > 0) { + [self kb_appendChunkToStreamView:text]; } + return; + } + if ([type isEqualToString:@"done"]) { + self.eventSourceDidReceiveDone = YES; + [self kb_finishEventSourceWithError:nil]; + return; + } } -- (void)kb_handleEventSourceError:(NSError * _Nullable)error { - if (self.eventSourceDidReceiveDone) { return; } - [self kb_finishEventSourceWithError:error]; +- (void)kb_handleEventSourceError:(NSError *_Nullable)error { + if (self.eventSourceDidReceiveDone) { + return; + } + [self kb_finishEventSourceWithError:error]; } -- (void)kb_finishEventSourceWithError:(NSError * _Nullable)error { - [self.eventSource close]; - self.eventSource = nil; - if (!self.streamHasOutput && self.loadingTagIndex) { - [self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue]; - self.loadingTagIndex = nil; - self.loadingTagTitle = nil; - } - BOOL shouldShowError = (error != nil); - if (shouldShowError) { - [KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")]; - } - if (self.streamOverlay) { - [self.streamOverlay finish]; - } - self.eventSourceSplitPrefix = nil; - self.eventSourceDidReceiveDone = NO; +- (void)kb_finishEventSourceWithError:(NSError *_Nullable)error { + [self.eventSource close]; + self.eventSource = nil; + if (!self.streamHasOutput && self.loadingTagIndex) { + [self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue]; + self.loadingTagIndex = nil; + self.loadingTagTitle = nil; + } + BOOL shouldShowError = (error != nil); + if (shouldShowError) { + [KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")]; + } + if (self.streamOverlay) { + [self.streamOverlay finish]; + } + self.eventSourceSplitPrefix = nil; + self.eventSourceDidReceiveDone = NO; } - (BOOL)kb_handleBizErrorIfNeeded:(NSDictionary *)payload { - NSInteger code = KBBizCodeFromJSONObject(payload); - if (code == NSNotFound || code == KBBizCodeSuccess) { - return NO; - } - BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted); - NSString *msg = KBBizMessageFromJSONObject(payload); - if (msg.length == 0) { - msg = KBLocalized(@"拉取失败"); - } - NSError *bizError = [NSError errorWithDomain:@"KBStreamBizError" - code:code - userInfo:@{NSLocalizedDescriptionKey: msg}]; - [self kb_finishEventSourceWithError:bizError]; - if (needSubscriptionGuide) { - [self kb_requestSubscriptionGuide]; - } - return YES; + NSInteger code = KBBizCodeFromJSONObject(payload); + if (code == NSNotFound || code == KBBizCodeSuccess) { + return NO; + } + BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted); + NSString *msg = KBBizMessageFromJSONObject(payload); + if (msg.length == 0) { + msg = KBLocalized(@"拉取失败"); + } + NSError *bizError = + [NSError errorWithDomain:@"KBStreamBizError" + code:code + userInfo:@{NSLocalizedDescriptionKey : msg}]; + [self kb_finishEventSourceWithError:bizError]; + if (needSubscriptionGuide) { + [self kb_requestSubscriptionGuide]; + } + return YES; } - (void)kb_requestSubscriptionGuide { - if ([self.delegate respondsToSelector:@selector(functionViewDidRequestSubscription:)]) { - [self.delegate functionViewDidRequestSubscription:self]; - } + if ([self.delegate + respondsToSelector:@selector(functionViewDidRequestSubscription:)]) { + [self.delegate functionViewDidRequestSubscription:self]; + } } #pragma mark - Event Parsing - (NSString *)kb_normalizedLLMChunkString:(id)dataValue { - if (![dataValue isKindOfClass:[NSString class]]) { return @""; } - NSString *text = (NSString *)dataValue; + if (![dataValue isKindOfClass:[NSString class]]) { + return @""; + } + NSString *text = (NSString *)dataValue; - // 1. 处理上一个包遗留的 前缀(比如 "") - if (self.eventSourceSplitPrefix.length > 0) { - text = [self.eventSourceSplitPrefix stringByAppendingString:text ?: @""]; - self.eventSourceSplitPrefix = nil; + // 1. 处理上一个包遗留的 前缀(比如 "") + if (self.eventSourceSplitPrefix.length > 0) { + text = [self.eventSourceSplitPrefix stringByAppendingString:text ?: @""]; + self.eventSourceSplitPrefix = nil; + } + if (text.length == 0) { + return @""; + } + + // 2. 去掉开头多余换行(避免一开始就空一大块) + while (text.length > 0) { + unichar c0 = [text characterAtIndex:0]; + if (c0 == '\n' || c0 == '\r') { + text = [text substringFromIndex:1]; + continue; } - if (text.length == 0) { return @""; } + break; + } + if (text.length == 0) { + return @""; + } - // 2. 去掉开头多余换行(避免一开始就空一大块) - while (text.length > 0) { - unichar c0 = [text characterAtIndex:0]; - if (c0 == '\n' || c0 == '\r') { - text = [text substringFromIndex:1]; - continue; - } - break; - } - if (text.length == 0) { return @""; } + // 3. 处理结尾可能是不完整的 " 0) { + self.eventSourceSplitPrefix = suffix; + text = [text substringToIndex:text.length - suffix.length]; + } + if (text.length == 0) { + return @""; + } - // 3. 处理结尾可能是不完整的 " 0) { - self.eventSourceSplitPrefix = suffix; - text = [text substringToIndex:text.length - suffix.length]; - } - if (text.length == 0) { return @""; } + // 4. 处理完整的 ,变成段落分隔符 \t + text = [text stringByReplacingOccurrencesOfString:@"" + withString:@"\t"]; - // 4. 处理完整的 ,变成段落分隔符 \t - text = [text stringByReplacingOccurrencesOfString:@"" withString:@"\t"]; - - // 不再做其它替换,不合并 /t、不改行,只把真正内容原样丢给 UI - return text; + // 不再做其它替换,不合并 /t、不改行,只把真正内容原样丢给 UI + return text; } - - (NSString *)kb_formattedSearchResultString:(id)dataValue { - // data 不是数组就直接返回空串 - if (![dataValue isKindOfClass:[NSArray class]]) { return @""; } - NSArray *list = (NSArray *)dataValue; + // data 不是数组就直接返回空串 + if (![dataValue isKindOfClass:[NSArray class]]) { + return @""; + } + NSArray *list = (NSArray *)dataValue; - NSMutableArray *segments = [NSMutableArray array]; + NSMutableArray *segments = [NSMutableArray array]; - [list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSString *payload = nil; + [list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, + BOOL *_Nonnull stop) { + NSString *payload = nil; - if ([obj isKindOfClass:[NSDictionary class]]) { - id val = obj[@"payload"]; - if ([val isKindOfClass:[NSString class]]) { - payload = (NSString *)val; - } - } else if ([obj isKindOfClass:[NSString class]]) { - // 兼容后端直接给字符串数组的情况 - payload = (NSString *)obj; - } + if ([obj isKindOfClass:[NSDictionary class]]) { + id val = obj[@"payload"]; + if ([val isKindOfClass:[NSString class]]) { + payload = (NSString *)val; + } + } else if ([obj isKindOfClass:[NSString class]]) { + // 兼容后端直接给字符串数组的情况 + payload = (NSString *)obj; + } - payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if (payload.length > 0) { - // 每一个 payload 就是一段 - [segments addObject:payload]; - } - }]; + payload = [payload + stringByTrimmingCharactersInSet:[NSCharacterSet + whitespaceAndNewlineCharacterSet]]; + if (payload.length > 0) { + // 每一个 payload 就是一段 + [segments addObject:payload]; + } + }]; - if (segments.count == 0) { return @""; } + if (segments.count == 0) { + return @""; + } - // 用 \t 拼起来,KBStreamTextView 会按 \t 拆成多个 label - NSMutableString *result = [NSMutableString string]; + // 用 \t 拼起来,KBStreamTextView 会按 \t 拆成多个 label + NSMutableString *result = [NSMutableString string]; - [segments enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - // 每段前面加一个 \t,保证是新的一段 - [result appendFormat:@"\t%@", obj]; - }]; + [segments enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, + BOOL *_Nonnull stop) { + // 每段前面加一个 \t,保证是新的一段 + [result appendFormat:@"\t%@", obj]; + }]; - return result; + return result; } - - (NSString *)kb_pendingSplitSuffixForString:(NSString *)text { - static NSString * const token = @""; - if (text.length == 0) { return @""; } - NSUInteger tokenLen = token.length; - if (tokenLen <= 1) { return @""; } - NSUInteger maxLen = MIN(tokenLen - 1, text.length); - for (NSUInteger len = maxLen; len > 0; len--) { - NSString *suffix = [text substringFromIndex:text.length - len]; - NSString *prefix = [token substringToIndex:len]; - if ([suffix isEqualToString:prefix]) { - return suffix; - } - } + static NSString *const token = @""; + if (text.length == 0) { return @""; + } + NSUInteger tokenLen = token.length; + if (tokenLen <= 1) { + return @""; + } + NSUInteger maxLen = MIN(tokenLen - 1, text.length); + for (NSUInteger len = maxLen; len > 0; len--) { + NSString *suffix = [text substringFromIndex:text.length - len]; + NSString *prefix = [token substringToIndex:len]; + if ([suffix isEqualToString:prefix]) { + return suffix; + } + } + return @""; } #pragma mark - Helpers @@ -533,190 +700,237 @@ /// - 已将 `` 转换为 `\t` 并去掉多余换行 /// - 这里仅负责附加到视图与标记首段状态,避免 UI 抖动 - (void)kb_appendChunkToStreamView:(NSString *)chunk { - if (chunk.length == 0) return; - // 第一次有数据才创建 overlay,并取消 cell 上的小菊花 - if (!self.streamOverlay) { - [self kb_showStreamTextViewIfNeededWithTitle:self.loadingTagTitle ?: @""]; - if (self.loadingTagIndex) { - [self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue]; - self.loadingTagIndex = nil; self.loadingTagTitle = nil; - } + if (chunk.length == 0) + return; + // 第一次有数据才创建 overlay,并取消 cell 上的小菊花 + if (!self.streamOverlay) { + [self kb_showStreamTextViewIfNeededWithTitle:self.loadingTagTitle ?: @""]; + if (self.loadingTagIndex) { + [self.tagListView setLoading:NO + atIndex:self.loadingTagIndex.integerValue]; + self.loadingTagIndex = nil; + self.loadingTagTitle = nil; } - if (!self.streamOverlay) return; - [self.streamOverlay appendChunk:chunk]; - self.streamHasOutput = YES; + } + if (!self.streamOverlay) + return; + [self.streamOverlay appendChunk:chunk]; + self.streamHasOutput = YES; } /// 统一更新左侧粘贴按钮的展示: /// - 有粘贴文本:只显示文字,不再展示左侧图标; /// - 无粘贴文本:恢复原始图标 + 占位文案。 -- (void)kb_updatePasteButtonWithDisplayText:(NSString * _Nullable)text { - if (text.length > 0) { - NSString *displayText = text; - if (displayText.length > 30) { - displayText = [[displayText substringToIndex:30] stringByAppendingString:@"…"]; - } - [self.pasteView.pasBtn setImage:nil forState:UIControlStateNormal]; - [self.pasteView.pasBtn setTitle:displayText forState:UIControlStateNormal]; - } else { - UIImage *img = [UIImage imageNamed:@"kb_zt_icon"]; - [self.pasteView.pasBtn setImage:img forState:UIControlStateNormal]; - [self.pasteView.pasBtn setTitle:KBLocalized(@" Paste Ta's Words") forState:UIControlStateNormal]; +- (void)kb_updatePasteButtonWithDisplayText:(NSString *_Nullable)text { + if (text.length > 0) { + NSString *displayText = text; + if (displayText.length > 30) { + displayText = + [[displayText substringToIndex:30] stringByAppendingString:@"…"]; } + [self.pasteView.pasBtn setImage:nil forState:UIControlStateNormal]; + [self.pasteView.pasBtn setTitle:displayText forState:UIControlStateNormal]; + } else { + UIImage *img = [UIImage imageNamed:@"kb_zt_icon"]; + [self.pasteView.pasBtn setImage:img forState:UIControlStateNormal]; + [self.pasteView.pasBtn setTitle:KBLocalized(@" Paste Ta's Words") + forState:UIControlStateNormal]; + } } #pragma mark - KBFunctionTagListViewDelegate -- (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title { - // 1) 先判断权限:未开启“完全访问”则走引导逻辑 - if (![[KBFullAccessManager shared] hasFullAccess]) { - // 未开启完全访问:保持原有引导路径 - [KBHUD showInfo:KBLocalized(@"处理中…")]; - [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; - return; - } - - // 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App 负责完成登录 - if (!KBAuthManager.shared.isLoggedIn) { - - - UIInputViewController *ivc = KBFindInputViewController(self); - - NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]; - NSURL *scheme = [NSURL URLWithString:schemeStr]; - // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App - BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:ivc.view]; - return; -// if (!ivc) return; -// NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @""; -// NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]]; -// if (!ul) return; -// // 发起 UL,不依赖 ok 结果 -// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ -// [ivc.extensionContext openURL:ul completionHandler:^(__unused BOOL ok) {}]; -// }); -// // 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 Scheme(通过宿主 UIApplication 打开) -// self.kb_ulHandledFlag = NO; -// NSUInteger token = ++self.kb_ulSeq; -// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ -// if (token != self.kb_ulSeq) return; // 已有新请求覆盖 -// if (self.kb_ulHandledFlag) return; // 主 App 已确认处理 -// NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]]; -// if (!scheme) return; -// UIResponder *start = ivc.view ?: (UIResponder *)self; -// // 让键盘失去焦点 -// [ivc dismissKeyboard]; -// BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start]; -// if (!ok) { -// [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")]; -// }else{ -// -// } -// }); -// return; - } - BOOL hasPasteText = ![self.pasteView.pasBtn.currentTitle isEqualToString:KBLocalized(@" Paste Ta's Words")]; -// BOOL hasPasteText = (self.pasteView.pasBtn.imageView.image == nil); - if (!hasPasteText) { - [KBHUD showInfo:KBLocalized(@"Please copy the text first")]; - return; - } - NSString *copyTitle = self.pasteView.pasBtn.currentTitle; - // 3) 已登录:开始业务逻辑(展示加载并拉取流式内容) - [self.tagListView setLoading:YES atIndex:index]; - self.loadingTagIndex = @(index); - self.loadingTagTitle = title ?: @""; - [self kb_startNetworkStreamingWithSeed:copyTitle]; +- (void)tagListView:(KBFunctionTagListView *)view + didSelectIndex:(NSInteger)index + title:(NSString *)title { + // 1) 先判断权限:未开启“完全访问”则走引导逻辑 + if (![[KBFullAccessManager shared] hasFullAccess]) { + // 未开启完全访问:保持原有引导路径 + [KBHUD showInfo:KBLocalized(@"处理中…")]; + [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; return; + } + + // 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App + // 负责完成登录 + if (!KBAuthManager.shared.isLoggedIn) { + + UIInputViewController *ivc = KBFindInputViewController(self); + + NSString *schemeStr = + [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]; + NSURL *scheme = [NSURL URLWithString:schemeStr]; + // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App + BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:ivc.view]; + return; + // if (!ivc) return; + // NSString *encodedTitle = [title + // stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet + // URLQueryAllowedCharacterSet]] ?: @""; NSURL *ul = [NSURL + // URLWithString:[NSString + // stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", + // KB_UL_LOGIN, (long)index, encodedTitle]]; if (!ul) return; + // // 发起 UL,不依赖 ok 结果 + // dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * + // NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // [ivc.extensionContext openURL:ul completionHandler:^(__unused + // BOOL ok) {}]; + // }); + // // 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 + // Scheme(通过宿主 UIApplication 打开) self.kb_ulHandledFlag = NO; + // NSUInteger token = ++self.kb_ulSeq; + // dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * + // NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // if (token != self.kb_ulSeq) return; // 已有新请求覆盖 + // if (self.kb_ulHandledFlag) return; // 主 App 已确认处理 + // NSURL *scheme = [NSURL URLWithString:[NSString + // stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", + // KB_APP_SCHEME, (long)index, encodedTitle]]; if (!scheme) + // return; UIResponder *start = ivc.view ?: (UIResponder *)self; + // // 让键盘失去焦点 + // [ivc dismissKeyboard]; + // BOOL ok = [KBHostAppLauncher openHostAppURL:scheme + // fromResponder:start]; if (!ok) { + // [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")]; + // }else{ + // + // } + // }); + // return; + } + BOOL hasPasteText = ![self.pasteView.pasBtn.currentTitle + isEqualToString:KBLocalized(@" Paste Ta's Words")]; + // BOOL hasPasteText = (self.pasteView.pasBtn.imageView.image == nil); + if (!hasPasteText) { + [KBHUD showInfo:KBLocalized(@"Please copy the text first")]; + return; + } + NSString *copyTitle = self.pasteView.pasBtn.currentTitle; + // 3) 已登录:开始业务逻辑(展示加载并拉取流式内容) + [self.tagListView setLoading:YES atIndex:index]; + self.loadingTagIndex = @(index); + self.loadingTagTitle = title ?: @""; + [self kb_startNetworkStreamingWithSeed:copyTitle]; + return; } // Darwin 回调:主 App 已处理 UL -static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { - KBFunctionView *self_ = (__bridge KBFunctionView *)observer; - if (!self_) return; - dispatch_async(dispatch_get_main_queue(), ^{ self_.kb_ulHandledFlag = YES; }); +static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, + CFStringRef name, const void *object, + CFDictionaryRef userInfo) { + KBFunctionView *self_ = (__bridge KBFunctionView *)observer; + if (!self_) + return; + dispatch_async(dispatch_get_main_queue(), ^{ + self_.kb_ulHandledFlag = YES; + }); } -// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问。 -// 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView,并在其右上角提供删除按钮关闭。 -- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - // 点击上报已下沉到 KBFunctionTagListView(保证能拿到人设 id/name) - // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 - if ([[KBFullAccessManager shared] hasFullAccess]) { - KBTagItemModel *selModel = self.modelArray[indexPath.item]; - [self kb_showStreamTextViewIfNeededWithTitle:selModel.characterName]; - return; - } +// 用户点击功能标签:优先 UL 拉起主App,失败再 +// Scheme;两次都失败则提示开启完全访问。 若已开启“完全访问”,则直接在键盘侧创建 +// KBStreamTextView,并在其右上角提供删除按钮关闭。 +- (void)collectionView:(UICollectionView *)collectionView + didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + // 点击上报已下沉到 KBFunctionTagListView(保证能拿到人设 id/name) + // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 + if ([[KBFullAccessManager shared] hasFullAccess]) { + KBTagItemModel *selModel = self.modelArray[indexPath.item]; + [self kb_showStreamTextViewIfNeededWithTitle:selModel.characterName]; + return; + } - [KBHUD showInfo:KBLocalized(@"处理中…")]; + [KBHUD showInfo:KBLocalized(@"处理中…")]; - UIInputViewController *ivc = KBFindInputViewController(self); - if (!ivc) return; + UIInputViewController *ivc = KBFindInputViewController(self); + if (!ivc) + return; - NSString *title = self.modelArray[indexPath.item].characterName; - NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @""; + NSString *title = self.modelArray[indexPath.item].characterName; + NSString *encodedTitle = + [title stringByAddingPercentEncodingWithAllowedCharacters: + [NSCharacterSet URLQueryAllowedCharacterSet]] + ?: @""; - NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)indexPath.item, encodedTitle]]; - if (!ul) return; + NSURL *ul = [NSURL + URLWithString: + [NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", + KB_UL_LOGIN, (long)indexPath.item, + encodedTitle]]; + if (!ul) + return; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ // 先尝试通过 extensionContext 打开 UL - [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { - if (ok) { + [ivc.extensionContext + openURL:ul + completionHandler:^(BOOL ok) { + if (ok) { return; - } - // UL 失败时,再通过宿主 UIApplication + 自定义 Scheme 兜底 - NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]]; - UIResponder *start = ivc.view ?: (UIResponder *)self; - BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start]; - if (!ok2) { - // 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。 + } + // UL 失败时,再通过宿主 UIApplication + 自定义 Scheme 兜底 + NSURL *scheme = [NSURL + URLWithString: + [NSString + stringWithFormat: + @"%@@//login?src=functionView&index=%ld&title=%@", + KB_APP_SCHEME, (long)indexPath.item, + encodedTitle]]; + UIResponder *start = ivc.view ?: (UIResponder *)self; + BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme + fromResponder:start]; + if (!ok2) { + // 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager + // 引导。 dispatch_async(dispatch_get_main_queue(), ^{ - [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; + [[KBFullAccessManager shared] + ensureFullAccessOrGuideInView:self]; }); - } - }]; - }); + } + }]; + }); } - #pragma mark - Button Actions - (void)onTapPaste { - [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_paste_btn" - pageId:@"keyboard_function_panel" - elementId:@"paste_btn" - extra:nil - completion:nil]; - // 用户点击“粘贴”时才读取剪贴板: - // - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗; - // - iOS15 及以下不会弹窗,直接返回内容; - // 注意:不要在非用户触发的时机主动读取(如 viewDidLoad),否则会造成“立刻弹窗”的体验。 - // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 - if (![[KBFullAccessManager shared] hasFullAccess]) { - // 未开启完全访问:保持原有引导路径 - [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; - return; - } - UIPasteboard *pb = [UIPasteboard generalPasteboard]; - NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗) + [[KBMaiPointReporter sharedReporter] + reportClickWithEventName:@"click_keyboard_function_paste_btn" + pageId:@"keyboard_function_panel" + elementId:@"paste_btn" + extra:nil + completion:nil]; + // 用户点击“粘贴”时才读取剪贴板: + // - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗; + // - iOS15 及以下不会弹窗,直接返回内容; + // 注意:不要在非用户触发的时机主动读取(如 + // viewDidLoad),否则会造成“立刻弹窗”的体验。 权限全部打开(键盘已启用 + + // 完全访问)。在扩展进程中仅需判断“完全访问”。 + if (![[KBFullAccessManager shared] hasFullAccess]) { + // 未开启完全访问:保持原有引导路径 + [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; + return; + } + UIPasteboard *pb = [UIPasteboard generalPasteboard]; + NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗) - if (text.length <= 0) { - // 无可用文本或用户拒绝了粘贴权限;保持占位文案不变 - NSLog(@"粘贴板无可用文本或未授权粘贴"); - [KBHUD showInfo:KBLocalized(@"Clipboard is empty")]; - return; - } + if (text.length <= 0) { + // 无可用文本或用户拒绝了粘贴权限;保持占位文案不变 + NSLog(@"粘贴板无可用文本或未授权粘贴"); + [KBHUD showInfo:KBLocalized(@"Clipboard is empty")]; + return; + } - // 1)把内容真正「粘贴」到当前输入框 -// UIInputViewController *ivc = KBFindInputViewController(self); -// if (ivc) { -// id proxy = ivc.textDocumentProxy; -// [proxy insertText:text]; -// } + // 1)把内容真正「粘贴」到当前输入框 + // UIInputViewController *ivc = KBFindInputViewController(self); + // if (ivc) { + // id proxy = ivc.textDocumentProxy; + // [proxy insertText:text]; + // } - // 2)顺便把最新的剪贴板内容展示在左侧粘贴区按钮上,便于用户确认 - [self kb_updatePasteButtonWithDisplayText:text]; + // 2)顺便把最新的剪贴板内容展示在左侧粘贴区按钮上,便于用户确认 + [self kb_updatePasteButtonWithDisplayText:text]; } #pragma mark - 自动监控剪贴板(复制即弹窗) @@ -728,213 +942,269 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C // - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。 - (void)startPasteboardMonitor { - // 禁用自动读取剪贴板,避免触发系统“允许粘贴”弹窗 + // 禁用自动读取剪贴板,避免触发系统“允许粘贴”弹窗 + return; + // 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志 + if (![[KBFullAccessManager shared] hasFullAccess]) return; - // 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志 - if (![[KBFullAccessManager shared] hasFullAccess]) return; - if (self.pasteboardTimer) return; - KBWeakSelf - self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) { - __strong typeof(weakSelf) self = weakSelf; if (!self) return; - UIPasteboard *pb = [UIPasteboard generalPasteboard]; - NSInteger cc = pb.changeCount; - if (cc <= self.lastHandledPBCount) return; // 没有新复制 - self.lastHandledPBCount = cc; // 标记已处理,避免重复 + if (self.pasteboardTimer) + return; + KBWeakSelf self.pasteboardTimer = [NSTimer + scheduledTimerWithTimeInterval:0.5 + repeats:YES + block:^(NSTimer *_Nonnull timer) { + __strong typeof(weakSelf) self = weakSelf; + if (!self) + return; + UIPasteboard *pb = + [UIPasteboard generalPasteboard]; + NSInteger cc = pb.changeCount; + if (cc <= self.lastHandledPBCount) + return; // 没有新复制 + self.lastHandledPBCount = + cc; // 标记已处理,避免重复 - // 实际读取触发系统弹窗(iOS16+) - NSString *text = pb.string; - // 有文字 -> 仅展示文字;无文字/非文本 -> 恢复图标 + 原占位文案 - [self kb_updatePasteButtonWithDisplayText:text]; - }]; + // 实际读取触发系统弹窗(iOS16+) + NSString *text = pb.string; + // 有文字 -> 仅展示文字;无文字/非文本 -> + // 恢复图标 + 原占位文案 + [self + kb_updatePasteButtonWithDisplayText:text]; + }]; } - (void)stopPasteboardMonitor { - [self.pasteboardTimer invalidate]; - self.pasteboardTimer = nil; + [self.pasteboardTimer invalidate]; + self.pasteboardTimer = nil; } - (void)didMoveToWindow { - [super didMoveToWindow]; - [self kb_refreshPasteboardMonitor]; + [super didMoveToWindow]; + [self kb_refreshPasteboardMonitor]; } - (void)setHidden:(BOOL)hidden { - BOOL wasHidden = self.isHidden; - [super setHidden:hidden]; - if (wasHidden != hidden) { - [self kb_refreshPasteboardMonitor]; - } + BOOL wasHidden = self.isHidden; + [super setHidden:hidden]; + if (wasHidden != hidden) { + [self kb_refreshPasteboardMonitor]; + } } // 根据窗口可见性与完全访问状态,统一启停粘贴板监控 - (void)kb_refreshPasteboardMonitor { - BOOL visible = (self.window && !self.isHidden); - if (visible && [[KBFullAccessManager shared] hasFullAccess]) { - [self startPasteboardMonitor]; - } else { - [self stopPasteboardMonitor]; - } + BOOL visible = (self.window && !self.isHidden); + if (visible && [[KBFullAccessManager shared] hasFullAccess]) { + [self startPasteboardMonitor]; + } else { + [self stopPasteboardMonitor]; + } } - (void)kb_fullAccessChanged { - dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; }); + dispatch_async(dispatch_get_main_queue(), ^{ + [self kb_refreshPasteboardMonitor]; + }); } - (void)onTapDelete { - NSLog(@"点击:删除"); - [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_delete_btn" - pageId:@"keyboard_function_panel" - elementId:@"delete_btn" - extra:nil - completion:nil]; - UIInputViewController *ivc = KBFindInputViewController(self); - id proxy = ivc.textDocumentProxy; - [[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy]; - [[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput - after:proxy.documentContextAfterInput]; - [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1]; - [[KBInputBufferManager shared] applyHoldDeleteCount:1]; + NSLog(@"点击:删除"); + [[KBMaiPointReporter sharedReporter] + reportClickWithEventName:@"click_keyboard_function_delete_btn" + pageId:@"keyboard_function_panel" + elementId:@"delete_btn" + extra:nil + completion:nil]; + UIInputViewController *ivc = KBFindInputViewController(self); + id proxy = ivc.textDocumentProxy; + [[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy]; + [[KBInputBufferManager shared] + prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput + after:proxy + .documentContextAfterInput]; + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy + count:1]; + [[KBInputBufferManager shared] applyHoldDeleteCount:1]; } - (void)onTapClear { - NSLog(@"点击:清空"); - [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_clear_btn" - pageId:@"keyboard_function_panel" - elementId:@"clear_btn" - extra:nil - completion:nil]; - [self.backspaceHandler performClearAction]; + NSLog(@"点击:清空"); + [[KBMaiPointReporter sharedReporter] + reportClickWithEventName:@"click_keyboard_function_clear_btn" + pageId:@"keyboard_function_panel" + elementId:@"clear_btn" + extra:nil + completion:nil]; + [self.backspaceHandler performClearAction]; } - (void)onTapSend { - NSLog(@"点击:发送"); - [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_send_btn" - pageId:@"keyboard_function_panel" - elementId:@"send_btn" - extra:nil - completion:nil]; - [[KBBackspaceUndoManager shared] registerNonClearAction]; - // 发送:插入换行。大多数聊天类 App 会把回车视为“发送” - UIInputViewController *ivc = KBFindInputViewController(self); - id proxy = ivc.textDocumentProxy; - [proxy insertText:@"\n"]; - [[KBInputBufferManager shared] appendText:@"\n"]; + NSLog(@"点击:发送"); + [[KBMaiPointReporter sharedReporter] + reportClickWithEventName:@"click_keyboard_function_send_btn" + pageId:@"keyboard_function_panel" + elementId:@"send_btn" + extra:nil + completion:nil]; + [[KBBackspaceUndoManager shared] registerNonClearAction]; + // 发送:插入换行。大多数聊天类 App 会把回车视为“发送” + UIInputViewController *ivc = KBFindInputViewController(self); + id proxy = ivc.textDocumentProxy; + [proxy insertText:@"\n"]; + [[KBInputBufferManager shared] appendText:@"\n"]; } #pragma mark - Lazy - (KBFunctionBarView *)barViewInternal { - if (!_barViewInternal) { - _barViewInternal = [[KBFunctionBarView alloc] init]; - _barViewInternal.delegate = self; // 顶部功能Bar事件下发到本View - } - return _barViewInternal; + if (!_barViewInternal) { + _barViewInternal = [[KBFunctionBarView alloc] init]; + _barViewInternal.delegate = self; // 顶部功能Bar事件下发到本View + } + return _barViewInternal; } #pragma mark - KBFunctionBarViewDelegate -- (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index { - // 将事件继续透传给上层(如键盘控制器),用于切换界面或其它业务 - if ([self.delegate respondsToSelector:@selector(functionView:didTapToolActionAtIndex:)]) { - [self.delegate functionView:self didTapToolActionAtIndex:index]; - } +- (void)functionBarView:(KBFunctionBarView *)bar + didTapLeftAtIndex:(NSInteger)index { + // 将事件继续透传给上层(如键盘控制器),用于切换界面或其它业务 + if ([self.delegate respondsToSelector:@selector(functionView: + didTapToolActionAtIndex:)]) { + [self.delegate functionView:self didTapToolActionAtIndex:index]; + } } -- (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index { - // 右侧按钮点击,如收藏/宫格等,按需继续向外抛出(这里暂不定义单独协议方法,可在此内部处理或扩展) - if ([self.delegate respondsToSelector:@selector(functionView:didRightTapToolActionAtIndex:)]) { - [self.delegate functionView:self didRightTapToolActionAtIndex:index]; - } +- (void)functionBarView:(KBFunctionBarView *)bar + didTapRightAtIndex:(NSInteger)index { + // 右侧按钮点击,如收藏/宫格等,按需继续向外抛出(这里暂不定义单独协议方法,可在此内部处理或扩展) + if ([self.delegate respondsToSelector:@selector(functionView: + didRightTapToolActionAtIndex:)]) { + [self.delegate functionView:self didRightTapToolActionAtIndex:index]; + } } - (KBFunctionPasteView *)pasteViewInternal { - if (!_pasteViewInternal) { - _pasteViewInternal = [[KBFunctionPasteView alloc] init]; - } - return _pasteViewInternal; + if (!_pasteViewInternal) { + _pasteViewInternal = [[KBFunctionPasteView alloc] init]; + } + return _pasteViewInternal; } - (KBFunctionTagListView *)tagListView { - if (!_tagListView) { - _tagListView = [[KBFunctionTagListView alloc] init]; - _tagListView.delegate = (id)self; - } - return _tagListView; + if (!_tagListView) { + _tagListView = [[KBFunctionTagListView alloc] init]; + _tagListView.delegate = (id)self; + } + return _tagListView; } - (UIView *)rightButtonContainer { - if (!_rightButtonContainer) { - _rightButtonContainer = [[UIView alloc] init]; - _rightButtonContainer.backgroundColor = [UIColor clearColor]; - } - return _rightButtonContainer; + if (!_rightButtonContainer) { + _rightButtonContainer = [[UIView alloc] init]; + _rightButtonContainer.backgroundColor = [UIColor clearColor]; + } + return _rightButtonContainer; } -- (UIButton *)buildRightButtonWithTitle:(NSString *)title color:(UIColor *)color { - UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; - btn.backgroundColor = color; - btn.layer.cornerRadius = 8.0; - btn.layer.masksToBounds = YES; - btn.titleLabel.font = [KBFont medium:13]; - [btn setTitle:title forState:UIControlStateNormal]; - [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - return btn; +- (UIButton *)buildRightButtonWithTitle:(NSString *)title + color:(UIColor *)color { + UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; + btn.backgroundColor = color; + btn.layer.cornerRadius = 8.0; + btn.layer.masksToBounds = YES; + btn.titleLabel.font = [KBFont medium:13]; + [btn setTitle:title forState:UIControlStateNormal]; + [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + return btn; } - (UIButton *)pasteButtonInternal { - if (!_pasteButtonInternal) { - _pasteButtonInternal = [self buildRightButtonWithTitle:KBLocalized(@"Paste") color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]]; - [_pasteButtonInternal addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside]; - } - return _pasteButtonInternal; + if (!_pasteButtonInternal) { + _pasteButtonInternal = + [self buildRightButtonWithTitle:KBLocalized(@"Paste") + color:[UIColor colorWithRed:0.13 + green:0.73 + blue:0.60 + alpha:1.0]]; + [_pasteButtonInternal addTarget:self + action:@selector(onTapPaste) + forControlEvents:UIControlEventTouchUpInside]; + } + return _pasteButtonInternal; } - (UIButton *)deleteButtonInternal { - if (!_deleteButtonInternal) { - _deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom]; - _deleteButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8]; - _deleteButtonInternal.layer.cornerRadius = 8.0; - _deleteButtonInternal.layer.masksToBounds = YES; - [_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] forState:UIControlStateNormal]; + if (!_deleteButtonInternal) { + _deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom]; + _deleteButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8]; + _deleteButtonInternal.layer.cornerRadius = 8.0; + _deleteButtonInternal.layer.masksToBounds = YES; + [_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] + forState:UIControlStateNormal]; - [_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside]; - [self.backspaceHandler bindDeleteButton:_deleteButtonInternal showClearLabel:NO]; - } - return _deleteButtonInternal; + [_deleteButtonInternal addTarget:self + action:@selector(onTapDelete) + forControlEvents:UIControlEventTouchUpInside]; + [self.backspaceHandler bindDeleteButton:_deleteButtonInternal + showClearLabel:NO]; + } + return _deleteButtonInternal; } - (UIButton *)clearButtonInternal { - if (!_clearButtonInternal) { - _clearButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom]; - _clearButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8]; - _clearButtonInternal.layer.cornerRadius = 8.0; - _clearButtonInternal.layer.masksToBounds = YES; - _clearButtonInternal.titleLabel.font = [KBFont medium:13]; - [_clearButtonInternal setTitle:KBLocalized(@"Clear") forState:UIControlStateNormal]; - [_clearButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; - [_clearButtonInternal addTarget:self action:@selector(onTapClear) forControlEvents:UIControlEventTouchUpInside]; - } - return _clearButtonInternal; + if (!_clearButtonInternal) { + _clearButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom]; + _clearButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8]; + _clearButtonInternal.layer.cornerRadius = 8.0; + _clearButtonInternal.layer.masksToBounds = YES; + _clearButtonInternal.titleLabel.font = [KBFont medium:13]; + [_clearButtonInternal setTitle:KBLocalized(@"Clear") + forState:UIControlStateNormal]; + [_clearButtonInternal setTitleColor:[UIColor blackColor] + forState:UIControlStateNormal]; + [_clearButtonInternal addTarget:self + action:@selector(onTapClear) + forControlEvents:UIControlEventTouchUpInside]; + } + return _clearButtonInternal; } - (UIButton *)sendButtonInternal { - if (!_sendButtonInternal) { - _sendButtonInternal = [self buildRightButtonWithTitle:KBLocalized(@"Send") color:[UIColor colorWithHex:0x02BEAC]]; - [_sendButtonInternal addTarget:self action:@selector(onTapSend) forControlEvents:UIControlEventTouchUpInside]; - } - return _sendButtonInternal; + if (!_sendButtonInternal) { + _sendButtonInternal = + [self buildRightButtonWithTitle:KBLocalized(@"Send") + color:[UIColor colorWithHex:0x02BEAC]]; + [_sendButtonInternal addTarget:self + action:@selector(onTapSend) + forControlEvents:UIControlEventTouchUpInside]; + } + return _sendButtonInternal; } - #pragma mark - Expose -- (UICollectionView *)collectionView { return self.tagListView.collectionView; } +- (UICollectionView *)collectionView { + return self.tagListView.collectionView; +} //- (NSArray *)items { return self.itemsInternal; } -- (KBFunctionBarView *)barView { return self.barViewInternal; } -- (KBFunctionPasteView *)pasteView { return self.pasteViewInternal; } -- (UIButton *)pasteButton { return self.pasteButtonInternal; } -- (UIButton *)deleteButton { return self.deleteButtonInternal; } -- (UIButton *)clearButton { return self.clearButtonInternal; } -- (UIButton *)sendButton { return self.sendButtonInternal; } +- (KBFunctionBarView *)barView { + return self.barViewInternal; +} +- (KBFunctionPasteView *)pasteView { + return self.pasteViewInternal; +} +- (UIButton *)pasteButton { + return self.pasteButtonInternal; +} +- (UIButton *)deleteButton { + return self.deleteButtonInternal; +} +- (UIButton *)clearButton { + return self.clearButtonInternal; +} +- (UIButton *)sendButton { + return self.sendButtonInternal; +} #pragma mark - Find Owner Controller