ECC backend-patterns 技能:Node.js/Next.js 后端架构模式全解——从 API 设计、数据库优化到限流与日志
ECC 仓库中的 backend-patterns 技能文档(.agents/skills/backend-patterns/SKILL.md)是一份面向 Node.js、Express 与 Next.js API 路由的后端架构模式手册,覆盖 API 设计、Repository/Service 分层、数据库查询优化、Redis 缓存、集中式错误处理、JWT 鉴权与 RBAC、限流、后台任务队列和结构化日志九大主题。读完本文,你不仅能获得一套可直接复制运行的 TypeScript 参考实现,还能了解这份技能文档在 ECC 多 harness 分发体系中的存放位置、触发机制,以及同一技能在不同安装路径下的版本差异。
一、这份技能文档在 ECC 仓库中的位置与分发机制
ECC(The agent harness performance optimization system)将可复用的工程实践组织为"技能(Skill)",由 Agent 在构建或评审代码时按需加载。backend-patterns 的 frontmatter 声明了它的触发语义:
---
name: backend-patterns
description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes. Use when building or reviewing Node.js, Express, or Next.js API routes and their data access.
---
从仓库结构看,这份文档存在两个副本:
- 本文分析的
.agents/skills/backend-patterns/SKILL.md,是面向 Kimi Code / Antigravity 等 harness 的项目级技能发现位置。README.md 中明确写道,项目级.agents/skills/是官方发现位置之一; - 规范源文件 skills/backend-patterns/SKILL.md,两者内容高度一致,仅有个别差异(下文"限流"一节会重点说明)。
支撑这一分发机制的源码证据:
- scripts/lib/harness-capabilities.js 中 Antigravity 适配器的安装目标即
./.agents,说明.agents/skills/下放置的技能会被该 harness 原样发现; - package.json 的
files字段显式包含.agents/,保证该目录随仓库一起被打包分发; - 技能目录下的 agents/openai.yaml 提供了面向插件市场的展示元数据:
display_name为 "Backend Patterns",短描述为 "API, database, and server-side patterns",且allow_implicit_invocation: true——即允许 Agent 在匹配场景下隐式调用该技能,无需用户显式指名。
文档开头的 "When to Activate" 一节列出了七个触发场景,这也是理解该技能适用边界的关键:
- 设计 REST 或 GraphQL API 端点;
- 实现 Repository、Service、Controller 分层;
- 优化数据库查询(N+1、索引、连接池);
- 添加缓存(Redis、内存缓存、HTTP 缓存头);
- 搭建后台任务或异步处理;
- 结构化 API 的错误处理与校验;
- 构建中间件(鉴权、日志、限流)。
二、API 设计模式
RESTful 资源 URL 设计
文档给出的基线是"资源导向"的 URL 结构,方法语义与资源生命周期一一对应:
// PASS: Resource-based URLs
GET /api/markets # List resources
GET /api/markets/:id # Get single resource
POST /api/markets # Create resource
PUT /api/markets/:id # Replace resource
PATCH /api/markets/:id # Update resource
DELETE /api/markets/:id # Delete resource
// PASS: Query parameters for filtering, sorting, pagination
GET /api/markets?status=active&sort=volume&limit=20&offset=0
要点是:路径只放资源标识,过滤、排序、分页全部收敛到查询参数。这样 URL 保持正交,limit/offset 分页协议对所有列表端点统一生效。
Repository 模式:抽象数据访问
Repository 模式将"如何取数"与"取什么数"解耦。文档以接口 + 具体实现的方式给出范式:
// Abstract data access logic
interface MarketRepository {
findAll(filters?: MarketFilters): Promise<Market[]>
findById(id: string): Promise<Market | null>
create(data: CreateMarketDto): Promise<Market>
update(id: string, data: UpdateMarketDto): Promise<Market>
delete(id: string): Promise<void>
}
class SupabaseMarketRepository implements MarketRepository {
async findAll(filters?: MarketFilters): Promise<Market[]> {
let query = supabase.from('markets').select('*')
if (filters?.status) {
query = query.eq('status', filters.status)
}
if (filters?.limit) {
query = query.limit(filters.limit)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
// Other methods...
}
接口只暴露 CRUD 语义,具体实现 SupabaseMarketRepository 负责把过滤条件翻译成查询构造器调用,并对驱动返回的 error 字段做显式抛出。上层依赖 MarketRepository 接口而非 Supabase 客户端,后续更换为 Prisma、直连 Postgres 或内存实现时业务层零改动——这一点在下文的缓存装饰器设计中还会再次体现。
Service 层:业务逻辑与数据访问分离
Service 层通过构造函数注入 Repository,承载跨数据源的业务编排。文档用"向量搜索 + 回表"的例子展示了典型编排逻辑:
// Business logic separated from data access
class MarketService {
constructor(private marketRepo: MarketRepository) {}
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
// Business logic
const embedding = await generateEmbedding(query)
const results = await this.vectorSearch(embedding, limit)
// Fetch full data
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
// Sort by similarity
return markets.sort((a, b) => {
const scoreA = results.find(r => r.id === a.id)?.score || 0
const scoreB = results.find(r => r.id === b.id)?.score || 0
return scoreA - scoreB
})
}
private async vectorSearch(embedding: number[], limit: number) {
// Vector search implementation
}
}
这里有两个值得注意的实现细节:其一,向量检索先返回带相似度的轻量结果,再通过 findByIds 一次批量回表取完整实体,避免逐条查询(与下文 N+1 防治同构);其二,相似度排序在内存中完成,因为向量库只负责召回。
中间件:高阶函数封装请求管线
文档给出的鉴权中间件采用 HOF(Higher-Order Function)包裹 handler 的写法,Next.js 的 pages 与 app router 均可使用:
// Request/response processing pipeline
export function withAuth(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
}
// Usage
export default withAuth(async (req, res) => {
// Handler has access to req.user
})
withAuth 把"取 token → 校验 → 挂载 req.user"收敛到一处,未携带 token 与 token 无效分别返回 401,业务 handler 内不再散落鉴权代码。这个包裹器模式同样适用于日志、限流等横切关注点,组合时按"限流 → 鉴权 → 业务"的顺序嵌套即可。
三、数据库模式
查询优化:只取需要的列
// PASS: GOOD: Select only needed columns
const { data } = await supabase
.from('markets')
.select('id, name, status, volume')
.eq('status', 'active')
.order('volume', { ascending: false })
.limit(10)
// FAIL: BAD: Select everything
const { data } = await supabase
.from('markets')
.select('*')
select('*') 会把宽表上所有列(含大字段)经网络传输并占用应用内存,列表页通常只需要 3~4 个展示列。显式列名 + limit 是最小成本的第一道优化。
N+1 查询防治:批量回表 + Map 装配
// FAIL: BAD: N+1 query problem
const markets = await getMarkets()
for (const market of markets) {
market.creator = await getUser(market.creator_id) // N queries
}
// PASS: GOOD: Batch fetch
const markets = await getMarkets()
const creatorIds = markets.map(m => m.creator_id)
const creators = await getUsers(creatorIds) // 1 query
const creatorMap = new Map(creators.map(c => [c.id, c]))
markets.forEach(market => {
market.creator = creatorMap.get(market.creator_id)
})
坏例对 N 条记录发起 N 次 getUser,好例收敛为 1 次 IN (...) 批量查询,再用 Map 做 O(1) 装配。凡是在循环中出现 await 单条查询 的代码,都应按这个模式重写;对应到 ORM 层面即"手动批量"或"显式预加载/JOIN"。
事务模式:数据库端 RPC 保证原子性
当一次业务操作需要写入多张表时,文档的方案是把事务下沉到数据库端,应用层通过 rpc 调用:
async function createMarketWithPosition(
marketData: CreateMarketDto,
positionData: CreatePositionDto
) {
// Use Supabase transaction
const { data, error } = await supabase.rpc('create_market_with_position', {
market_data: marketData,
position_data: positionData
})
if (error) throw new Error('Transaction failed')
return data
}
// SQL function in Supabase
CREATE OR REPLACE FUNCTION create_market_with_position(
market_data jsonb,
position_data jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
BEGIN
-- Start transaction automatically
INSERT INTO markets VALUES (market_data);
INSERT INTO positions VALUES (position_data);
RETURN jsonb_build_object('success', true);
EXCEPTION
WHEN OTHERS THEN
-- Rollback happens automatically
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
END;
$$;
plpgsql 函数体天然运行在单个隐式事务中:两条 INSERT 要么全部提交,异常时由 WHEN OTHERS 捕获并整体回滚,应用端只通过返回的 jsonb 判断成败。相比在应用层拼 BEGIN/COMMIT,数据库端事务不跨网络连接、不受超时中断影响,是"多表写入原子性"最省心的落点。
四、缓存策略
基于 Redis 的缓存装饰器
利用 Repository 的接口抽象,可以叠加一个"缓存版 Repository"而不动业务代码:
class CachedMarketRepository implements MarketRepository {
constructor(
private baseRepo: MarketRepository,
private redis: RedisClient
) {}
async findById(id: string): Promise<Market | null> {
// Check cache first
const cached = await this.redis.get(`market:${id}`)
if (cached) {
return JSON.parse(cached)
}
// Cache miss - fetch from database
const market = await this.baseRepo.findById(id)
if (market) {
// Cache for 5 minutes
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
}
return market
}
async invalidateCache(id: string): Promise<void> {
await this.redis.del(`market:${id}`)
}
}
三个实现细节值得强调:缓存键带实体前缀(market:${id})避免跨实体串键;TTL 设为 300 秒(setex 第二参数),用"允许最多 5 分钟陈旧"换写入免失效;对查不到的实体不写缓存,防止缓存穿透时把 null 固化(如需防穿透,可在工程上改为缓存空值且设更短 TTL,但那是文档之外的取舍)。
Cache-Aside 手写版
不引入装饰器时,同样的语义可以直接写在函数里:
async function getMarketWithCache(id: string): Promise<Market> {
const cacheKey = `market:${id}`
// Try cache
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Cache miss - fetch from DB
const market = await db.markets.findUnique({ where: { id } })
if (!market) throw new Error('Market not found')
// Update cache
await redis.setex(cacheKey, 300, JSON.stringify(market))
return market
}
Cache-Aside 的读路径固定为三步:查缓存 → 未命中查库 → 回填缓存。写路径上则应遵循"先更新数据库、再删除缓存"的顺序,保证缓存不会长期滞留旧值。
五、错误处理与重试
集中式错误处理器
文档给出的错误分层是:业务错误用 ApiError 显式携带状态码,校验错误识别 Zod 的 ZodError,其余一律按 500 兜底且不泄露内部信息:
class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message)
Object.setPrototypeOf(this, ApiError.prototype)
}
}
export function errorHandler(error: unknown, req: Request): Response {
if (error instanceof ApiError) {
return NextResponse.json({
success: false,
error: error.message
}, { status: error.statusCode })
}
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation failed',
details: error.errors
}, { status: 400 })
}
// Log unexpected errors
console.error('Unexpected error:', error)
return NextResponse.json({
success: false,
error: 'Internal server error'
}, { status: 500 })
}
// Usage
export async function GET(request: Request) {
try {
const data = await fetchData()
return NextResponse.json({ success: true, data })
} catch (error) {
return errorHandler(error, request)
}
}
Object.setPrototypeOf(this, ApiError.prototype) 这一行不能省略:TypeScript 的 extends 在编译产物中依赖原型链修复,否则 instanceof ApiError 判断会失效。另外注意 ApiError 分支直接回显 error.message 而 500 分支只回显通用文案——业务错误的信息是给调用方看的设计,系统错误的细节只进日志。
一处值得留意的版本差异:本文分析的 .agents 副本 中 Zod 错误字段写作 error.errors,而规范源文件 skills/backend-patterns/SKILL.md 写作 error.issues。以 Zod 现行 API 为准,ZodError 暴露的属性是 issues,实际编码时应以规范源文件的写法为准,并在 package.json 锁定的 zod 大版本上验证。
指数退避重试
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError!
}
// Usage
const data = await fetchWithRetry(() => fetchFromAPI())
退避序列为 1s → 2s → 4s(2^i * 1000),默认重试 3 次后抛出最后一次错误。该包装器适用于幂等的网络调用(GET、带幂等键的写);对非幂等写操作盲目重试可能放大副作用,使用前应确认下游接口的幂等性。
六、认证与授权
JWT 校验
import jwt from 'jsonwebtoken'
interface JWTPayload {
userId: string
email: string
role: 'admin' | 'user'
}
export function verifyToken(token: string): JWTPayload {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
return payload
} catch (error) {
throw new ApiError(401, 'Invalid token')
}
}
export async function requireAuth(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new ApiError(401, 'Missing authorization token')
}
return verifyToken(token)
}
// Usage in API route
export async function GET(request: Request) {
const user = await requireAuth(request)
const data = await getDataForUser(user.userId)
return NextResponse.json({ success: true, data })
}
要点:密钥来自环境变量 JWT_SECRET 而非硬编码;verifyToken 把 jsonwebtoken 的底层异常统一转成 ApiError(401),与上文集中式错误处理器衔接;路由层只需 const user = await requireAuth(request) 一行即可获得已鉴权的 payload。
基于角色的访问控制(RBAC)
type Permission = 'read' | 'write' | 'delete' | 'admin'
interface User {
id: string
role: 'admin' | 'moderator' | 'user'
}
const rolePermissions: Record<User['role'], Permission[]> = {
admin: ['read', 'write', 'delete', 'admin'],
moderator: ['read', 'write', 'delete'],
user: ['read', 'write']
}
export function hasPermission(user: User, permission: Permission): boolean {
return rolePermissions[user.role].includes(permission)
}
export function requirePermission(permission: Permission) {
return (handler: (request: Request, user: User) => Promise<Response>) => {
return async (request: Request) => {
const user = await requireAuth(request)
if (!hasPermission(user, permission)) {
throw new ApiError(403, 'Insufficient permissions')
}
return handler(request, user)
}
}
}
// Usage - HOF wraps the handler
export const DELETE = requirePermission('delete')(
async (request: Request, user: User) => {
// Handler receives authenticated user with verified permission
return new Response('Deleted', { status: 200 })
}
)
角色 → 权限集合的映射集中在一张 Record 表里,requirePermission('delete') 作为高阶函数先完成鉴权 + 授权两道检查,再把 (request, user) 交给 handler。权限模型演进(如增加 audit 权限)只需改映射表,路由代码不动。注意授权失败抛 ApiError(403) 而非 401——401 表示"你是谁都没证明",403 表示"知道你是谁但没有权限"。
七、限流:.agents 副本与规范源文件的分歧
这是两个副本差异最大的一节,恰好说明"模式示例"与"生产约束"之间的边界。
.agents 副本给出了一个完整的进程内存限流器实现,用于演示滑动窗口计数器的写法:
class RateLimiter {
private requests = new Map<string, number[]>()
async checkLimit(
identifier: string,
maxRequests: number,
windowMs: number
): Promise<boolean> {
const now = Date.now()
const requests = this.requests.get(identifier) || []
// Remove old requests outside window
const recentRequests = requests.filter(time => now - time < windowMs)
if (recentRequests.length >= maxRequests) {
return false // Rate limit exceeded
}
// Add current request
recentRequests.push(now)
this.requests.set(identifier, recentRequests)
return true
}
}
const limiter = new RateLimiter()
export async function GET(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min
if (!allowed) {
return NextResponse.json({
error: 'Rate limit exceeded'
}, { status: 429 })
}
// Continue with request
}
实现要点:按标识符(示例中取 x-forwarded-for 头)维护时间戳数组,每次请求先剔除窗口外的旧记录再判断是否超限;示例配额为 100 次 / 60000ms,超限返回 429。该实现的语义(时间戳滑动窗口、429 错误形状)是可借鉴的,但不要把它原样用于生产:
- 它依赖
x-forwarded-for头,该头可被客户端伪造,实际部署需信任代理链并按最内层可信代理的配置解析真实客户端 IP; - 进程内
Map在多实例、无服务器冷启动场景下计数互相隔离且随部署重置。
规范源文件 skills/backend-patterns/SKILL.md 已将此节的代码示例替换为明确的生产约束:
Rate limiting must use a shared store such as Redis, a gateway, or the platform's native limiter. Do not use per-process in-memory counters for production APIs: they reset on deploy, split across replicas, and fail open in serverless or multi-instance environments.
即生产环境限流必须使用共享存储(Redis)、API 网关或平台原生限流器;该技能同时声明限流的"集成点选择与错误形状"仍由后端层负责,而 HTTP 契约细节交给 api-design 技能(skills/api-design/SKILL.md)、滥用场景审查交给 security-review 技能(skills/security-review/SKILL.md)——体现了 ECC 技能之间职责不重叠的组织原则。
八、后台任务与队列
class JobQueue<T> {
private queue: T[] = []
private processing = false
async add(job: T): Promise<void> {
this.queue.push(job)
if (!this.processing) {
this.process()
}
}
private async process(): Promise<void> {
this.processing = true
while (this.queue.length > 0) {
const job = this.queue.shift()!
try {
await this.execute(job)
} catch (error) {
console.error('Job failed:', error)
}
}
this.processing = false
}
private async execute(job: T): Promise<void> {
// Job execution logic
}
}
// Usage for indexing markets
interface IndexJob {
marketId: string
}
const indexQueue = new JobQueue<IndexJob>()
export async function POST(request: Request) {
const { marketId } = await request.json()
// Add to queue instead of blocking
await indexQueue.add({ marketId })
return NextResponse.json({ success: true, message: 'Job queued' })
}
这个"简单队列"解决的核心问题是把慢操作从请求路径上摘下来:POST 立即应答 Job queued,索引在后台串行消费。设计上值得学习的点包括:processing 标志保证同一时刻只有一个消费循环(入队与出队并发安全的最简处理);单作业失败被 catch 吞掉并打日志,不会中断整个队列。同时应清楚它的适用边界——队列存活于进程内存,进程重启即丢失未执行作业;从源码结构看它面向的是"任务可丢失或可重放"的轻场景(如重建索引),持久化、失败重试、多 worker 消费这类需求应换成 Redis/BullMQ 级别的队列系统。
九、结构化日志与监控
interface LogContext {
userId?: string
requestId?: string
method?: string
path?: string
[key: string]: unknown
}
class Logger {
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...context
}
console.log(JSON.stringify(entry))
}
info(message: string, context?: LogContext) {
this.log('info', message, context)
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context)
}
error(message: string, error: Error, context?: LogContext) {
this.log('error', message, {
...context,
error: error.message,
stack: error.stack
})
}
}
const logger = new Logger()
// Usage
export async function GET(request: Request) {
const requestId = crypto.randomUUID()
logger.info('Fetching markets', {
requestId,
method: 'GET',
path: '/api/markets'
})
try {
const markets = await fetchMarkets()
return NextResponse.json({ success: true, data: markets })
} catch (error) {
logger.error('Failed to fetch markets', error as Error, { requestId })
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
}
}
两条实践原则内嵌在这个实现里:其一,每请求生成 requestId(crypto.randomUUID())并贯穿全部日志条目,使分散的 info/warn/error 能被聚合成一条完整请求轨迹;其二,日志即 JSON 对象,timestamp 用 ISO 8601、error 级别自动附带 message 与 stack,可直接被日志采集器按字段索引,而不必再解析自由文本。
十、参考与延伸
回到技能文档的收尾提醒:"Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level."——模式选择应与系统复杂度匹配,小服务不必强行铺满六边形架构。
若要在 Agent 中复用这套模式,可关注的仓库入口:
- 规范技能源:skills/backend-patterns/SKILL.md(含生产限流约束与 Zod
issues修正); - 本文分析副本:.agents/skills/backend-patterns/SKILL.md,及其插件元数据 .agents/skills/backend-patterns/agents/openai.yaml;
- 相邻技能:skills/api-design/SKILL.md(HTTP 契约)、skills/security-review/SKILL.md(滥用与安全审查)、skills/redis-patterns/SKILL.md(Redis 专题);
- 安装与发现机制:scripts/lib/harness-capabilities.js 中的 adapter 目标目录定义,以及 README.md 关于
.agents/skills/作为项目级技能发现位置的说明。
综上,这份技能文档的价值不在于某一单点技巧,而在于它把"分层解耦(Repository/Service)→ 查询优化(列裁剪、N+1、事务)→ 缓存(Cache-Aside 与装饰器)→ 错误处理(ApiError/集中处理器/退避重试)→ 安全(JWT/RBAC/限流)→ 异步(队列)→ 可观测(结构化日志)"串成了一条与复杂度递增一致的后端工程主线,每个环节都附带了可运行的 TypeScript 参考实现与明确的取舍依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00