feat: 后端 Notion 开发者同步 API + 前端自动同步集成
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
027b126f87
commit
4b8d8908bc
|
|
@ -0,0 +1,173 @@
|
|||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const router = express.Router();
|
||||
|
||||
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
||||
const DEVELOPERS_DB_ID = process.env.DEVELOPERS_DB_ID || '';
|
||||
const NOTION_VERSION = '2022-06-28';
|
||||
const BASE_URL = 'https://api.notion.com/v1';
|
||||
|
||||
const headers = () => ({
|
||||
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': NOTION_VERSION
|
||||
});
|
||||
|
||||
// Validate DEV-XXX format
|
||||
const DEV_ID_RE = /^DEV-\d{3,}$/;
|
||||
|
||||
// Notion database property names (match your Notion schema)
|
||||
const PROP = {
|
||||
DEV_ID: 'DEV编号',
|
||||
DEV_ID_ALT: 'DEV ID',
|
||||
TITLE: '标题',
|
||||
NAME: '姓名',
|
||||
NAME_ALT: '名称',
|
||||
ROLE: '角色',
|
||||
JOB_TITLE: '职位',
|
||||
JOB_TITLE_ALT: '岗位',
|
||||
EMAIL: '邮箱',
|
||||
GITHUB: 'GitHub',
|
||||
STATUS: '状态',
|
||||
};
|
||||
|
||||
// Build Notion filter query for developer lookup by DEV ID
|
||||
function devIdFilter(devId) {
|
||||
return {
|
||||
or: [
|
||||
{ property: PROP.DEV_ID, title: { equals: devId } },
|
||||
{ property: PROP.DEV_ID_ALT, title: { equals: devId } },
|
||||
{ property: PROP.TITLE, title: { equals: devId } }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Health check
|
||||
router.get('/test', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: '开发者路由正常',
|
||||
notion_configured: !!NOTION_TOKEN,
|
||||
db_configured: !!DEVELOPERS_DB_ID
|
||||
});
|
||||
});
|
||||
|
||||
// Extract developer info from a Notion page result
|
||||
function extractDevInfo(page) {
|
||||
const props = page.properties || {};
|
||||
const getText = (prop) => {
|
||||
if (!prop) return '';
|
||||
if (prop.title) return prop.title.map(t => t.plain_text).join('');
|
||||
if (prop.rich_text) return prop.rich_text.map(t => t.plain_text).join('');
|
||||
if (prop.select) return prop.select.name;
|
||||
if (prop.email) return prop.email || '';
|
||||
return '';
|
||||
};
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
devId: getText(props[PROP.DEV_ID] || props[PROP.DEV_ID_ALT] || props[PROP.TITLE]),
|
||||
name: getText(props[PROP.NAME] || props[PROP.NAME_ALT]),
|
||||
role: getText(props[PROP.ROLE]),
|
||||
title: getText(props[PROP.JOB_TITLE] || props[PROP.JOB_TITLE_ALT]),
|
||||
email: getText(props[PROP.EMAIL]),
|
||||
github: getText(props[PROP.GITHUB]),
|
||||
status: getText(props[PROP.STATUS]),
|
||||
notionUrl: page.url || ''
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/v1/developers/:devId — Look up developer by DEV-XXX ID from Notion
|
||||
router.get('/:devId', async (req, res) => {
|
||||
const devId = (req.params.devId || '').trim().toUpperCase();
|
||||
|
||||
if (!DEV_ID_RE.test(devId)) {
|
||||
return res.status(400).json({ error: '开发者编号格式不正确,请使用 DEV-XXX 格式' });
|
||||
}
|
||||
|
||||
if (!NOTION_TOKEN) {
|
||||
return res.status(503).json({ error: 'Notion API 未配置', devId: devId, fallback: true });
|
||||
}
|
||||
|
||||
if (!DEVELOPERS_DB_ID) {
|
||||
return res.status(503).json({ error: '开发者数据库未配置', devId: devId, fallback: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
BASE_URL + '/databases/' + DEVELOPERS_DB_ID + '/query',
|
||||
{ filter: devIdFilter(devId), page_size: 1 },
|
||||
{ headers: headers() }
|
||||
);
|
||||
|
||||
const results = response.data.results || [];
|
||||
if (results.length > 0) {
|
||||
const dev = extractDevInfo(results[0]);
|
||||
return res.json({ found: true, developer: dev });
|
||||
}
|
||||
|
||||
return res.json({ found: false, devId: devId, message: '未在 Notion 中找到该开发者,将使用默认配置' });
|
||||
} catch (err) {
|
||||
const errStatus = err.response ? err.response.status : 500;
|
||||
const detail = err.response ? err.response.data : null;
|
||||
return res.status(errStatus >= 400 && errStatus < 600 ? errStatus : 500).json({
|
||||
error: '查询 Notion 失败: ' + err.message,
|
||||
devId: devId,
|
||||
fallback: true,
|
||||
detail: detail
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/developers — Register a new developer in Notion
|
||||
router.post('/', async (req, res) => {
|
||||
const { devId, name } = req.body || {};
|
||||
|
||||
if (!devId || !DEV_ID_RE.test(devId)) {
|
||||
return res.status(400).json({ error: '开发者编号格式不正确' });
|
||||
}
|
||||
|
||||
if (!NOTION_TOKEN || !DEVELOPERS_DB_ID) {
|
||||
return res.status(503).json({ error: 'Notion 未配置', devId: devId, fallback: true });
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if already exists
|
||||
const checkResp = await axios.post(
|
||||
BASE_URL + '/databases/' + DEVELOPERS_DB_ID + '/query',
|
||||
{ filter: devIdFilter(devId), page_size: 1 },
|
||||
{ headers: headers() }
|
||||
);
|
||||
|
||||
if (checkResp.data.results && checkResp.data.results.length > 0) {
|
||||
return res.json({ created: false, message: '开发者已存在', developer: extractDevInfo(checkResp.data.results[0]) });
|
||||
}
|
||||
|
||||
// Create new developer page
|
||||
const newPage = await axios.post(
|
||||
BASE_URL + '/pages',
|
||||
{
|
||||
parent: { database_id: DEVELOPERS_DB_ID },
|
||||
properties: {
|
||||
[PROP.TITLE]: { title: [{ text: { content: devId } }] },
|
||||
[PROP.NAME]: { rich_text: [{ text: { content: name || devId } }] },
|
||||
[PROP.ROLE]: { select: { name: '开发者' } },
|
||||
[PROP.STATUS]: { select: { name: '活跃' } }
|
||||
}
|
||||
},
|
||||
{ headers: headers() }
|
||||
);
|
||||
|
||||
return res.json({ created: true, message: '开发者已自动注册', developer: extractDevInfo(newPage.data) });
|
||||
} catch (err) {
|
||||
const detail = err.response ? err.response.data : null;
|
||||
return res.status(500).json({
|
||||
error: '注册失败: ' + err.message,
|
||||
devId: devId,
|
||||
fallback: true,
|
||||
detail: detail
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -11,12 +11,14 @@ const notionRoutes = require('./routes/notion');
|
|||
const feishuRoutes = require('./routes/feishu');
|
||||
const routerRoutes = require('./routes/router');
|
||||
const coldstartRoutes = require('./routes/coldstart');
|
||||
const developersRoutes = require('./routes/developers');
|
||||
const hliRoutes = require('../src/routes/hli');
|
||||
|
||||
app.use('/notion', notionRoutes);
|
||||
app.use('/feishu', feishuRoutes);
|
||||
app.use('/router', routerRoutes);
|
||||
app.use('/api/coldstart', coldstartRoutes);
|
||||
app.use('/api/v1/developers', developersRoutes);
|
||||
app.use('/hli', hliRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
|
|
@ -24,7 +26,7 @@ app.get('/', (req, res) => {
|
|||
status: 'ok',
|
||||
message: 'HoloLake 后端服务运行中',
|
||||
version: '0.2.0',
|
||||
routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat', '/api/coldstart', '/hli/test']
|
||||
routes: ['/notion/test', '/feishu/test', '/router/test', '/router/chat', '/api/coldstart', '/api/v1/developers/test', '/hli/test']
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1464,9 +1464,55 @@ function doTeamLogin(){
|
|||
A.mdl = TEAM_API.model;
|
||||
lss('zy_teammode','1');
|
||||
setIdentity(un, '');
|
||||
|
||||
// For custom DEV-XXX IDs, try to sync from Notion (non-blocking)
|
||||
if(sel==='__custom__' && DEV_ID_RE.test(un)){
|
||||
syncDevFromNotion(un);
|
||||
}
|
||||
|
||||
showApp();
|
||||
}
|
||||
|
||||
// Sync developer info from Notion (non-blocking, best-effort)
|
||||
function syncDevFromNotion(devId){
|
||||
var apiBase = (location.hostname==='localhost'||location.hostname==='127.0.0.1')
|
||||
? 'http://localhost:3000' : '';
|
||||
var url = apiBase + '/api/v1/developers/' + encodeURIComponent(devId);
|
||||
|
||||
fetch(url).then(function(r){ return r.json(); }).then(function(data){
|
||||
if(data.found && data.developer){
|
||||
var dev = data.developer;
|
||||
// Update identity with Notion data if available
|
||||
if(dev.name && dev.name !== devId){
|
||||
A.userMeta = {
|
||||
role: 'dev',
|
||||
title: dev.title || dev.role || '开发者',
|
||||
emoji: '💻',
|
||||
badgeCls: 'rb-dev',
|
||||
devId: devId
|
||||
};
|
||||
A.userName = dev.name;
|
||||
lss('zy_uname', A.userName);
|
||||
applyIdentityUI();
|
||||
console.log('[铸渊] Notion 开发者同步成功:', devId, '→', dev.name);
|
||||
}
|
||||
} else {
|
||||
// Developer not found in Notion — try to register (best effort)
|
||||
fetch(apiBase + '/api/v1/developers', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({devId: devId, name: devId})
|
||||
}).then(function(r){ return r.json(); }).then(function(reg){
|
||||
console.log('[铸渊] 开发者自动注册:', reg.created ? '成功' : '已存在', devId);
|
||||
}).catch(function(e){
|
||||
console.log('[铸渊] 开发者注册跳过(后端未就绪):', e.message);
|
||||
});
|
||||
}
|
||||
}).catch(function(e){
|
||||
console.log('[铸渊] Notion 同步跳过(后端未就绪):', e.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// SETTINGS PANEL
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Reference in New Issue