opencode会话管理

问题背景

使用 opencode 时,退出后会提示一句:

1
2
3
4
5
6
7

█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀

Session New session - 2026-05-08T09:32:17.883Z
Continue opencode -s ses_xxxxxxxxxxxxxxxxxxxx

但如果终端窗口关得太快,或忘记复制,这个会话 ID 就丢了,导致无法恢复之前的会话。

阅读全文 →

cc-connect

问题背景

日常开发中,AI 编码工具(如 Claude Code、Cursor)只能在电脑终端使用,离开电脑就无法交互。当你在手机上想让人 AI 执行任务(如查日志、改代码、跑脚本),或者想通过微信/Telegram 远程操控时,就缺少一个桥梁。

cc-connect 正是解决这个问题的工具——它连接 AI 编码工具和即时通讯软件(微信、Telegram),让你在手机上发消息,电脑上的 AI 接收后执行并返回结果。

阅读全文 →

日志系统

前言:主要事项

1、异常数据的发现及补充、日志分类优化
2、日志数据的文件保存、滚动存储
3、日志文件上传
4、日志回捞

一、日志收集

1、日志分类

1.1、日志目标

序号 目标(Target)
1 app
2 sdk 三方库
3 h5 网页

1.2、日志类型

序号 类型 描述
1 api_app app中的网络请求
2 api_app_cache app中的网络请求的网络缓存请求
3 api_buriedPoint 埋点的网络请求
4 sdk_other sdk的各种事件(初始化等)
5 sdk_api sdk中的网络请求
6 dart 语法
7 widget 视图(布局像素越界等)
8 click_other 点击、
9 click_share 分享
10 native_route 路由/跳转
11 h5_route 与网页跳转有关
12 h5_js js交互
13 monitor_network 监控:网络类型变化
14 monitor_lifecycle 监控:生命周期变化
15 buriedPoint_other 埋点数据生成等
16 im IM
17 heartbeat 心跳
18 other 其他

2、日志等级

序号 目标(Level) 描述
1 Normal 正常信息(目前用于请求开始)
2 Success 成功信息(目前用于请求结束:成功)
3 Warning 警告信息(目前用于请求结束:报错)
4 Error 错误日志(目前用于请求结束:失败)
5 Dangerous 危险(处理白屏等) 一般会进行额外的埋点

3、日志整合归类

序号 列表 标志 包含
1 全部 all 所有
2 警告 warning 所有的警告
3 错误 error 所有的错误
4 接口 api api_app、api_cache(不包括sdk_api、api_buriedPoint)
5 点击 click click_share、click_other、h5_js
6 路由 route navite_route、h5_route
7 网页 H5 h5_route、h5_js
8 sdk sdk sdk_api、sdk_other
9 code code dart、widget
10 埋点 buriedPoint api_buriedPoint、buriedPoint_other
11 监控 monitor monitor_lifecycle、monitor_network
12 其他 other other
13 api结果 api_result type=api_app & level != Normal
14 im im 本地缓存消息、历史消息、收到的消息等
15 心跳 heartbeat
devtool_log_home_page

阅读全文 →

架构模式-③组件化

一、概念了解

1、组件化

就是**”基础库”或者“基础组件”,**意思是把代码重复的部分提炼出一个个组件供给功能使用。

​ 使用:Dialog,各种自定义的UI控件、能在项目或者不同项目重复应用的代码等等。

​ 目的:复用,解耦。

​ 依赖:组件之间低依赖,比较独立。

​ 架构定位:纵向分层(位于架构底层,被其他层所依赖)。

2、模块化

就是**”业务框架”或者“业务模块”**,也可以理解为“框架”,意思是把功能进行划分,将同一类型的代码整合在一起,所以模块的功能相对复杂,但都同属于一个业务。

​ 使用:按照项目功能需求划分成不同类型的业务框架(例如:注册、登录、外卖、直播…..)

​ 目的:隔离/封装 (高内聚)。

​ 依赖:模块之间有依赖的关系,可通过路由器进行模块之间的耦合问题。

​ 架构定位:横向分块(位于架构业务框架层)。

3、总结

​ 其实组件相当于,把一些能在项目里或者不同类型项目中可复用的代码进行工具性的封装。

​ 而模块相应于业务逻辑模块,把同一类型项目里的功能逻辑进行进行需求性的封装。

