1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:09,038
这一集聊两件连在一起的事。

3
00:00:09,038 --> 00:00:15,192
第一件，外部 MCP server 的工具，是怎么变成模型眼睛里看得见的名字的。

4
00:00:15,192 --> 00:00:22,307
第二件，你要从别的产品搬到 Codex，去年攒的 hook、MCP 和插件，到底能带过来多少。

5
00:00:22,307 --> 00:00:25,228
先说第一个问题为什么值得关心。

6
00:00:25,228 --> 00:00:29,435
你把 Codex 写成 MCP server，填进 Cursor 的配置里。

7
00:00:29,435 --> 00:00:33,137
这时候 Cursor 是客户端，Codex 是服务端。

8
00:00:33,137 --> 00:00:41,310
如果 Codex 在这一次的工具列表里，把自己会话内部挂着的内部工具也一并交出去，那会怎么样。

9
00:00:41,310 --> 00:00:45,264
IDE 调一次 Codex，就直接摸到了内部能力。

10
00:00:45,264 --> 00:00:49,951
权限边界从调一次 Codex，扩成了直接调内部工具。

11
00:00:49,951 --> 00:00:54,735
这是一次很典型的越权扩散，而且它发生得非常安静。

12
00:00:54,735 --> 00:00:56,875
第二个问题同样现实。

13
00:00:56,875 --> 00:01:00,180
有人告诉你会自动从另一家搬家。

14
00:01:00,180 --> 00:01:03,052
你以为是把整个配置目录拷过来。

15
00:01:03,052 --> 00:01:08,978
结果第二天一看，hook 少了一条，MCP 少了一台，memory 没出现在清单里。

16
00:01:08,978 --> 00:01:12,187
少掉的那条 hook 用了提示词类型。

17
00:01:12,187 --> 00:01:16,862
少掉的那台 MCP，命令里写了环境变量占位符。

18
00:01:16,862 --> 00:01:19,891
这一集就把这两个口子讲清楚。

19
00:01:19,891 --> 00:01:25,000
Codex 的做法是，对外只交两个入口，对内另做一份目录。

20
00:01:25,000 --> 00:01:30,168
模型看见的是翻译过的名字，撞了就哈希，原名留给协议。

21
00:01:30,168 --> 00:01:36,069
搬家的做法一样克制，对得上的字段走拷或改，对不上的直接丢掉。

22
00:01:36,220 --> 00:01:38,513
先看 crate 是怎么拆的。

23
00:01:38,463 --> 00:01:47,033
这一层很关键，很多人看资料会被绕进去，因为早期资料常把这两套画成同一个运行时的两张脸。

24
00:01:47,033 --> 00:01:51,648
服务端这一侧从标准输入读行，一行一条 JSON。

25
00:01:51,648 --> 00:01:54,701
初始化的时候只打开工具能力。

26
00:01:54,701 --> 00:02:00,182
工具列表这一层写死两个名字，一个叫 codex，一个叫 codex-reply。

27
00:02:00,182 --> 00:02:05,470
codex 这个工具会启动线程，嵌套线程再起自己的运行时。

28
00:02:05,470 --> 00:02:09,172
连接集和外部 server 的目录，归另一个 crate 管。

29
00:02:09,172 --> 00:02:16,011
也就是说，会话内部挂的那些外部店，跟交给 IDE 的那两个入口，是两套东西。

30
00:02:16,011 --> 00:02:20,218
同一个 JSON-RPC 线协议，处理器不是同一个。

31
00:02:20,218 --> 00:02:24,172
当前的源码里，它们甚至不共享消息处理器。

32
00:02:24,172 --> 00:02:28,247
这一点很重要，因为它是权限边界的物理实现。

33
00:02:28,247 --> 00:02:31,516
不是靠判断，是靠代码不共用。

34
00:02:31,516 --> 00:02:35,446
还有一个细节能看出这层边界的严格程度。

35
00:02:35,446 --> 00:02:41,252
连接集是整份发布的，也就是说一次性把当前这份连接关系交出去。

