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
+1
View File
@@ -2,6 +2,7 @@ node_modules/
.pnpm-store/
dist/
apps/*/dist/
packages/*/dist/
.vite/
*.tsbuildinfo
.env
+1 -1
View File
@@ -164,7 +164,7 @@ packages:
mind-test-harness:
path: apps/mind-test-harness
onetalk-contract:
path: apps/onetalk-contract
path: packages/onetalk-contract
server:
path: apps/server
default_package: @trade-message-center/chrome-extension
@@ -288,7 +288,7 @@ decodeOneTalkMessageContent(source: OneTalkMessageContentSource) ->
| { 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
+2 -2
View File
@@ -84,13 +84,13 @@ const retryLimit = options.retryLimit ?? DEFAULT_RETRY_LIMIT;
### 1. Scope / Trigger
- 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
- `validatePackageVersion(value: unknown, source?: string): string`:验证同时满足 package metadata 和 Chrome Manifest 的三段数字版本。
- `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:ensure`:先执行 `version:sync`,再执行 `version:check`;根级 dev/build/typecheck/test/format:check 通过它获得稳定的一致版本环境。
- `TMC_PACKAGE_VERSION`:由 workspace 根命令注入给下游扩展命令;扩展直接执行时缺失必须失败。
+1 -1
View File
@@ -86,7 +86,7 @@ export const isObjectRecord = (value: unknown): value is Record<string, unknown>
### 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
current-realm plain objects and null-prototype objects, and rejects arrays,
primitives, class/custom-prototype values, and cross-realm ordinary objects.
@@ -8,11 +8,11 @@ scope_lanes:
path: /Users/ybf/work/trade-message-center
role: task-branch
owns:
- apps/onetalk-contract/src/content.ts
- apps/onetalk-contract/src/guards.ts
- apps/onetalk-contract/src/contact-profiles.ts
- apps/onetalk-contract/src/index.ts
- apps/onetalk-contract/test/contract.test.ts
- packages/onetalk-contract/src/content.ts
- packages/onetalk-contract/src/guards.ts
- packages/onetalk-contract/src/contact-profiles.ts
- packages/onetalk-contract/src/index.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/history.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
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/chrome-extension/package.json apps/chrome-extension/package.json
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
RUN pnpm --filter @trade-message-center/server build && \
@@ -436,7 +436,7 @@ Base64
→ normalized image content
```
输出合同定义于 [`content.ts`](../apps/onetalk-contract/src/content.ts)
输出合同定义于 [`content.ts`](../packages/onetalk-contract/src/content.ts)
```ts
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 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)
- 归一化媒体合同:[`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)
- 当前文本发送确认:[`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)
@@ -416,7 +416,7 @@ nodeName
### 10.1 共享内容解码器
`apps/onetalk-contract` 或其最近共同父目录增加一个唯一的内容投影器,输入原始 `content`,输出判别联合:
`packages/onetalk-contract` 或其最近共同父目录增加一个唯一的内容投影器,输入原始 `content`,输出判别联合:
```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` |
| 页面到 SW | `apps/chrome-extension/src/onetalk/service-worker/sync-engine/helpers.ts` | 原样复制观察字段 |
| IndexedDB 校验 | `apps/chrome-extension/src/onetalk/service-worker/storage.ts` | 只检查 JSON 与基础字段类型 |
| 共享合同 | `apps/onetalk-contract/src/model.ts` | `content` 为任意 JSON value |
| 共享 decoder | `apps/onetalk-contract/src/decoder.ts` | 不理解 text/image/file 语义 |
| 共享合同 | `packages/onetalk-contract/src/model.ts` | `content` 为任意 JSON value |
| 共享 decoder | `packages/onetalk-contract/src/decoder.ts` | 不理解 text/image/file 语义 |
| Bright 清洗 | `apps/server/src/onetalk/service.ts` | 按键过滤对象,字符串原样通过 |
| Bright 持久化 | `apps/server/src/database/schema/onetalk.ts` | `content jsonb``text nullable` |
| 开发展示 | `apps/mind-test-harness/src/harness.ts` | 只展示 raw JSON |
+2 -1
View File
@@ -3,7 +3,8 @@
"version": "0.8.27",
"private": true,
"workspaces": [
"apps/*"
"apps/*",
"packages/*"
],
"type": "module",
"scripts": {
+12 -12
View File
@@ -25,7 +25,7 @@ importers:
dependencies:
'@trade-message-center/onetalk-contract':
specifier: workspace:*
version: link:../onetalk-contract
version: link:../../packages/onetalk-contract
devDependencies:
'@types/node':
specifier: ^22.10.2
@@ -41,16 +41,7 @@ importers:
dependencies:
'@trade-message-center/onetalk-contract':
specifier: workspace:*
version: link:../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:
version: link:../../packages/onetalk-contract
devDependencies:
'@types/node':
specifier: ^22.10.2
@@ -66,7 +57,7 @@ importers:
version: 11.3.0
'@trade-message-center/onetalk-contract':
specifier: workspace:*
version: link:../onetalk-contract
version: link:../../packages/onetalk-contract
drizzle-orm:
specifier: ^0.45.2
version: 0.45.2(postgres@3.4.9)
@@ -87,6 +78,15 @@ importers:
specifier: ^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:
'@drizzle-team/brocli@0.10.2':
+1
View File
@@ -1,4 +1,5 @@
packages:
- apps/*
- packages/*
allowBuilds:
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)), "..");
export const GENERATED_DIST_RELATIVE_PATHS = [
"apps/onetalk-contract/dist",
"packages/onetalk-contract/dist",
"apps/server/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) =>
readdirSync(resolve(rootDir, "apps"), { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => resolve(rootDir, "apps", entry.name, "package.json"))
.filter((packagePath) => existsSync(packagePath))
.sort();
workspacePackageDirectories.flatMap((directory) => {
const directoryPath = resolve(rootDir, directory);
if (!existsSync(directoryPath)) return [];
return readdirSync(directoryPath, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => resolve(directoryPath, entry.name, "package.json"))
.filter((packagePath) => existsSync(packagePath))
.sort();
});
/** 校验同时满足 npm 与 Chrome Manifest 的三段版本。 */
export const validatePackageVersion = (version, source = "package.json") => {
+20 -6
View File
@@ -3,13 +3,14 @@
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { join, relative } from "node:path";
import test from "node:test";
import {
bumpPackageVersion,
readRootPackageVersion,
validatePackageVersion,
listWorkspacePackagePaths,
writeRootPackageVersion,
} from "./package-version.mjs";
import { checkPackageVersions } from "./check-package-versions.mjs";
@@ -18,16 +19,17 @@ import { syncPackageVersions } from "./sync-package-versions.mjs";
const createWorkspace = (rootVersion, packageVersions) => {
const rootDir = mkdtempSync(join(tmpdir(), "tmc-package-version-"));
mkdirSync(join(rootDir, "apps"));
mkdirSync(join(rootDir, "packages"));
writeFileSync(
join(rootDir, "package.json"),
`${JSON.stringify({ name: "fixture", version: rootVersion }, null, 4)}\n`,
);
for (const [name, version] of Object.entries(packageVersions)) {
const packageDir = join(rootDir, "apps", name);
for (const [relativePackagePath, version] of Object.entries(packageVersions)) {
const packageDir = join(rootDir, relativePackagePath);
mkdirSync(packageDir);
writeFileSync(
join(packageDir, "package.json"),
`${JSON.stringify({ name, version }, null, 4)}\n`,
`${JSON.stringify({ name: relativePackagePath, version }, null, 4)}\n`,
);
}
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");
});
test("syncs and checks every app package against the root version", () => {
const rootDir = createWorkspace("1.2.3", { extension: "0.1.0", server: "0.1.0" });
test("syncs and checks app and shared workspace package mirrors against the root version", () => {
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);
syncPackageVersions(rootDir);
assert.equal(readRootPackageVersion(rootDir), "1.2.3");
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", () => {
+18 -6
View File
@@ -11,12 +11,17 @@ import { prepareRelease, publishRelease } from "./release.mjs";
const createWorkspace = (version = "0.8.16") => {
const rootDir = mkdtempSync(join(tmpdir(), "tmc-release-"));
mkdirSync(join(rootDir, "apps"));
mkdirSync(join(rootDir, "packages"));
writeFileSync(
join(rootDir, "package.json"),
`${JSON.stringify({ name: "fixture", version }, null, 4)}\n`,
);
for (const name of ["extension", "server"]) {
const packageDir = join(rootDir, "apps", name);
for (const [directory, name] of [
["apps", "extension"],
["apps", "server"],
["packages", "contract"],
]) {
const packageDir = join(rootDir, directory, name);
mkdirSync(packageDir);
writeFileSync(
join(packageDir, "package.json"),
@@ -41,7 +46,12 @@ const createRunner = (responses) => {
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", () => {
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(JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")).version, "0.8.17");
for (const packageName of ["extension", "server"]) {
const packagePath = join(rootDir, "apps", packageName, "package.json");
assert.equal(JSON.parse(readFileSync(packagePath, "utf8")).version, "0.8.17");
for (const packagePath of expectedFiles.slice(1)) {
assert.equal(
JSON.parse(readFileSync(join(rootDir, packagePath), "utf8")).version,
"0.8.17",
);
}
assert.deepEqual(
calls.filter(({ command }) => command === "pnpm").map(({ arguments_ }) => arguments_),