mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
refactor: dedupe tlon helpers
This commit is contained in:
@@ -27,6 +27,25 @@ export type TlonHistoryEntry = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
function createHistoryEntryFromMemo(params: {
|
||||
memo?: Record<string, unknown> | null;
|
||||
seal?: Record<string, unknown> | null;
|
||||
fallbackId?: unknown;
|
||||
}): TlonHistoryEntry {
|
||||
const { memo, seal, fallbackId } = params;
|
||||
return {
|
||||
author: typeof memo?.author === "string" ? memo.author : "unknown",
|
||||
content: extractMessageText(memo?.content || []),
|
||||
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
|
||||
id:
|
||||
typeof seal?.id === "string"
|
||||
? seal.id
|
||||
: typeof fallbackId === "string"
|
||||
? fallbackId
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const messageCache = new Map<string, TlonHistoryEntry[]>();
|
||||
const MAX_CACHED_MESSAGES = 100;
|
||||
|
||||
@@ -166,17 +185,7 @@ export async function fetchThreadHistory(
|
||||
const memo = asRecord(itemRecord?.memo) ?? asRecord(replySet?.memo) ?? itemRecord;
|
||||
const seal = asRecord(itemRecord?.seal) ?? asRecord(replySet?.seal);
|
||||
|
||||
return {
|
||||
author: typeof memo?.author === "string" ? memo.author : "unknown",
|
||||
content: extractMessageText(memo?.content || []),
|
||||
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
|
||||
id:
|
||||
typeof seal?.id === "string"
|
||||
? seal.id
|
||||
: typeof itemRecord?.id === "string"
|
||||
? itemRecord.id
|
||||
: undefined,
|
||||
} as TlonHistoryEntry;
|
||||
return createHistoryEntryFromMemo({ memo, seal, fallbackId: itemRecord?.id });
|
||||
})
|
||||
.filter((msg) => msg.content);
|
||||
|
||||
@@ -202,12 +211,7 @@ export async function fetchThreadHistory(
|
||||
const replyRecord = asRecord(reply);
|
||||
const memo = asRecord(replyRecord?.memo);
|
||||
const seal = asRecord(replyRecord?.seal);
|
||||
return {
|
||||
author: typeof memo?.author === "string" ? memo.author : "unknown",
|
||||
content: extractMessageText(memo?.content || []),
|
||||
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
|
||||
id: typeof seal?.id === "string" ? seal.id : undefined,
|
||||
};
|
||||
return createHistoryEntryFromMemo({ memo, seal });
|
||||
})
|
||||
.filter((msg: TlonHistoryEntry) => msg.content);
|
||||
|
||||
|
||||
@@ -26,28 +26,27 @@ const baseAccount: TlonResolvedAccount = {
|
||||
ownerShip: "~marzod",
|
||||
};
|
||||
|
||||
function allowlistMigrationDecisions(currentSettings: Record<string, unknown>) {
|
||||
const allowlistKeys = new Set(["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"]);
|
||||
return Object.fromEntries(
|
||||
buildTlonSettingsMigrations(baseAccount, currentSettings)
|
||||
.filter((migration) => allowlistKeys.has(migration.key))
|
||||
.map((migration) => [
|
||||
migration.key,
|
||||
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
describe("shouldMigrateTlonSetting", () => {
|
||||
it("does not rehydrate explicit empty-array revocations during startup migration", () => {
|
||||
const migrations = buildTlonSettingsMigrations(baseAccount, {
|
||||
const decisions = allowlistMigrationDecisions({
|
||||
dmAllowlist: [],
|
||||
groupInviteAllowlist: [],
|
||||
defaultAuthorizedShips: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
migrations
|
||||
.filter((migration) =>
|
||||
["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes(
|
||||
migration.key,
|
||||
),
|
||||
)
|
||||
.map((migration) => [
|
||||
migration.key,
|
||||
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
expect(decisions).toEqual({
|
||||
dmAllowlist: false,
|
||||
groupInviteAllowlist: false,
|
||||
defaultAuthorizedShips: false,
|
||||
@@ -55,22 +54,9 @@ describe("shouldMigrateTlonSetting", () => {
|
||||
});
|
||||
|
||||
it("still seeds file-config allowlists on first run when settings are missing", () => {
|
||||
const migrations = buildTlonSettingsMigrations(baseAccount, {});
|
||||
const decisions = allowlistMigrationDecisions({});
|
||||
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
migrations
|
||||
.filter((migration) =>
|
||||
["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes(
|
||||
migration.key,
|
||||
),
|
||||
)
|
||||
.map((migration) => [
|
||||
migration.key,
|
||||
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
expect(decisions).toEqual({
|
||||
dmAllowlist: true,
|
||||
groupInviteAllowlist: true,
|
||||
defaultAuthorizedShips: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import { buildTlonAccountFields } from "./account-fields.js";
|
||||
import { buildTlonAccountFields, type TlonAccountFieldsInput } from "./account-fields.js";
|
||||
import { normalizeShip } from "./targets.js";
|
||||
import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js";
|
||||
import { validateUrbitBaseUrl } from "./urbit/base-url.js";
|
||||
@@ -23,16 +23,7 @@ function tlonChannelId() {
|
||||
return "tlon" as const;
|
||||
}
|
||||
|
||||
export type TlonSetupInput = ChannelSetupInput & {
|
||||
ship?: string;
|
||||
url?: string;
|
||||
code?: string;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
groupChannels?: string[];
|
||||
dmAllowlist?: string[];
|
||||
autoDiscoverChannels?: boolean;
|
||||
ownerShip?: string;
|
||||
};
|
||||
export type TlonSetupInput = ChannelSetupInput & TlonAccountFieldsInput;
|
||||
|
||||
function isConfigured(account: TlonResolvedAccount): boolean {
|
||||
return Boolean(account.ship && account.url && account.code);
|
||||
|
||||
@@ -23,6 +23,15 @@ export function parseChannelNest(raw: string): { hostShip: string; channelName:
|
||||
return { hostShip, channelName };
|
||||
}
|
||||
|
||||
function makeGroupTarget(parsed: { hostShip: string; channelName: string }): TlonTarget {
|
||||
return {
|
||||
kind: "group",
|
||||
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
|
||||
hostShip: parsed.hostShip,
|
||||
channelName: parsed.channelName,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTlonTarget(raw?: string | null): TlonTarget | null {
|
||||
const trimmed = raw?.trim();
|
||||
if (!trimmed) {
|
||||
@@ -43,12 +52,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: "group",
|
||||
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
|
||||
hostShip: parsed.hostShip,
|
||||
channelName: parsed.channelName,
|
||||
};
|
||||
return makeGroupTarget(parsed);
|
||||
}
|
||||
const parts = groupTarget.split("/");
|
||||
if (parts.length === 2) {
|
||||
@@ -69,12 +73,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: "group",
|
||||
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
|
||||
hostShip: parsed.hostShip,
|
||||
channelName: parsed.channelName,
|
||||
};
|
||||
return makeGroupTarget(parsed);
|
||||
}
|
||||
|
||||
if (SHIP_RE.test(withoutPrefix)) {
|
||||
|
||||
@@ -2,12 +2,17 @@ import { describe, expect, it } from "vitest";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
|
||||
describe("validateUrbitBaseUrl", () => {
|
||||
it("adds https:// when scheme is missing and strips path/query fragments", () => {
|
||||
const result = validateUrbitBaseUrl("example.com/foo?bar=baz");
|
||||
function expectValidBaseUrl(raw: string) {
|
||||
const result = validateUrbitBaseUrl(raw);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it("adds https:// when scheme is missing and strips path/query fragments", () => {
|
||||
const result = expectValidBaseUrl("example.com/foo?bar=baz");
|
||||
expect(result.baseUrl).toBe("https://example.com");
|
||||
expect(result.hostname).toBe("example.com");
|
||||
});
|
||||
@@ -31,21 +36,13 @@ describe("validateUrbitBaseUrl", () => {
|
||||
});
|
||||
|
||||
it("normalizes a trailing dot in the hostname for origin construction", () => {
|
||||
const result = validateUrbitBaseUrl("https://example.com./foo");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
const result = expectValidBaseUrl("https://example.com./foo");
|
||||
expect(result.baseUrl).toBe("https://example.com");
|
||||
expect(result.hostname).toBe("example.com");
|
||||
});
|
||||
|
||||
it("preserves port in the normalized origin", () => {
|
||||
const result = validateUrbitBaseUrl("http://example.com:8080/~/login");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
const result = expectValidBaseUrl("http://example.com:8080/~/login");
|
||||
expect(result.baseUrl).toBe("http://example.com:8080");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,18 @@ export class UrbitSSEClient {
|
||||
this.fetchImpl = options.fetchImpl;
|
||||
}
|
||||
|
||||
private channelRequestContext() {
|
||||
return {
|
||||
baseUrl: this.url,
|
||||
cookie: this.cookie,
|
||||
ship: this.ship,
|
||||
channelId: this.channelId,
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
};
|
||||
}
|
||||
|
||||
async subscribe(params: {
|
||||
app: string;
|
||||
path: string;
|
||||
@@ -133,21 +145,10 @@ export class UrbitSSEClient {
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await ensureUrbitChannelOpen(
|
||||
{
|
||||
baseUrl: this.url,
|
||||
cookie: this.cookie,
|
||||
ship: this.ship,
|
||||
channelId: this.channelId,
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
},
|
||||
{
|
||||
createBody: this.subscriptions,
|
||||
createAuditContext: "tlon-urbit-channel-create",
|
||||
},
|
||||
);
|
||||
await ensureUrbitChannelOpen(this.channelRequestContext(), {
|
||||
createBody: this.subscriptions,
|
||||
createAuditContext: "tlon-urbit-channel-create",
|
||||
});
|
||||
|
||||
await this.openStream();
|
||||
this.isConnected = true;
|
||||
@@ -305,18 +306,10 @@ export class UrbitSSEClient {
|
||||
}
|
||||
|
||||
async poke(params: { app: string; mark: string; json: unknown }) {
|
||||
return await pokeUrbitChannel(
|
||||
{
|
||||
baseUrl: this.url,
|
||||
cookie: this.cookie,
|
||||
ship: this.ship,
|
||||
channelId: this.channelId,
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
},
|
||||
{ ...params, auditContext: "tlon-urbit-poke" },
|
||||
);
|
||||
return await pokeUrbitChannel(this.channelRequestContext(), {
|
||||
...params,
|
||||
auditContext: "tlon-urbit-poke",
|
||||
});
|
||||
}
|
||||
|
||||
async scry(path: string) {
|
||||
|
||||
Reference in New Issue
Block a user