36
00:02:41,252 --> 00:02:47,850
已经存在的绑定，继续拿它自己那一份连接，不会因为新接进来一家店就被改写。

37
00:02:47,850 --> 00:02:53,163
各家的工具列表汇成一张表之后，才交给命名翻译那一层。

38
00:02:53,163 --> 00:03:00,038
翻译只发生在会话内部，发生在朝向模型的那一侧，不发生在朝向 IDE 的那一侧。

39
00:03:00,038 --> 00:03:02,622
为什么这个形状长期成立。

40
00:03:02,622 --> 00:03:07,394
对外承诺和对内能力分开，是所有网关的通用形状。

41
00:03:07,394 --> 00:03:10,939
换成别的语言写，也只是两个函数。

42
00:03:10,939 --> 00:03:15,987
一个返回宿主的两个入口，一个返回带前缀的外部工具。

43
00:03:15,987 --> 00:03:20,122
IDE 只看见入口，会话里才看得见外部店。

44
00:03:20,122 --> 00:03:24,749
你把这条想清楚，很多网关设计都能套上去。

45
00:03:24,890 --> 00:03:26,967
现在看名字怎么变。

46
00:03:26,917 --> 00:03:35,775
一个外部 server 接进来，系统先把各家的工具列表汇成一张表，再交给一个专门做命名翻译的函数。

47
00:03:35,775 --> 00:03:37,878
顺序是固定四步。

48
00:03:37,878 --> 00:03:40,631
第一步，给命名空间加前缀。

49
00:03:40,631 --> 00:03:45,811
命名空间就是这家店的名字，前面加 mcp 和两条下划线。

50
00:03:45,811 --> 00:03:49,116
第二步，把非法字符洗成下划线。

51
00:03:49,116 --> 00:03:52,290
只留字母、数字和下划线。

52
00:03:52,290 --> 00:03:57,806
剩下的东西，连字符、点号、空格，全部变成下划线。

53
00:03:57,806 --> 00:04:01,665
第三步，完全相同的原始身份丢掉一份。

54
00:04:01,665 --> 00:04:05,198
注意是原始身份，不是清洗后的名字。

55
00:04:05,198 --> 00:04:09,201
同一个人报了两遍，去重的依据是原始身份。

56
00:04:09,201 --> 00:04:16,015
第四步，如果清洗之后命名空间或者工具名还撞，就在末尾加十二位哈希。

57
00:04:16,015 --> 00:04:18,912
算法是 SHA-1，取前十二位。

58
00:04:18,912 --> 00:04:21,232
这四步的顺序不能换。

59
00:04:21,232 --> 00:04:25,138
先清洗，再判断撞不撞，最后才哈希。

60
00:04:25,138 --> 00:04:28,119
因为哈希是消歧手段，不是装饰。

61
00:04:28,119 --> 00:04:30,378
这里有个很反直觉的点。

62
00:04:30,378 --> 00:04:32,842
两家干净店不会加后缀。

63
00:04:32,842 --> 00:04:41,689
比如一家叫 basic-server，一家叫 basic_server，连字符被洗成下划线之后，这两个命名空间就一样了。

64
00:04:41,689 --> 00:04:46,989
模型看见两个同名工具，下一次调用根本不知道该进哪家店。

65
00:04:46,989 --> 00:04:50,078
所以这时候必须拆开，加哈希。

66
00:04:50,078 --> 00:04:53,083
工具名那一档也是同样的逻辑。

67
00:04:53,083 --> 00:04:56,989
两家都报 search，前缀能分开，那是安全的。

68
00:04:56,989 --> 00:05:00,114
前缀分不开的时候才动哈希。

69
00:05:00,270 --> 00:05:03,717
翻译完之后，系统里存在两层名字。

70
00:05:03,667 --> 00:05:08,222
左边是协议上的原名，右边是给模型看的翻译名。

71
00:05:08,222 --> 00:05:10,458
调回去的时候走左边。

72
00:05:10,458 --> 00:05:19,184
也就是说，模型调用那个带哈希后缀的名字，控制面会把它还原成原始 server 名和原始工具名，再去寻址。

