logistics-development
SKILL.md
Logistics Adapter Development Guide (物流 Adapter 開發指南)
Overview
本指南說明如何基於 @rytass/logistics 基礎套件開發新的物流服務適配器。
Base Package Architecture
@rytass/logistics (Base)
├── LogisticsService<T> # 核心服務介面
├── LogisticsInterface<T> # 配置介面
├── LogisticsBaseStatus # 基礎狀態類型 ('DELIVERED' | 'DELIVERING' | 'SHELVED')
├── LogisticsStatus<T> # 泛型狀態類型
├── LogisticsTraceResponse # 追蹤結果
├── LogisticsStatusHistory # 狀態歷史
├── LogisticsErrorInterface # 錯誤介面
├── LogisticsError # 統一錯誤類別
└── ErrorCode # 錯誤代碼列舉
Core Interfaces
LogisticsService
interface LogisticsService<T extends LogisticsInterface<LogisticsStatus<T>>> {
trace(request: string): Promise<LogisticsTraceResponse<T>[]>;
trace(request: string[]): Promise<LogisticsTraceResponse<T>[]>;
}
LogisticsInterface
interface LogisticsInterface<T = LogisticsBaseStatus> {
reference?: T;
url: string;
}
type LogisticsBaseStatus = 'DELIVERED' | 'DELIVERING' | 'SHELVED';
Response Types
interface LogisticsTraceResponse<K extends LogisticsInterface<LogisticsStatus<K>>> {
logisticsId: string;
statusHistory: LogisticsStatusHistory<K['reference']>[];
}
interface LogisticsStatusHistory<T> {
date: string;
status: T;
}
interface LogisticsErrorInterface {
readonly code: string;
readonly message?: string;
}
Implementing a New Adapter
Step 1: Define Status Types
// my-logistics-adapter/src/typings.ts
export type MyLogisticsStatus =
| 'DELIVERED'
| 'DELIVERING'
| 'SHELVED'
| 'PENDING'
| 'CANCELLED';
export interface MyLogisticsInterface<T> extends LogisticsInterface<T> {
apiKey: string;
apiSecret: string;
ignoreNotFound?: boolean;
}
Step 2: Create Status Map
// my-logistics-adapter/src/constants.ts
export const MyLogisticsStatusMap: Record<string, MyLogisticsStatus> = {
'已送達': 'DELIVERED',
'配送中': 'DELIVERING',
'待取': 'SHELVED',
'待處理': 'PENDING',
'已取消': 'CANCELLED',
};
Step 3: Implement Service Class
// my-logistics-adapter/src/my-logistics.service.ts
import { LogisticsService, LogisticsTraceResponse, LogisticsError, ErrorCode } from '@rytass/logistics';
import axios from 'axios';
export class MyLogisticsService<T extends MyLogisticsInterface<LogisticsStatus<T>>>
implements LogisticsService<T> {
constructor(private readonly configuration: T) {}
async trace(logisticsIds: string | string[]): Promise<LogisticsTraceResponse<T>[]> {
const ids = Array.isArray(logisticsIds) ? logisticsIds : [logisticsIds];
return Promise.all(ids.map(id => this.getLogisticsStatus(id)));
}
private async getLogisticsStatus(trackingId: string): Promise<LogisticsTraceResponse<T>> {
try {
const response = await axios.get(`${this.configuration.url}/track/${trackingId}`, {
headers: {
'X-API-Key': this.configuration.apiKey,
'X-API-Secret': this.configuration.apiSecret,
},
});
if (!response.data.success) {
if (this.configuration.ignoreNotFound) {
return { logisticsId: trackingId, statusHistory: [] };
}
throw new LogisticsError(ErrorCode.NOT_FOUND_ERROR, `Tracking ${trackingId} not found`);
}
return {
logisticsId: trackingId,
statusHistory: this.mapStatusHistory(response.data.history),
};
} catch (error) {
if (error instanceof LogisticsError) throw error;
if (axios.isAxiosError(error)) {
if (error.response?.status === 403) {
throw new LogisticsError(ErrorCode.PERMISSION_DENIED, 'Invalid API credentials');
}
if (error.response?.status === 400) {
throw new LogisticsError(ErrorCode.INVALID_PARAMETER, 'Invalid tracking number');
}
}
throw new LogisticsError(ErrorCode.NOT_IMPLEMENTED, 'Unknown error');
}
}
private mapStatusHistory(history: any[]): LogisticsStatusHistory<T['reference']>[] {
return history.map(item => ({
date: item.timestamp,
status: MyLogisticsStatusMap[item.status] || 'DELIVERING',
}));
}
}
Step 4: Export Default Configuration
// my-logistics-adapter/src/index.ts
export * from './typings';
export * from './constants';
export * from './my-logistics.service';
export const MyLogistics: MyLogisticsInterface<MyLogisticsStatus> = {
url: 'https://api.mylogistics.com/v1',
apiKey: '',
apiSecret: '',
ignoreNotFound: false,
};
Error Handling
ErrorCode Reference
| Code | Constant | Usage |
|---|---|---|
| 999 | NOT_IMPLEMENTED | 未實現的功能 |
| 101 | NOT_FOUND_ERROR | 找不到追蹤號碼 |
| 102 | PERMISSION_DENIED | API 認證失敗 |
| 103 | INVALID_PARAMETER | 無效的參數 |
Best Practices
// 1. 使用統一的 LogisticsError
throw new LogisticsError(ErrorCode.NOT_FOUND_ERROR, 'Custom message');
// 2. 支援 ignoreNotFound 選項
if (this.configuration.ignoreNotFound) {
return { logisticsId: id, statusHistory: [] };
}
// 3. 適當的 HTTP 錯誤映射
if (response.status === 403) {
throw new LogisticsError(ErrorCode.PERMISSION_DENIED);
}
Testing Guidelines
// __tests__/my-logistics.spec.ts
import { MyLogisticsService, MyLogistics } from '../src';
import { LogisticsError, ErrorCode } from '@rytass/logistics';
describe('MyLogisticsService', () => {
const service = new MyLogisticsService({
...MyLogistics,
apiKey: 'test-key',
apiSecret: 'test-secret',
});
it('should trace single package', async () => {
const result = await service.trace('TRACK-001');
expect(result).toHaveLength(1);
expect(result[0].logisticsId).toBe('TRACK-001');
});
it('should trace multiple packages', async () => {
const result = await service.trace(['TRACK-001', 'TRACK-002']);
expect(result).toHaveLength(2);
});
it('should handle not found with ignoreNotFound', async () => {
const serviceWithIgnore = new MyLogisticsService({
...MyLogistics,
ignoreNotFound: true,
});
const result = await serviceWithIgnore.trace('INVALID');
expect(result[0].statusHistory).toHaveLength(0);
});
});
Package Structure
my-logistics-adapter/
├── src/
│ ├── index.ts
│ ├── typings.ts
│ ├── constants.ts
│ └── my-logistics.service.ts
├── __tests__/
│ └── my-logistics.spec.ts
├── package.json
├── tsconfig.build.json
└── README.md
Publishing Checklist
- 實現
LogisticsService介面 - 定義完整的狀態類型
- 實現錯誤處理
- 支援批量追蹤
- 撰寫單元測試
- 更新 README
- 遵循
@rytass/logistics-adapter-*命名規範
Weekly Installs
8
Repository
rytass/utilsGitHub Stars
6
First Seen
Feb 5, 2026
Security Audits
Installed on
gemini-cli8
github-copilot8
codex8
kimi-cli8
amp8
opencode8