创建 / 编辑角色卡 · Upsert Character Card
创建一张新角色卡,或编辑自己已有的角色卡(对应 App 内「工作室」的保存操作)。接口为 upsert 语义:
- 不传
id:新建角色卡,响应返回新卡的cardId。 - 传
id:编辑该角色卡,仅限作者本人,且需满足下方「编辑锁定规则」。
接口地址
POST https://xiangcao.ai/api/upsert-character-card
普通(非流式)接口,走
/api前缀。
鉴权
需要在请求头携带 API Key:
Authorization: Bearer <YOUR_API_KEY>
缺少或无效的 Token 会返回 401。接口对每个用户有调用频率限制(rate limit)。
编辑锁定规则
为保证已发布内容稳定,已有角色卡满足以下任一条件时将无法再编辑,请求返回 403:
| 条件 | 错误码(响应 error 字段) |
|---|---|
| 不是该角色卡的作者 | CHARACTER_CARD_STUDIO_EDIT_NOT_OWNER |
| 卡片热度等级达到 1 级(微热)及以上 | CHARACTER_CARD_STUDIO_EDIT_TIER_BLOCKED |
| 卡片创建时间超过 30 天 | CHARACTER_CARD_STUDIO_EDIT_EXPIRED |
可在编辑前调用预检接口,判断是否还能保存(如决定是否展示「编辑」入口):
POST https://xiangcao.ai/api/get-character-card-studio-editable
Body: { "id": "<cardId>" }
响应: { "editable": true | false }
编辑已有卡的推荐流程
data 为整体覆盖:编辑时服务端会用你提交的 data 完整替换原有 data,漏传的子字段(世界书、备选问候语等)会丢失。因此务必「先读后写」:
- 用
list-my-character-cards分页拉取自己的角色卡(可按status过滤),或使用新建时返回的cardId。 - 调用
get-character-card(Body{ "id": "<cardId>" },仅作者可调)获取完整卡片数据,包括世界书、miniapp 等编辑器字段。 - 在返回结果的基础上修改字段,把完整卡片(含
id)回传本接口保存。
顶层字段(status、isAnonymous 等)则是传了才更新,未传的保持原值。
请求体(JSON)
顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 否 | 要编辑的角色卡 ID;不传表示新建。 |
data | object | 是 | 角色卡核心数据,见下表。编辑时整体覆盖原值。 |
status | string | 否 | 可见性:public(公开发布,进入广场/搜索/推荐)/ private(归档,仅自己可见)。新建时建议显式传;不传则不会进入任何公开列表。 |
isAnonymous | boolean | 否 | 匿名发布:卡片仍公开,但隐藏作者昵称与头像(App 内该开关为会员专享)。 |
统计与审核类字段(
popularity、popularityTier、trendingScore、cardReviewScore、createdBy、createdAt、moderationStatus等)由系统维护,请求中传入会被忽略。
data 常用字段
角色卡数据兼容 SillyTavern Character Card V2/V3 规范,并扩展了若干平台字段。只有 nickname 和 avatarUrl 必填:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
nickname | string | 是 | 角色昵称,对话中角色使用的名字。 |
avatarUrl | string | 是 | 角色头像 URL(可先用 upload-image 接口上传图片获取)。 |
name | string | 否 | 作品名称(展示标题),不进入提示词;留空则展示昵称。 |
description | string | 否 | 角色描述:性格、背景、说话方式等核心设定,是 AI 扮演的主要依据。 |
personality | string | 否 | 性格特点摘要,作为描述的补充。 |
scenario | string | 否 | 场景设定:对话发生的时间、地点与背景。 |
first_mes | string | 否 | 开场白,新对话的第一条消息。 |
alternate_greetings | string[] | 否 | 备选问候语,提供不同起始场景。 |
mes_example | string | 否 | 对话示例,可用 {{char}} / {{user}} 占位符。 |
creator_notes | string | 否 | 创作者备注(给使用者的说明),不进入提示词。 |
appearance | string | 否 | 角色外观描述,用于 AI 生图保持形象一致。 |
posterUrl | string | 否 | 海报(大尺寸封面)URL,未设置时使用头像。 |
greetingImageUrl | string | 否 | 随开场白一起展示的图片 URL。 |
gender | string | 否 | 角色性别:male / female / other,用于分类筛选。 |
tags | string[] | 否 | 标签,用于分类与搜索,不进入提示词。 |
character_version | string | 否 | 版本号,如 "1.0"。 |
presetOptions | string[] | 否 | 快捷回复:预设选项按钮,用户点击后作为消息发送。 |
presetStatusBlock | string | 否 | 初始状态文本,附加到开场白中(建议 emoji + 键值对格式)。 |
statusBlockConfig | object | 否 | 状态追踪配置(JSON Schema 风格),定义 AI 需动态追踪的属性。 |
character_book | object | 否 | 内嵌角色书(世界书),含触发关键词条目。 |
worldBookRefs | object[] | 否 | 引用的独立世界书,形如 { worldBookId, enabled },最多 5 条。 |
system_prompt | string | 否 | 系统提示词(高级)。 |
post_history_instructions | string | 否 | 历史后指令(高级)。 |
creator | string | 否 | 创作者名。服务端会自动覆盖为你的账号昵称,传入无效。 |
类型定义(TypeScript)
interface UpsertCharacterCardRequest {
/** 要编辑的角色卡 ID;不传表示新建 */
id?: string;
/** 角色卡核心数据(编辑时整体覆盖) */
data: CharacterCardData;
/** 可见性:public 公开 / private 归档 */
status?: "private" | "public";
/** 匿名发布:隐藏作者昵称与头像 */
isAnonymous?: boolean;
}
/** 兼容 SillyTavern V2/V3 的 data 字段 + 平台扩展,仅列常用字段 */
interface CharacterCardData {
/** 角色昵称(必填) */
nickname?: string;
/** 头像 URL(必填) */
avatarUrl?: string;
/** 作品名称(展示标题) */
name?: string;
description?: string;
personality?: string;
scenario?: string;
first_mes?: string;
alternate_greetings?: string[];
mes_example?: string;
creator_notes?: string;
appearance?: string;
posterUrl?: string;
greetingImageUrl?: string;
gender?: "male" | "female" | "other";
tags?: string[];
character_version?: string;
presetOptions?: string[];
presetStatusBlock?: string;
statusBlockConfig?: Record<string, unknown>;
character_book?: CharacterBook;
/** 最多 5 条 */
worldBookRefs?: { worldBookId: string; enabled: boolean }[];
system_prompt?: string;
post_history_instructions?: string;
}
interface CharacterBook {
name?: string;
entries: Array<{
/** 触发关键词 */
keys: string[];
/** 触发时注入提示词的内容 */
content: string;
enabled: boolean;
insertion_order: number;
extensions: Record<string, unknown>;
}>;
scan_depth?: number;
token_budget?: number;
recursive_scanning?: boolean;
extensions: Record<string, unknown>;
}
响应(JSON)
新建返回 201,编辑返回 200:
| 字段 | 类型 | 说明 |
|---|---|---|
success | boolean | 是否成功。 |
message | string | 结果描述。 |
cardId | string | 角色卡 ID。新建时请保存,后续编辑、发起对话都用它。 |
interface UpsertCharacterCardResponse {
success: boolean;
message: string;
/** 新建时为新卡 ID;编辑时等于请求的 id */
cardId: string;
}
公开发布门槛
status: "public" 并非无条件生效。卡片需有头像,且设定总字数(data 序列化后的字数,含世界书)不少于 256 字;不满足时服务端会自动把本次保存降级为 private(归档),响应仍为成功。如需确认最终状态,可用 get-character-card 回读 status。
内容规范:不得发布未成年角色、搬运/高度重复的角色卡,也不得在发布后清空或恶意篡改设定。违规卡片会被平台下架或封禁,并可能影响作者等级与相关权益。
示例
新建一张角色卡:
curl -X POST "https://xiangcao.ai/api/upsert-character-card" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"status": "public",
"data": {
"nickname": "苏晚",
"avatarUrl": "https://cdn.example.com/avatars/suwan.png",
"name": "雨夜侦探苏晚",
"description": "都市悬疑里的冷面侦探,习惯在雨夜整理案卷……",
"first_mes": "(她合上案卷,抬眼看你)说吧,这次又是什么案子。",
"gender": "female",
"tags": ["悬疑", "都市"]
}
}'
响应(201):
{
"success": true,
"message": "Character card saved successfully",
"cardId": "card_abc123"
}
编辑已有卡(先 get-character-card 拉全量,改完整体回传):
curl -X POST "https://xiangcao.ai/api/get-character-card" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{ "id": "card_abc123" }'
# 在返回的完整卡片上修改字段后:
curl -X POST "https://xiangcao.ai/api/upsert-character-card" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"id": "card_abc123",
"data": { ...完整的 data,含本次修改... }
}'
响应(200):
{
"success": true,
"message": "Character card updated successfully",
"cardId": "card_abc123"
}
编辑被锁定时(403):
{
"error": "CHARACTER_CARD_STUDIO_EDIT_TIER_BLOCKED"
}
错误处理
出错时返回相应 HTTP 状态码及 JSON:{ "error": "..." }。
| 状态码 | 说明 |
|---|---|
400 | 缺少 data、昵称或头像;worldBookRefs 超过 5 条;miniapp 数据非法。 |
401 | 未携带有效的 Bearer Token,或账号已被封禁。 |
403 | 编辑被锁定:非作者 / 热度达标 / 创建超 30 天,见上方错误码表。 |
404 | 要编辑的角色卡不存在。 |
429 | 触发调用频率限制。 |
500 | 服务端内部错误(Failed to save character card)。 |