73
00:05:19,184 --> 00:05:22,754
所以右边加了哈希，不会导致进错店。

74
00:05:22,754 --> 00:05:24,797
还有一层长度约束。

75
00:05:24,797 --> 00:05:26,936
API 有字节上限。

76
00:05:26,936 --> 00:05:30,951
合起来超过一百二十八字节，就截断再哈希。

77
00:05:30,951 --> 00:05:38,475
同样地，原始 server 名和原始工具名会留在工具信息结构里，协议调用永远走原名。

78
00:05:38,475 --> 00:05:41,299
这一层设计为什么长期成立。

79
00:05:41,299 --> 00:05:46,432
因为给模型看的名字，和用来寻址的名字，本来就是两件事。

80
00:05:46,432 --> 00:05:50,254
一层给人读、给接口用，一层用来定位。

81
00:05:50,254 --> 00:05:56,059
哈希消歧是解决撞名问题的通用手段，但这层翻译结构不会变。

82
00:05:56,059 --> 00:06:00,903
你将来换哈希算法，换长度上限，这两层还是两层。

83
00:06:00,903 --> 00:06:03,535
有一个反例很能说明问题。

84
00:06:03,535 --> 00:06:07,645
假设没有这层翻译，模型直接看见原始名字。

85
00:06:07,645 --> 00:06:11,600
那两家店都叫 search 的时候，模型只能靠猜。

86
00:06:11,600 --> 00:06:15,254
你给它加前缀，它就要学会前缀规则。

87
00:06:15,254 --> 00:06:18,595
你给它加哈希，它又要学会忽略哈希。

88
00:06:18,595 --> 00:06:26,299
而有了两层之后，模型只需要认一个稳定可见的名字，回程的复杂度全被控制面吃掉了。

89
00:06:26,299 --> 00:06:33,126
这就是分层的价值，把复杂度关在一处，而不是摊到模型和用户两边。

90
00:06:33,270 --> 00:06:36,128
再讲 skill，也就是技能说明书。

91
00:06:36,078 --> 00:06:38,542
这里有个很值得学的选择。

92
00:06:38,542 --> 00:06:42,737
目录是常在的，跟 MCP 活没活着没关系。

93
00:06:42,737 --> 00:06:44,624
为什么要这么设计。

94
00:06:44,624 --> 00:06:49,720
如果用 MCP 存活状态去过滤目录，会发生一件很糟的事。

95
00:06:49,720 --> 00:06:53,903
冷启动那几秒，模型会以为某个 skill 不存在。

96
00:06:53,903 --> 00:06:56,379
下一轮它又突然冒出来。

97
00:06:56,379 --> 00:07:00,609
这种闪烁对模型判断的伤害，比缺失还大。

98
00:07:00,609 --> 00:07:02,629
另一个极端也不可取。

99
00:07:02,629 --> 00:07:07,028
把说明书整份灌进每一轮，上下文会被吃光。

100
00:07:07,028 --> 00:07:09,299
所以做法是两段式。

101
00:07:09,299 --> 00:07:11,547
点名记号是美元符号。

102
00:07:11,547 --> 00:07:16,186
目录只看两个开关，一个是启用，一个是提示可见。

103
00:07:16,186 --> 00:07:21,968
用户点了名，或者任务和描述对得上，这一轮才去读说明书正文。

104
00:07:21,968 --> 00:07:23,530
还有一条约束。

105
00:07:23,530 --> 00:07:27,076
守护者评审会话直接返回空注入。

106
00:07:27,076 --> 00:07:32,701
父对话记录里的美元符号加技能名，不能再触发新的说明书注入。

107
00:07:32,701 --> 00:07:35,093
这是防注入扩散的口子。

108
00:07:35,093 --> 00:07:39,407
这个限制很有必要，因为对话记录是可以被引用的。

109
00:07:39,407 --> 00:07:49,504
如果历史里的一个记号能在新会话里持续触发注入，那这份说明书就变成了长期驻留的提示，用户完全失去了控制。

110
00:07:49,504 --> 00:07:51,763
缺 MCP 的时候怎么办。

