180 lines
6.4 KiB
PHP
180 lines
6.4 KiB
PHP
<?php
|
||
|
||
namespace Leantime\Domain\Bom\Services;
|
||
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
/**
|
||
* Teable OpenAPI 客户端。
|
||
*
|
||
* 把 Teable 作为外部数据库:实时拉取表的字段元数据 + 全量记录。
|
||
* GET {base}/api/table/{tableId}/field 字段元数据
|
||
* GET {base}/api/table/{tableId}/record 记录(take/skip 分页)
|
||
* 统一 fieldKeyType=name,字段名(中文亦可)直接作为 JSON key。
|
||
*/
|
||
class Teable
|
||
{
|
||
private const DEFAULT_BASE = 'https://table.universal-onebot.com';
|
||
|
||
/**
|
||
* @return array{success:bool,fields:array,records:array,message?:string}
|
||
*/
|
||
public function fetchTable(array $source): array
|
||
{
|
||
$base = rtrim(trim($source['baseUrl'] ?? '') ?: self::DEFAULT_BASE, '/');
|
||
$tableId = $source['tableId'] ?? '';
|
||
$token = $source['token'] ?? '';
|
||
|
||
if ($tableId === '' || $token === '') {
|
||
return ['success' => false, 'message' => '缺少 tableId 或 token', 'fields' => [], 'records' => []];
|
||
}
|
||
|
||
try {
|
||
$fieldResp = Http::withToken($token)
|
||
->timeout(60)
|
||
->get("$base/api/table/$tableId/field");
|
||
|
||
if (! $fieldResp->successful()) {
|
||
return ['success' => false, 'message' => "字段接口 HTTP {$fieldResp->status()}", 'fields' => [], 'records' => []];
|
||
}
|
||
|
||
$fields = $fieldResp->json() ?? [];
|
||
|
||
$records = [];
|
||
$skip = 0;
|
||
$take = 1000;
|
||
|
||
while (true) {
|
||
$recResp = Http::withToken($token)
|
||
->timeout(60)
|
||
->get("$base/api/table/$tableId/record", [
|
||
'fieldKeyType' => 'name',
|
||
'take' => $take,
|
||
'skip' => $skip,
|
||
]);
|
||
|
||
if (! $recResp->successful()) {
|
||
return ['success' => false, 'message' => "记录接口 HTTP {$recResp->status()}", 'fields' => $fields, 'records' => []];
|
||
}
|
||
|
||
$body = $recResp->json() ?? [];
|
||
$page = $body['records'] ?? [];
|
||
$records = array_merge($records, $page);
|
||
|
||
if (count($page) < $take) {
|
||
break;
|
||
}
|
||
$skip += $take;
|
||
}
|
||
|
||
return ['success' => true, 'fields' => $fields, 'records' => $records];
|
||
} catch (\Throwable $e) {
|
||
return ['success' => false, 'message' => $e->getMessage(), 'fields' => [], 'records' => []];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从用户粘贴的整段 API 文档 / curl 命令中解析出连接信息。
|
||
*
|
||
* 容错策略:先清洗零宽字符/markdown 符号,再按 URL → Token → 表名顺序多模式兜底,
|
||
* 解析不完整时返回 missing 清单与已找到的部分,供前端手动补齐。
|
||
*
|
||
* @return array{success:bool,baseUrl:string,tableId:string,token:string,name:string,missing:array,message?:string}
|
||
*/
|
||
public function parseConfig(string $text): array
|
||
{
|
||
// 粘贴文本可能带零宽字符(U+200B 等)或 markdown 装饰,先做无害化清洗
|
||
$clean = (string) preg_replace('/[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{00AD}]/u', '', (string) $text);
|
||
$clean = str_replace(["\r\n", "\r"], "\n", $clean);
|
||
$clean = str_replace(['`', '**'], ['', ''], $clean);
|
||
|
||
$base = '';
|
||
$tableId = '';
|
||
$token = '';
|
||
$name = '';
|
||
|
||
// 1) 表名:# Table: xxx(兼容中英文冒号)
|
||
if (preg_match('/^#+\s*Table\s*[::]\s*(.+)$/mi', $clean, $m)) {
|
||
$name = trim($m[1]);
|
||
}
|
||
|
||
// 2) 收集所有 URL,优先取含 /api/table/ 或 /table/ 的完整地址
|
||
$urls = [];
|
||
if (preg_match_all('#https?://[^\s"\'<>()\[\]]+#', $clean, $mm)) {
|
||
$urls = $mm[0];
|
||
}
|
||
foreach ($urls as $u) {
|
||
if ($tableId === '' && preg_match('#/(?:api/)?table/([A-Za-z0-9_-]+)#', $u, $m)) {
|
||
$tableId = $m[1];
|
||
if (preg_match('#^(https?://[^/\s]+)#', $u, $bm)) {
|
||
$base = $bm[1];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 3) Token:Bearer 前缀 → Token:/API Key: 行 → 任意位置 teable_ 开头串
|
||
$tokPatterns = [
|
||
'#(?:Bearer|bearer)\s+([A-Za-z0-9_.+/=~%-]+)#',
|
||
'#(?:Token|token|API\s*Key|api\s*key)\s*[::=]\s*([A-Za-z0-9_.+/=~%-]+)#',
|
||
'#(teable_[A-Za-z0-9_.+/=~%-]+)#',
|
||
];
|
||
foreach ($tokPatterns as $p) {
|
||
if (preg_match($p, $clean, $m)) {
|
||
$token = $m[1];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 4) 表 ID 兜底:任意 tbl 开头长串
|
||
if ($tableId === '' && preg_match('#\btbl[A-Za-z0-9_-]{8,}#i', $clean, $m)) {
|
||
$tableId = $m[0];
|
||
}
|
||
|
||
// 5) Base URL 兜底:Base URL 行(值可在下一行)→ 任意 URL 的 host
|
||
if ($base === '') {
|
||
if (preg_match('#(?:Base\s*URL|baseUrl|基础\s*URL)\s*[::]\s*(https?://[^\s"\'<>()\[\]]+)#i', $clean, $m)
|
||
|| preg_match('#(?:Base\s*URL|baseUrl|基础\s*URL)\s*[::]?\s*\n\s*(https?://[^\s"\'<>()\[\]]+)#i', $clean, $m)) {
|
||
$base = rtrim($m[1], '/');
|
||
} elseif (! empty($urls)) {
|
||
if (preg_match('#^(https?://[^/\s]+)#', $urls[0], $bm)) {
|
||
$base = $bm[1];
|
||
}
|
||
}
|
||
}
|
||
|
||
$missing = [];
|
||
if ($base === '') {
|
||
$missing[] = 'Base URL';
|
||
}
|
||
if ($tableId === '') {
|
||
$missing[] = 'Table ID';
|
||
}
|
||
if ($token === '') {
|
||
$missing[] = 'Token';
|
||
}
|
||
|
||
if (! empty($missing)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '缺少:'.implode('、', $missing).'。请确认粘贴内容包含表地址(https://…/api/table/tbl…)和 Token(teable_…),或直接在下方输入框手动补齐',
|
||
'baseUrl' => $base,
|
||
'tableId' => $tableId,
|
||
'token' => $token,
|
||
'name' => $name,
|
||
'missing' => $missing,
|
||
];
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => '解析成功',
|
||
'baseUrl' => $base,
|
||
'tableId' => $tableId,
|
||
'token' => $token,
|
||
'name' => $name,
|
||
'missing' => [],
|
||
];
|
||
}
|
||
}
|