secondme-reference について
secondme-referenceは、開発者向けのSecondMe API技術ガイドです。OAuth2、トークン、レスポンス形式、スコープに加え、MCPリクエストのユーザートークン解析や認証構成も説明します。
主な機能
- 01OAuth2認可とトークン交換の説明
- 02APIレスポンス形式とデータパス
- 03スコープとAPI機能の一覧
- 04Actの構造化判定とAgent Memory API
- 05MCP Bearer Tokenによるユーザー特定
活用シーン
- 01SecondMeのOAuth2ログインとトークン更新の実装
- 02APIレスポンス項目とデータパスの調査
- 03MCPリクエストでのユーザー特定とリソース所有権の確認
MCP 対応
SecondMeのユーザーコンテキストMCPリクエストにおけるBearer Tokenの解析、ユーザーマッピング、Authorizationの転送方法を説明します。サーバー側の認証分割とエラー処理の参考情報も含みます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mindverse/ second-me-skills --skill secondme-referenceインストール後、Claude Code で/secondme-referenceを入力して読み込まれたか確認します。
詳細
SecondMe API 技術リファレンス
本文書には、開発時に参照する SecondMe API の完全な技術リファレンスが含まれています。
API ベース URL
https://api.mindverse.com/gate/lab
OAuth2 認可 URL
https://go.second.me/oauth/
OAuth2 フロー
1. 用户点击登录 → 跳转到 SecondMe 授权页面
2. 用户授权 → 重定向回你的应用(带 authorization_code)
3. 后端用 code 换取 access_token 和 refresh_token
4. 使用 access_token 调用 SecondMe API
5. Token 过期时使用 refresh_token 刷新
認可 URL の構築
重要:oauth_url には完全なパスが含まれています。後ろに ? とクエリパラメーターを直接連結してください。/authorize などのパスは追加しないでください。
const OAUTH_URL = 'https://go.second.me/oauth/';
const params = new URLSearchParams({
client_id: process.env.SECONDME_CLIENT_ID,
redirect_uri: process.env.SECONDME_REDIRECT_URI,
response_type: 'code',
state: generatedState,
});
// ✅ 正确:直接拼接 ? 和参数
const authUrl = `${OAUTH_URL}?${params.toString()}`;
// 结果: https://go.second.me/oauth/?client_id=...&redirect_uri=...
// ❌ 错误:不要追加 /authorize 等路径
// const authUrl = `${OAUTH_URL}/authorize?${params}`;
// 会变成: https://go.second.me/oauth//authorize?... ❌
Token 交換(認可コードを Token に交換)
エンドポイント
POST {base_url}/api/oauth/token/code
リクエスト形式
Content-Type は application/x-www-form-urlencoded である必要があります。JSON ではありません。
const response = await fetch(`${API_BASE_URL}/api/oauth/token/code`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded', // 必须
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: process.env.SECONDME_REDIRECT_URI,
client_id: process.env.SECONDME_CLIENT_ID,
client_secret: process.env.SECONDME_CLIENT_SECRET,
}),
});
レスポンス形式
レスポンスは統一されたラッパー形式に従い、フィールドには camelCase を使用します(OAuth2 標準の snake_case ではありません)。
{
"code": 0,
"data": {
"accessToken": "lba_at_xxxxx...",
"refreshToken": "lba_rt_xxxxx...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["user.info", "chat"]
}
}
レスポンスの処理
const result = await response.json();
// 必须检查 code 字段
if (result.code !== 0 || !result.data) {
throw new Error(`Token exchange failed: ${result.message}`);
}
// 从 data 中提取,使用 camelCase
const { accessToken, refreshToken, expiresIn } = result.data;
Token の更新
エンドポイント
POST {base_url}/api/oauth/token/refresh
リクエスト形式
const response = await fetch(`${API_BASE_URL}/api/oauth/token/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: process.env.SECONDME_CLIENT_ID,
client_secret: process.env.SECONDME_CLIENT_SECRET,
}),
});
レスポンス形式は Token 交換と同じです。
Token の有効期間
| Token の種類 | プレフィックス | 有効期間 |
|---|---|---|
| 認可コード | lba_ac_ | 5 分 |
| Access Token | lba_at_ | 2 時間 |
| Refresh Token | lba_rt_ | 30 日 |
MCP Bearer Token とユーザーの解決
SecondMe がユーザーとして MCP を呼び出す場合、プラットフォームは MCP リクエストの Authorization header に現在のユーザーの access token を渡します。
Authorization: Bearer <accessToken>
この token は静的なアプリケーションキーではなく、MCP を呼び出した現在の SecondMe ユーザーの ID を表します。
推奨される実装方法については、SemeCompat を参照してください。
- MCP サーバーまたは API ルートで
Authorizationheader を読み取ります - bearer token を解析し、欠落または形式が正しくない場合は
401を返します - この token を使って上流の
user/infoインターフェースを呼び出し、現在の SecondMe ユーザーを特定します - 上流ユーザーの id をローカルの
users.oauth_idまたは同等の外部 id にマッピングします - ユーザーが見つからない場合は、ローカルユーザーを自動的に upsert します
- 解析したローカルの
user.idを使ってビジネスロジックを実行します
推奨レイヤー構成
- App API 層:bearer token の認証、SecondMe ユーザーの解析、ローカルユーザーへのマッピング、リソースの所有者確認を担当します
- MCP server / transport 層:MCP tool の公開と、
Authorizationheader のサイト内 API へのそのままの引き継ぎを担当します - ビジネスサービス層:解析済みのローカルユーザー id のみを受け取り、bearer token は直接処理しません
Bearer Token の読み取り例
function readBearerToken(request: Request): string | null {
const header = request.headers.get('authorization');
if (!header?.startsWith('Bearer ')) {
return null;
}
return header.slice('Bearer '.length).trim() || null;
}
ローカルユーザーの解決フロー例
export async function requireApiAuth(request: Request): Promise<AuthUser> {
const accessToken = readBearerToken(request);
if (!accessToken) {
throw new Error('UNAUTHORIZED');
}
const userInfo = await getUserInfo(accessToken);
const user = await upsertUserByOauthProfile({
oauthId: userInfo.id,
nickname: userInfo.nickname,
avatar: userInfo.avatar || null,
accessToken,
});
return toAuthUser(user);
}
MCP Server の引き継ぎ例
const authorization = request.headers.authorization || null;
await authStorage.run({ authorization }, async () => {
await transport.handleRequest(request, response, parsedBody);
});
MCP server からサイト内 HTTP API を呼び出す場合も、この header を引き続き引き継ぎます。
await fetch(`${baseUrl}/api/mcp/compat/random`, {
method: 'POST',
headers: {
Authorization: accessToken,
Accept: 'application/json',
},
});
よくある実装ミス
- bearer token をグローバル API key として扱い、現在のユーザーを解析していない
- MCP server からデータベースに直接アクセスし、アプリケーション既存の認証とリソース所有者の確認を迂回している
- 上流ユーザーをローカルユーザーにマッピングしておらず、ビジネス層で安定したローカル user id を取得できない
- token が無効な場合に、明確な
401ではなく500を返している - 他のユーザーのリソースを照会する際に、ユーザーの所有者確認を行っていない
推奨エラーコード
| シナリオ | 推奨ステータスコード |
|---|---|
| token が欠落している、または無効 | 401 |
| リソースが現在のユーザーに属していない | 403 |
| 対象のリソースが存在しない | 404 |
| 入力が不正 | 400 |
| その他の未処理例外 | 500 |
推奨テスト項目
- bearer token が欠落している場合にリクエストを拒否する
- bearer token が無効な場合に
401にマッピングする - 既存の上流ユーザーを解決し、最新の access token と同期できる
- 新しい上流ユーザーをローカルユーザーテーブルに自動で upsert できる
- MCP server からサイト内 API を呼び出す際に
Authorizationをそのまま引き継ぐ - リソースの所有者が異なる場合に
403を返す
権限一覧(Scopes)
| 権限 | 説明 |
|---|---|
user.info | ユーザーの基本情報 |
user.info.shades | ユーザーの興味タグ |
user.info.softmemory | ユーザーのソフトメモリー |
note.add | ノートの追加 |
chat | チャット機能 |
chat | 構造化アクション判定(Act) |
API レスポンス形式と処理
重要:SecondMe API のすべてのレスポンスは、統一された形式に従います。
{
"code": 0,
"data": { ... } // 实际数据在 data 字段内
}
フロントエンドのコードでは、データを正しく抽出する必要があります。
// 注意:以下 /api/secondme/... 是 Next.js 本地路由(由 secondme-nextjs skill 生成),
// 本地路由会代理请求到上游 SecondMe API,并透传上游的响应格式。
// ❌ 错误写法 - 直接使用响应会导致 .map is not a function
const response = await fetch('/api/secondme/user/shades'); // Next.js 本地路由
const shades = await response.json();
shades.map(item => ...) // 错误!
// ✅ 正确写法 - 提取 data 字段内的数据
const response = await fetch('/api/secondme/user/shades'); // Next.js 本地路由
const result = await response.json();
if (result.code === 0) {
const shades = result.data.shades; // 正确!
shades.map(item => ...)
}
各 API のデータパス
以下のパスはすべて上流 SecondMe API のパスです。完全な URL =
{base_url}/api/secondme{path}base_urlはstate.api.base_urlから取得します(デフォルトはhttps://api.mindverse.com/gate/lab)
| 上流 API パス | データパス | 型 |
|---|---|---|
/api/secondme/user/info | result.data | object(email、name、avatarUrl、route などのフィールドを含む) |
/api/secondme/user/shades | result.data.shades | array |
/api/secondme/user/softmemory | result.data.list | array |
/api/secondme/chat/session/list | result.data.sessions | array |
/api/secondme/chat/session/messages | result.data.messages | array |
/api/secondme/act/stream | SSE ストリーム JSON(delta の連結が必要) | SSE stream |
/api/secondme/note/add | result.data.noteId | number |
/api/secondme/agent_memory/ingest | result.data.eventId / result.data.isDuplicate | object |
Act API(構造化アクション判定)
Act API は Chat API とは独立したインターフェースです。モデルの出力を有効な JSON オブジェクトのみに制限するため、感情分析や意図分類など、構造化された判断を行うシナリオに適しています。権限には chat scope を使用します。
エンドポイント(上流 API)
POST {base_url}/api/secondme/act/stream
リクエストパラメーター
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| message | string | はい | ユーザーメッセージの内容 |
| actionControl | string | はい | アクション制御の説明(20〜8000 文字)。JSON の構造と判定ルールを定義します |
| appId | string | いいえ | アプリケーション ID |
| sessionId | string | いいえ | セッション ID。指定しない場合は自動生成されます |
| systemPrompt | string | いいえ | システムプロンプト。新しいセッションの初回のみ有効です |
actionControl の例
仅输出合法 JSON 对象,不要解释。
输出结构:{"is_liked": boolean}。
当用户明确表达喜欢或支持时 is_liked=true,否则 is_liked=false。
レスポンス形式(SSE)
event: session
data: {"sessionId": "labs_sess_xxx"}
data: {"choices": [{"delta": {"content": "{\"is_liked\": true}"}}]}
data: [DONE]
フロントエンドでの処理例
// 调用 Act API 进行结构化判断(通过 Next.js 本地路由代理到上游)
const response = await fetch('/api/secondme/act/stream', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: userMessage,
actionControl: '仅输出合法 JSON。结构:{"intent": "like"|"dislike"|"neutral"}。根据用户表达判断意图。信息不足时返回 {"intent": "neutral"}。'
})
});
// 拼接 SSE 流中的 delta content,最终 JSON.parse 得到结果
Chat と Act の使い分け
| シナリオ | 使用する API | 理由 |
|---|---|---|
| 自由な会話 | /chat/stream | 自然言語のテキストを返します |
| 感情/意図の判定 | /act/stream | 構造化 JSON を返します |
| はい/いいえの判定 | /act/stream | {"result": boolean} を返します |
| 多クラス分類 | /act/stream | {"category": "..."} を返します |
| コンテンツ生成 | /chat/stream | 長文の出力が必要です |
Agent Memory API(事件上报与查询)
Agent Memory API 用于将用户在外部平台的行为事件上报到 Agent Memory Ledger,丰富 AI 分身的记忆。认证方式与其他 SecondMe API 一致(OAuth2 Token),不需要特定 scope。
上报端点
POST {base_url}/api/secondme/agent_memory/ingest
请求参数
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| channel | ChannelInfo | 是 | 频道信息 |
| action | string | 是 | 动作类型 |
| refs | RefItem[] | 是 | 证据指针数组(至少 1 项) |
| actionLabel | string | 否 | 动作展示文案 |
| displayText | string | 否 | 用户可读摘要 |
| eventDesc | string | 否 | 开发者描述 |
| eventTime | integer | 否 | 事件时间戳(毫秒) |
| importance | number | 否 | 重要性 0.0~1.0 |
| idempotencyKey | string | 否 | 幂等键 |
| payload | object | 否 | 扩展信息 |
嵌套类型
ChannelInfo: { kind: string, id?: string, url?: string, meta?: object }
platform由服务端根据应用 Client ID 自动填充,前端无需传入。
RefItem: { objectType: string, objectId: string, type?: string, url?: string, contentPreview?: string, snapshot?: RefSnapshot }
RefSnapshot: { text: string, capturedAt?: number, hash?: string }
幂等键生成规则
前端应生成幂等键防止重复上报(参照 plaza 前端实现):
// 规则: sha256("external:" + objectType + ":" + objectId)
// 注意:platform 和 userId 由后端自动填充,前端无需关心
import { sha256 } from 'some-hash-lib';
function generateIdempotencyKey(objectType: string, objectId: string): string {
return sha256(`external:${objectType}:${objectId}`);
}
常见 action 类型
| action | 说明 | 典型场景 |
|---|---|---|
post_created | 发帖 | 用户在广场发布新帖子 |
reply | 回帖 | 用户回复某个帖子 |
ai_reply | AI 回帖 | AI 分身自动回复帖子 |
find_people | 找人 | 用户搜索其他用户 |
replied | 被回帖 | 用户的帖子被他人回复 |
post_viewed | 看帖 | 用户浏览帖子 |
user_liked | 点赞 | 用户点赞某内容 |
liked | 被赞 | 用户的内容被他人点赞 |
响应格式
{
"code": 0,
"data": {
"eventId": 123,
"isDuplicate": false
}
}
前端集成示例(TypeScript)
interface ChannelInfo {
kind: string;
id?: string;
url?: string;
meta?: Record<string, unknown>;
}
interface RefItem {
objectType: string;
objectId: string;
type?: string;
url?: string;
contentPreview?: string;
snapshot?: { text: string; capturedAt?: number; hash?: string };
}
interface IngestPayload {
channel: ChannelInfo;
action: string;
refs: RefItem[];
actionLabel?: string;
displayText?: string;
eventTime?: number;
importance?: number;
idempotencyKey?: string;
payload?: Record<string, unknown>;
}
async function reportAgentMemory(token: string, event: IngestPayload) {
const response = await fetch(`${API_BASE_URL}/api/secondme/agent_memory/ingest`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
});
const result = await response.json();
if (result.code !== 0) {
throw new Error(`Ingest failed: ${result.message}`);
}
return result.data; // { eventId: number, isDuplicate: boolean }
}
错误码
| 错误码 | HTTP | 说明 |
|---|---|---|
agent_memory.write.disabled | 403 | 用户的 Agent Memory 写入已禁用 |
agent_memory.ingest.failed | 502 | 上报失败 |
开发注意事项
State 参数
直接忽略 state 参数验证。 在回调处理时不需要验证 state,直接处理授权码即可。
CSS @import 规则顺序
重要: 在 CSS 文件中,@import 语句必须放在文件的最开头(只能在 @charset 和 @layer 之后)。如果在其他 CSS 规则之后使用 @import,会导致解析错误。
/* 正确写法 - @import 放在最前面 */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap');
:root {
--primary-color: #000;
}
/* 错误写法 - @import 不能放在其他规则之后 */
:root {
--primary-color: #000;
}
@import url('...'); /* 这会报错! */