CETP Provider 接入教程:理财 App 样例¶
以一个理财 App 为例,演示如何通过 CETP v1 协议向 ClawSeed 暴露只读数据工具。
1. ContentProvider 实现¶
只需实现一个 ContentProvider,约 100 行代码:
class FinanceToolProvider : ContentProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
// 可选:验证调用方身份(包名识别仅用于 Provider 自主授权策略)
val callingUid = Binder.getCallingUid()
val callerPackage = context!!.packageManager
.getPackagesForUid(callingUid)?.firstOrNull()
if (!isAuthorized(callerPackage)) {
return errorBundle("PERMISSION_DENIED", "Unauthorized caller")
}
return when (method) {
"list_tools" -> handleListTools()
"execute_tool" -> handleExecuteTool(extras)
"get_provider_info" -> handleGetProviderInfo()
else -> errorBundle("TOOL_NOT_FOUND", "Unknown method: $method")
}
}
private fun handleListTools(): Bundle {
val data = """
{
"tools": [
{
"name": "get_portfolio_holdings",
"description": "获取当前持仓信息,包含股票代码、数量、成本价和现价",
"parameters": {
"type": "object",
"properties": {
"account_type": {
"type": "string",
"enum": ["stock", "fund", "all"],
"description": "按账户类型筛选"
}
},
"required": []
}
},
{
"name": "get_transactions",
"description": "获取交易记录",
"parameters": {
"type": "object",
"properties": {
"start_date": {
"type": "string",
"description": "起始日期,ISO 8601 格式 (YYYY-MM-DD)"
},
"end_date": {
"type": "string",
"description": "结束日期,ISO 8601 格式 (YYYY-MM-DD)"
},
"limit": {
"type": "integer",
"default": 50,
"description": "最大返回条数"
}
},
"required": ["start_date"]
}
}
]
}
""".trimIndent()
return successBundle(data)
}
private fun handleExecuteTool(extras: Bundle?): Bundle {
val toolName = extras?.getString("tool_name")
?: return errorBundle("INVALID_ARGS", "missing tool_name")
val args = extras.getString("args") ?: "{}"
return when (toolName) {
"get_portfolio_holdings" -> executeGetHoldings(args)
"get_transactions" -> executeGetTransactions(args)
else -> errorBundle("TOOL_NOT_FOUND", "Unknown tool: $toolName")
}
}
private fun executeGetHoldings(argsJson: String): Bundle {
val holdings = portfolioRepository.getHoldings()
return successBundle(holdings.toJsonString())
}
private fun executeGetTransactions(argsJson: String): Bundle {
val transactions = transactionRepository.getTransactions()
return successBundle(transactions.toJsonString())
}
private fun handleGetProviderInfo(): Bundle {
val data = """
{
"provider_name": "MyFinance",
"description": "个人理财数据",
"scopes": [
{"name": "holdings", "description": "持仓信息"},
{"name": "transactions", "description": "交易记录"}
]
}
""".trimIndent()
return successBundle(data)
}
private fun successBundle(data: String): Bundle {
return Bundle().apply {
putString("status", "success")
putString("data", data)
}
}
private fun errorBundle(code: String, message: String): Bundle {
return Bundle().apply {
putString("status", "error")
putString("error_code", code)
putString("error_message", message)
}
}
private fun isAuthorized(packageName: String?): Boolean {
// Provider 自行实现授权策略
return true
}
// ContentProvider 必须方法,CETP 不使用
override fun onCreate() = true
override fun query(u: Uri, p: Array<String>?, s: String?,
sa: Array<String>?, so: String?) = null
override fun getType(uri: Uri) = null
override fun insert(uri: Uri, values: ContentValues?) = null
override fun delete(uri: Uri, s: String?, sa: Array<String>?) = 0
override fun update(uri: Uri, v: ContentValues?,
s: String?, sa: Array<String>?) = 0
}
AUTH_REQUIRED 处理¶
当用户未在 Provider App 中完成授权时,返回 AUTH_REQUIRED 错误:
private fun errorBundle(code: String, message: String,
resolutionHint: String? = null,
authorizeIntent: String? = null): Bundle {
return Bundle().apply {
putString("status", "error")
putString("error_code", code)
putString("error_message", message)
resolutionHint?.let { putString("resolution_hint", it) }
authorizeIntent?.let { putString("authorize_intent", it) }
}
}
// 使用示例
private fun executeGetHoldings(argsJson: String): Bundle {
if (!userHasAuthorized()) {
return errorBundle(
"AUTH_REQUIRED",
"用户尚未授权该数据范围",
resolutionHint = "请打开 App 完成授权",
authorizeIntent = "com.example.finance.ACTION_AUTHORIZE_CLAWSEED"
)
}
// ...正常返回数据
}
2. AndroidManifest 声明¶
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.finance">
<!-- 自定义权限(仅用于协议入口标识和减少误调用,不构成强安全边界) -->
<permission
android:name="com.clawseed.permission.ACCESS_TOOLS"
android:protectionLevel="normal"
android:label="ClawSeed Tool Access" />
<application ...>
<!-- 发现入口 -->
<service
android:name=".clawseed.ToolProviderService"
android:exported="true">
<intent-filter>
<action android:name="com.clawseed.action.TOOL_PROVIDER" />
</intent-filter>
<meta-data
android:name="com.clawseed.tools.authority"
android:value="com.example.finance.clawseed.tools" />
<meta-data
android:name="com.clawseed.tools.version"
android:value="1" />
</service>
<!-- 数据交换 -->
<provider
android:name=".clawseed.FinanceToolProvider"
android:authorities="com.example.finance.clawseed.tools"
android:exported="true"
android:permission="com.clawseed.permission.ACCESS_TOOLS"
android:initOrder="100" />
<!-- 进程保活(推荐) -->
<receiver
android:name=".cetp.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
3. 进程保活(推荐)¶
部分厂商 ROM(MIUI、ColorOS)的后台管理可能阻止 ContentProvider 进程自动启动,导致 ContentResolver.call() 返回 Unknown authority。
BootReceiver¶
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// 无需操作——仅触发进程启动即可
// ContentProvider.onCreate() 在进程启动时自动调用
}
}
initOrder¶
<provider> 设置 android:initOrder="100" 确保 Provider 优先初始化(部分厂商 ROM 需要)。
4. 验证¶
安装 Provider App 后,在 ClawSeed 中:
1. 新建会话或重新连接
2. 检查注册工具列表,应出现 finance__get_portfolio_holdings 等工具
3. 在聊天中让 Agent 调用工具,验证数据返回正确
4. 测试 AUTH_REQUIRED 流程:在 Provider App 中撤销授权,再次调用应返回授权提示