feat: add reusable core store and OpenClaw state-file adapter skeleton / 新增可重用 state store 與 OpenClaw state-file adapter 骨架

This commit is contained in:
Alice (OpenClaw)
2026-05-13 11:35:15 +08:00
parent d5a7e98fb3
commit ca55445bcb
12 changed files with 244 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import type { NormalizedReplyEndEvent, ReplyEndChoice } from "../types.js"
export function parseTelegramCallbackData(raw: string): ReplyEndChoice | null {
if (raw === "rec:continue") return "continue"
if (raw === "rec:stop") return "stop"
return null
}
export function normalizeTelegramCallback(params: {
callbackData: string
conversationId: string
sessionKey: string | null
sourceMessageId: string
sourceCallbackId: string
timestamp: string
}): NormalizedReplyEndEvent | null {
const choice = parseTelegramCallbackData(params.callbackData)
if (!choice) return null
return {
choice,
conversationId: params.conversationId,
sessionKey: params.sessionKey,
sourceMessageId: params.sourceMessageId,
sourceCallbackId: params.sourceCallbackId,
channel: "telegram",
timestamp: params.timestamp,
}
}

13
src/core/policy.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { ReplyEndPolicyDecision, ReplyEndPolicyInput } from "../types.js"
export function evaluateReplyEndPolicy(input: ReplyEndPolicyInput): ReplyEndPolicyDecision {
if (input.hasTypedUserFollowup) {
return { suppressProactiveContinuation: false }
}
if (input.state?.lastChoice === "stop" && input.state.active) {
return { suppressProactiveContinuation: true }
}
return { suppressProactiveContinuation: false }
}

32
src/core/state.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { NormalizedReplyEndEvent, ReplyEndState, ReplyEndStateMap } from "../types.js"
export function buildReplyEndState(event: NormalizedReplyEndEvent): ReplyEndState {
return {
lastChoice: event.choice,
lastChoiceAt: event.timestamp,
sourceMessageId: event.sourceMessageId,
sourceCallbackId: event.sourceCallbackId,
active: true,
}
}
export function clearReplyEndState(): null {
return null
}
export function setReplyEndState(map: ReplyEndStateMap, conversationId: string, state: ReplyEndState): ReplyEndStateMap {
return {
...map,
[conversationId]: state,
}
}
export function getReplyEndState(map: ReplyEndStateMap, conversationId: string): ReplyEndState | null {
return map[conversationId] ?? null
}
export function clearReplyEndStateForConversation(map: ReplyEndStateMap, conversationId: string): ReplyEndStateMap {
const next = { ...map }
delete next[conversationId]
return next
}

41
src/core/store.ts Normal file
View File

@@ -0,0 +1,41 @@
import fs from "node:fs"
import path from "node:path"
import type { ReplyEndState, ReplyEndStateMap } from "../types.js"
import { clearReplyEndStateForConversation, getReplyEndState, setReplyEndState } from "./state.js"
export function resolveReplyEndStateFile(baseDir: string): string {
return path.join(baseDir, "reply-end-controls.json")
}
export function readReplyEndStateMap(filePath: string): ReplyEndStateMap {
if (!fs.existsSync(filePath)) return {}
try {
const raw = fs.readFileSync(filePath, "utf-8")
const parsed = JSON.parse(raw)
return parsed && typeof parsed === "object" ? parsed as ReplyEndStateMap : {}
} catch {
return {}
}
}
export function writeReplyEndStateMap(filePath: string, stateMap: ReplyEndStateMap): void {
fs.writeFileSync(filePath, JSON.stringify(stateMap, null, 2))
}
export function saveReplyEndState(filePath: string, conversationId: string, state: ReplyEndState): ReplyEndStateMap {
const current = readReplyEndStateMap(filePath)
const next = setReplyEndState(current, conversationId, state)
writeReplyEndStateMap(filePath, next)
return next
}
export function loadReplyEndState(filePath: string, conversationId: string): ReplyEndState | null {
return getReplyEndState(readReplyEndStateMap(filePath), conversationId)
}
export function removeReplyEndState(filePath: string, conversationId: string): ReplyEndStateMap {
const current = readReplyEndStateMap(filePath)
const next = clearReplyEndStateForConversation(current, conversationId)
writeReplyEndStateMap(filePath, next)
return next
}