From c979912da77d80a1a4bf68fb1f65790fccb61d30 Mon Sep 17 00:00:00 2001
From: YBF <47051132+YBFACC@users.noreply.github.com>
Date: Wed, 26 Aug 2026 03:31:51 +0800
Subject: [PATCH] chore: standardize repository formatting and editor setup
---
.editorconfig | 11 +
.oxfmtrc.json | 13 +-
.../frontend/quality-guidelines.md | 4 +
.trellis/spec/project/architecture.md | 36 ++
.vscode/extensions.json | 3 +
.vscode/settings.json | 29 +
README.md | 22 +
apps/chrome-extension/package.json | 30 +-
apps/chrome-extension/popup/popup.html | 66 +--
apps/chrome-extension/popup/popup.ts | 26 +-
apps/chrome-extension/public/manifest.json | 52 +-
apps/chrome-extension/src/lib/error.ts | 2 +-
.../src/lib/websocket-observer.ts | 38 +-
.../current-conversation-history/entry.ts | 8 +-
.../current-conversation-history/index.ts | 142 ++---
.../current-conversation-history/model.ts | 180 +++---
.../current-conversation-history/sdk.ts | 184 +++---
.../main-page/message-observer/entry.ts | 2 +-
.../main-page/message-observer/history.ts | 26 +-
.../main-page/message-observer/index.ts | 24 +-
.../main-page/message-observer/model.ts | 112 ++--
.../onetalk/main-page/message-observer/new.ts | 52 +-
.../main-page/message-observer/websocket.ts | 58 +-
.../src/onetalk/main-page/model.ts | 14 +-
.../onetalk/main-page/page-script-entry.ts | 4 +-
.../onetalk/main-page/sync-push-decoder.ts | 372 ++++++------
.../src/onetalk/main-page/utils.ts | 2 +-
...etalk-current-conversation-history.test.js | 554 +++++++++---------
.../test/onetalk-sync-push-decoder.test.js | 38 +-
.../test/onetalk-websocket-tap.test.js | 300 +++++-----
apps/chrome-extension/tsconfig.json | 20 +-
apps/chrome-extension/vite.config.ts | 57 +-
apps/server/package.json | 14 +-
package.json | 58 +-
pnpm-workspace.yaml | 4 +-
tsconfig.base.json | 20 +-
36 files changed, 1347 insertions(+), 1230 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .vscode/extensions.json
create mode 100644 .vscode/settings.json
create mode 100644 README.md
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..29ff089
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index aadcc03..f17de6d 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -1,4 +1,13 @@
{
- "$schema": "./node_modules/oxfmt/configuration_schema.json",
- "ignorePatterns": [".agents/**", ".codex/**", ".trellis/**", "AGENTS.md", "pnpm-lock.yaml"]
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "tabWidth": 4,
+ "useTabs": false,
+ "ignorePatterns": [
+ ".agents/**",
+ ".codex/**",
+ ".trellis/**",
+ "AGENTS.md",
+ "pnpm-lock.yaml",
+ "prd.html"
+ ]
}
diff --git a/.trellis/spec/chrome-extension/frontend/quality-guidelines.md b/.trellis/spec/chrome-extension/frontend/quality-guidelines.md
index ff5d894..18fc516 100644
--- a/.trellis/spec/chrome-extension/frontend/quality-guidelines.md
+++ b/.trellis/spec/chrome-extension/frontend/quality-guidelines.md
@@ -8,6 +8,7 @@
- 仓库统一使用 Oxfmt;lint-staged 只格式化暂存文件,Husky 在 `pre-commit` 阶段触发该检查。
- 扩展包使用 Vite `6.x` 和 TypeScript `5.7.x`。`typecheck` 执行 `tsc -p tsconfig.json --noEmit`,`build` 先检查类型再执行 `vite build`,`test` 使用 Node 内置 test runner;尚无 lint 脚本。
- Vite 依赖的 `esbuild` 安装脚本在根 `pnpm-workspace.yaml` 的 `allowBuilds` 中显式允许。
+- 代码格式统一使用根目录 Oxfmt,缩进为 4 个空格;VSCode 保存格式化使用工作区推荐的 `oxc.oxc-vscode`,不得使用其它 formatter。
## 必须遵守
@@ -32,6 +33,8 @@ pnpm build
pnpm test
```
+编辑器保存格式化必须与 `pnpm format` 相同。VSCode 使用 `.vscode/settings.json` 指向根 `.oxfmtrc.json`;未安装 Oxc 扩展时关闭 `formatOnSave`,不要让内置 TypeScript formatter 或其它 formatter 生成提交前会被改写的代码。
+
其中 `typecheck` 会执行严格 TypeScript 检查,`build` 会生成 `apps/chrome-extension/dist/`,`test` 会执行 `apps/chrome-extension/test/*.test.js`。新增非平凡逻辑时,应补充最小可运行测试。
## 评审清单
@@ -43,3 +46,4 @@ pnpm test
- Popup 移动后,Vite input、TypeScript include、Manifest 路径和 `dist/` 产物是否一致?
- 是否覆盖了用户可见的加载、空、错误和键盘交互状态?
- 是否执行了当前可用的根级验证命令,并如实说明跳过项?
+- 编辑器保存后的代码是否仍通过 Oxfmt 检查,并保持 4 个空格缩进?
diff --git a/.trellis/spec/project/architecture.md b/.trellis/spec/project/architecture.md
index 8047971..c4cd131 100644
--- a/.trellis/spec/project/architecture.md
+++ b/.trellis/spec/project/architecture.md
@@ -174,6 +174,16 @@ types / constants
这里的“主函数”是文件中负责组织其它声明完成该文件主要职责的函数,通常是入口函数、编排函数或主要公开函数;不是按函数长度判断,也不是强制命名为 `main`。
+### 3.8 格式化器唯一来源与编辑器保存
+
+仓库统一使用根目录 [`package.json`](../../package.json) 声明的 Oxfmt。缩进使用 4 个空格,不使用 Tab;具体格式规则由根目录 [`.oxfmtrc.json`](../../.oxfmtrc.json) 维护,基础编辑器空白行为由 [`.editorconfig`](../../.editorconfig) 对齐。
+
+1. `pnpm format` 是修改格式的唯一标准命令,`pnpm format:check` 是提交前的格式门禁。
+2. VSCode 保存格式化只能使用 `oxc.oxc-vscode`,并且必须读取仓库的 `.oxfmtrc.json`;工作区设置位于 `.vscode/settings.json`,推荐扩展位于 `.vscode/extensions.json`。
+3. 未安装 Oxc 扩展时,不得让 VSCode 内置 TypeScript formatter、Prettier、Biome 或其它 formatter 接管保存格式化;应先安装 Oxc 扩展,或关闭 `formatOnSave` 后执行 `pnpm format`。
+4. `.editorconfig` 只提供缩进、换行和文件末尾换行等基础编辑器行为,不能替代 Oxfmt,也不能成为第二套格式规则。
+5. 提交钩子、编辑器保存和 CI 检查必须产生同一份 Oxfmt 结果;若保存后再次运行 `pnpm format` 仍产生差异,视为格式化配置冲突,必须先修复配置。
+
## 4. Validation & Error Matrix
| 发现的代码形态 | 处理 |
@@ -190,6 +200,8 @@ types / constants
| 文件头缺少职责注释,或正文少于 10 / 多于 30 个字符 | 补充或改写为 10–30 个字符的职责描述 |
| 入口文件头只描述“这是入口” | 改为描述入口所在目录的整体职责 |
| 有主函数但主函数前没有独立职责注释 | 在主函数声明正上方补充职责说明 |
+| 编辑器保存后与 Oxfmt 结果不同 | 将保存 formatter 切换为 Oxc,或关闭保存格式化后运行 `pnpm format` |
+| 代码使用 2 空格或 Tab,与项目约定不一致 | 按 `.oxfmtrc.json` 和 `.editorconfig` 统一为 4 个空格 |
## 5. Good / Base / Bad Cases
@@ -208,6 +220,8 @@ types / constants
- 入口、分发和分支重构应保留原有行为测试,证明只改变职责归属,没有改变输出契约。
- 新增或修改手写代码时,检查文件第一行职责注释的正文长度为 10–30 个字符;入口文件还要检查注释描述的是目录职责。
- 检查主函数前存在独立职责注释,且主函数是最后一个函数声明;允许其后出现直接启动调用或显式导出。
+- 检查手工保存后的文件通过 `pnpm format:check`,且 VSCode 使用 `oxc.oxc-vscode` 读取根 `.oxfmtrc.json`。
+- 检查代码缩进为 4 个空格,未混入 Tab、Prettier 或其它 formatter 的结果。
## 7. Wrong vs Correct
@@ -261,6 +275,27 @@ export function parseFeatureInput(data: unknown): DomainModel[] {
}
```
+```jsonc
+// 正确:VSCode 保存与提交钩子使用同一 Oxfmt 配置。
+{
+ "oxc.fmt.configPath": "${workspaceFolder}/.oxfmtrc.json",
+ "[typescript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ }
+}
+```
+
+```jsonc
+// 错误:保存时使用未声明的其它 formatter。
+{
+ "[typescript]": {
+ "editor.defaultFormatter": "some.other-formatter",
+ "editor.formatOnSave": true
+ }
+}
+```
+
```ts
// 组装并启动当前目录的历史同步能力
import { fetchPage } from "./sdk.ts";
@@ -286,6 +321,7 @@ export function syncHistory(): void {
- `utils.ts` 是否仍然不含业务流程、重要类型和业务常量?
- 基础原语是否保持无业务语义,而不是通过业务化命名制造无行为差异的包装?
- 重要常量是否位于使用它的所有者或功能级 `constants.ts`?
+- 编辑器保存是否与 Oxfmt 结果一致,并且缩进是否统一为 4 个空格?
- 文件第一行是否有 10–30 个字符的职责注释,入口文件是否描述目录职责?
- 主函数是否有独立职责注释并位于所有依赖声明之后?
- 入口函数是否是最后一个函数声明,其后是否只有启动调用或显式导出?
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..9d5eb3d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["oxc.oxc-vscode"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..282d976
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,29 @@
+{
+ "oxc.fmt.configPath": "${workspaceFolder}/.oxfmtrc.json",
+ "oxc.path.oxfmt": "${workspaceFolder}/node_modules/.bin/oxfmt",
+ "editor.formatOnSaveMode": "file",
+ "[javascript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[javascriptreact]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[json]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[jsonc]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..77ee033
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# Trade Message Center
+
+## VSCode 开发环境
+
+本项目统一使用 Oxfmt 格式化代码,缩进为 4 个空格。请在 VSCode 中安装 Oxc 插件 `oxc.oxc-vscode`,否则保存文件时可能使用其它 formatter,导致代码在提交钩子中再次变化。
+
+安装方式:
+
+1. 打开 VSCode 扩展面板(macOS 快捷键:`⇧⌘X`)。
+2. 搜索 `Oxc`,安装扩展 `oxc.oxc-vscode`。
+3. 重新打开项目窗口,确认右下角或状态栏使用 Oxc formatter。
+
+仓库已经在 `.vscode/settings.json` 中配置 Oxc 保存格式化,并通过 `.oxfmtrc.json` 固定 4 空格规则。项目本地依赖已经包含 Oxfmt,不需要单独全局安装。
+
+如果暂时不安装插件,请关闭 VSCode 的 `formatOnSave`,需要格式化时在项目根目录执行:
+
+```bash
+pnpm format
+pnpm format:check
+```
+
+不要同时启用 Prettier、Biome 或 VSCode 内置 TypeScript formatter。
diff --git a/apps/chrome-extension/package.json b/apps/chrome-extension/package.json
index f65b1d4..cdbcb6c 100644
--- a/apps/chrome-extension/package.json
+++ b/apps/chrome-extension/package.json
@@ -1,17 +1,17 @@
{
- "name": "@trade-message-center/chrome-extension",
- "version": "0.5.0",
- "private": true,
- "type": "module",
- "scripts": {
- "dev": "vite build --watch --mode development",
- "build": "tsc -p tsconfig.json && vite build",
- "typecheck": "tsc -p tsconfig.json --noEmit",
- "test": "node --experimental-strip-types --test test/*.test.js"
- },
- "devDependencies": {
- "@types/node": "^22.10.2",
- "typescript": "^5.7.3",
- "vite": "^6.0.7"
- }
+ "name": "@trade-message-center/chrome-extension",
+ "version": "0.5.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite build --watch --mode development",
+ "build": "tsc -p tsconfig.json && vite build",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "node --experimental-strip-types --test test/*.test.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "typescript": "^5.7.3",
+ "vite": "^6.0.7"
+ }
}
diff --git a/apps/chrome-extension/popup/popup.html b/apps/chrome-extension/popup/popup.html
index c0b8554..909d1d1 100644
--- a/apps/chrome-extension/popup/popup.html
+++ b/apps/chrome-extension/popup/popup.html
@@ -1,37 +1,37 @@
-
-
-
- Trade Message Center
-
-
-
- Trade Message Center
- 正在读取当前页面…
-
-
-
+
+
+
+ Trade Message Center
+
+
+
+ Trade Message Center
+ 正在读取当前页面…
+
+
+
diff --git a/apps/chrome-extension/popup/popup.ts b/apps/chrome-extension/popup/popup.ts
index 9ca0e3d..64e9c4e 100644
--- a/apps/chrome-extension/popup/popup.ts
+++ b/apps/chrome-extension/popup/popup.ts
@@ -1,14 +1,14 @@
// 读取并展示当前浏览器标签页信息
interface ChromeTab {
- title?: string;
- url?: string;
+ title?: string;
+ url?: string;
}
interface ChromeApi {
- tabs: {
- query(queryInfo: { active: true; currentWindow: true }): Promise;
- };
+ tabs: {
+ query(queryInfo: { active: true; currentWindow: true }): Promise;
+ };
}
const chromeApi = (globalThis as unknown as { chrome: ChromeApi }).chrome;
@@ -21,14 +21,14 @@ const url = urlElement;
/** 查询当前活动标签页并更新弹窗文本。 */
async function renderCurrentTab(): Promise {
- try {
- const [tab] = await chromeApi.tabs.query({ active: true, currentWindow: true });
- title.textContent = tab?.title || "无法读取当前页面";
- url.textContent = tab?.url || "";
- } catch {
- title.textContent = "无法读取当前页面";
- url.textContent = "";
- }
+ try {
+ const [tab] = await chromeApi.tabs.query({ active: true, currentWindow: true });
+ title.textContent = tab?.title || "无法读取当前页面";
+ url.textContent = tab?.url || "";
+ } catch {
+ title.textContent = "无法读取当前页面";
+ url.textContent = "";
+ }
}
void renderCurrentTab();
diff --git a/apps/chrome-extension/public/manifest.json b/apps/chrome-extension/public/manifest.json
index 1ee317a..94916aa 100644
--- a/apps/chrome-extension/public/manifest.json
+++ b/apps/chrome-extension/public/manifest.json
@@ -1,29 +1,29 @@
{
- "manifest_version": 3,
- "name": "Trade Message Center",
- "version": "0.5.0",
- "description": "Sync authorized Alibaba, Made in China, and Global Sources conversations to TradeBridge.",
- "icons": {
- "16": "icons/icon-16.png",
- "32": "icons/icon-32.png",
- "48": "icons/icon-48.png",
- "128": "icons/icon-128.png"
- },
- "permissions": ["activeTab"],
- "content_scripts": [
- {
- "matches": ["https://onetalk.alibaba.com/*"],
- "js": ["onetalk/main-page/page-script.js"],
- "run_at": "document_start",
- "world": "MAIN"
- }
- ],
- "action": {
- "default_icon": {
- "16": "icons/icon-16.png",
- "32": "icons/icon-32.png",
- "48": "icons/icon-48.png"
+ "manifest_version": 3,
+ "name": "Trade Message Center",
+ "version": "0.5.0",
+ "description": "Sync authorized Alibaba, Made in China, and Global Sources conversations to TradeBridge.",
+ "icons": {
+ "16": "icons/icon-16.png",
+ "32": "icons/icon-32.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
},
- "default_popup": "popup/popup.html"
- }
+ "permissions": ["activeTab"],
+ "content_scripts": [
+ {
+ "matches": ["https://onetalk.alibaba.com/*"],
+ "js": ["onetalk/main-page/page-script.js"],
+ "run_at": "document_start",
+ "world": "MAIN"
+ }
+ ],
+ "action": {
+ "default_icon": {
+ "16": "icons/icon-16.png",
+ "32": "icons/icon-32.png",
+ "48": "icons/icon-48.png"
+ },
+ "default_popup": "popup/popup.html"
+ }
}
diff --git a/apps/chrome-extension/src/lib/error.ts b/apps/chrome-extension/src/lib/error.ts
index 61f27b2..1a1e0c3 100644
--- a/apps/chrome-extension/src/lib/error.ts
+++ b/apps/chrome-extension/src/lib/error.ts
@@ -2,5 +2,5 @@
/** 以错误消息终止当前控制流。 */
export function fail(message: string): never {
- throw new Error(message);
+ throw new Error(message);
}
diff --git a/apps/chrome-extension/src/lib/websocket-observer.ts b/apps/chrome-extension/src/lib/websocket-observer.ts
index e5b000a..299eb54 100644
--- a/apps/chrome-extension/src/lib/websocket-observer.ts
+++ b/apps/chrome-extension/src/lib/websocket-observer.ts
@@ -1,31 +1,31 @@
// 代理浏览器 WebSocket 并安全旁路消息
export interface WebSocketHost {
- WebSocket: typeof WebSocket;
+ WebSocket: typeof WebSocket;
}
/** 为匹配的 WebSocket 消息注册旁路观察回调。 */
export function observeNewWebSocketMessages(
- host: WebSocketHost,
- matches: (url: string) => boolean,
- observe: (data: unknown) => void,
+ host: WebSocketHost,
+ matches: (url: string) => boolean,
+ observe: (data: unknown) => void,
): void {
- const NativeWebSocket = host.WebSocket;
+ const NativeWebSocket = host.WebSocket;
- host.WebSocket = new Proxy(NativeWebSocket, {
- construct(target, args, newTarget) {
- const socket = Reflect.construct(target, args, newTarget) as WebSocket;
- const url = String(args[0]);
+ host.WebSocket = new Proxy(NativeWebSocket, {
+ construct(target, args, newTarget) {
+ const socket = Reflect.construct(target, args, newTarget) as WebSocket;
+ const url = String(args[0]);
- try {
- if (matches(url)) {
- socket.addEventListener("message", (event) => observe(event.data));
- }
- } catch {
- // Observation must never affect the page's WebSocket behavior.
- }
+ try {
+ if (matches(url)) {
+ socket.addEventListener("message", (event) => observe(event.data));
+ }
+ } catch {
+ // Observation must never affect the page's WebSocket behavior.
+ }
- return socket;
- },
- });
+ return socket;
+ },
+ });
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/entry.ts b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/entry.ts
index 8b80252..0ad5cb9 100644
--- a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/entry.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/entry.ts
@@ -6,8 +6,8 @@ import { syncCurrentConversationHistory } from "./index.ts";
/** 将当前会话历史同步能力注册到页面命名空间。 */
export function installCurrentConversationHistorySync(pageWindow: OneTalkPageWindow): void {
- const existing = pageWindow.__tradeMessageCenterOneTalk;
- const namespace: Record = isRecord(existing) ? existing : {};
- namespace.syncCurrentConversationHistory = () => syncCurrentConversationHistory(pageWindow);
- pageWindow.__tradeMessageCenterOneTalk = namespace;
+ const existing = pageWindow.__tradeMessageCenterOneTalk;
+ const namespace: Record = isRecord(existing) ? existing : {};
+ namespace.syncCurrentConversationHistory = () => syncCurrentConversationHistory(pageWindow);
+ pageWindow.__tradeMessageCenterOneTalk = namespace;
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/index.ts b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/index.ts
index 5621797..9032c83 100644
--- a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/index.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/index.ts
@@ -3,102 +3,102 @@
import { fail } from "../../../lib/error.ts";
import type { OneTalkPageWindow } from "../model.ts";
import {
- activeAccountId,
- assertActiveAccount,
- fetchMessagesWithoutUpdateToRead,
- findCurrentConversation,
- messageService,
- pageSdk,
+ activeAccountId,
+ assertActiveAccount,
+ fetchMessagesWithoutUpdateToRead,
+ findCurrentConversation,
+ messageService,
+ pageSdk,
} from "./sdk.ts";
import {
- historyErrorCodes,
- type CurrentConversationHistorySyncResult,
- type SyncDependencies,
+ historyErrorCodes,
+ type CurrentConversationHistorySyncResult,
+ type SyncDependencies,
} from "./model.ts";
const MIN_PAGE_DELAY_MS = 1_000;
const MAX_PAGE_DELAY_MS = 3_000;
const inFlightSyncs = new WeakMap<
- OneTalkPageWindow,
- Promise
+ OneTalkPageWindow,
+ Promise
>();
function pageDelay(random: () => number): number {
- const value = random();
- if (!Number.isFinite(value) || value < 0 || value > 1) {
- fail(historyErrorCodes.invalidRandomValue);
- }
- const range = MAX_PAGE_DELAY_MS - MIN_PAGE_DELAY_MS;
- return MIN_PAGE_DELAY_MS + Math.min(range, Math.floor(value * (range + 1)));
+ const value = random();
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
+ fail(historyErrorCodes.invalidRandomValue);
+ }
+ const range = MAX_PAGE_DELAY_MS - MIN_PAGE_DELAY_MS;
+ return MIN_PAGE_DELAY_MS + Math.min(range, Math.floor(value * (range + 1)));
}
async function runSync(
- pageWindow: OneTalkPageWindow,
- dependencies: SyncDependencies,
+ pageWindow: OneTalkPageWindow,
+ dependencies: SyncDependencies,
): Promise {
- const now = dependencies.now ?? Date.now;
- const random = dependencies.random ?? Math.random;
- const sleep =
- dependencies.sleep ??
- ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
+ const now = dependencies.now ?? Date.now;
+ const random = dependencies.random ?? Math.random;
+ const sleep =
+ dependencies.sleep ??
+ ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
- const accountId = activeAccountId(pageWindow);
- const firstTimeStamp = now();
- if (!Number.isFinite(firstTimeStamp)) fail(historyErrorCodes.invalidTime);
- const sdk = pageSdk(pageWindow);
- const conversation = await findCurrentConversation(pageWindow, sdk, accountId);
- assertActiveAccount(pageWindow, accountId);
- const service = messageService(sdk);
-
- const messageIds = new Set();
- let pages = 0;
- let timeStamp = firstTimeStamp;
- while (true) {
- assertActiveAccount(pageWindow, accountId);
- const page = await fetchMessagesWithoutUpdateToRead(service, conversation, timeStamp);
+ const accountId = activeAccountId(pageWindow);
+ const firstTimeStamp = now();
+ if (!Number.isFinite(firstTimeStamp)) fail(historyErrorCodes.invalidTime);
+ const sdk = pageSdk(pageWindow);
+ const conversation = await findCurrentConversation(pageWindow, sdk, accountId);
assertActiveAccount(pageWindow, accountId);
+ const service = messageService(sdk);
- let oldestTime = Number.POSITIVE_INFINITY;
- let newMessages = 0;
- for (const message of page.messages) {
- oldestTime = Math.min(oldestTime, message.sendTime);
- if (!messageIds.has(message.messageIdKey)) {
- messageIds.add(message.messageIdKey);
- newMessages += 1;
- }
- }
- if (newMessages === 0 || oldestTime >= timeStamp) {
- fail(historyErrorCodes.messageCursorStalled);
- }
- pages += 1;
- if (!page.hasMore) {
- return { exit: "history_exhausted", pages, uniqueMessages: messageIds.size };
- }
+ const messageIds = new Set();
+ let pages = 0;
+ let timeStamp = firstTimeStamp;
+ while (true) {
+ assertActiveAccount(pageWindow, accountId);
+ const page = await fetchMessagesWithoutUpdateToRead(service, conversation, timeStamp);
+ assertActiveAccount(pageWindow, accountId);
- timeStamp = oldestTime;
- try {
- await sleep(pageDelay(random));
- } catch {
- fail(historyErrorCodes.sleepFailed);
+ let oldestTime = Number.POSITIVE_INFINITY;
+ let newMessages = 0;
+ for (const message of page.messages) {
+ oldestTime = Math.min(oldestTime, message.sendTime);
+ if (!messageIds.has(message.messageIdKey)) {
+ messageIds.add(message.messageIdKey);
+ newMessages += 1;
+ }
+ }
+ if (newMessages === 0 || oldestTime >= timeStamp) {
+ fail(historyErrorCodes.messageCursorStalled);
+ }
+ pages += 1;
+ if (!page.hasMore) {
+ return { exit: "history_exhausted", pages, uniqueMessages: messageIds.size };
+ }
+
+ timeStamp = oldestTime;
+ try {
+ await sleep(pageDelay(random));
+ } catch {
+ fail(historyErrorCodes.sleepFailed);
+ }
}
- }
}
/** 完成当前会话历史同步并复用同页面进行中的请求。 */
export function syncCurrentConversationHistory(
- pageWindow: OneTalkPageWindow,
- dependencies: SyncDependencies = {},
+ pageWindow: OneTalkPageWindow,
+ dependencies: SyncDependencies = {},
): Promise {
- const existing = inFlightSyncs.get(pageWindow);
- if (existing) return existing;
+ const existing = inFlightSyncs.get(pageWindow);
+ if (existing) return existing;
- const sync = runSync(pageWindow, dependencies);
- inFlightSyncs.set(pageWindow, sync);
- const clear = (): void => {
- if (inFlightSyncs.get(pageWindow) === sync) inFlightSyncs.delete(pageWindow);
- };
- void sync.then(clear, clear);
- return sync;
+ const sync = runSync(pageWindow, dependencies);
+ inFlightSyncs.set(pageWindow, sync);
+ const clear = (): void => {
+ if (inFlightSyncs.get(pageWindow) === sync) inFlightSyncs.delete(pageWindow);
+ };
+ void sync.then(clear, clear);
+ return sync;
}
export type { CurrentConversationHistorySyncResult, SyncDependencies } from "./model.ts";
diff --git a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/model.ts b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/model.ts
index 7d72a0e..8db1d57 100644
--- a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/model.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/model.ts
@@ -5,143 +5,143 @@ import { isAccountId, type OneTalkAccountId } from "../model.ts";
import { isRecord } from "../utils.ts";
export const historyErrorCodes = {
- activeAccountChanged: "onetalk_history_active_account_changed",
- conversationCursorStalled: "onetalk_history_conversation_cursor_stalled",
- conversationRequestFailed: "onetalk_history_conversation_request_failed",
- currentConversationNotFound: "onetalk_history_current_conversation_not_found",
- invalidActiveAccount: "onetalk_history_invalid_active_account",
- invalidConversationPage: "onetalk_history_invalid_conversation_page",
- invalidMessagePage: "onetalk_history_invalid_message_page",
- invalidRandomValue: "onetalk_history_invalid_random_value",
- invalidTime: "onetalk_history_invalid_time",
- messageCursorStalled: "onetalk_history_message_cursor_stalled",
- messageRequestFailed: "onetalk_history_message_request_failed",
- sdkUnavailable: "onetalk_history_sdk_unavailable",
- sleepFailed: "onetalk_history_sleep_failed",
+ activeAccountChanged: "onetalk_history_active_account_changed",
+ conversationCursorStalled: "onetalk_history_conversation_cursor_stalled",
+ conversationRequestFailed: "onetalk_history_conversation_request_failed",
+ currentConversationNotFound: "onetalk_history_current_conversation_not_found",
+ invalidActiveAccount: "onetalk_history_invalid_active_account",
+ invalidConversationPage: "onetalk_history_invalid_conversation_page",
+ invalidMessagePage: "onetalk_history_invalid_message_page",
+ invalidRandomValue: "onetalk_history_invalid_random_value",
+ invalidTime: "onetalk_history_invalid_time",
+ messageCursorStalled: "onetalk_history_message_cursor_stalled",
+ messageRequestFailed: "onetalk_history_message_request_failed",
+ sdkUnavailable: "onetalk_history_sdk_unavailable",
+ sleepFailed: "onetalk_history_sleep_failed",
} as const;
export type HistoryErrorCode = (typeof historyErrorCodes)[keyof typeof historyErrorCodes];
export type Cursor = string | number;
export type Conversation = Record & {
- cid: string;
- accountId: OneTalkAccountId;
- accountIdEncrypt: string;
- aliId: string;
- aliIdEncrypt?: string;
+ cid: string;
+ accountId: OneTalkAccountId;
+ accountIdEncrypt: string;
+ aliId: string;
+ aliIdEncrypt?: string;
};
export type ConversationPage = {
- list: unknown[];
- hasMore: boolean;
- nextCursor?: unknown;
+ list: unknown[];
+ hasMore: boolean;
+ nextCursor?: unknown;
};
export type HistoryMessage = {
- messageIdKey: string;
- sendTime: number;
+ messageIdKey: string;
+ sendTime: number;
};
export type HistoryMessagePage = {
- messages: HistoryMessage[];
- hasMore: boolean;
+ messages: HistoryMessage[];
+ hasMore: boolean;
};
export type SyncDependencies = {
- now?: () => number;
- random?: () => number;
- sleep?: (milliseconds: number) => Promise;
+ now?: () => number;
+ random?: () => number;
+ sleep?: (milliseconds: number) => Promise;
};
export type CurrentConversationHistorySyncResult = {
- exit: "history_exhausted";
- pages: number;
- uniqueMessages: number;
+ exit: "history_exhausted";
+ pages: number;
+ uniqueMessages: number;
};
export function normalizeActiveAccountId(value: string | null): string {
- return value || fail(historyErrorCodes.invalidActiveAccount);
+ return value || fail(historyErrorCodes.invalidActiveAccount);
}
export function isCursor(value: unknown): value is Cursor {
- return (
- (typeof value === "string" && value.length > 0) ||
- (typeof value === "number" && Number.isFinite(value))
- );
+ return (
+ (typeof value === "string" && value.length > 0) ||
+ (typeof value === "number" && Number.isFinite(value))
+ );
}
export function normalizePageSdk(value: unknown): Record {
- if (!isRecord(value) || !isRecord(value.IMBaaSSDK) || !isRecord(value.IMBaaSSDK.default)) {
- fail(historyErrorCodes.sdkUnavailable);
- }
- return value.IMBaaSSDK.default;
+ if (!isRecord(value) || !isRecord(value.IMBaaSSDK) || !isRecord(value.IMBaaSSDK.default)) {
+ fail(historyErrorCodes.sdkUnavailable);
+ }
+ return value.IMBaaSSDK.default;
}
export function normalizeSdkService(value: unknown): Record {
- return isRecord(value) ? value : fail(historyErrorCodes.sdkUnavailable);
+ return isRecord(value) ? value : fail(historyErrorCodes.sdkUnavailable);
}
function normalizeConversation(value: unknown, accountId: string): Conversation | null {
- if (!isRecord(value)) fail(historyErrorCodes.invalidConversationPage);
- if (!isAccountId(value.accountId) || String(value.accountId) !== accountId) return null;
- if (
- typeof value.cid !== "string" ||
- value.cid.length === 0 ||
- typeof value.accountIdEncrypt !== "string" ||
- value.accountIdEncrypt.length === 0 ||
- typeof value.aliId !== "string" ||
- value.aliId.length === 0 ||
- (value.aliIdEncrypt !== undefined &&
- (typeof value.aliIdEncrypt !== "string" || value.aliIdEncrypt.length === 0))
- ) {
- fail(historyErrorCodes.invalidConversationPage);
- }
- return value as Conversation;
+ if (!isRecord(value)) fail(historyErrorCodes.invalidConversationPage);
+ if (!isAccountId(value.accountId) || String(value.accountId) !== accountId) return null;
+ if (
+ typeof value.cid !== "string" ||
+ value.cid.length === 0 ||
+ typeof value.accountIdEncrypt !== "string" ||
+ value.accountIdEncrypt.length === 0 ||
+ typeof value.aliId !== "string" ||
+ value.aliId.length === 0 ||
+ (value.aliIdEncrypt !== undefined &&
+ (typeof value.aliIdEncrypt !== "string" || value.aliIdEncrypt.length === 0))
+ ) {
+ fail(historyErrorCodes.invalidConversationPage);
+ }
+ return value as Conversation;
}
export function normalizeConversationPage(value: unknown): ConversationPage {
- if (!isRecord(value) || !Array.isArray(value.list) || typeof value.hasMore !== "boolean") {
- fail(historyErrorCodes.invalidConversationPage);
- }
- return { list: value.list, hasMore: value.hasMore, nextCursor: value.nextCursor };
+ if (!isRecord(value) || !Array.isArray(value.list) || typeof value.hasMore !== "boolean") {
+ fail(historyErrorCodes.invalidConversationPage);
+ }
+ return { list: value.list, hasMore: value.hasMore, nextCursor: value.nextCursor };
}
export function findMatchingConversation(
- values: unknown[],
- accountId: string,
+ values: unknown[],
+ accountId: string,
): Conversation | null {
- for (const value of values) {
- const conversation = normalizeConversation(value, accountId);
- if (conversation) return conversation;
- }
- return null;
+ for (const value of values) {
+ const conversation = normalizeConversation(value, accountId);
+ if (conversation) return conversation;
+ }
+ return null;
}
function normalizeHistoryMessage(value: unknown): HistoryMessage {
- if (!isRecord(value)) fail(historyErrorCodes.invalidMessagePage);
- const messageId = value.messageId;
- const sendTime = value.sendTime;
- if (
- !(
- (typeof messageId === "string" && messageId.length > 0) ||
- (typeof messageId === "number" && Number.isFinite(messageId))
- ) ||
- typeof sendTime !== "number" ||
- !Number.isFinite(sendTime)
- ) {
- fail(historyErrorCodes.invalidMessagePage);
- }
- return { messageIdKey: String(messageId), sendTime };
+ if (!isRecord(value)) fail(historyErrorCodes.invalidMessagePage);
+ const messageId = value.messageId;
+ const sendTime = value.sendTime;
+ if (
+ !(
+ (typeof messageId === "string" && messageId.length > 0) ||
+ (typeof messageId === "number" && Number.isFinite(messageId))
+ ) ||
+ typeof sendTime !== "number" ||
+ !Number.isFinite(sendTime)
+ ) {
+ fail(historyErrorCodes.invalidMessagePage);
+ }
+ return { messageIdKey: String(messageId), sendTime };
}
export function normalizeHistoryMessagePage(value: unknown): HistoryMessagePage {
- if (
- !isRecord(value) ||
- !Array.isArray(value.list) ||
- value.list.length === 0 ||
- typeof value.hasMore !== "boolean"
- ) {
- fail(historyErrorCodes.invalidMessagePage);
- }
- return { messages: value.list.map(normalizeHistoryMessage), hasMore: value.hasMore };
+ if (
+ !isRecord(value) ||
+ !Array.isArray(value.list) ||
+ value.list.length === 0 ||
+ typeof value.hasMore !== "boolean"
+ ) {
+ fail(historyErrorCodes.invalidMessagePage);
+ }
+ return { messages: value.list.map(normalizeHistoryMessage), hasMore: value.hasMore };
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts
index 02441be..bd5200b 100644
--- a/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts
@@ -2,131 +2,131 @@
import { fail } from "../../../lib/error.ts";
import {
- findMatchingConversation,
- historyErrorCodes,
- isCursor,
- normalizeActiveAccountId,
- normalizeConversationPage,
- normalizeHistoryMessagePage,
- normalizePageSdk,
- normalizeSdkService,
- type Conversation,
- type Cursor,
- type HistoryMessagePage,
+ findMatchingConversation,
+ historyErrorCodes,
+ isCursor,
+ normalizeActiveAccountId,
+ normalizeConversationPage,
+ normalizeHistoryMessagePage,
+ normalizePageSdk,
+ normalizeSdkService,
+ type Conversation,
+ type Cursor,
+ type HistoryMessagePage,
} from "./model.ts";
import type { OneTalkPageWindow } from "../model.ts";
const HISTORY_PAGE_SIZE = 20;
function conversationCursorKey(cursor: Cursor): string {
- return `${typeof cursor}:${cursor}`;
+ return `${typeof cursor}:${cursor}`;
}
export function activeAccountId(pageWindow: OneTalkPageWindow): string {
- try {
- return normalizeActiveAccountId(
- new URL(pageWindow.location.href).searchParams.get("activeAccountId"),
- );
- } catch (error) {
- if (error instanceof Error && error.message === historyErrorCodes.invalidActiveAccount) {
- throw error;
+ try {
+ return normalizeActiveAccountId(
+ new URL(pageWindow.location.href).searchParams.get("activeAccountId"),
+ );
+ } catch (error) {
+ if (error instanceof Error && error.message === historyErrorCodes.invalidActiveAccount) {
+ throw error;
+ }
+ return fail(historyErrorCodes.invalidActiveAccount);
}
- return fail(historyErrorCodes.invalidActiveAccount);
- }
}
export function assertActiveAccount(pageWindow: OneTalkPageWindow, expected: string): void {
- if (activeAccountId(pageWindow) !== expected) {
- fail(historyErrorCodes.activeAccountChanged);
- }
+ if (activeAccountId(pageWindow) !== expected) {
+ fail(historyErrorCodes.activeAccountChanged);
+ }
}
export function pageSdk(pageWindow: OneTalkPageWindow): Record {
- return normalizePageSdk(pageWindow.IcbuIM);
+ return normalizePageSdk(pageWindow.IcbuIM);
}
export function messageService(sdk: Record): Record {
- const getMessageService = sdk.getMessageService;
- if (typeof getMessageService !== "function") {
- fail(historyErrorCodes.sdkUnavailable);
- }
+ const getMessageService = sdk.getMessageService;
+ if (typeof getMessageService !== "function") {
+ fail(historyErrorCodes.sdkUnavailable);
+ }
- try {
- return normalizeSdkService(getMessageService.call(sdk));
- } catch {
- return fail(historyErrorCodes.sdkUnavailable);
- }
+ try {
+ return normalizeSdkService(getMessageService.call(sdk));
+ } catch {
+ return fail(historyErrorCodes.sdkUnavailable);
+ }
}
export async function fetchMessagesWithoutUpdateToRead(
- service: Record,
- conversation: Conversation,
- timeStamp: number,
+ service: Record,
+ conversation: Conversation,
+ timeStamp: number,
): Promise {
- const fetchPage = service.fetchMessagesWithoutUpdateToRead;
- if (typeof fetchPage !== "function") fail(historyErrorCodes.sdkUnavailable);
+ const fetchPage = service.fetchMessagesWithoutUpdateToRead;
+ if (typeof fetchPage !== "function") fail(historyErrorCodes.sdkUnavailable);
- let response: unknown;
- try {
- const aliIdEncrypt = conversation.aliIdEncrypt;
- response = await fetchPage.call(
- service,
- {
- contactAccountId: conversation.accountId,
- contactAccountIdEncrypt: conversation.accountIdEncrypt,
- aliId: conversation.aliId,
- ...(aliIdEncrypt === undefined ? {} : { aliIdEncrypt }),
- searchMessageId: "",
- timeSlide: { forward: false, timeStamp, pageSize: HISTORY_PAGE_SIZE },
- },
- conversation,
- );
- } catch {
- return fail(historyErrorCodes.messageRequestFailed);
- }
- return normalizeHistoryMessagePage(response);
+ let response: unknown;
+ try {
+ const aliIdEncrypt = conversation.aliIdEncrypt;
+ response = await fetchPage.call(
+ service,
+ {
+ contactAccountId: conversation.accountId,
+ contactAccountIdEncrypt: conversation.accountIdEncrypt,
+ aliId: conversation.aliId,
+ ...(aliIdEncrypt === undefined ? {} : { aliIdEncrypt }),
+ searchMessageId: "",
+ timeSlide: { forward: false, timeStamp, pageSize: HISTORY_PAGE_SIZE },
+ },
+ conversation,
+ );
+ } catch {
+ return fail(historyErrorCodes.messageRequestFailed);
+ }
+ return normalizeHistoryMessagePage(response);
}
/** 扫描会话分页并返回与当前账号匹配的会话。 */
export async function findCurrentConversation(
- pageWindow: OneTalkPageWindow,
- sdk: Record,
- accountId: string,
+ pageWindow: OneTalkPageWindow,
+ sdk: Record,
+ accountId: string,
): Promise {
- const getService = sdk.getConversationServiceV2;
- if (typeof getService !== "function") fail(historyErrorCodes.sdkUnavailable);
+ const getService = sdk.getConversationServiceV2;
+ if (typeof getService !== "function") fail(historyErrorCodes.sdkUnavailable);
- let service: Record;
- try {
- service = normalizeSdkService(getService.call(sdk));
- } catch {
- return fail(historyErrorCodes.sdkUnavailable);
- }
- const getPage = service.getConversationListByPagination;
- if (typeof getPage !== "function") fail(historyErrorCodes.sdkUnavailable);
-
- let cursor: Cursor = 0;
- const seenCursors = new Set();
- while (true) {
- assertActiveAccount(pageWindow, accountId);
- const key = conversationCursorKey(cursor);
- if (seenCursors.has(key)) fail(historyErrorCodes.conversationCursorStalled);
- seenCursors.add(key);
-
- let response: unknown;
+ let service: Record;
try {
- response = await getPage.call(service, { cursor, count: HISTORY_PAGE_SIZE });
+ service = normalizeSdkService(getService.call(sdk));
} catch {
- return fail(historyErrorCodes.conversationRequestFailed);
+ return fail(historyErrorCodes.sdkUnavailable);
}
- assertActiveAccount(pageWindow, accountId);
- const page = normalizeConversationPage(response);
- const conversation = findMatchingConversation(page.list, accountId);
- if (conversation) return conversation;
- if (!page.hasMore) fail(historyErrorCodes.currentConversationNotFound);
- if (!isCursor(page.nextCursor)) {
- fail(historyErrorCodes.conversationCursorStalled);
+ const getPage = service.getConversationListByPagination;
+ if (typeof getPage !== "function") fail(historyErrorCodes.sdkUnavailable);
+
+ let cursor: Cursor = 0;
+ const seenCursors = new Set();
+ while (true) {
+ assertActiveAccount(pageWindow, accountId);
+ const key = conversationCursorKey(cursor);
+ if (seenCursors.has(key)) fail(historyErrorCodes.conversationCursorStalled);
+ seenCursors.add(key);
+
+ let response: unknown;
+ try {
+ response = await getPage.call(service, { cursor, count: HISTORY_PAGE_SIZE });
+ } catch {
+ return fail(historyErrorCodes.conversationRequestFailed);
+ }
+ assertActiveAccount(pageWindow, accountId);
+ const page = normalizeConversationPage(response);
+ const conversation = findMatchingConversation(page.list, accountId);
+ if (conversation) return conversation;
+ if (!page.hasMore) fail(historyErrorCodes.currentConversationNotFound);
+ if (!isCursor(page.nextCursor)) {
+ fail(historyErrorCodes.conversationCursorStalled);
+ }
+ cursor = page.nextCursor;
}
- cursor = page.nextCursor;
- }
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/entry.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/entry.ts
index c876dd1..a70fa6c 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/entry.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/entry.ts
@@ -4,5 +4,5 @@ import { installOneTalkWebSocketTap, type OneTalkWebSocketPageWindow } from "./w
/** 组合并安装 OneTalk 消息观察入口。 */
export function installOneTalkMessageObserver(pageWindow: OneTalkWebSocketPageWindow): void {
- installOneTalkWebSocketTap(pageWindow);
+ installOneTalkWebSocketTap(pageWindow);
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts
index d249137..d758085 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts
@@ -5,18 +5,18 @@ import { conversationParticipants, observedMessage, type ObservedOneTalkMessage
/** 将历史消息响应条目转换为可观察消息。 */
export function parseHistoryMessages(items: unknown[]): ObservedOneTalkMessage[] {
- return items.flatMap((item) => {
- if (!isRecord(item) || !isRecord(item.message)) return [];
- const participantIds = conversationParticipants(item.message.cid);
- if (!participantIds) return [];
+ return items.flatMap((item) => {
+ if (!isRecord(item) || !isRecord(item.message)) return [];
+ const participantIds = conversationParticipants(item.message.cid);
+ if (!participantIds) return [];
- const message = observedMessage(
- item.message,
- participantIds,
- item.readStatus,
- item.msgStatus,
- "history",
- );
- return message ? [message] : [];
- });
+ const message = observedMessage(
+ item.message,
+ participantIds,
+ item.readStatus,
+ item.msgStatus,
+ "history",
+ );
+ return message ? [message] : [];
+ });
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts
index d3c74d6..1cd78a1 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts
@@ -7,20 +7,20 @@ import type { ObservedOneTalkMessage } from "./model.ts";
/** 识别响应帧并委派到历史或新消息解析器。 */
export function parseOneTalkMessages(data: unknown): ObservedOneTalkMessage[] {
- if (typeof data !== "string") return [];
+ if (typeof data !== "string") return [];
- let frame: unknown;
- try {
- frame = JSON.parse(data);
- } catch {
- return [];
- }
+ let frame: unknown;
+ try {
+ frame = JSON.parse(data);
+ } catch {
+ return [];
+ }
- if (!isRecord(frame) || frame.code !== 200) return [];
+ if (!isRecord(frame) || frame.code !== 200) return [];
- if (isRecord(frame.body) && Array.isArray(frame.body.userMessageModels)) {
- return parseHistoryMessages(frame.body.userMessageModels);
- }
+ if (isRecord(frame.body) && Array.isArray(frame.body.userMessageModels)) {
+ return parseHistoryMessages(frame.body.userMessageModels);
+ }
- return Array.isArray(frame.body) ? parseNewMessages(frame.body) : [];
+ return Array.isArray(frame.body) ? parseNewMessages(frame.body) : [];
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts
index 0682c92..564ff7c 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts
@@ -3,71 +3,71 @@
import { isRecord } from "../utils.ts";
export interface ObservedOneTalkMessage {
- messageType: "new" | "history";
- conversationId: string;
- messageId: string;
- sentAt: number;
- contentType: number;
- text: string | null;
- senderId: string;
- participantIds: [string, string];
- direction: "sent" | "received";
- readStatus: number;
- messageStatus: number;
- unreadCount: number;
+ messageType: "new" | "history";
+ conversationId: string;
+ messageId: string;
+ sentAt: number;
+ contentType: number;
+ text: string | null;
+ senderId: string;
+ participantIds: [string, string];
+ direction: "sent" | "received";
+ readStatus: number;
+ messageStatus: number;
+ unreadCount: number;
}
export function conversationParticipants(value: unknown): [string, string] | null {
- if (typeof value !== "string") return null;
- const match = /^([^-]+)-([^#]+)#[^@]+@(.+)$/.exec(value);
- return match ? [`${match[1]}@${match[3]}`, `${match[2]}@${match[3]}`] : null;
+ if (typeof value !== "string") return null;
+ const match = /^([^-]+)-([^#]+)#[^@]+@(.+)$/.exec(value);
+ return match ? [`${match[1]}@${match[3]}`, `${match[2]}@${match[3]}`] : null;
}
/** 校验消息字段并生成统一的观察消息模型。 */
export function observedMessage(
- message: Record,
- participantIds: [string, string],
- readStatus: unknown,
- messageStatus: unknown,
- messageType: ObservedOneTalkMessage["messageType"],
+ message: Record,
+ participantIds: [string, string],
+ readStatus: unknown,
+ messageStatus: unknown,
+ messageType: ObservedOneTalkMessage["messageType"],
): ObservedOneTalkMessage | null {
- if (!isRecord(message.content) || !isRecord(message.sender)) return null;
- const text = message.content.text;
+ if (!isRecord(message.content) || !isRecord(message.sender)) return null;
+ const text = message.content.text;
- const conversationId = message.cid;
- const messageId = message.messageId;
- const sentAt = message.createAt;
- const contentType = message.content.contentType;
- const content = isRecord(text) && typeof text.content === "string" ? text.content : null;
- const senderId = message.sender.uid;
- const unreadCount = message.unreadCount;
+ const conversationId = message.cid;
+ const messageId = message.messageId;
+ const sentAt = message.createAt;
+ const contentType = message.content.contentType;
+ const content = isRecord(text) && typeof text.content === "string" ? text.content : null;
+ const senderId = message.sender.uid;
+ const unreadCount = message.unreadCount;
- if (
- typeof conversationId !== "string" ||
- typeof messageId !== "string" ||
- typeof sentAt !== "number" ||
- typeof contentType !== "number" ||
- typeof senderId !== "string" ||
- !participantIds.includes(senderId) ||
- typeof readStatus !== "number" ||
- typeof messageStatus !== "number" ||
- typeof unreadCount !== "number"
- ) {
- return null;
- }
+ if (
+ typeof conversationId !== "string" ||
+ typeof messageId !== "string" ||
+ typeof sentAt !== "number" ||
+ typeof contentType !== "number" ||
+ typeof senderId !== "string" ||
+ !participantIds.includes(senderId) ||
+ typeof readStatus !== "number" ||
+ typeof messageStatus !== "number" ||
+ typeof unreadCount !== "number"
+ ) {
+ return null;
+ }
- return {
- messageType,
- conversationId,
- messageId,
- sentAt,
- contentType,
- text: content,
- senderId,
- participantIds,
- direction: senderId === participantIds[1] ? "sent" : "received",
- readStatus,
- messageStatus,
- unreadCount,
- };
+ return {
+ messageType,
+ conversationId,
+ messageId,
+ sentAt,
+ contentType,
+ text: content,
+ senderId,
+ participantIds,
+ direction: senderId === participantIds[1] ? "sent" : "received",
+ readStatus,
+ messageStatus,
+ unreadCount,
+ };
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts
index c7c585d..1f6c628 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts
@@ -4,37 +4,37 @@ import { isRecord } from "../utils.ts";
import { observedMessage, type ObservedOneTalkMessage } from "./model.ts";
function parseNewMessage(conversation: Record): ObservedOneTalkMessage | null {
- const lastMessage = conversation.lastMessage;
- const singleChatConversation = conversation.singleChatConversation;
- if (!isRecord(lastMessage) || !isRecord(singleChatConversation)) return null;
+ const lastMessage = conversation.lastMessage;
+ const singleChatConversation = conversation.singleChatConversation;
+ if (!isRecord(lastMessage) || !isRecord(singleChatConversation)) return null;
- const pairFirst = singleChatConversation.pairFirst;
- const pairSecond = singleChatConversation.pairSecond;
+ const pairFirst = singleChatConversation.pairFirst;
+ const pairSecond = singleChatConversation.pairSecond;
- if (
- typeof pairFirst !== "string" ||
- typeof pairSecond !== "string" ||
- !isRecord(lastMessage.message)
- ) {
- return null;
- }
+ if (
+ typeof pairFirst !== "string" ||
+ typeof pairSecond !== "string" ||
+ !isRecord(lastMessage.message)
+ ) {
+ return null;
+ }
- return observedMessage(
- lastMessage.message,
- [pairFirst, pairSecond],
- lastMessage.readStatus,
- lastMessage.msgStatus,
- "new",
- );
+ return observedMessage(
+ lastMessage.message,
+ [pairFirst, pairSecond],
+ lastMessage.readStatus,
+ lastMessage.msgStatus,
+ "new",
+ );
}
/** 将新消息响应条目转换为可观察消息。 */
export function parseNewMessages(items: unknown[]): ObservedOneTalkMessage[] {
- const messages: ObservedOneTalkMessage[] = [];
- for (const item of items) {
- if (!isRecord(item) || !isRecord(item.singleChatUserConversation)) continue;
- const message = parseNewMessage(item.singleChatUserConversation);
- if (message) messages.push(message);
- }
- return messages;
+ const messages: ObservedOneTalkMessage[] = [];
+ for (const item of items) {
+ if (!isRecord(item) || !isRecord(item.singleChatUserConversation)) continue;
+ const message = parseNewMessage(item.singleChatUserConversation);
+ if (message) messages.push(message);
+ }
+ return messages;
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts b/apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts
index 528e186..d4e7fbd 100644
--- a/apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts
@@ -1,8 +1,8 @@
// 接入 OneTalk WebSocket 并旁路记录消息
import {
- observeNewWebSocketMessages,
- type WebSocketHost,
+ observeNewWebSocketMessages,
+ type WebSocketHost,
} from "../../../lib/websocket-observer.ts";
import type { OneTalkPageWindow } from "../model.ts";
import { parseOneTalkMessages } from "./index.ts";
@@ -12,39 +12,39 @@ const LOG_PREFIX = "[Trade Message Center][OneTalk WebSocket]";
const ONETALK_WEBSOCKET_HOST = "wss-icbu.dingtalk.com";
export interface OneTalkWebSocketPageWindow extends OneTalkPageWindow, WebSocketHost {
- [INSTALL_KEY]?: boolean;
- console: Pick;
+ [INSTALL_KEY]?: boolean;
+ console: Pick;
}
function isWebSocketUrlForHost(url: string, hostname: string): boolean {
- try {
- const parsed = new URL(url);
- return parsed.protocol === "wss:" && parsed.hostname === hostname;
- } catch {
- return false;
- }
+ try {
+ const parsed = new URL(url);
+ return parsed.protocol === "wss:" && parsed.hostname === hostname;
+ } catch {
+ return false;
+ }
}
/** 安装只观察指定 OneTalk 主机的 WebSocket 旁路。 */
export function installOneTalkWebSocketTap(pageWindow: OneTalkWebSocketPageWindow): void {
- if (pageWindow[INSTALL_KEY]) return;
- pageWindow[INSTALL_KEY] = true;
+ if (pageWindow[INSTALL_KEY]) return;
+ pageWindow[INSTALL_KEY] = true;
- observeNewWebSocketMessages(
- pageWindow,
- (url) => isWebSocketUrlForHost(url, ONETALK_WEBSOCKET_HOST),
- (data) => {
- try {
- pageWindow.console.log(LOG_PREFIX, data);
- for (const message of parseOneTalkMessages(data)) {
- pageWindow.console.log(
- `[Trade Message Center][OneTalk ${message.messageType} message]`,
- message,
- );
- }
- } catch {
- // Logging must never affect the page's WebSocket behavior.
- }
- },
- );
+ observeNewWebSocketMessages(
+ pageWindow,
+ (url) => isWebSocketUrlForHost(url, ONETALK_WEBSOCKET_HOST),
+ (data) => {
+ try {
+ pageWindow.console.log(LOG_PREFIX, data);
+ for (const message of parseOneTalkMessages(data)) {
+ pageWindow.console.log(
+ `[Trade Message Center][OneTalk ${message.messageType} message]`,
+ message,
+ );
+ }
+ } catch {
+ // Logging must never affect the page's WebSocket behavior.
+ }
+ },
+ );
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/model.ts b/apps/chrome-extension/src/onetalk/main-page/model.ts
index caceb03..0409d8a 100644
--- a/apps/chrome-extension/src/onetalk/main-page/model.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/model.ts
@@ -3,15 +3,15 @@
export type OneTalkAccountId = string | number;
export type OneTalkPageWindow = {
- location: Pick;
- IcbuIM?: unknown;
- __tradeMessageCenterOneTalk?: unknown;
+ location: Pick;
+ IcbuIM?: unknown;
+ __tradeMessageCenterOneTalk?: unknown;
};
/** 判断值是否符合 OneTalk 账号标识形状。 */
export function isAccountId(value: unknown): value is OneTalkAccountId {
- return (
- (typeof value === "string" && value.length > 0) ||
- (typeof value === "number" && Number.isFinite(value))
- );
+ return (
+ (typeof value === "string" && value.length > 0) ||
+ (typeof value === "number" && Number.isFinite(value))
+ );
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts b/apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts
index a73452e..4da8505 100644
--- a/apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts
@@ -5,8 +5,8 @@ import { installOneTalkMessageObserver } from "./message-observer/entry.ts";
/** 启动 main-page 目录下的全部 OneTalk 页面能力。 */
function installOneTalkPageFeatures(): void {
- installOneTalkMessageObserver(window);
- installCurrentConversationHistorySync(window);
+ installOneTalkMessageObserver(window);
+ installCurrentConversationHistorySync(window);
}
installOneTalkPageFeatures();
diff --git a/apps/chrome-extension/src/onetalk/main-page/sync-push-decoder.ts b/apps/chrome-extension/src/onetalk/main-page/sync-push-decoder.ts
index 958aef1..b58b49b 100644
--- a/apps/chrome-extension/src/onetalk/main-page/sync-push-decoder.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/sync-push-decoder.ts
@@ -1,203 +1,203 @@
// 解码 OneTalk 的同步推送数据
export type MessagePackValue =
- | null
- | boolean
- | number
- | bigint
- | string
- | Uint8Array
- | MessagePackValue[]
- | { [key: string]: MessagePackValue };
+ | null
+ | boolean
+ | number
+ | bigint
+ | string
+ | Uint8Array
+ | MessagePackValue[]
+ | { [key: string]: MessagePackValue };
class MessagePackDecoder {
- private readonly bytes: Uint8Array;
- private readonly view: DataView;
- private readonly textDecoder = new TextDecoder("utf-8", { fatal: true });
- private offset = 0;
+ private readonly bytes: Uint8Array;
+ private readonly view: DataView;
+ private readonly textDecoder = new TextDecoder("utf-8", { fatal: true });
+ private offset = 0;
- constructor(bytes: Uint8Array) {
- this.bytes = bytes;
- this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
- }
-
- get done(): boolean {
- return this.offset === this.bytes.byteLength;
- }
-
- private decodeArray(length: number): MessagePackValue[] {
- return Array.from({ length }, () => this.decode());
- }
-
- private decodeMap(length: number): { [key: string]: MessagePackValue } {
- const result: { [key: string]: MessagePackValue } = {};
-
- for (let index = 0; index < length; index += 1) {
- const key = this.decode();
- if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint") {
- throw new Error("Unsupported MessagePack map key");
- }
- result[String(key)] = this.decode();
+ constructor(bytes: Uint8Array) {
+ this.bytes = bytes;
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
- return result;
- }
-
- private decodeString(length: number): string {
- return this.textDecoder.decode(this.readBytes(length));
- }
-
- private readBytes(length: number): Uint8Array {
- this.ensureAvailable(length);
- const start = this.offset;
- this.offset += length;
- return this.bytes.subarray(start, this.offset);
- }
-
- private readUint8(): number {
- this.ensureAvailable(1);
- return this.view.getUint8(this.offset++);
- }
-
- private readInt8(): number {
- this.ensureAvailable(1);
- const value = this.view.getInt8(this.offset);
- this.offset += 1;
- return value;
- }
-
- private readUint16(): number {
- this.ensureAvailable(2);
- const value = this.view.getUint16(this.offset);
- this.offset += 2;
- return value;
- }
-
- private readInt16(): number {
- this.ensureAvailable(2);
- const value = this.view.getInt16(this.offset);
- this.offset += 2;
- return value;
- }
-
- private readUint32(): number {
- this.ensureAvailable(4);
- const value = this.view.getUint32(this.offset);
- this.offset += 4;
- return value;
- }
-
- private readInt32(): number {
- this.ensureAvailable(4);
- const value = this.view.getInt32(this.offset);
- this.offset += 4;
- return value;
- }
-
- private readBigUint64(): bigint {
- this.ensureAvailable(8);
- const value = this.view.getBigUint64(this.offset);
- this.offset += 8;
- return value;
- }
-
- private readBigInt64(): bigint {
- this.ensureAvailable(8);
- const value = this.view.getBigInt64(this.offset);
- this.offset += 8;
- return value;
- }
-
- private readFloat32(): number {
- this.ensureAvailable(4);
- const value = this.view.getFloat32(this.offset);
- this.offset += 4;
- return value;
- }
-
- private readFloat64(): number {
- this.ensureAvailable(8);
- const value = this.view.getFloat64(this.offset);
- this.offset += 8;
- return value;
- }
-
- private ensureAvailable(length: number): void {
- if (length < 0 || this.offset + length > this.bytes.byteLength) {
- throw new Error("Unexpected end of MessagePack data");
+ get done(): boolean {
+ return this.offset === this.bytes.byteLength;
}
- }
- /** 解码一个完整的 MessagePack 值。 */
- decode(): MessagePackValue {
- const prefix = this.readUint8();
-
- if (prefix <= 0x7f) return prefix;
- if (prefix >= 0xe0) return prefix - 0x100;
- if ((prefix & 0xf0) === 0x80) return this.decodeMap(prefix & 0x0f);
- if ((prefix & 0xf0) === 0x90) return this.decodeArray(prefix & 0x0f);
- if ((prefix & 0xe0) === 0xa0) return this.decodeString(prefix & 0x1f);
-
- switch (prefix) {
- case 0xc0:
- return null;
- case 0xc2:
- return false;
- case 0xc3:
- return true;
- case 0xc4:
- return this.readBytes(this.readUint8());
- case 0xc5:
- return this.readBytes(this.readUint16());
- case 0xc6:
- return this.readBytes(this.readUint32());
- case 0xca:
- return this.readFloat32();
- case 0xcb:
- return this.readFloat64();
- case 0xcc:
- return this.readUint8();
- case 0xcd:
- return this.readUint16();
- case 0xce:
- return this.readUint32();
- case 0xcf:
- return this.readBigUint64();
- case 0xd0:
- return this.readInt8();
- case 0xd1:
- return this.readInt16();
- case 0xd2:
- return this.readInt32();
- case 0xd3:
- return this.readBigInt64();
- case 0xd9:
- return this.decodeString(this.readUint8());
- case 0xda:
- return this.decodeString(this.readUint16());
- case 0xdb:
- return this.decodeString(this.readUint32());
- case 0xdc:
- return this.decodeArray(this.readUint16());
- case 0xdd:
- return this.decodeArray(this.readUint32());
- case 0xde:
- return this.decodeMap(this.readUint16());
- case 0xdf:
- return this.decodeMap(this.readUint32());
- default:
- throw new Error(`Unsupported MessagePack prefix 0x${prefix.toString(16)}`);
+ private decodeArray(length: number): MessagePackValue[] {
+ return Array.from({ length }, () => this.decode());
+ }
+
+ private decodeMap(length: number): { [key: string]: MessagePackValue } {
+ const result: { [key: string]: MessagePackValue } = {};
+
+ for (let index = 0; index < length; index += 1) {
+ const key = this.decode();
+ if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint") {
+ throw new Error("Unsupported MessagePack map key");
+ }
+ result[String(key)] = this.decode();
+ }
+
+ return result;
+ }
+
+ private decodeString(length: number): string {
+ return this.textDecoder.decode(this.readBytes(length));
+ }
+
+ private readBytes(length: number): Uint8Array {
+ this.ensureAvailable(length);
+ const start = this.offset;
+ this.offset += length;
+ return this.bytes.subarray(start, this.offset);
+ }
+
+ private readUint8(): number {
+ this.ensureAvailable(1);
+ return this.view.getUint8(this.offset++);
+ }
+
+ private readInt8(): number {
+ this.ensureAvailable(1);
+ const value = this.view.getInt8(this.offset);
+ this.offset += 1;
+ return value;
+ }
+
+ private readUint16(): number {
+ this.ensureAvailable(2);
+ const value = this.view.getUint16(this.offset);
+ this.offset += 2;
+ return value;
+ }
+
+ private readInt16(): number {
+ this.ensureAvailable(2);
+ const value = this.view.getInt16(this.offset);
+ this.offset += 2;
+ return value;
+ }
+
+ private readUint32(): number {
+ this.ensureAvailable(4);
+ const value = this.view.getUint32(this.offset);
+ this.offset += 4;
+ return value;
+ }
+
+ private readInt32(): number {
+ this.ensureAvailable(4);
+ const value = this.view.getInt32(this.offset);
+ this.offset += 4;
+ return value;
+ }
+
+ private readBigUint64(): bigint {
+ this.ensureAvailable(8);
+ const value = this.view.getBigUint64(this.offset);
+ this.offset += 8;
+ return value;
+ }
+
+ private readBigInt64(): bigint {
+ this.ensureAvailable(8);
+ const value = this.view.getBigInt64(this.offset);
+ this.offset += 8;
+ return value;
+ }
+
+ private readFloat32(): number {
+ this.ensureAvailable(4);
+ const value = this.view.getFloat32(this.offset);
+ this.offset += 4;
+ return value;
+ }
+
+ private readFloat64(): number {
+ this.ensureAvailable(8);
+ const value = this.view.getFloat64(this.offset);
+ this.offset += 8;
+ return value;
+ }
+
+ private ensureAvailable(length: number): void {
+ if (length < 0 || this.offset + length > this.bytes.byteLength) {
+ throw new Error("Unexpected end of MessagePack data");
+ }
+ }
+
+ /** 解码一个完整的 MessagePack 值。 */
+ decode(): MessagePackValue {
+ const prefix = this.readUint8();
+
+ if (prefix <= 0x7f) return prefix;
+ if (prefix >= 0xe0) return prefix - 0x100;
+ if ((prefix & 0xf0) === 0x80) return this.decodeMap(prefix & 0x0f);
+ if ((prefix & 0xf0) === 0x90) return this.decodeArray(prefix & 0x0f);
+ if ((prefix & 0xe0) === 0xa0) return this.decodeString(prefix & 0x1f);
+
+ switch (prefix) {
+ case 0xc0:
+ return null;
+ case 0xc2:
+ return false;
+ case 0xc3:
+ return true;
+ case 0xc4:
+ return this.readBytes(this.readUint8());
+ case 0xc5:
+ return this.readBytes(this.readUint16());
+ case 0xc6:
+ return this.readBytes(this.readUint32());
+ case 0xca:
+ return this.readFloat32();
+ case 0xcb:
+ return this.readFloat64();
+ case 0xcc:
+ return this.readUint8();
+ case 0xcd:
+ return this.readUint16();
+ case 0xce:
+ return this.readUint32();
+ case 0xcf:
+ return this.readBigUint64();
+ case 0xd0:
+ return this.readInt8();
+ case 0xd1:
+ return this.readInt16();
+ case 0xd2:
+ return this.readInt32();
+ case 0xd3:
+ return this.readBigInt64();
+ case 0xd9:
+ return this.decodeString(this.readUint8());
+ case 0xda:
+ return this.decodeString(this.readUint16());
+ case 0xdb:
+ return this.decodeString(this.readUint32());
+ case 0xdc:
+ return this.decodeArray(this.readUint16());
+ case 0xdd:
+ return this.decodeArray(this.readUint32());
+ case 0xde:
+ return this.decodeMap(this.readUint16());
+ case 0xdf:
+ return this.decodeMap(this.readUint32());
+ default:
+ throw new Error(`Unsupported MessagePack prefix 0x${prefix.toString(16)}`);
+ }
}
- }
}
/** 解码 Base64 编码的 OneTalk 同步推送数据。 */
export function decodeOneTalkSyncPushData(encoded: string): MessagePackValue {
- const binary = atob(encoded);
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
- const decoder = new MessagePackDecoder(bytes);
- const value = decoder.decode();
+ const binary = atob(encoded);
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
+ const decoder = new MessagePackDecoder(bytes);
+ const value = decoder.decode();
- if (!decoder.done) throw new Error("Unexpected trailing MessagePack bytes");
- return value;
+ if (!decoder.done) throw new Error("Unexpected trailing MessagePack bytes");
+ return value;
}
diff --git a/apps/chrome-extension/src/onetalk/main-page/utils.ts b/apps/chrome-extension/src/onetalk/main-page/utils.ts
index 54600aa..2ca7e37 100644
--- a/apps/chrome-extension/src/onetalk/main-page/utils.ts
+++ b/apps/chrome-extension/src/onetalk/main-page/utils.ts
@@ -1,5 +1,5 @@
// 提供 main-page 内共享的无副作用辅助判断
export function isRecord(value: unknown): value is Record {
- return typeof value === "object" && value !== null && !Array.isArray(value);
+ return typeof value === "object" && value !== null && !Array.isArray(value);
}
diff --git a/apps/chrome-extension/test/onetalk-current-conversation-history.test.js b/apps/chrome-extension/test/onetalk-current-conversation-history.test.js
index 9f09255..9a3e283 100644
--- a/apps/chrome-extension/test/onetalk-current-conversation-history.test.js
+++ b/apps/chrome-extension/test/onetalk-current-conversation-history.test.js
@@ -9,340 +9,342 @@ import { isAccountId } from "../src/onetalk/main-page/model.ts";
const targetAccountId = 2500002169502;
const targetConversation = {
- cid: "conversation-1",
- accountId: targetAccountId,
- accountIdEncrypt: "encrypted-contact-account",
- aliId: "seller@icbu",
- aliIdEncrypt: "encrypted-seller-account",
+ cid: "conversation-1",
+ accountId: targetAccountId,
+ accountIdEncrypt: "encrypted-contact-account",
+ aliId: "seller@icbu",
+ aliIdEncrypt: "encrypted-seller-account",
};
function historyMessage(messageId, sendTime) {
- return { messageId, sendTime };
+ return { messageId, sendTime };
}
function createPageWindow({ conversationPages, historyPages }) {
- const conversationCalls = [];
- const historyCalls = [];
- let conversationIndex = 0;
- let historyIndex = 0;
- let forbiddenCalls = 0;
+ const conversationCalls = [];
+ const historyCalls = [];
+ let conversationIndex = 0;
+ let historyIndex = 0;
+ let forbiddenCalls = 0;
- const messageService = {
- async fetchMessagesWithoutUpdateToRead(options, conversation) {
- historyCalls.push({ options, conversation });
- return historyPages[historyIndex++];
- },
- fetchMessages() {
- forbiddenCalls += 1;
- },
- updateMessageToRead() {
- forbiddenCalls += 1;
- },
- };
- const sdk = {
- getConversationServiceV2() {
- return {
- async getConversationListByPagination(options) {
- conversationCalls.push(options);
- return conversationPages[conversationIndex++];
+ const messageService = {
+ async fetchMessagesWithoutUpdateToRead(options, conversation) {
+ historyCalls.push({ options, conversation });
+ return historyPages[historyIndex++];
},
- };
- },
- getMessageService() {
- return messageService;
- },
- };
- const pageWindow = {
- location: {
- href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
- },
- IcbuIM: {
- IMBaaSSDK: { default: sdk },
- },
- };
+ fetchMessages() {
+ forbiddenCalls += 1;
+ },
+ updateMessageToRead() {
+ forbiddenCalls += 1;
+ },
+ };
+ const sdk = {
+ getConversationServiceV2() {
+ return {
+ async getConversationListByPagination(options) {
+ conversationCalls.push(options);
+ return conversationPages[conversationIndex++];
+ },
+ };
+ },
+ getMessageService() {
+ return messageService;
+ },
+ };
+ const pageWindow = {
+ location: {
+ href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
+ },
+ IcbuIM: {
+ IMBaaSSDK: { default: sdk },
+ },
+ };
- return {
- pageWindow,
- conversationCalls,
- historyCalls,
- messageService,
- forbiddenCalls: () => forbiddenCalls,
- };
+ return {
+ pageWindow,
+ conversationCalls,
+ historyCalls,
+ messageService,
+ forbiddenCalls: () => forbiddenCalls,
+ };
}
test("accepts only finite non-empty OneTalk account identifiers", () => {
- for (const value of ["account", 0, -1, Number.MAX_SAFE_INTEGER]) {
- assert.equal(isAccountId(value), true);
- }
+ for (const value of ["account", 0, -1, Number.MAX_SAFE_INTEGER]) {
+ assert.equal(isAccountId(value), true);
+ }
- for (const value of ["", Number.NaN, Number.POSITIVE_INFINITY, null, true]) {
- assert.equal(isAccountId(value), false);
- }
+ for (const value of ["", Number.NaN, Number.POSITIVE_INFINITY, null, true]) {
+ assert.equal(isAccountId(value), false);
+ }
});
test("scans the current conversation and exhausts history through the no-read API", async () => {
- const fixture = createPageWindow({
- conversationPages: [
- {
- list: [{ ...targetConversation, accountId: "other" }],
- hasMore: true,
- nextCursor: "page-2",
- },
- { list: [targetConversation], hasMore: false },
- ],
- historyPages: [
- {
- list: [historyMessage(4, 400), historyMessage(3, 300)],
- hasMore: true,
- },
- {
- list: [historyMessage(3, 300), historyMessage(2, 200)],
- hasMore: true,
- },
- { list: [historyMessage(1, 100)], hasMore: false },
- ],
- });
- const sleeps = [];
- const randomValues = [0, 1];
+ const fixture = createPageWindow({
+ conversationPages: [
+ {
+ list: [{ ...targetConversation, accountId: "other" }],
+ hasMore: true,
+ nextCursor: "page-2",
+ },
+ { list: [targetConversation], hasMore: false },
+ ],
+ historyPages: [
+ {
+ list: [historyMessage(4, 400), historyMessage(3, 300)],
+ hasMore: true,
+ },
+ {
+ list: [historyMessage(3, 300), historyMessage(2, 200)],
+ hasMore: true,
+ },
+ { list: [historyMessage(1, 100)], hasMore: false },
+ ],
+ });
+ const sleeps = [];
+ const randomValues = [0, 1];
- const result = await syncCurrentConversationHistory(fixture.pageWindow, {
- now: () => 500,
- random: () => randomValues.shift(),
- sleep: async (milliseconds) => sleeps.push(milliseconds),
- });
+ const result = await syncCurrentConversationHistory(fixture.pageWindow, {
+ now: () => 500,
+ random: () => randomValues.shift(),
+ sleep: async (milliseconds) => sleeps.push(milliseconds),
+ });
- assert.deepEqual(result, { exit: "history_exhausted", pages: 3, uniqueMessages: 4 });
- assert.deepEqual(fixture.conversationCalls, [
- { cursor: 0, count: 20 },
- { cursor: "page-2", count: 20 },
- ]);
- assert.deepEqual(
- fixture.historyCalls.map(({ options }) => options),
- [
- {
- contactAccountId: targetAccountId,
- contactAccountIdEncrypt: "encrypted-contact-account",
- aliId: "seller@icbu",
- aliIdEncrypt: "encrypted-seller-account",
- searchMessageId: "",
- timeSlide: { forward: false, timeStamp: 500, pageSize: 20 },
- },
- {
- contactAccountId: targetAccountId,
- contactAccountIdEncrypt: "encrypted-contact-account",
- aliId: "seller@icbu",
- aliIdEncrypt: "encrypted-seller-account",
- searchMessageId: "",
- timeSlide: { forward: false, timeStamp: 300, pageSize: 20 },
- },
- {
- contactAccountId: targetAccountId,
- contactAccountIdEncrypt: "encrypted-contact-account",
- aliId: "seller@icbu",
- aliIdEncrypt: "encrypted-seller-account",
- searchMessageId: "",
- timeSlide: { forward: false, timeStamp: 200, pageSize: 20 },
- },
- ],
- );
- assert.equal(fixture.historyCalls[0].conversation, targetConversation);
- assert.deepEqual(sleeps, [1_000, 3_000]);
- assert.equal(fixture.forbiddenCalls(), 0);
+ assert.deepEqual(result, { exit: "history_exhausted", pages: 3, uniqueMessages: 4 });
+ assert.deepEqual(fixture.conversationCalls, [
+ { cursor: 0, count: 20 },
+ { cursor: "page-2", count: 20 },
+ ]);
+ assert.deepEqual(
+ fixture.historyCalls.map(({ options }) => options),
+ [
+ {
+ contactAccountId: targetAccountId,
+ contactAccountIdEncrypt: "encrypted-contact-account",
+ aliId: "seller@icbu",
+ aliIdEncrypt: "encrypted-seller-account",
+ searchMessageId: "",
+ timeSlide: { forward: false, timeStamp: 500, pageSize: 20 },
+ },
+ {
+ contactAccountId: targetAccountId,
+ contactAccountIdEncrypt: "encrypted-contact-account",
+ aliId: "seller@icbu",
+ aliIdEncrypt: "encrypted-seller-account",
+ searchMessageId: "",
+ timeSlide: { forward: false, timeStamp: 300, pageSize: 20 },
+ },
+ {
+ contactAccountId: targetAccountId,
+ contactAccountIdEncrypt: "encrypted-contact-account",
+ aliId: "seller@icbu",
+ aliIdEncrypt: "encrypted-seller-account",
+ searchMessageId: "",
+ timeSlide: { forward: false, timeStamp: 200, pageSize: 20 },
+ },
+ ],
+ );
+ assert.equal(fixture.historyCalls[0].conversation, targetConversation);
+ assert.deepEqual(sleeps, [1_000, 3_000]);
+ assert.equal(fixture.forbiddenCalls(), 0);
});
test("the installed global entry has no dependency override and returns completion", async () => {
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
- });
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
+ });
- installCurrentConversationHistorySync(fixture.pageWindow);
- const entry = fixture.pageWindow.__tradeMessageCenterOneTalk.syncCurrentConversationHistory;
+ installCurrentConversationHistorySync(fixture.pageWindow);
+ const entry = fixture.pageWindow.__tradeMessageCenterOneTalk.syncCurrentConversationHistory;
- assert.equal(entry.length, 0);
- assert.deepEqual(await entry(), { exit: "history_exhausted", pages: 1, uniqueMessages: 1 });
+ assert.equal(entry.length, 0);
+ assert.deepEqual(await entry(), { exit: "history_exhausted", pages: 1, uniqueMessages: 1 });
});
test("skips unrelated conversation shapes and validates only the active account match", async () => {
- const fixture = createPageWindow({
- conversationPages: [
- {
- list: [
- { cid: "group-conversation" },
- { accountId: "system-conversation" },
- targetConversation,
+ const fixture = createPageWindow({
+ conversationPages: [
+ {
+ list: [
+ { cid: "group-conversation" },
+ { accountId: "system-conversation" },
+ targetConversation,
+ ],
+ hasMore: false,
+ },
],
- hasMore: false,
- },
- ],
- historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
- });
+ historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
+ });
- assert.deepEqual(await syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }), {
- exit: "history_exhausted",
- pages: 1,
- uniqueMessages: 1,
- });
- assert.equal(fixture.historyCalls[0].conversation, targetConversation);
+ assert.deepEqual(await syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }), {
+ exit: "history_exhausted",
+ pages: 1,
+ uniqueMessages: 1,
+ });
+ assert.equal(fixture.historyCalls[0].conversation, targetConversation);
});
test("converts SDK request failures to a stable secret-free error", async () => {
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [],
- });
- fixture.messageService.fetchMessagesWithoutUpdateToRead = async () => {
- throw new Error("chatToken=PAGE-SECRET&contactAccountIdEncrypt=BUYER-SECRET");
- };
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [],
+ });
+ fixture.messageService.fetchMessagesWithoutUpdateToRead = async () => {
+ throw new Error("chatToken=PAGE-SECRET&contactAccountIdEncrypt=BUYER-SECRET");
+ };
- const error = await syncCurrentConversationHistory(fixture.pageWindow).catch((reason) => reason);
- assert.equal(error.message, "onetalk_history_message_request_failed");
- assert.equal(error.message.includes("SECRET"), false);
+ const error = await syncCurrentConversationHistory(fixture.pageWindow).catch(
+ (reason) => reason,
+ );
+ assert.equal(error.message, "onetalk_history_message_request_failed");
+ assert.equal(error.message.includes("SECRET"), false);
});
test("reuses the exact in-flight promise for the same page", async () => {
- let resolveHistory;
- const historyResponse = new Promise((resolve) => {
- resolveHistory = resolve;
- });
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [historyResponse],
- });
- const dependencies = { now: () => 500, sleep: async () => {}, random: () => 0 };
+ let resolveHistory;
+ const historyResponse = new Promise((resolve) => {
+ resolveHistory = resolve;
+ });
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [historyResponse],
+ });
+ const dependencies = { now: () => 500, sleep: async () => {}, random: () => 0 };
- const first = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
- const second = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
- assert.equal(first, second);
+ const first = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
+ const second = syncCurrentConversationHistory(fixture.pageWindow, dependencies);
+ assert.equal(first, second);
- resolveHistory({ list: [historyMessage("m1", 100)], hasMore: false });
- await first;
- assert.equal(fixture.historyCalls.length, 1);
+ resolveHistory({ list: [historyMessage("m1", 100)], hasMore: false });
+ await first;
+ assert.equal(fixture.historyCalls.length, 1);
});
test("fails closed when conversation pagination does not advance", async () => {
- const fixture = createPageWindow({
- conversationPages: [
- {
- list: [{ ...targetConversation, accountId: "other" }],
- hasMore: true,
- nextCursor: "same",
- },
- {
- list: [{ ...targetConversation, accountId: "other" }],
- hasMore: true,
- nextCursor: "same",
- },
- ],
- historyPages: [],
- });
+ const fixture = createPageWindow({
+ conversationPages: [
+ {
+ list: [{ ...targetConversation, accountId: "other" }],
+ hasMore: true,
+ nextCursor: "same",
+ },
+ {
+ list: [{ ...targetConversation, accountId: "other" }],
+ hasMore: true,
+ nextCursor: "same",
+ },
+ ],
+ historyPages: [],
+ });
- await assert.rejects(
- syncCurrentConversationHistory(fixture.pageWindow),
- /onetalk_history_conversation_cursor_stalled/,
- );
+ await assert.rejects(
+ syncCurrentConversationHistory(fixture.pageWindow),
+ /onetalk_history_conversation_cursor_stalled/,
+ );
});
test("fails closed when history pages cannot make safe progress", async (t) => {
- const cases = [
- { name: "empty page", page: { list: [], hasMore: false }, error: "invalid_message_page" },
- {
- name: "missing finite sendTime",
- page: { list: [{ messageId: 1 }], hasMore: false },
- error: "invalid_message_page",
- },
- {
- name: "time cursor stalls",
- page: { list: [historyMessage("m1", 500)], hasMore: false },
- error: "message_cursor_stalled",
- },
- ];
+ const cases = [
+ { name: "empty page", page: { list: [], hasMore: false }, error: "invalid_message_page" },
+ {
+ name: "missing finite sendTime",
+ page: { list: [{ messageId: 1 }], hasMore: false },
+ error: "invalid_message_page",
+ },
+ {
+ name: "time cursor stalls",
+ page: { list: [historyMessage("m1", 500)], hasMore: false },
+ error: "message_cursor_stalled",
+ },
+ ];
- for (const scenario of cases) {
- await t.test(scenario.name, async () => {
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [scenario.page],
- });
- await assert.rejects(
- syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }),
- new RegExp(`onetalk_history_${scenario.error}`),
- );
- });
- }
+ for (const scenario of cases) {
+ await t.test(scenario.name, async () => {
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [scenario.page],
+ });
+ await assert.rejects(
+ syncCurrentConversationHistory(fixture.pageWindow, { now: () => 500 }),
+ new RegExp(`onetalk_history_${scenario.error}`),
+ );
+ });
+ }
});
test("fails closed when a page contains no new message IDs", async () => {
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [
- { list: [historyMessage(1, 300)], hasMore: true },
- { list: [historyMessage("1", 200)], hasMore: false },
- ],
- });
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [
+ { list: [historyMessage(1, 300)], hasMore: true },
+ { list: [historyMessage("1", 200)], hasMore: false },
+ ],
+ });
- await assert.rejects(
- syncCurrentConversationHistory(fixture.pageWindow, {
- now: () => 500,
- random: () => 0,
- sleep: async () => {},
- }),
- /onetalk_history_message_cursor_stalled/,
- );
+ await assert.rejects(
+ syncCurrentConversationHistory(fixture.pageWindow, {
+ now: () => 500,
+ random: () => 0,
+ sleep: async () => {},
+ }),
+ /onetalk_history_message_cursor_stalled/,
+ );
});
test("stops when the active account changes during a history request", async () => {
- const fixture = createPageWindow({
- conversationPages: [{ list: [targetConversation], hasMore: false }],
- historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
- });
- const originalFetch = fixture.messageService.fetchMessagesWithoutUpdateToRead;
- fixture.messageService.fetchMessagesWithoutUpdateToRead = async (...args) => {
- const response = await originalFetch(...args);
- fixture.pageWindow.location.href =
- "https://onetalk.alibaba.com/?activeAccountId=different-account";
- return response;
- };
+ const fixture = createPageWindow({
+ conversationPages: [{ list: [targetConversation], hasMore: false }],
+ historyPages: [{ list: [historyMessage("m1", 100)], hasMore: false }],
+ });
+ const originalFetch = fixture.messageService.fetchMessagesWithoutUpdateToRead;
+ fixture.messageService.fetchMessagesWithoutUpdateToRead = async (...args) => {
+ const response = await originalFetch(...args);
+ fixture.pageWindow.location.href =
+ "https://onetalk.alibaba.com/?activeAccountId=different-account";
+ return response;
+ };
- await assert.rejects(
- syncCurrentConversationHistory(fixture.pageWindow),
- /onetalk_history_active_account_changed/,
- );
+ await assert.rejects(
+ syncCurrentConversationHistory(fixture.pageWindow),
+ /onetalk_history_active_account_changed/,
+ );
});
test("fails closed when the required page SDK is unavailable", async () => {
- let topLevelGetterCalls = 0;
- await assert.rejects(
- syncCurrentConversationHistory({
- location: {
- href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
- },
- getConversationServiceV2() {
- topLevelGetterCalls += 1;
- },
- getMessageService() {
- topLevelGetterCalls += 1;
- },
- }),
- /onetalk_history_sdk_unavailable/,
- );
- assert.equal(topLevelGetterCalls, 0);
+ let topLevelGetterCalls = 0;
+ await assert.rejects(
+ syncCurrentConversationHistory({
+ location: {
+ href: `https://onetalk.alibaba.com/?activeAccountId=${targetAccountId}`,
+ },
+ getConversationServiceV2() {
+ topLevelGetterCalls += 1;
+ },
+ getMessageService() {
+ topLevelGetterCalls += 1;
+ },
+ }),
+ /onetalk_history_sdk_unavailable/,
+ );
+ assert.equal(topLevelGetterCalls, 0);
});
test("fails closed when the matched conversation lacks required identity fields", async () => {
- const fixture = createPageWindow({
- conversationPages: [
- {
- list: [{ ...targetConversation, cid: undefined }],
- hasMore: false,
- },
- ],
- historyPages: [],
- });
+ const fixture = createPageWindow({
+ conversationPages: [
+ {
+ list: [{ ...targetConversation, cid: undefined }],
+ hasMore: false,
+ },
+ ],
+ historyPages: [],
+ });
- await assert.rejects(
- syncCurrentConversationHistory(fixture.pageWindow),
- /onetalk_history_invalid_conversation_page/,
- );
- assert.equal(fixture.historyCalls.length, 0);
+ await assert.rejects(
+ syncCurrentConversationHistory(fixture.pageWindow),
+ /onetalk_history_invalid_conversation_page/,
+ );
+ assert.equal(fixture.historyCalls.length, 0);
});
diff --git a/apps/chrome-extension/test/onetalk-sync-push-decoder.test.js b/apps/chrome-extension/test/onetalk-sync-push-decoder.test.js
index 09cd342..f520500 100644
--- a/apps/chrome-extension/test/onetalk-sync-push-decoder.test.js
+++ b/apps/chrome-extension/test/onetalk-sync-push-decoder.test.js
@@ -6,31 +6,31 @@ import test from "node:test";
import { decodeOneTalkSyncPushData } from "../src/onetalk/main-page/sync-push-decoder.ts";
test("decodes OneTalk status payloads", () => {
- const decoded = decodeOneTalkSyncPushData(
- "gQGRhAHaACYyMjA4MzE0MDAwNzk4LTI1MDAwMDIxNjk1MDIjMTEwMTFAaWNidQIBAwEEsjI1MDAwMDIxNjk1MDJAaWNidQ==",
- );
+ const decoded = decodeOneTalkSyncPushData(
+ "gQGRhAHaACYyMjA4MzE0MDAwNzk4LTI1MDAwMDIxNjk1MDIjMTEwMTFAaWNidQIBAwEEsjI1MDAwMDIxNjk1MDJAaWNidQ==",
+ );
- assert.deepEqual(decoded, {
- 1: [
- {
- 1: "2208314000798-2500002169502#11011@icbu",
- 2: 1,
- 3: 1,
- 4: "2500002169502@icbu",
- },
- ],
- });
+ assert.deepEqual(decoded, {
+ 1: [
+ {
+ 1: "2208314000798-2500002169502#11011@icbu",
+ 2: 1,
+ 3: 1,
+ 4: "2500002169502@icbu",
+ },
+ ],
+ });
});
test("decodes OneTalk message payloads", () => {
- const decoded = decodeOneTalkSyncPushData("ggGs5om+5L2g55yf6Zq+As8AAAGgODt1FA==");
+ const decoded = decodeOneTalkSyncPushData("ggGs5om+5L2g55yf6Zq+As8AAAGgODt1FA==");
- assert.deepEqual(decoded, {
- 1: "找你真难",
- 2: 1787649815828n,
- });
+ assert.deepEqual(decoded, {
+ 1: "找你真难",
+ 2: 1787649815828n,
+ });
});
test("rejects truncated payloads", () => {
- assert.throws(() => decodeOneTalkSyncPushData("2gA="), /Unexpected end/);
+ assert.throws(() => decodeOneTalkSyncPushData("2gA="), /Unexpected end/);
});
diff --git a/apps/chrome-extension/test/onetalk-websocket-tap.test.js b/apps/chrome-extension/test/onetalk-websocket-tap.test.js
index b73d991..56bfdb3 100644
--- a/apps/chrome-extension/test/onetalk-websocket-tap.test.js
+++ b/apps/chrome-extension/test/onetalk-websocket-tap.test.js
@@ -6,188 +6,188 @@ import test from "node:test";
import { installOneTalkMessageObserver } from "../src/onetalk/main-page/message-observer/entry.ts";
class FakeWebSocket extends EventTarget {
- static CONNECTING = 0;
- static OPEN = 1;
- static CLOSING = 2;
- static CLOSED = 3;
+ static CONNECTING = 0;
+ static OPEN = 1;
+ static CLOSING = 2;
+ static CLOSED = 3;
- constructor(url, protocols) {
- super();
- this.url = String(url);
- this.protocols = protocols;
- }
+ constructor(url, protocols) {
+ super();
+ this.url = String(url);
+ this.protocols = protocols;
+ }
- receive(data) {
- this.dispatchEvent(new MessageEvent("message", { data }));
- }
+ receive(data) {
+ this.dispatchEvent(new MessageEvent("message", { data }));
+ }
}
function testPageWindow() {
- const logs = [];
- return {
- logs,
- pageWindow: {
- WebSocket: FakeWebSocket,
- console: {
- log(...args) {
- logs.push(args);
+ const logs = [];
+ return {
+ logs,
+ pageWindow: {
+ WebSocket: FakeWebSocket,
+ console: {
+ log(...args) {
+ logs.push(args);
+ },
+ },
},
- },
- },
- };
+ };
}
test("prints raw messages from the OneTalk WebSocket", () => {
- const { logs, pageWindow } = testPageWindow();
+ const { logs, pageWindow } = testPageWindow();
- installOneTalkMessageObserver(pageWindow);
- const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
- const payload = { raw: "unchanged" };
- socket.receive(payload);
+ installOneTalkMessageObserver(pageWindow);
+ const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
+ const payload = { raw: "unchanged" };
+ socket.receive(payload);
- assert.ok(socket instanceof FakeWebSocket);
- assert.equal(pageWindow.WebSocket.OPEN, FakeWebSocket.OPEN);
- assert.equal(logs.length, 1);
- assert.equal(logs[0][1], payload);
+ assert.ok(socket instanceof FakeWebSocket);
+ assert.equal(pageWindow.WebSocket.OPEN, FakeWebSocket.OPEN);
+ assert.equal(logs.length, 1);
+ assert.equal(logs[0][1], payload);
});
test("prints plaintext messages from conversation response frames", () => {
- const { logs, pageWindow } = testPageWindow();
+ const { logs, pageWindow } = testPageWindow();
- installOneTalkMessageObserver(pageWindow);
- const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
- const message = {
- messageId: "4264696193791.PNM",
- cid: "2208314000798-2500002169502#11011@icbu",
- createAt: 1787649815828,
- content: { text: { content: "找你真难" }, contentType: 1 },
- sender: { uid: "2208314000798@icbu" },
- unreadCount: 0,
- };
- const frame = JSON.stringify({
- code: 200,
- body: [
- {
- singleChatUserConversation: {
- lastMessage: { message, readStatus: 2, msgStatus: 1 },
- singleChatConversation: {
- pairFirst: "2208314000798@icbu",
- pairSecond: "2500002169502@icbu",
- },
- },
- },
- ],
- });
- socket.receive(frame);
+ installOneTalkMessageObserver(pageWindow);
+ const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
+ const message = {
+ messageId: "4264696193791.PNM",
+ cid: "2208314000798-2500002169502#11011@icbu",
+ createAt: 1787649815828,
+ content: { text: { content: "找你真难" }, contentType: 1 },
+ sender: { uid: "2208314000798@icbu" },
+ unreadCount: 0,
+ };
+ const frame = JSON.stringify({
+ code: 200,
+ body: [
+ {
+ singleChatUserConversation: {
+ lastMessage: { message, readStatus: 2, msgStatus: 1 },
+ singleChatConversation: {
+ pairFirst: "2208314000798@icbu",
+ pairSecond: "2500002169502@icbu",
+ },
+ },
+ },
+ ],
+ });
+ socket.receive(frame);
- assert.equal(logs.length, 2);
- assert.equal(logs[1][0], "[Trade Message Center][OneTalk new message]");
- assert.equal(logs[0][1], frame);
- assert.deepEqual(logs[1][1], {
- messageType: "new",
- conversationId: "2208314000798-2500002169502#11011@icbu",
- messageId: "4264696193791.PNM",
- sentAt: 1787649815828,
- contentType: 1,
- text: "找你真难",
- senderId: "2208314000798@icbu",
- participantIds: ["2208314000798@icbu", "2500002169502@icbu"],
- direction: "received",
- readStatus: 2,
- messageStatus: 1,
- unreadCount: 0,
- });
+ assert.equal(logs.length, 2);
+ assert.equal(logs[1][0], "[Trade Message Center][OneTalk new message]");
+ assert.equal(logs[0][1], frame);
+ assert.deepEqual(logs[1][1], {
+ messageType: "new",
+ conversationId: "2208314000798-2500002169502#11011@icbu",
+ messageId: "4264696193791.PNM",
+ sentAt: 1787649815828,
+ contentType: 1,
+ text: "找你真难",
+ senderId: "2208314000798@icbu",
+ participantIds: ["2208314000798@icbu", "2500002169502@icbu"],
+ direction: "received",
+ readStatus: 2,
+ messageStatus: 1,
+ unreadCount: 0,
+ });
});
test("prints messages from history response frames", () => {
- const { logs, pageWindow } = testPageWindow();
+ const { logs, pageWindow } = testPageWindow();
- installOneTalkMessageObserver(pageWindow);
- const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
- const frame = JSON.stringify({
- code: 200,
- body: {
- nextCursor: 1787157210197,
- hasMore: 1,
- userMessageModels: [
- {
- readStatus: 2,
- msgStatus: 1,
- message: {
- messageId: "4268023992386.PNM",
- cid: "2208314000798-2500002169502#11011@icbu",
- createAt: 1787212003008,
- content: { text: { content: "我需要深色的" }, contentType: 1 },
- sender: { uid: "2500002169502@icbu" },
- unreadCount: 0,
- },
+ installOneTalkMessageObserver(pageWindow);
+ const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
+ const frame = JSON.stringify({
+ code: 200,
+ body: {
+ nextCursor: 1787157210197,
+ hasMore: 1,
+ userMessageModels: [
+ {
+ readStatus: 2,
+ msgStatus: 1,
+ message: {
+ messageId: "4268023992386.PNM",
+ cid: "2208314000798-2500002169502#11011@icbu",
+ createAt: 1787212003008,
+ content: { text: { content: "我需要深色的" }, contentType: 1 },
+ sender: { uid: "2500002169502@icbu" },
+ unreadCount: 0,
+ },
+ },
+ {
+ readStatus: 2,
+ msgStatus: 1,
+ message: {
+ messageId: "4260308763261.PNM",
+ cid: "2208314000798-2500002169502#11011@icbu",
+ createAt: 1787210475840,
+ content: { custom: { type: 10010 }, contentType: 101 },
+ sender: { uid: "2500002169502@icbu" },
+ unreadCount: 0,
+ },
+ },
+ ],
},
- {
- readStatus: 2,
- msgStatus: 1,
- message: {
- messageId: "4260308763261.PNM",
- cid: "2208314000798-2500002169502#11011@icbu",
- createAt: 1787210475840,
- content: { custom: { type: 10010 }, contentType: 101 },
- sender: { uid: "2500002169502@icbu" },
- unreadCount: 0,
- },
- },
- ],
- },
- });
- socket.receive(frame);
+ });
+ socket.receive(frame);
- assert.equal(logs.length, 3);
- assert.equal(logs[1][0], "[Trade Message Center][OneTalk history message]");
- assert.deepEqual(logs[1][1], {
- messageType: "history",
- conversationId: "2208314000798-2500002169502#11011@icbu",
- messageId: "4268023992386.PNM",
- sentAt: 1787212003008,
- contentType: 1,
- text: "我需要深色的",
- senderId: "2500002169502@icbu",
- participantIds: ["2208314000798@icbu", "2500002169502@icbu"],
- direction: "sent",
- readStatus: 2,
- messageStatus: 1,
- unreadCount: 0,
- });
- assert.equal(logs[2][0], "[Trade Message Center][OneTalk history message]");
- assert.equal(logs[2][1].contentType, 101);
- assert.equal(logs[2][1].text, null);
+ assert.equal(logs.length, 3);
+ assert.equal(logs[1][0], "[Trade Message Center][OneTalk history message]");
+ assert.deepEqual(logs[1][1], {
+ messageType: "history",
+ conversationId: "2208314000798-2500002169502#11011@icbu",
+ messageId: "4268023992386.PNM",
+ sentAt: 1787212003008,
+ contentType: 1,
+ text: "我需要深色的",
+ senderId: "2500002169502@icbu",
+ participantIds: ["2208314000798@icbu", "2500002169502@icbu"],
+ direction: "sent",
+ readStatus: 2,
+ messageStatus: 1,
+ unreadCount: 0,
+ });
+ assert.equal(logs[2][0], "[Trade Message Center][OneTalk history message]");
+ assert.equal(logs[2][1].contentType, 101);
+ assert.equal(logs[2][1].text, null);
});
test("does not treat sync push packages as plaintext messages", () => {
- const { logs, pageWindow } = testPageWindow();
+ const { logs, pageWindow } = testPageWindow();
- installOneTalkMessageObserver(pageWindow);
- const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
- socket.receive(
- JSON.stringify({
- lwp: "/s/sync",
- body: { syncPushPackage: { data: [{ data: "encoded" }] } },
- }),
- );
+ installOneTalkMessageObserver(pageWindow);
+ const socket = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com/");
+ socket.receive(
+ JSON.stringify({
+ lwp: "/s/sync",
+ body: { syncPushPackage: { data: [{ data: "encoded" }] } },
+ }),
+ );
- assert.equal(logs.length, 1);
+ assert.equal(logs.length, 1);
});
test("ignores other hosts and installs only once", () => {
- const { logs, pageWindow } = testPageWindow();
+ const { logs, pageWindow } = testPageWindow();
- installOneTalkMessageObserver(pageWindow);
- const installedConstructor = pageWindow.WebSocket;
- installOneTalkMessageObserver(pageWindow);
+ installOneTalkMessageObserver(pageWindow);
+ const installedConstructor = pageWindow.WebSocket;
+ installOneTalkMessageObserver(pageWindow);
- assert.equal(pageWindow.WebSocket, installedConstructor);
+ assert.equal(pageWindow.WebSocket, installedConstructor);
- const unrelated = new pageWindow.WebSocket("wss://example.com/");
- const lookalike = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com.example.com/");
- unrelated.receive("unrelated");
- lookalike.receive("lookalike");
+ const unrelated = new pageWindow.WebSocket("wss://example.com/");
+ const lookalike = new pageWindow.WebSocket("wss://wss-icbu.dingtalk.com.example.com/");
+ unrelated.receive("unrelated");
+ lookalike.receive("lookalike");
- assert.deepEqual(logs, []);
+ assert.deepEqual(logs, []);
});
diff --git a/apps/chrome-extension/tsconfig.json b/apps/chrome-extension/tsconfig.json
index c919a31..814fa63 100644
--- a/apps/chrome-extension/tsconfig.json
+++ b/apps/chrome-extension/tsconfig.json
@@ -1,12 +1,12 @@
{
- "extends": "../../tsconfig.base.json",
- "compilerOptions": {
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "allowImportingTsExtensions": true,
- "lib": ["ES2022", "DOM"],
- "types": [],
- "noEmit": true
- },
- "include": ["src", "vite.config.ts", "popup"]
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "lib": ["ES2022", "DOM"],
+ "types": [],
+ "noEmit": true
+ },
+ "include": ["src", "vite.config.ts", "popup"]
}
diff --git a/apps/chrome-extension/vite.config.ts b/apps/chrome-extension/vite.config.ts
index dd4a6b5..8edf254 100644
--- a/apps/chrome-extension/vite.config.ts
+++ b/apps/chrome-extension/vite.config.ts
@@ -9,33 +9,34 @@ const configDir = fileURLToPath(new URL(".", import.meta.url));
const buildHash = process.env.BUILD_HASH || randomBytes(8).toString("hex");
export default defineConfig({
- root: configDir,
- publicDir: resolve(configDir, "public"),
- plugins: [
- {
- name: "build-hash",
- closeBundle() {
- console.info(`Build hash: ${buildHash}`);
- },
+ root: configDir,
+ publicDir: resolve(configDir, "public"),
+ plugins: [
+ {
+ name: "build-hash",
+ closeBundle() {
+ console.info(`Build hash: ${buildHash}`);
+ },
+ },
+ ],
+ build: {
+ outDir: resolve(configDir, "dist"),
+ emptyOutDir: true,
+ rollupOptions: {
+ input: {
+ "popup/popup": resolve(configDir, "popup/popup.html"),
+ "onetalk/main-page/page-script": resolve(
+ configDir,
+ "src/onetalk/main-page/page-script-entry.ts",
+ ),
+ },
+ output: {
+ banner: (chunk) =>
+ chunk.isEntry ? `console.info("Build hash: ${buildHash}");` : "",
+ entryFileNames: "[name].js",
+ chunkFileNames: "chunks/[name]-[hash].js",
+ assetFileNames: "assets/[name]-[hash][extname]",
+ },
+ },
},
- ],
- build: {
- outDir: resolve(configDir, "dist"),
- emptyOutDir: true,
- rollupOptions: {
- input: {
- "popup/popup": resolve(configDir, "popup/popup.html"),
- "onetalk/main-page/page-script": resolve(
- configDir,
- "src/onetalk/main-page/page-script-entry.ts",
- ),
- },
- output: {
- banner: (chunk) => (chunk.isEntry ? `console.info("Build hash: ${buildHash}");` : ""),
- entryFileNames: "[name].js",
- chunkFileNames: "chunks/[name]-[hash].js",
- assetFileNames: "assets/[name]-[hash][extname]",
- },
- },
- },
});
diff --git a/apps/server/package.json b/apps/server/package.json
index 4c4fa5d..6e42493 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -1,9 +1,9 @@
{
- "name": "@trade-message-center/server",
- "private": true,
- "version": "0.1.0",
- "type": "module",
- "dependencies": {
- "fastify": "^5.12.1"
- }
+ "name": "@trade-message-center/server",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "fastify": "^5.12.1"
+ }
}
diff --git a/package.json b/package.json
index 00a7d5e..2ad7bcf 100644
--- a/package.json
+++ b/package.json
@@ -1,31 +1,31 @@
{
- "name": "trade-message-center",
- "version": "0.1.0",
- "private": true,
- "workspaces": [
- "apps/*"
- ],
- "type": "module",
- "scripts": {
- "dev": "pnpm --parallel --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run dev",
- "build": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run build",
- "typecheck": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run typecheck",
- "test": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run test",
- "format": "oxfmt",
- "format:check": "oxfmt --check",
- "prepare": "husky"
- },
- "devDependencies": {
- "husky": "^9.1.7",
- "lint-staged": "^17.3.0",
- "oxfmt": "^0.64.0",
- "typescript": "^5.7.3"
- },
- "lint-staged": {
- "*": "oxfmt --no-error-on-unmatched-pattern"
- },
- "engines": {
- "node": ">=22.22.2 <23"
- },
- "packageManager": "pnpm@11.7.0"
+ "name": "trade-message-center",
+ "version": "0.1.0",
+ "private": true,
+ "workspaces": [
+ "apps/*"
+ ],
+ "type": "module",
+ "scripts": {
+ "dev": "pnpm --parallel --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run dev",
+ "build": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run build",
+ "typecheck": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run typecheck",
+ "test": "pnpm --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run test",
+ "format": "oxfmt",
+ "format:check": "oxfmt --check",
+ "prepare": "husky"
+ },
+ "devDependencies": {
+ "husky": "^9.1.7",
+ "lint-staged": "^17.3.0",
+ "oxfmt": "^0.64.0",
+ "typescript": "^5.7.3"
+ },
+ "lint-staged": {
+ "*": "oxfmt --no-error-on-unmatched-pattern"
+ },
+ "engines": {
+ "node": ">=22.22.2 <23"
+ },
+ "packageManager": "pnpm@11.7.0"
}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 313f2ec..6d670a6 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,4 @@
packages:
- - apps/*
+ - apps/*
allowBuilds:
- esbuild: true
+ esbuild: true
diff --git a/tsconfig.base.json b/tsconfig.base.json
index d3eb122..625252e 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1,12 +1,12 @@
{
- "compilerOptions": {
- "target": "ES2022",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
- "strict": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "skipLibCheck": true
- }
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true
+ }
}