mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
refactor: separate workshop access and revision sessions
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import type { ApplicationGatewaySnapshot } from "../../app/context.ts";
|
||||
import { canCallGatewayMethod } from "../../lib/gateway-methods.ts";
|
||||
|
||||
export type SkillWorkshopAccess = {
|
||||
canEvaluate: boolean;
|
||||
canApply: boolean;
|
||||
canRevise: boolean;
|
||||
canReject: boolean;
|
||||
canScanHistory: boolean;
|
||||
};
|
||||
|
||||
type SkillWorkshopAdminMethod =
|
||||
| "config.patch"
|
||||
| "skills.proposals.apply"
|
||||
| "skills.proposals.evaluate"
|
||||
| "skills.proposals.historyScan"
|
||||
| "skills.proposals.reject"
|
||||
| "skills.proposals.requestRevision";
|
||||
|
||||
export function canCallWorkshopAdminMethod(
|
||||
snapshot: ApplicationGatewaySnapshot | null | undefined,
|
||||
method: SkillWorkshopAdminMethod,
|
||||
): boolean {
|
||||
return canCallGatewayMethod(snapshot, method, "operator.admin");
|
||||
}
|
||||
|
||||
export function resolveWorkshopAccess(snapshot: ApplicationGatewaySnapshot): SkillWorkshopAccess {
|
||||
return {
|
||||
canEvaluate: canCallWorkshopAdminMethod(snapshot, "skills.proposals.evaluate"),
|
||||
canApply: canCallWorkshopAdminMethod(snapshot, "skills.proposals.apply"),
|
||||
canRevise: canCallWorkshopAdminMethod(snapshot, "skills.proposals.requestRevision"),
|
||||
canReject: canCallWorkshopAdminMethod(snapshot, "skills.proposals.reject"),
|
||||
canScanHistory: canCallWorkshopAdminMethod(snapshot, "skills.proposals.historyScan"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts";
|
||||
import { loadSettings } from "../../app/settings.ts";
|
||||
import { readSessionMethodAccess } from "../../lib/session-method-access.ts";
|
||||
import { resolveSessionKey } from "../../lib/sessions/index.ts";
|
||||
import { normalizeAgentId } from "../../lib/sessions/session-key.ts";
|
||||
import type { SkillWorkshopProposal } from "./page-types.ts";
|
||||
import type { SkillWorkshopState } from "./proposals.ts";
|
||||
import type { SkillWorkshopPageContext } from "./source-scope.ts";
|
||||
|
||||
function findRevisionSessionRow(
|
||||
result: SessionsListResult | null,
|
||||
sessionKey: string | undefined,
|
||||
): GatewaySessionRow | null {
|
||||
const key = sessionKey?.trim();
|
||||
return key ? (result?.sessions.find((row) => row.key === key) ?? null) : null;
|
||||
}
|
||||
|
||||
function isUsableRevisionSession(row: GatewaySessionRow | null): row is GatewaySessionRow {
|
||||
return Boolean(row && !row.archived && !row.hasActiveRun);
|
||||
}
|
||||
|
||||
async function loadRevisionSessionsForAgent(
|
||||
context: SkillWorkshopPageContext,
|
||||
agentId: string,
|
||||
): Promise<SessionsListResult | null> {
|
||||
const current = context.sessions.state;
|
||||
if (current.agentId === agentId && current.result?.sessions.length) {
|
||||
return current.result;
|
||||
}
|
||||
return context.sessions.list({ agentId });
|
||||
}
|
||||
|
||||
export async function resolveSkillWorkshopRevisionSessionKey(
|
||||
state: SkillWorkshopState,
|
||||
context: SkillWorkshopPageContext,
|
||||
proposal: SkillWorkshopProposal,
|
||||
proposalAgentId: string,
|
||||
isCurrent: () => boolean,
|
||||
): Promise<string | null> {
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const gatewayHello = context.gateway.snapshot.hello;
|
||||
if (state.skillWorkshopUseCurrentChatForRevisions) {
|
||||
return resolveSessionKey(loadSettings().sessionKey, gatewayHello).trim() || null;
|
||||
}
|
||||
|
||||
const agentId = normalizeAgentId(proposal.origin?.agentId ?? proposalAgentId);
|
||||
const sessions = await loadRevisionSessionsForAgent(context, agentId);
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const originRow = findRevisionSessionRow(sessions, proposal.origin?.sessionKey);
|
||||
if (isUsableRevisionSession(originRow)) {
|
||||
return originRow.key;
|
||||
}
|
||||
|
||||
const createParams = {
|
||||
agentId,
|
||||
label: truncateUtf16Safe(`Skill Workshop: ${proposal.slug || proposal.key}`, 80),
|
||||
};
|
||||
const createAccess = readSessionMethodAccess(context.gateway.snapshot, {
|
||||
method: "sessions.create",
|
||||
params: createParams,
|
||||
});
|
||||
if (!createAccess.allowed) {
|
||||
throw new Error(createAccess.reason);
|
||||
}
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const createdKey = await context.sessions.create(createParams);
|
||||
const sessionKey = resolveSessionKey(createdKey, gatewayHello).trim();
|
||||
if (!sessionKey) {
|
||||
throw new Error(context.sessions.state.error ?? "Could not prepare a Skill Workshop thread.");
|
||||
}
|
||||
return sessionKey;
|
||||
}
|
||||
@@ -1,33 +1,24 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState, Task } from "@lit/task";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { html, nothing } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts";
|
||||
import { applicationContext, type ApplicationGatewaySnapshot } from "../../app/context.ts";
|
||||
import { loadSettings } from "../../app/settings.ts";
|
||||
import { renderHubTabs } from "../../components/hub-tabs.ts";
|
||||
import "../../components/tooltip.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { canCallGatewayMethod } from "../../lib/gateway-methods.ts";
|
||||
import { readSessionMethodAccess } from "../../lib/session-method-access.ts";
|
||||
import { resolveSessionKey } from "../../lib/sessions/index.ts";
|
||||
import { sessionNavigationTarget } from "../../lib/sessions/route-navigation.ts";
|
||||
import { normalizeAgentId } from "../../lib/sessions/session-key.ts";
|
||||
import { filterSkillWorkshopProposals } from "../../lib/skill-workshop/index.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import { PLUGINS_HUB_PANEL_ID, pluginsHubTabs } from "../plugins/plugins-hub.ts";
|
||||
import { canCallWorkshopAdminMethod, resolveWorkshopAccess } from "./access.ts";
|
||||
import { renderSkillWorkshopHeaderControls, setSkillWorkshopMode } from "./header-controls.ts";
|
||||
import {
|
||||
loadSkillWorkshopPageData,
|
||||
runSkillWorkshopPageHistoryScan,
|
||||
} from "./history-scan-page-controller.ts";
|
||||
import type {
|
||||
SkillWorkshopProposal,
|
||||
SkillWorkshopRenderContext,
|
||||
SkillWorkshopRevisionRequest,
|
||||
} from "./page-types.ts";
|
||||
import type { SkillWorkshopRenderContext, SkillWorkshopRevisionRequest } from "./page-types.ts";
|
||||
import { selectPluginsHubTab } from "./plugins-hub-navigation.ts";
|
||||
import {
|
||||
countSkillWorkshopProposals,
|
||||
@@ -39,6 +30,7 @@ import {
|
||||
type SkillWorkshopRouteData,
|
||||
type SkillWorkshopState,
|
||||
} from "./proposals.ts";
|
||||
import { resolveSkillWorkshopRevisionSessionKey } from "./revision-session.ts";
|
||||
import { resolveSelfLearning, setSelfLearningEnabled } from "./self-learning.ts";
|
||||
import {
|
||||
captureSkillWorkshopSourceScope,
|
||||
@@ -49,76 +41,6 @@ import {
|
||||
import { loadSkillWorkshopMode, loadSkillWorkshopUseCurrentChatForRevisions } from "./storage.ts";
|
||||
import { renderSkillWorkshop } from "./view.ts";
|
||||
|
||||
function findRevisionSessionRow(
|
||||
result: SessionsListResult | null,
|
||||
sessionKey: string | undefined,
|
||||
): GatewaySessionRow | null {
|
||||
const key = sessionKey?.trim();
|
||||
return key ? (result?.sessions.find((row) => row.key === key) ?? null) : null;
|
||||
}
|
||||
|
||||
function isUsableRevisionSession(row: GatewaySessionRow | null): row is GatewaySessionRow {
|
||||
return Boolean(row && !row.archived && !row.hasActiveRun);
|
||||
}
|
||||
|
||||
async function loadRevisionSessionsForAgent(
|
||||
context: SkillWorkshopPageContext,
|
||||
agentId: string,
|
||||
): Promise<SessionsListResult | null> {
|
||||
const current = context.sessions.state;
|
||||
if (current.agentId === agentId && current.result?.sessions.length) {
|
||||
return current.result;
|
||||
}
|
||||
return context.sessions.list({ agentId });
|
||||
}
|
||||
|
||||
async function resolveRevisionSessionKey(
|
||||
state: SkillWorkshopState,
|
||||
context: SkillWorkshopPageContext,
|
||||
proposal: SkillWorkshopProposal,
|
||||
proposalAgentId: string,
|
||||
isCurrent: () => boolean,
|
||||
): Promise<string | null> {
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const gatewayHello = context.gateway.snapshot.hello;
|
||||
if (state.skillWorkshopUseCurrentChatForRevisions) {
|
||||
return resolveSessionKey(loadSettings().sessionKey, gatewayHello).trim() || null;
|
||||
}
|
||||
|
||||
const agentId = normalizeAgentId(proposal.origin?.agentId ?? proposalAgentId);
|
||||
const sessions = await loadRevisionSessionsForAgent(context, agentId);
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const originRow = findRevisionSessionRow(sessions, proposal.origin?.sessionKey);
|
||||
if (isUsableRevisionSession(originRow)) {
|
||||
return originRow.key;
|
||||
}
|
||||
|
||||
const createParams = {
|
||||
agentId,
|
||||
label: truncateUtf16Safe(`Skill Workshop: ${proposal.slug || proposal.key}`, 80),
|
||||
};
|
||||
const createAccess = readSessionMethodAccess(context.gateway.snapshot, {
|
||||
method: "sessions.create",
|
||||
params: createParams,
|
||||
});
|
||||
if (!createAccess.allowed) {
|
||||
throw new Error(createAccess.reason);
|
||||
}
|
||||
if (!isCurrent()) {
|
||||
return null;
|
||||
}
|
||||
const createdKey = await context.sessions.create(createParams);
|
||||
const sessionKey = resolveSessionKey(createdKey, gatewayHello).trim();
|
||||
if (!sessionKey) {
|
||||
throw new Error(context.sessions.state.error ?? "Could not prepare a Skill Workshop thread.");
|
||||
}
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
function renderSkillWorkshopPage(
|
||||
state: SkillWorkshopState,
|
||||
renderContext: SkillWorkshopRenderContext,
|
||||
@@ -137,15 +59,7 @@ function renderSkillWorkshopPage(
|
||||
state.skillWorkshopMode === "today"
|
||||
? "content--skill-workshop content--skill-workshop-today"
|
||||
: "content--skill-workshop";
|
||||
const canCall = (method: string) =>
|
||||
canCallGatewayMethod(context.gateway.snapshot, method, "operator.admin");
|
||||
const access = {
|
||||
canEvaluate: canCall("skills.proposals.evaluate"),
|
||||
canApply: canCall("skills.proposals.apply"),
|
||||
canRevise: canCall("skills.proposals.requestRevision"),
|
||||
canReject: canCall("skills.proposals.reject"),
|
||||
canScanHistory: canCall("skills.proposals.historyScan"),
|
||||
};
|
||||
const access = resolveWorkshopAccess(context.gateway.snapshot);
|
||||
|
||||
return html`
|
||||
<section class=${pageClass}>
|
||||
@@ -280,7 +194,9 @@ function renderSkillWorkshopPage(
|
||||
onPrev: () => selectRelativeProposal(-1),
|
||||
onNext: () => selectRelativeProposal(1),
|
||||
onApply: (key) => {
|
||||
if (!canCall("skills.proposals.apply")) {
|
||||
if (
|
||||
!canCallWorkshopAdminMethod(context.gateway.snapshot, "skills.proposals.apply")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void runSkillWorkshopLifecycleAction(state, context, "apply", key).finally(
|
||||
@@ -289,14 +205,21 @@ function renderSkillWorkshopPage(
|
||||
requestUpdate();
|
||||
},
|
||||
onEvaluate: (key) => {
|
||||
if (!canCall("skills.proposals.evaluate")) {
|
||||
if (
|
||||
!canCallWorkshopAdminMethod(context.gateway.snapshot, "skills.proposals.evaluate")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void runSkillWorkshopEvaluation(state, context, key).finally(requestUpdate);
|
||||
requestUpdate();
|
||||
},
|
||||
onRevise: (key) => {
|
||||
if (!canCall("skills.proposals.requestRevision")) {
|
||||
if (
|
||||
!canCallWorkshopAdminMethod(
|
||||
context.gateway.snapshot,
|
||||
"skills.proposals.requestRevision",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
state.skillWorkshopRevisionKey = key;
|
||||
@@ -304,7 +227,9 @@ function renderSkillWorkshopPage(
|
||||
requestUpdate();
|
||||
},
|
||||
onReject: (key) => {
|
||||
if (!canCall("skills.proposals.reject")) {
|
||||
if (
|
||||
!canCallWorkshopAdminMethod(context.gateway.snapshot, "skills.proposals.reject")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void runSkillWorkshopLifecycleAction(state, context, "reject", key).finally(
|
||||
@@ -322,7 +247,11 @@ function renderSkillWorkshopPage(
|
||||
requestUpdate();
|
||||
},
|
||||
onRevisionSubmit: (key) =>
|
||||
onRevisionRequest && canCall("skills.proposals.requestRevision")
|
||||
onRevisionRequest &&
|
||||
canCallWorkshopAdminMethod(
|
||||
context.gateway.snapshot,
|
||||
"skills.proposals.requestRevision",
|
||||
)
|
||||
? void requestSkillWorkshopRevision(
|
||||
state,
|
||||
context,
|
||||
@@ -507,7 +436,7 @@ class SkillWorkshopPage extends OpenClawLightDomElement {
|
||||
}
|
||||
let sessionKey: string | null;
|
||||
try {
|
||||
sessionKey = await resolveRevisionSessionKey(
|
||||
sessionKey = await resolveSkillWorkshopRevisionSessionKey(
|
||||
scope.state,
|
||||
scope.context,
|
||||
proposal,
|
||||
@@ -656,11 +585,7 @@ class SkillWorkshopPage extends OpenClawLightDomElement {
|
||||
|
||||
private readonly handleHistoryScan = () => {
|
||||
if (
|
||||
!canCallGatewayMethod(
|
||||
this.context?.gateway?.snapshot,
|
||||
"skills.proposals.historyScan",
|
||||
"operator.admin",
|
||||
)
|
||||
!canCallWorkshopAdminMethod(this.context?.gateway?.snapshot, "skills.proposals.historyScan")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -685,7 +610,7 @@ class SkillWorkshopPage extends OpenClawLightDomElement {
|
||||
};
|
||||
|
||||
private async applySelfLearningToggle(enabled: boolean): Promise<void> {
|
||||
if (!canCallGatewayMethod(this.context?.gateway?.snapshot, "config.patch", "operator.admin")) {
|
||||
if (!canCallWorkshopAdminMethod(this.context?.gateway?.snapshot, "config.patch")) {
|
||||
return;
|
||||
}
|
||||
const runtimeConfig = this.context?.runtimeConfig;
|
||||
@@ -731,7 +656,7 @@ class SkillWorkshopPage extends OpenClawLightDomElement {
|
||||
this.context.runtimeConfig,
|
||||
this.selfLearningBusy,
|
||||
this.selfLearningError,
|
||||
canCallGatewayMethod(this.context.gateway.snapshot, "config.patch", "operator.admin"),
|
||||
canCallWorkshopAdminMethod(this.context.gateway.snapshot, "config.patch"),
|
||||
),
|
||||
onSelfLearningToggle: this.handleSelfLearningToggle,
|
||||
onHistoryScan: this.handleHistoryScan,
|
||||
|
||||
@@ -5,17 +5,12 @@ import type {
|
||||
SkillWorkshopProposal,
|
||||
SkillWorkshopStatusFilter,
|
||||
} from "../../lib/skill-workshop/index.ts";
|
||||
import type { SkillWorkshopAccess } from "./access.ts";
|
||||
import type { SkillWorkshopSelfLearning } from "./self-learning.ts";
|
||||
import type { SkillWorkshopHistoryScanState } from "./state.ts";
|
||||
|
||||
export type SkillWorkshopProps = {
|
||||
access: {
|
||||
canEvaluate: boolean;
|
||||
canApply: boolean;
|
||||
canRevise: boolean;
|
||||
canReject: boolean;
|
||||
canScanHistory: boolean;
|
||||
};
|
||||
access: SkillWorkshopAccess;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
inspectingKey: string | null;
|
||||
|
||||
Reference in New Issue
Block a user