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,759 @@
|
|
|
1
|
+
# OMP 技能扫描设置实施计划
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 为 OMP 技能管理页和抽屉增加四项扫描来源设置,并通过白名单 API 安全持久化到 OMP `config.yml`。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 新增 `omp-skill-settings-service.js` 封装默认值、校验和保留式合并,`skills.js` 路由只负责 GET/PUT HTTP 协议。前端新增专用窄弹窗和轻量交互辅助模块,`SkillsPanel` 仅控制 OMP 条件入口、弹窗可见性以及保存后的强制刷新。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js、Express、js-yaml、Vue 3 `<script setup>`、Naive UI、Vitest、Vite。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 文件结构
|
|
14
|
+
|
|
15
|
+
- Create: `src/server/services/omp-skill-settings-service.js`,四字段设置的默认值、选择、校验、读取和保留式更新。
|
|
16
|
+
- Modify: `src/server/api/skills.js`,注册专用 GET/PUT 路由。
|
|
17
|
+
- Create: `tests/unit/services/omp-skill-settings-service.test.js`,默认值、部分更新、字段保留和非法写入保护。
|
|
18
|
+
- Modify: `tests/unit/api/skills-api.test.js`,验证专用 API 的成功响应与 400 错误映射。
|
|
19
|
+
- Modify: `src/web/src/api/skills.js`,封装 GET/PUT 客户端调用。
|
|
20
|
+
- Create: `src/web/src/utils/omp-skill-settings.js`,OMP 入口条件和保存后刷新编排。
|
|
21
|
+
- Create: `tests/unit/web/omp-skill-settings.test.js`,入口条件、成功刷新和失败不刷新回归。
|
|
22
|
+
- Create: `src/web/src/components/OmpSkillSettingsModal.vue`,四开关窄弹窗及加载、保存、错误状态。
|
|
23
|
+
- Modify: `src/web/src/components/SkillsPanel.vue`,独立页与抽屉页入口和保存事件集成。
|
|
24
|
+
- Modify: `CHANGELOG.md`,在最终验证通过后记录功能。
|
|
25
|
+
|
|
26
|
+
### Task 1: 服务端设置服务
|
|
27
|
+
|
|
28
|
+
**Files:**
|
|
29
|
+
- Create: `tests/unit/services/omp-skill-settings-service.test.js`
|
|
30
|
+
- Create: `src/server/services/omp-skill-settings-service.js`
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: 编写默认值和部分更新失败测试**
|
|
33
|
+
|
|
34
|
+
在测试中先通过 `require.cache` 注入 `readOmpSettings` 与 `writeOmpSettings`,再加载待测服务。覆盖缺失配置默认全开、部分更新仅修改目标字段:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const CONFIG_MODULE = require.resolve('../../../src/server/services/omp-config');
|
|
38
|
+
const SERVICE_MODULE = require.resolve('../../../src/server/services/omp-skill-settings-service');
|
|
39
|
+
|
|
40
|
+
let persisted;
|
|
41
|
+
let readOmpSettings;
|
|
42
|
+
let writeOmpSettings;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
persisted = {};
|
|
46
|
+
readOmpSettings = vi.fn(() => structuredClone(persisted));
|
|
47
|
+
writeOmpSettings = vi.fn(next => { persisted = structuredClone(next); });
|
|
48
|
+
require.cache[CONFIG_MODULE] = {
|
|
49
|
+
id: CONFIG_MODULE,
|
|
50
|
+
filename: CONFIG_MODULE,
|
|
51
|
+
loaded: true,
|
|
52
|
+
exports: { readOmpSettings, writeOmpSettings }
|
|
53
|
+
};
|
|
54
|
+
delete require.cache[SERVICE_MODULE];
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
delete require.cache[SERVICE_MODULE];
|
|
59
|
+
delete require.cache[CONFIG_MODULE];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('returns true defaults for every managed scan source', () => {
|
|
63
|
+
const { readOmpSkillSettings } = require(SERVICE_MODULE);
|
|
64
|
+
expect(readOmpSkillSettings()).toEqual({
|
|
65
|
+
enableCodexUser: true,
|
|
66
|
+
enableClaudeUser: true,
|
|
67
|
+
enablePiUser: true,
|
|
68
|
+
enablePiProject: true
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('partially updates one scan source', () => {
|
|
73
|
+
persisted = { skills: { enableCodexUser: false } };
|
|
74
|
+
const { updateOmpSkillSettings } = require(SERVICE_MODULE);
|
|
75
|
+
expect(updateOmpSkillSettings({ enablePiProject: false })).toEqual({
|
|
76
|
+
enableCodexUser: false,
|
|
77
|
+
enableClaudeUser: true,
|
|
78
|
+
enablePiUser: true,
|
|
79
|
+
enablePiProject: false
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 2: 运行测试并确认 RED**
|
|
85
|
+
|
|
86
|
+
Run: `npx vitest run tests/unit/services/omp-skill-settings-service.test.js`
|
|
87
|
+
|
|
88
|
+
Expected: FAIL,原因是 `omp-skill-settings-service` 模块不存在。
|
|
89
|
+
|
|
90
|
+
- [ ] **Step 3: 实现最小默认值、选择和部分更新**
|
|
91
|
+
|
|
92
|
+
创建服务模块:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
const { readOmpSettings, writeOmpSettings } = require('./omp-config');
|
|
96
|
+
|
|
97
|
+
const OMP_SKILL_SETTING_DEFAULTS = Object.freeze({
|
|
98
|
+
enableCodexUser: true,
|
|
99
|
+
enableClaudeUser: true,
|
|
100
|
+
enablePiUser: true,
|
|
101
|
+
enablePiProject: true
|
|
102
|
+
});
|
|
103
|
+
const OMP_SKILL_SETTING_KEYS = Object.freeze(Object.keys(OMP_SKILL_SETTING_DEFAULTS));
|
|
104
|
+
|
|
105
|
+
function isPlainObject(value) {
|
|
106
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function selectOmpSkillSettings(skills) {
|
|
110
|
+
const source = isPlainObject(skills) ? skills : {};
|
|
111
|
+
return Object.fromEntries(
|
|
112
|
+
OMP_SKILL_SETTING_KEYS.map(key => [
|
|
113
|
+
key,
|
|
114
|
+
typeof source[key] === 'boolean' ? source[key] : OMP_SKILL_SETTING_DEFAULTS[key]
|
|
115
|
+
])
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readOmpSkillSettings() {
|
|
120
|
+
return selectOmpSkillSettings(readOmpSettings().skills);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateOmpSkillSettingsPatch(patch) {
|
|
124
|
+
if (!isPlainObject(patch)) throw new Error('Invalid OMP skill settings: expected an object');
|
|
125
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
126
|
+
if (!OMP_SKILL_SETTING_KEYS.includes(key)) {
|
|
127
|
+
throw new Error(`Invalid OMP skill setting: ${key}`);
|
|
128
|
+
}
|
|
129
|
+
if (typeof value !== 'boolean') {
|
|
130
|
+
throw new Error(`Invalid OMP skill setting value for ${key}: expected boolean`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return patch;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function updateOmpSkillSettings(patch) {
|
|
137
|
+
validateOmpSkillSettingsPatch(patch);
|
|
138
|
+
const config = readOmpSettings();
|
|
139
|
+
const existingSkills = isPlainObject(config.skills) ? config.skills : {};
|
|
140
|
+
const nextConfig = {
|
|
141
|
+
...config,
|
|
142
|
+
skills: { ...existingSkills, ...patch }
|
|
143
|
+
};
|
|
144
|
+
writeOmpSettings(nextConfig);
|
|
145
|
+
return selectOmpSkillSettings(nextConfig.skills);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
OMP_SKILL_SETTING_DEFAULTS,
|
|
150
|
+
OMP_SKILL_SETTING_KEYS,
|
|
151
|
+
readOmpSkillSettings,
|
|
152
|
+
updateOmpSkillSettings,
|
|
153
|
+
validateOmpSkillSettingsPatch
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 4: 运行测试并确认 GREEN**
|
|
158
|
+
|
|
159
|
+
Run: `npx vitest run tests/unit/services/omp-skill-settings-service.test.js`
|
|
160
|
+
|
|
161
|
+
Expected: PASS。
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 5: 添加字段保留和非法请求失败测试**
|
|
164
|
+
|
|
165
|
+
追加测试:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
test('preserves unrelated top-level and skills fields', () => {
|
|
169
|
+
persisted = {
|
|
170
|
+
theme: 'night',
|
|
171
|
+
skills: {
|
|
172
|
+
enabled: false,
|
|
173
|
+
customDirectories: ['/opt/skills'],
|
|
174
|
+
enableClaudeUser: true
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const { updateOmpSkillSettings } = require(SERVICE_MODULE);
|
|
178
|
+
updateOmpSkillSettings({ enableClaudeUser: false });
|
|
179
|
+
expect(persisted).toEqual({
|
|
180
|
+
theme: 'night',
|
|
181
|
+
skills: {
|
|
182
|
+
enabled: false,
|
|
183
|
+
customDirectories: ['/opt/skills'],
|
|
184
|
+
enableClaudeUser: false
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test.each([
|
|
190
|
+
[{ enableAgentsUser: false }, /Invalid OMP skill setting/],
|
|
191
|
+
[{ enablePiUser: 'false' }, /expected boolean/],
|
|
192
|
+
[null, /expected an object/],
|
|
193
|
+
[[], /expected an object/]
|
|
194
|
+
])('rejects invalid patch without writing it', (patch, message) => {
|
|
195
|
+
persisted = { skills: { enabled: true } };
|
|
196
|
+
const before = structuredClone(persisted);
|
|
197
|
+
const { updateOmpSkillSettings } = require(SERVICE_MODULE);
|
|
198
|
+
expect(() => updateOmpSkillSettings(patch)).toThrow(message);
|
|
199
|
+
expect(writeOmpSettings).not.toHaveBeenCalled();
|
|
200
|
+
expect(persisted).toEqual(before);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('accepts an empty patch without destroying existing config', () => {
|
|
204
|
+
persisted = { providers: { demo: {} }, skills: { enablePiUser: false } };
|
|
205
|
+
const { updateOmpSkillSettings } = require(SERVICE_MODULE);
|
|
206
|
+
expect(updateOmpSkillSettings({}).enablePiUser).toBe(false);
|
|
207
|
+
expect(persisted.providers).toEqual({ demo: {} });
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- [ ] **Step 6: 运行新增测试并确认 GREEN**
|
|
212
|
+
|
|
213
|
+
Run: `npx vitest run tests/unit/services/omp-skill-settings-service.test.js`
|
|
214
|
+
|
|
215
|
+
Expected: PASS;非法 patch 不调用 `writeOmpSettings`,合法部分更新保留所有无关字段。
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 7: 提交服务层**
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git add src/server/services/omp-skill-settings-service.js tests/unit/services/omp-skill-settings-service.test.js
|
|
221
|
+
git commit -m "feat: add OMP skill settings service"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Task 2: 专用 Skills API
|
|
225
|
+
|
|
226
|
+
**Files:**
|
|
227
|
+
- Modify: `tests/unit/api/skills-api.test.js`
|
|
228
|
+
- Modify: `src/server/api/skills.js`
|
|
229
|
+
|
|
230
|
+
- [ ] **Step 1: 为路由测试注入设置服务桩**
|
|
231
|
+
|
|
232
|
+
在 `beforeEach` 中、加载路由前加入:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
ompSkillSettings = {
|
|
236
|
+
readOmpSkillSettings: vi.fn(() => ({
|
|
237
|
+
enableCodexUser: true,
|
|
238
|
+
enableClaudeUser: true,
|
|
239
|
+
enablePiUser: true,
|
|
240
|
+
enablePiProject: true
|
|
241
|
+
})),
|
|
242
|
+
updateOmpSkillSettings: vi.fn(patch => ({
|
|
243
|
+
enableCodexUser: true,
|
|
244
|
+
enableClaudeUser: true,
|
|
245
|
+
enablePiUser: true,
|
|
246
|
+
enablePiProject: patch.enablePiProject ?? true
|
|
247
|
+
}))
|
|
248
|
+
};
|
|
249
|
+
const ompSkillSettingsPath = require.resolve('../../../src/server/services/omp-skill-settings-service');
|
|
250
|
+
require.cache[ompSkillSettingsPath] = {
|
|
251
|
+
id: ompSkillSettingsPath,
|
|
252
|
+
filename: ompSkillSettingsPath,
|
|
253
|
+
loaded: true,
|
|
254
|
+
exports: ompSkillSettings
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
并在 `afterEach` 删除该缓存。
|
|
259
|
+
|
|
260
|
+
- [ ] **Step 2: 编写 GET、PUT 和非法字段 API 测试**
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
describe('OMP skill settings', () => {
|
|
264
|
+
test('GET /omp-settings returns persisted settings', async () => {
|
|
265
|
+
const res = await request(buildApp()).get('/omp-settings');
|
|
266
|
+
expect(res.status).toBe(200);
|
|
267
|
+
expect(res.body).toEqual({
|
|
268
|
+
success: true,
|
|
269
|
+
settings: {
|
|
270
|
+
enableCodexUser: true,
|
|
271
|
+
enableClaudeUser: true,
|
|
272
|
+
enablePiUser: true,
|
|
273
|
+
enablePiProject: true
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('PUT /omp-settings applies a partial update', async () => {
|
|
279
|
+
const res = await request(buildApp()).put('/omp-settings', { enablePiProject: false });
|
|
280
|
+
expect(res.status).toBe(200);
|
|
281
|
+
expect(ompSkillSettings.updateOmpSkillSettings).toHaveBeenCalledWith({ enablePiProject: false });
|
|
282
|
+
expect(res.body.settings.enablePiProject).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('PUT /omp-settings maps validation failure to 400', async () => {
|
|
286
|
+
ompSkillSettings.updateOmpSkillSettings.mockImplementation(() => {
|
|
287
|
+
throw new Error('Invalid OMP skill setting: unknown');
|
|
288
|
+
});
|
|
289
|
+
const res = await request(buildApp()).put('/omp-settings', { unknown: true });
|
|
290
|
+
expect(res.status).toBe(400);
|
|
291
|
+
expect(res.body.success).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 3: 运行 API 测试并确认 RED**
|
|
297
|
+
|
|
298
|
+
Run: `npx vitest run tests/unit/api/skills-api.test.js`
|
|
299
|
+
|
|
300
|
+
Expected: FAIL,`/omp-settings` 尚未注册或被通配技能详情路由截获。
|
|
301
|
+
|
|
302
|
+
- [ ] **Step 4: 在通配路由之前注册专用路由**
|
|
303
|
+
|
|
304
|
+
在 `src/server/api/skills.js` 顶部导入服务:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const {
|
|
308
|
+
readOmpSkillSettings,
|
|
309
|
+
updateOmpSkillSettings
|
|
310
|
+
} = require('../services/omp-skill-settings-service');
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
在 `router.get('/detail/*', ...)` 之前加入,确保固定路径不被后续参数路由截获:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
router.get('/omp-settings', (req, res) => {
|
|
317
|
+
try {
|
|
318
|
+
res.json({ success: true, settings: readOmpSkillSettings() });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error('[Skills API] Read OMP settings error:', err);
|
|
321
|
+
sendApiError(res, err);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
router.put('/omp-settings', (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
res.json({ success: true, settings: updateOmpSkillSettings(req.body) });
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error('[Skills API] Update OMP settings error:', err);
|
|
330
|
+
sendApiError(res, err);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- [ ] **Step 5: 运行 API 和服务测试并确认 GREEN**
|
|
336
|
+
|
|
337
|
+
Run: `npx vitest run tests/unit/api/skills-api.test.js tests/unit/services/omp-skill-settings-service.test.js`
|
|
338
|
+
|
|
339
|
+
Expected: PASS。
|
|
340
|
+
|
|
341
|
+
- [ ] **Step 6: 提交 API**
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
git add src/server/api/skills.js tests/unit/api/skills-api.test.js
|
|
345
|
+
git commit -m "feat: expose OMP skill settings API"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Task 3: 前端 API 与交互辅助模块
|
|
349
|
+
|
|
350
|
+
**Files:**
|
|
351
|
+
- Modify: `src/web/src/api/skills.js`
|
|
352
|
+
- Create: `src/web/src/utils/omp-skill-settings.js`
|
|
353
|
+
- Create: `tests/unit/web/omp-skill-settings.test.js`
|
|
354
|
+
|
|
355
|
+
- [ ] **Step 1: 编写前端 API 和入口条件失败测试**
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
359
|
+
|
|
360
|
+
const client = {
|
|
361
|
+
get: vi.fn(async () => ({ data: { success: true, settings: {} } })),
|
|
362
|
+
put: vi.fn(async () => ({ data: { success: true, settings: {} } }))
|
|
363
|
+
};
|
|
364
|
+
vi.mock('../../../src/web/src/api/client.js', () => ({ client }));
|
|
365
|
+
|
|
366
|
+
describe('OMP skill settings web integration', () => {
|
|
367
|
+
beforeEach(() => vi.clearAllMocks());
|
|
368
|
+
|
|
369
|
+
test('calls the dedicated GET and PUT endpoints', async () => {
|
|
370
|
+
const { getOmpSkillSettings, updateOmpSkillSettings } = await import('../../../src/web/src/api/skills.js');
|
|
371
|
+
await getOmpSkillSettings();
|
|
372
|
+
await updateOmpSkillSettings({ enablePiProject: false });
|
|
373
|
+
expect(client.get).toHaveBeenCalledWith('/skills/omp-settings');
|
|
374
|
+
expect(client.put).toHaveBeenCalledWith('/skills/omp-settings', { enablePiProject: false });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test.each([
|
|
378
|
+
['omp', true],
|
|
379
|
+
['claude', false],
|
|
380
|
+
['codex', false],
|
|
381
|
+
['gemini', false],
|
|
382
|
+
['opencode', false]
|
|
383
|
+
])('shows settings only for %s', async (platform, expected) => {
|
|
384
|
+
const { supportsOmpSkillSettings } = await import('../../../src/web/src/utils/omp-skill-settings.js');
|
|
385
|
+
expect(supportsOmpSkillSettings(platform)).toBe(expected);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 2: 运行测试并确认 RED**
|
|
391
|
+
|
|
392
|
+
Run: `npx vitest run tests/unit/web/omp-skill-settings.test.js`
|
|
393
|
+
|
|
394
|
+
Expected: FAIL,API 方法和辅助模块不存在。
|
|
395
|
+
|
|
396
|
+
- [ ] **Step 3: 实现前端 API 和入口判断**
|
|
397
|
+
|
|
398
|
+
在 `src/web/src/api/skills.js` 增加:
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
export async function getOmpSkillSettings() {
|
|
402
|
+
const response = await client.get('/skills/omp-settings');
|
|
403
|
+
return response.data;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function updateOmpSkillSettings(settings) {
|
|
407
|
+
const response = await client.put('/skills/omp-settings', settings);
|
|
408
|
+
return response.data;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
创建辅助模块:
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
export function supportsOmpSkillSettings(platform) {
|
|
416
|
+
return platform === 'omp';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function refreshAfterOmpSkillSettingsSave(refreshSkills) {
|
|
420
|
+
await refreshSkills(true);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- [ ] **Step 4: 编写保存成功刷新一次的测试**
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
test('refreshes the skill list once after a successful modal save', async () => {
|
|
428
|
+
const { refreshAfterOmpSkillSettingsSave } = await import('../../../src/web/src/utils/omp-skill-settings.js');
|
|
429
|
+
const refreshSkills = vi.fn(async () => {});
|
|
430
|
+
await refreshAfterOmpSkillSettingsSave(refreshSkills);
|
|
431
|
+
expect(refreshSkills).toHaveBeenCalledTimes(1);
|
|
432
|
+
expect(refreshSkills).toHaveBeenCalledWith(true);
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
写入失败时弹窗不会发出 `saved`,因此 SkillsPanel 不会调用该刷新辅助函数。API reject 测试验证失败保持向上传播:
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
test('propagates a settings write failure without touching an existing list', async () => {
|
|
440
|
+
const existingSkills = [{ key: 'kept' }];
|
|
441
|
+
client.put.mockRejectedValueOnce(new Error('write failed'));
|
|
442
|
+
const { updateOmpSkillSettings } = await import('../../../src/web/src/api/skills.js');
|
|
443
|
+
await expect(updateOmpSkillSettings({ enablePiUser: false })).rejects.toThrow('write failed');
|
|
444
|
+
expect(existingSkills).toEqual([{ key: 'kept' }]);
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
读取失败由设置弹窗自身处理,且读取函数不接收或修改技能列表。补一个失败断言:
|
|
449
|
+
|
|
450
|
+
```js
|
|
451
|
+
test('a settings read failure leaves the existing skill list untouched', async () => {
|
|
452
|
+
const existingSkills = [{ key: 'kept' }];
|
|
453
|
+
client.get.mockRejectedValueOnce(new Error('read failed'));
|
|
454
|
+
const { getOmpSkillSettings } = await import('../../../src/web/src/api/skills.js');
|
|
455
|
+
await expect(getOmpSkillSettings()).rejects.toThrow('read failed');
|
|
456
|
+
expect(existingSkills).toEqual([{ key: 'kept' }]);
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
- [ ] **Step 5: 运行测试并确认 GREEN**
|
|
461
|
+
|
|
462
|
+
Run: `npx vitest run tests/unit/web/omp-skill-settings.test.js`
|
|
463
|
+
|
|
464
|
+
Expected: PASS。
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 6: 提交前端数据层**
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
git add src/web/src/api/skills.js src/web/src/utils/omp-skill-settings.js tests/unit/web/omp-skill-settings.test.js
|
|
470
|
+
git commit -m "feat: add OMP skill settings web flow"
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Task 4: 设置弹窗组件
|
|
474
|
+
|
|
475
|
+
**Files:**
|
|
476
|
+
- Create: `src/web/src/components/OmpSkillSettingsModal.vue`
|
|
477
|
+
|
|
478
|
+
- [ ] **Step 1: 创建四开关弹窗模板**
|
|
479
|
+
|
|
480
|
+
组件使用受控 `visible` 属性,读取时 `NSpin`,读取失败时 `NAlert`,设置项按行展示:
|
|
481
|
+
|
|
482
|
+
```vue
|
|
483
|
+
<template>
|
|
484
|
+
<n-modal
|
|
485
|
+
v-model:show="visible"
|
|
486
|
+
preset="card"
|
|
487
|
+
title="技能扫描设置"
|
|
488
|
+
:bordered="false"
|
|
489
|
+
:closable="!saving"
|
|
490
|
+
:mask-closable="!saving"
|
|
491
|
+
:close-on-esc="!saving"
|
|
492
|
+
style="width: 420px; max-width: 92vw;"
|
|
493
|
+
>
|
|
494
|
+
<n-spin :show="loading">
|
|
495
|
+
<n-alert v-if="loadError" type="error" :show-icon="true">
|
|
496
|
+
{{ loadError }}
|
|
497
|
+
</n-alert>
|
|
498
|
+
<div v-else class="scan-settings">
|
|
499
|
+
<div class="section-title">扫描来源</div>
|
|
500
|
+
<div v-for="item in settingItems" :key="item.key" class="setting-row">
|
|
501
|
+
<div class="setting-copy">
|
|
502
|
+
<div class="setting-label">{{ item.label }}</div>
|
|
503
|
+
<div class="setting-description">{{ item.description }}</div>
|
|
504
|
+
</div>
|
|
505
|
+
<n-switch v-model:value="form[item.key]" :disabled="loading || saving" />
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</n-spin>
|
|
509
|
+
<template #footer>
|
|
510
|
+
<div class="modal-actions">
|
|
511
|
+
<n-button :disabled="saving" @click="visible = false">取消</n-button>
|
|
512
|
+
<n-button type="primary" :loading="saving" :disabled="loading || !!loadError" @click="handleSave">
|
|
513
|
+
保存
|
|
514
|
+
</n-button>
|
|
515
|
+
</div>
|
|
516
|
+
</template>
|
|
517
|
+
</n-modal>
|
|
518
|
+
</template>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- [ ] **Step 2: 实现读取、表单和保存状态**
|
|
522
|
+
|
|
523
|
+
```vue
|
|
524
|
+
<script setup>
|
|
525
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
526
|
+
import { NAlert, NButton, NModal, NSpin, NSwitch, useMessage } from 'naive-ui'
|
|
527
|
+
import { getOmpSkillSettings, updateOmpSkillSettings } from '../api/skills'
|
|
528
|
+
|
|
529
|
+
const props = defineProps({ visible: Boolean })
|
|
530
|
+
const emit = defineEmits(['update:visible', 'saved'])
|
|
531
|
+
const message = useMessage()
|
|
532
|
+
const loading = ref(false)
|
|
533
|
+
const saving = ref(false)
|
|
534
|
+
const loadError = ref('')
|
|
535
|
+
const form = reactive({
|
|
536
|
+
enableCodexUser: true,
|
|
537
|
+
enableClaudeUser: true,
|
|
538
|
+
enablePiUser: true,
|
|
539
|
+
enablePiProject: true
|
|
540
|
+
})
|
|
541
|
+
const settingItems = [
|
|
542
|
+
{ key: 'enableCodexUser', label: 'Codex 用户 Skills', description: '扫描 Codex 用户技能目录' },
|
|
543
|
+
{ key: 'enableClaudeUser', label: 'Claude 用户与插件 Skills', description: '扫描 Claude 用户目录和插件技能' },
|
|
544
|
+
{ key: 'enablePiUser', label: 'OMP 用户与插件 Skills', description: '扫描 OMP 用户目录和插件技能' },
|
|
545
|
+
{ key: 'enablePiProject', label: '当前项目 Skills', description: '扫描当前项目 .omp/skills' }
|
|
546
|
+
]
|
|
547
|
+
const visible = computed({
|
|
548
|
+
get: () => props.visible,
|
|
549
|
+
set: value => {
|
|
550
|
+
if (!saving.value) emit('update:visible', value)
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
async function loadSettings() {
|
|
555
|
+
loading.value = true
|
|
556
|
+
loadError.value = ''
|
|
557
|
+
try {
|
|
558
|
+
const result = await getOmpSkillSettings()
|
|
559
|
+
Object.assign(form, result.settings)
|
|
560
|
+
} catch (error) {
|
|
561
|
+
loadError.value = `加载技能扫描设置失败: ${error.message}`
|
|
562
|
+
message.error(loadError.value)
|
|
563
|
+
} finally {
|
|
564
|
+
loading.value = false
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function handleSave() {
|
|
569
|
+
saving.value = true
|
|
570
|
+
try {
|
|
571
|
+
await updateOmpSkillSettings({ ...form })
|
|
572
|
+
message.success('技能扫描设置已保存')
|
|
573
|
+
emit('saved')
|
|
574
|
+
} catch (error) {
|
|
575
|
+
message.error(`保存技能扫描设置失败: ${error.message}`)
|
|
576
|
+
} finally {
|
|
577
|
+
saving.value = false
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
watch(() => props.visible, value => {
|
|
582
|
+
if (value) loadSettings()
|
|
583
|
+
})
|
|
584
|
+
</script>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
- [ ] **Step 3: 添加局部样式**
|
|
588
|
+
|
|
589
|
+
```vue
|
|
590
|
+
<style scoped>
|
|
591
|
+
.scan-settings { display: flex; flex-direction: column; }
|
|
592
|
+
.section-title { margin-bottom: 8px; color: var(--text-secondary); font-size: 13px; font-weight: 600; }
|
|
593
|
+
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 12px 0; border-bottom: 1px solid var(--border-light); }
|
|
594
|
+
.setting-row:last-child { border-bottom: 0; }
|
|
595
|
+
.setting-copy { min-width: 0; }
|
|
596
|
+
.setting-label { color: var(--text-primary); font-weight: 500; }
|
|
597
|
+
.setting-description { margin-top: 3px; color: var(--text-tertiary); font-size: 12px; line-height: 1.45; }
|
|
598
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
599
|
+
</style>
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
- [ ] **Step 4: 运行 Web 构建并处理模板错误**
|
|
603
|
+
|
|
604
|
+
Run: `npm run build:web`
|
|
605
|
+
|
|
606
|
+
Expected: PASS,Vite 输出生产构建文件,无 Vue 编译错误。
|
|
607
|
+
|
|
608
|
+
- [ ] **Step 5: 提交弹窗组件**
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
git add src/web/src/components/OmpSkillSettingsModal.vue
|
|
612
|
+
git commit -m "feat: add OMP skill scan settings modal"
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Task 5: SkillsPanel 条件入口与保存刷新
|
|
616
|
+
|
|
617
|
+
**Files:**
|
|
618
|
+
- Modify: `src/web/src/components/SkillsPanel.vue`
|
|
619
|
+
|
|
620
|
+
- [ ] **Step 1: 在独立页和抽屉页加入同一条件入口**
|
|
621
|
+
|
|
622
|
+
在两处 `asset-action-row` 的刷新按钮前加入:
|
|
623
|
+
|
|
624
|
+
```vue
|
|
625
|
+
<n-button
|
|
626
|
+
v-if="showOmpSettings"
|
|
627
|
+
text
|
|
628
|
+
:focusable="false"
|
|
629
|
+
class="action-btn"
|
|
630
|
+
@click="showOmpSettingsModal = true"
|
|
631
|
+
>
|
|
632
|
+
<template #icon><n-icon><SettingsOutline /></n-icon></template>
|
|
633
|
+
设置
|
|
634
|
+
</n-button>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
在现有弹窗组件区域加入:
|
|
638
|
+
|
|
639
|
+
```vue
|
|
640
|
+
<OmpSkillSettingsModal
|
|
641
|
+
v-model:visible="showOmpSettingsModal"
|
|
642
|
+
@saved="handleOmpSettingsSaved"
|
|
643
|
+
/>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 2: 接入平台判断和保存编排**
|
|
647
|
+
|
|
648
|
+
更新导入:
|
|
649
|
+
|
|
650
|
+
```js
|
|
651
|
+
import { SettingsOutline } from '@vicons/ionicons5'
|
|
652
|
+
import OmpSkillSettingsModal from './OmpSkillSettingsModal.vue'
|
|
653
|
+
import {
|
|
654
|
+
refreshAfterOmpSkillSettingsSave,
|
|
655
|
+
supportsOmpSkillSettings
|
|
656
|
+
} from '../utils/omp-skill-settings'
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
增加状态和计算属性:
|
|
660
|
+
|
|
661
|
+
```js
|
|
662
|
+
const showOmpSettingsModal = ref(false)
|
|
663
|
+
const showOmpSettings = computed(() => supportsOmpSkillSettings(currentPlatform.value))
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`saved` 仅在弹窗 PUT 成功后发出;SkillsPanel 只负责关闭弹窗并执行强制刷新:
|
|
667
|
+
|
|
668
|
+
```js
|
|
669
|
+
async function handleOmpSettingsSaved() {
|
|
670
|
+
showOmpSettingsModal.value = false
|
|
671
|
+
await refreshAfterOmpSkillSettingsSave(loadData)
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
- [ ] **Step 3: 核对最终回归边界**
|
|
676
|
+
|
|
677
|
+
Task 3 的回归测试必须保持以下断言:
|
|
678
|
+
|
|
679
|
+
- `supportsOmpSkillSettings('omp')` 为 `true`,其他内建平台为 `false`。
|
|
680
|
+
- `refreshAfterOmpSkillSettingsSave` 仅调用一次 `loadData(true)` 形态的回调。
|
|
681
|
+
- GET 或 PUT reject 时调用方收到错误,既有技能数组未被修改。
|
|
682
|
+
|
|
683
|
+
- [ ] **Step 4: 运行前端回归与生产构建**
|
|
684
|
+
|
|
685
|
+
Run: `npx vitest run tests/unit/web/omp-skill-settings.test.js && npm run build:web`
|
|
686
|
+
|
|
687
|
+
Expected: 测试 PASS,Web 构建 PASS。
|
|
688
|
+
|
|
689
|
+
- [ ] **Step 5: 提交 SkillsPanel 集成**
|
|
690
|
+
|
|
691
|
+
```bash
|
|
692
|
+
git add src/web/src/components/SkillsPanel.vue src/web/src/utils/omp-skill-settings.js tests/unit/web/omp-skill-settings.test.js
|
|
693
|
+
git commit -m "feat: expose OMP skill settings in skills panel"
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Task 6: 端到端验证与收尾
|
|
697
|
+
|
|
698
|
+
**Files:**
|
|
699
|
+
- Modify: `CHANGELOG.md`
|
|
700
|
+
|
|
701
|
+
- [ ] **Step 1: 运行服务端和前端定向测试**
|
|
702
|
+
|
|
703
|
+
Run:
|
|
704
|
+
|
|
705
|
+
```bash
|
|
706
|
+
npx vitest run \
|
|
707
|
+
tests/unit/services/omp-skill-settings-service.test.js \
|
|
708
|
+
tests/unit/api/skills-api.test.js \
|
|
709
|
+
tests/unit/web/omp-skill-settings.test.js
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Expected: 全部 PASS,无未处理异常。
|
|
713
|
+
|
|
714
|
+
- [ ] **Step 2: 运行完整单元测试**
|
|
715
|
+
|
|
716
|
+
Run: `npm run test:unit`
|
|
717
|
+
|
|
718
|
+
Expected: PASS。若已有无关失败,记录准确测试名与现象,不修改无关代码掩盖失败。
|
|
719
|
+
|
|
720
|
+
- [ ] **Step 3: 构建 Web 生产包**
|
|
721
|
+
|
|
722
|
+
Run: `npm run build:web`
|
|
723
|
+
|
|
724
|
+
Expected: PASS,Vite 完成生产构建。
|
|
725
|
+
|
|
726
|
+
- [ ] **Step 4: 启动应用并烟测真实路径**
|
|
727
|
+
|
|
728
|
+
Run: `npm run dev:server`
|
|
729
|
+
|
|
730
|
+
使用受管进程启动服务后,在浏览器访问当前项目 Web 地址并验证:
|
|
731
|
+
|
|
732
|
+
1. OMP Skills 独立页显示“设置”,Claude/Codex 等平台不显示。
|
|
733
|
+
2. OMP Skills 抽屉显示同一入口。
|
|
734
|
+
3. 打开弹窗能读取四个持久化值。
|
|
735
|
+
4. 修改一项并保存后提示成功、关闭弹窗、列表发起强制刷新。
|
|
736
|
+
5. 检查 OMP `config.yml`,目标字段已改变,`skills.enabled`、其他 skills 字段和顶层字段仍存在。
|
|
737
|
+
|
|
738
|
+
Expected: 五项全部符合。
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 5: 更新 CHANGELOG**
|
|
741
|
+
|
|
742
|
+
在当前未发布版本的 Added 或 Changed 下加入:
|
|
743
|
+
|
|
744
|
+
```markdown
|
|
745
|
+
- **OMP 技能扫描设置** - OMP 技能独立页与抽屉新增扫描来源设置,可安全更新 Codex、Claude、OMP 用户/插件及项目 Skills 扫描开关,并在保存后刷新技能列表
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
- [ ] **Step 6: 复跑变更相关验证**
|
|
749
|
+
|
|
750
|
+
Run: `npx vitest run tests/unit/services/omp-skill-settings-service.test.js tests/unit/api/skills-api.test.js tests/unit/web/omp-skill-settings.test.js && npm run build:web`
|
|
751
|
+
|
|
752
|
+
Expected: 全部 PASS。
|
|
753
|
+
|
|
754
|
+
- [ ] **Step 7: 提交收尾**
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
git add CHANGELOG.md
|
|
758
|
+
git commit -m "docs: note OMP skill scan settings"
|
|
759
|
+
```
|