H-289: add Captain Skill management UI (#47)

* H-289: add Captain Skill management UI

* H-289: guard Captain Skill assistant switches

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-18 15:34:26 +08:00
committed by GitHub
co-authored by rogee
parent 0f2b8d7857
commit f50eacdc92
17 changed files with 1725 additions and 0 deletions
@@ -0,0 +1,68 @@
import CaptainSkills from 'dashboard/api/captain/skills';
import { createStore } from '../storeFactory';
const withFlag = (mutations, flag, request) => async (context, payload) => {
context.commit(mutations.SET_UI_FLAG, { [flag]: true });
try {
return await request(context, payload);
} finally {
context.commit(mutations.SET_UI_FLAG, { [flag]: false });
}
};
let latestListRequest = 0;
export default createStore({
name: 'CaptainSkill',
API: CaptainSkills,
actions: mutations => ({
get: async ({ commit }, params) => {
const requestId = ++latestListRequest;
commit(mutations.SET_UI_FLAG, { fetchingList: true });
try {
const { data } = await CaptainSkills.get(params);
if (requestId === latestListRequest) {
commit(mutations.SET, data.payload);
commit(mutations.SET_META, data.meta);
}
return data.payload;
} finally {
if (requestId === latestListRequest) {
commit(mutations.SET_UI_FLAG, { fetchingList: false });
}
}
},
detail: withFlag(mutations, 'fetchingItem', async (_, id) => {
return (await CaptainSkills.show(id)).data;
}),
create: withFlag(
mutations,
'creatingItem',
async ({ commit }, payload) => {
const { data } = await CaptainSkills.create(payload);
commit(mutations.UPSERT, data);
return data;
}
),
update: withFlag(
mutations,
'updatingItem',
async ({ commit }, { id, ...payload }) => {
const { data } = await CaptainSkills.update(id, payload);
commit(mutations.UPSERT, data);
return data;
}
),
delete: withFlag(mutations, 'deletingItem', async ({ commit }, id) => {
await CaptainSkills.delete(id);
commit(mutations.DELETE, id);
return id;
}),
bind: withFlag(mutations, 'updatingItem', (_, payload) =>
CaptainSkills.bind(payload)
),
unbind: withFlag(mutations, 'updatingItem', (_, payload) =>
CaptainSkills.unbind(payload)
),
}),
});
@@ -0,0 +1,93 @@
import CaptainSkillsAPI from 'dashboard/api/captain/skills';
import store from './skills';
vi.mock('dashboard/api/captain/skills', () => ({
default: {
get: vi.fn(),
bind: vi.fn(),
unbind: vi.fn(),
},
}));
describe('captainSkills store', () => {
beforeEach(() => vi.clearAllMocks());
it('keeps only the latest assistant list response and loading state', async () => {
let resolveA;
let resolveB;
CaptainSkillsAPI.get
.mockReturnValueOnce(
new Promise(resolve => {
resolveA = resolve;
})
)
.mockReturnValueOnce(
new Promise(resolve => {
resolveB = resolve;
})
);
const commit = vi.fn();
const requestA = store.actions.get({ commit }, { assistantId: 1 });
const requestB = store.actions.get({ commit }, { assistantId: 2 });
resolveB({ data: { payload: ['B'], meta: { page: 2 } } });
await requestB;
resolveA({ data: { payload: ['A'], meta: { page: 1 } } });
await requestA;
expect(commit).toHaveBeenCalledWith('SET_CAPTAINSKILL', ['B']);
expect(commit).toHaveBeenCalledWith('SET_CAPTAINSKILL_META', {
page: 2,
});
expect(commit).not.toHaveBeenCalledWith('SET_CAPTAINSKILL', ['A']);
expect(commit).not.toHaveBeenCalledWith('SET_CAPTAINSKILL_META', {
page: 1,
});
expect(
commit.mock.calls.filter(
([type, value]) =>
type === 'SET_CAPTAINSKILL_UI_FLAG' &&
value.fetchingList === false
)
).toHaveLength(1);
});
it.each(['bind', 'unbind'])(
'%s toggles the row loading state',
async action => {
CaptainSkillsAPI[action].mockResolvedValue({});
const commit = vi.fn();
const payload = { assistantId: 2, skillId: 3 };
await store.actions[action]({ commit }, payload);
expect(CaptainSkillsAPI[action]).toHaveBeenCalledWith(payload);
expect(commit).toHaveBeenNthCalledWith(
1,
'SET_CAPTAINSKILL_UI_FLAG',
{
updatingItem: true,
}
);
expect(commit).toHaveBeenLastCalledWith(
'SET_CAPTAINSKILL_UI_FLAG',
{
updatingItem: false,
}
);
}
);
it('restores loading state when binding fails', async () => {
const error = new Error('conflict');
CaptainSkillsAPI.bind.mockRejectedValue(error);
const commit = vi.fn();
await expect(
store.actions.bind({ commit }, { assistantId: 2, skillId: 3 })
).rejects.toBe(error);
expect(commit).toHaveBeenLastCalledWith('SET_CAPTAINSKILL_UI_FLAG', {
updatingItem: false,
});
});
});
@@ -59,6 +59,7 @@ import copilotMessages from './captain/copilotMessages';
import captainScenarios from './captain/scenarios';
import captainTools from './captain/tools';
import captainCustomTools from './captain/customTools';
import captainSkills from './captain/skills';
import platform from './modules/platform';
const plugins = [];
@@ -124,6 +125,7 @@ export default createStore({
captainScenarios,
captainTools,
captainCustomTools,
captainSkills,
platform,
},
plugins,