{
  "components": {
    "schemas": {
      "ErrorBody": {
        "properties": {
          "error": {
            "description": "给人读的错误说明。响应 JSON 内不再另放状态码或业务码",
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "OpAGT001Response": {
        "properties": {
          "agents": {
            "items": {
              "properties": {
                "enabled": {
                  "example": true,
                  "type": "boolean"
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "material_ids": {
                  "items": {
                    "example": 1,
                    "type": "integer"
                  },
                  "type": "array"
                },
                "name": {
                  "example": "数学助手",
                  "type": "string"
                },
                "prompt": {
                  "example": "用本班数学资料引导学生。",
                  "type": "string"
                },
                "skill_ids": {
                  "items": {
                    "example": 1,
                    "type": "integer"
                  },
                  "type": "array"
                }
              },
              "required": [
                "enabled",
                "id",
                "material_ids",
                "name",
                "prompt",
                "skill_ids"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "agents"
        ],
        "type": "object"
      },
      "OpAGT002Request": {
        "properties": {
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "material_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          },
          "name": {
            "example": "数学助手",
            "type": "string"
          },
          "prompt": {
            "example": "用本班数学资料引导学生。",
            "type": "string"
          },
          "skill_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "enabled",
          "material_ids",
          "name",
          "prompt",
          "skill_ids"
        ],
        "type": "object"
      },
      "OpAGT002Response": {
        "properties": {
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "material_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          },
          "name": {
            "example": "数学助手",
            "type": "string"
          },
          "prompt": {
            "example": "用本班数学资料引导学生。",
            "type": "string"
          },
          "skill_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "enabled",
          "id",
          "material_ids",
          "name",
          "prompt",
          "skill_ids"
        ],
        "type": "object"
      },
      "OpAGT003Response": {
        "properties": {
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "material_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          },
          "name": {
            "example": "数学助手",
            "type": "string"
          },
          "prompt": {
            "example": "用本班数学资料引导学生。",
            "type": "string"
          },
          "skill_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "enabled",
          "id",
          "material_ids",
          "name",
          "prompt",
          "skill_ids"
        ],
        "type": "object"
      },
      "OpAGT004Request": {
        "properties": {
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "material_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          },
          "name": {
            "example": "数学助手",
            "type": "string"
          },
          "prompt": {
            "example": "用本班数学资料引导学生。",
            "type": "string"
          },
          "skill_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "enabled",
          "material_ids",
          "name",
          "prompt",
          "skill_ids"
        ],
        "type": "object"
      },
      "OpAGT004Response": {
        "properties": {
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "material_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          },
          "name": {
            "example": "数学助手",
            "type": "string"
          },
          "prompt": {
            "example": "用本班数学资料引导学生。",
            "type": "string"
          },
          "skill_ids": {
            "items": {
              "example": 1,
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "enabled",
          "id",
          "material_ids",
          "name",
          "prompt",
          "skill_ids"
        ],
        "type": "object"
      },
      "OpAUTH001Response": {
        "properties": {
          "check": {
            "example": "liveness",
            "type": "string"
          },
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "required": [
          "check",
          "status"
        ],
        "type": "object"
      },
      "OpAUTH002Response": {
        "properties": {
          "accounts": {
            "items": {
              "properties": {
                "class_code": {
                  "example": "A",
                  "type": "string"
                },
                "label": {
                  "example": "A 班教师",
                  "type": "string"
                },
                "password": {
                  "example": "\u003cSEED_TEACHER_PASSWORD\u003e",
                  "type": "string"
                },
                "role": {
                  "example": "teacher",
                  "type": "string"
                },
                "username": {
                  "example": "teacher_a",
                  "type": "string"
                }
              },
              "required": [
                "class_code",
                "label",
                "password",
                "role",
                "username"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "accounts"
        ],
        "type": "object"
      },
      "OpAUTH003Request": {
        "properties": {
          "password": {
            "example": "example-password",
            "type": "string"
          },
          "username": {
            "example": "teacher_a",
            "type": "string"
          }
        },
        "required": [
          "password",
          "username"
        ],
        "type": "object"
      },
      "OpAUTH003Response": {
        "properties": {
          "token": {
            "example": "\u003cjwt\u003e",
            "type": "string"
          },
          "user": {
            "properties": {
              "class_code": {
                "example": "A",
                "type": "string"
              },
              "class_id": {
                "example": 1,
                "type": "integer"
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "role": {
                "example": "teacher",
                "type": "string"
              },
              "username": {
                "example": "teacher_a",
                "type": "string"
              }
            },
            "required": [
              "class_code",
              "class_id",
              "id",
              "role",
              "username"
            ],
            "type": "object"
          }
        },
        "required": [
          "token",
          "user"
        ],
        "type": "object"
      },
      "OpAUTH004Response": {
        "properties": {
          "status": {
            "example": "logged_out",
            "type": "string"
          }
        },
        "required": [
          "status"
        ],
        "type": "object"
      },
      "OpAUTH005Response": {
        "properties": {
          "user": {
            "properties": {
              "class_code": {
                "example": "A",
                "type": "string"
              },
              "class_id": {
                "example": 1,
                "type": "integer"
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "role": {
                "example": "teacher",
                "type": "string"
              },
              "username": {
                "example": "teacher_a",
                "type": "string"
              }
            },
            "required": [
              "class_code",
              "class_id",
              "id",
              "role",
              "username"
            ],
            "type": "object"
          }
        },
        "required": [
          "user"
        ],
        "type": "object"
      },
      "OpAUTH006Response": {
        "properties": {
          "students": {
            "items": {
              "properties": {
                "username": {
                  "example": "student_a1",
                  "type": "string"
                }
              },
              "required": [
                "username"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "students"
        ],
        "type": "object"
      },
      "OpCONV001Response": {
        "properties": {
          "conversations": {
            "items": {
              "properties": {
                "agent_id": {
                  "example": 1,
                  "type": "integer"
                },
                "class_id": {
                  "example": 1,
                  "type": "integer"
                },
                "created_at": {
                  "example": "2026-10-06T00:00:00Z",
                  "type": "string"
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "title": {
                  "example": "新的提问",
                  "type": "string"
                },
                "updated_at": {
                  "example": "2026-10-06T00:00:00Z",
                  "type": "string"
                },
                "user_id": {
                  "example": 2,
                  "type": "integer"
                }
              },
              "required": [
                "agent_id",
                "class_id",
                "created_at",
                "id",
                "title",
                "updated_at",
                "user_id"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "conversations"
        ],
        "type": "object"
      },
      "OpCONV002Request": {
        "properties": {
          "agent_id": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "agent_id"
        ],
        "type": "object"
      },
      "OpCONV002Response": {
        "properties": {
          "agent_id": {
            "example": 1,
            "type": "integer"
          },
          "class_id": {
            "example": 1,
            "type": "integer"
          },
          "created_at": {
            "example": "2026-10-06T00:00:00Z",
            "type": "string"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "title": {
            "example": "新的提问",
            "type": "string"
          },
          "updated_at": {
            "example": "2026-10-06T00:00:00Z",
            "type": "string"
          },
          "user_id": {
            "example": 2,
            "type": "integer"
          }
        },
        "required": [
          "agent_id",
          "class_id",
          "created_at",
          "id",
          "title",
          "updated_at",
          "user_id"
        ],
        "type": "object"
      },
      "OpCONV003Response": {
        "properties": {
          "conversation": {
            "properties": {
              "agent_id": {
                "example": 1,
                "type": "integer"
              },
              "class_id": {
                "example": 1,
                "type": "integer"
              },
              "created_at": {
                "example": "2026-10-06T00:00:00Z",
                "type": "string"
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "title": {
                "example": "新的提问",
                "type": "string"
              },
              "updated_at": {
                "example": "2026-10-06T00:00:00Z",
                "type": "string"
              },
              "user_id": {
                "example": 2,
                "type": "integer"
              }
            },
            "required": [
              "agent_id",
              "class_id",
              "created_at",
              "id",
              "title",
              "updated_at",
              "user_id"
            ],
            "type": "object"
          },
          "messages": {
            "items": {
              "properties": {
                "citations": {
                  "items": {},
                  "type": "array"
                },
                "content": {
                  "example": "移项怎么做",
                  "type": "string"
                },
                "created_at": {
                  "example": "2026-10-06T00:00:00Z",
                  "type": "string"
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "role": {
                  "example": "user",
                  "type": "string"
                },
                "skill_name": {
                  "example": "",
                  "type": "string"
                }
              },
              "required": [
                "citations",
                "content",
                "created_at",
                "id",
                "role",
                "skill_name"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "conversation",
          "messages"
        ],
        "type": "object"
      },
      "OpCONV004Request": {
        "properties": {
          "content": {
            "example": "移项怎么做",
            "type": "string"
          }
        },
        "required": [
          "content"
        ],
        "type": "object"
      },
      "OpDOC001Response": {
        "properties": {
          "info": {
            "properties": {
              "title": {
                "example": "CampusClaw API",
                "type": "string"
              }
            },
            "required": [
              "title"
            ],
            "type": "object"
          },
          "openapi": {
            "example": "3.0.3",
            "type": "string"
          }
        },
        "required": [
          "info",
          "openapi"
        ],
        "type": "object"
      },
      "OpFLD001Response": {
        "properties": {
          "folders": {
            "items": {
              "properties": {
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "name": {
                  "example": "数学",
                  "type": "string"
                },
                "parent_id": {
                  "nullable": true
                }
              },
              "required": [
                "id",
                "name"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "folders"
        ],
        "type": "object"
      },
      "OpFLD002Request": {
        "properties": {
          "name": {
            "example": "数学",
            "type": "string"
          },
          "parent_id": {
            "nullable": true
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "OpFLD002Response": {
        "properties": {
          "folder": {
            "properties": {
              "id": {
                "example": 1,
                "type": "integer"
              },
              "name": {
                "example": "数学",
                "type": "string"
              },
              "parent_id": {
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ],
            "type": "object"
          }
        },
        "required": [
          "folder"
        ],
        "type": "object"
      },
      "OpFLD003Request": {
        "properties": {
          "name": {
            "example": "代数",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "OpFLD003Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpFLD004Request": {
        "properties": {
          "parent_id": {
            "nullable": true
          }
        },
        "type": "object"
      },
      "OpFLD004Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpFLD005Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpMAT001Response": {
        "properties": {
          "materials": {
            "items": {
              "properties": {
                "class_code": {
                  "example": "A",
                  "type": "string"
                },
                "created_at": {
                  "example": "2026-10-06T00:00:00Z",
                  "type": "string"
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "title": {
                  "example": "一元一次方程",
                  "type": "string"
                }
              },
              "required": [
                "class_code",
                "created_at",
                "id",
                "title"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "page": {
            "example": 1,
            "type": "integer"
          },
          "page_size": {
            "example": 50,
            "type": "integer"
          },
          "total": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "materials",
          "page",
          "page_size",
          "total"
        ],
        "type": "object"
      },
      "OpMAT002Request": {
        "properties": {
          "chunk_collapse_ws": {
            "description": "值为 1 或 true 时折叠空白",
            "enum": [
              "1",
              "true"
            ],
            "example": "1",
            "type": "string"
          },
          "chunk_max_chars": {
            "description": "custom 或 hierarchy 时可选，100 到 2000",
            "example": 800,
            "maximum": 2000,
            "minimum": 100,
            "type": "integer"
          },
          "chunk_overlap_percent": {
            "description": "custom 或 hierarchy 时可选，0 到 50",
            "example": 10,
            "maximum": 50,
            "minimum": 0,
            "type": "integer"
          },
          "chunk_separator": {
            "description": "custom 或 hierarchy 时可选，缺省 newline",
            "enum": [
              "newline",
              "blank",
              "period"
            ],
            "example": "newline",
            "type": "string"
          },
          "chunk_strategy": {
            "description": "缺省 auto。auto 使用服务器默认窗口",
            "enum": [
              "auto",
              "custom",
              "hierarchy"
            ],
            "example": "auto",
            "type": "string"
          },
          "chunk_strip_links": {
            "description": "值为 1 或 true 时去掉链接",
            "enum": [
              "1",
              "true"
            ],
            "example": "1",
            "type": "string"
          },
          "file": {
            "description": "必填。扩展名只能是 .txt 或 .md，内容为非空 UTF-8 文本。大小不超过 MAX_UPLOAD_BYTES，默认 2097152 字节",
            "format": "binary",
            "type": "string"
          }
        },
        "required": [
          "file"
        ],
        "type": "object"
      },
      "OpMAT002Response": {
        "properties": {
          "chunk_count": {
            "example": 2,
            "type": "integer"
          },
          "chunk_strategy": {
            "example": "auto",
            "type": "string"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "title": {
            "example": "notes.md",
            "type": "string"
          }
        },
        "required": [
          "chunk_count",
          "chunk_strategy",
          "id",
          "title"
        ],
        "type": "object"
      },
      "OpMAT003Response": {
        "properties": {
          "material": {
            "properties": {
              "body_text": {
                "example": "移项后合并同类项。",
                "type": "string"
              },
              "class_code": {
                "example": "A",
                "type": "string"
              },
              "created_at": {
                "example": "2026-10-06T00:00:00Z",
                "type": "string"
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "title": {
                "example": "一元一次方程",
                "type": "string"
              }
            },
            "required": [
              "body_text",
              "class_code",
              "created_at",
              "id",
              "title"
            ],
            "type": "object"
          }
        },
        "required": [
          "material"
        ],
        "type": "object"
      },
      "OpMAT005Request": {
        "properties": {
          "chunk_collapse_ws": {
            "example": false,
            "type": "boolean"
          },
          "chunk_max_chars": {
            "example": 800,
            "type": "integer"
          },
          "chunk_overlap_percent": {
            "example": 10,
            "type": "integer"
          },
          "chunk_separator": {
            "example": "newline",
            "type": "string"
          },
          "chunk_strategy": {
            "example": "auto",
            "type": "string"
          },
          "chunk_strip_links": {
            "example": false,
            "type": "boolean"
          }
        },
        "required": [
          "chunk_collapse_ws",
          "chunk_max_chars",
          "chunk_overlap_percent",
          "chunk_separator",
          "chunk_strategy",
          "chunk_strip_links"
        ],
        "type": "object"
      },
      "OpMAT005Response": {
        "properties": {
          "chunk_count": {
            "example": 2,
            "type": "integer"
          },
          "chunk_strategy": {
            "example": "auto",
            "type": "string"
          },
          "material_id": {
            "example": 1,
            "type": "integer"
          },
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "required": [
          "chunk_count",
          "chunk_strategy",
          "material_id",
          "status"
        ],
        "type": "object"
      },
      "OpMAT006Response": {
        "properties": {
          "chunks": {
            "items": {
              "properties": {
                "chunk_index": {
                  "example": 0,
                  "type": "integer"
                },
                "dim": {
                  "example": 4,
                  "type": "integer"
                },
                "end_offset": {
                  "example": 12,
                  "type": "integer"
                },
                "excerpt": {
                  "example": "移项后合并同类项",
                  "type": "string"
                },
                "index_status": {
                  "example": "ready",
                  "type": "string"
                },
                "mysql_chunk_id": {
                  "example": 1,
                  "type": "integer"
                },
                "mysql_class_id": {
                  "example": 1,
                  "type": "integer"
                },
                "mysql_entry_id": {
                  "example": 1,
                  "type": "integer"
                },
                "mysql_material_id": {
                  "example": 1,
                  "type": "integer"
                },
                "point_id": {
                  "example": 1,
                  "type": "integer"
                },
                "preview": {
                  "items": {
                    "example": 0.1,
                    "type": "number"
                  },
                  "type": "array"
                },
                "start_offset": {
                  "example": 0,
                  "type": "integer"
                }
              },
              "required": [
                "chunk_index",
                "dim",
                "end_offset",
                "excerpt",
                "index_status",
                "mysql_chunk_id",
                "mysql_class_id",
                "mysql_entry_id",
                "mysql_material_id",
                "point_id",
                "preview",
                "start_offset"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "class_id": {
            "example": 1,
            "type": "integer"
          },
          "collection": {
            "example": "campusclaw_chunks",
            "type": "string"
          },
          "dim": {
            "example": 4,
            "type": "integer"
          },
          "distance": {
            "example": "Cosine",
            "type": "string"
          },
          "knowledge_entry_id": {
            "example": 1,
            "type": "integer"
          },
          "material_id": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "chunks",
          "class_id",
          "collection",
          "dim",
          "distance",
          "knowledge_entry_id",
          "material_id"
        ],
        "type": "object"
      },
      "OpNOTE001Response": {
        "properties": {
          "notes": {
            "items": {
              "properties": {
                "folder_id": {
                  "nullable": true
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "title": {
                  "example": "移项",
                  "type": "string"
                }
              },
              "required": [
                "id",
                "title"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "notes"
        ],
        "type": "object"
      },
      "OpNOTE002Request": {
        "properties": {
          "body_text": {
            "example": "先保持等号两边相等。",
            "type": "string"
          },
          "folder_id": {
            "nullable": true
          },
          "title": {
            "example": "移项",
            "type": "string"
          }
        },
        "required": [
          "body_text",
          "title"
        ],
        "type": "object"
      },
      "OpNOTE002Response": {
        "properties": {
          "note": {
            "properties": {
              "body_text": {
                "example": "先保持等号两边相等。",
                "type": "string"
              },
              "folder_id": {
                "nullable": true
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "title": {
                "example": "移项",
                "type": "string"
              }
            },
            "required": [
              "body_text",
              "id",
              "title"
            ],
            "type": "object"
          }
        },
        "required": [
          "note"
        ],
        "type": "object"
      },
      "OpNOTE003Response": {
        "properties": {
          "note": {
            "properties": {
              "body_text": {
                "example": "先保持等号两边相等。",
                "type": "string"
              },
              "folder_id": {
                "nullable": true
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "title": {
                "example": "移项",
                "type": "string"
              }
            },
            "required": [
              "body_text",
              "id",
              "title"
            ],
            "type": "object"
          }
        },
        "required": [
          "note"
        ],
        "type": "object"
      },
      "OpNOTE004Request": {
        "properties": {
          "body_text": {
            "example": "先保持等号两边相等。",
            "type": "string"
          },
          "title": {
            "example": "移项",
            "type": "string"
          }
        },
        "required": [
          "body_text",
          "title"
        ],
        "type": "object"
      },
      "OpNOTE004Response": {
        "properties": {
          "note": {
            "properties": {
              "body_text": {
                "example": "先保持等号两边相等。",
                "type": "string"
              },
              "folder_id": {
                "nullable": true
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "title": {
                "example": "移项",
                "type": "string"
              }
            },
            "required": [
              "body_text",
              "id",
              "title"
            ],
            "type": "object"
          }
        },
        "required": [
          "note"
        ],
        "type": "object"
      },
      "OpNOTE005Request": {
        "properties": {
          "folder_id": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "folder_id"
        ],
        "type": "object"
      },
      "OpNOTE005Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpNOTE006Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpPRM001Response": {
        "properties": {
          "body": {
            "example": "你是学生自学时的解题助手。用简体中文。",
            "type": "string"
          }
        },
        "required": [
          "body"
        ],
        "type": "object"
      },
      "OpPRM002Request": {
        "properties": {
          "body": {
            "example": "你是学生自学时的解题助手。用简体中文。",
            "type": "string"
          }
        },
        "required": [
          "body"
        ],
        "type": "object"
      },
      "OpPRM002Response": {
        "properties": {
          "body": {
            "example": "你是学生自学时的解题助手。用简体中文。",
            "type": "string"
          }
        },
        "required": [
          "body"
        ],
        "type": "object"
      },
      "OpRES001Response": {
        "properties": {
          "resources": {
            "items": {
              "properties": {
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "picked": {
                  "example": false,
                  "type": "boolean"
                },
                "subject": {
                  "example": "数学",
                  "type": "string"
                },
                "title": {
                  "example": "一元一次方程",
                  "type": "string"
                }
              },
              "required": [
                "id",
                "picked",
                "subject",
                "title"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "resources"
        ],
        "type": "object"
      },
      "OpRES002Response": {
        "properties": {
          "resource": {
            "properties": {
              "body_text": {
                "example": "移项后合并同类项。",
                "type": "string"
              },
              "id": {
                "example": 1,
                "type": "integer"
              },
              "picked": {
                "example": false,
                "type": "boolean"
              },
              "subject": {
                "example": "数学",
                "type": "string"
              },
              "title": {
                "example": "一元一次方程",
                "type": "string"
              }
            },
            "required": [
              "body_text",
              "id",
              "picked",
              "subject",
              "title"
            ],
            "type": "object"
          }
        },
        "required": [
          "resource"
        ],
        "type": "object"
      },
      "OpRES003Request": {
        "properties": {
          "folder_id": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "folder_id"
        ],
        "type": "object"
      },
      "OpRES003Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpRES004Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpRES005Response": {
        "properties": {
          "resources": {
            "items": {
              "properties": {
                "folder_id": {
                  "nullable": true
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "picked": {
                  "example": true,
                  "type": "boolean"
                },
                "subject": {
                  "example": "数学",
                  "type": "string"
                },
                "title": {
                  "example": "一元一次方程",
                  "type": "string"
                }
              },
              "required": [
                "id",
                "picked",
                "subject",
                "title"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "resources"
        ],
        "type": "object"
      },
      "OpRES006Request": {
        "properties": {
          "folder_id": {
            "example": 1,
            "type": "integer"
          }
        },
        "required": [
          "folder_id"
        ],
        "type": "object"
      },
      "OpRES006Response": {
        "properties": {
          "ok": {
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      },
      "OpSKL001Response": {
        "properties": {
          "skills": {
            "items": {
              "properties": {
                "description": {
                  "example": "先让学生说出等号两边要保持相等。",
                  "type": "string"
                },
                "enabled": {
                  "example": true,
                  "type": "boolean"
                },
                "id": {
                  "example": 1,
                  "type": "integer"
                },
                "name": {
                  "example": "方程解题引导",
                  "type": "string"
                },
                "source": {
                  "example": "名称：方程解题引导",
                  "type": "string"
                },
                "subject": {
                  "example": "数学",
                  "type": "string"
                },
                "tools": {
                  "items": {
                    "example": "knowledge_search",
                    "type": "string"
                  },
                  "type": "array"
                },
                "trigger": {
                  "example": "方程,移项",
                  "type": "string"
                }
              },
              "required": [
                "description",
                "enabled",
                "id",
                "name",
                "source",
                "subject",
                "tools",
                "trigger"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "skills"
        ],
        "type": "object"
      },
      "OpSKL002Request": {
        "properties": {
          "source": {
            "example": "名称：方程解题引导\n适用条件：方程,移项\n工具：knowledge_search\n资料：数学\n启用：是\n\n先让学生说出等号两边要保持相等。",
            "type": "string"
          }
        },
        "required": [
          "source"
        ],
        "type": "object"
      },
      "OpSKL002Response": {
        "properties": {
          "description": {
            "example": "先让学生说出等号两边要保持相等。",
            "type": "string"
          },
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "name": {
            "example": "方程解题引导",
            "type": "string"
          },
          "source": {
            "example": "名称：方程解题引导",
            "type": "string"
          },
          "subject": {
            "example": "数学",
            "type": "string"
          },
          "tools": {
            "items": {
              "example": "knowledge_search",
              "type": "string"
            },
            "type": "array"
          },
          "trigger": {
            "example": "方程,移项",
            "type": "string"
          }
        },
        "required": [
          "description",
          "enabled",
          "id",
          "name",
          "source",
          "subject",
          "tools",
          "trigger"
        ],
        "type": "object"
      },
      "OpSKL003Request": {
        "properties": {
          "source": {
            "example": "名称：方程解题引导\n适用条件：方程,移项\n工具：knowledge_search\n资料：数学\n启用：是\n\n先让学生说出等号两边要保持相等。",
            "type": "string"
          }
        },
        "required": [
          "source"
        ],
        "type": "object"
      },
      "OpSKL003Response": {
        "properties": {
          "description": {
            "example": "先让学生说出等号两边要保持相等。",
            "type": "string"
          },
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": 1,
            "type": "integer"
          },
          "name": {
            "example": "方程解题引导",
            "type": "string"
          },
          "source": {
            "example": "名称：方程解题引导",
            "type": "string"
          },
          "subject": {
            "example": "数学",
            "type": "string"
          },
          "tools": {
            "items": {
              "example": "knowledge_search",
              "type": "string"
            },
            "type": "array"
          },
          "trigger": {
            "example": "方程,移项",
            "type": "string"
          }
        },
        "required": [
          "description",
          "enabled",
          "id",
          "name",
          "source",
          "subject",
          "tools",
          "trigger"
        ],
        "type": "object"
      },
      "OpSRCH001Request": {
        "properties": {
          "limit": {
            "example": 10,
            "type": "integer"
          },
          "mode": {
            "example": "keyword",
            "type": "string"
          },
          "offset": {
            "example": 0,
            "type": "integer"
          },
          "q": {
            "example": "移项",
            "type": "string"
          }
        },
        "required": [
          "limit",
          "mode",
          "offset",
          "q"
        ],
        "type": "object"
      },
      "OpSRCH001Response": {
        "properties": {
          "hits": {
            "items": {
              "properties": {
                "chunk_id": {
                  "example": 1,
                  "type": "integer"
                },
                "chunk_index": {
                  "example": 0,
                  "type": "integer"
                },
                "end_offset": {
                  "example": 12,
                  "type": "integer"
                },
                "material_id": {
                  "example": 1,
                  "type": "integer"
                },
                "material_title": {
                  "example": "一元一次方程",
                  "type": "string"
                },
                "score_final": {
                  "example": 0.5,
                  "type": "number"
                },
                "score_keyword": {
                  "example": 1,
                  "type": "integer"
                },
                "score_vector": {
                  "nullable": true
                },
                "snippet": {
                  "example": "移项后合并同类项",
                  "type": "string"
                },
                "start_offset": {
                  "example": 0,
                  "type": "integer"
                }
              },
              "required": [
                "chunk_id",
                "chunk_index",
                "end_offset",
                "material_id",
                "material_title",
                "score_final",
                "score_keyword",
                "snippet",
                "start_offset"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "required": [
          "hits",
          "status"
        ],
        "type": "object"
      },
      "OpSRCH002Request": {
        "properties": {
          "history": {
            "items": {
              "properties": {
                "content": {
                  "example": "我卡在等号左边",
                  "type": "string"
                },
                "role": {
                  "example": "user",
                  "type": "string"
                }
              },
              "required": [
                "content",
                "role"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "q": {
            "example": "移项怎么做",
            "type": "string"
          }
        },
        "required": [
          "history",
          "q"
        ],
        "type": "object"
      },
      "OpSRCH002Response": {
        "properties": {
          "answer": {
            "example": "先保持等号两边相等，再把项变号移过去。",
            "type": "string"
          },
          "citations": {
            "items": {
              "properties": {
                "chunk_id": {
                  "example": 1,
                  "type": "integer"
                },
                "chunk_index": {
                  "example": 0,
                  "type": "integer"
                },
                "end_offset": {
                  "example": 12,
                  "type": "integer"
                },
                "material_id": {
                  "example": 1,
                  "type": "integer"
                },
                "material_title": {
                  "example": "一元一次方程",
                  "type": "string"
                },
                "score_final": {
                  "example": 0.5,
                  "type": "number"
                },
                "score_keyword": {
                  "example": 1,
                  "type": "integer"
                },
                "score_vector": {
                  "nullable": true
                },
                "snippet": {
                  "example": "移项后合并同类项",
                  "type": "string"
                },
                "start_offset": {
                  "example": 0,
                  "type": "integer"
                }
              },
              "required": [
                "chunk_id",
                "chunk_index",
                "end_offset",
                "material_id",
                "material_title",
                "score_final",
                "score_keyword",
                "snippet",
                "start_offset"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "required": [
          "answer",
          "citations",
          "status"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "bearerFormat": "JWT",
        "description": "HS256 JWT。请求头为 Authorization: Bearer \u003ctoken\u003e。Payload 见文档开头的认证说明",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "CampusClaw 按班级隔离的知识库与解题助手。本文件由处理函数上方的 API 注解生成，是接口文档的唯一来源。自动化测试直接解析它。\n\n## 认证\n\n认证方式是 JWT。签名算法 HS256，密钥只来自环境变量 SESSION_SECRET。客户端把访问令牌放在请求头 Authorization，值为 Bearer 加一个空格再加令牌。Cookie 不是登录凭据。\n\n访问令牌的 Payload 只有注册声明，不写角色，也不写 class_id：\n\n| 字段 | 含义 |\n| --- | --- |\n| sub | 用户 ID 的十进制字符串 |\n| jti | 16 字节随机数的十六进制。登出和再次登录都按它吊销 |\n| iat | 签发时间 |\n| exp | 过期时间 |\n\n角色和班级只从 sub 对应的用户表行读取。query、JSON 正文、请求头和表单里的 class_id 一律丢弃。X-Role 之类由客户端声明的角色不参与判定。\n\nAccess Token 有效期是环境变量 SESSION_TTL_SECONDS，默认 86400 秒，也就是 24 小时。不签发 Refresh Token，也没有单独的刷新端点。\n\n令牌要更换时，重新调用 POST /api/login。如果这次登录还带着未过期的旧 Bearer，服务先吊销旧的 jti，再签发新令牌。过期或已吊销的令牌不能用来换发，只能重新登录。建议在 exp 之前重新登录。\n\n不需要认证的白名单：\n\n- GET /health\n- GET /api/demo-accounts\n- POST /api/login\n- GET /openapi.json\n- GET /swagger/index.html\n\n其余 /api 接口都要 Bearer。标注了教师角色的接口还要求用户表中的角色是 teacher，学生调用返回 403。按编号读取资源时，跨班和真实不存在返回同一个 404，正文都是 {\"error\":\"资源不存在\"}。\n\n每次响应都带 X-Request-Id。排错时把这串编号交给维护者即可定位那一次请求。\n\n## 响应\n\n成功状态码只使用 200、201 或 204。204 没有响应体，文档里会写明「无响应体」。成功和失败的 JSON 都不外包 status 或业务码。错误正文只有一个 error 字符串。错误码、HTTP 状态、描述和触发条件写在每个操作的说明和 x-errors 里。\n\n个别资源字段也叫 status，例如检索结论 ok 或 no_evidence、存活检查的 ok、登出结果 logged_out。它们描述这一条结果，不是协议层的状态码。\n\n## 编号、幂等与限流\n\noperationId 全局唯一且稳定。接口的语义或路径变了，就停用旧编号，不把旧编号配给新接口。退役编号在 x-retired-operation-ids。\n\n每个操作的 x-idempotency 标明是否幂等，以及重复提交是原样成功且不新增数据，还是返回冲突或其他错误。x-rate-limit 标明是否限流。当前只有登录会限流：维度是用户名加来源 IP，统计的是窗口内的失败次数。超限时与口令错误使用同一个 401，不返回 429，避免暴露账号是否存在。",
    "title": "CampusClaw API",
    "version": "1.0.0",
    "x-auth": {
      "access_token_ttl": "SESSION_TTL_SECONDS，默认 86400 秒",
      "header": "Authorization: Bearer \u003ctoken\u003e",
      "payload": [
        {
          "description": "用户 ID 的十进制字符串。不含角色，不含 class_id",
          "name": "sub"
        },
        {
          "description": "16 字节随机数的十六进制，用于吊销",
          "name": "jti"
        },
        {
          "description": "签发时间",
          "name": "iat"
        },
        {
          "description": "过期时间，等于 iat 加上 SESSION_TTL_SECONDS",
          "name": "exp"
        }
      ],
      "refresh": "重新调用 POST /api/login。请求若携带未过期的旧 Bearer，先吊销该 jti 再签发新令牌",
      "refresh_token": "无。不签发 Refresh Token",
      "scheme": "JWT",
      "signing": "HS256",
      "whitelist": [
        "GET /api/demo-accounts",
        "GET /health",
        "GET /openapi.json",
        "GET /swagger/index.html",
        "POST /api/login"
      ]
    },
    "x-operation-id-policy": "operationId 全局唯一且稳定。接口变更后停用旧编号，不复用。",
    "x-retired-operation-ids": []
  },
  "openapi": "3.0.3",
  "paths": {
    "/api/agents": {
      "get": {
        "description": "教师看到本班全部智能体。学生只看到已启用的。响应不包含 class_id。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用 |\n",
        "operationId": "AGT-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "agents": [
                    {
                      "enabled": true,
                      "id": 1,
                      "material_ids": [
                        1
                      ],
                      "name": "数学助手",
                      "prompt": "用本班数学资料引导学生。",
                      "skill_ids": [
                        1
                      ]
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAGT001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出本班智能体",
        "tags": [
          "agents"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "只有教师可以新建。名称去掉空白后为 1 到 64 个字符，提示词为 1 到 2000 个字符。skill_ids 和 material_ids 必须都属于本班，否则 404。响应不包含 class_id。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：否。重复提交：每次成功都新增一个智能体。同班同名返回 400，不是 409\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；写入智能体时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 名称长度不正确 | 名称为空或超过 64 个字符 |\n| BAD_REQUEST | 400 | 提示词长度不正确 | 提示词为空或超过 2000 个字符 |\n| BAD_REQUEST | 400 | 智能体名称已存在 | 本班已有同名智能体 |\n| NOT_FOUND | 404 | 资源不存在 | skill_ids 或 material_ids 中有不存在、小于 1 或不属于本班的编号 |\n",
        "operationId": "AGT-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "enabled": true,
                "material_ids": [
                  1
                ],
                "name": "数学助手",
                "prompt": "用本班数学资料引导学生。",
                "skill_ids": [
                  1
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/OpAGT002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "enabled": true,
                  "id": 1,
                  "material_ids": [
                    1
                  ],
                  "name": "数学助手",
                  "prompt": "用本班数学资料引导学生。",
                  "skill_ids": [
                    1
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAGT002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "名称长度不正确",
                    "value": {
                      "error": "名称长度不正确"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "提示词长度不正确",
                    "value": {
                      "error": "提示词长度不正确"
                    }
                  },
                  "BAD_REQUEST_5": {
                    "summary": "智能体名称已存在",
                    "value": {
                      "error": "智能体名称已存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：名称长度不正确。触发条件：名称为空或超过 64 个字符\nBAD_REQUEST：提示词长度不正确。触发条件：提示词为空或超过 2000 个字符\nBAD_REQUEST：智能体名称已存在。触发条件：本班已有同名智能体"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：skill_ids 或 material_ids 中有不存在、小于 1 或不属于本班的编号"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；写入智能体时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "新建智能体",
        "tags": [
          "agents"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；写入智能体时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "名称长度不正确",
            "status": 400,
            "when": "名称为空或超过 64 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "提示词长度不正确",
            "status": 400,
            "when": "提示词为空或超过 2000 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "智能体名称已存在",
            "status": 400,
            "when": "本班已有同名智能体"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "skill_ids 或 material_ids 中有不存在、小于 1 或不属于本班的编号"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新增一个智能体。同班同名返回 400，不是 409"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/agents/{id}": {
      "get": {
        "description": "教师可以读本班未启用的智能体。学生读到未启用、其他班或不存在时都是 404。响应不包含 class_id。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析 |\n| NOT_FOUND | 404 | 资源不存在 | 智能体不存在、不属于本班，或学生读取未启用的智能体 |\n",
        "operationId": "AGT-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "智能体编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "enabled": true,
                  "id": 1,
                  "material_ids": [
                    1
                  ],
                  "name": "数学助手",
                  "prompt": "用本班数学资料引导学生。",
                  "skill_ids": [
                    1
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAGT003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：智能体不存在、不属于本班，或学生读取未启用的智能体"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取一个智能体",
        "tags": [
          "agents"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "智能体不存在、不属于本班，或学生读取未启用的智能体"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "put": {
        "description": "只有教师可以改本班智能体。名称规则与新建相同。更新时提示词允许为空。跨班与不存在都是 404。响应不包含 class_id。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：是。重复提交：用相同内容再次提交覆盖原智能体，不新增一行，返回 200。改成与另一个智能体同名时返回 400\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；更新智能体时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析，或读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 名称长度不正确 | 名称为空或超过 64 个字符 |\n| BAD_REQUEST | 400 | 提示词长度不正确 | 提示词超过 2000 个字符 |\n| BAD_REQUEST | 400 | 智能体名称已存在 | 本班另一个智能体已使用这个名称 |\n| NOT_FOUND | 404 | 资源不存在 | 智能体不存在或不属于本班，或技能、材料编号不属于本班 |\n",
        "operationId": "AGT-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "智能体编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "enabled": true,
                "material_ids": [
                  1
                ],
                "name": "数学助手",
                "prompt": "用本班数学资料引导学生。",
                "skill_ids": [
                  1
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/OpAGT004Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "enabled": true,
                  "id": 1,
                  "material_ids": [
                    1
                  ],
                  "name": "数学助手",
                  "prompt": "用本班数学资料引导学生。",
                  "skill_ids": [
                    1
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAGT004Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "名称长度不正确",
                    "value": {
                      "error": "名称长度不正确"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "提示词长度不正确",
                    "value": {
                      "error": "提示词长度不正确"
                    }
                  },
                  "BAD_REQUEST_5": {
                    "summary": "智能体名称已存在",
                    "value": {
                      "error": "智能体名称已存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析，或读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：名称长度不正确。触发条件：名称为空或超过 64 个字符\nBAD_REQUEST：提示词长度不正确。触发条件：提示词超过 2000 个字符\nBAD_REQUEST：智能体名称已存在。触发条件：本班另一个智能体已使用这个名称"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：智能体不存在或不属于本班，或技能、材料编号不属于本班"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；更新智能体时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "修改智能体",
        "tags": [
          "agents"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；更新智能体时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析，或读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "名称长度不正确",
            "status": 400,
            "when": "名称为空或超过 64 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "提示词长度不正确",
            "status": 400,
            "when": "提示词超过 2000 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "智能体名称已存在",
            "status": 400,
            "when": "本班另一个智能体已使用这个名称"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "智能体不存在或不属于本班，或技能、材料编号不属于本班"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "用相同内容再次提交覆盖原智能体，不新增一行，返回 200。改成与另一个智能体同名时返回 400"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/ask": {
      "post": {
        "description": "用本班混合检索的前几条切片组织回答。没有命中时不调用模型，answer 为「资料中未找到相关内容」，citations 为空，且不带伪造的 material_id。正文里的 class_id 会被丢弃。q 为 2 到 500 个字符。history 可选，元素只应使用 user 或 assistant。响应里的 status 是 ok 或 no_evidence，不是外包的业务码。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：不把这次调用存成会话消息。重复提交不会因为重复而冲突；模型回答可能不同，但不新增本接口自己的记录\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或检索、模型依赖不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 问题至少需要 2 个字符 | q 去掉空白后不足 2 个字符 |\n| BAD_REQUEST | 400 | 问题过长 | q 超过 500 个字符 |\n| ASK_FAILED | 500 | 回答失败 | 回答过程出现未归类的内部错误 |\n",
        "operationId": "SRCH-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "history": [
                  {
                    "content": "我卡在等号左边",
                    "role": "user"
                  }
                ],
                "q": "移项怎么做"
              },
              "schema": {
                "$ref": "#/components/schemas/OpSRCH002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "answer": "先保持等号两边相等，再把项变号移过去。",
                  "citations": [
                    {
                      "chunk_id": 1,
                      "chunk_index": 0,
                      "end_offset": 12,
                      "material_id": 1,
                      "material_title": "一元一次方程",
                      "score_final": 0.5,
                      "score_keyword": 1,
                      "score_vector": null,
                      "snippet": "移项后合并同类项",
                      "start_offset": 0
                    }
                  ],
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpSRCH002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "问题至少需要 2 个字符",
                    "value": {
                      "error": "问题至少需要 2 个字符"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "问题过长",
                    "value": {
                      "error": "问题过长"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：问题至少需要 2 个字符。触发条件：q 去掉空白后不足 2 个字符\nBAD_REQUEST：问题过长。触发条件：q 超过 500 个字符"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "ASK_FAILED": {
                    "summary": "回答失败",
                    "value": {
                      "error": "回答失败"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "ASK_FAILED：回答失败。触发条件：回答过程出现未归类的内部错误"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或检索、模型依赖不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "按本班资料作简短回答",
        "tags": [
          "search"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或检索、模型依赖不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "问题至少需要 2 个字符",
            "status": 400,
            "when": "q 去掉空白后不足 2 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "问题过长",
            "status": 400,
            "when": "q 超过 500 个字符"
          },
          {
            "code": "ASK_FAILED",
            "description": "回答失败",
            "status": 500,
            "when": "回答过程出现未归类的内部错误"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "不把这次调用存成会话消息。重复提交不会因为重复而冲突；模型回答可能不同，但不新增本接口自己的记录"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/class/students": {
      "get": {
        "description": "班级与角色只取自登录用户行。不接受 query、正文或请求头里的 class_id。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：是。重复提交：只读。重复请求不写库，返回当前班级的学生用户名\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取本班学生名单时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n",
        "operationId": "AUTH-006",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "students": [
                    {
                      "username": "student_a1"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH006Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取本班学生名单时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出本班学生用户名",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取本班学生名单时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回当前班级的学生用户名"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/conversations": {
      "get": {
        "description": "只返回当前用户自己的会话。没有会话时 conversations 为空数组。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取会话时数据库不可用 |\n",
        "operationId": "CONV-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "conversations": [
                    {
                      "agent_id": 1,
                      "class_id": 1,
                      "created_at": "2026-10-06T00:00:00Z",
                      "id": 1,
                      "title": "新的提问",
                      "updated_at": "2026-10-06T00:00:00Z",
                      "user_id": 2
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpCONV001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取会话时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出我的会话",
        "tags": [
          "conversations"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取会话时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "必须指定本班已启用的智能体。其他班、未启用或不存在的智能体都是 404。本班一个已启用智能体都没有时返回 400。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：每次成功都新建一条会话，不因为重复提交而复用上一次的会话\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体或写入会话时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请选择智能体 | 正文为空，或 agent_id 小于 1 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 本班没有已启用的智能体 | 当前班级没有任何 enabled 的智能体 |\n| NOT_FOUND | 404 | 资源不存在 | 智能体不存在、不属于本班，或未启用 |\n",
        "operationId": "CONV-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "agent_id": 1
              },
              "schema": {
                "$ref": "#/components/schemas/OpCONV002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "agent_id": 1,
                  "class_id": 1,
                  "created_at": "2026-10-06T00:00:00Z",
                  "id": 1,
                  "title": "新的提问",
                  "updated_at": "2026-10-06T00:00:00Z",
                  "user_id": 2
                },
                "schema": {
                  "$ref": "#/components/schemas/OpCONV002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请选择智能体",
                    "value": {
                      "error": "请选择智能体"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "本班没有已启用的智能体",
                    "value": {
                      "error": "本班没有已启用的智能体"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请选择智能体。触发条件：正文为空，或 agent_id 小于 1\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：本班没有已启用的智能体。触发条件：当前班级没有任何 enabled 的智能体"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：智能体不存在、不属于本班，或未启用"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体或写入会话时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "按智能体新建会话",
        "tags": [
          "conversations"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取智能体或写入会话时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请选择智能体",
            "status": 400,
            "when": "正文为空，或 agent_id 小于 1"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "本班没有已启用的智能体",
            "status": 400,
            "when": "当前班级没有任何 enabled 的智能体"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "智能体不存在、不属于本班，或未启用"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新建一条会话，不因为重复提交而复用上一次的会话"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/conversations/{id}": {
      "get": {
        "description": "只能读取当前用户自己的、且属于本班的会话。别人的会话与不存在返回同一个 404。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取会话或消息时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析 |\n| NOT_FOUND | 404 | 资源不存在 | 会话不存在、不属于当前用户，或班级不一致 |\n",
        "operationId": "CONV-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "会话编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "conversation": {
                    "agent_id": 1,
                    "class_id": 1,
                    "created_at": "2026-10-06T00:00:00Z",
                    "id": 1,
                    "title": "新的提问",
                    "updated_at": "2026-10-06T00:00:00Z",
                    "user_id": 2
                  },
                  "messages": [
                    {
                      "citations": [],
                      "content": "移项怎么做",
                      "created_at": "2026-10-06T00:00:00Z",
                      "id": 1,
                      "role": "user",
                      "skill_name": ""
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpCONV003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：会话不存在、不属于当前用户，或班级不一致"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取会话或消息时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取会话和消息",
        "tags": [
          "conversations"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取会话或消息时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "会话不存在、不属于当前用户，或班级不一致"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/conversations/{id}/messages": {
      "post": {
        "description": "成功时响应是 text/event-stream，依次出现 meta、delta 和 done。meta 里的 status 表示是否依据资料，不是外包的业务码。正文里的 agent_id 不参与选择智能体。content 去掉空白后为 2 到 500 个字符。流已经开始后若失败，会再写一条 event 为 error 的数据，HTTP 状态仍是已经发出的 200。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：每次成功都追加用户消息和助手消息，不返回冲突，也不会复用上一条回答\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或流开始前检索与模型依赖不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析，或读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 问题至少需要 2 个字符 | content 去掉空白后不足 2 个字符 |\n| BAD_REQUEST | 400 | 问题过长 | content 超过 500 个字符 |\n| NOT_FOUND | 404 | 资源不存在 | 会话不存在或不属于当前用户。流开始前才会返回这个状态 |\n",
        "operationId": "CONV-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "会话编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "content": "移项怎么做"
              },
              "schema": {
                "$ref": "#/components/schemas/OpCONV004Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "text/event-stream": {
                "example": "event: meta\ndata: {\"status\":\"ok\",\"citations\":[]}\n\nevent: delta\ndata: {\"text\":\"先看等号两边。\"}\n\nevent: done\ndata: {\"message_id\":1}\n\n",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "完整响应体见 example"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "问题至少需要 2 个字符",
                    "value": {
                      "error": "问题至少需要 2 个字符"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "问题过长",
                    "value": {
                      "error": "问题过长"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析，或读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：问题至少需要 2 个字符。触发条件：content 去掉空白后不足 2 个字符\nBAD_REQUEST：问题过长。触发条件：content 超过 500 个字符"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：会话不存在或不属于当前用户。流开始前才会返回这个状态"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或流开始前检索与模型依赖不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "在会话中继续提问",
        "tags": [
          "conversations"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；对话组件未接上，或流开始前检索与模型依赖不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析，或读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "问题至少需要 2 个字符",
            "status": 400,
            "when": "content 去掉空白后不足 2 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "问题过长",
            "status": 400,
            "when": "content 超过 500 个字符"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "会话不存在或不属于当前用户。流开始前才会返回这个状态"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都追加用户消息和助手消息，不返回冲突，也不会复用上一条回答"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/demo-accounts": {
      "get": {
        "description": "不查用户表，因此后来插入的用户不会出现。口令来自环境变量 SEED_*_PASSWORD。日志只记条数，不记口令。示例里的 password 是占位符，实际响应写入当前配置的口令。\n\n认证：不需要令牌。此接口在白名单中。\n\n幂等：是。重复提交：只读。重复请求不写库，仍返回这三个固定账号\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n无业务错误码。",
        "operationId": "AUTH-002",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "accounts": [
                    {
                      "class_code": "A",
                      "label": "A 班教师",
                      "password": "\u003cSEED_TEACHER_PASSWORD\u003e",
                      "role": "teacher",
                      "username": "teacher_a"
                    },
                    {
                      "class_code": "A",
                      "label": "A 班学生",
                      "password": "\u003cSEED_STUDENT_A_PASSWORD\u003e",
                      "role": "student",
                      "username": "student_a1"
                    },
                    {
                      "class_code": "B",
                      "label": "B 班学生",
                      "password": "\u003cSEED_STUDENT_B_PASSWORD\u003e",
                      "role": "student",
                      "username": "student_b1"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          }
        },
        "security": [],
        "summary": "列出登录页上的三个种子账号",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [],
          "scheme": "none"
        },
        "x-errors": [],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，仍返回这三个固定账号"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/login": {
      "post": {
        "description": "校验用户名与口令。用户不存在、口令错误和登录锁定返回完全相同的状态码与响应体，不能用来枚举账号。角色和班级不写入令牌。\n\n认证：不需要令牌。此接口在白名单中。\n\n幂等：否。重复提交：再次提交会签发新令牌，并吊销本次请求里仍未过期的旧 Bearer。不复用上一枚令牌，也不返回冲突\n\n限流：是。维度：用户名 + 来源 IP（X-Forwarded-For 第一段，否则 RemoteAddr）。阈值：LOGIN_MAX_FAILURES，默认 5 次失败。时间窗口：LOGIN_LOCKOUT_SECONDS，默认 300 秒滑动窗口。超限错误码：INVALID_CREDENTIALS。\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON，或超过 4096 字节 |\n| INVALID_CREDENTIALS | 401 | 用户名或密码错误 | 用户不存在、口令错误，或该用户名与来源 IP 在窗口内的失败次数已达阈值。锁定期间即使口令正确也返回这一响应，不使用 429 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 按用户名查询或吊销旧令牌时数据库不可用 |\n| BAD_REQUEST | 500 | 请求无效 | 签发令牌失败 |\n",
        "operationId": "AUTH-003",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "password": "example-password",
                "username": "teacher_a"
              },
              "schema": {
                "$ref": "#/components/schemas/OpAUTH003Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "token": "\u003cjwt\u003e",
                  "user": {
                    "class_code": "A",
                    "class_id": 1,
                    "id": 1,
                    "role": "teacher",
                    "username": "teacher_a"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON，或超过 4096 字节"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "INVALID_CREDENTIALS": {
                    "summary": "用户名或密码错误",
                    "value": {
                      "error": "用户名或密码错误"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "INVALID_CREDENTIALS：用户名或密码错误。触发条件：用户不存在、口令错误，或该用户名与来源 IP 在窗口内的失败次数已达阈值。锁定期间即使口令正确也返回这一响应，不使用 429"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：签发令牌失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：按用户名查询或吊销旧令牌时数据库不可用"
          }
        },
        "security": [],
        "summary": "登录并签发访问令牌",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [],
          "scheme": "none"
        },
        "x-errors": [
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON，或超过 4096 字节"
          },
          {
            "code": "INVALID_CREDENTIALS",
            "description": "用户名或密码错误",
            "status": 401,
            "when": "用户不存在、口令错误，或该用户名与来源 IP 在窗口内的失败次数已达阈值。锁定期间即使口令正确也返回这一响应，不使用 429"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "按用户名查询或吊销旧令牌时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 500,
            "when": "签发令牌失败"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "再次提交会签发新令牌，并吊销本次请求里仍未过期的旧 Bearer。不复用上一枚令牌，也不返回冲突"
        },
        "x-rate-limit": {
          "dimension": "用户名 + 来源 IP（X-Forwarded-For 第一段，否则 RemoteAddr）",
          "enabled": true,
          "exceeded": "INVALID_CREDENTIALS",
          "note": "",
          "threshold": "LOGIN_MAX_FAILURES，默认 5 次失败",
          "window": "LOGIN_LOCKOUT_SECONDS，默认 300 秒滑动窗口"
        }
      }
    },
    "/api/logout": {
      "post": {
        "description": "把当前令牌的 jti 写入吊销表。响应里的 status 只表示已经登出，不是 HTTP 状态，也不是业务码。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：第一次返回 200。同一令牌再次调用会在鉴权阶段得到 401，不会再次写入吊销表\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；把 jti 写入吊销表时数据库不可用 |\n",
        "operationId": "AUTH-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "logged_out"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH004Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；把 jti 写入吊销表时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "登出并吊销当前令牌",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；把 jti 写入吊销表时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "第一次返回 200。同一令牌再次调用会在鉴权阶段得到 401，不会再次写入吊销表"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/materials": {
      "get": {
        "description": "班级只从会话取。query、正文和请求头里的 class_id 都没有生效路径。page_size 大于 100 时按 100 返回，不因此报错。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库，返回当前页\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | page 无效 | page 不是正整数 |\n| BAD_REQUEST | 400 | page_size 无效 | page_size 不是正整数 |\n| LIST_FAILED | 500 | 材料列表读取失败 | 读取本班材料时数据库失败 |\n",
        "operationId": "MAT-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "页码，从 1 开始。缺省 1。非整数或小于 1 返回 400",
            "example": 1,
            "in": "query",
            "name": "page",
            "required": false,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "每页条数。缺省 50，最大按 100 收束。非整数或小于 1 返回 400",
            "example": 50,
            "in": "query",
            "name": "page_size",
            "required": false,
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "按标题筛选的子串。缺省为不过滤。首尾空白会去掉",
            "example": "方程",
            "in": "query",
            "name": "title",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "materials": [
                    {
                      "class_code": "A",
                      "created_at": "2026-10-06T00:00:00Z",
                      "id": 1,
                      "title": "一元一次方程"
                    }
                  ],
                  "page": 1,
                  "page_size": 50,
                  "total": 1
                },
                "schema": {
                  "$ref": "#/components/schemas/OpMAT001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "page 无效",
                    "value": {
                      "error": "page 无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "page_size 无效",
                    "value": {
                      "error": "page_size 无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：page 无效。触发条件：page 不是正整数\nBAD_REQUEST：page_size 无效。触发条件：page_size 不是正整数"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "LIST_FAILED": {
                    "summary": "材料列表读取失败",
                    "value": {
                      "error": "材料列表读取失败"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "LIST_FAILED：材料列表读取失败。触发条件：读取本班材料时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "分页列出本班材料",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "page 无效",
            "status": 400,
            "when": "page 不是正整数"
          },
          {
            "code": "BAD_REQUEST",
            "description": "page_size 无效",
            "status": 400,
            "when": "page_size 不是正整数"
          },
          {
            "code": "LIST_FAILED",
            "description": "材料列表读取失败",
            "status": 500,
            "when": "读取本班材料时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回当前页"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "只接受 multipart/form-data 中的 .txt 或 .md。文件先限长再校验扩展名，失败时不留数据库记录，也不留孤儿文件。表单里的班级字段会被丢弃，目录只由会话班级决定。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：否。重复提交：每次成功都新增一条材料。文件名相同也不冲突，不会复用已有记录\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求必须是 multipart/form-data 并包含 file 字段 | 请求不是 multipart |\n| BAD_REQUEST | 400 | 请求缺少 file 字段 | 没有带文件名的 file 部分 |\n| BAD_REQUEST | 400 | 仅支持 .txt 与 .md 两种格式 | 扩展名不在白名单 |\n| BAD_REQUEST | 400 | 文件内容为空或不是 UTF-8 纯文本 | 解析后没有可用正文 |\n| BAD_REQUEST | 400 | 分段参数不正确 | 策略、分隔符或窗口不在允许范围内 |\n| BAD_REQUEST | 500 | 请求无效 | 生成存储名、创建班级目录或移动临时文件失败 |\n| PERSIST_FAILED | 500 | 入库失败，未创建任何记录 | 写入材料事务失败。已移动的文件会被删掉 |\n",
        "operationId": "MAT-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/OpMAT002Request"
              }
            }
          },
          "description": "multipart/form-data。班级字段即使提交也会被丢弃",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "chunk_count": 2,
                  "chunk_strategy": "auto",
                  "id": 1,
                  "title": "notes.md"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpMAT002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求必须是 multipart/form-data 并包含 file 字段",
                    "value": {
                      "error": "请求必须是 multipart/form-data 并包含 file 字段"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求缺少 file 字段",
                    "value": {
                      "error": "请求缺少 file 字段"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "仅支持 .txt 与 .md 两种格式",
                    "value": {
                      "error": "仅支持 .txt 与 .md 两种格式"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "文件内容为空或不是 UTF-8 纯文本",
                    "value": {
                      "error": "文件内容为空或不是 UTF-8 纯文本"
                    }
                  },
                  "BAD_REQUEST_5": {
                    "summary": "分段参数不正确",
                    "value": {
                      "error": "分段参数不正确"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求必须是 multipart/form-data 并包含 file 字段。触发条件：请求不是 multipart\nBAD_REQUEST：请求缺少 file 字段。触发条件：没有带文件名的 file 部分\nBAD_REQUEST：仅支持 .txt 与 .md 两种格式。触发条件：扩展名不在白名单\nBAD_REQUEST：文件内容为空或不是 UTF-8 纯文本。触发条件：解析后没有可用正文\nBAD_REQUEST：分段参数不正确。触发条件：策略、分隔符或窗口不在允许范围内"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "PERSIST_FAILED": {
                    "summary": "入库失败，未创建任何记录",
                    "value": {
                      "error": "入库失败，未创建任何记录"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：生成存储名、创建班级目录或移动临时文件失败\nPERSIST_FAILED：入库失败，未创建任何记录。触发条件：写入材料事务失败。已移动的文件会被删掉"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "教师上传本班材料",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 multipart/form-data 并包含 file 字段",
            "status": 400,
            "when": "请求不是 multipart"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求缺少 file 字段",
            "status": 400,
            "when": "没有带文件名的 file 部分"
          },
          {
            "code": "BAD_REQUEST",
            "description": "仅支持 .txt 与 .md 两种格式",
            "status": 400,
            "when": "扩展名不在白名单"
          },
          {
            "code": "BAD_REQUEST",
            "description": "文件内容为空或不是 UTF-8 纯文本",
            "status": 400,
            "when": "解析后没有可用正文"
          },
          {
            "code": "BAD_REQUEST",
            "description": "分段参数不正确",
            "status": 400,
            "when": "策略、分隔符或窗口不在允许范围内"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 500,
            "when": "生成存储名、创建班级目录或移动临时文件失败"
          },
          {
            "code": "PERSIST_FAILED",
            "description": "入库失败，未创建任何记录",
            "status": 500,
            "when": "写入材料事务失败。已移动的文件会被删掉"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新增一条材料。文件名相同也不冲突，不会复用已有记录"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/materials/{id}": {
      "get": {
        "description": "返回材料标题和知识库正文，不含服务器文件路径。先按编号取行，再比对会话班级。跨班与不存在的响应一致。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 编号无法解析、材料不存在，或材料属于其他班级 |\n| BAD_REQUEST | 500 | 请求无效 | 读取知识库正文时数据库失败 |\n",
        "operationId": "MAT-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "材料编号，正整数。非整数按不存在处理，返回 404，与跨班相同",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "material": {
                    "body_text": "移项后合并同类项。",
                    "class_code": "A",
                    "created_at": "2026-10-06T00:00:00Z",
                    "id": 1,
                    "title": "一元一次方程"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpMAT003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：编号无法解析、材料不存在，或材料属于其他班级"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取知识库正文时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取本班材料详情",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "编号无法解析、材料不存在，或材料属于其他班级"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 500,
            "when": "读取知识库正文时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/materials/{id}/file": {
      "get": {
        "description": "校验会话和班级后以 text/plain 返回文件。上传目录不进入任何静态资源根。响应没有 JSON 正文。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库，再次返回同一文件\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 编号无法解析、材料不存在、材料属于其他班级，或库中路径无法在上传目录内打开 |\n",
        "operationId": "MAT-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "材料编号，正整数。非整数按不存在处理，返回 404，与跨班相同",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "text/plain": {
                "example": "移项后合并同类项。",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "完整响应体见 example"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：编号无法解析、材料不存在、材料属于其他班级，或库中路径无法在上传目录内打开"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "下载本班材料原文",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "编号无法解析、材料不存在、材料属于其他班级，或库中路径无法在上传目录内打开"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，再次返回同一文件"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/materials/{id}/reindex": {
      "post": {
        "description": "只有教师可以调用。学生在进入处理函数前得到 403。空正文视为 auto 策略。分段策略只能是 auto、custom 或 hierarchy。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：是。重复提交：不新增材料。按本次分段参数重写该材料的切片和向量，再次调用仍返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；重建切片或写入向量失败 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| NOT_FOUND | 404 | 资源不存在 | 编号无法解析、材料不存在，或材料属于其他班级 |\n| BAD_REQUEST | 400 | 分段参数不正确 | 策略、分隔符或窗口不在允许范围内。custom 与 hierarchy 的 chunk_max_chars 为 100 到 2000，chunk_overlap_percent 为 0 到 50，分隔符只能是 newline、blank 或 period |\n| INDEX_MISSING | 500 | 索引尚未配置 | 服务没有接上索引器 |\n",
        "operationId": "MAT-005",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "材料编号，正整数。非整数按不存在处理，返回 404，与跨班相同",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "chunk_collapse_ws": false,
                "chunk_max_chars": 800,
                "chunk_overlap_percent": 10,
                "chunk_separator": "newline",
                "chunk_strategy": "auto",
                "chunk_strip_links": false
              },
              "schema": {
                "$ref": "#/components/schemas/OpMAT005Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "chunk_count": 2,
                  "chunk_strategy": "auto",
                  "material_id": 1,
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpMAT005Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "分段参数不正确",
                    "value": {
                      "error": "分段参数不正确"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：分段参数不正确。触发条件：策略、分隔符或窗口不在允许范围内。custom 与 hierarchy 的 chunk_max_chars 为 100 到 2000，chunk_overlap_percent 为 0 到 50，分隔符只能是 newline、blank 或 period"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：编号无法解析、材料不存在，或材料属于其他班级"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "INDEX_MISSING": {
                    "summary": "索引尚未配置",
                    "value": {
                      "error": "索引尚未配置"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "INDEX_MISSING：索引尚未配置。触发条件：服务没有接上索引器"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；重建切片或写入向量失败"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "重建本班某一材料的切片和向量",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；重建切片或写入向量失败"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "编号无法解析、材料不存在，或材料属于其他班级"
          },
          {
            "code": "BAD_REQUEST",
            "description": "分段参数不正确",
            "status": 400,
            "when": "策略、分隔符或窗口不在允许范围内。custom 与 hierarchy 的 chunk_max_chars 为 100 到 2000，chunk_overlap_percent 为 0 到 50，分隔符只能是 newline、blank 或 period"
          },
          {
            "code": "INDEX_MISSING",
            "description": "索引尚未配置",
            "status": 500,
            "when": "服务没有接上索引器"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "不新增材料。按本次分段参数重写该材料的切片和向量，再次调用仍返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/materials/{id}/vectors": {
      "get": {
        "description": "学生和教师都能看。跨班与不存在都是 404。vector 为空表示该切片尚未写入或写入失败。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；向量库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 编号无法解析、材料不存在，或材料属于其他班级 |\n| INDEX_MISSING | 500 | 索引尚未配置 | 服务没有接上向量来源 |\n| VECTOR_FAILED | 500 | 向量读取失败 | 读取向量时出现未归类的内部错误 |\n",
        "operationId": "MAT-006",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "材料编号。小于 1 或非整数按不存在处理，返回 404，与跨班相同",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "chunks": [
                    {
                      "chunk_index": 0,
                      "dim": 4,
                      "end_offset": 12,
                      "excerpt": "移项后合并同类项",
                      "index_status": "ready",
                      "mysql_chunk_id": 1,
                      "mysql_class_id": 1,
                      "mysql_entry_id": 1,
                      "mysql_material_id": 1,
                      "point_id": 1,
                      "preview": [
                        0.1,
                        0.2
                      ],
                      "start_offset": 0
                    }
                  ],
                  "class_id": 1,
                  "collection": "campusclaw_chunks",
                  "dim": 4,
                  "distance": "Cosine",
                  "knowledge_entry_id": 1,
                  "material_id": 1
                },
                "schema": {
                  "$ref": "#/components/schemas/OpMAT006Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：编号无法解析、材料不存在，或材料属于其他班级"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "INDEX_MISSING": {
                    "summary": "索引尚未配置",
                    "value": {
                      "error": "索引尚未配置"
                    }
                  },
                  "VECTOR_FAILED": {
                    "summary": "向量读取失败",
                    "value": {
                      "error": "向量读取失败"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "INDEX_MISSING：索引尚未配置。触发条件：服务没有接上向量来源\nVECTOR_FAILED：向量读取失败。触发条件：读取向量时出现未归类的内部错误"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；向量库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "查看本班材料的向量记录",
        "tags": [
          "materials"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；向量库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "编号无法解析、材料不存在，或材料属于其他班级"
          },
          {
            "code": "INDEX_MISSING",
            "description": "索引尚未配置",
            "status": 500,
            "when": "服务没有接上向量来源"
          },
          {
            "code": "VECTOR_FAILED",
            "description": "向量读取失败",
            "status": 500,
            "when": "读取向量时出现未归类的内部错误"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me": {
      "get": {
        "description": "返回会话对应的角色与班级，供界面决定是否显示教师入口。这只影响可发现性，授权仍以 RequireTeacher 和用户表为准。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库，返回同一用户\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n",
        "operationId": "AUTH-005",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "user": {
                    "class_code": "A",
                    "class_id": 1,
                    "id": 1,
                    "role": "teacher",
                    "username": "teacher_a"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH005Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取当前登录用户",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回同一用户"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/folders": {
      "get": {
        "description": "只返回当前用户的个人目录。parent_id 为空表示位于根上。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| LIST_FAILED | 500 | 服务暂时不可用，请稍后重试 | 读取个人目录时数据库失败 |\n",
        "operationId": "FLD-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "folders": [
                    {
                      "id": 1,
                      "name": "数学",
                      "parent_id": null
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpFLD001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "LIST_FAILED": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "LIST_FAILED：服务暂时不可用，请稍后重试。触发条件：读取个人目录时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出我的目录",
        "tags": [
          "folders"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "LIST_FAILED",
            "description": "服务暂时不可用，请稍后重试",
            "status": 500,
            "when": "读取个人目录时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "目录名去掉首尾空白后为 1 到 8 个字符，且不能包含斜杠。parent_id 省略或为 null 时建在根上。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：每次成功都新增一行。同级重名返回 400，不是 409，也不会改成更新已有目录\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON |\n| BAD_REQUEST | 400 | 目录名无效 | 名称为空、超过 8 个字符，或包含斜杠 |\n| BAD_REQUEST | 400 | 目录名称重复 | 同一父目录下已有同名目录 |\n| BAD_REQUEST | 400 | 不能再建下一层目录 | 父目录已经达到允许的深度 |\n| NOT_FOUND | 404 | 资源不存在 | parent_id 不是当前用户的目录 |\n",
        "operationId": "FLD-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "name": "数学",
                "parent_id": null
              },
              "schema": {
                "$ref": "#/components/schemas/OpFLD002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "folder": {
                    "id": 1,
                    "name": "数学",
                    "parent_id": null
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpFLD002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "目录名无效",
                    "value": {
                      "error": "目录名无效"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "目录名称重复",
                    "value": {
                      "error": "目录名称重复"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "不能再建下一层目录",
                    "value": {
                      "error": "不能再建下一层目录"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON\nBAD_REQUEST：目录名无效。触发条件：名称为空、超过 8 个字符，或包含斜杠\nBAD_REQUEST：目录名称重复。触发条件：同一父目录下已有同名目录\nBAD_REQUEST：不能再建下一层目录。触发条件：父目录已经达到允许的深度"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：parent_id 不是当前用户的目录"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "新建个人目录",
        "tags": [
          "folders"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON"
          },
          {
            "code": "BAD_REQUEST",
            "description": "目录名无效",
            "status": 400,
            "when": "名称为空、超过 8 个字符，或包含斜杠"
          },
          {
            "code": "BAD_REQUEST",
            "description": "目录名称重复",
            "status": 400,
            "when": "同一父目录下已有同名目录"
          },
          {
            "code": "BAD_REQUEST",
            "description": "不能再建下一层目录",
            "status": 400,
            "when": "父目录已经达到允许的深度"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "parent_id 不是当前用户的目录"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新增一行。同级重名返回 400，不是 409，也不会改成更新已有目录"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/folders/{id}": {
      "delete": {
        "description": "只删除当前用户自己的空目录。目录里还有内容时拒绝。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：第一次删除成功返回 200。同一编号再次删除返回 404，不是 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 目录里还有内容 | 目录下仍有子目录、笔记或收藏 |\n| NOT_FOUND | 404 | 资源不存在 | 目录不存在或不属于当前用户 |\n",
        "operationId": "FLD-005",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "目录编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpFLD005Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "目录里还有内容",
                    "value": {
                      "error": "目录里还有内容"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：目录里还有内容。触发条件：目录下仍有子目录、笔记或收藏"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：目录不存在或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "删除个人目录",
        "tags": [
          "folders"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "目录里还有内容",
            "status": 400,
            "when": "目录下仍有子目录、笔记或收藏"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "目录不存在或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "第一次删除成功返回 200。同一编号再次删除返回 404，不是 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "patch": {
        "description": "只改当前用户自己的目录名称。名称规则与新建相同。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：用相同名称再次提交覆盖原名，不新增目录，返回 200。与同级已有目录重名时返回 400\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON |\n| BAD_REQUEST | 400 | 目录名无效 | 名称为空、超过 8 个字符，或包含斜杠 |\n| BAD_REQUEST | 400 | 目录名称重复 | 同一父目录下已有另一个同名目录 |\n| NOT_FOUND | 404 | 资源不存在 | 目录不存在或不属于当前用户 |\n",
        "operationId": "FLD-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "目录编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "name": "代数"
              },
              "schema": {
                "$ref": "#/components/schemas/OpFLD003Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpFLD003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "目录名无效",
                    "value": {
                      "error": "目录名无效"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "目录名称重复",
                    "value": {
                      "error": "目录名称重复"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON\nBAD_REQUEST：目录名无效。触发条件：名称为空、超过 8 个字符，或包含斜杠\nBAD_REQUEST：目录名称重复。触发条件：同一父目录下已有另一个同名目录"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：目录不存在或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "重命名个人目录",
        "tags": [
          "folders"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON"
          },
          {
            "code": "BAD_REQUEST",
            "description": "目录名无效",
            "status": 400,
            "when": "名称为空、超过 8 个字符，或包含斜杠"
          },
          {
            "code": "BAD_REQUEST",
            "description": "目录名称重复",
            "status": 400,
            "when": "同一父目录下已有另一个同名目录"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "目录不存在或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "用相同名称再次提交覆盖原名，不新增目录，返回 200。与同级已有目录重名时返回 400"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/folders/{id}/move": {
      "post": {
        "description": "parent_id 省略或为 null 时移到根上。不能把目录移到自身或它的子孙下面。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：再次移动到同一位置不新增目录，返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON。空正文表示移到根上 |\n| BAD_REQUEST | 400 | 不能移动到这个位置 | 目标是自身、子孙，或移动后会超出深度 |\n| NOT_FOUND | 404 | 资源不存在 | 目录不存在、不属于当前用户，或 parent_id 不是正整数且不是 null |\n",
        "operationId": "FLD-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "目录编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "parent_id": null
              },
              "schema": {
                "$ref": "#/components/schemas/OpFLD004Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpFLD004Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "不能移动到这个位置",
                    "value": {
                      "error": "不能移动到这个位置"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON。空正文表示移到根上\nBAD_REQUEST：不能移动到这个位置。触发条件：目标是自身、子孙，或移动后会超出深度"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：目录不存在、不属于当前用户，或 parent_id 不是正整数且不是 null"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "移动个人目录",
        "tags": [
          "folders"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON。空正文表示移到根上"
          },
          {
            "code": "BAD_REQUEST",
            "description": "不能移动到这个位置",
            "status": 400,
            "when": "目标是自身、子孙，或移动后会超出深度"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "目录不存在、不属于当前用户，或 parent_id 不是正整数且不是 null"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "再次移动到同一位置不新增目录，返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/notes": {
      "get": {
        "description": "只返回当前用户的笔记摘要，不含正文。folder_id 为空表示放在根上。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| LIST_FAILED | 500 | 服务暂时不可用，请稍后重试 | 读取笔记列表时数据库失败 |\n",
        "operationId": "NOTE-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "notes": [
                    {
                      "folder_id": null,
                      "id": 1,
                      "title": "移项"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "LIST_FAILED": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "LIST_FAILED：服务暂时不可用，请稍后重试。触发条件：读取笔记列表时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出我的笔记",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "LIST_FAILED",
            "description": "服务暂时不可用，请稍后重试",
            "status": 500,
            "when": "读取笔记列表时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "title 可省略，省略时标题为「未命名」。显式空字符串返回 400。标题最长 32 个字符，正文最长 20000 个字符。folder_id 可省略或为 null。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：每次成功都新增一篇笔记，不因为标题相同而冲突\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON |\n| BAD_REQUEST | 400 | 标题无效 | 标题显式为空或超过 32 个字符 |\n| BAD_REQUEST | 400 | 正文过长 | body_text 超过 20000 个字符 |\n| NOT_FOUND | 404 | 资源不存在 | folder_id 不是正整数，或不属于当前用户 |\n",
        "operationId": "NOTE-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "body_text": "先保持等号两边相等。",
                "folder_id": null,
                "title": "移项"
              },
              "schema": {
                "$ref": "#/components/schemas/OpNOTE002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "note": {
                    "body_text": "先保持等号两边相等。",
                    "folder_id": null,
                    "id": 1,
                    "title": "移项"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "标题无效",
                    "value": {
                      "error": "标题无效"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "正文过长",
                    "value": {
                      "error": "正文过长"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON\nBAD_REQUEST：标题无效。触发条件：标题显式为空或超过 32 个字符\nBAD_REQUEST：正文过长。触发条件：body_text 超过 20000 个字符"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：folder_id 不是正整数，或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "新建笔记",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON"
          },
          {
            "code": "BAD_REQUEST",
            "description": "标题无效",
            "status": 400,
            "when": "标题显式为空或超过 32 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "正文过长",
            "status": 400,
            "when": "body_text 超过 20000 个字符"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "folder_id 不是正整数，或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新增一篇笔记，不因为标题相同而冲突"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/notes/{id}": {
      "delete": {
        "description": "只删除当前用户自己的笔记。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：否。重复提交：第一次删除成功返回 200。同一编号再次删除返回 404，不是 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 笔记不存在或不属于当前用户 |\n",
        "operationId": "NOTE-006",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "笔记编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE006Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：笔记不存在或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "删除一篇笔记",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "笔记不存在或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "第一次删除成功返回 200。同一编号再次删除返回 404，不是 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "get": {
        "description": "返回当前用户自己的笔记，包含正文。别人的笔记与不存在返回同一个 404。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 笔记不存在或不属于当前用户 |\n",
        "operationId": "NOTE-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "笔记编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "note": {
                    "body_text": "先保持等号两边相等。",
                    "folder_id": null,
                    "id": 1,
                    "title": "移项"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：笔记不存在或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取一篇笔记",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "笔记不存在或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "patch": {
        "description": "标题不能为空，最长 32 个字符。正文最长 20000 个字符。不能靠这个接口改目录。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：用相同内容再次提交覆盖原文，不新增笔记，返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON |\n| BAD_REQUEST | 400 | 标题无效 | 标题为空或超过 32 个字符 |\n| BAD_REQUEST | 400 | 正文过长 | body_text 超过 20000 个字符 |\n| NOT_FOUND | 404 | 资源不存在 | 笔记不存在或不属于当前用户 |\n",
        "operationId": "NOTE-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "笔记编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "body_text": "先保持等号两边相等。",
                "title": "移项"
              },
              "schema": {
                "$ref": "#/components/schemas/OpNOTE004Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "note": {
                    "body_text": "先保持等号两边相等。",
                    "folder_id": null,
                    "id": 1,
                    "title": "移项"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE004Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "标题无效",
                    "value": {
                      "error": "标题无效"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "正文过长",
                    "value": {
                      "error": "正文过长"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON\nBAD_REQUEST：标题无效。触发条件：标题为空或超过 32 个字符\nBAD_REQUEST：正文过长。触发条件：body_text 超过 20000 个字符"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：笔记不存在或不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "修改笔记标题和正文",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON"
          },
          {
            "code": "BAD_REQUEST",
            "description": "标题无效",
            "status": 400,
            "when": "标题为空或超过 32 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "正文过长",
            "status": 400,
            "when": "body_text 超过 20000 个字符"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "笔记不存在或不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "用相同内容再次提交覆盖原文，不新增笔记，返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/notes/{id}/move": {
      "post": {
        "description": "folder_id 省略或为 null 时放到根上。只能移动当前用户自己的笔记。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：再次移动到同一位置不新增笔记，返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON。空正文表示放到根上 |\n| NOT_FOUND | 404 | 资源不存在 | 笔记不存在、目录不属于当前用户，或 folder_id 不是正整数 |\n",
        "operationId": "NOTE-005",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "笔记编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "folder_id": 1
              },
              "schema": {
                "$ref": "#/components/schemas/OpNOTE005Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpNOTE005Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON。空正文表示放到根上"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：笔记不存在、目录不属于当前用户，或 folder_id 不是正整数"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "移动笔记到目录",
        "tags": [
          "notes"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON。空正文表示放到根上"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "笔记不存在、目录不属于当前用户，或 folder_id 不是正整数"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "再次移动到同一位置不新增笔记，返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/resources": {
      "get": {
        "description": "只返回当前用户收藏的资料，以及它们所在的个人目录。folder_id 为空表示放在根上。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| LIST_FAILED | 500 | 服务暂时不可用，请稍后重试 | 读取个人收藏时数据库失败 |\n",
        "operationId": "RES-005",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "resources": [
                    {
                      "folder_id": null,
                      "id": 1,
                      "picked": true,
                      "subject": "数学",
                      "title": "一元一次方程"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES005Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "LIST_FAILED": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "LIST_FAILED：服务暂时不可用，请稍后重试。触发条件：读取个人收藏时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出我收藏的资料",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "LIST_FAILED",
            "description": "服务暂时不可用，请稍后重试",
            "status": 500,
            "when": "读取个人收藏时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/me/resources/{id}/move": {
      "post": {
        "description": "只移动当前用户已经收藏的资料。folder_id 省略或为 null 时放到根上。未收藏的资料不能靠这个接口新建收藏。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：再次移动到同一目录不新增收藏，返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON。空正文表示放到根上 |\n| NOT_FOUND | 404 | 资源不存在 | 资料未收藏、资料不存在、目录不属于当前用户，或 folder_id 不是正整数 |\n",
        "operationId": "RES-006",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "资料编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "folder_id": 1
              },
              "schema": {
                "$ref": "#/components/schemas/OpRES006Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES006Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON。空正文表示放到根上"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：资料未收藏、资料不存在、目录不属于当前用户，或 folder_id 不是正整数"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "移动已收藏的资料",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON。空正文表示放到根上"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "资料未收藏、资料不存在、目录不属于当前用户，或 folder_id 不是正整数"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "再次移动到同一目录不新增收藏，返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/prompt": {
      "get": {
        "description": "返回当前会话所在班级的提示词。学生和教师都能读。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取班级提示词时数据库不可用 |\n",
        "operationId": "PRM-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "body": "你是学生自学时的解题助手。用简体中文。"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpPRM001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取班级提示词时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取本班提示词",
        "tags": [
          "prompt"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取班级提示词时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "put": {
        "description": "只有教师可以改。正文去掉空白后为 1 到 2000 个字符。学生在进入处理函数前得到 403。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：是。重复提交：用相同正文再次保存会覆盖原提示词，不新增一行，返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；保存提示词时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 提示词长度不正确 | 去掉空白后少于 1 个字符或超过 2000 个字符 |\n",
        "operationId": "PRM-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "body": "你是学生自学时的解题助手。用简体中文。"
              },
              "schema": {
                "$ref": "#/components/schemas/OpPRM002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "body": "你是学生自学时的解题助手。用简体中文。"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpPRM002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "提示词长度不正确",
                    "value": {
                      "error": "提示词长度不正确"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：提示词长度不正确。触发条件：去掉空白后少于 1 个字符或超过 2000 个字符"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；保存提示词时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "保存本班提示词",
        "tags": [
          "prompt"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；保存提示词时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "提示词长度不正确",
            "status": 400,
            "when": "去掉空白后少于 1 个字符或超过 2000 个字符"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "用相同正文再次保存会覆盖原提示词，不新增一行，返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/resources": {
      "get": {
        "description": "返回全部资料库条目，并标明当前用户是否已收藏。班级不从请求读取。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| LIST_FAILED | 500 | 服务暂时不可用，请稍后重试 | 读取资料库时数据库失败 |\n",
        "operationId": "RES-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "resources": [
                    {
                      "id": 1,
                      "picked": false,
                      "subject": "数学",
                      "title": "一元一次方程"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "LIST_FAILED": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "LIST_FAILED：服务暂时不可用，请稍后重试。触发条件：读取资料库时数据库失败"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出资料库",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "LIST_FAILED",
            "description": "服务暂时不可用，请稍后重试",
            "status": 500,
            "when": "读取资料库时数据库失败"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/resources/{id}": {
      "get": {
        "description": "资料库不按班级拆行。编号无法解析或不存在时返回 404。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 编号无法解析、资料不存在，或读取时数据库没有这一行 |\n",
        "operationId": "RES-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "资料编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "resource": {
                    "body_text": "移项后合并同类项。",
                    "id": 1,
                    "picked": false,
                    "subject": "数学",
                    "title": "一元一次方程"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：编号无法解析、资料不存在，或读取时数据库没有这一行"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "读取一条资料库正文",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "编号无法解析、资料不存在，或读取时数据库没有这一行"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/resources/{id}/pick": {
      "delete": {
        "description": "删除当前用户与该资料的收藏关系。资料仍在资料库中。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：资料存在时，无论之前有没有收藏，都返回 200，不会新增数据，也不返回冲突\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| NOT_FOUND | 404 | 资源不存在 | 资料不存在，或编号无法解析 |\n",
        "operationId": "RES-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "资料编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES004Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：资料不存在，或编号无法解析"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "取消收藏一条资料",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "资料不存在，或编号无法解析"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "资料存在时，无论之前有没有收藏，都返回 200，不会新增数据，也不返回冲突"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "把资料放进当前用户的个人库。正文可以省略；folder_id 省略或为 null 时放在根上。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：同一用户再次收藏同一资料不会新增第二行，只更新 folder_id，并返回 200\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用 |\n| BAD_REQUEST | 400 | 请求无效 | 正文不是 JSON。空正文表示不指定目录，可以成功 |\n| NOT_FOUND | 404 | 资源不存在 | 资料不存在，folder_id 不是正整数，或目录不属于当前用户 |\n",
        "operationId": "RES-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "资料编号。0、负数和非整数按不存在处理，返回 404",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "folder_id": 1
              },
              "schema": {
                "$ref": "#/components/schemas/OpRES003Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "$ref": "#/components/schemas/OpRES003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：正文不是 JSON。空正文表示不指定目录，可以成功"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：资料不存在，folder_id 不是正整数，或目录不属于当前用户"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "收藏一条资料",
        "tags": [
          "resources"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "正文不是 JSON。空正文表示不指定目录，可以成功"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "资料不存在，folder_id 不是正整数，或目录不属于当前用户"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "同一用户再次收藏同一资料不会新增第二行，只更新 folder_id，并返回 200"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/search": {
      "post": {
        "description": "班级只从会话取，正文里的 class_id 读入后丢弃。q 去掉空白后为 2 到 500 个字符。mode 缺省 hybrid，只能是 keyword、vector 或 hybrid。limit 缺省 10，超过 SEARCH_MAX_RESULTS（默认 20）时按上限收束。offset 缺省 0。响应里的 status 是检索结论 ok 或 no_evidence，不是外包的业务码。无依据时 message 为「资料中未找到相关内容」，hits 为空，且不带伪造的 material_id。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：不新增业务数据。重复提交返回同一次检索的判定，不因为重复而冲突\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；mode 为 vector 或 hybrid 时向量库不可用。keyword 在向量库停止后仍可检索 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 查询至少需要 2 个字符 | q 去掉空白后不足 2 个字符 |\n| BAD_REQUEST | 400 | 查询过长 | q 超过 500 个字符 |\n| BAD_REQUEST | 400 | mode 只能是 keyword、vector 或 hybrid | mode 不是这三个值之一 |\n| BAD_REQUEST | 400 | limit 与 offset 不能为负 | limit 或 offset 小于 0 |\n| SEARCH_FAILED | 500 | 检索失败 | 检索过程出现未归类的内部错误 |\n",
        "operationId": "SRCH-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "limit": 10,
                "mode": "keyword",
                "offset": 0,
                "q": "移项"
              },
              "schema": {
                "$ref": "#/components/schemas/OpSRCH001Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "hits": [
                    {
                      "chunk_id": 1,
                      "chunk_index": 0,
                      "end_offset": 12,
                      "material_id": 1,
                      "material_title": "一元一次方程",
                      "score_final": 0.5,
                      "score_keyword": 1,
                      "score_vector": null,
                      "snippet": "移项后合并同类项",
                      "start_offset": 0
                    }
                  ],
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpSRCH001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "查询至少需要 2 个字符",
                    "value": {
                      "error": "查询至少需要 2 个字符"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "查询过长",
                    "value": {
                      "error": "查询过长"
                    }
                  },
                  "BAD_REQUEST_5": {
                    "summary": "mode 只能是 keyword、vector 或 hybrid",
                    "value": {
                      "error": "mode 只能是 keyword、vector 或 hybrid"
                    }
                  },
                  "BAD_REQUEST_6": {
                    "summary": "limit 与 offset 不能为负",
                    "value": {
                      "error": "limit 与 offset 不能为负"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：查询至少需要 2 个字符。触发条件：q 去掉空白后不足 2 个字符\nBAD_REQUEST：查询过长。触发条件：q 超过 500 个字符\nBAD_REQUEST：mode 只能是 keyword、vector 或 hybrid。触发条件：mode 不是这三个值之一\nBAD_REQUEST：limit 与 offset 不能为负。触发条件：limit 或 offset 小于 0"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "500": {
            "content": {
              "application/json": {
                "examples": {
                  "SEARCH_FAILED": {
                    "summary": "检索失败",
                    "value": {
                      "error": "检索失败"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "SEARCH_FAILED：检索失败。触发条件：检索过程出现未归类的内部错误"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；mode 为 vector 或 hybrid 时向量库不可用。keyword 在向量库停止后仍可检索"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "检索本班切片",
        "tags": [
          "search"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；mode 为 vector 或 hybrid 时向量库不可用。keyword 在向量库停止后仍可检索"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "查询至少需要 2 个字符",
            "status": 400,
            "when": "q 去掉空白后不足 2 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "查询过长",
            "status": 400,
            "when": "q 超过 500 个字符"
          },
          {
            "code": "BAD_REQUEST",
            "description": "mode 只能是 keyword、vector 或 hybrid",
            "status": 400,
            "when": "mode 不是这三个值之一"
          },
          {
            "code": "BAD_REQUEST",
            "description": "limit 与 offset 不能为负",
            "status": 400,
            "when": "limit 或 offset 小于 0"
          },
          {
            "code": "SEARCH_FAILED",
            "description": "检索失败",
            "status": 500,
            "when": "检索过程出现未归类的内部错误"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "不新增业务数据。重复提交返回同一次检索的判定，不因为重复而冲突"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/skills": {
      "get": {
        "description": "返回当前班级的技能。学生和教师都能读。没有技能时 skills 为空数组。\n\n认证：需要 Authorization: Bearer 访问令牌。角色可以是 teacher 或 student。\n\n幂等：是。重复提交：只读。重复请求不写库\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；读取技能时数据库不可用 |\n",
        "operationId": "SKL-001",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "skills": [
                    {
                      "description": "先让学生说出等号两边要保持相等。",
                      "enabled": true,
                      "id": 1,
                      "name": "方程解题引导",
                      "source": "名称：方程解题引导",
                      "subject": "数学",
                      "tools": [
                        "knowledge_search"
                      ],
                      "trigger": "方程,移项"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/OpSKL001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；读取技能时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "列出本班技能",
        "tags": [
          "skills"
        ],
        "x-auth": {
          "roles": [
            "student",
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；读取技能时数据库不可用"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "post": {
        "description": "只有教师可以新建。source 是技能文件原文：空行以上写名称、适用条件、资料、工具和启用，空行以下写说明。名称最长 32，适用条件最长 120，说明最长 2000，整份文件最长 8000。工具只能是 knowledge_search。启用只能写是或否。名称含「解题引导」的技能不写最终答案，这一约束由教师撰写，接口不另做检测。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：否。重复提交：每次成功都新增一条技能。同班同名返回 400，不是 409，也不会改成更新已有技能\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；写入技能时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 技能文件是空的 | source 为空 |\n| BAD_REQUEST | 400 | 技能文件需要一个空行，空行下面写技能说明 | 缺少分隔说明的空行 |\n| BAD_REQUEST | 400 | 技能名称已存在 | 本班已有同名技能 |\n",
        "operationId": "SKL-002",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "source": "名称：方程解题引导\n适用条件：方程,移项\n工具：knowledge_search\n资料：数学\n启用：是\n\n先让学生说出等号两边要保持相等。"
              },
              "schema": {
                "$ref": "#/components/schemas/OpSKL002Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "description": "先让学生说出等号两边要保持相等。",
                  "enabled": true,
                  "id": 1,
                  "name": "方程解题引导",
                  "source": "名称：方程解题引导",
                  "subject": "数学",
                  "tools": [
                    "knowledge_search"
                  ],
                  "trigger": "方程,移项"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpSKL002Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "技能文件是空的",
                    "value": {
                      "error": "技能文件是空的"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "技能文件需要一个空行，空行下面写技能说明",
                    "value": {
                      "error": "技能文件需要一个空行，空行下面写技能说明"
                    }
                  },
                  "BAD_REQUEST_5": {
                    "summary": "技能名称已存在",
                    "value": {
                      "error": "技能名称已存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：技能文件是空的。触发条件：source 为空\nBAD_REQUEST：技能文件需要一个空行，空行下面写技能说明。触发条件：缺少分隔说明的空行\nBAD_REQUEST：技能名称已存在。触发条件：本班已有同名技能"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；写入技能时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "用技能文件新建技能",
        "tags": [
          "skills"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；写入技能时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "技能文件是空的",
            "status": 400,
            "when": "source 为空"
          },
          {
            "code": "BAD_REQUEST",
            "description": "技能文件需要一个空行，空行下面写技能说明",
            "status": 400,
            "when": "缺少分隔说明的空行"
          },
          {
            "code": "BAD_REQUEST",
            "description": "技能名称已存在",
            "status": 400,
            "when": "本班已有同名技能"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "每次成功都新增一条技能。同班同名返回 400，不是 409，也不会改成更新已有技能"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/api/skills/{id}": {
      "delete": {
        "description": "只有教师可以删。成功时没有响应体。跨班与不存在都是 404。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：否。重复提交：第一次返回 204 且无响应体。同一编号再次删除返回 404，不是 204\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；删除技能时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析 |\n| NOT_FOUND | 404 | 资源不存在 | 技能不存在或不属于本班 |\n",
        "operationId": "SKL-004",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "技能编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "无响应体"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：技能不存在或不属于本班"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；删除技能时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "删除本班技能",
        "tags": [
          "skills"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；删除技能时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "技能不存在或不属于本班"
          }
        ],
        "x-idempotency": {
          "idempotent": false,
          "repeat": "第一次返回 204 且无响应体。同一编号再次删除返回 404，不是 204"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      },
      "put": {
        "description": "只有教师可以改本班技能。文件格式与新建相同。跨班与不存在都是 404。编号无法解析时是 400，发生在按编号取行之前。\n\n认证：需要 Authorization: Bearer 访问令牌，并且用户表中的角色必须是 teacher。\n\n幂等：是。重复提交：用相同文件再次提交覆盖原技能，不新增一行，返回 200。改成与另一条技能同名时返回 400\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n| 错误码 | HTTP 状态码 | 描述 | 触发条件 |\n| --- | --- | --- | --- |\n| UNAUTHORIZED | 401 | 未登录或会话已失效 | 没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在 |\n| UNAVAILABLE | 503 | 服务暂时不可用，请稍后重试 | 查询吊销记录或按 sub 读取用户时数据库不可用；更新技能时数据库不可用 |\n| FORBIDDEN | 403 | 当前角色无权执行该操作 | 已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色 |\n| BAD_REQUEST | 400 | 请求无效 | 编号无法解析，或读取正文失败 |\n| BAD_REQUEST | 400 | 请求必须是 JSON | 正文不是 JSON 对象 |\n| BAD_REQUEST | 400 | 技能文件需要一个空行，空行下面写技能说明 | 技能文件格式不对 |\n| BAD_REQUEST | 400 | 技能名称已存在 | 本班另一条技能已使用这个名称 |\n| NOT_FOUND | 404 | 资源不存在 | 技能不存在或不属于本班 |\n",
        "operationId": "SKL-003",
        "parameters": [
          {
            "description": "访问令牌。格式为 Bearer 加一个空格再加 JWT。角色和班级不在令牌里",
            "example": "Bearer \u003cjwt\u003e",
            "in": "header",
            "name": "Authorization",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "技能编号，必须是大于等于 1 的整数。无法解析时返回 400",
            "example": 1,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "source": "名称：方程解题引导\n适用条件：方程,移项\n工具：knowledge_search\n资料：数学\n启用：是\n\n先让学生说出等号两边要保持相等。"
              },
              "schema": {
                "$ref": "#/components/schemas/OpSKL003Request"
              }
            }
          },
          "description": "字段、是否必填和校验规则见 schema 与示例",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "description": "先让学生说出等号两边要保持相等。",
                  "enabled": true,
                  "id": 1,
                  "name": "方程解题引导",
                  "source": "名称：方程解题引导",
                  "subject": "数学",
                  "tools": [
                    "knowledge_search"
                  ],
                  "trigger": "方程,移项"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpSKL003Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "BAD_REQUEST": {
                    "summary": "请求无效",
                    "value": {
                      "error": "请求无效"
                    }
                  },
                  "BAD_REQUEST_2": {
                    "summary": "请求必须是 JSON",
                    "value": {
                      "error": "请求必须是 JSON"
                    }
                  },
                  "BAD_REQUEST_3": {
                    "summary": "技能文件需要一个空行，空行下面写技能说明",
                    "value": {
                      "error": "技能文件需要一个空行，空行下面写技能说明"
                    }
                  },
                  "BAD_REQUEST_4": {
                    "summary": "技能名称已存在",
                    "value": {
                      "error": "技能名称已存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "BAD_REQUEST：请求无效。触发条件：编号无法解析，或读取正文失败\nBAD_REQUEST：请求必须是 JSON。触发条件：正文不是 JSON 对象\nBAD_REQUEST：技能文件需要一个空行，空行下面写技能说明。触发条件：技能文件格式不对\nBAD_REQUEST：技能名称已存在。触发条件：本班另一条技能已使用这个名称"
          },
          "401": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAUTHORIZED": {
                    "summary": "未登录或会话已失效",
                    "value": {
                      "error": "未登录或会话已失效"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAUTHORIZED：未登录或会话已失效。触发条件：没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "FORBIDDEN": {
                    "summary": "当前角色无权执行该操作",
                    "value": {
                      "error": "当前角色无权执行该操作"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "FORBIDDEN：当前角色无权执行该操作。触发条件：已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "NOT_FOUND": {
                    "summary": "资源不存在",
                    "value": {
                      "error": "资源不存在"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "NOT_FOUND：资源不存在。触发条件：技能不存在或不属于本班"
          },
          "503": {
            "content": {
              "application/json": {
                "examples": {
                  "UNAVAILABLE": {
                    "summary": "服务暂时不可用，请稍后重试",
                    "value": {
                      "error": "服务暂时不可用，请稍后重试"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "UNAVAILABLE：服务暂时不可用，请稍后重试。触发条件：查询吊销记录或按 sub 读取用户时数据库不可用；更新技能时数据库不可用"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "用技能文件改写技能",
        "tags": [
          "skills"
        ],
        "x-auth": {
          "roles": [
            "teacher"
          ],
          "scheme": "bearer"
        },
        "x-errors": [
          {
            "code": "UNAUTHORIZED",
            "description": "未登录或会话已失效",
            "status": 401,
            "when": "没有 Authorization: Bearer、方案不是 Bearer、令牌为空或含空格、签名或算法不对、缺少 sub、jti 或 exp、已过期、jti 已吊销，或 sub 对应的用户不存在"
          },
          {
            "code": "UNAVAILABLE",
            "description": "服务暂时不可用，请稍后重试",
            "status": 503,
            "when": "查询吊销记录或按 sub 读取用户时数据库不可用；更新技能时数据库不可用"
          },
          {
            "code": "FORBIDDEN",
            "description": "当前角色无权执行该操作",
            "status": 403,
            "when": "已经登录，但用户表中的角色不是 teacher。不采信 X-Role、表单或正文里声明的角色"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求无效",
            "status": 400,
            "when": "编号无法解析，或读取正文失败"
          },
          {
            "code": "BAD_REQUEST",
            "description": "请求必须是 JSON",
            "status": 400,
            "when": "正文不是 JSON 对象"
          },
          {
            "code": "BAD_REQUEST",
            "description": "技能文件需要一个空行，空行下面写技能说明",
            "status": 400,
            "when": "技能文件格式不对"
          },
          {
            "code": "BAD_REQUEST",
            "description": "技能名称已存在",
            "status": 400,
            "when": "本班另一条技能已使用这个名称"
          },
          {
            "code": "NOT_FOUND",
            "description": "资源不存在",
            "status": 404,
            "when": "技能不存在或不属于本班"
          }
        ],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "用相同文件再次提交覆盖原技能，不新增一行，返回 200。改成与另一条技能同名时返回 400"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/health": {
      "get": {
        "description": "不经鉴权，也不查数据库。status 表示进程仍在响应，不是外包的业务码。\n\n认证：不需要令牌。此接口在白名单中。\n\n幂等：是。重复提交：只读。重复请求不写库，返回同一形状的正文\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n无业务错误码。",
        "operationId": "AUTH-001",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "check": "liveness",
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpAUTH001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          }
        },
        "security": [],
        "summary": "存活检查",
        "tags": [
          "auth"
        ],
        "x-auth": {
          "roles": [],
          "scheme": "none"
        },
        "x-errors": [],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回同一形状的正文"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/openapi.json": {
      "get": {
        "description": "返回由代码注解生成的整份 OpenAPI 3 文档。自动化测试直接解析这份 JSON。下面的成功示例只标出文档开头，实际响应体就是正在提供的这一份文档。\n\n认证：不需要令牌。此接口在白名单中。\n\n幂等：是。重复提交：只读。重复请求不写库，返回同一份文档\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n无业务错误码。",
        "operationId": "DOC-001",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "info": {
                    "title": "CampusClaw API"
                  },
                  "openapi": "3.0.3"
                },
                "schema": {
                  "$ref": "#/components/schemas/OpDOC001Response"
                }
              }
            },
            "description": "完整响应体见 example。JSON 内不再另放 HTTP 状态或业务码"
          }
        },
        "security": [],
        "summary": "读取 OpenAPI 文档",
        "tags": [
          "docs"
        ],
        "x-auth": {
          "roles": [],
          "scheme": "none"
        },
        "x-errors": [],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回同一份文档"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    },
    "/swagger/index.html": {
      "get": {
        "description": "返回 Swagger UI 页面，页面再请求 /openapi.json。同目录下的样式和脚本是页面资源，不单独编号。GET /swagger 返回同一份页面。\n\n认证：不需要令牌。此接口在白名单中。\n\n幂等：是。重复提交：只读。重复请求不写库，返回同一份页面\n\n限流：不限流。无限流维度、无阈值、无时间窗口、无超限错误码\n\n错误码：\n\n无业务错误码。",
        "operationId": "DOC-002",
        "responses": {
          "200": {
            "content": {
              "text/html": {
                "example": "\u003c!DOCTYPE html\u003e\u003chtml lang=\"zh-CN\"\u003e\u003chead\u003e\u003ctitle\u003eCampusClaw API\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003cdiv id=\"swagger-ui\"\u003e\u003c/div\u003e\u003c/body\u003e\u003c/html\u003e",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "完整响应体见 example"
          }
        },
        "security": [],
        "summary": "打开 Swagger UI",
        "tags": [
          "docs"
        ],
        "x-auth": {
          "roles": [],
          "scheme": "none"
        },
        "x-errors": [],
        "x-idempotency": {
          "idempotent": true,
          "repeat": "只读。重复请求不写库，返回同一份页面"
        },
        "x-rate-limit": {
          "dimension": "",
          "enabled": false,
          "exceeded": "",
          "note": "不限流。无限流维度、无阈值、无时间窗口、无超限错误码",
          "threshold": "",
          "window": ""
        }
      }
    }
  },
  "servers": [
    {
      "description": "同源入口。本机经 Nginx 默认为 http://localhost:8086",
      "url": "/"
    }
  ],
  "tags": [
    {
      "description": "登录、会话与班级名册",
      "name": "auth"
    },
    {
      "description": "班级材料",
      "name": "materials"
    },
    {
      "description": "资料库与个人收藏",
      "name": "resources"
    },
    {
      "description": "个人目录",
      "name": "folders"
    },
    {
      "description": "个人笔记",
      "name": "notes"
    },
    {
      "description": "检索与依据回答",
      "name": "search"
    },
    {
      "description": "解题智能体",
      "name": "agents"
    },
    {
      "description": "班级提示词",
      "name": "prompt"
    },
    {
      "description": "教师技能",
      "name": "skills"
    },
    {
      "description": "解题会话",
      "name": "conversations"
    },
    {
      "description": "接口文档",
      "name": "docs"
    }
  ]
}
