coding-tool-x 3.7.10 → 3.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/README.md +1 -1
- package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-8dRv1aYW.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-BQRXSICO.js} +1 -1
- package/dist/web/assets/Home-BtzWhKIa.js +1 -0
- package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-BTa-Ygf6.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-F_cB8ldA.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-LOrbPQvu.js} +1 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
- package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-Db9RGiz7.js} +1 -1
- package/dist/web/assets/index-DIWJmyL6.js +2 -0
- package/dist/web/assets/{index-C5nLTbOk.css → index-bJRG3RXH.css} +1 -1
- package/dist/web/index.html +2 -2
- package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
- package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
- package/package.json +1 -1
- package/src/commands/proxy-control.js +12 -12
- package/src/commands/toggle-proxy.js +16 -17
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/config/paths.js +52 -29
- package/src/index.js +42 -16
- package/src/server/api/omp-channels.js +6 -4
- package/src/server/api/omp-config.js +1 -1
- package/src/server/api/omp-proxy.js +13 -42
- package/src/server/api/plugins.js +119 -47
- package/src/server/api/skills.js +63 -10
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +142 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/model-definition-schema.js +2 -1
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +168 -12
- package/src/server/services/omp-config.js +71 -6
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-native-plugin-adapter.js +456 -0
- package/src/server/services/omp-session-log-observer.js +138 -0
- package/src/server/services/omp-sessions.js +80 -0
- package/src/server/services/omp-settings-manager.js +338 -87
- package/src/server/services/omp-skill-discovery.js +358 -0
- package/src/server/services/omp-skill-settings-service.js +97 -0
- package/src/server/services/pi-omp-migration.js +273 -0
- package/src/server/services/platform-resolution.js +44 -0
- package/src/server/services/plugins-service.js +85 -561
- package/src/server/services/project-path-validation.js +65 -0
- package/src/server/services/skill-service.js +169 -77
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- package/dist/web/assets/SkillManager-Bhzhkeis.js +0 -1
- package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
- package/dist/web/assets/index-LYvv7wtx.js +0 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# OMP 技能扫描设置设计
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
在技能管理界面为 OMP 平台提供独立的扫描来源设置入口。用户可以控制 OMP 技能发现是否扫描 Codex 用户目录、Claude 用户及插件目录、OMP 用户及插件目录、当前项目 `.omp/skills`,并在保存后立即刷新当前技能列表。
|
|
6
|
+
|
|
7
|
+
本功能不展示、读取或写入 `skills.enabled`。现有 `skills.enabled` 以及所有未列入本功能白名单的配置必须原样保留。
|
|
8
|
+
|
|
9
|
+
## 范围
|
|
10
|
+
|
|
11
|
+
### 包含
|
|
12
|
+
|
|
13
|
+
- OMP 平台下,SkillsPanel 独立页和抽屉页工具栏均显示“设置”按钮。
|
|
14
|
+
- 非 OMP 平台不显示该入口。
|
|
15
|
+
- 新增窄幅“技能扫描设置”弹窗。
|
|
16
|
+
- 新增专用 `GET /api/skills/omp-settings` 与 `PUT /api/skills/omp-settings`。
|
|
17
|
+
- 持久化 OMP `config.yml` 中 `skills` 节点的四个白名单字段。
|
|
18
|
+
- 保存成功后强制刷新当前技能列表。
|
|
19
|
+
- API 与前端交互回归测试。
|
|
20
|
+
|
|
21
|
+
### 不包含
|
|
22
|
+
|
|
23
|
+
- `skills.enabled` 总开关。
|
|
24
|
+
- Claude 项目、Agents、customDirectories、includeSkills、ignoredSkills 等其他 OMP Skills 配置。
|
|
25
|
+
- 通用 SettingsDrawer 集成。
|
|
26
|
+
- 修改 OMP 技能发现本身的扫描算法。
|
|
27
|
+
|
|
28
|
+
## 配置模型
|
|
29
|
+
|
|
30
|
+
API 仅暴露以下四个布尔字段:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
{
|
|
34
|
+
enableCodexUser: true,
|
|
35
|
+
enableClaudeUser: true,
|
|
36
|
+
enablePiUser: true,
|
|
37
|
+
enablePiProject: true
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
含义:
|
|
42
|
+
|
|
43
|
+
- `enableCodexUser`:扫描 Codex 用户 Skills。
|
|
44
|
+
- `enableClaudeUser`:扫描 Claude 用户 Skills 与 Claude 插件 Skills。
|
|
45
|
+
- `enablePiUser`:扫描 OMP 用户 Skills 与 OMP 插件 Skills。
|
|
46
|
+
- `enablePiProject`:扫描当前项目 `.omp/skills`。
|
|
47
|
+
|
|
48
|
+
任一字段在持久化配置中缺失时,GET 返回 `true`。默认值只补齐 API 响应,不要求 GET 写盘。
|
|
49
|
+
|
|
50
|
+
## 前端设计
|
|
51
|
+
|
|
52
|
+
### 入口
|
|
53
|
+
|
|
54
|
+
SkillsPanel 根据 `currentPlatform === 'omp'` 决定是否渲染“设置”按钮。按钮加入现有 `asset-action-row`:
|
|
55
|
+
|
|
56
|
+
- 独立页头部工具栏可用。
|
|
57
|
+
- 抽屉页工具栏可用。
|
|
58
|
+
- 其他平台完全不渲染按钮,而不是仅禁用。
|
|
59
|
+
|
|
60
|
+
入口逻辑沿用 SkillsPanel 已有的平台解析结果,避免建立第二套平台判断。
|
|
61
|
+
|
|
62
|
+
### 设置弹窗
|
|
63
|
+
|
|
64
|
+
新增独立 `OmpSkillSettingsModal` 组件,职责仅包括读取、编辑和保存四个扫描来源。SkillsPanel 只负责控制可见性,并在组件发出保存成功事件后刷新列表。
|
|
65
|
+
|
|
66
|
+
弹窗设计:
|
|
67
|
+
|
|
68
|
+
- 标题:`技能扫描设置`。
|
|
69
|
+
- 宽度约 420px,并受视口最大宽度约束。
|
|
70
|
+
- 分组标题:`扫描来源`。
|
|
71
|
+
- 四行开关,每行包含主标签和简短说明。
|
|
72
|
+
- 底部操作:`取消`、`保存`。
|
|
73
|
+
|
|
74
|
+
打开弹窗时立即请求实际持久化值。读取期间显示加载状态,避免用户编辑尚未初始化的表单。
|
|
75
|
+
|
|
76
|
+
### 交互状态
|
|
77
|
+
|
|
78
|
+
- 读取成功:以 GET 返回值初始化四个开关。
|
|
79
|
+
- 读取失败:显示错误提示,不修改 SkillsPanel 已有技能列表;弹窗保留错误状态,允许关闭后重试。
|
|
80
|
+
- 保存期间:四个开关、取消、关闭与保存操作全部禁用,防止并发写入或丢失提交状态。
|
|
81
|
+
- 保存成功:显示成功提示,关闭弹窗,并等待 `loadData(true)` 完成以强制刷新当前列表。
|
|
82
|
+
- 保存失败:显示错误提示,弹窗保持打开,保留用户当前表单值,不刷新列表。
|
|
83
|
+
|
|
84
|
+
## 服务端设计
|
|
85
|
+
|
|
86
|
+
### 专用服务边界
|
|
87
|
+
|
|
88
|
+
新增聚焦于 OMP 技能设置的服务模块,提供:
|
|
89
|
+
|
|
90
|
+
- 读取并补齐四个默认值。
|
|
91
|
+
- 校验 PUT 请求。
|
|
92
|
+
- 将部分更新合并进现有 `skills` 节点。
|
|
93
|
+
- 调用现有 OMP YAML 配置读写能力持久化结果。
|
|
94
|
+
|
|
95
|
+
Skills API 路由仅负责 HTTP 状态和响应封装,不直接实现白名单合并规则。
|
|
96
|
+
|
|
97
|
+
### GET `/api/skills/omp-settings`
|
|
98
|
+
|
|
99
|
+
成功响应:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"success": true,
|
|
104
|
+
"settings": {
|
|
105
|
+
"enableCodexUser": true,
|
|
106
|
+
"enableClaudeUser": true,
|
|
107
|
+
"enablePiUser": true,
|
|
108
|
+
"enablePiProject": true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
读取现有 `config.yml` 的 `skills` 节点,仅选择白名单字段。缺失字段返回默认值 `true`。顶层配置不存在或 `skills` 节点不存在时,同样返回四项默认值。
|
|
114
|
+
|
|
115
|
+
底层读取错误交由统一 API 错误处理返回失败响应,不用空配置掩盖真实持久化错误。
|
|
116
|
+
|
|
117
|
+
### PUT `/api/skills/omp-settings`
|
|
118
|
+
|
|
119
|
+
请求体允许只包含一个或多个白名单字段,例如:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"enablePiProject": false
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
验证规则:
|
|
128
|
+
|
|
129
|
+
- 请求体必须是普通对象,不能是数组或 `null`。
|
|
130
|
+
- 每个键必须属于四字段白名单。
|
|
131
|
+
- 每个值必须是布尔值。
|
|
132
|
+
- 空对象允许作为无变化的有效部分更新,并返回当前有效设置。
|
|
133
|
+
- 任一字段非法时整体返回 400,且不得写盘。
|
|
134
|
+
|
|
135
|
+
写入步骤:
|
|
136
|
+
|
|
137
|
+
1. 读取完整 OMP 配置对象。
|
|
138
|
+
2. 将现有 `skills` 视为对象;若缺失则使用空对象。
|
|
139
|
+
3. 仅把请求中的白名单字段浅合并到现有 `skills`。
|
|
140
|
+
4. 将合并后的 `skills` 放回完整配置对象。
|
|
141
|
+
5. 通过现有 YAML 写入能力写回 `config.yml`。
|
|
142
|
+
6. 返回补齐默认值后的四字段设置。
|
|
143
|
+
|
|
144
|
+
该流程必须保留:
|
|
145
|
+
|
|
146
|
+
- `skills.enabled`。
|
|
147
|
+
- `skills` 节点中的其他字段。
|
|
148
|
+
- 整个配置文件的其他顶层节点。
|
|
149
|
+
|
|
150
|
+
## 数据流
|
|
151
|
+
|
|
152
|
+
```mermaid
|
|
153
|
+
sequenceDiagram
|
|
154
|
+
participant U as 用户
|
|
155
|
+
participant P as SkillsPanel
|
|
156
|
+
participant M as OmpSkillSettingsModal
|
|
157
|
+
participant A as Skills API
|
|
158
|
+
participant S as OMP Skill Settings Service
|
|
159
|
+
participant Y as config.yml
|
|
160
|
+
|
|
161
|
+
U->>P: 点击设置
|
|
162
|
+
P->>M: 打开弹窗
|
|
163
|
+
M->>A: GET /skills/omp-settings
|
|
164
|
+
A->>S: 读取设置
|
|
165
|
+
S->>Y: 读取完整配置
|
|
166
|
+
Y-->>S: YAML 配置
|
|
167
|
+
S-->>M: 四字段有效值
|
|
168
|
+
U->>M: 修改并保存
|
|
169
|
+
M->>A: PUT /skills/omp-settings
|
|
170
|
+
A->>S: 校验并部分更新
|
|
171
|
+
S->>Y: 写回完整合并配置
|
|
172
|
+
A-->>M: 保存成功
|
|
173
|
+
M-->>P: saved
|
|
174
|
+
P->>P: loadData(true)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 错误处理
|
|
178
|
+
|
|
179
|
+
- API 校验错误使用 400,并返回项目统一的失败响应格式。
|
|
180
|
+
- YAML 读取或写入错误使用现有统一错误处理,不返回伪成功。
|
|
181
|
+
- 非法 PUT 在校验完成前不得调用写入函数。
|
|
182
|
+
- 前端读取或写入失败均不触发技能列表刷新。
|
|
183
|
+
- 前端错误提示包含操作上下文,如“加载技能扫描设置失败”或“保存技能扫描设置失败”。
|
|
184
|
+
- 设置请求与技能列表请求彼此独立,设置失败不得清空或替换当前 `skills` 状态。
|
|
185
|
+
|
|
186
|
+
## 测试策略
|
|
187
|
+
|
|
188
|
+
严格按红、绿、重构顺序实现。
|
|
189
|
+
|
|
190
|
+
### 服务端/API 回归
|
|
191
|
+
|
|
192
|
+
先编写并运行失败测试,再实现:
|
|
193
|
+
|
|
194
|
+
1. 空配置和缺失字段时,GET 返回四项 `true`。
|
|
195
|
+
2. PUT 部分更新只改变请求字段。
|
|
196
|
+
3. PUT 保留 `skills.enabled`、其他 skills 字段和顶层无关字段。
|
|
197
|
+
4. PUT 包含未知字段时返回 400,配置文件内容不变。
|
|
198
|
+
5. PUT 包含非布尔值时返回 400,配置文件内容不变。
|
|
199
|
+
6. 空对象 PUT 不破坏配置,并返回当前有效设置。
|
|
200
|
+
|
|
201
|
+
### 前端回归
|
|
202
|
+
|
|
203
|
+
当前 Web 包没有 Vue Test Utils 或 DOM 测试环境。为避免仅通过源代码文本断言,将平台入口判断和保存编排提取为可在现有 Vitest Node 环境直接测试的轻量辅助模块。测试覆盖:
|
|
204
|
+
|
|
205
|
+
1. OMP 平台允许显示设置入口。
|
|
206
|
+
2. 非 OMP 平台不允许显示设置入口。
|
|
207
|
+
3. 保存成功只触发一次 `loadData(true)`。
|
|
208
|
+
4. 读取失败不触发刷新,并保留当前技能列表。
|
|
209
|
+
5. 写入失败不触发刷新,并保留当前技能列表。
|
|
210
|
+
|
|
211
|
+
组件模板继续通过 Web 生产构建验证 Vue 模板、导入和类型形态是否有效。
|
|
212
|
+
|
|
213
|
+
## 验收标准
|
|
214
|
+
|
|
215
|
+
- OMP 独立技能页和技能抽屉均可打开扫描设置。
|
|
216
|
+
- 非 OMP 平台看不到设置入口。
|
|
217
|
+
- 弹窗只展示四个来源开关,不展示总开关。
|
|
218
|
+
- 缺失配置默认显示为启用。
|
|
219
|
+
- 保存过程中无法重复提交或关闭弹窗。
|
|
220
|
+
- 成功保存后出现成功提示,弹窗关闭,当前技能列表强制刷新。
|
|
221
|
+
- 读取或写入失败时已有技能列表保持不变,并显示明确错误。
|
|
222
|
+
- PUT 不会覆盖任何非白名单配置。
|
|
223
|
+
- 非法字段或非法值被拒绝,且配置文件不发生变化。
|
|
224
|
+
- 定向 Vitest 测试通过,Web 生产构建通过。
|
package/package.json
CHANGED
|
@@ -28,18 +28,18 @@ const CHANNEL_CONFIG = {
|
|
|
28
28
|
name: 'OMP',
|
|
29
29
|
icon: '[*]',
|
|
30
30
|
aompPath: '/api/omp/proxy',
|
|
31
|
-
serviceLabel: 'OMP
|
|
32
|
-
proxyLabel: '
|
|
33
|
-
startAction: '
|
|
34
|
-
stopAction: '
|
|
35
|
-
restartAction: '
|
|
36
|
-
startedMessage: 'OMP
|
|
37
|
-
stoppedMessage: 'OMP
|
|
38
|
-
runningText: '
|
|
39
|
-
stoppedText: '
|
|
40
|
-
portLabel: '
|
|
41
|
-
addressLabel: '
|
|
42
|
-
startTip: '
|
|
31
|
+
serviceLabel: 'OMP 动态切换',
|
|
32
|
+
proxyLabel: '动态切换',
|
|
33
|
+
startAction: '启动',
|
|
34
|
+
stopAction: '停止',
|
|
35
|
+
restartAction: '重启',
|
|
36
|
+
startedMessage: 'OMP 动态切换已启用',
|
|
37
|
+
stoppedMessage: 'OMP 已切换为单渠道直连模式',
|
|
38
|
+
runningText: '运行中',
|
|
39
|
+
stoppedText: '单渠道直连',
|
|
40
|
+
portLabel: '网关端口',
|
|
41
|
+
addressLabel: '网关地址',
|
|
42
|
+
startTip: '启动动态切换'
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -205,9 +205,10 @@ async function handleStartProxy(cliType, services) {
|
|
|
205
205
|
|
|
206
206
|
console.log(chalk.cyan('动态切换功能说明:'));
|
|
207
207
|
if (services.managedProviderConfig) {
|
|
208
|
-
console.log(chalk.gray('•
|
|
209
|
-
console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName}
|
|
210
|
-
console.log(chalk.gray(
|
|
208
|
+
console.log(chalk.gray('• 开启后会启动 OMP 专用本地网关,并把受管 provider 指向该网关'));
|
|
209
|
+
console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName} 路由组`));
|
|
210
|
+
console.log(chalk.gray('• 同路由组内由 coding-tool-x 动态选择渠道,跨模型 fallback 仍由 OMP 处理'));
|
|
211
|
+
console.log(chalk.gray(`• OMP 网关地址: http://127.0.0.1:${defaultPort}\n`));
|
|
211
212
|
} else {
|
|
212
213
|
console.log(chalk.gray('• 开启后会在本地启动一个代理服务'));
|
|
213
214
|
console.log(chalk.gray(`• 可以在不重启 ${toolName} 的情况下动态管理渠道`));
|
|
@@ -217,8 +218,8 @@ async function handleStartProxy(cliType, services) {
|
|
|
217
218
|
|
|
218
219
|
console.log(chalk.yellow('[WARN] 重要提示:'));
|
|
219
220
|
if (services.managedProviderConfig) {
|
|
220
|
-
console.log(chalk.yellow('•
|
|
221
|
-
console.log(chalk.yellow('•
|
|
221
|
+
console.log(chalk.yellow('• 启用后会写入指向本地网关的 OMP models.yml provider 配置'));
|
|
222
|
+
console.log(chalk.yellow('• 关闭后会保留当前渠道的直连 provider,不会清理 OMP 原生 auth.json\n'));
|
|
222
223
|
} else {
|
|
223
224
|
console.log(chalk.yellow('• 开启期间请勿关闭 CLI 终端窗口'));
|
|
224
225
|
console.log(chalk.yellow('• 如果异常关闭导致代理失效,请运行: ctx reset'));
|
|
@@ -253,9 +254,7 @@ async function handleStartProxy(cliType, services) {
|
|
|
253
254
|
|
|
254
255
|
if (services.managedProviderConfig) {
|
|
255
256
|
console.log(chalk.green('[OK] OMP models.yml 受管 provider 已启用'));
|
|
256
|
-
|
|
257
|
-
console.log(chalk.gray(`状态端口: http://127.0.0.1:${proxyResult.port}`));
|
|
258
|
-
}
|
|
257
|
+
console.log(chalk.gray('OMP 新增会话用量日志观察已启用'));
|
|
259
258
|
(proxyResult.warnings || []).forEach((warning) => {
|
|
260
259
|
console.log(chalk.yellow(`[WARN] ${warning}`));
|
|
261
260
|
});
|
|
@@ -275,7 +274,7 @@ async function handleStartProxy(cliType, services) {
|
|
|
275
274
|
}
|
|
276
275
|
}
|
|
277
276
|
|
|
278
|
-
console.log(chalk.cyan(services.managedProviderConfig ? '\n[TIP] OMP
|
|
277
|
+
console.log(chalk.cyan(services.managedProviderConfig ? '\n[TIP] OMP 动态切换已启用!' : '\n[TIP] 动态切换已启用!'));
|
|
279
278
|
if (services.managedProviderConfig) {
|
|
280
279
|
console.log(chalk.gray(' 现在可以通过"渠道管理"功能调整已启用的 OMP provider,并重新同步 models.yml\n'));
|
|
281
280
|
} else {
|
|
@@ -323,9 +322,9 @@ async function handleStopProxy(cliType, services) {
|
|
|
323
322
|
|
|
324
323
|
console.log(chalk.cyan('当前状态:'));
|
|
325
324
|
if (services.managedProviderConfig) {
|
|
326
|
-
console.log(chalk.gray(`•
|
|
325
|
+
console.log(chalk.gray(`• 动态切换: ${chalk.green('已启用')}`));
|
|
327
326
|
console.log(chalk.gray(`• 配置模式: ${proxyStatus.mode || 'models-yml-provider-config'}`));
|
|
328
|
-
console.log(chalk.gray(`•
|
|
327
|
+
console.log(chalk.gray(`• 会话日志观察: ${proxyStatus.sessionLogObserver?.running ? chalk.green('运行中') : chalk.yellow('未运行')}\n`));
|
|
329
328
|
} else {
|
|
330
329
|
console.log(chalk.gray(`• 代理服务: ${chalk.green('运行中')}`));
|
|
331
330
|
console.log(chalk.gray(`• 代理端口: ${proxyStatus.port}`));
|
|
@@ -334,8 +333,8 @@ async function handleStopProxy(cliType, services) {
|
|
|
334
333
|
|
|
335
334
|
console.log(chalk.yellow('关闭后:'));
|
|
336
335
|
if (services.managedProviderConfig) {
|
|
337
|
-
console.log(chalk.gray('• OMP
|
|
338
|
-
console.log(chalk.gray(`• 之后调整 ${toolName} provider
|
|
336
|
+
console.log(chalk.gray('• OMP 将保留当前渠道的单一、直连 models.yml provider'));
|
|
337
|
+
console.log(chalk.gray(`• 之后调整 ${toolName} provider 或模型会立即同步到 OMP\n`));
|
|
339
338
|
} else {
|
|
340
339
|
console.log(chalk.gray('• 代理服务将被停止'));
|
|
341
340
|
console.log(chalk.gray('• 配置将恢复为当前激活渠道的单渠道模式'));
|
|
@@ -358,13 +357,13 @@ async function handleStopProxy(cliType, services) {
|
|
|
358
357
|
|
|
359
358
|
try {
|
|
360
359
|
console.log(chalk.cyan(services.managedProviderConfig
|
|
361
|
-
? '\n[STOP]
|
|
360
|
+
? '\n[STOP] 正在切换 OMP 到单渠道直连模式...\n'
|
|
362
361
|
: '\n[STOP] 正在停止代理服务...\n'));
|
|
363
362
|
|
|
364
363
|
// 停止代理服务器
|
|
365
364
|
await services.stopProxyServer();
|
|
366
365
|
console.log(chalk.green(services.managedProviderConfig
|
|
367
|
-
? '[OK] OMP
|
|
366
|
+
? '[OK] OMP 已切换到单渠道直连模式'
|
|
368
367
|
: '[OK] 代理服务已停止'));
|
|
369
368
|
|
|
370
369
|
// 恢复配置文件
|
|
@@ -383,9 +382,9 @@ async function handleStopProxy(cliType, services) {
|
|
|
383
382
|
console.log(chalk.green('[OK] 已清理代理接管状态'));
|
|
384
383
|
}
|
|
385
384
|
|
|
386
|
-
console.log(chalk.cyan(services.managedProviderConfig ? '\n[TIP] OMP
|
|
385
|
+
console.log(chalk.cyan(services.managedProviderConfig ? '\n[TIP] OMP 动态切换已关闭' : '\n[TIP] 动态切换已关闭'));
|
|
387
386
|
if (services.managedProviderConfig) {
|
|
388
|
-
console.log(chalk.gray('
|
|
387
|
+
console.log(chalk.gray(' 当前渠道保持直连;后续渠道和模型修改会立即同步到 OMP\n'));
|
|
389
388
|
} else {
|
|
390
389
|
console.log(chalk.gray(` 现在调整渠道需要重启 ${toolName} 才能生效\n`));
|
|
391
390
|
}
|
package/src/commands/ui.js
CHANGED
|
@@ -87,6 +87,16 @@ async function handleUI() {
|
|
|
87
87
|
// 忽略错误
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// OMP 动态网关持有 models.yml 事务,退出前必须先恢复原生配置。
|
|
91
|
+
try {
|
|
92
|
+
const { getOmpProxyStatus, stopOmpProxyServer } = require('../server/omp-proxy-server');
|
|
93
|
+
if (getOmpProxyStatus().running) {
|
|
94
|
+
await stopOmpProxyServer({ forceAfterMs: 1500 });
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${err.message}`));
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
console.log(chalk.green('[OK] Web UI 已停止\n'));
|
|
91
101
|
process.exit(0);
|
|
92
102
|
});
|
|
@@ -99,7 +109,7 @@ async function handleUI() {
|
|
|
99
109
|
|
|
100
110
|
} catch (error) {
|
|
101
111
|
console.error(chalk.red('启动服务器失败:'), error.message);
|
|
102
|
-
|
|
112
|
+
throw error;
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
package/src/config/default.js
CHANGED
|
@@ -21,7 +21,7 @@ const DEFAULT_CONFIG = {
|
|
|
21
21
|
codexProxy: 20089, // Codex 代理服务端口
|
|
22
22
|
geminiProxy: 20090, // Gemini 代理服务端口
|
|
23
23
|
opencodeProxy: 20091, // OpenCode 代理服务端口
|
|
24
|
-
ompProxy: 20092 // OMP
|
|
24
|
+
ompProxy: 20092 // OMP 专用动态网关端口
|
|
25
25
|
},
|
|
26
26
|
maxLogs: 100,
|
|
27
27
|
statsInterval: 30,
|
package/src/config/paths.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
6
7
|
const { resolvePreferredHomeDir, isWindowsLikePlatform } = require('../utils/home-dir');
|
|
7
8
|
|
|
8
9
|
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
@@ -804,26 +805,48 @@ function normalizeOmpProfileName(profile = '') {
|
|
|
804
805
|
return value && value !== 'default' ? value : '';
|
|
805
806
|
}
|
|
806
807
|
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
808
|
+
function getOmpAgentDir(env = process.env, options = {}) {
|
|
809
|
+
if (options.resolveRuntime !== false) {
|
|
810
|
+
const command = String(env.OMP_COMMAND || 'omp').trim() || 'omp';
|
|
811
|
+
const commandRunner = options.commandRunner || execFileSync;
|
|
812
|
+
try {
|
|
813
|
+
const output = commandRunner(command, ['config', 'path'], {
|
|
814
|
+
encoding: 'utf8',
|
|
815
|
+
env: { ...process.env, ...env },
|
|
816
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
817
|
+
timeout: options.timeout || 3000,
|
|
818
|
+
windowsHide: true
|
|
819
|
+
});
|
|
820
|
+
const configuredByCli = String(output || '').trim().split(/\r?\n/).find(Boolean);
|
|
821
|
+
if (configuredByCli) {
|
|
822
|
+
return path.resolve(expandHomePath(configuredByCli));
|
|
823
|
+
}
|
|
824
|
+
} catch {
|
|
825
|
+
// Fall through to environment/profile compatibility paths.
|
|
826
|
+
}
|
|
827
|
+
}
|
|
814
828
|
|
|
815
|
-
|
|
816
|
-
|
|
829
|
+
// OMP 17.x kept the historical PI_CODING_AGENT_DIR variable as the
|
|
830
|
+
// canonical relocation knob. OMP_CODING_AGENT_DIR was used by older
|
|
831
|
+
// cc-tool releases, so retain it as a lower-priority compatibility alias.
|
|
832
|
+
const configuredDir = resolveExistingEnvPath(
|
|
833
|
+
env.PI_CODING_AGENT_DIR || env.OMP_CODING_AGENT_DIR
|
|
834
|
+
);
|
|
817
835
|
if (configuredDir) {
|
|
818
836
|
return path.resolve(expandHomePath(configuredDir));
|
|
819
837
|
}
|
|
820
|
-
const profile =
|
|
838
|
+
const profile = normalizeOmpProfileName(env.OMP_PROFILE);
|
|
839
|
+
const configRoot = expandHomePath(
|
|
840
|
+
resolveExistingEnvPath(env.OMP_CONFIG_DIR) || path.join(HOME_DIR, '.omp')
|
|
841
|
+
);
|
|
821
842
|
const agentDir = profile
|
|
822
|
-
? path.join(
|
|
823
|
-
: path.join(
|
|
843
|
+
? path.join(configRoot, 'profiles', profile, 'agent')
|
|
844
|
+
: path.join(configRoot, 'agent');
|
|
824
845
|
return path.resolve(agentDir);
|
|
825
846
|
}
|
|
826
847
|
|
|
848
|
+
const OMP_AGENT_DIR = getOmpAgentDir();
|
|
849
|
+
|
|
827
850
|
// 工具特定的原生配置路径(不改变)
|
|
828
851
|
const NATIVE_PATHS = {
|
|
829
852
|
// Claude Code 原生配置
|
|
@@ -877,23 +900,23 @@ const NATIVE_PATHS = {
|
|
|
877
900
|
|
|
878
901
|
// OMP 原生配置。
|
|
879
902
|
omp: {
|
|
880
|
-
dir:
|
|
881
|
-
config: path.join(
|
|
882
|
-
settings: path.join(
|
|
883
|
-
settingsJsonLegacy: path.join(
|
|
884
|
-
auth: path.join(
|
|
885
|
-
models: path.join(
|
|
886
|
-
modelsYml: path.join(
|
|
887
|
-
modelsJsonLegacy: path.join(
|
|
888
|
-
mcp: path.join(
|
|
889
|
-
sessions: path.join(
|
|
890
|
-
skills: path.join(
|
|
891
|
-
prompts: path.join(
|
|
892
|
-
commands: path.join(
|
|
893
|
-
notes: path.join(
|
|
894
|
-
extensions: path.join(
|
|
895
|
-
themes: path.join(
|
|
896
|
-
packages: path.join(
|
|
903
|
+
dir: OMP_AGENT_DIR,
|
|
904
|
+
config: path.join(OMP_AGENT_DIR, 'config.yml'),
|
|
905
|
+
settings: path.join(OMP_AGENT_DIR, 'config.yml'),
|
|
906
|
+
settingsJsonLegacy: path.join(OMP_AGENT_DIR, 'settings.json'),
|
|
907
|
+
auth: path.join(OMP_AGENT_DIR, 'auth.json'),
|
|
908
|
+
models: path.join(OMP_AGENT_DIR, 'models.yml'),
|
|
909
|
+
modelsYml: path.join(OMP_AGENT_DIR, 'models.yml'),
|
|
910
|
+
modelsJsonLegacy: path.join(OMP_AGENT_DIR, 'models.json'),
|
|
911
|
+
mcp: path.join(OMP_AGENT_DIR, 'mcp.json'),
|
|
912
|
+
sessions: path.join(OMP_AGENT_DIR, 'sessions'),
|
|
913
|
+
skills: path.join(OMP_AGENT_DIR, 'skills'),
|
|
914
|
+
prompts: path.join(OMP_AGENT_DIR, 'prompts'),
|
|
915
|
+
commands: path.join(OMP_AGENT_DIR, 'commands'),
|
|
916
|
+
notes: path.join(OMP_AGENT_DIR, 'notes'),
|
|
917
|
+
extensions: path.join(OMP_AGENT_DIR, 'extensions'),
|
|
918
|
+
themes: path.join(OMP_AGENT_DIR, 'themes'),
|
|
919
|
+
packages: path.join(OMP_AGENT_DIR, 'packages')
|
|
897
920
|
}
|
|
898
921
|
};
|
|
899
922
|
|
package/src/index.js
CHANGED
|
@@ -34,9 +34,7 @@ function getVersion() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function finishCli(code = 0) {
|
|
37
|
-
|
|
38
|
-
PluginManager.shutdownPlugins();
|
|
39
|
-
process.exit(code);
|
|
37
|
+
return shutdownProcess(code);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
// 显示帮助信息
|
|
@@ -72,7 +70,7 @@ function showHelp() {
|
|
|
72
70
|
console.log(' ctx codex start 启动 Codex 代理');
|
|
73
71
|
console.log(' ctx gemini start 启动 Gemini 代理');
|
|
74
72
|
console.log(' ctx opencode start 启动 OpenCode 代理');
|
|
75
|
-
console.log(' ctx omp start
|
|
73
|
+
console.log(' ctx omp start 启动 OMP 动态切换');
|
|
76
74
|
console.log(chalk.gray(' (codex/gemini/opencode/omp 命令与 claude 类似)\n'));
|
|
77
75
|
|
|
78
76
|
console.log(chalk.yellow('[LOG] 日志管理:'));
|
|
@@ -128,20 +126,49 @@ function showHelp() {
|
|
|
128
126
|
console.log(chalk.gray(' 问题: https://github.com/ZeaoZhang/coding-tool/issues\n'));
|
|
129
127
|
}
|
|
130
128
|
|
|
129
|
+
let processShutdownPromise = null;
|
|
130
|
+
|
|
131
|
+
async function stopOwnedOmpGatewayBeforeExit() {
|
|
132
|
+
try {
|
|
133
|
+
const {
|
|
134
|
+
getOmpProxyStatus,
|
|
135
|
+
stopOmpProxyServer
|
|
136
|
+
} = require('./server/omp-proxy-server');
|
|
137
|
+
if (getOmpProxyStatus().running) {
|
|
138
|
+
await stopOmpProxyServer({ forceAfterMs: 1500 });
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${error.message}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shutdownProcess(code = 0, error = null) {
|
|
146
|
+
if (processShutdownPromise) return processShutdownPromise;
|
|
147
|
+
processShutdownPromise = (async () => {
|
|
148
|
+
await stopOwnedOmpGatewayBeforeExit();
|
|
149
|
+
eventBus.emitSync('cli:shutdown', {});
|
|
150
|
+
PluginManager.shutdownPlugins();
|
|
151
|
+
if (error) {
|
|
152
|
+
console.error(error);
|
|
153
|
+
}
|
|
154
|
+
process.exit(code);
|
|
155
|
+
})();
|
|
156
|
+
return processShutdownPromise;
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
// 全局错误处理
|
|
132
160
|
process.on('uncaughtException', (err) => {
|
|
133
161
|
// 忽略终端相关的错误(通常在 Ctrl+C 时发生)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
throw err;
|
|
162
|
+
const terminalError = err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE';
|
|
163
|
+
void shutdownProcess(terminalError ? 0 : 1, terminalError ? null : err);
|
|
138
164
|
});
|
|
139
165
|
|
|
140
|
-
//
|
|
141
|
-
process.on('SIGINT',
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
// 处理进程退出信号,OMP 必须先排空并恢复原生配置。
|
|
167
|
+
process.on('SIGINT', () => {
|
|
168
|
+
void shutdownProcess(0);
|
|
169
|
+
});
|
|
170
|
+
process.on('SIGTERM', () => {
|
|
171
|
+
void shutdownProcess(0);
|
|
145
172
|
});
|
|
146
173
|
|
|
147
174
|
/**
|
|
@@ -416,7 +443,7 @@ async function main() {
|
|
|
416
443
|
eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
|
|
417
444
|
if (!result.success) {
|
|
418
445
|
console.error(chalk.red(result.error));
|
|
419
|
-
|
|
446
|
+
return shutdownProcess(1);
|
|
420
447
|
}
|
|
421
448
|
return;
|
|
422
449
|
}
|
|
@@ -712,6 +739,5 @@ async function main() {
|
|
|
712
739
|
|
|
713
740
|
// 启动应用
|
|
714
741
|
main().catch((error) => {
|
|
715
|
-
|
|
716
|
-
process.exit(1);
|
|
742
|
+
void shutdownProcess(1, error);
|
|
717
743
|
});
|
|
@@ -294,7 +294,7 @@ module.exports = () => {
|
|
|
294
294
|
baseUrl,
|
|
295
295
|
apiKey: apiKey || '',
|
|
296
296
|
gatewaySourceType: gatewaySourceType || 'openai_compatible',
|
|
297
|
-
authMode:
|
|
297
|
+
authMode: ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key',
|
|
298
298
|
oauthProviderId: oauthProviderId || '',
|
|
299
299
|
model: model || null,
|
|
300
300
|
speedTestModel: speedTestModel || null,
|
|
@@ -407,6 +407,7 @@ module.exports = () => {
|
|
|
407
407
|
providerApi,
|
|
408
408
|
authMode,
|
|
409
409
|
oauthProviderId,
|
|
410
|
+
routingGroup,
|
|
410
411
|
presetId,
|
|
411
412
|
websiteUrl,
|
|
412
413
|
balanceToken,
|
|
@@ -425,8 +426,8 @@ module.exports = () => {
|
|
|
425
426
|
if (!name || !baseUrl) {
|
|
426
427
|
return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
|
|
427
428
|
}
|
|
428
|
-
const normalizedAuthMode =
|
|
429
|
-
if (!apiKey && normalizedAuthMode
|
|
429
|
+
const normalizedAuthMode = ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key';
|
|
430
|
+
if (!apiKey && normalizedAuthMode === 'api_key') {
|
|
430
431
|
return res.status(400).json({ error: 'API Key is required' });
|
|
431
432
|
}
|
|
432
433
|
|
|
@@ -435,7 +436,8 @@ module.exports = () => {
|
|
|
435
436
|
providerApi: providerApi || wireApi || 'openai-completions',
|
|
436
437
|
providerKey,
|
|
437
438
|
authMode: normalizedAuthMode,
|
|
438
|
-
oauthProviderId: oauthProviderId || providerKey || '',
|
|
439
|
+
oauthProviderId: normalizedAuthMode === 'oauth' ? (oauthProviderId || providerKey || '') : '',
|
|
440
|
+
routingGroup: String(routingGroup || '').trim(),
|
|
439
441
|
enabled,
|
|
440
442
|
weight,
|
|
441
443
|
maxConcurrency,
|
|
@@ -81,7 +81,7 @@ function buildCapabilities() {
|
|
|
81
81
|
},
|
|
82
82
|
notes: [
|
|
83
83
|
'The omp channel is backed by OMP and uses omp as its platform key.',
|
|
84
|
-
'
|
|
84
|
+
'Dynamic mode exposes managed ctx-* providers through the loopback OMP HTTP gateway; disabling it restores native OMP configuration.'
|
|
85
85
|
]
|
|
86
86
|
};
|
|
87
87
|
}
|