111
00:07:51,763 --> 00:07:53,410
答案是另问人。

112
00:07:53,410 --> 00:07:58,470
只有第一方来源，并且功能开关打开，才会弹出安装提示。

113
00:07:58,470 --> 00:08:02,004
审批如果设成永不，就静默跳过。

114
00:08:02,004 --> 00:08:07,388
用户选了仍然继续，目录还在，只是对应的工具可能用不了。

115
00:08:07,388 --> 00:08:08,987
一句话总结。

116
00:08:08,987 --> 00:08:11,282
发现和就绪是两件事。

117
00:08:11,282 --> 00:08:17,400
索引先给，全文按需再给，缺依赖问人，不要把条目从目录里抹掉。

118
00:08:17,400 --> 00:08:20,934
装不装是配置变更，列不列是发现。

119
00:08:21,080 --> 00:08:23,001
下半场讲搬家。

120
00:08:22,951 --> 00:08:28,912
搬家源在类型上只有两个变体，一个代表 Claude Code，一个代表 Cursor。

121
00:08:28,912 --> 00:08:32,350
字符串对不上 cursor，就落到 Claude Code。

122
00:08:32,350 --> 00:08:37,169
调用方漏传源，检测会去翻用户主目录下的 dot claude。

123
00:08:37,169 --> 00:08:40,198
一次检测最多产出十种条目。

124
00:08:40,198 --> 00:08:43,347
每种后面的导入必须给一个去处。

125
00:08:43,347 --> 00:08:49,850
字段先过白名单，然后走三条出口之一，原样搬走、改写、或者丢掉。

126
00:08:49,850 --> 00:08:51,220
先看 hook。

127
00:08:51,220 --> 00:08:55,210
Codex 认十一个 hook 事件，另一家发出二十七个。

128
00:08:55,210 --> 00:08:58,167
名字对不上的组，整组消失。

129
00:08:58,167 --> 00:09:02,614
单条 hook 的类型默认当命令类型，对不上就跳过。

130
00:09:02,614 --> 00:09:07,278
提示词类型的 hook，整条跳过，不会被改写成命令。

131
00:09:07,278 --> 00:09:08,960
再看 MCP。

132
00:09:08,960 --> 00:09:15,198
命令或者链接里出现美元符号加花括号的占位符，整台 server 都不要。

133
00:09:15,198 --> 00:09:20,907
因为展开它意味着猜测用户的环境变量，猜错的代价远大于不搬。

134
00:09:20,907 --> 00:09:24,537
市场来源只收 git 家族和本地目录。

135
00:09:24,537 --> 00:09:28,984
文件、链接、npm、设置这几种直接丢掉。

136
00:09:28,984 --> 00:09:31,340
最后是说明文件改名。

137
00:09:31,340 --> 00:09:34,273
CLAUDE.md 变成 AGENTS.md。

138
00:09:34,273 --> 00:09:37,386
产品名按词边界改写成 Codex。

139
00:09:37,386 --> 00:09:45,739
这里有个细节，Cursor 这一个词只用大小写敏感的匹配，避免把普通英文里的光标这个词一起改掉。

140
00:09:45,739 --> 00:09:49,922
导入器的通用形状就是白名单加三条出口。

141
00:09:49,922 --> 00:09:53,720
对不上的字段丢掉，不要改写成近似物。

142
00:09:53,860 --> 00:09:57,572
插件这一块有一条硬边界，我很喜欢。

143
00:09:57,522 --> 00:10:01,163
仓库里的启用名单，不能当安装权威。

144
00:10:01,163 --> 00:10:02,305
为什么。

145
00:10:02,305 --> 00:10:09,312
另一家允许仓库设置里带着启用插件名单，同事克隆之后自动就有同一批插件。

146
00:10:09,312 --> 00:10:13,567
但对 Codex 来说，装上去的插件会进用户配置。

147
00:10:13,567 --> 00:10:19,312
不可信的仓库想在你机器上装可执行内容，这条路必须先切断。

148
00:10:19,312 --> 00:10:22,750
所以检测只在用户主目录扫插件。

