fix: allow development database reset on approved remote host

This commit is contained in:
YBF
2026-09-14 14:50:13 +08:00
parent 55d02a87df
commit d4f16c7c6f
3 changed files with 53 additions and 7 deletions
+37
View File
@@ -0,0 +1,37 @@
## 概览
本 PR 汇总近期 OneTalk 消息中心的同步、单会话历史重建、商品消息采集和页面交互整理,并补齐共享协议、服务端、Chrome 扩展、数据库迁移及回归测试。
## 最近改动
- **消息同步链路**
- 移除旧的 flat-history publication flow。
- 调整 Plugin ↔ Bright 的同步、ACK、checkpoint 和生命周期处理,确保消息与完成状态按会话边界推进。
- **单会话历史重建**
- 新增 `POST /api/bright/onetalk/accounts/:channelAccountId/conversations/:conversationId/history/rebuild`
- 使用独立的 `rebuild` 权限、精确会话范围和新鲜 heartbeat 选择唯一 Plugin。
- 新增 `storage.delete.*``history.sync.*``rebuild.status` 协议帧,并升级 OneTalk protocol version 到 v7。
-`channelAccountId + conversationId` 清理 Plugin 本地消息账本,再通过 Bright 事务清理目标消息/会话级异常并重置消息派生状态。
- 通过 `historyGeneration` 隔离重建前后的旧消息、ACK、页面结果和完成结果;服务端 reset 提交后复用现有单会话 full sync,并区分 `server_reset_committed` 与后续 resync 结果。
- **商品消息采集**
- 严格识别 `https://chinese.alibaba.com/product-detail/...-<productId>.html` 商品详情链接。
- 归一化为不带 query/token 的 `{ version, kind: "product", sourceUrl, productId }`,沿用现有观察、持久化和读取链路。
- 增加 `product` 数据库约束和迁移,不回填既有消息。
- **OneTalk 页面 DOM 交互整理**
- 将动作状态提示、会话 ID 复制、文件上传器定位和会话选择辅助逻辑收敛到 `main-page/dom/`
- 引入 `@testing-library/dom``@testing-library/user-event``jsdom` 验证真实 DOM 控件交互,同时保留现有 SDK 发送路径和 bridge 行为。
- **契约、数据层与测试**
- 扩展共享消息内容、同步、授权、读取和 WebSocket wire contract,并保持严格字段校验。
- 新增 `0012_onetalk_history_generation.sql``0013_nifty_thunderbird.sql` 迁移。
- 补充或更新 contract、Chrome extension、server 的同步、历史重建、商品内容、数据库读取和 DOM 回归测试。
## 影响范围
- `packages/onetalk-contract`
- `apps/chrome-extension`
- `apps/server`
- OneTalk 数据库迁移及相关测试
+5 -5
View File
@@ -17,11 +17,11 @@ export type DevelopmentResetDependencies = {
runMigrations?: (databaseUrl: string) => Promise<void>;
};
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
const DEVELOPMENT_DATABASE_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1", "170.9.52.93"]);
const entryPath = process.argv[1];
const isMainModule = entryPath !== undefined && import.meta.url === pathToFileURL(entryPath).href;
const isLoopbackDatabaseUrl = (databaseUrl: string): boolean => {
const isAllowedDevelopmentDatabaseUrl = (databaseUrl: string): boolean => {
let url: URL;
try {
@@ -33,7 +33,7 @@ const isLoopbackDatabaseUrl = (databaseUrl: string): boolean => {
const hostname = url.hostname.replace(/^\[|\]$/g, "");
const isPostgresUrl = url.protocol === "postgres:" || url.protocol === "postgresql:";
return isPostgresUrl && LOOPBACK_HOSTNAMES.has(hostname);
return isPostgresUrl && DEVELOPMENT_DATABASE_HOSTNAMES.has(hostname);
};
export const readDevelopmentDatabaseUrl = (
@@ -45,8 +45,8 @@ export const readDevelopmentDatabaseUrl = (
const databaseUrl = readDatabaseUrl(environment);
if (!isLoopbackDatabaseUrl(databaseUrl)) {
throw new Error("Development database reset requires a loopback DATABASE_URL");
if (!isAllowedDevelopmentDatabaseUrl(databaseUrl)) {
throw new Error("Development database reset requires an approved development DATABASE_URL");
}
return databaseUrl;
+11 -2
View File
@@ -37,6 +37,15 @@ test("accepts and trims a development loopback database URL", () => {
);
});
test("accepts the dedicated development database server", () => {
const databaseUrl = "postgres://postgres:postgres@170.9.52.93:15432/trade_message_center";
assert.equal(
readDevelopmentDatabaseUrl({ ...developmentEnvironment, DATABASE_URL: databaseUrl }),
databaseUrl,
);
});
test("rejects a reset outside development", () => {
assert.throws(
() => readDevelopmentDatabaseUrl({ ...developmentEnvironment, NODE_ENV: "production" }),
@@ -44,14 +53,14 @@ test("rejects a reset outside development", () => {
);
});
test("rejects a non-loopback database URL", () => {
test("rejects an unapproved development database URL", () => {
assert.throws(
() =>
readDevelopmentDatabaseUrl({
...developmentEnvironment,
DATABASE_URL: "postgres://postgres:postgres@db.example.com/trade_message_center",
}),
/loopback DATABASE_URL/,
/approved development DATABASE_URL/,
);
});