| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- 'use strict';
- // 钉钉 AI 表格「操作者发现」脚本
- // ----------------------------------------------------------------------------
- // 背景:AI 表格(notable)所有接口必须在 query 上带 operatorId=操作人 unionId。
- // 企业应用的 token 推不出用户身份,unionId 要靠通讯录接口转换。
- // 前置:在钉钉开放平台给应用开通这几个权限点(第一次跑报错会给出精确的申请链接,
- // 点链接 → 开通 → 重跑本脚本即可):
- // · 通讯录部门信息读权限 qyapi_get_department_list
- // · 通讯录部门成员读权限 qyapi_get_department_member
- // · 通讯录个人信息读权限(v2/user/get 取 unionId 用)
- // 用法:node scripts/aitable-discover.js
- // 输出:每个成员的 unionId + 谁能读 config.json 里配置的 Base(★ 命中者即为操作者),
- // 把命中的 unionId 填入 config.json → aitable.operatorId。
- const fs = require('fs');
- const path = require('path');
- const https = require('https');
- const ROOT = path.join(__dirname, '..');
- const cfg = (() => {
- const raw = JSON.parse(fs.readFileSync(path.join(ROOT, 'config.json'), 'utf8'));
- const a = raw.aitable || {};
- if (!a.appKey || !a.appSecret || !a.baseId) {
- console.error('✗ config.json 缺 aitable.appKey / appSecret / baseId');
- process.exit(1);
- }
- return a;
- })();
- function call(url, { method = 'GET', headers = {}, body } = {}) {
- return new Promise((resolve, reject) => {
- const data = body ? JSON.stringify(body) : null;
- const h = { ...headers };
- if (data) { h['Content-Type'] = 'application/json'; h['Content-Length'] = Buffer.byteLength(data); }
- const req = https.request(url, { method, headers: h, timeout: 20000 }, (res) => {
- let buf = '';
- res.on('data', (c) => { buf += c; });
- res.on('end', () => {
- let json = null;
- try { json = JSON.parse(buf); } catch { /* 保留原文 */ }
- resolve({ status: res.statusCode, json });
- });
- });
- req.on('error', reject);
- req.on('timeout', () => req.destroy(new Error('请求超时')));
- if (data) req.write(data);
- req.end();
- });
- }
- function oldApi(pathQ, token, body) {
- return call(`https://oapi.dingtalk.com${pathQ}${pathQ.includes('?') ? '&' : '?'}access_token=${token}`, { method: 'POST', body });
- }
- function permissionHint(json) {
- const m = json && typeof json.sub_msg === 'string' ? json.sub_msg : '';
- const link = /https:\S+/.exec(m);
- return link ? `\n → 点这个链接开通权限后重跑:${link[0]}` : '';
- }
- (async () => {
- console.log('⊙ 前置权限:请先在钉钉开放平台给应用开通以下三个权限点(点击链接 → 申请开通):');
- for (const scope of ['qyapi_get_department_list', 'qyapi_get_department_member', 'qyapi_get_member_detail']) {
- console.log(` · ${scope}`);
- console.log(` https://open-dev.dingtalk.com/appscope/apply?content=${encodeURIComponent(cfg.appKey)}%23${scope}`);
- }
- console.log('');
- const t = await call('https://api.dingtalk.com/v1.0/oauth2/accessToken', { method: 'POST', body: { appKey: cfg.appKey, appSecret: cfg.appSecret } });
- if (t.status !== 200 || !t.json.accessToken) { console.error('✗ 获取 token 失败', t.json); process.exit(1); }
- const T = t.json.accessToken;
- console.log('✓ token OK');
- // 1) 部门:根部门 + 子部门
- const depts = [1];
- const r1 = await oldApi('/topapi/v2/department/listsub', T, { dept_id: 1 });
- if (r1.json.errcode !== 0) { console.error(`✗ 拉部门失败 errcode=${r1.json.errcode}${permissionHint(r1.json)}`, r1.json.errmsg || ''); process.exit(1); }
- for (const d of r1.json.result || []) depts.push(d.dept_id);
- console.log(`✓ 部门 ${depts.length} 个`);
- // 2) 成员
- const userIds = [];
- for (const d of depts) {
- const r = await oldApi('/topapi/v2/user/list', T, { dept_id: d, cursor: 0, size: 50 });
- if (r.json.errcode !== 0) { console.error(`✗ 拉成员失败 dept=${d} errcode=${r.json.errcode}${permissionHint(r.json)}`); process.exit(1); }
- for (const u of (r.json.result && r.json.result.list) || []) if (!userIds.includes(u.userid)) userIds.push(u.userid);
- }
- console.log(`✓ 成员 ${userIds.length} 人`);
- // 3) unionId
- const cands = [];
- for (const uid of userIds) {
- const r = await oldApi('/topapi/v2/user/get', T, { userid: uid });
- if (r.json.errcode !== 0) { console.error(`✗ 用户详情失败 ${uid} errcode=${r.json.errcode}${permissionHint(r.json)}`); process.exit(1); }
- const u = r.json.result || {};
- cands.push({ name: u.name, userid: uid, unionid: u.unionid, admin: !!u.admin });
- }
- // 4) 谁能读这个 Base
- let hit = null;
- for (const c of cands) {
- const res = await call(`https://api.dingtalk.com/v1.0/notable/bases/${encodeURIComponent(cfg.baseId)}/sheets?operatorId=${encodeURIComponent(c.unionid)}`,
- { headers: { 'x-acs-dingtalk-access-token': T } });
- const ok = res.status === 200;
- const sheets = ok && Array.isArray(res.json.value) ? res.json.value.length : (ok ? '?' : '');
- console.log(` ${ok ? '★' : '×'} ${c.name}${c.admin ? ' [管理员]' : ''} unionid=${c.unionid} 读Base=${ok}${ok ? ` 数据表数=${sheets}` : ` HTTP ${res.status}`}`);
- if (ok && !hit) hit = c;
- }
- if (hit) {
- console.log(`\n★ 操作者建议:${hit.name} unionid=${hit.unionid}`);
- console.log(` 写入方法:config.json → "aitable" 增加/修改 "operatorId": "${hit.unionid}"`);
- if (hit.admin) console.log(' (该账号是管理员,推荐用管理员身份做系统级推送)');
- } else {
- console.log('\n✗ 没有人能读这个 Base:请确认 baseId 正确、且 AI 表格已分享给上述某个成员');
- }
- })().catch((e) => { console.error('FATAL', e.message); process.exit(1); });
|