Add room skill setting mutations (#133)
* add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle * reuse status dashboard message and simplify settings actions * refine settings page UX * fix settings nav hash routing * add dashboard UX verification * refine dashboard settings and inbox UX * remove inbox top-level navigation * remove dashboard health top-level navigation * fix: allow runtime image attachment paths * feat: configure attachment allowlist dirs * fix: clean duplicate dashboard status messages * fix: poll dashboard duplicate cleanup between status updates * fix: delete dashboard duplicates on create * Refine settings IA with tabbed sections * Add read-only runtime inventory settings * Fix runtime inventory MCP JSON parsing * Add room skill settings inventory * Add room skill setting mutations
This commit is contained in:
@@ -171,6 +171,43 @@
|
|||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggles {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggle {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(15, 23, 42, 0.38);
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggle input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggle > span {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggle strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-room-skill-toggle small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.runtime-summary-card,
|
.runtime-summary-card,
|
||||||
.runtime-skill-grid,
|
.runtime-skill-grid,
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { useEffect, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
fetchRoomSkillSettings,
|
fetchRoomSkillSettings,
|
||||||
fetchRuntimeInventory,
|
fetchRuntimeInventory,
|
||||||
|
updateRoomSkillSetting,
|
||||||
type RuntimeAgentInventory,
|
type RuntimeAgentInventory,
|
||||||
type RuntimeInventorySnapshot,
|
type RuntimeInventorySnapshot,
|
||||||
type RuntimePathSnapshot,
|
type RuntimePathSnapshot,
|
||||||
type RuntimeSkillDirSnapshot,
|
type RuntimeSkillDirSnapshot,
|
||||||
type RoomSkillCatalogItem,
|
type RoomSkillCatalogItem,
|
||||||
|
type RoomSkillSettingUpdateInput,
|
||||||
type RoomSkillSettingsSnapshot,
|
type RoomSkillSettingsSnapshot,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { SettingsSectionHeading } from './SettingsPanelChrome';
|
import { SettingsSectionHeading } from './SettingsPanelChrome';
|
||||||
@@ -106,8 +108,12 @@ function agentLabel(agentType: 'claude-code' | 'codex') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RoomSkillPolicyCard({
|
function RoomSkillPolicyCard({
|
||||||
|
onToggle,
|
||||||
|
savingKey,
|
||||||
snapshot,
|
snapshot,
|
||||||
}: {
|
}: {
|
||||||
|
onToggle: (input: RoomSkillSettingUpdateInput) => void;
|
||||||
|
savingKey: string | null;
|
||||||
snapshot: RoomSkillSettingsSnapshot | null;
|
snapshot: RoomSkillSettingsSnapshot | null;
|
||||||
}) {
|
}) {
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
@@ -159,6 +165,7 @@ function RoomSkillPolicyCard({
|
|||||||
const disabledNames = agent.disabledSkillIds
|
const disabledNames = agent.disabledSkillIds
|
||||||
.map((id) => catalogById.get(id)?.displayName ?? id)
|
.map((id) => catalogById.get(id)?.displayName ?? id)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
const enabledIds = new Set(agent.effectiveEnabledSkillIds);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="runtime-room-agent-policy"
|
className="runtime-room-agent-policy"
|
||||||
@@ -177,6 +184,39 @@ function RoomSkillPolicyCard({
|
|||||||
{disabledNames.length > 0 ? (
|
{disabledNames.length > 0 ? (
|
||||||
<small>OFF: {disabledNames.join(', ')}</small>
|
<small>OFF: {disabledNames.join(', ')}</small>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="runtime-room-skill-toggles">
|
||||||
|
{agent.availableSkillIds.map((skillId) => {
|
||||||
|
const skill = catalogById.get(skillId);
|
||||||
|
const key = `${room.jid}:${agent.agentType}:${skillId}`;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className="runtime-room-skill-toggle"
|
||||||
|
key={skillId}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={enabledIds.has(skillId)}
|
||||||
|
disabled={savingKey !== null}
|
||||||
|
onChange={(event) =>
|
||||||
|
onToggle({
|
||||||
|
roomJid: room.jid,
|
||||||
|
agentType: agent.agentType,
|
||||||
|
skillId,
|
||||||
|
enabled: event.currentTarget.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<strong>{skill?.displayName ?? skillId}</strong>
|
||||||
|
<small>
|
||||||
|
{skill?.scope ?? 'unknown'}
|
||||||
|
{savingKey === key ? ' · 저장 중' : ''}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -201,6 +241,10 @@ export function RuntimeInventorySettings() {
|
|||||||
const [roomSkillSnapshot, setRoomSkillSnapshot] =
|
const [roomSkillSnapshot, setRoomSkillSnapshot] =
|
||||||
useState<RoomSkillSettingsSnapshot | null>(null);
|
useState<RoomSkillSettingsSnapshot | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [roomSkillError, setRoomSkillError] = useState<string | null>(null);
|
||||||
|
const [savingRoomSkillKey, setSavingRoomSkillKey] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -220,6 +264,20 @@ export function RuntimeInventorySettings() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function handleRoomSkillToggle(input: RoomSkillSettingUpdateInput) {
|
||||||
|
const key = `${input.roomJid}:${input.agentType}:${input.skillId}`;
|
||||||
|
setSavingRoomSkillKey(key);
|
||||||
|
setRoomSkillError(null);
|
||||||
|
try {
|
||||||
|
const next = await updateRoomSkillSetting(input);
|
||||||
|
setRoomSkillSnapshot(next);
|
||||||
|
} catch (err) {
|
||||||
|
setRoomSkillError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setSavingRoomSkillKey(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-labelledby="settings-runtime-tab"
|
aria-labelledby="settings-runtime-tab"
|
||||||
@@ -255,7 +313,16 @@ export function RuntimeInventorySettings() {
|
|||||||
|
|
||||||
<AgentInventoryCard title="Codex" inventory={snapshot.codex} />
|
<AgentInventoryCard title="Codex" inventory={snapshot.codex} />
|
||||||
<AgentInventoryCard title="Claude Code" inventory={snapshot.claude} />
|
<AgentInventoryCard title="Claude Code" inventory={snapshot.claude} />
|
||||||
<RoomSkillPolicyCard snapshot={roomSkillSnapshot} />
|
{roomSkillError ? (
|
||||||
|
<p className="settings-error">{roomSkillError}</p>
|
||||||
|
) : null}
|
||||||
|
<RoomSkillPolicyCard
|
||||||
|
onToggle={(input) => {
|
||||||
|
void handleRoomSkillToggle(input);
|
||||||
|
}}
|
||||||
|
savingKey={savingRoomSkillKey}
|
||||||
|
snapshot={roomSkillSnapshot}
|
||||||
|
/>
|
||||||
|
|
||||||
<article className="runtime-agent-card">
|
<article className="runtime-agent-card">
|
||||||
<header className="runtime-card-head">
|
<header className="runtime-card-head">
|
||||||
|
|||||||
@@ -473,6 +473,13 @@ export interface RoomSkillSettingsSnapshot {
|
|||||||
rooms: RoomSkillPolicyRoom[];
|
rooms: RoomSkillPolicyRoom[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoomSkillSettingUpdateInput {
|
||||||
|
roomJid: string;
|
||||||
|
agentType: 'claude-code' | 'codex';
|
||||||
|
skillId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MoaReferenceStatus {
|
export interface MoaReferenceStatus {
|
||||||
model: string;
|
model: string;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
@@ -635,6 +642,30 @@ export async function fetchRoomSkillSettings(): Promise<RoomSkillSettingsSnapsho
|
|||||||
return fetchJson('/api/settings/room-skills');
|
return fetchJson('/api/settings/room-skills');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateRoomSkillSetting(
|
||||||
|
input: RoomSkillSettingUpdateInput,
|
||||||
|
): Promise<RoomSkillSettingsSnapshot> {
|
||||||
|
const response = await fetch('/api/settings/room-skills', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
let msg = `update room skill failed: ${response.status}`;
|
||||||
|
try {
|
||||||
|
const payload = (await response.json()) as { error?: string };
|
||||||
|
if (payload.error) msg = payload.error;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return (await response.json()) as RoomSkillSettingsSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateCodexFeatures(
|
export async function updateCodexFeatures(
|
||||||
input: Partial<CodexFeatureSnapshot>,
|
input: Partial<CodexFeatureSnapshot>,
|
||||||
): Promise<CodexFeatureSnapshot> {
|
): Promise<CodexFeatureSnapshot> {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface MockApiState {
|
|||||||
codexFeatureUpdates: number;
|
codexFeatureUpdates: number;
|
||||||
ciWatcherFailures: number;
|
ciWatcherFailures: number;
|
||||||
restartRequests: number;
|
restartRequests: number;
|
||||||
|
roomSkillUpdates: number;
|
||||||
|
roomSkillsDisableCodexBrowser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriousImpacts = new Set(['serious', 'critical']);
|
const seriousImpacts = new Set(['serious', 'critical']);
|
||||||
@@ -27,7 +29,7 @@ async function main() {
|
|||||||
'settings nav keeps hash route stable',
|
'settings nav keeps hash route stable',
|
||||||
browser,
|
browser,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
async (page) => {
|
async (page, state) => {
|
||||||
await openSettings(page, baseUrl);
|
await openSettings(page, baseUrl);
|
||||||
|
|
||||||
const originalUrl = page.url();
|
const originalUrl = page.url();
|
||||||
@@ -52,6 +54,17 @@ async function main() {
|
|||||||
await assertVisible(page.locator('#settings-runtime'));
|
await assertVisible(page.locator('#settings-runtime'));
|
||||||
await page.getByText('Runtime inventory').waitFor();
|
await page.getByText('Runtime inventory').waitFor();
|
||||||
await page.getByText('EJClaw bridge').waitFor();
|
await page.getByText('EJClaw bridge').waitFor();
|
||||||
|
const codexPolicy = page
|
||||||
|
.locator('.runtime-room-agent-policy')
|
||||||
|
.filter({ hasText: 'Codex' })
|
||||||
|
.first();
|
||||||
|
const codexBrowserToggle = codexPolicy
|
||||||
|
.locator('input[type="checkbox"]')
|
||||||
|
.first();
|
||||||
|
assert.equal(await codexBrowserToggle.isChecked(), true);
|
||||||
|
await codexBrowserToggle.click();
|
||||||
|
await codexPolicy.getByText('1개 OFF').waitFor();
|
||||||
|
assert.equal(state.roomSkillUpdates, 1);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
@@ -225,6 +238,8 @@ function createMockApiState(): MockApiState {
|
|||||||
codexFeatureUpdates: 0,
|
codexFeatureUpdates: 0,
|
||||||
ciWatcherFailures: 0,
|
ciWatcherFailures: 0,
|
||||||
restartRequests: 0,
|
restartRequests: 0,
|
||||||
|
roomSkillUpdates: 0,
|
||||||
|
roomSkillsDisableCodexBrowser: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,8 +334,7 @@ async function handleMockApi(route: Route, state: MockApiState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'GET' && url.pathname === '/api/settings/room-skills') {
|
if (await handleMockRoomSkillRoute(route, state, url, method)) {
|
||||||
await fulfillJson(route, roomSkillSettingsFixture());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +433,33 @@ async function handleMockApi(route: Route, state: MockApiState) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleMockRoomSkillRoute(
|
||||||
|
route: Route,
|
||||||
|
state: MockApiState,
|
||||||
|
url: URL,
|
||||||
|
method: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (url.pathname !== '/api/settings/room-skills') return false;
|
||||||
|
if (method === 'GET') {
|
||||||
|
await fulfillJson(route, roomSkillSettingsFixture(state));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (method !== 'PATCH' && method !== 'PUT') return false;
|
||||||
|
|
||||||
|
const body = parseJsonBody(route.request().postData());
|
||||||
|
if (
|
||||||
|
body.roomJid === 'room@example' &&
|
||||||
|
body.agentType === 'codex' &&
|
||||||
|
body.skillId === 'codex-user:agent-browser' &&
|
||||||
|
body.enabled === false
|
||||||
|
) {
|
||||||
|
state.roomSkillUpdates += 1;
|
||||||
|
state.roomSkillsDisableCodexBrowser = true;
|
||||||
|
}
|
||||||
|
await fulfillJson(route, roomSkillSettingsFixture(state));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function mockOverview(state: MockApiState) {
|
function mockOverview(state: MockApiState) {
|
||||||
return {
|
return {
|
||||||
generatedAt: MOCK_TIME,
|
generatedAt: MOCK_TIME,
|
||||||
@@ -588,7 +629,8 @@ function runtimeInventoryFixture() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function roomSkillSettingsFixture() {
|
function roomSkillSettingsFixture(state?: MockApiState) {
|
||||||
|
const codexDisabled = state?.roomSkillsDisableCodexBrowser === true;
|
||||||
return {
|
return {
|
||||||
generatedAt: '2026-05-04T00:00:00.000Z',
|
generatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
catalog: [
|
catalog: [
|
||||||
@@ -629,17 +671,16 @@ function roomSkillSettingsFixture() {
|
|||||||
agents: [
|
agents: [
|
||||||
{
|
{
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
mode: 'all-enabled',
|
mode: codexDisabled ? 'custom' : 'all-enabled',
|
||||||
availableSkillIds: [
|
availableSkillIds: [
|
||||||
'codex-user:agent-browser',
|
'codex-user:agent-browser',
|
||||||
'runner:agent-browser',
|
'runner:agent-browser',
|
||||||
],
|
],
|
||||||
disabledSkillIds: [],
|
disabledSkillIds: codexDisabled ? ['codex-user:agent-browser'] : [],
|
||||||
explicitEnabledSkillIds: [],
|
explicitEnabledSkillIds: [],
|
||||||
effectiveEnabledSkillIds: [
|
effectiveEnabledSkillIds: codexDisabled
|
||||||
'codex-user:agent-browser',
|
? ['runner:agent-browser']
|
||||||
'runner:agent-browser',
|
: ['codex-user:agent-browser', 'runner:agent-browser'],
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export {
|
|||||||
createTask,
|
createTask,
|
||||||
deleteAllSessionsForGroup,
|
deleteAllSessionsForGroup,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
|
deleteStoredRoomSkillOverride,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
findDuplicateCiWatcher,
|
findDuplicateCiWatcher,
|
||||||
getAllRoomBindings,
|
getAllRoomBindings,
|
||||||
@@ -72,6 +73,7 @@ export {
|
|||||||
setRouterStateForService,
|
setRouterStateForService,
|
||||||
setSession,
|
setSession,
|
||||||
setSessionForAgentType,
|
setSessionForAgentType,
|
||||||
|
upsertStoredRoomSkillOverride,
|
||||||
updateRegisteredGroupName,
|
updateRegisteredGroupName,
|
||||||
updateTask,
|
updateTask,
|
||||||
updateTaskAfterRun,
|
updateTaskAfterRun,
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export interface StoredRoomSkillOverride {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StoredRoomSkillOverrideInput {
|
||||||
|
chatJid: string;
|
||||||
|
agentType: AgentType;
|
||||||
|
skillScope: string;
|
||||||
|
skillName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssignRoomInput {
|
export interface AssignRoomInput {
|
||||||
name: string;
|
name: string;
|
||||||
roomMode?: RoomMode;
|
roomMode?: RoomMode;
|
||||||
@@ -373,6 +381,56 @@ export function getStoredRoomSkillOverridesFromDatabase(
|
|||||||
.filter((row): row is StoredRoomSkillOverride => Boolean(row));
|
.filter((row): row is StoredRoomSkillOverride => Boolean(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function upsertStoredRoomSkillOverrideInDatabase(
|
||||||
|
database: Database,
|
||||||
|
input: StoredRoomSkillOverrideInput,
|
||||||
|
): void {
|
||||||
|
const agentType = normalizeStoredAgentType(input.agentType);
|
||||||
|
if (!agentType) {
|
||||||
|
throw new Error(`Unsupported agent type: ${input.agentType}`);
|
||||||
|
}
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
database
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO room_skill_overrides (
|
||||||
|
chat_jid, agent_type, skill_scope, skill_name, enabled,
|
||||||
|
created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(chat_jid, agent_type, skill_scope, skill_name)
|
||||||
|
DO UPDATE SET
|
||||||
|
enabled = excluded.enabled,
|
||||||
|
updated_at = excluded.updated_at`,
|
||||||
|
)
|
||||||
|
.run(
|
||||||
|
input.chatJid,
|
||||||
|
agentType,
|
||||||
|
input.skillScope,
|
||||||
|
input.skillName,
|
||||||
|
input.enabled ? 1 : 0,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteStoredRoomSkillOverrideFromDatabase(
|
||||||
|
database: Database,
|
||||||
|
input: Omit<StoredRoomSkillOverrideInput, 'enabled'>,
|
||||||
|
): void {
|
||||||
|
const agentType = normalizeStoredAgentType(input.agentType);
|
||||||
|
if (!agentType) {
|
||||||
|
throw new Error(`Unsupported agent type: ${input.agentType}`);
|
||||||
|
}
|
||||||
|
database
|
||||||
|
.prepare(
|
||||||
|
`DELETE FROM room_skill_overrides
|
||||||
|
WHERE chat_jid = ?
|
||||||
|
AND agent_type = ?
|
||||||
|
AND skill_scope = ?
|
||||||
|
AND skill_name = ?`,
|
||||||
|
)
|
||||||
|
.run(input.chatJid, agentType, input.skillScope, input.skillName);
|
||||||
|
}
|
||||||
|
|
||||||
function getStoredRoomModeRowFromDatabase(
|
function getStoredRoomModeRowFromDatabase(
|
||||||
database: Database,
|
database: Database,
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
assignRoomInDatabase,
|
assignRoomInDatabase,
|
||||||
clearExplicitRoomModeInDatabase,
|
clearExplicitRoomModeInDatabase,
|
||||||
deleteStoredRoomSettingsForTestsInDatabase,
|
deleteStoredRoomSettingsForTestsInDatabase,
|
||||||
|
deleteStoredRoomSkillOverrideFromDatabase,
|
||||||
getAllRoomBindingsFromDatabase,
|
getAllRoomBindingsFromDatabase,
|
||||||
getEffectiveRoomModeFromDatabase,
|
getEffectiveRoomModeFromDatabase,
|
||||||
getEffectiveRuntimeRoomModeFromDatabase,
|
getEffectiveRuntimeRoomModeFromDatabase,
|
||||||
@@ -41,7 +42,9 @@ import {
|
|||||||
setRegisteredGroupForTestsInDatabase,
|
setRegisteredGroupForTestsInDatabase,
|
||||||
setStoredRoomOwnerAgentTypeForTestsInDatabase,
|
setStoredRoomOwnerAgentTypeForTestsInDatabase,
|
||||||
type StoredRoomSkillOverride,
|
type StoredRoomSkillOverride,
|
||||||
|
type StoredRoomSkillOverrideInput,
|
||||||
updateRegisteredGroupNameInDatabase,
|
updateRegisteredGroupNameInDatabase,
|
||||||
|
upsertStoredRoomSkillOverrideInDatabase,
|
||||||
} from './rooms.js';
|
} from './rooms.js';
|
||||||
import { type StoredRoomSettings } from './room-registration.js';
|
import { type StoredRoomSettings } from './room-registration.js';
|
||||||
import {
|
import {
|
||||||
@@ -373,6 +376,18 @@ export function getStoredRoomSkillOverrides(
|
|||||||
return getStoredRoomSkillOverridesFromDatabase(db, chatJid);
|
return getStoredRoomSkillOverridesFromDatabase(db, chatJid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function upsertStoredRoomSkillOverride(
|
||||||
|
input: StoredRoomSkillOverrideInput,
|
||||||
|
): void {
|
||||||
|
upsertStoredRoomSkillOverrideInDatabase(requireDatabase(), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteStoredRoomSkillOverride(
|
||||||
|
input: Omit<StoredRoomSkillOverrideInput, 'enabled'>,
|
||||||
|
): void {
|
||||||
|
deleteStoredRoomSkillOverrideFromDatabase(requireDatabase(), input);
|
||||||
|
}
|
||||||
|
|
||||||
export function getExplicitRoomMode(chatJid: string): RoomMode | undefined {
|
export function getExplicitRoomMode(chatJid: string): RoomMode | undefined {
|
||||||
return getExplicitRoomModeFromDatabase(requireDatabase(), chatJid);
|
return getExplicitRoomModeFromDatabase(requireDatabase(), chatJid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { Database } from 'bun:sqlite';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { initializeDatabaseSchema } from './db/bootstrap.js';
|
import { initializeDatabaseSchema } from './db/bootstrap.js';
|
||||||
import { getStoredRoomSkillOverridesFromDatabase } from './db/rooms.js';
|
import {
|
||||||
|
deleteStoredRoomSkillOverrideFromDatabase,
|
||||||
|
getStoredRoomSkillOverridesFromDatabase,
|
||||||
|
upsertStoredRoomSkillOverrideInDatabase,
|
||||||
|
} from './db/rooms.js';
|
||||||
import {
|
import {
|
||||||
buildRoomSkillSettingsSnapshot,
|
buildRoomSkillSettingsSnapshot,
|
||||||
type RoomSkillSettingsBuildInput,
|
type RoomSkillSettingsBuildInput,
|
||||||
@@ -146,6 +150,39 @@ describe('room skill settings', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps default mode when explicit enable rows do not change effective skills', () => {
|
||||||
|
const snapshot = buildRoomSkillSettingsSnapshot({
|
||||||
|
generatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
inventory: inventoryFixture(),
|
||||||
|
roomBindings: {
|
||||||
|
'room-1': {
|
||||||
|
name: 'Room One',
|
||||||
|
folder: 'room-one',
|
||||||
|
added_at: '2026-05-04T00:00:00.000Z',
|
||||||
|
agentType: 'codex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registeredAgentTypesByJid: new Map([['room-1', ['codex']]]),
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
chatJid: 'room-1',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'runner-browser',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.rooms[0]?.agents[0]).toMatchObject({
|
||||||
|
mode: 'all-enabled',
|
||||||
|
explicitEnabledSkillIds: ['runner:runner-browser'],
|
||||||
|
effectiveEnabledSkillIds: ['codex-user:browser', 'runner:runner-browser'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('reads normalized room skill overrides from the database', () => {
|
it('reads normalized room skill overrides from the database', () => {
|
||||||
const database = new Database(':memory:');
|
const database = new Database(':memory:');
|
||||||
try {
|
try {
|
||||||
@@ -192,4 +229,58 @@ describe('room skill settings', () => {
|
|||||||
database.close();
|
database.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('upserts and deletes room skill overrides in the database', () => {
|
||||||
|
const database = new Database(':memory:');
|
||||||
|
try {
|
||||||
|
initializeDatabaseSchema(database);
|
||||||
|
database
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO room_settings (
|
||||||
|
chat_jid, room_mode, mode_source, name, folder, updated_at
|
||||||
|
) VALUES (?, 'single', 'explicit', ?, ?, ?)`,
|
||||||
|
)
|
||||||
|
.run('room-1', 'Room One', 'room-one', '2026-05-04T00:00:00.000Z');
|
||||||
|
|
||||||
|
upsertStoredRoomSkillOverrideInDatabase(database, {
|
||||||
|
chatJid: 'room-1',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'agent-browser',
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
expect(getStoredRoomSkillOverridesFromDatabase(database)).toMatchObject([
|
||||||
|
{
|
||||||
|
chatJid: 'room-1',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'agent-browser',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
upsertStoredRoomSkillOverrideInDatabase(database, {
|
||||||
|
chatJid: 'room-1',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'agent-browser',
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getStoredRoomSkillOverridesFromDatabase(database)[0],
|
||||||
|
).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteStoredRoomSkillOverrideFromDatabase(database, {
|
||||||
|
chatJid: 'room-1',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillScope: 'runner',
|
||||||
|
skillName: 'agent-browser',
|
||||||
|
});
|
||||||
|
expect(getStoredRoomSkillOverridesFromDatabase(database)).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
deleteStoredRoomSkillOverride,
|
||||||
getAllRoomBindings,
|
getAllRoomBindings,
|
||||||
getRegisteredAgentTypesForJid,
|
getRegisteredAgentTypesForJid,
|
||||||
getStoredRoomSkillOverrides,
|
getStoredRoomSkillOverrides,
|
||||||
|
upsertStoredRoomSkillOverride,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
import {
|
import {
|
||||||
getRuntimeInventory,
|
getRuntimeInventory,
|
||||||
@@ -57,6 +59,23 @@ export interface RoomSkillSettingsBuildInput {
|
|||||||
overrides?: StoredRoomSkillOverride[];
|
overrides?: StoredRoomSkillOverride[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoomSkillSettingUpdateInput {
|
||||||
|
roomJid: string;
|
||||||
|
agentType: string;
|
||||||
|
skillId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomSkillSettingsError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
readonly status: number,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'RoomSkillSettingsError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function skillId(scope: RoomSkillScope, name: string): string {
|
function skillId(scope: RoomSkillScope, name: string): string {
|
||||||
return `${scope}:${name}`;
|
return `${scope}:${name}`;
|
||||||
}
|
}
|
||||||
@@ -134,6 +153,26 @@ function overrideKey(
|
|||||||
return `${jid}\u0000${agentType}\u0000${scope}\u0000${name}`;
|
return `${jid}\u0000${agentType}\u0000${scope}\u0000${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSkillId(id: string): { scope: RoomSkillScope; name: string } {
|
||||||
|
const separator = id.indexOf(':');
|
||||||
|
if (separator <= 0 || separator === id.length - 1) {
|
||||||
|
throw new RoomSkillSettingsError('skillId must be "<scope>:<name>"', 400);
|
||||||
|
}
|
||||||
|
const scope = id.slice(0, separator);
|
||||||
|
const name = id.slice(separator + 1);
|
||||||
|
if (scope !== 'codex-user' && scope !== 'claude-user' && scope !== 'runner') {
|
||||||
|
throw new RoomSkillSettingsError(`Unsupported skill scope: ${scope}`, 400);
|
||||||
|
}
|
||||||
|
return { scope, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRoomSkillAgentType(agentType: string): AgentType {
|
||||||
|
if (agentType === 'codex' || agentType === 'claude-code') {
|
||||||
|
return agentType;
|
||||||
|
}
|
||||||
|
throw new RoomSkillSettingsError(`Unsupported agent type: ${agentType}`, 400);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRoomAgentTypes(
|
function resolveRoomAgentTypes(
|
||||||
jid: string,
|
jid: string,
|
||||||
group: RegisteredGroup,
|
group: RegisteredGroup,
|
||||||
@@ -202,7 +241,8 @@ export function buildRoomSkillSettingsSnapshot({
|
|||||||
return {
|
return {
|
||||||
agentType,
|
agentType,
|
||||||
mode:
|
mode:
|
||||||
disabledSkillIds.length > 0 || explicitEnabledSkillIds.length > 0
|
disabledSkillIds.length > 0 ||
|
||||||
|
effectiveEnabledSkillIds.length !== available.length
|
||||||
? 'custom'
|
? 'custom'
|
||||||
: 'all-enabled',
|
: 'all-enabled',
|
||||||
availableSkillIds: available.map((skill) => skill.id),
|
availableSkillIds: available.map((skill) => skill.id),
|
||||||
@@ -237,3 +277,53 @@ export function getRoomSkillSettings(): RoomSkillSettingsSnapshot {
|
|||||||
overrides: getStoredRoomSkillOverrides(),
|
overrides: getStoredRoomSkillOverrides(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateRoomSkillSetting(
|
||||||
|
input: RoomSkillSettingUpdateInput,
|
||||||
|
): RoomSkillSettingsSnapshot {
|
||||||
|
const roomJid = input.roomJid.trim();
|
||||||
|
const agentType = normalizeRoomSkillAgentType(input.agentType);
|
||||||
|
const { scope, name } = parseSkillId(input.skillId.trim());
|
||||||
|
const before = getRoomSkillSettings();
|
||||||
|
const room = before.rooms.find((candidate) => candidate.jid === roomJid);
|
||||||
|
if (!room) {
|
||||||
|
throw new RoomSkillSettingsError('Room not found', 404);
|
||||||
|
}
|
||||||
|
const agent = room.agents.find(
|
||||||
|
(candidate) => candidate.agentType === agentType,
|
||||||
|
);
|
||||||
|
if (!agent) {
|
||||||
|
throw new RoomSkillSettingsError(
|
||||||
|
'Agent is not registered for this room',
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const skill = before.catalog.find(
|
||||||
|
(candidate) => candidate.scope === scope && candidate.name === name,
|
||||||
|
);
|
||||||
|
if (!skill || !agent.availableSkillIds.includes(skill.id)) {
|
||||||
|
throw new RoomSkillSettingsError(
|
||||||
|
'Skill is not available for this room agent',
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.enabled) {
|
||||||
|
deleteStoredRoomSkillOverride({
|
||||||
|
chatJid: roomJid,
|
||||||
|
agentType,
|
||||||
|
skillScope: scope,
|
||||||
|
skillName: name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
upsertStoredRoomSkillOverride({
|
||||||
|
chatJid: roomJid,
|
||||||
|
agentType,
|
||||||
|
skillScope: scope,
|
||||||
|
skillName: name,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRoomSkillSettings();
|
||||||
|
}
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ function makeDeps(
|
|||||||
updateFastMode: () => fastMode,
|
updateFastMode: () => fastMode,
|
||||||
updateModelConfig: () => modelConfig,
|
updateModelConfig: () => modelConfig,
|
||||||
updateMoaSettings: () => moaSettings,
|
updateMoaSettings: () => moaSettings,
|
||||||
|
updateRoomSkillSetting: () => roomSkillSettings,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -336,6 +337,25 @@ describe('web dashboard settings routes', () => {
|
|||||||
calls.push(`moa:${JSON.stringify(input)}`);
|
calls.push(`moa:${JSON.stringify(input)}`);
|
||||||
return moaSettings;
|
return moaSettings;
|
||||||
},
|
},
|
||||||
|
updateRoomSkillSetting: (input) => {
|
||||||
|
calls.push(`room-skill:${JSON.stringify(input)}`);
|
||||||
|
return {
|
||||||
|
...roomSkillSettings,
|
||||||
|
rooms: [
|
||||||
|
{
|
||||||
|
...roomSkillSettings.rooms[0]!,
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
...roomSkillSettings.rooms[0]!.agents[0]!,
|
||||||
|
mode: 'custom',
|
||||||
|
disabledSkillIds: [input.skillId],
|
||||||
|
effectiveEnabledSkillIds: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelUpdate = await route(
|
const modelUpdate = await route(
|
||||||
@@ -404,6 +424,31 @@ describe('web dashboard settings routes', () => {
|
|||||||
status: { model: 'glm', ok: true },
|
status: { model: 'glm', ok: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const roomSkill = await route(
|
||||||
|
'/api/settings/room-skills',
|
||||||
|
'PATCH',
|
||||||
|
{
|
||||||
|
roomJid: 'room@example',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillId: 'runner:agent-browser',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(roomSkill?.status).toBe(200);
|
||||||
|
await expect(roomSkill?.json()).resolves.toMatchObject({
|
||||||
|
rooms: [
|
||||||
|
{
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
mode: 'custom',
|
||||||
|
disabledSkillIds: ['runner:agent-browser'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
'models:{"owner":{"model":"gpt-5.5"}}',
|
'models:{"owner":{"model":"gpt-5.5"}}',
|
||||||
'add:claude-token',
|
'add:claude-token',
|
||||||
@@ -411,6 +456,42 @@ describe('web dashboard settings routes', () => {
|
|||||||
'current:4',
|
'current:4',
|
||||||
'codex-features:{"goals":true}',
|
'codex-features:{"goals":true}',
|
||||||
'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}',
|
'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}',
|
||||||
|
'room-skill:{"roomJid":"room@example","agentType":"codex","skillId":"runner:agent-browser","enabled":false}',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects unsupported room skill methods with 405', async () => {
|
||||||
|
const response = await route('/api/settings/room-skills', 'DELETE');
|
||||||
|
expect(response?.status).toBe(405);
|
||||||
|
expect(response?.headers.get('allow')).toBe('GET, HEAD, PATCH, PUT');
|
||||||
|
await expect(response?.json()).resolves.toEqual({
|
||||||
|
error: 'Method not allowed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates room skill mutation payloads', async () => {
|
||||||
|
const missingEnabled = await route('/api/settings/room-skills', 'PATCH', {
|
||||||
|
roomJid: 'room@example',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillId: 'runner:agent-browser',
|
||||||
|
});
|
||||||
|
expect(missingEnabled?.status).toBe(400);
|
||||||
|
|
||||||
|
const invalid = await route(
|
||||||
|
'/api/settings/room-skills',
|
||||||
|
'PATCH',
|
||||||
|
{
|
||||||
|
roomJid: 'room@example',
|
||||||
|
agentType: 'codex',
|
||||||
|
skillId: 'runner:agent-browser',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
makeDeps({
|
||||||
|
updateRoomSkillSetting: () => {
|
||||||
|
throw new Error('boom');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(invalid?.status).toBe(500);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import {
|
|||||||
} from './runtime-inventory.js';
|
} from './runtime-inventory.js';
|
||||||
import {
|
import {
|
||||||
getRoomSkillSettings,
|
getRoomSkillSettings,
|
||||||
|
RoomSkillSettingsError,
|
||||||
|
updateRoomSkillSetting,
|
||||||
type RoomSkillSettingsSnapshot,
|
type RoomSkillSettingsSnapshot,
|
||||||
|
type RoomSkillSettingUpdateInput,
|
||||||
} from './room-skill-settings.js';
|
} from './room-skill-settings.js';
|
||||||
import {
|
import {
|
||||||
checkMoaModel,
|
checkMoaModel,
|
||||||
@@ -60,6 +63,7 @@ export interface SettingsRouteDependencies {
|
|||||||
updateFastMode: typeof updateFastMode;
|
updateFastMode: typeof updateFastMode;
|
||||||
updateModelConfig: typeof updateModelConfig;
|
updateModelConfig: typeof updateModelConfig;
|
||||||
updateMoaSettings: typeof updateMoaSettings;
|
updateMoaSettings: typeof updateMoaSettings;
|
||||||
|
updateRoomSkillSetting: typeof updateRoomSkillSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsRouteContext {
|
interface SettingsRouteContext {
|
||||||
@@ -89,6 +93,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
|
|||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModelConfig,
|
updateModelConfig,
|
||||||
updateMoaSettings,
|
updateMoaSettings,
|
||||||
|
updateRoomSkillSetting,
|
||||||
};
|
};
|
||||||
|
|
||||||
function readMethod(method: string): boolean {
|
function readMethod(method: string): boolean {
|
||||||
@@ -118,6 +123,19 @@ function errorMessage(err: unknown): string {
|
|||||||
return err instanceof Error ? err.message : String(err);
|
return err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function methodNotAllowed(
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
allowed: string[],
|
||||||
|
): Response {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: 'Method not allowed' },
|
||||||
|
{
|
||||||
|
status: 405,
|
||||||
|
headers: { Allow: allowed.join(', ') },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleModelSettingsRoute(
|
async function handleModelSettingsRoute(
|
||||||
request: Request,
|
request: Request,
|
||||||
jsonResponse: JsonResponse,
|
jsonResponse: JsonResponse,
|
||||||
@@ -228,13 +246,42 @@ function handleRuntimeInventoryRoute(
|
|||||||
return jsonResponse(deps.getRuntimeInventory());
|
return jsonResponse(deps.getRuntimeInventory());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRoomSkillsRoute(
|
async function handleRoomSkillsRoute(
|
||||||
request: Request,
|
request: Request,
|
||||||
jsonResponse: JsonResponse,
|
jsonResponse: JsonResponse,
|
||||||
deps: SettingsRouteDependencies,
|
deps: SettingsRouteDependencies,
|
||||||
): Response | null {
|
): Promise<Response> {
|
||||||
if (!readMethod(request.method)) return null;
|
if (readMethod(request.method)) {
|
||||||
return jsonResponse(deps.getRoomSkillSettings());
|
return jsonResponse(deps.getRoomSkillSettings());
|
||||||
|
}
|
||||||
|
if (request.method !== 'PATCH' && request.method !== 'PUT') {
|
||||||
|
return methodNotAllowed(jsonResponse, ['GET', 'HEAD', 'PATCH', 'PUT']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readJsonObject(request, jsonResponse);
|
||||||
|
if (body instanceof Response) return body;
|
||||||
|
|
||||||
|
const input: RoomSkillSettingUpdateInput = {
|
||||||
|
roomJid: typeof body.roomJid === 'string' ? body.roomJid : '',
|
||||||
|
agentType: typeof body.agentType === 'string' ? body.agentType : '',
|
||||||
|
skillId: typeof body.skillId === 'string' ? body.skillId : '',
|
||||||
|
enabled: body.enabled === true,
|
||||||
|
};
|
||||||
|
if (typeof body.enabled !== 'boolean') {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: 'enabled must be a boolean' },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return jsonResponse(deps.updateRoomSkillSetting(input));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof RoomSkillSettingsError) {
|
||||||
|
return jsonResponse({ error: err.message }, { status: err.status });
|
||||||
|
}
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClaudeAccountAddRoute(
|
async function handleClaudeAccountAddRoute(
|
||||||
|
|||||||
Reference in New Issue
Block a user