二、组件化的必要性/产生背景

1、产生背景:

所有模块代码都编写在一个项目中,在项目越来越大后,测试/使用某个模块或功能,需要编译运行整个项目,麻烦

2、组件化思路:

将每个模块作为一个组件,加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。)

3、组件化好处:

业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。

项目可维护性更强,提高开发效率。

更好排查问题,某个组件出现问题,直接对组件进行处理。

开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。

方便集成,项目需要哪个模块直接通过CocoaPods集成即可。

三、组件化设计思路1:有管理中心

完整的项目:https://github.com/dvlproad/033-Data-Notification-iOS

1、核心思想

启动时候注册关系表,执行时候遍历关系表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 核心思想1:注册,以统一管理,后续调用时候,从注册表中查询并执行对应的操作
void register(url, handle) {
model.url = url;
model.handle = handle;
models.add(model); // 问题1:用什么管理所有数据
}

// 核心思想2:执行,遍历在注册表中的所有操作,找到对应的操作后进行执行
void exec(url) {
for (var model in models) {
if (model.url == url) {
model.handle();
break;
}
}
}

2、实现方案

2.1、方案1:Url+block 路由器

缺点:入参,取参不明显。

2.2、方法2:protocal - class

哈希的本质 哈希冲突的解决:链地址法和开放寻址法。

问:使用多代理模式实现一个类似通知的功能,请问应该用什么来什么结构来存储代理和类的结构?

答:在多代理(Multiple Delegates)模式下,要存储多个代理对象,并且这些代理对象通常是弱引用,以防止循环引用。实现一个类似通知的功能,可以考虑使用 NSPointerArrayNSMapTable 来存储代理对象。如果你只是想管理多个代理对象,NSPointerArray 是更简单的选择。如果你需要将某个对象(如 UIViewController)与多个代理进行关联NSMapTable 更合适,所以我们选择 NSMapTable

将要执行的方法统一放到类中,通过protocal或者类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据 protocal 查找 class
// 方法1(可一对多):为 protocal 添加监听者数组属性(通过runtime,形成类似于 protocal.listeners ),
NSHashTable *protocalListeners = objc_getAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER); // 找到之前绑定到这个protocal上的监听者列表。
objc_setAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER, protocalListeners, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

注册的时候 [protocalListeners addObject:listener];

// 方法2(只能一对一):以 protocal 为 key , 通过 protocal 存取 module 模块(模块自身遵循协议)
// 创建一个NSMapTable实例,其中键是强引用,值是弱引用
NSMapTable<NSString *, NSObject *> *weakValueMapTable = [NSMapTable strongToWeakObjectsMapTable];

// 向哈希表中添加一个键值对
NSString *key = @"someKey";
NSObject *value = [[NSObject alloc] init];
[weakRefHashTable setObject:value forKey:key];

模块协议拆分为多个子协议,用户

网络处理协议、数据管理协议(单独)、用户界面更新协议

改进思想

1
2
3
4
5
6
7
8
9
10
// 1、发起请求(LoginViewController)
id<OrderModulePublic> moduler = [[CJMoudleManager shared] modulerForProtocal:@protocal(OrderModulePublic)];
UIViewController *vc = [moduler orderViewControllerWithPageType:xxx];

// 2、实现请求(TSOrderModuleImpl)
[CJProtocolCenter addModule:self forProtocol:@protocol(OrderModulePublic)];
#pragma mark - OrderModulePublic
- (UIViewController *)orderViewControllerWithPageType:(int)pageType

}

上述代码还可以演变成实现通知的一种形式(当注入的类中,有多个类都遵循了(实际上不应该出现)模块协议时候)

1
2
3
4
5
6
7
8
9
10
11
12
// 1、发起请求(LoginViewController)
NSMutableSet<id<CJUserServiceProtocolForModule>> listener = [[CJMoudleManager shared] listenersForProtocal:@protocal(CJUserServiceProtocolForModule)];
for (id<CJUserServiceProtocolForModule> listener in listeners) {
[listener userNameUpadte:userName];
}

// 2、实现请求(OrderHomeViewController)
[CJProtocolCenter addModule:self forProtocol:@protocol(CJUserServiceProtocolForModule)];
#pragma mark - CJUserServiceProtocolForModule
- (void)userNameUpadte:(NSString *)userName

}

