fix(feishu): guard thread store startup order (#1700)

This commit is contained in:
Hunter Bown
2026-05-16 18:25:23 -05:00
committed by GitHub
parent 5401eaae08
commit f0a7e15d30
4 changed files with 88 additions and 59 deletions
+59 -59
View File
@@ -20,6 +20,65 @@ import {
stripGroupPrefix
} from "./lib.mjs";
class ThreadStore {
static async open(filePath) {
const store = new ThreadStore(filePath);
await store.load();
return store;
}
constructor(filePath) {
this.filePath = filePath;
this.data = { chats: {} };
}
async load() {
try {
const raw = await fs.readFile(this.filePath, "utf8");
this.data = JSON.parse(raw);
if (!this.data.chats) this.data.chats = {};
if (!this.data.messages) this.data.messages = [];
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
async recordMessage(messageId) {
if (!messageId) return false;
if (!Array.isArray(this.data.messages)) this.data.messages = [];
if (this.data.messages.includes(messageId)) return true;
this.data.messages.push(messageId);
this.data.messages = this.data.messages.slice(-200);
await this.save();
return false;
}
async getChat(chatId) {
return this.data.chats[chatId] || null;
}
async setChat(chatId, state) {
this.data.chats[chatId] = state;
await this.save();
return state;
}
async patchChat(chatId, patch) {
const current = this.data.chats[chatId] || {};
this.data.chats[chatId] = { ...current, ...patch };
await this.save();
return this.data.chats[chatId];
}
async save() {
const dir = path.dirname(this.filePath);
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
const tmp = `${this.filePath}.tmp`;
await fs.writeFile(tmp, `${JSON.stringify(this.data, null, 2)}\n`, { mode: 0o600 });
await fs.rename(tmp, this.filePath);
}
}
const config = {
appId: requiredEnv("FEISHU_APP_ID"),
appSecret: requiredEnv("FEISHU_APP_SECRET"),
@@ -509,62 +568,3 @@ function resolveLarkDomain(domain) {
if (normalized === "feishu") return Lark.Domain?.Feishu || "https://open.feishu.cn";
return domain;
}
class ThreadStore {
static async open(filePath) {
const store = new ThreadStore(filePath);
await store.load();
return store;
}
constructor(filePath) {
this.filePath = filePath;
this.data = { chats: {} };
}
async load() {
try {
const raw = await fs.readFile(this.filePath, "utf8");
this.data = JSON.parse(raw);
if (!this.data.chats) this.data.chats = {};
if (!this.data.messages) this.data.messages = [];
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
async recordMessage(messageId) {
if (!messageId) return false;
if (!Array.isArray(this.data.messages)) this.data.messages = [];
if (this.data.messages.includes(messageId)) return true;
this.data.messages.push(messageId);
this.data.messages = this.data.messages.slice(-200);
await this.save();
return false;
}
async getChat(chatId) {
return this.data.chats[chatId] || null;
}
async setChat(chatId, state) {
this.data.chats[chatId] = state;
await this.save();
return state;
}
async patchChat(chatId, patch) {
const current = this.data.chats[chatId] || {};
this.data.chats[chatId] = { ...current, ...patch };
await this.save();
return this.data.chats[chatId];
}
async save() {
const dir = path.dirname(this.filePath);
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
const tmp = `${this.filePath}.tmp`;
await fs.writeFile(tmp, `${JSON.stringify(this.data, null, 2)}\n`, { mode: 0o600 });
await fs.rename(tmp, this.filePath);
}
}
@@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
test("ThreadStore is initialized before bridge startup opens it", async () => {
const source = await fs.readFile(path.join(__dirname, "../src/index.mjs"), "utf8");
const declaration = source.indexOf("class ThreadStore");
const startupUse = source.indexOf("await ThreadStore.open");
assert.notEqual(declaration, -1);
assert.notEqual(startupUse, -1);
assert.ok(declaration < startupUse);
});