f7c23fb325
Add ts-worker/ with the Bun/TypeScript worker that replaces claw-profile-worker. The Dockerfile now builds a single image containing both the Rust gateway (claw-telegram) and the TS worker. The image defaults to worker mode (bun run ts-worker/main.ts). The gateway Unraid XML overrides with --entrypoint claw-telegram. Worker containers use the same image with the default CMD. - Add ts-worker/ (12 files): HTTP/SSE server, Anthropic SDK engine, approval broker, event translator, state stores - Add package.json with @anthropic-ai/sdk dependency - Rewrite Dockerfile: three-stage build (Rust + Bun + runtime) - Revert CLAW_GATEWAY_WORKER_IMAGE to claw-telegram:latest - Remove image pull from docker_worker_manager (same image, already local) - Add ts-worker paths to CI trigger Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
/**
|
|
* FeedStore — manages feed items and their payload files.
|
|
*
|
|
* Feed items are non-text outputs from Claude (images, HTML previews,
|
|
* code files, diffs, etc.) that get surfaced in the Mini App.
|
|
*/
|
|
|
|
import { randomUUID } from 'crypto'
|
|
import { readFile, writeFile, mkdir, readdir, stat } from 'fs/promises'
|
|
import { join, basename } from 'path'
|
|
import type { FeedItemRecord, FeedItemKind, FeedItemSource } from './records.js'
|
|
|
|
export class FeedStore {
|
|
private items = new Map<string, FeedItemRecord>()
|
|
private stateRoot: string
|
|
private payloadDir: string
|
|
|
|
constructor(stateRoot: string) {
|
|
this.stateRoot = stateRoot
|
|
this.payloadDir = join(stateRoot, 'claw', 'feed')
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
await mkdir(this.payloadDir, { recursive: true })
|
|
// Load existing feed index if present
|
|
try {
|
|
const indexPath = join(this.payloadDir, 'index.json')
|
|
const data = await readFile(indexPath, 'utf8')
|
|
const records = JSON.parse(data) as FeedItemRecord[]
|
|
for (const r of records) {
|
|
this.items.set(r.feed_item_id, r)
|
|
}
|
|
} catch {
|
|
// No existing index — start fresh
|
|
}
|
|
}
|
|
|
|
async addItem(
|
|
title: string,
|
|
kind: FeedItemKind,
|
|
source: FeedItemSource,
|
|
payload?: Uint8Array,
|
|
options?: {
|
|
mediaType?: string
|
|
fileName?: string
|
|
linkedAppId?: string
|
|
previewText?: string
|
|
},
|
|
): Promise<FeedItemRecord> {
|
|
const id = randomUUID()
|
|
const now = Date.now()
|
|
let storedPath: string | undefined
|
|
|
|
if (payload) {
|
|
const ext = options?.fileName
|
|
? basename(options.fileName)
|
|
: id
|
|
storedPath = join(this.payloadDir, ext)
|
|
await writeFile(storedPath, payload)
|
|
}
|
|
|
|
const record: FeedItemRecord = {
|
|
feed_item_id: id,
|
|
title,
|
|
kind,
|
|
source,
|
|
linked_app_id: options?.linkedAppId,
|
|
media_type: options?.mediaType,
|
|
file_name: options?.fileName,
|
|
stored_path: storedPath,
|
|
preview_text: options?.previewText,
|
|
deleted: false,
|
|
created_at: now,
|
|
updated_at: now,
|
|
}
|
|
|
|
this.items.set(id, record)
|
|
await this.persistIndex()
|
|
return record
|
|
}
|
|
|
|
getItem(feedItemId: string): FeedItemRecord | undefined {
|
|
return this.items.get(feedItemId)
|
|
}
|
|
|
|
listItems(turnId?: string): FeedItemRecord[] {
|
|
let items = Array.from(this.items.values()).filter((i) => !i.deleted)
|
|
if (turnId) {
|
|
items = items.filter((i) => i.source.turn_id === turnId)
|
|
}
|
|
return items.sort((a, b) => b.created_at - a.created_at)
|
|
}
|
|
|
|
async getPayload(feedItemId: string): Promise<Uint8Array | null> {
|
|
const item = this.items.get(feedItemId)
|
|
if (!item?.stored_path) return null
|
|
try {
|
|
return await readFile(item.stored_path) as unknown as Uint8Array
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
deleteItem(feedItemId: string): boolean {
|
|
const item = this.items.get(feedItemId)
|
|
if (!item) return false
|
|
item.deleted = true
|
|
item.updated_at = Date.now()
|
|
return true
|
|
}
|
|
|
|
reset(): void {
|
|
this.items.clear()
|
|
}
|
|
|
|
private async persistIndex(): Promise<void> {
|
|
const indexPath = join(this.payloadDir, 'index.json')
|
|
const records = Array.from(this.items.values())
|
|
await writeFile(indexPath, JSON.stringify(records, null, 2))
|
|
}
|
|
}
|