下列图片来源于 《架构分层.graffle》 中的【三、模块间设计】

跳转的使用示例:

modular_CJProtocalManager 跳转

通知的实现示例:

modular_CJProtocalManager 通知

跳转 + 数据变化后内部通知的实现示例:

modular_CJProtocalManager 跳转 + 数据变化后内部通知

四、组件化设计思路2:无管理中心

项目示例:CJStandardProject 中 CTMediator+CJDemoModuleMain.h

1、核心思想

核心思想:直接执行,让系统通过反射来找到要响应事件的类和方法

1
2
3
void exec(target, action, params) {

}

2、实现方案

2.1、方案1:CTMediator target+action+param

实现示例:

modular_CTMediator target+action+param

2.2、方案2:XXXService+Implemention

modular_XXXService+Implemention

五、组件化实施过程的思考

1、架构分层

阅读全文 →

页面加载体验优化

一、页面初始加载优化

1、数据携带/数据参照

通过前一个页面的已获得数据,对所进入的新页面中的数据进行预填充。

eg1:商品列表 —> 商品详情:使用数据携带

eg2:愿望星count个数:通过count的0与非0,知晓所进入的页面初始状态更有可能是哪种状态

1、如果所进入的页面没有缓存数据,则携带的数据在进入的时候直接使用,后台接口返回实时数据后,再更新

2、如果所进入的页面有缓存数据,则携带的数据只能给缓存数据,而不能是后台接口返回的实时数据

2、管理的数据的缓存

2.1、普通数据的缓存

通过缓存框架,将数据缓存起来(key需携带uid),下次界面展示时候,优先从缓存中获取。

附:缓存框架详见:flutter_cache_kit使用文档

2.2、管理数据的缓存

建立Service层,管理用户所有数据的变动。

好处:与普通数据的缓存相比,能够在将来增加数据变动时候,通过本地通知系统,告知相关页面更新相应数据,而不用等到重新下载后才能显示已知道会更新的数据。

使用要点:

修改的时候,同步数据;下次界面需要数据,优先从用户管理服务中获取初始数据

eg1:用户愿望单收藏、商品收藏、品牌收藏、足迹数据

eg2:用户会员中心数据

3、默认数据

如果没有从前一页携带数据过来,则使用与产品约定的默认数据来加载。

1
2
3
if (携带数据) return 携带的.orderCount; 
if (管理数据) return servier.orderCount;
if (默认数据) return default_orderCount;

4、预览页:美团/饿了么的骨架屏灰底效果

对携带的数据与默认数据中相同的对象,使用携带的数据替换掉,一起整合成初始默认数据(携带的数据优先级高)。

5、添加请求加载圈

6、网络接口的缓存数据

页面处理优化及处理过程中的内容(图片)加载优化如下图所示:

图片来源可查看 《app启动与页面加载.graffle》中的【二、页面加载】版面

page_and_image_load_optimize1

从上图中,可以看出弱网情况下,我们可以针对不同的网络:做不同超时,重试设置,以及取不同质量的图片数据。

二、图片优化

图片优化,请查看我的另一篇文章:图片框架.md

图片高磁盘占用,请查看我的另一篇文章:《高磁盘占用的排查与优化.md

三、网络优化

api数据缓存,请查看我的另一篇文章:《网络框架.md

弱网优化,请查看我的另一篇文章:《弱网优化空间探索.md

阅读全文 →

架构模式-①概览

前言:架构模式、框架、设计模式的概念

iOS—架构模式、框架、设计模式的理解

架构模式:架构模式的出现是为了管理复杂的应用程序,这样可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC架构MVVM架构属于此类。

框架:这个最好理解了,通常是代码重用。框架与设计模式的概念容易弄混,两者有相似之处,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述规范,它比框架更抽象;框架为已经解决问题的具体实现方法,能直接执行或复用;设计模式是比框架更小的元素,一个框架中往往含有一种或多种设计模式。Xcode自带的FoundationUIKit,以及我们经常使用的AFNetworkingMJExtensionSVProgressHUD就属于这一类。

设计模式:设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范。增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式迭代器模式策略模式就属于这一类。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。

三者关系:架构(动词)>框架>设计模式。

软件通过架构,可以设计出很多不同的框架。在一个框架中,也可以使用很多的设计模式。设计模式不是哪儿哪儿都可以用的,只有当出现了某一特定的问题时,才利用设计模式去解决。设计模式不是用的越多越好,在维护的时候,过多的设计模式会极大的增添维护成本。

