fix(feishu): guard thread store startup order (#1700)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user