update: auto-reply
This commit is contained in:
parent
f285429952
commit
e0541f772e
@ -358,6 +358,7 @@
|
||||
"@mariozechner/pi-tui": "0.57.1",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@pierre/diffs": "1.1.0",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"@slack/bolt": "^4.6.0",
|
||||
"@slack/web-api": "^7.15.0",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -79,6 +79,9 @@ importers:
|
||||
'@napi-rs/canvas':
|
||||
specifier: ^0.1.89
|
||||
version: 0.1.95
|
||||
'@pierre/diffs':
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@sinclair/typebox':
|
||||
specifier: 0.34.48
|
||||
version: 0.34.48
|
||||
|
||||
@ -150,22 +150,23 @@ export async function getReplyFromConfig(
|
||||
typeof btwQuestion === "string" &&
|
||||
shouldHandleTextCommands({
|
||||
cfg,
|
||||
surface: finalized.Surface,
|
||||
surface: finalized.Surface ?? "",
|
||||
commandSource: finalized.CommandSource,
|
||||
});
|
||||
const useBtwSideTurn = allowBtwSideTurn && typeof btwQuestion === "string";
|
||||
if (useBtwSideTurn && !commandAuth.isAuthorizedSender) {
|
||||
return undefined;
|
||||
}
|
||||
if (useBtwSideTurn && btwQuestion.length === 0) {
|
||||
return { text: "⚙️ Usage: /btw <question>" };
|
||||
}
|
||||
if (useBtwSideTurn) {
|
||||
finalized.Body = btwQuestion;
|
||||
finalized.BodyForAgent = btwQuestion;
|
||||
finalized.RawBody = btwQuestion;
|
||||
finalized.CommandBody = btwQuestion;
|
||||
finalized.BodyForCommands = btwQuestion;
|
||||
const btwQuestionText = btwQuestion ?? "";
|
||||
if (btwQuestionText.length === 0) {
|
||||
return { text: "⚙️ Usage: /btw <question>" };
|
||||
}
|
||||
finalized.Body = btwQuestionText;
|
||||
finalized.BodyForAgent = btwQuestionText;
|
||||
finalized.RawBody = btwQuestionText;
|
||||
finalized.CommandBody = btwQuestionText;
|
||||
finalized.BodyForCommands = btwQuestionText;
|
||||
}
|
||||
|
||||
if (!isFastTestEnv) {
|
||||
|
||||
@ -20,6 +20,7 @@ import { logsHandlers } from "./server-methods/logs.js";
|
||||
import { modelsHandlers } from "./server-methods/models.js";
|
||||
import { nodePendingHandlers } from "./server-methods/nodes-pending.js";
|
||||
import { nodeHandlers } from "./server-methods/nodes.js";
|
||||
import { ptyHandlers } from "./server-methods/pty.js";
|
||||
import { pushHandlers } from "./server-methods/push.js";
|
||||
import { sendHandlers } from "./server-methods/send.js";
|
||||
import { sessionsHandlers } from "./server-methods/sessions.js";
|
||||
|
||||
@ -9,6 +9,7 @@ type GatewayClientMock = {
|
||||
start: ReturnType<typeof vi.fn>;
|
||||
stop: ReturnType<typeof vi.fn>;
|
||||
options: { clientVersion?: string };
|
||||
emitHello: (hello: Record<string, unknown>) => void;
|
||||
emitClose: (info: {
|
||||
code: number;
|
||||
reason?: string;
|
||||
@ -39,6 +40,7 @@ vi.mock("./gateway.ts", () => {
|
||||
constructor(
|
||||
private opts: {
|
||||
clientVersion?: string;
|
||||
onHello?: (hello: Record<string, unknown>) => void;
|
||||
onClose?: (info: {
|
||||
code: number;
|
||||
reason: string;
|
||||
@ -52,6 +54,9 @@ vi.mock("./gateway.ts", () => {
|
||||
start: this.start,
|
||||
stop: this.stop,
|
||||
options: { clientVersion: this.opts.clientVersion },
|
||||
emitHello: (hello) => {
|
||||
this.opts.onHello?.(hello as never);
|
||||
},
|
||||
emitClose: (info) => {
|
||||
this.opts.onClose?.({
|
||||
code: info.code,
|
||||
@ -158,6 +163,31 @@ describe("connectGateway", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the main session when persisted session keys point at cron chats", () => {
|
||||
const host = createHost();
|
||||
host.sessionKey = "agent:main:cron:nightly-brief";
|
||||
host.settings.sessionKey = "agent:main:cron:nightly-brief";
|
||||
host.settings.lastActiveSessionKey = "cron:nightly-brief";
|
||||
|
||||
connectGateway(host);
|
||||
const client = gatewayClientInstances[0];
|
||||
expect(client).toBeDefined();
|
||||
|
||||
client.emitHello({
|
||||
snapshot: {
|
||||
sessionDefaults: {
|
||||
mainSessionKey: "agent:main:main",
|
||||
mainKey: "main",
|
||||
defaultAgentId: "main",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(host.sessionKey).toBe("agent:main:main");
|
||||
expect(host.settings.sessionKey).toBe("agent:main:main");
|
||||
expect(host.settings.lastActiveSessionKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("ignores stale client onEvent callbacks after reconnect", () => {
|
||||
const host = createHost();
|
||||
|
||||
|
||||
@ -118,6 +118,24 @@ export function resolveControlUiClientVersion(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function isCronSessionKey(value: string | undefined): boolean {
|
||||
const normalized = (value ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized.startsWith("cron:")) {
|
||||
return true;
|
||||
}
|
||||
if (!normalized.startsWith("agent:")) {
|
||||
return false;
|
||||
}
|
||||
const parts = normalized.split(":").filter(Boolean);
|
||||
if (parts.length < 3) {
|
||||
return false;
|
||||
}
|
||||
return parts.slice(2).join(":").startsWith("cron:");
|
||||
}
|
||||
|
||||
function normalizeSessionKeyForDefaults(
|
||||
value: string | undefined,
|
||||
defaults: SessionDefaultsSnapshot,
|
||||
@ -130,6 +148,9 @@ function normalizeSessionKeyForDefaults(
|
||||
if (!raw) {
|
||||
return mainSessionKey;
|
||||
}
|
||||
if (isCronSessionKey(raw)) {
|
||||
return mainSessionKey;
|
||||
}
|
||||
const mainKey = defaults.mainKey?.trim() || "main";
|
||||
const defaultAgentId = defaults.defaultAgentId?.trim();
|
||||
const isAlias =
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const KEY = "openclaw.control.settings.v1";
|
||||
const LEGACY_TOKEN_SESSION_KEY = "openclaw.control.token.v1";
|
||||
const TOKEN_SESSION_KEY_PREFIX = "openclaw.control.token.v1:";
|
||||
const TOKEN_LOCAL_KEY_PREFIX = "openclaw.control.token.persisted.v1:";
|
||||
|
||||
type PersistedUiSettings = Omit<UiSettings, "token"> & { token?: never };
|
||||
|
||||
@ -11,6 +12,7 @@ import { parseThemeSelection, type ThemeMode, type ThemeName } from "./theme.ts"
|
||||
export type UiSettings = {
|
||||
gatewayUrl: string;
|
||||
token: string;
|
||||
rememberGatewayAuth: boolean;
|
||||
sessionKey: string;
|
||||
lastActiveSessionKey: string;
|
||||
theme: ThemeName;
|
||||
@ -86,6 +88,10 @@ function tokenSessionKeyForGateway(gatewayUrl: string): string {
|
||||
return `${TOKEN_SESSION_KEY_PREFIX}${normalizeGatewayTokenScope(gatewayUrl)}`;
|
||||
}
|
||||
|
||||
function tokenLocalKeyForGateway(gatewayUrl: string): string {
|
||||
return `${TOKEN_LOCAL_KEY_PREFIX}${normalizeGatewayTokenScope(gatewayUrl)}`;
|
||||
}
|
||||
|
||||
function loadSessionToken(gatewayUrl: string): string {
|
||||
try {
|
||||
const storage = getSessionStorage();
|
||||
@ -119,12 +125,37 @@ function persistSessionToken(gatewayUrl: string, token: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function loadRememberedToken(gatewayUrl: string): string {
|
||||
try {
|
||||
const token = localStorage.getItem(tokenLocalKeyForGateway(gatewayUrl)) ?? "";
|
||||
return token.trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function persistGatewayToken(gatewayUrl: string, token: string, remember: boolean) {
|
||||
try {
|
||||
const normalized = token.trim();
|
||||
persistSessionToken(gatewayUrl, remember ? "" : normalized);
|
||||
const localKey = tokenLocalKeyForGateway(gatewayUrl);
|
||||
if (remember && normalized) {
|
||||
localStorage.setItem(localKey, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(localKey);
|
||||
}
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
export function loadSettings(): UiSettings {
|
||||
const { pageUrl: pageDerivedUrl, effectiveUrl: defaultUrl } = deriveDefaultGatewayUrl();
|
||||
|
||||
const defaults: UiSettings = {
|
||||
gatewayUrl: defaultUrl,
|
||||
token: loadSessionToken(defaultUrl),
|
||||
rememberGatewayAuth: false,
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "claw",
|
||||
@ -152,10 +183,12 @@ export function loadSettings(): UiSettings {
|
||||
(parsed as { theme?: unknown }).theme,
|
||||
(parsed as { themeMode?: unknown }).themeMode,
|
||||
);
|
||||
const rememberGatewayAuth =
|
||||
typeof parsed.rememberGatewayAuth === "boolean" ? parsed.rememberGatewayAuth : false;
|
||||
const settings = {
|
||||
gatewayUrl,
|
||||
// Gateway auth is intentionally in-memory only; scrub any legacy persisted token on load.
|
||||
token: loadSessionToken(gatewayUrl),
|
||||
token: rememberGatewayAuth ? loadRememberedToken(gatewayUrl) : loadSessionToken(gatewayUrl),
|
||||
rememberGatewayAuth,
|
||||
sessionKey:
|
||||
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
|
||||
? parsed.sessionKey.trim()
|
||||
@ -205,9 +238,10 @@ export function saveSettings(next: UiSettings) {
|
||||
}
|
||||
|
||||
function persistSettings(next: UiSettings) {
|
||||
persistSessionToken(next.gatewayUrl, next.token);
|
||||
persistGatewayToken(next.gatewayUrl, next.token, next.rememberGatewayAuth);
|
||||
const persisted: PersistedUiSettings = {
|
||||
gatewayUrl: next.gatewayUrl,
|
||||
rememberGatewayAuth: next.rememberGatewayAuth,
|
||||
sessionKey: next.sessionKey,
|
||||
lastActiveSessionKey: next.lastActiveSessionKey,
|
||||
theme: next.theme,
|
||||
|
||||
@ -97,6 +97,17 @@ export function renderLoginGate(state: AppViewState) {
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field" style="gap:8px; flex-direction:row; align-items:center;">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${state.settings.rememberGatewayAuth}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
state.applySettings({ ...state.settings, rememberGatewayAuth: checked });
|
||||
}}
|
||||
/>
|
||||
<span>Remember me on this device</span>
|
||||
</label>
|
||||
<button
|
||||
class="btn primary login-gate__connect"
|
||||
@click=${() => state.connect()}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user