一、架构模式

1、架构模式的演变

1.1、MVC

1、架构的设计其实是为了更好的维护和迭代,而不是只考虑眼前当下的开发。

2、最基本的架构是MVC,试想下如果我一个应用的所有功能都是像”关于“功能的,那我还需要其他架构干嘛?显然MVC就已经很足够,而且还显得不冗余(这里指的是设计或者粒度不冗余)。

3、其他的架构都是从MVC演变而来。再讲其他架构的演变之前,我们先明确最原始的MVC代码是如何的。

①、首先M主要负责数据、V主要负责视图、C主要负责数据的视图显示。而这里的M是原始的瘦Model

所以,我们拿常见的”我的”页面来举例,部分原始的MVC代码一般为如下样子:

1
2
3
4
5
6
7
8
9
UserModel


View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = user.sex == 1? "女" :"男";

4、在MVC+瘦Model的演变下,还有MVC+胖Model

胖Model就是瘦Model+部分弱业务逻辑(这些弱业务重复一般都是经常出现,或者说是要求可复用性的,如根据枚举获取性别字符串)。它使得Controller可以从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上.

1
2
3
4
5
6
7
8
9
UserModel
+ (NSString *)sexStringFrom:(int)sex;

View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserModel sexStringFrom:user.sex];

1.2、MVP?

5、有时候在MVC+瘦Model的演变下,不是转成MVC+胖Model,而是MVC+瘦Model+Helper

其实这个思路已经很接近MVP了、但是还差提点。MVP还需要为View提供数据

1
2
3
4
5
6
7
8
9
10
UserModel

View

UserHelper
+ (NSString *)sexStringFrom:(int)sex;

ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserHelper sexStringFrom:user.sex];

1.3、MVVM

6、关于MVC+瘦Model+Helper,还可以改为MVC+瘦Model+ViewModel

刚才的胖Model只从Controller移植走了一些简单的弱业务。

而ViewModel则干脆把数据的处理全部从Controller移植了出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
UserModel

View

UserViewModel
name
sexString
- (id)initWithName:(NSString *)name sex:(int)sex;


ViewController
self.userInfoView.name = userViewModel.name;
self.userInfoView.sexString = userViewModel.sexString;

1.4、MVCS -> MVVM+S

拿有数据操作的”购物车“页面来说,

假设其除了有网络数据请求外,还有本地数据库处理。(附:购物车是否需要本地数据库,个人认为是基于服务端的压力来考虑,不然一般是放在服务端合适点。压力设想如下,千万级用户,购物车加减商品频繁,假设放在后台,则操作和计算也就频繁,压力也就大了。如果放在前端,则后台只需要负责是否可加减商品的检验,而不用再建购物车数据表和计算价格等,压力就小了。)

每个页面一般都会有网络请求,这些网络请求最原始的时候,是放在ViewController,也就是MVC的C中。

为了避免MVC中的ViewController后期变得十分臃肿。我们肯定要在MVC的基础上额外增加一层来处理这个。

综合考虑后,我们选用MVCS来处理。

其中S代表着Store,且我们这里的S代表着是整个模块的S,而不是每个页面都有一个S。其下层又包含着Storage和Service,即 数据库DB部分网络Network部分

Store类名,个人习惯是使用Manager来命令。如UserManager。

MVCS1

所以,最终我们选用的是 MVCS+MVVM 或者说 MVVM+S。

S层(service 层)提供一种和外界(比如远程服务 API 或文件系统)交互的独立机制。

MVVM+S 纵向的数据流就不用多说,横向的数据流,我们采用自定义的类系统通知。区别在于它有着系统Notification的一对多方便,又有着delegate的接口对接方便。

MVCS2

抛开数据共享问题(如每个请求都要带userToken),每个页面

二、架构分层(Architectural Layering)

架构分层是一种将系统分解为多个逻辑层次的方法,每一层都有特定的职责和功能。

  • 架构分层有助于实现关注点分离(Separation of Concerns),使得系统的不同部分可以独立开发和维护。
  • 典型的分层包括表示层(或用户界面层)、业务逻辑层、数据访问层等。

    架构分层图片来源于 《架构分层.graffle》中的【一、架构分层】

其他图片见我的项目 CJStandardProject 中的 Screenshots