149
00:10:22,750 --> 00:10:27,894
源码注释写得很直白，仓库控制的设置不能当安装权威。

150
00:10:27,894 --> 00:10:33,014
对 Cursor 来说，只要看见仓库根，插件检测直接返回空。

151
00:10:33,014 --> 00:10:39,577
导入看见非空的工作目录，直接报一句，仓库范围的插件迁移不被允许。

152
00:10:39,577 --> 00:10:41,512
本地插件当场装。

153
00:10:41,512 --> 00:10:45,911
远程市场推进待装队列，不在同步阶段装完。

154
00:10:45,911 --> 00:10:47,738
会话也是两截。

155
00:10:47,738 --> 00:10:54,192
检测清单里可以出现会话，但同步导入对会话直接返回成功，什么都不写。

156
00:10:54,192 --> 00:10:58,207
真正把它写成 Codex 线程的，是后台任务。

157
00:10:58,207 --> 00:11:02,089
调用方先拿到一个导入 id，等后台慢慢跑。

158
00:11:02,089 --> 00:11:08,687
会话进清单还要先过一道年龄过滤，三十天和五十条，太旧的直接丢掉。

159
00:11:08,687 --> 00:11:11,043
另外提一句市场查找。

160
00:11:11,043 --> 00:11:17,654
Codex 认市场清单的时候，是按固定的一组相对路径去找第一个存在的文件。

161
00:11:17,654 --> 00:11:22,029
这组路径里两条是自己的，两条是竞品的。

162
00:11:22,029 --> 00:11:30,466
同一套查找逻辑同时服务两种场景，一种是用户主动加市场，一种是从竞品搬市场过来。

163
00:11:30,466 --> 00:11:38,639
复用同一份查找表，好处是搬过来的市场能被正确认出来，代价是得多认两条别人的路径。

164
00:11:38,639 --> 00:11:40,983
这个接口形状很通用。

165
00:11:40,983 --> 00:11:45,046
同步阶段只承认我看见了，慢活留给后台。

166
00:11:45,046 --> 00:11:48,183
长任务接口都应该长这样。

167
00:11:48,340 --> 00:11:50,297
搬家还有一条保护。

168
00:11:50,247 --> 00:11:53,131
目标文件非空，整项取消。

169
00:11:53,131 --> 00:11:58,588
已有的 hook 文件里有内容，整项就取消掉，不做整文件替换。

170
00:11:58,588 --> 00:12:01,653
已有的配置文件只补缺失的键。

171
00:12:01,653 --> 00:12:04,718
同名的 MCP server 保留旧的。

172
00:12:04,718 --> 00:12:11,689
所以第一次搬家像填空，第二次再点迁移，多数条目会因为已经有了而不再出现。

173
00:12:11,689 --> 00:12:13,804
memory 另有一道门。

174
00:12:13,804 --> 00:12:17,338
特性开关默认关，阶段还写着开发中。

175
00:12:17,338 --> 00:12:21,352
检测函数不看这个开关，导入前的处理器会查。

176
00:12:21,352 --> 00:12:25,379
关着就回一句，外部 agent 的 memory 导入已禁用。

177
00:12:25,379 --> 00:12:31,737
原因是另一家 agent 的项目记忆会按明文进盘，导入是按字节拷贝，不脱敏。

178
00:12:31,737 --> 00:12:33,468
横向对比一下。

179
00:12:33,468 --> 00:12:35,187
先看 DSH。

180
00:12:35,187 --> 00:12:47,663
它的 MCP 客户端把范围写死，连一台外部 server，工具注册进上下文里的工具表，公开名是 mcp 加两条下划线加 server 名再加两条下划线加原始名。

181
00:12:47,663 --> 00:12:49,802
干净情况原样拼接。

182
00:12:49,802 --> 00:12:57,795
字符或长度被改过，就在末尾加十二位哈希，但算法是 SHA-256，上限是六十四字符。

183
00:12:57,795 --> 00:13:01,521
卸载就断连、注销、释放命名空间。

184
00:13:01,521 --> 00:13:05,944
哈希长度碰巧也是十二，算法和拼接规则都不同。

