zhuxiaojin / rocky-open
落基伟业开放平台 SDK(鉴权/账号/智能体/用户中心/归因渠道)
Requires
- php: >=8.0
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- phpunit/phpunit: ^9.0
README
包名:
zhuxiaojin/rocky-open适用 PHP 版本:8.0+ 依赖:guzzlehttp/guzzle ^7.10
封装落基伟业开放平台 13 个 API,覆盖鉴权、账号、智能体、用户中心、归因渠道五大模块。
目录
一、安装
composer require zhuxiaojin/rocky-open
二、核心概念
理解三个核心对象,避免使用歧义:
| 对象 | 职责 | 是否有状态 | 生命周期 |
|---|---|---|---|
Client |
SDK 入口,持有配置与鉴权方法 | 无状态(配置不可变) | 全局单例 |
Conversation |
已认证会话,聚合所有业务 API,持有 token | 有状态(持有 token) | 每次调用 withToken() 新建,按需创建 |
Apis\*Api |
业务接口分类,由 Conversation 持有 | 持有同一 token | 随 Conversation 生命周期 |
调用流程(必读)
┌─────────────┐ withToken(token) ┌──────────────────┐
│ Client │ ──────────────────→ │ Conversation │
│ (无状态) │ │ (持有 token) │
│ │ │ │
│ • auth() │ │ • account() │
│ • withToken │ │ • agent() │
│ • setBaseUrl│ │ • user() │
└─────────────┘ │ • channel() │
↑ └──────────────────┘
│ │
│ 业务方负责 token 缓存 │ 方法内自动注入 Bearer 头
│ (Redis/文件/内存任选) │ 调用方无需传 token
└───────────────────────────────────┘
Token 分类与使用范围(关键业务说明)
| Token 类型 | 获取方法 | 适用接口 | TTL |
|---|---|---|---|
| 企业 token | Client::auth()->getCompanyToken() |
账号、用户中心、归因渠道、换取账号 token | 7 天(604800s) |
| 账号 token | Client::auth()->getAccountToken($accountId, $companyToken) |
智能体(会话、列表、知识库) | 7 天(604800s) |
业务说明:
- 企业 token 用签名换取(需要 app_key/app_secret)
- 账号 token 用企业 token 换取(需要指定 accountId)
- 智能体相关接口必须用账号 token,不能用企业 token
- SDK 不缓存 token,业务方必须自行缓存与续期(见 Token 缓存策略)
三、快速开始
use Zhuxiaojin\RockyOpen\Client; // 1. 创建 Client(全局单例,配置不可变) $client = new Client([ 'app_key' => 'your-app-key', 'app_secret' => 'your-app-secret', // base_url 默认 https://rocky-micro.gantanhao.vip/gateway,可不传 ]); // 2. 获取企业 token(业务方自行缓存,见下文 Token 缓存策略) $companyToken = $client->auth()->getCompanyToken()['token']; // 3. 用企业 token 创建会话,调用账号/用户/渠道接口 $conversation = $client->withToken($companyToken); $conversation->account()->getDetail(13); $conversation->user()->detail('935641859206959105'); $conversation->channel()->list(); // 4. 调用智能体接口:先用企业 token 换账号 token,再创建新会话 $accountToken = $client->auth()->getAccountToken(15, $companyToken)['token']; $agentConv = $client->withToken($accountToken); $agentConv->agent()->list(); $agentConv->agent()->conversation([ 'id' => 'agent-665c8d42-a9ba-4ed2-9b91-facfb26fb29e', 'platform' => 'aliyun', 'messages' => [['role' => 'user', 'content' => '你好']], ]);
配置项
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
app_key |
string | 是 | - | 应用 Key(OPEN 开头) |
app_secret |
string | 是 | - | 应用 Secret |
base_url |
string | 否 | https://rocky-micro.gantanhao.vip/gateway |
API 网关地址 |
timeout |
float | 否 | 30.0 | 请求超时秒数 |
四、完整 API 参考
4.1 鉴权 Client::auth()
getCompanyToken(array $extra = []): array
获取企业 token。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$extra |
array | 否 | 额外参与签名的业务参数(按服务端要求传入,默认不传) |
返回:['token' => string, 'ttl' => int](ttl 单位秒)
示例:
$result = $client->auth()->getCompanyToken(); // ['token' => 'eyJ0eXAi...', 'ttl' => 604800]
getAccountToken(int $accountId, string $companyToken): array
获取账号 token(用于智能体接口)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$accountId |
int | 是 | 账号 ID(业务方上游账号 ID) |
$companyToken |
string | 是 | 企业 token |
返回:['token' => string, 'ttl' => int]
示例:
$result = $client->auth()->getAccountToken(15, $companyToken); // ['token' => 'eyJ0eXAi...', 'ttl' => 604800]
4.2 账号 Conversation::account()
使用企业 token 创建会话
getDetail(int $id): array
获取账号详情。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$id |
int | 是 | 账号 ID |
返回字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 账号 ID |
name |
string | 姓名 |
mobile |
string | 手机号 |
status |
mixed | 状态 |
avatar |
string | 头像 URL |
company_account_id |
string | 业务方账号 ID |
示例:
$conversation = $client->withToken($companyToken); $detail = $conversation->account()->getDetail(13); // ['id' => 13, 'name' => '赵云', 'mobile' => '', ...]
create(array $data): array
同步账号信息(创建或更新)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$data['name'] |
string | 是 | 姓名 |
$data['mobile'] |
string | 否 | 手机号 |
$data['avatar'] |
string | 否 | 头像 URL |
$data['company_account_id'] |
string | 是 | 业务方账号 ID |
返回:['id' => int](上游对应 ID)
示例:
$result = $conversation->account()->create([ 'name' => '赵云', 'mobile' => '', 'avatar' => '', 'company_account_id' => '12398', ]); // ['id' => 13]
4.3 智能体 Conversation::agent()
使用账号 token 创建会话(必须先用企业 token 换取账号 token)
list(): array
获取智能体列表。
返回:智能体数组,每个元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 智能体 ID(如 agent-xxx-xxx) |
name |
string | 智能体名称 |
icon |
string | 图标 URL |
status |
int | 状态 |
publish |
int | 发布状态 |
model_id |
string|null | 模型 ID |
platform_model |
object|null | 平台模型信息 |
business_scene |
object|null | 业务场景 |
created_at |
string | 创建时间 |
示例:
$agents = $agentConv->agent()->list(); // [['id' => 'agent-32ed3355...', 'name' => 'test', ...], ...]
knowledgeList(): array
查询知识库列表。
返回:知识库数组
示例:
$kbList = $agentConv->agent()->knowledgeList();
conversation(array $params): array
发起智能体会话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$params['id'] |
string | 是 | 智能体 ID(如 agent-xxx) |
$params['platform'] |
string | 是 | 平台标识(如 aliyun) |
$params['messages'] |
array | 二选一 | 消息列表(传入时不能传 conversation_id) |
$params['message'] |
array | 二选一 | 单条消息 |
$params['conversation_id'] |
string | 否 | 会话 ID(续接会话时传) |
$params['model_id'] |
string | 否 | 模型 ID |
$params['notify_url'] |
string | 否 | 异步回调地址(携带则转为异步回调) |
messages 元素结构:
[
'role' => 'user', // 角色
'content' => '你好', // 内容
'metadata' => [ // 元数据
'platform_id' => 1,
'corp_id' => 3,
'employee_id' => 2,
'session_id' => 1,
'session_msg_id' => 1,
'internal_user_id' => '221',
'external_contact_id' => 72513,
'ai_request_context_id' => 1,
'ai_request_context_request_id' => 'req_xxx',
'agent_switch_key' => 1,
],
'vars' => [], // 变量
]
返回字段:
| 字段 | 类型 | 说明 |
|---|---|---|
run_id |
string | 本次对话 ID |
conversation_id |
string | 会话 ID |
message.content |
string | Agent 回复内容 |
message.role |
string | 固定 assistant |
message.used_time |
string | 耗时(秒) |
metadata |
object | 元数据 |
usage.input_tokens |
int | 输入 token 数 |
usage.output_tokens |
int | 输出 token 数 |
示例:
$result = $agentConv->agent()->conversation([ 'id' => 'agent-665c8d42-a9ba-4ed2-9b91-facfb26fb29e', 'platform' => 'aliyun', 'messages' => [ ['role' => 'user', 'content' => '最近有什么号卡'], ], // 'conversation_id' => '894706989361778689', // 续接会话时传 // 'notify_url' => 'https://your.com/callback', // 异步回调 ]); // [ // 'run_id' => 'run-6402be45-...', // 'conversation_id' => '894706989361778689', // 'message' => ['content' => '我知道,请稍后', 'role' => 'assistant', 'used_time' => '2.27'], // ... // ]
4.4 用户中心 Conversation::user()
使用企业 token 创建会话
sync(array $data): mixed
用户同步(创建或更新用户)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$data['company_user_id'] |
string | 否 | 业务方自己的用户 ID |
$data['private_data'] |
array | 是 | 唯一属性(见下) |
$data['identity_data'] |
array | 否 | 私有属性(见下) |
$data['append_data'] |
array | 否 | 补充属性(见下) |
private_data 结构(唯一属性):
| 字段 | 类型 | 说明 |
|---|---|---|
mobile |
string | 手机号 |
unionid |
string | 微信 unionid |
email |
string | 邮箱 |
avatar |
string | 头像 |
nickname |
string | 昵称 |
extra |
object | 业务方自定义属性 |
registered_at |
string | 注册时间 |
remark |
string | 备注 |
channel_id |
string | 归因渠道 ID |
identity_data 结构(私有属性):
| 字段 | 类型 | 说明 |
|---|---|---|
gender |
int | 性别:0未知 / 1男 / 2女 |
birthday |
string | 生日(如 1989-03-01) |
province |
string | 省 |
city |
string | 市 |
district |
string | 区 |
real_name |
string | 真实姓名 |
id_card |
string | 身份证 |
append_data 结构(补充属性,针对游客类身份):
| 字段 | 类型 | 说明 |
|---|---|---|
external_user_id |
string | 企微用户 ID |
openid |
string | 公众号微信 ID |
示例:
$result = $conversation->user()->sync([ 'company_user_id' => '0981737618913123273', 'private_data' => [ 'mobile' => '13780964504', 'unionid' => '', 'email' => '', 'avatar' => '', 'nickname' => '昵称', 'extra' => new \stdClass(), 'registered_at'=> '', 'remark' => '', 'channel_id' => '', ], 'identity_data' => [ 'gender' => 0, 'birthday' => '1989-03-01', 'province' => '', 'city' => '', 'district' => '', 'real_name'=> '', 'id_card' => '', ], 'append_data' => [ 'external_user_id' => '', 'openid' => '', ], ]);
completeInfo(array $data): mixed
用户信息补充(手机号等)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$data['user_id'] |
string | 是 | 用户 ID |
$data['mobile'] |
string | 否 | 手机号 |
$data['unionid'] |
string | 否 | 微信 unionid |
$data['openid'] |
string | 否 | 微信 openid |
$data['company_user_id'] |
string | 否 | 业务方用户 ID |
示例:
$result = $conversation->user()->completeInfo([ 'user_id' => '935641859206959105', 'mobile' => '13154393691', 'unionid' => '6666g6', 'openid' => '8888g8', 'company_user_id' => '3_85698', ]);
detail(string $userId): array
用户详情。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$userId |
string | 是 | 用户 ID |
返回字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 用户 ID |
gender |
int | 性别 |
birthday |
string | 生日 |
real_name |
string | 真实姓名 |
province/city/district |
string|null | 地区 |
merge |
int | 合并标记 |
top_user_id |
int | 顶层用户 ID |
phones |
array | 手机号列表 [['user_id'=>..., 'phone'=>...]] |
identities |
array | 身份列表 |
company_profiles |
array | 公司档案列表 |
created_at/updated_at |
string | 时间戳 |
示例:
$detail = $conversation->user()->detail('935641859206959105'); // ['id' => 933728952886042624, 'gender' => 1, 'phones' => [...], ...]
4.5 归因渠道 Conversation::channel()
使用企业 token 创建会话
list(): array
渠道列表(不分页)。
返回:渠道数组,每个元素:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 渠道 ID |
company_id |
int | 公司 ID |
name |
string | 渠道名称 |
created_at |
string | 创建时间 |
updated_at |
string | 更新时间 |
paginate(string $name, int $pageSize = 15): array
渠道分页列表。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
$name |
string | 是 | - | 渠道名称(模糊查询,传空字符串查全部) |
$pageSize |
int | 否 | 15 | 每页条数 |
返回(Laravel 分页结构):
| 字段 | 类型 | 说明 |
|---|---|---|
current_page |
int | 当前页 |
data |
array | 数据列表 |
total |
int | 总数 |
per_page |
int | 每页条数 |
last_page |
int | 末页 |
next_page_url |
string|null | 下一页 URL |
prev_page_url |
string|null | 上一页 URL |
add(string $name): array
添加渠道。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
$name |
string | 是 | 渠道名称 |
返回:新建渠道对象(含 id、name、company_id、created_at、updated_at)
五、异常处理
所有接口失败均抛 Zhuxiaojin\RockyOpen\RockyOpenException。
use Zhuxiaojin\RockyOpen\RockyOpenException; try { $conversation->account()->getDetail(13); } catch (RockyOpenException $e) { $e->getMessage(); // 错误信息(如 "账号不存在") $e->getBusinessCode(); // 业务码(int|string|null,如 400、403) $e->getResponse(); // 原始响应(array 或 string,调试用) }
异常场景
| 场景 | 业务码 | 说明 |
|---|---|---|
| 业务错误 | 400/403/500 等 | 服务端返回的业务错误,code 字段非 200 |
| HTTP 失败 | 0 | 网络异常、超时等,getMessage() 含 Guzzle 异常信息 |
| 响应解析失败 | 0 | 返回非 JSON,getResponse() 为原始字符串 |
| 参数校验 | 0 | SDK 本地校验失败(如 conversation 缺 id 参数) |
六、Hyperf 协程接入(重点)
6.1 为什么需要特殊处理
SDK 默认使用 Guzzle 的 CurlMultiHandler,在 Swoole 协程环境下会阻塞整个 worker 进程而非当前协程,导致并发性能退化。必须替换为协程化 Handler。
6.2 配置定义
// config/autoload/rocky_open.php <?php declare(strict_types=1); return [ 'app_key' => env('ROCKY_APP_KEY', ''), 'app_secret' => env('ROCKY_APP_SECRET', ''), 'base_url' => env('ROCKY_BASE_URL', ''), // 留空使用 SDK 默认地址 'timeout' => (float) env('ROCKY_TIMEOUT', 30), ];
# .env ROCKY_APP_KEY=your-app-key ROCKY_APP_SECRET=your-app-secret # ROCKY_BASE_URL= # 留空使用默认 # ROCKY_TIMEOUT=30
6.3 Client 工厂(协程化关键)
// app/Provider/RockyOpenClientFactory.php <?php declare(strict_types=1); namespace App\Provider; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\HandlerStack; use Hyperf\Contract\ConfigInterface; use Hyperf\Guzzle\CoroutineHandler; use Psr\Container\ContainerInterface; use Zhuxiaojin\RockyOpen\Client; class RockyOpenClientFactory { public function __invoke(ContainerInterface $container): Client { $config = $container->get(ConfigInterface::class)->get('rocky_open', []); // ★ 关键:使用 Hyperf 的协程化 Handler $handler = new CoroutineHandler(); $stack = HandlerStack::create($handler); $guzzle = new GuzzleClient([ 'handler' => $stack, 'timeout' => (float) ($config['timeout'] ?? 30), ]); return new Client($config, $guzzle); } }
// config/dependencies.php <?php declare(strict_types=1); return [ \Zhuxiaojin\RockyOpen\Client::class => \App\Provider\RockyOpenClientFactory::class, ];
6.4 Token 缓存(协程安全)
SDK 不缓存 token,业务方必须自行实现。Hyperf 下推荐用 Redis:
// app/Service/RockyTokenService.php <?php declare(strict_types=1); namespace App\Service; use Hyperf\Redis\Redis; use Zhuxiaojin\RockyOpen\Client; class RockyTokenService { public function __construct( private Client $client, private Redis $redis, ) {} /** * 获取企业 token(自动缓存与续期) */ public function getCompanyToken(): string { $key = 'rocky:company_token'; $token = $this->redis->get($key); if (!empty($token)) { return $token; } $result = $this->client->auth()->getCompanyToken(); // 提前 60 秒过期,避免临界过期 $this->redis->setex($key, $result['ttl'] - 60, $result['token']); return $result['token']; } /** * 获取账号 token(按 accountId 分别缓存) */ public function getAccountToken(int $accountId): string { $key = "rocky:account_token:{$accountId}"; $token = $this->redis->get($key); if (!empty($token)) { return $token; } $companyToken = $this->getCompanyToken(); $result = $this->client->auth()->getAccountToken($accountId, $companyToken); $this->redis->setex($key, $result['ttl'] - 60, $result['token']); return $result['token']; } }
注册到容器:
// config/dependencies.php return [ \Zhuxiaojin\RockyOpen\Client::class => \App\Provider\RockyOpenClientFactory::class, \App\Service\RockyTokenService::class => \App\Service\RockyTokenService::class, ];
6.5 业务层使用(协程安全模式)
<?php declare(strict_types=1); namespace App\Service; use Hyperf\Redis\Redis; use Zhuxiaojin\RockyOpen\Client; use App\Service\RockyTokenService; class UserService { public function __construct( private Client $client, private RockyTokenService $tokenService, ) {} public function getUserDetail(string $userId): array { // 1. 获取 token(命中 Redis 缓存,多数情况无 HTTP 开销) $companyToken = $this->tokenService->getCompanyToken(); // 2. 每次调用创建独立 Conversation(协程内创建,协程间互不影响) $conversation = $this->client->withToken($companyToken); // 3. 调用业务接口 return $conversation->user()->detail($userId); } }
6.6 协程环境关键约束(必读)
| 约束 | 说明 | 违反后果 |
|---|---|---|
| 必须注入 CoroutineHandler | 见 6.3,或确保 SWOOLE_HOOK_ALL 启用 |
阻塞整个 worker,并发退化 |
| Client 可单例 | 无状态,配置不可变 | - |
| Conversation 不可单例 | 持有 token,必须每次 withToken() 新建 |
协程间 token 串用 |
| 不要运行时改配置 | setBaseUrl() 影响所有协程 |
竞态条件 |
| Token 必须用 Redis 缓存 | 内存缓存在协程间不共享,每次请求重新获取 token 造成浪费 | 性能下降 |
避免在协程内 sleep |
用 Hyperf\Coroutine\Coroutine::sleep() 代替原生 sleep |
阻塞 worker |
6.7 完整调用示例(Hyperf Controller)
<?php declare(strict_types=1); namespace App\Controller; use App\Service\RockyTokenService; use Zhuxiaojin\RockyOpen\Client; use Zhuxiaojin\RockyOpen\RockyOpenException; class AgentController extends AbstractController { public function __construct( private Client $client, private RockyTokenService $tokenService, ) {} public function chat() { $params = $this->request->all(); $accountId = (int) $this->request->input('account_id', 15); try { // 1. 换账号 token(Redis 缓存命中时无 HTTP 开销) $accountToken = $this->tokenService->getAccountToken($accountId); // 2. 创建会话(协程内独立) $conversation = $this->client->withToken($accountToken); // 3. 发起会话 $result = $conversation->agent()->conversation([ 'id' => $params['agent_id'], 'platform' => $params['platform'] ?? 'aliyun', 'messages' => $params['messages'], ]); return $this->response->json(['code' => 0, 'data' => $result]); } catch (RockyOpenException $e) { return $this->response->json([ 'code' => $e->getBusinessCode() ?? -1, 'msg' => $e->getMessage(), ]); } } }
七、常见问题
Q1: 调用渠道接口返回 403 "The token does not support the current scene"
原因:渠道接口路径为 /web-api/...,企业 token 的 scene 是 open,不支持 web-api。
解决:渠道接口需要后台 token,开放平台 SDK 暂不支持。如需调用,联系平台方确认 token 类型。
Q2: 调用智能体接口报 "该账号不存在"
原因:getAccountToken($accountId, ...) 的 $accountId 必须是上游真实存在的账号 ID。
解决:先用 account()->create([...]) 同步账号,用返回的 id 作为 accountId。
Q3: Token 缓存策略
推荐做法:
// 用 Redis 缓存,提前 60 秒过期避免临界 $key = 'rocky:company_token'; if ($token = $redis->get($key)) { return $token; } $result = $client->auth()->getCompanyToken(); $redis->setex($key, $result['ttl'] - 60, $result['token']); return $result['token'];
避免做法:
- ❌ 用静态变量/单例属性缓存(协程间不共享)
- ❌ 每次请求都重新获取(浪费 HTTP 往返)
Q4: 如何切换不同环境(测试/生产)
通过 base_url 配置切换:
// 测试环境 $client = new Client([ 'app_key' => 'xxx', 'app_secret' => 'xxx', 'base_url' => 'https://test-host/gateway', ]); // 生产环境(使用默认) $client = new Client([ 'app_key' => 'xxx', 'app_secret' => 'xxx', // base_url 不传,默认 https://rocky-micro.gantanhao.vip/gateway ]);
Q5: 异步会话回调
conversation() 传 notify_url 后转为异步,接口立即返回,结果通过回调推送:
$conversation->agent()->conversation([ 'id' => 'agent-xxx', 'platform' => 'aliyun', 'messages' => [...], 'notify_url'=> 'https://your-domain.com/callback/rocky', ]); // 接口立即返回,实际结果 POST 到 notify_url
回调地址需自行实现接收端点。