refactor(onetalk-contract): move shared contract to packages

This commit is contained in:
YBF
2026-09-12 14:01:16 +08:00
parent 221d32be15
commit 4d46b5b839
33 changed files with 85 additions and 50 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ node_modules/
.pnpm-store/ .pnpm-store/
dist/ dist/
apps/*/dist/ apps/*/dist/
packages/*/dist/
.vite/ .vite/
*.tsbuildinfo *.tsbuildinfo
.env .env
@@ -13,4 +14,4 @@ apps/chrome-extension/.keys/
mock-log.text mock-log.text
.claude/skills/gitnexus .claude/skills/gitnexus
+1 -1
View File
@@ -164,7 +164,7 @@ packages:
mind-test-harness: mind-test-harness:
path: apps/mind-test-harness path: apps/mind-test-harness
onetalk-contract: onetalk-contract:
path: apps/onetalk-contract path: packages/onetalk-contract
server: server:
path: apps/server path: apps/server
default_package: @trade-message-center/chrome-extension default_package: @trade-message-center/chrome-extension
@@ -288,7 +288,7 @@ decodeOneTalkMessageContent(source: OneTalkMessageContentSource) ->
| { status: "anomaly", code: OneTalkMediaAnomalyCode, mediaKind: "image" | "file" | "card" }; | { status: "anomaly", code: OneTalkMediaAnomalyCode, mediaKind: "image" | "file" | "card" };
``` ```
`apps/onetalk-contract/src/content.ts` 是这六个 kind、exact-shape guard 和跨层读取模型的唯一 owner。`message-observer/content-decoder.ts` 是历史 SDK tuple 分类、受控 Base64 摘要解码和白名单投影的唯一生产点;它对名片只生产 marker,不生产客户资料 view。`read-projection.ts` 是服务端读取时将 marker 与同一账号/会话的当前 profile 组合为 `OneTalkBusinessCardViewContent` 的唯一 owner。 `packages/onetalk-contract/src/content.ts` 是这六个 kind、exact-shape guard 和跨层读取模型的唯一 owner。`message-observer/content-decoder.ts` 是历史 SDK tuple 分类、受控 Base64 摘要解码和白名单投影的唯一生产点;它对名片只生产 marker,不生产客户资料 view。`read-projection.ts` 是服务端读取时将 marker 与同一账号/会话的当前 profile 组合为 `OneTalkBusinessCardViewContent` 的唯一 owner。
### 7.3 Contracts ### 7.3 Contracts
+2 -2
View File
@@ -84,13 +84,13 @@ const retryLimit = options.retryLimit ?? DEFAULT_RETRY_LIMIT;
### 1. Scope / Trigger ### 1. Scope / Trigger
- Trigger:根 package 版本跨 workspace package metadata、扩展构建/watch 和 release CI 传播,且这些边界不得各自补偿或推导版本。 - Trigger:根 package 版本跨 workspace package metadata、扩展构建/watch 和 release CI 传播,且这些边界不得各自补偿或推导版本。
- Scope:根 `package.json:version` 是唯一可编辑事实源;`apps/*/package.json` 的具体版本是同步镜像;扩展最终只加载生成的 `dist/manifest.json` - Scope:根 `package.json:version` 是唯一可编辑事实源;`apps/*/package.json``packages/*/package.json` 的具体版本是同步镜像;扩展最终只加载生成的 `dist/manifest.json`
### 2. Signatures ### 2. Signatures
- `validatePackageVersion(value: unknown, source?: string): string`:验证同时满足 package metadata 和 Chrome Manifest 的三段数字版本。 - `validatePackageVersion(value: unknown, source?: string): string`:验证同时满足 package metadata 和 Chrome Manifest 的三段数字版本。
- `readRootPackageVersion(rootDir?: string): string`:只读取并验证 workspace 根 `package.json` - `readRootPackageVersion(rootDir?: string): string`:只读取并验证 workspace 根 `package.json`
- `pnpm version:sync`:把根版本写入所有 `apps/*/package.json` 镜像。 - `pnpm version:sync`:把根版本写入所有 `apps/*/package.json``packages/*/package.json` 镜像。
- `pnpm version:check`:只检查镜像,不自动修复。 - `pnpm version:check`:只检查镜像,不自动修复。
- `pnpm version:ensure`:先执行 `version:sync`,再执行 `version:check`;根级 dev/build/typecheck/test/format:check 通过它获得稳定的一致版本环境。 - `pnpm version:ensure`:先执行 `version:sync`,再执行 `version:check`;根级 dev/build/typecheck/test/format:check 通过它获得稳定的一致版本环境。
- `TMC_PACKAGE_VERSION`:由 workspace 根命令注入给下游扩展命令;扩展直接执行时缺失必须失败。 - `TMC_PACKAGE_VERSION`:由 workspace 根命令注入给下游扩展命令;扩展直接执行时缺失必须失败。
+1 -1
View File
@@ -86,7 +86,7 @@ export const isObjectRecord = (value: unknown): value is Record<string, unknown>
### 3. Contracts ### 3. Contracts
- `apps/onetalk-contract/src/guards.ts` owns strict `isPlainRecord` and the - `packages/onetalk-contract/src/guards.ts` owns strict `isPlainRecord` and the
contract root facade is the only cross-package import path. It accepts contract root facade is the only cross-package import path. It accepts
current-realm plain objects and null-prototype objects, and rejects arrays, current-realm plain objects and null-prototype objects, and rejects arrays,
primitives, class/custom-prototype values, and cross-realm ordinary objects. primitives, class/custom-prototype values, and cross-realm ordinary objects.
@@ -8,11 +8,11 @@ scope_lanes:
path: /Users/ybf/work/trade-message-center path: /Users/ybf/work/trade-message-center
role: task-branch role: task-branch
owns: owns:
- apps/onetalk-contract/src/content.ts - packages/onetalk-contract/src/content.ts
- apps/onetalk-contract/src/guards.ts - packages/onetalk-contract/src/guards.ts
- apps/onetalk-contract/src/contact-profiles.ts - packages/onetalk-contract/src/contact-profiles.ts
- apps/onetalk-contract/src/index.ts - packages/onetalk-contract/src/index.ts
- apps/onetalk-contract/test/contract.test.ts - packages/onetalk-contract/test/contract.test.ts
- apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts - apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts
- apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts - apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts
- apps/chrome-extension/src/onetalk/page-bridge/model.ts - apps/chrome-extension/src/onetalk/page-bridge/model.ts
+2 -2
View File
@@ -8,13 +8,13 @@ RUN corepack enable && corepack prepare pnpm@11.7.0 --activate
ENV HUSKY=0 ENV HUSKY=0
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
COPY apps/onetalk-contract/package.json apps/onetalk-contract/package.json COPY packages/onetalk-contract/package.json packages/onetalk-contract/package.json
COPY apps/server/package.json apps/server/package.json COPY apps/server/package.json apps/server/package.json
COPY apps/chrome-extension/package.json apps/chrome-extension/package.json COPY apps/chrome-extension/package.json apps/chrome-extension/package.json
RUN pnpm install --frozen-lockfile --registry=https://registry.npmjs.org RUN pnpm install --frozen-lockfile --registry=https://registry.npmjs.org
COPY apps/onetalk-contract apps/onetalk-contract COPY packages/onetalk-contract packages/onetalk-contract
COPY apps/server apps/server COPY apps/server apps/server
RUN pnpm --filter @trade-message-center/server build && \ RUN pnpm --filter @trade-message-center/server build && \
@@ -436,7 +436,7 @@ Base64
→ normalized image content → normalized image content
``` ```
输出合同定义于 [`content.ts`](../apps/onetalk-contract/src/content.ts) 输出合同定义于 [`content.ts`](../packages/onetalk-contract/src/content.ts)
```ts ```ts
type OneTalkImageContent = { type OneTalkImageContent = {
@@ -688,7 +688,7 @@ raw `originalData` 只应在 MAIN world 短暂存在。send correlator 应消费
- live frame 分发:[`index.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts) - live frame 分发:[`index.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts)
- live message 解析:[`new.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts) - live message 解析:[`new.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts)
- MAIN 内容归一化:[`content-decoder.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts) - MAIN 内容归一化:[`content-decoder.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts)
- 归一化媒体合同:[`content.ts`](../apps/onetalk-contract/src/content.ts) - 归一化媒体合同:[`content.ts`](../packages/onetalk-contract/src/content.ts)
- observer/correlator 接线:[`page-script-entry.ts`](../apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts) - observer/correlator 接线:[`page-script-entry.ts`](../apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts)
- 当前文本发送确认:[`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) - 当前文本发送确认:[`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts)
- 当前页面命令入口:[`page-command.ts`](../apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts) - 当前页面命令入口:[`page-command.ts`](../apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts)
@@ -416,7 +416,7 @@ nodeName
### 10.1 共享内容解码器 ### 10.1 共享内容解码器
`apps/onetalk-contract` 或其最近共同父目录增加一个唯一的内容投影器,输入原始 `content`,输出判别联合: `packages/onetalk-contract` 或其最近共同父目录增加一个唯一的内容投影器,输入原始 `content`,输出判别联合:
```text ```text
text text
+2 -2
View File
@@ -902,8 +902,8 @@ aliIdEncrypt
| 历史只读 SDK | `apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts` | 调用 `fetchMessagesWithoutUpdateToRead` | | 历史只读 SDK | `apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts` | 调用 `fetchMessagesWithoutUpdateToRead` |
| 页面到 SW | `apps/chrome-extension/src/onetalk/service-worker/sync-engine/helpers.ts` | 原样复制观察字段 | | 页面到 SW | `apps/chrome-extension/src/onetalk/service-worker/sync-engine/helpers.ts` | 原样复制观察字段 |
| IndexedDB 校验 | `apps/chrome-extension/src/onetalk/service-worker/storage.ts` | 只检查 JSON 与基础字段类型 | | IndexedDB 校验 | `apps/chrome-extension/src/onetalk/service-worker/storage.ts` | 只检查 JSON 与基础字段类型 |
| 共享合同 | `apps/onetalk-contract/src/model.ts` | `content` 为任意 JSON value | | 共享合同 | `packages/onetalk-contract/src/model.ts` | `content` 为任意 JSON value |
| 共享 decoder | `apps/onetalk-contract/src/decoder.ts` | 不理解 text/image/file 语义 | | 共享 decoder | `packages/onetalk-contract/src/decoder.ts` | 不理解 text/image/file 语义 |
| Bright 清洗 | `apps/server/src/onetalk/service.ts` | 按键过滤对象,字符串原样通过 | | Bright 清洗 | `apps/server/src/onetalk/service.ts` | 按键过滤对象,字符串原样通过 |
| Bright 持久化 | `apps/server/src/database/schema/onetalk.ts` | `content jsonb``text nullable` | | Bright 持久化 | `apps/server/src/database/schema/onetalk.ts` | `content jsonb``text nullable` |
| 开发展示 | `apps/mind-test-harness/src/harness.ts` | 只展示 raw JSON | | 开发展示 | `apps/mind-test-harness/src/harness.ts` | 只展示 raw JSON |
+2 -1
View File
@@ -3,7 +3,8 @@
"version": "0.8.27", "version": "0.8.27",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"apps/*" "apps/*",
"packages/*"
], ],
"type": "module", "type": "module",
"scripts": { "scripts": {
+12 -12
View File
@@ -25,7 +25,7 @@ importers:
dependencies: dependencies:
'@trade-message-center/onetalk-contract': '@trade-message-center/onetalk-contract':
specifier: workspace:* specifier: workspace:*
version: link:../onetalk-contract version: link:../../packages/onetalk-contract
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22.10.2 specifier: ^22.10.2
@@ -41,16 +41,7 @@ importers:
dependencies: dependencies:
'@trade-message-center/onetalk-contract': '@trade-message-center/onetalk-contract':
specifier: workspace:* specifier: workspace:*
version: link:../onetalk-contract version: link:../../packages/onetalk-contract
devDependencies:
'@types/node':
specifier: ^22.10.2
version: 22.20.1
typescript:
specifier: ^5.7.3
version: 5.7.3
apps/onetalk-contract:
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22.10.2 specifier: ^22.10.2
@@ -66,7 +57,7 @@ importers:
version: 11.3.0 version: 11.3.0
'@trade-message-center/onetalk-contract': '@trade-message-center/onetalk-contract':
specifier: workspace:* specifier: workspace:*
version: link:../onetalk-contract version: link:../../packages/onetalk-contract
drizzle-orm: drizzle-orm:
specifier: ^0.45.2 specifier: ^0.45.2
version: 0.45.2(postgres@3.4.9) version: 0.45.2(postgres@3.4.9)
@@ -87,6 +78,15 @@ importers:
specifier: ^5.7.3 specifier: ^5.7.3
version: 5.7.3 version: 5.7.3
packages/onetalk-contract:
devDependencies:
'@types/node':
specifier: ^22.10.2
version: 22.20.1
typescript:
specifier: ^5.7.3
version: 5.7.3
packages: packages:
'@drizzle-team/brocli@0.10.2': '@drizzle-team/brocli@0.10.2':
+1
View File
@@ -1,4 +1,5 @@
packages: packages:
- apps/* - apps/*
- packages/*
allowBuilds: allowBuilds:
esbuild: true esbuild: true
+1 -1
View File
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
const workspaceDir = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); const workspaceDir = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
export const GENERATED_DIST_RELATIVE_PATHS = [ export const GENERATED_DIST_RELATIVE_PATHS = [
"apps/onetalk-contract/dist", "packages/onetalk-contract/dist",
"apps/server/dist", "apps/server/dist",
"apps/chrome-extension/dist", "apps/chrome-extension/dist",
]; ];
+12 -6
View File
@@ -25,13 +25,19 @@ const readJsonFile = (filePath) => {
} }
}; };
/** 枚举 apps 下可参与版本镜像的 workspace package。 */ const workspacePackageDirectories = ["apps", "packages"];
/** 按 workspace 根目录顺序枚举可参与版本镜像的 package。 */
export const listWorkspacePackagePaths = (rootDir = workspaceDir) => export const listWorkspacePackagePaths = (rootDir = workspaceDir) =>
readdirSync(resolve(rootDir, "apps"), { withFileTypes: true }) workspacePackageDirectories.flatMap((directory) => {
.filter((entry) => entry.isDirectory()) const directoryPath = resolve(rootDir, directory);
.map((entry) => resolve(rootDir, "apps", entry.name, "package.json")) if (!existsSync(directoryPath)) return [];
.filter((packagePath) => existsSync(packagePath)) return readdirSync(directoryPath, { withFileTypes: true })
.sort(); .filter((entry) => entry.isDirectory())
.map((entry) => resolve(directoryPath, entry.name, "package.json"))
.filter((packagePath) => existsSync(packagePath))
.sort();
});
/** 校验同时满足 npm 与 Chrome Manifest 的三段版本。 */ /** 校验同时满足 npm 与 Chrome Manifest 的三段版本。 */
export const validatePackageVersion = (version, source = "package.json") => { export const validatePackageVersion = (version, source = "package.json") => {
+20 -6
View File
@@ -3,13 +3,14 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join, relative } from "node:path";
import test from "node:test"; import test from "node:test";
import { import {
bumpPackageVersion, bumpPackageVersion,
readRootPackageVersion, readRootPackageVersion,
validatePackageVersion, validatePackageVersion,
listWorkspacePackagePaths,
writeRootPackageVersion, writeRootPackageVersion,
} from "./package-version.mjs"; } from "./package-version.mjs";
import { checkPackageVersions } from "./check-package-versions.mjs"; import { checkPackageVersions } from "./check-package-versions.mjs";
@@ -18,16 +19,17 @@ import { syncPackageVersions } from "./sync-package-versions.mjs";
const createWorkspace = (rootVersion, packageVersions) => { const createWorkspace = (rootVersion, packageVersions) => {
const rootDir = mkdtempSync(join(tmpdir(), "tmc-package-version-")); const rootDir = mkdtempSync(join(tmpdir(), "tmc-package-version-"));
mkdirSync(join(rootDir, "apps")); mkdirSync(join(rootDir, "apps"));
mkdirSync(join(rootDir, "packages"));
writeFileSync( writeFileSync(
join(rootDir, "package.json"), join(rootDir, "package.json"),
`${JSON.stringify({ name: "fixture", version: rootVersion }, null, 4)}\n`, `${JSON.stringify({ name: "fixture", version: rootVersion }, null, 4)}\n`,
); );
for (const [name, version] of Object.entries(packageVersions)) { for (const [relativePackagePath, version] of Object.entries(packageVersions)) {
const packageDir = join(rootDir, "apps", name); const packageDir = join(rootDir, relativePackagePath);
mkdirSync(packageDir); mkdirSync(packageDir);
writeFileSync( writeFileSync(
join(packageDir, "package.json"), join(packageDir, "package.json"),
`${JSON.stringify({ name, version }, null, 4)}\n`, `${JSON.stringify({ name: relativePackagePath, version }, null, 4)}\n`,
); );
} }
return rootDir; return rootDir;
@@ -72,13 +74,25 @@ test("bumps and writes the root version without creating another version source"
assert.equal(readRootPackageVersion(rootDir), "0.8.17"); assert.equal(readRootPackageVersion(rootDir), "0.8.17");
}); });
test("syncs and checks every app package against the root version", () => { test("syncs and checks app and shared workspace package mirrors against the root version", () => {
const rootDir = createWorkspace("1.2.3", { extension: "0.1.0", server: "0.1.0" }); const rootDir = createWorkspace("1.2.3", {
"apps/extension": "0.1.0",
"apps/server": "0.1.0",
"packages/contract": "0.1.0",
});
assert.throws(() => checkPackageVersions(rootDir), /must equal root version 1.2.3/u); assert.throws(() => checkPackageVersions(rootDir), /must equal root version 1.2.3/u);
syncPackageVersions(rootDir); syncPackageVersions(rootDir);
assert.equal(readRootPackageVersion(rootDir), "1.2.3"); assert.equal(readRootPackageVersion(rootDir), "1.2.3");
checkPackageVersions(rootDir); checkPackageVersions(rootDir);
assert.deepEqual(
listWorkspacePackagePaths(rootDir).map((packagePath) => relative(rootDir, packagePath)),
[
"apps/extension/package.json",
"apps/server/package.json",
"packages/contract/package.json",
],
);
}); });
test("syncs version mirrors before root quality commands check them", () => { test("syncs version mirrors before root quality commands check them", () => {
+18 -6
View File
@@ -11,12 +11,17 @@ import { prepareRelease, publishRelease } from "./release.mjs";
const createWorkspace = (version = "0.8.16") => { const createWorkspace = (version = "0.8.16") => {
const rootDir = mkdtempSync(join(tmpdir(), "tmc-release-")); const rootDir = mkdtempSync(join(tmpdir(), "tmc-release-"));
mkdirSync(join(rootDir, "apps")); mkdirSync(join(rootDir, "apps"));
mkdirSync(join(rootDir, "packages"));
writeFileSync( writeFileSync(
join(rootDir, "package.json"), join(rootDir, "package.json"),
`${JSON.stringify({ name: "fixture", version }, null, 4)}\n`, `${JSON.stringify({ name: "fixture", version }, null, 4)}\n`,
); );
for (const name of ["extension", "server"]) { for (const [directory, name] of [
const packageDir = join(rootDir, "apps", name); ["apps", "extension"],
["apps", "server"],
["packages", "contract"],
]) {
const packageDir = join(rootDir, directory, name);
mkdirSync(packageDir); mkdirSync(packageDir);
writeFileSync( writeFileSync(
join(packageDir, "package.json"), join(packageDir, "package.json"),
@@ -41,7 +46,12 @@ const createRunner = (responses) => {
return { calls, execute }; return { calls, execute };
}; };
const expectedFiles = ["package.json", "apps/extension/package.json", "apps/server/package.json"]; const expectedFiles = [
"package.json",
"apps/extension/package.json",
"apps/server/package.json",
"packages/contract/package.json",
];
test("prepares a patch release using only root-derived version mirrors", () => { test("prepares a patch release using only root-derived version mirrors", () => {
const rootDir = createWorkspace(); const rootDir = createWorkspace();
@@ -63,9 +73,11 @@ test("prepares a patch release using only root-derived version mirrors", () => {
assert.equal(prepareRelease({ rootDir, execute }), "0.8.17"); assert.equal(prepareRelease({ rootDir, execute }), "0.8.17");
assert.equal(JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")).version, "0.8.17"); assert.equal(JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")).version, "0.8.17");
for (const packageName of ["extension", "server"]) { for (const packagePath of expectedFiles.slice(1)) {
const packagePath = join(rootDir, "apps", packageName, "package.json"); assert.equal(
assert.equal(JSON.parse(readFileSync(packagePath, "utf8")).version, "0.8.17"); JSON.parse(readFileSync(join(rootDir, packagePath), "utf8")).version,
"0.8.17",
);
} }
assert.deepEqual( assert.deepEqual(
calls.filter(({ command }) => command === "pnpm").map(({ arguments_ }) => arguments_), calls.filter(({ command }) => command === "pnpm").map(({ arguments_ }) => arguments_),