|
@@ -11,6 +11,9 @@
|
|
|
// token: POST /v1.0/oauth2/accessToken {appKey, appSecret} → {accessToken, expireIn}
|
|
// token: POST /v1.0/oauth2/accessToken {appKey, appSecret} → {accessToken, expireIn}
|
|
|
// 记录: /v1.0/notable/bases/{baseId}/sheets/{sheetName}/records
|
|
// 记录: /v1.0/notable/bases/{baseId}/sheets/{sheetName}/records
|
|
|
// POST 批量新增 / PUT 批量更新 / GET 分页列表
|
|
// POST 批量新增 / PUT 批量更新 / GET 分页列表
|
|
|
|
|
+// ⚠ 所有 notable 接口必须在 query 上带 operatorId=操作人 unionId(企业内部应用
|
|
|
|
|
+// 无法从 token 推出用户身份;unionId 用 scripts/aitable-discover.js 发现一次后
|
|
|
|
|
+// 配到 config 的 aitable.operatorId,同一组织内固定不变)。
|
|
|
// ⚠ 端点形态与批量上限在首次真机联调时核对——所有钉钉细节都收敛在本文件,
|
|
// ⚠ 端点形态与批量上限在首次真机联调时核对——所有钉钉细节都收敛在本文件,
|
|
|
// 如有偏差只需改这里的常量与两个 transport 方法。
|
|
// 如有偏差只需改这里的常量与两个 transport 方法。
|
|
|
|
|
|
|
@@ -155,10 +158,17 @@ function createAitableService(ctx) {
|
|
|
|
|
|
|
|
function err(message, code) { const e = new Error(message); e.code = code; e.status = 400; return e; }
|
|
function err(message, code) { const e = new Error(message); e.code = code; e.status = 400; return e; }
|
|
|
|
|
|
|
|
|
|
+ // notable 接口统一拼 query:业务参数 + 必填 operatorId
|
|
|
|
|
+ function notableQuery(cfg, params = {}) {
|
|
|
|
|
+ const q = new URLSearchParams({ operatorId: cfg.operatorId || '', ...params });
|
|
|
|
|
+ return `?${q.toString()}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function requireConfig() {
|
|
function requireConfig() {
|
|
|
const cfg = config.aitable || {};
|
|
const cfg = config.aitable || {};
|
|
|
if (!cfg.enabled) throw err('钉钉 AI 表格同步未启用(系统设置中开启)', 'AITABLE_DISABLED');
|
|
if (!cfg.enabled) throw err('钉钉 AI 表格同步未启用(系统设置中开启)', 'AITABLE_DISABLED');
|
|
|
if (!cfg.baseId || !cfg.sheetName || !cfg.appKey) throw err('钉钉 AI 表格配置不完整(AppKey / Base ID / 数据表名称)', 'AITABLE_NO_CFG');
|
|
if (!cfg.baseId || !cfg.sheetName || !cfg.appKey) throw err('钉钉 AI 表格配置不完整(AppKey / Base ID / 数据表名称)', 'AITABLE_NO_CFG');
|
|
|
|
|
+ if (!cfg.operatorId) throw err('缺少操作者 unionId:先在钉钉开放平台给应用开通通讯录读权限,再运行 node scripts/aitable-discover.js 发现并填入', 'AITABLE_NO_OPERATOR');
|
|
|
return cfg;
|
|
return cfg;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -167,7 +177,7 @@ function createAitableService(ctx) {
|
|
|
const out = [];
|
|
const out = [];
|
|
|
let nextToken = '';
|
|
let nextToken = '';
|
|
|
for (;;) {
|
|
for (;;) {
|
|
|
- const q = `?maxResults=${LIST_PAGE}${nextToken ? `&nextToken=${encodeURIComponent(nextToken)}` : ''}`;
|
|
|
|
|
|
|
+ const q = notableQuery(cfg, { maxResults: LIST_PAGE, ...(nextToken ? { nextToken } : {}) });
|
|
|
const res = await transport('GET', RECORDS_PATH(cfg.baseId, cfg.sheetName) + q, undefined, token);
|
|
const res = await transport('GET', RECORDS_PATH(cfg.baseId, cfg.sheetName) + q, undefined, token);
|
|
|
if (res.statusCode !== 200) throw err(`拉取记录失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_LIST_FAIL');
|
|
if (res.statusCode !== 200) throw err(`拉取记录失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_LIST_FAIL');
|
|
|
const d = res.json || {};
|
|
const d = res.json || {};
|
|
@@ -250,12 +260,12 @@ function createAitableService(ctx) {
|
|
|
else toCreate.push(rec);
|
|
else toCreate.push(rec);
|
|
|
}
|
|
}
|
|
|
for (const part of chunk(toCreate, INSERT_CHUNK)) {
|
|
for (const part of chunk(toCreate, INSERT_CHUNK)) {
|
|
|
- const res = await transport('POST', RECORDS_PATH(cfg.baseId, cfg.sheetName), { records: part }, token);
|
|
|
|
|
|
|
+ const res = await transport('POST', RECORDS_PATH(cfg.baseId, cfg.sheetName) + notableQuery(cfg), { records: part }, token);
|
|
|
if (res.statusCode >= 300) throw err(`批量新增失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_CREATE_FAIL');
|
|
if (res.statusCode >= 300) throw err(`批量新增失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_CREATE_FAIL');
|
|
|
created += part.length;
|
|
created += part.length;
|
|
|
}
|
|
}
|
|
|
for (const part of chunk(toUpdate, UPDATE_CHUNK)) {
|
|
for (const part of chunk(toUpdate, UPDATE_CHUNK)) {
|
|
|
- const res = await transport('PUT', RECORDS_PATH(cfg.baseId, cfg.sheetName), { records: part }, token);
|
|
|
|
|
|
|
+ const res = await transport('PUT', RECORDS_PATH(cfg.baseId, cfg.sheetName) + notableQuery(cfg), { records: part }, token);
|
|
|
if (res.statusCode >= 300) throw err(`批量更新失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_UPDATE_FAIL');
|
|
if (res.statusCode >= 300) throw err(`批量更新失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_UPDATE_FAIL');
|
|
|
updated += part.length;
|
|
updated += part.length;
|
|
|
}
|
|
}
|
|
@@ -306,13 +316,18 @@ function createAitableService(ctx) {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 连通性测试:token + 表可读 */
|
|
|
|
|
|
|
+ /** 连通性测试:token + 操作者身份 + 表可读 */
|
|
|
async function testConnection() {
|
|
async function testConnection() {
|
|
|
const cfg = requireConfig();
|
|
const cfg = requireConfig();
|
|
|
const token = await getToken();
|
|
const token = await getToken();
|
|
|
- const res = await transport('GET', RECORDS_PATH(cfg.baseId, cfg.sheetName) + `?maxResults=1`, undefined, token);
|
|
|
|
|
- if (res.statusCode !== 200) throw err(`表读取失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}`, 'AITABLE_TEST_FAIL');
|
|
|
|
|
- return { ok: true, baseId: cfg.baseId, sheetName: cfg.sheetName };
|
|
|
|
|
|
|
+ const res = await transport('GET', RECORDS_PATH(cfg.baseId, cfg.sheetName) + notableQuery(cfg, { maxResults: 1 }), undefined, token);
|
|
|
|
|
+ if (res.statusCode !== 200) {
|
|
|
|
|
+ const hint = res.statusCode === 403 || res.statusCode === 404
|
|
|
|
|
+ ? '——请确认该 AI 表格已分享给操作者账号(钉钉里把表格协作权限授给对应同事)'
|
|
|
|
|
+ : '';
|
|
|
|
|
+ throw err(`表读取失败 HTTP ${res.statusCode}: ${(res.json && res.json.message) || res.raw}${hint}`, 'AITABLE_TEST_FAIL');
|
|
|
|
|
+ }
|
|
|
|
|
+ return { ok: true, baseId: cfg.baseId, sheetName: cfg.sheetName, operatorId: cfg.operatorId };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function lastPushInfo(entity) {
|
|
function lastPushInfo(entity) {
|