185
00:13:05,944 --> 00:13:11,184
它没有那种向用户追问的交互，也不把自己交出去当 MCP server。

186
00:13:11,184 --> 00:13:12,987
再看 Claude Code。

187
00:13:12,987 --> 00:13:17,663
它给模型一个技能工具，模型主动调用才拿正文。

188
00:13:17,663 --> 00:13:24,742
源码注释写明同一时间只跑一个技能，因为这个工具会把命令展开成整份提示词。

189
00:13:24,742 --> 00:13:26,316
方向也相反。

190
00:13:26,316 --> 00:13:31,677
Codex 是技能需要 MCP，Claude Code 是 MCP 反过来贡献技能。

191
00:13:31,677 --> 00:13:38,516
触发方式也不同，Codex 扫描美元符号，命中就直接注入，不经过一次工具调用。

192
00:13:38,516 --> 00:13:41,533
Grok 是自建市场，不搬竞品。

193
00:13:41,533 --> 00:13:47,314
它从市场根按相对路径拷进自己的安装登记，并写下来源记录。

194
00:13:47,314 --> 00:13:51,617
相对路径禁止上级目录、绝对路径和盘符。

195
00:13:51,617 --> 00:13:57,098
没有清单文件但根上有说明书的时候，它会补一份合成清单。

196
00:13:57,098 --> 00:14:02,398
所以这不是谁更先进的问题，是三条不同的产品选择。

197
00:14:02,550 --> 00:14:04,362
最后收成五条。

198
00:14:04,312 --> 00:14:08,856
第一条，对外入口和对内目录必须在代码上分开。

199
00:14:08,856 --> 00:14:13,123
不要靠运行时判断去挡，要让它们不共享处理器。

200
00:14:13,123 --> 00:14:15,695
物理隔离比逻辑判断可靠。

201
00:14:15,695 --> 00:14:18,832
第二条，展示名和寻址名分层。

202
00:14:18,832 --> 00:14:27,738
给模型看的名字可以清洗、可以加哈希、可以截断，但原始身份必须一直挂在旁边，回程永远走原始身份。

203
00:14:27,738 --> 00:14:31,981
这一层不分开，撞名和截断迟早会搞出事故。

204
00:14:31,981 --> 00:14:35,526
第三条，目录常在，正文按需。

205
00:14:35,526 --> 00:14:38,928
不要用依赖的存活状态去过滤索引。

206
00:14:38,928 --> 00:14:43,255
缺失依赖就去问人，不要把条目从目录里抹掉。

207
00:14:43,255 --> 00:14:45,803
闪烁比缺失伤害更大。

208
00:14:45,803 --> 00:14:50,995
第四条，导入器是白名单加三条出口，没有第四条路径。

209
00:14:50,995 --> 00:14:54,757
对不上的字段丢掉，绝不改写成近似物。

210
00:14:54,757 --> 00:15:00,514
提示词类型的 hook 不要塞进命令类型，带占位符的命令不要瞎展开。

211
00:15:00,514 --> 00:15:05,106
第五条，可执行内容的安装权威必须落在用户级。

212
00:15:05,106 --> 00:15:10,058
面向任意 git clone 的产品，仓库里的启用名单天生不可信。

213
00:15:10,058 --> 00:15:15,635
同步先回执，慢活进后台，这是长任务接口的通用形状。

214
00:15:15,635 --> 00:15:17,750
再补两个容易踩的点。

215
00:15:17,750 --> 00:15:24,577
目标非空就不覆盖，用户的手写配置是最高优先级，搬家是填空不是重写。

216
00:15:24,577 --> 00:15:30,034
以及改产品名的时候要用词边界，否则会把普通英文一起改坏。

217
00:15:30,034 --> 00:15:31,656
一句话收尾。

218
00:15:31,656 --> 00:15:36,981
外部能力进来要过一层翻译，撞名才哈希，原名留给协议。

219
00:15:36,981 --> 00:15:46,091
旧家当搬过来只搬对得上的字段，插件只从用户级装，会话和远程市场进后台，目标非空不覆盖。

