Files
Leantime/app/Domain/Bom/Services/Teable.php
wangruiguo d647428529
Some checks failed
Security Audit / Composer Security Audit (push) Has been cancelled
Security Audit / NPM Security Audit (push) Has been cancelled
Security Audit / CodeQL Analysis (push) Has been cancelled
OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
2026-09-03 18:49:20 +08:00

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) 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' => [],
];
}
}