Files
Leantime/app/Domain/Bom/Services/Teable.php

180 lines
6.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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) TokenBearer 前缀 → 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…和 Tokenteable_…或直接在下方输入框手动补齐',
'baseUrl' => $base,
'tableId' => $tableId,
'token' => $token,
'name' => $name,
'missing' => $missing,
];
}
return [
'success' => true,
'message' => '解析成功',
'baseUrl' => $base,
'tableId' => $tableId,
'token' => $token,
'name' => $name,
'missing' => [],
];
}
}