其他参考文章

iOS:

三、常见的项目目录结构

常见的项目目录结构:

框架①项目目录结构

项目目录结构规范说明 请点击本链接跳转下文附录进行查看

完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

四、页面与服务类设计

1、核心理念

页面执行自己的动作,调用Service服务类。无需关心数据请求、数据请求结束后是否需要缓存等等。

Service服务中心,内部包含请求和可能的数据存储等处理,不对外暴露。

2、设计举例

页面调用服务类举例

2.1、设计

arc_layer_2

图片来源于 《架构分层.graffle》中的【二、页面与服务类设计】

2.2、服务类中的代码演示

阅读全文 →

开发工具

背景

问:我不要频繁安装+卸载来安装我想要的包。我能不能同时装一个测试环境和正式环境?

答:不行。安装两个包,严格上来讲,不用想了,不行。具体原因涉及应用id,及app根据该id配置了各种三方key环境(除非你连三方的也都额外提供一套),太详细的道理我就不讲了。”不行“就对了。但是,我能让你安装一个包,却使用不同环境的功能。或者额外安装一个除与应用id相关的功能外其他皆一样的安装包

一、切换环境常见的场景为:

1、测试时候:换环境不用一直下载;

2、演示时候:某个环境使用不了,不用重新下载;

3、抓包时候:想切换代理,不用重新打包安装;

见下文:app环境与切换

二、额外安装一个除与应用id相关的功能外其他皆一样的安装包的使用场景:

1、app中不能接受切换环境,即严格限制一个包只能有一个环境

不允许切换环境的考虑原因:

测试包不允许切到生产:怕脏数据

生产包不允许切到测试:测试环境不是外界用户真正需要的

见我的另一篇文章:《iOS的重签名.md

一、功能开关

tool_entrance_0

tool_entrance_2log

二、app安装包与切换

1、app安装包信息

devtool_appinfo_0package

1.1、换包:app下载页

devtool_appinfo_0package_downloadurl

1.2、版本记录:历史版本记录页

devtool_appinfo_0package_version_records

2、各文件目录及大小

devtool_appinfo_0package_dirsize

三、app环境与切换

package_env_test package_env_product

流程图:《开发工具(含环境切换等).graffle》中的【一、切换环境】版面

devtool_appenv_home

1、切换环境

2、切换平台

3、添加代理

devtool_appenv_3proxy_1 devtool_appenv_3proxy_2

四、设备信息

devtool_device_0devicetoken

devtool_deviceinfo_0

1、设备网络类型

2、设备屏幕大小

2.1、设备适配验证

devtool_device_uiadapt_check

3、设备ip及代理

4、设备埋点参数

未截取到的其他埋点参数见我的另一篇文章:《埋点规范_结构.md

5、是否模拟器

devtool_device_simulator_check

五、用户相关

devtool_user_info

六、调试页面

支付 支付宝
认证 实名认证、真人认证
选择器 生日选择、活动时间选择、项目多选
分享
关键页面入口 个人主页、发布页、引导页、完善性别/生日
位置相关 地图位置选择

快捷入口

1、检查更新

devtool_debug_0checkversion_1

devtool_debug_0checkversion_2

2、网页测试

devtool_debug_2webview_0home

2.1、网页快捷入口

devtool_debug_2webview_1url_entrance

2.2、白屏测试

devtool_debug_2webview_2blank_test.gif

2.3、JS测试

h5js.md

devtool_debug_2webview_3js

2.4、Route测试

h5_open_app.md

devtool_debug_2webview_4route

七、开发工具安全性处理

见我在”安全”专题里的另一篇文章:《开发工具安全性处理

八、脚本工具

目前的脚本操作如下:

qtool_ui_home

其中自定义的脚本操作有如下:

qtool_ui_custom_home

附1:Charles

文档:《Charles.md

附2:代理的实现(Flutter版)

dio添加代理

1
2
3
4
5
6
7
8
9
10
11
   (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
// config the http client
client.findProxy = (uri) {
CommonUtils.log('fcs-myproxy-begin:${uri is Uri} uri:${uri.toString()}');
return "PROXY 192.168.10.47:8888";//如果设置代理(localhost,127.0.0.1这样的是不行的。必须是电脑的ip)
// return 'DIRECT';// 如果不设置代理
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;//忽略证书
};

End