## GitHub 同步说明 | 项 | 内容 | |:---|:---| | 仓库 | https://github.com/fnvtk/cunkebao_v3 | | 分支 | develop | | 方向 | 本地 → GitHub(单向 push) | | 是否拉取远程 | 否(未 fetch / pull / merge) | | 是否改本地源文件 | 否(仅 git add/commit,未编辑任何业务/文档正文) | | 远程基准 | origin/develop @2753265e| | 本批工作区变更 | 563 files, +30324 / -7013 lines | ## 一并推送的本地已有 commit(此前未 push) 1.f7a7c56cchore(触客宝): 全量构建部署 kr-kf 生产并通过线上 smoke 2.8801f5fffeat(触客宝): TKB-120 快捷语整组互拷与 AI 五步整理 3.c396ed35docs(触客宝): 0603 已完成项归档并交付 TKB-117-B 超管存储 4.e4ed53bcfix(部署): SSH 主机密钥变更兼容并完成五端全量上线 ## 本 commit 目录统计 | 目录 | 文件数 | 说明 | |:---|---:|:---| | 开发文档/ | 251 | 触客宝 0603~0608 归档、未完成项、五端账号速查 | | Touchkebao/ | 109 | 微信客服 CustomerList、快捷语、AI 获客等 | | Server/ | 108 | BFF/API、TaskCustomerList 等 | | Cunkebao/ | 53 | 存客宝移动端迭代 | | SuperAdmin/ | 24 | 超管后台 | | 官网/ | 7 | 官网小改 | | 其他 | 21 | .cursor、Moncter、docker-compose 等 | ## 重点文件(本地为准) - Touchkebao/.../CustomerList/index.tsx(微信客户列表) - 开发文档/1、需求/修改/触客宝_进行中_20260608.md - 开发文档/1、需求/修改/未完成项与跨仓验收汇总.md - 开发文档/10、项目管理/线上五端网址与账号密码速查.md - 开发文档/10、项目管理/已完成/触客宝_0607* 归档 - 开发文档/5、接口/02-存客宝对接/触客宝PC接口对接索引.md ## 刻意未上传 - node_modules/、.DS_Store、.smart-env/ - .开发文档_nested_git_backup/ - **/__pycache__/ ## 验收 - 本提交说明: https://github.com/fnvtk/cunkebao_v3/commit/HEAD - 分支: https://github.com/fnvtk/cunkebao_v3/tree/develop Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
7.3 KiB
PHP
214 lines
7.3 KiB
PHP
<?php
|
||
|
||
namespace app\chukebao\controller;
|
||
|
||
use app\chukebao\model\AiKnowledgeBaseType;
|
||
use library\ResponseHelper;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 模块 D · 知识库(需求三 §3.2 · 需求六 §6.7)
|
||
* 列表 = 存客宝已分配(enabledForTouchkebao=true),非全量;默认库由存客宝项目配置。
|
||
* 配置真源在存客宝 H5 + Server;触客宝只读已分配。
|
||
* 数据表:ck_kb_touchkebao_config(懒建,存放存客宝下发的分配结果)。
|
||
*/
|
||
class KnowledgeBaseController extends BaseController
|
||
{
|
||
private function ensureTable()
|
||
{
|
||
Db::execute("CREATE TABLE IF NOT EXISTS `ck_kb_touchkebao_config` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`companyId` int(11) NOT NULL DEFAULT 0,
|
||
`kbId` int(11) NOT NULL DEFAULT 0,
|
||
`kbName` varchar(128) NOT NULL DEFAULT '',
|
||
`isDefault` tinyint(1) NOT NULL DEFAULT 0,
|
||
`kefuId` int(11) NOT NULL DEFAULT 0,
|
||
`currentKbId` int(11) NOT NULL DEFAULT 0,
|
||
`enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||
`updateTime` int(11) NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_company` (`companyId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||
}
|
||
|
||
/**
|
||
* 仅返回已分配知识库
|
||
* GET /v1/kefu/ai/knowledge-bases
|
||
*/
|
||
public function knowledgeBases()
|
||
{
|
||
$this->ensureTable();
|
||
$companyId = $this->getUserInfo('companyId');
|
||
$kefuId = $this->getUserInfo('id');
|
||
|
||
// 仅返回 enabled 的库(存客宝下发);无分配 → 回退读存客宝项目知识库(REQ-TKB-133-C)
|
||
$rows = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('enabled', 1)
|
||
->where('kefuId', 0) // 项目级开放
|
||
->select();
|
||
|
||
$list = [];
|
||
$defaultKbId = null;
|
||
foreach ($rows as $r) {
|
||
$list[] = [
|
||
'id' => (int)$r['kbId'],
|
||
'name' => $r['kbName'],
|
||
'isDefault' => (bool)$r['isDefault'],
|
||
];
|
||
if ($r['isDefault']) {
|
||
$defaultKbId = (int)$r['kbId'];
|
||
}
|
||
}
|
||
|
||
if (empty($list)) {
|
||
$fallback = $this->fallbackKnowledgeFromWorkspace($companyId);
|
||
$list = $fallback['list'];
|
||
$defaultKbId = $fallback['defaultKbId'];
|
||
}
|
||
|
||
// 客服个人当前选择(覆盖默认,须在已开放范围内)
|
||
$currentKbId = $defaultKbId;
|
||
$myCurrent = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('kefuId', $kefuId)
|
||
->value('currentKbId');
|
||
if ($myCurrent && in_array((int)$myCurrent, array_column($list, 'id'))) {
|
||
$currentKbId = (int)$myCurrent;
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'defaultKbId' => $defaultKbId,
|
||
'currentKbId' => $currentKbId,
|
||
'list' => $list,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 客服在已分配范围内切换默认库
|
||
* POST /v1/kefu/ai/knowledge-base/select
|
||
*/
|
||
public function selectKnowledgeBase()
|
||
{
|
||
$this->ensureTable();
|
||
$companyId = $this->getUserInfo('companyId');
|
||
$kefuId = $this->getUserInfo('id');
|
||
$kbId = (int)$this->request->param('kbId', 0);
|
||
if (!$kbId) {
|
||
return ResponseHelper::error('kbId 缺失', 400);
|
||
}
|
||
// 校验是否在已开放范围内
|
||
$allowed = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('enabled', 1)
|
||
->where('kbId', $kbId)
|
||
->count();
|
||
if (!$allowed) {
|
||
return ResponseHelper::error('该知识库未对触客宝开放', 403);
|
||
}
|
||
$exist = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('kefuId', $kefuId)
|
||
->find();
|
||
if ($exist) {
|
||
Db::name('kb_touchkebao_config')->where('id', $exist['id'])
|
||
->update(['currentKbId' => $kbId, 'updateTime' => time()]);
|
||
} else {
|
||
Db::name('kb_touchkebao_config')->insert([
|
||
'companyId' => $companyId,
|
||
'kefuId' => $kefuId,
|
||
'currentKbId' => $kbId,
|
||
'enabled' => 0,
|
||
'updateTime' => time(),
|
||
]);
|
||
}
|
||
return ResponseHelper::success(['currentKbId' => $kbId], '已切换');
|
||
}
|
||
|
||
/**
|
||
* REQ-TKB-133-C:本地分配表为空时,聚合存客宝 AiKnowledgeTouchkebao 配置 + 项目知识库
|
||
*
|
||
* @return array{list:array<int,array{id:int,name:string,isDefault:bool}>,defaultKbId:?int}
|
||
*/
|
||
private function fallbackKnowledgeFromWorkspace(int $companyId): array
|
||
{
|
||
$kbRows = AiKnowledgeBaseType::where([
|
||
['isDel', '=', 0],
|
||
['status', '=', 1],
|
||
])->where(function ($q) use ($companyId) {
|
||
$q->where('companyId', $companyId)->whereOr('companyId', 0);
|
||
})->field('id,name,companyId')
|
||
->order('companyId desc,id desc')
|
||
->select();
|
||
|
||
if (empty($kbRows)) {
|
||
return ['list' => [], 'defaultKbId' => null];
|
||
}
|
||
|
||
$cfgRows = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('kefuId', 0)
|
||
->select();
|
||
$cfgMap = [];
|
||
$defaultKbId = null;
|
||
foreach ($cfgRows ?: [] as $c) {
|
||
$cfgMap[(int)$c['kbId']] = $c;
|
||
if (!empty($c['isDefault'])) {
|
||
$defaultKbId = (int)$c['kbId'];
|
||
}
|
||
}
|
||
|
||
$list = [];
|
||
$now = time();
|
||
foreach ($kbRows as $kb) {
|
||
$kbId = (int)$kb['id'];
|
||
$cfg = $cfgMap[$kbId] ?? null;
|
||
$enabled = $cfg ? (bool)$cfg['enabled'] : ((int)$kb['companyId'] === $companyId);
|
||
if (!$enabled) {
|
||
continue;
|
||
}
|
||
$isDefault = $cfg ? (bool)$cfg['isDefault'] : false;
|
||
$list[] = [
|
||
'id' => $kbId,
|
||
'name' => (string)($kb['name'] ?: ''),
|
||
'isDefault' => $isDefault,
|
||
];
|
||
if ($isDefault) {
|
||
$defaultKbId = $kbId;
|
||
}
|
||
}
|
||
|
||
// 仍无默认:优先项目库首条,其次系统库
|
||
if ($defaultKbId === null && !empty($list)) {
|
||
$defaultKbId = (int)$list[0]['id'];
|
||
$list[0]['isDefault'] = true;
|
||
}
|
||
|
||
// 懒同步写入分配表,便于后续 select 校验
|
||
if (!empty($list)) {
|
||
foreach ($list as $item) {
|
||
$exist = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('kefuId', 0)
|
||
->where('kbId', $item['id'])
|
||
->find();
|
||
if ($exist) {
|
||
continue;
|
||
}
|
||
Db::name('kb_touchkebao_config')->insert([
|
||
'companyId' => $companyId,
|
||
'kbId' => $item['id'],
|
||
'kbName' => $item['name'],
|
||
'isDefault' => $item['isDefault'] ? 1 : 0,
|
||
'kefuId' => 0,
|
||
'currentKbId' => 0,
|
||
'enabled' => 1,
|
||
'updateTime' => $now,
|
||
]);
|
||
}
|
||
}
|
||
|
||
return ['list' => $list, 'defaultKbId' => $defaultKbId];
|
||
}
|
||
}
|