TypeScript OSDK:为前端开发者打造类型安全的 Ontology SDK
TypeScript OSDK(Ontology Software Development Kit)是 coomia-dip 面向前端和全栈开发者的核心 SDK,它将 Ontology 的类型系统映射为 TypeScript 类型,通过代码生成实现编译期类型安全。开发者可以像操作本地对象一样操作 Ontology 中的 ObjectType、LinkType 和 Action,同时获得完整的 IDE 自动补全、类型推断和编译期错误检查。本文从架构设计、类型生成、运行时层、查询 DSL 到测试策略,全面解析 TypeScript OSDK 的设计与实现。
“系列:S6 平台工程 · 第 15 篇 | 难度:高级 | 阅读时间:18 分钟
TypeScript OSDK:为前端开发者打造类型安全的 Ontology SDK
#TL;DR
TypeScript OSDK(Ontology Software Development Kit)是 coomia-dip 面向前端和全栈开发者的核心 SDK,它将 Ontology 的类型系统映射为 TypeScript 类型,通过代码生成实现编译期类型安全。开发者可以像操作本地对象一样操作 Ontology 中的 ObjectType、LinkType 和 Action,同时获得完整的 IDE 自动补全、类型推断和编译期错误检查。本文从架构设计、类型生成、运行时层、查询 DSL 到测试策略,全面解析 TypeScript OSDK 的设计与实现。
#1. 为什么需要 TypeScript OSDK
#1.1 前端开发的 Ontology 挑战
coomia-dip 以 Ontology 为核心数据模型,前端应用需要频繁与 Ontology 交互。传统的 REST API 调用方式存在诸多痛点:
- 类型不安全:API 返回
any类型的 JSON,属性名拼写错误只能在运行时发现 - 文档脱节:API 文档与实际接口容易不一致,开发者需要反复查阅文档
- 模板代码多:每个 API 调用都需要手写请求构建、响应解析、错误处理
- 缺乏领域语义:开发者看到的是 HTTP 方法和 URL,而非 Ontology 的领域概念
#1.2 对标 Palantir OSDK
Palantir 在 2023 年发布了 TypeScript OSDK,让开发者可以用类型安全的方式访问 Foundry Ontology。coomia-dip 的 TypeScript OSDK 实现了等价能力:
| 能力 | Palantir OSDK | coomia-dip OSDK |
|---|---|---|
| 类型生成 | 从 Ontology Schema 生成 | 从 Ontology Schema 生成 |
| ObjectType 操作 | CRUD + 搜索 | CRUD + 搜索 + 流式 |
| LinkType 遍历 | 支持 | 支持 + 深度遍历 |
| Action 执行 | 类型安全的参数 | 类型安全 + 乐观更新 |
| 查询 DSL | 链式 API | 链式 + 组合式 |
| 实时订阅 | ObjectSet 监听 | WebSocket + SSE |
| 批量操作 | 支持 | 支持 + 事务 |
| 离线支持 | 部分 | IndexedDB 本地缓存 |
#1.3 设计原则
TypeScript OSDK 遵循以下设计原则:
- 类型优先(Type-First):所有 API 在编译期完全类型安全
- 零配置(Zero Config):默认配置即可开箱使用
- 渐进式复杂度(Progressive Complexity):简单操作一行代码,复杂操作渐进暴露
- 框架无关(Framework Agnostic):不绑定 React/Vue/Angular,可在任何环境使用
- 可摇树(Tree-Shakeable):未使用的功能不会打入最终 bundle
#2. 整体架构
#2.1 分层架构
┌─────────────────────────────────────────────────┐
│ Application Layer │
│ (React/Vue/Angular/Node.js) │
└─────────────┬───────────────────────────────────┘
│
┌─────────────▼───────────────────────────────────┐
│ Generated Type Layer │
│ (ObjectTypes, LinkTypes, Actions — 编译期生成) │
└─────────────┬───────────────────────────────────┘
│
┌─────────────▼───────────────────────────────────┐
│ Runtime Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Client │ │ Cache │ │ Subscription │ │
│ │ Manager │ │ Manager │ │ Manager │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Query │ │ Batch │ │ Auth │ │
│ │ Builder │ │ Executor │ │ Provider │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────┬───────────────────────────────────┘
│
┌─────────────▼───────────────────────────────────┐
│ Transport Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ HTTP/2 │ │ WebSocket│ │ gRPC-Web │ │
│ │ Client │ │ Client │ │ Client │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
#2.2 代码生成流程
Ontology Schema ──▶ Schema Introspector ──▶ Type Generator ──▶ .ts files
(Runtime) (CLI Tool) (Template Engine)
步骤:
1. CLI 连接 coomia-dip API,获取当前 Ontology Schema
2. Schema Introspector 解析 ObjectType、LinkType、ActionType 定义
3. Type Generator 使用 ts-morph 生成 TypeScript 类型和操作接口
4. 生成文件输出到 `src/generated/` 目录
5. 开发者直接 import 使用
#3. 类型生成系统
#3.1 ObjectType 类型生成
给定一个 Ontology ObjectType 定义:
{
"apiName": "Employee",
"displayName": "员工",
"properties": {
"employeeId": { "type": "string", "primaryKey": true },
"name": { "type": "string" },
"department": { "type": "string" },
"salary": { "type": "double" },
"hireDate": { "type": "timestamp" },
"isActive": { "type": "boolean" },
"tags": { "type": "array", "items": { "type": "string" } },
"metadata": { "type": "object" }
}
}
生成的 TypeScript 类型:
// src/generated/objects/Employee.ts
/** 员工 ObjectType */
export interface Employee {
readonly employeeId: string;
readonly name: string;
readonly department: string;
readonly salary: number;
readonly hireDate: Date;
readonly isActive: boolean;
readonly tags: readonly string[];
readonly metadata: Record<string, unknown>;
readonly $primaryKey: string;
readonly $objectType: "Employee";
}
/** Employee 的可写字段 */
export interface EmployeeWritable {
name?: string;
department?: string;
salary?: number;
hireDate?: Date;
isActive?: boolean;
tags?: string[];
metadata?: Record<string, unknown>;
}
/** Employee 的过滤条件 */
export interface EmployeeFilter {
employeeId?: StringFilter;
name?: StringFilter;
department?: StringFilter;
salary?: NumericFilter;
hireDate?: DateFilter;
isActive?: BooleanFilter;
tags?: ArrayFilter<string>;
$and?: EmployeeFilter[];
$or?: EmployeeFilter[];
$not?: EmployeeFilter;
}
/** Employee 的排序字段 */
export type EmployeeOrderBy =
| "employeeId"
| "name"
| "department"
| "salary"
| "hireDate"
| "-employeeId"
| "-name"
| "-department"
| "-salary"
| "-hireDate";
/** Employee 的属性名集合 */
export type EmployeePropertyName = keyof Employee;
#3.2 LinkType 类型生成
// src/generated/links/EmployeeLinks.ts
/** Employee 的关联关系 */
export interface EmployeeLinks {
/** 员工所属部门 */
department: SingleLink<Department>;
/** 员工管理的项目 */
managedProjects: MultiLink<Project>;
/** 员工的直属上级 */
manager: SingleLink<Employee>;
/** 员工的直属下属 */
directReports: MultiLink<Employee>;
}
/** 单关联:恰好一个关联对象 */
export interface SingleLink<T> {
get(): Promise<T>;
getRid(): string;
}
/** 多关联:零到多个关联对象 */
export interface MultiLink<T> {
list(options?: PaginationOptions): Promise<Page<T>>;
count(): Promise<number>;
getRids(): Promise<string[]>;
where(filter: FilterOf<T>): MultiLink<T>;
orderBy(order: OrderByOf<T>): MultiLink<T>;
}
#3.3 ActionType 类型生成
// src/generated/actions/PromoteEmployee.ts
/** 晋升员工 Action */
export interface PromoteEmployeeParams {
/** 员工 ID */
employeeId: string;
/** 新职级 */
newLevel: number;
/** 薪资涨幅百分比 */
salaryIncreasePercent: number;
/** 生效日期 */
effectiveDate?: Date;
/** 备注 */
notes?: string;
}
export interface PromoteEmployeeResult {
/** 更新后的员工对象 */
employee: Employee;
/** 审批状态 */
approvalStatus: "approved" | "pending" | "rejected";
}
export type PromoteEmployeeAction = Action<
PromoteEmployeeParams,
PromoteEmployeeResult
>;
#4. 运行时 Client
#4.1 Client 初始化
import { OntoPlatformClient } from "@onto/osdk";
// 最简初始化
const client = new OntoPlatformClient({
baseUrl: "https://coomia-dip.example.com",
token: "your-api-token",
});
// 完整配置
const client = new OntoPlatformClient({
baseUrl: "https://coomia-dip.example.com",
auth: {
type: "oauth2",
clientId: "my-app",
clientSecret: process.env.CLIENT_SECRET,
scopes: ["ontology:read", "ontology:write"],
},
cache: {
enabled: true,
ttl: 60_000, // 缓存 60 秒
maxSize: 10_000, // 最多缓存 10000 个对象
strategy: "lru",
},
retry: {
maxRetries: 3,
backoffMs: 1000,
},
timeout: 30_000,
interceptors: [loggingInterceptor, metricsInterceptor],
});
#4.2 ObjectType 操作
// 获取单个对象
const employee = await client.objects.Employee.get("emp-001");
console.log(employee.name); // 类型安全:string
console.log(employee.salary); // 类型安全:number
// 搜索对象
const results = await client.objects.Employee
.where({
department: { eq: "Engineering" },
salary: { gte: 100000 },
isActive: { eq: true },
})
.orderBy("-salary")
.select("name", "salary", "department")
.limit(20)
.list();
for (const emp of results.data) {
console.log(`${emp.name}: ${emp.salary}`);
}
// 创建对象
const newEmployee = await client.objects.Employee.create({
name: "张三",
department: "Engineering",
salary: 120000,
hireDate: new Date(),
isActive: true,
tags: ["senior", "fullstack"],
});
// 更新对象
await client.objects.Employee.update("emp-001", {
salary: 150000,
tags: ["senior", "fullstack", "lead"],
});
// 删除对象
await client.objects.Employee.delete("emp-001");
#4.3 LinkType 遍历
// 获取员工的部门
const dept = await client.objects.Employee
.get("emp-001")
.then(emp => emp.$links.department.get());
// 获取部门下的所有员工
const employees = await client.objects.Department
.get("dept-eng")
.then(dept => dept.$links.employees
.where({ isActive: { eq: true } })
.orderBy("name")
.list()
);
// 深度遍历:获取经理的所有下属的项目
const projects = await client.objects.Employee
.get("emp-manager-001")
.then(mgr => mgr.$links.directReports.list())
.then(reports =>
Promise.all(
reports.data.map(r => r.$links.managedProjects.list())
)
);
#4.4 Action 执行
// 执行 Action
const result = await client.actions.PromoteEmployee.execute({
employeeId: "emp-001",
newLevel: 5,
salaryIncreasePercent: 15,
effectiveDate: new Date("2026-04-01"),
notes: "优秀绩效晋升",
});
if (result.approvalStatus === "approved") {
console.log(`晋升成功,新薪资: ${result.employee.salary}`);
} else {
console.log(`晋升待审批`);
}
// Action 参数验证(编译期)
await client.actions.PromoteEmployee.execute({
employeeId: "emp-001",
newLevel: "five", // TS Error: Type 'string' is not assignable to type 'number'
salaryIncreasePercent: 15,
});
// 批量 Action
const results = await client.actions.PromoteEmployee.executeBatch([
{ employeeId: "emp-001", newLevel: 5, salaryIncreasePercent: 15 },
{ employeeId: "emp-002", newLevel: 4, salaryIncreasePercent: 10 },
{ employeeId: "emp-003", newLevel: 6, salaryIncreasePercent: 20 },
]);
#5. 查询 DSL 设计
#5.1 链式查询构建
// 复杂查询示例
const seniorEngineers = await client.objects.Employee
.where({
$and: [
{ department: { eq: "Engineering" } },
{ salary: { gte: 150000 } },
{
$or: [
{ tags: { contains: "senior" } },
{ tags: { contains: "staff" } },
],
},
],
})
.orderBy("-salary", "name")
.select("employeeId", "name", "salary", "tags")
.page({ size: 50, token: nextPageToken })
.list();
#5.2 聚合查询
// 按部门统计薪资
const stats = await client.objects.Employee
.where({ isActive: { eq: true } })
.aggregate({
groupBy: ["department"],
metrics: {
avgSalary: { field: "salary", op: "avg" },
maxSalary: { field: "salary", op: "max" },
headcount: { op: "count" },
},
orderBy: "-avgSalary",
});
// 结果类型自动推断
for (const group of stats.groups) {
console.log(
`${group.key.department}: avg=${group.metrics.avgSalary}, count=${group.metrics.headcount}`
);
}
#5.3 时间序列查询
// 时间序列数据查询
const metrics = await client.objects.ServerMetrics
.timeSeries({
property: "cpuUsage",
range: {
start: new Date("2026-03-01"),
end: new Date("2026-03-24"),
},
granularity: "1h",
aggregation: "avg",
});
#6. 实时订阅
#6.1 ObjectSet 订阅
// 订阅对象变更
const subscription = client.objects.Employee
.where({ department: { eq: "Engineering" } })
.subscribe({
onAdded(employee) {
console.log(`新员工: ${employee.name}`);
},
onUpdated(employee, previousVersion) {
console.log(`更新: ${employee.name}`);
if (employee.salary !== previousVersion.salary) {
console.log(
`薪资变更: ${previousVersion.salary} -> ${employee.salary}`
);
}
},
onRemoved(employeeId) {
console.log(`员工离职: ${employeeId}`);
},
onError(error) {
console.error("订阅错误:", error);
},
});
// 取消订阅
subscription.unsubscribe();
#6.2 WebSocket 传输
// 底层 WebSocket 管理
class SubscriptionManager {
private ws: WebSocket | null = null;
private subscriptions = new Map<string, SubscriptionHandler>();
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
connect(url: string): void {
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data) as SubscriptionMessage;
const handler = this.subscriptions.get(message.subscriptionId);
if (handler) {
switch (message.type) {
case "OBJECT_ADDED":
handler.onAdded?.(message.object);
break;
case "OBJECT_UPDATED":
handler.onUpdated?.(message.object, message.previous);
break;
case "OBJECT_REMOVED":
handler.onRemoved?.(message.objectId);
break;
}
}
};
this.ws.onclose = () => {
this.reconnect();
};
}
private reconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts),
30000
);
this.reconnectAttempts++;
setTimeout(() => {
this.connect(this.url);
// 重新订阅所有活跃订阅
for (const [id, handler] of this.subscriptions) {
this.resubscribe(id, handler);
}
}, delay);
}
}
#7. 缓存系统
#7.1 多级缓存
class CacheManager {
private l1Cache: Map<string, CacheEntry>; // 内存缓存
private l2Cache: IDBDatabase | null; // IndexedDB
constructor(options: CacheOptions) {
this.l1Cache = new Map();
if (options.persistent) {
this.initIndexedDB();
}
}
async get<T>(key: string): Promise<T | undefined> {
// L1: 内存
const l1Entry = this.l1Cache.get(key);
if (l1Entry && !this.isExpired(l1Entry)) {
return l1Entry.value as T;
}
// L2: IndexedDB
if (this.l2Cache) {
const l2Entry = await this.getFromIDB(key);
if (l2Entry && !this.isExpired(l2Entry)) {
this.l1Cache.set(key, l2Entry); // 回填 L1
return l2Entry.value as T;
}
}
return undefined;
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const entry: CacheEntry = {
value,
createdAt: Date.now(),
ttl: ttl ?? this.defaultTtl,
};
this.l1Cache.set(key, entry);
this.evictIfNeeded();
if (this.l2Cache) {
await this.setToIDB(key, entry);
}
}
invalidate(pattern: string): void {
for (const key of this.l1Cache.keys()) {
if (key.match(pattern)) {
this.l1Cache.delete(key);
}
}
}
}
#7.2 乐观更新
// Action 执行时的乐观更新
async function executeWithOptimisticUpdate<P, R>(
action: Action<P, R>,
params: P,
optimisticUpdate: (cache: CacheManager) => void,
): Promise<R> {
// 1. 立即应用乐观更新
const rollback = cache.snapshot();
optimisticUpdate(cache);
try {
// 2. 执行实际 Action
const result = await action.execute(params);
// 3. 用实际结果替换乐观值
cache.applyServerResponse(result);
return result;
} catch (error) {
// 4. 失败时回滚
cache.restore(rollback);
throw error;
}
}
// 使用示例
await executeWithOptimisticUpdate(
client.actions.PromoteEmployee,
{ employeeId: "emp-001", newLevel: 5, salaryIncreasePercent: 15 },
(cache) => {
cache.update("Employee:emp-001", (emp) => ({
...emp,
salary: emp.salary * 1.15,
}));
},
);
#8. 错误处理
#8.1 类型安全的错误体系
// 错误类型层次
abstract class OsdkError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
readonly requestId: string;
readonly timestamp: Date;
}
class ObjectNotFoundError extends OsdkError {
readonly code = "OBJECT_NOT_FOUND";
readonly statusCode = 404;
readonly objectType: string;
readonly objectId: string;
}
class PermissionDeniedError extends OsdkError {
readonly code = "PERMISSION_DENIED";
readonly statusCode = 403;
readonly requiredPermission: string;
}
class ValidationError extends OsdkError {
readonly code = "VALIDATION_ERROR";
readonly statusCode = 400;
readonly violations: ValidationViolation[];
}
class ActionExecutionError extends OsdkError {
readonly code = "ACTION_EXECUTION_FAILED";
readonly statusCode = 500;
readonly actionType: string;
readonly failureReason: string;
}
// 使用
try {
const emp = await client.objects.Employee.get("emp-999");
} catch (error) {
if (error instanceof ObjectNotFoundError) {
console.log(`对象不存在: ${error.objectType}/${error.objectId}`);
} else if (error instanceof PermissionDeniedError) {
console.log(`权限不足: 需要 ${error.requiredPermission}`);
}
}
#8.2 Result 模式
// 提供 Result 模式作为异常的替代
type Result<T, E = OsdkError> =
| { ok: true; value: T }
| { ok: false; error: E };
// 安全版本的 API
const result = await client.objects.Employee.getSafe("emp-001");
if (result.ok) {
console.log(result.value.name);
} else {
console.log(`错误: ${result.error.code}`);
}
#9. 代码生成 CLI
#9.1 CLI 命令
# 初始化 OSDK 项目
npx @onto/osdk init --base-url https://coomia-dip.example.com
# 生成类型
npx @onto/osdk generate \
--output src/generated \
--ontology my-ontology \
--format esm \
--strict
# 增量生成(仅更新变更的类型)
npx @onto/osdk generate --incremental
# 验证生成代码
npx @onto/osdk validate
# 查看 Ontology Schema
npx @onto/osdk schema --ontology my-ontology
#9.2 配置文件
{
"$schema": "https://coomia-dip.dev/osdk.schema.json",
"version": 1,
"connection": {
"baseUrl": "https://coomia-dip.example.com",
"auth": {
"type": "token",
"envVar": "ONTO_API_TOKEN"
}
},
"generate": {
"output": "src/generated",
"ontologies": ["employee-ontology", "project-ontology"],
"format": "esm",
"strict": true,
"features": {
"subscriptions": true,
"aggregations": true,
"timeSeries": true,
"batchOperations": true
}
}
}
#10. 框架集成
#10.1 React 集成
// @onto/osdk-react
import { useObject, useObjectSet, useAction } from "@onto/osdk-react";
function EmployeeCard({ employeeId }: { employeeId: string }) {
const { data: employee, isLoading, error } = useObject(
client.objects.Employee,
employeeId,
);
if (isLoading) return <Spinner />;
if (error) return <ErrorBanner error={error} />;
return (
<div>
<h2>{employee.name}</h2>
<p>部门: {employee.department}</p>
<p>薪资: {employee.salary.toLocaleString()}</p>
</div>
);
}
function EmployeeList() {
const { data, isLoading, fetchNextPage, hasNextPage } = useObjectSet(
client.objects.Employee
.where({ isActive: { eq: true } })
.orderBy("name")
.limit(20),
);
return (
<div>
{data?.pages.map(page =>
page.data.map(emp => (
<EmployeeCard key={emp.employeeId} employeeId={emp.employeeId} />
))
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>加载更多</button>
)}
</div>
);
}
#10.2 Vue 集成
// @onto/osdk-vue
import { useObject, useObjectSet } from "@onto/osdk-vue";
const { data: employee, loading, error } = useObject(
() => client.objects.Employee.get(props.employeeId),
{ watch: () => props.employeeId },
);
#11. 性能优化
#11.1 请求合并
class RequestBatcher {
private pending = new Map<string, Promise<unknown>>();
private batchWindow = 10; // ms
async get<T>(objectType: string, id: string): Promise<T> {
const key = `${objectType}:${id}`;
if (this.pending.has(key)) {
return this.pending.get(key) as Promise<T>;
}
const promise = this.scheduleBatch<T>(objectType, id);
this.pending.set(key, promise);
return promise;
}
private async scheduleBatch<T>(
objectType: string,
id: string,
): Promise<T> {
await new Promise(r => setTimeout(r, this.batchWindow));
const batchIds = [...this.pending.keys()]
.filter(k => k.startsWith(`${objectType}:`))
.map(k => k.split(":")[1]);
const results = await this.batchFetch(objectType, batchIds);
for (const [batchId, result] of Object.entries(results)) {
this.pending.delete(`${objectType}:${batchId}`);
}
return results[id] as T;
}
}
#11.2 Select 字段优化
// 只请求需要的字段,减少传输数据量
const employees = await client.objects.Employee
.select("name", "salary") // 只返回 name 和 salary
.list();
// 类型自动收窄
employees.data[0].name; // OK: string
employees.data[0].salary; // OK: number
employees.data[0].department; // TS Error: Property does not exist
#12. 安全与认证
#12.1 Token 管理
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
private expiresAt: number = 0;
async getToken(): Promise<string> {
if (this.accessToken && Date.now() < this.expiresAt - 60_000) {
return this.accessToken;
}
return this.refresh();
}
private async refresh(): Promise<string> {
const response = await fetch(`${this.baseUrl}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: this.refreshToken,
client_id: this.clientId,
}),
});
const data = await response.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.expiresAt = Date.now() + data.expires_in * 1000;
return this.accessToken!;
}
}
#12.2 请求签名
// 敏感操作的请求签名
class RequestSigner {
sign(request: RequestInit, timestamp: number): RequestInit {
const payload = `${request.method}:${request.url}:${timestamp}`;
const signature = hmacSHA256(payload, this.secretKey);
return {
...request,
headers: {
...request.headers,
"X-Request-Timestamp": timestamp.toString(),
"X-Request-Signature": signature,
},
};
}
}
#Key Takeaways
- 类型生成是 TypeScript OSDK 的核心,从 Ontology Schema 自动生成完整的 TypeScript 类型系统,实现编译期类型安全
- 分层架构将生成层、运行时层和传输层分离,每层可独立演进
- 查询 DSL 提供链式和组合式两种风格,支持过滤、排序、分页、聚合和时间序列
- 实时订阅通过 WebSocket 实现 ObjectSet 变更推送,支持自动重连和重新订阅
- 框架集成提供 React/Vue 的 hooks 封装,实现声明式数据绑定
#Next Article
下一篇我们将探讨异步 SDK 设计,了解如何构建支持 async/await 的高性能 SDK。
Tags: TypeScript OSDK 代码生成 类型安全 Ontology SDK React 前端 平台工程