{
  "schema": "cfapi.devcenter.ai-context.v1",
  "generated_at": "2026-11-07T11:35:00+08:00",
  "docs_version": "2026.11.07-r1",
  "platform": {
    "base_url": "https://cfapi.aog.cc",
    "api_prefixes": [
      "/api",
      "/api/v1"
    ],
    "api_prefix_note": "两个前缀挂载同一批路由，不是两套 API。选一个并保持一致。",
    "openapi": "/api/openapi.json",
    "protocols": "/api/protocols",
    "human_docs": "/docs/devcenter/",
    "ai_full_context": "/docs/devcenter/llms-full.txt",
    "manifest_schema": "/docs/devcenter/schemas/plugin-manifest.schema.json",
    "public_contract": "/docs/devcenter/openapi.public.json",
    "sdk_typescript": "/docs/devcenter/downloads/cfapi-sdk-ts.zip",
    "postman_collection": "/docs/devcenter/downloads/cfapi-postman.json",
    "contract_scope": "openapi.json=full(including internal, marked internal-admin-generic); openapi.public.json=public+plugin only, all JSON bodies typed"
  },
  "canonical_facts": {
    "manifest_file": "plugin.json",
    "manifest_is_not": "plugin.toml",
    "plugin_code_pattern": "^[a-z0-9_]{1,64}$",
    "local_entry": "index.html",
    "local_entry_note": "entry 字段不会改变静态页面入口。",
    "zip_single_top_level_dir": "supported: unique top-level directory is lifted",
    "account_onboarding": {
      "external_self_registration": "POST /api/app/register",
      "register_fields_required": [
        "username",
        "password"
      ],
      "register_fields_optional": [
        "nickname",
        "email",
        "phone"
      ],
      "username_rule": "3-32 chars, ASCII letters/digits/_/-",
      "password_rule": "8-64 chars, must contain both a letter and a digit",
      "tier_after_register": "external (forced; cannot self-escalate)",
      "login": "POST /api/auth/login -> data.token",
      "internal_accounts": "created by admins via POST /api/admin/users; no public self-registration for internal tiers",
      "rate_limits": "register 5/min per IP, login 10/min per IP; exceeded -> 429/42900",
      "disabled_switch": "if open registration is turned off, register returns 403 40300"
    },
    "error_contract": {
      "shape": "{ code: int, message: string, data: null }",
      "codes": {
        "0": "ok",
        "40000": "bad request",
        "40100": "unauthenticated",
        "40300": "forbidden",
        "40400": "not found",
        "40900": "conflict",
        "42900": "rate limited (HTTP 429)",
        "50000": "internal",
        "50300": "unavailable"
      },
      "warning": "429 is 42900, not 40300; retrying with a different key does not bypass rate limits."
    },
    "pagination": {
      "params": "page (1-based, default 1) + page_size (default 20, clamped 1-100)",
      "response_fields": "data.items ([] when empty), data.total, data.page, data.page_size",
      "not_supported_as_convention": "offset/limit mixed into list endpoints"
    },
    "retry_policy": {
      "safe": "GET and idempotent PUT",
      "unsafe": "POST creates: market submit/upload, plugin data insert, payment initiation",
      "retry_on": [
        "429",
        "500",
        "503",
        "network error"
      ],
      "do_not_retry_on": [
        "400",
        "401",
        "403",
        "404",
        "409"
      ],
      "withdraw_semantics": "POST /api/market/submissions/{code}/withdraw deletes the pending submission row; it is not a soft cancel and keeps no draft"
    },
    "private_install": {
      "endpoint": "POST /api/plugins/upload?code={code}",
      "body": "raw application/zip bytes",
      "permission": "plugin.manage",
      "default_max": "50 MB",
      "who_can_use": "internal_advanced or platform administrators; external accounts cannot self-manage private plugins"
    },
    "market_submission": {
      "upload_endpoint": "POST /api/market/upload?plugin_code={code}",
      "body": "multipart/form-data with field named file",
      "max": "500 MB",
      "permission": "valid authenticated user; plugin.manage is not required",
      "captcha_endpoint": "GET /api/market/captcha",
      "submit_endpoint": "POST /api/market/submit",
      "approve_endpoint": "POST /api/market/submissions/{id}/approve",
      "approve_permission": "plugin.govern",
      "approval_effect": "updates market submission and market version only; does not install, enable, deploy, or start a sandbox",
      "signing_secret_endpoint": "GET /api/market/submissions/{code}/signing-secret",
      "signing_secret_access": "original submitter or super admin; submission status must be approved",
      "signing_secret_note": "returns a per-plugin derived HMAC key, never the platform global PLUGIN_SECRET"
    },
    "permissions": {
      "plugin.view": "read plugin metadata and GET/HEAD/OPTIONS via plugin proxy",
      "plugin.manage": "plugin lifecycle, private upload, write methods via proxy, sandbox exec, high-risk runtime writes",
      "plugin.govern": "governance: approvals, unpublishing, version current switch, scope grants, tier policy; held by admin RBAC/super admin, never automatically granted by developer tier",
      "dev.manage": "advanced internal developer capability code",
      "note": "API Key scopes are a prerequisite gate, not the authorization itself. A key cannot grant more than its owner account."
    },
    "developer_tiers": {
      "external": {
        "automatic_permissions": []
      },
      "internal": {
        "automatic_permissions": [
          "plugin.view"
        ]
      },
      "internal_advanced": {
        "automatic_permissions": [
          "plugin.view",
          "plugin.manage",
          "dev.manage"
        ]
      },
      "governance": "plugin.govern is never in the automatic developer tier grants"
    },
    "proxy": {
      "endpoint": "GET|POST|PUT|DELETE /api/plugins/{code}/proxy/{tail}",
      "target_type": "remote only; target must have non-empty base_url and status=1",
      "read_permission": "plugin.view",
      "write_permission": "plugin.manage",
      "response_limit": "16 MB",
      "default_timeout_seconds": 15,
      "stripped_request_headers": [
        "Authorization",
        "Cookie",
        "x-api-key",
        "x-auth-token"
      ],
      "injected_headers": [
        "X-Platform-Signature",
        "X-Platform-Caller-Signature",
        "X-Platform-Signature-Version",
        "X-Platform-Timestamp",
        "X-Platform-Plugin",
        "X-Platform-Tenant",
        "X-Platform-Caller-Sub",
        "X-Platform-Token",
        "X-Platform-Caller-Token"
      ],
      "recommended_signature_headers": [
        "X-Platform-Signature",
        "X-Platform-Caller-Signature"
      ],
      "legacy_signature_headers": [
        "X-Platform-Token",
        "X-Platform-Caller-Token"
      ],
      "per_plugin_secret_endpoint_private": "GET /api/plugins/{code}/signing-secret",
      "per_plugin_secret_endpoint_market": "GET /api/market/submissions/{code}/signing-secret"
    },
    "sandbox": {
      "exec_endpoint": "POST /api/plugins/{code}/sandbox/exec",
      "permission": "plugin.manage",
      "runtime_values": [
        "wasm",
        "process"
      ],
      "requires_active_config": true,
      "defaults": {
        "timeout_ms": 5000,
        "memory_mb": 256,
        "cpu_ms": 1000,
        "concurrency": 8,
        "runtime": "wasmi",
        "network_policy": "deny",
        "filesystem_policy": "readonly",
        "max_log_bytes_per_run": 65536
      },
      "hard_limits": {
        "timeout_ms": {
          "min": 500,
          "max": 120000
        },
        "memory_mb": {
          "min": 16,
          "max": 2048
        },
        "cpu_ms": {
          "max": 120000
        }
      }
    }
  },
  "workflow_external_market": [
    "POST /api/auth/login -> data.token",
    "package plugin.json + index.html (local) or base_url (remote)",
    "POST /api/market/upload?plugin_code={code} as multipart field file -> data.download_url",
    "GET /api/market/captcha -> token + question",
    "POST /api/market/submit with required fields and captcha",
    "GET /api/market/submissions to inspect status",
    "wait for plugin.govern holder to approve",
    "for remote: GET /api/market/submissions/{code}/signing-secret as original submitter after approval; set it only on your server",
    "install is a separate action: POST /api/plugins/market/install with plugin.manage"
  ],
  "workflow_internal_private": [
    "internal_advanced account",
    "POST /api/plugins for remote registration or POST /api/plugins/upload?code={code} raw ZIP",
    "POST /api/plugins/{code}/enable",
    "local page: /plugins/{code}/",
    "remote proxy: /api/plugins/{code}/proxy/**"
  ],
  "invariants": [
    "Every management write request needs Authorization; omitting it on later curl steps causes 401.",
    "Market approval is not installation, deployment, enablement, or sandbox execution.",
    "set-current only switches the market current version pointer; it does not redeploy an installed plugin.",
    "No API key, plugin secret, or plugin.manage token may be embedded in browser code.",
    "The platform strips caller Authorization/Cookie/x-api-key/x-auth-token before forwarding to a remote plugin.",
    "Response bodies follow {code,message,data}; code=0 means success only for platform responses. A proxied response is defined by the target plugin."
  ],
  "contract_status": {
    "openapi": "/api/openapi.json is the single canonical API contract.",
    "known_limitation": "OpenAPI covers the documented public and plugin routes that were audited; it is not a proof that every internal ops route is exhaustively represented. Some admin/internal operations still use generic request bodies and are marked internal-admin-generic. Plugin-facing schemas and this context take precedence for the documented workflows; verify undocumented payloads against a live error response before relying on them.",
    "retired": "openapi_extra.json is retired and no longer a source of truth."
  },
  "plugin_hierarchy": {
    "model": "single table plugins + (parent_id, tree_depth, tree_path, is_platform, manage_mode, node_kind, protocols)",
    "levels": {
      "0": "cfapi",
      "1": "总插件平台",
      "2": "一级子插件平台",
      "3": "二级子插件平台",
      "4": "三级子插件平台",
      "n": "第 n-1 级子插件平台（无上限，工程上限 16）"
    },
    "rules": [
      "只有 is_platform=1 的节点可挂子节点",
      "上级对任意深度子孙有越级管理权，无需逐层授权",
      "manage_mode=delegated 只委托日常运维，不剥夺上级越级权",
      "POST /api/plugins/{code}/tokens 签发 pscope 令牌，限该子树且恒非超管",
      "改挂会拒绝成环与超过 16 层"
    ],
    "endpoints": [
      "GET /api/plugins/tree",
      "GET|POST /api/plugins/{code}/children",
      "GET /api/plugins/{code}/descendants",
      "GET /api/plugins/{code}/lineage",
      "PUT /api/plugins/{code}/parent",
      "POST /api/plugins/{code}/platform",
      "GET /api/plugins/{code}/protocols",
      "POST /api/plugins/{code}/tokens"
    ],
    "protocols_per_node": 20,
    "kind": [
      "native",
      "api",
      "code",
      "db",
      "wasm",
      "sandbox"
    ]
  },
  "database_backends": {
    "plugin_direct_db_access": "forbidden",
    "note": "以下为平台自身存储层事实，插件只能走 REST 读写数据。",
    "available": [
      {
        "name": "sqlite",
        "version": "3.46.0",
        "default": true,
        "location": "app/var/data/cfapi.db",
        "journal_mode": "WAL",
        "schema_bootstrap": "内嵌 DDL，启动即自举（CREATE ... IF NOT EXISTS），无需手工建库"
      },
      {
        "name": "mysql",
        "version": "MariaDB 11.8.6",
        "default": false
      },
      {
        "name": "postgres",
        "version": "PostgreSQL 18.6",
        "default": false,
        "port_note": "5433（5432 属于同机其它项目）"
      }
    ],
    "config": {
      "slots": [
        "DATABASE_SQLITE_URL",
        "DATABASE_MYSQL_URL",
        "DATABASE_POSTGRES_URL"
      ],
      "active": [
        "DATABASE_URL",
        "DATABASE_TYPE"
      ]
    },
    "switch": {
      "ui": "/qq123008/ -> 数据库管理",
      "cli": "bash scripts/db_port/cutover.sh sqlite|mysql|postgres",
      "steps": [
        "停写",
        "目标库快照",
        "全量搬运",
        "行级摘要校验（mismatch 必须为 0）",
        "改 .env",
        "重启",
        "健康检查"
      ],
      "rollback": "任一步失败自动回滚，旧服务继续运行"
    },
    "status_api": {
      "path": "/api/db-backend/status",
      "auth": "需管理凭证",
      "fields": [
        "dialect",
        "server_version",
        "table_count",
        "production_ready",
        "backends"
      ]
    },
    "type_mapping_rule": {
      "bigint": "i64",
      "integer": "i32",
      "smallint": "i16",
      "why": "MySQL int(11) 在 PG 是 int4；解码类型不对齐时 MySQL 隐式放宽、PG 直接拒绝解码，表现为功能静默失效。部署前质量门会做一致性审计。"
    },
    "syntax_rewrites_for_sqlite": [
      "INSERT IGNORE -> INSERT OR IGNORE",
      "IF() -> iif()",
      "CONCAT() -> ||",
      "DATE_FORMAT() -> strftime",
      "LIMIT a,b -> LIMIT b OFFSET a",
      "ON DUPLICATE KEY UPDATE -> ON CONFLICT ... DO UPDATE",
      "COLLATE utf8mb4_* -> 剥离"
    ],
    "backup": "scripts/db_port/backup_db.sh 三后端通用（SQLite: .backup+integrity_check；MySQL: mariadb-dump；PG: pg_dump），crontab 03:20"
  }
}
