410 lines
13 KiB
JavaScript
410 lines
13 KiB
JavaScript
'use strict';
|
||
/**
|
||
* NAC AI 资产估值问答界面 - 服务端
|
||
* 职责:
|
||
* 1. 静态文件服务(index.html)
|
||
* 2. /api/v4/* → 代理到估值引擎 v4.0(:3003)
|
||
* 3. /api/inference/stream → 代理到推理引擎(:3001),支持 SSE 流式输出
|
||
* 4. /api/v4/xtzh-price → XTZH 实时价格(带本地缓存)
|
||
* 5. /api/inference/ask → 普通问答(非流式)
|
||
*/
|
||
|
||
const express = require('express');
|
||
const http = require('http');
|
||
const path = require('path');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3005;
|
||
|
||
// ============================================================
|
||
// 中间件
|
||
// ============================================================
|
||
app.use(express.json({ limit: '2mb' }));
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
// CORS(内部服务)
|
||
app.use((req, res, next) => {
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
|
||
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
||
next();
|
||
});
|
||
|
||
// ============================================================
|
||
// 配置
|
||
// ============================================================
|
||
const VALUATION_ENGINE = {
|
||
host: '127.0.0.1',
|
||
port: 3003,
|
||
timeout: 30000
|
||
};
|
||
|
||
const INFERENCE_ENGINE = {
|
||
host: '127.0.0.1',
|
||
port: 3001,
|
||
timeout: 60000
|
||
};
|
||
|
||
// XTZH 价格缓存(5分钟)
|
||
let xtzhPriceCache = null;
|
||
let xtzhPriceCacheTime = 0;
|
||
const XTZH_CACHE_TTL = 5 * 60 * 1000;
|
||
|
||
// ============================================================
|
||
// XTZH 实时价格
|
||
// ============================================================
|
||
app.get('/api/v4/xtzh-price', async (req, res) => {
|
||
try {
|
||
// 检查缓存
|
||
if (xtzhPriceCache && Date.now() - xtzhPriceCacheTime < XTZH_CACHE_TTL) {
|
||
return res.json(xtzhPriceCache);
|
||
}
|
||
|
||
// 从估值引擎获取
|
||
const data = await proxyRequest('GET', VALUATION_ENGINE, '/api/v4/xtzh-price', null, 5000);
|
||
xtzhPriceCache = data;
|
||
xtzhPriceCacheTime = Date.now();
|
||
return res.json(data);
|
||
} catch(e) {
|
||
// 降级:返回静态价格
|
||
const fallback = {
|
||
usd: 4.3944,
|
||
source: 'NAC_SDR_MODEL_V4_FALLBACK',
|
||
goldCoverage: 1.25,
|
||
sdrBasket: { USD: 0.5813, EUR: 0.1886, CNY: 0.1042, JPY: 0.0895, GBP: 0.0864 },
|
||
timestamp: new Date().toISOString(),
|
||
note: '估值引擎离线,使用最后已知价格'
|
||
};
|
||
return res.json(fallback);
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 估值引擎代理
|
||
// ============================================================
|
||
app.post('/api/v4/valuate', async (req, res) => {
|
||
try {
|
||
const data = await proxyRequest('POST', VALUATION_ENGINE, '/api/v4/valuate', req.body, VALUATION_ENGINE.timeout);
|
||
return res.json(data);
|
||
} catch(e) {
|
||
console.error('[ValuationProxy] Error:', e.message);
|
||
return res.status(502).json({ error: '估值引擎暂时不可用', detail: e.message });
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 推理引擎 SSE 流式输出
|
||
// ============================================================
|
||
app.post('/api/inference/stream', (req, res) => {
|
||
const body = JSON.stringify(req.body);
|
||
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
|
||
const options = {
|
||
hostname: INFERENCE_ENGINE.host,
|
||
port: INFERENCE_ENGINE.port,
|
||
path: '/api/inference/stream',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Content-Length': Buffer.byteLength(body)
|
||
},
|
||
timeout: INFERENCE_ENGINE.timeout
|
||
};
|
||
|
||
const proxyReq = http.request(options, (proxyRes) => {
|
||
proxyRes.on('data', chunk => {
|
||
if (!res.writableEnded) res.write(chunk);
|
||
});
|
||
proxyRes.on('end', () => {
|
||
if (!res.writableEnded) {
|
||
res.write('data: [DONE]\n\n');
|
||
res.end();
|
||
}
|
||
});
|
||
proxyRes.on('error', (err) => {
|
||
if (!res.writableEnded) {
|
||
res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
|
||
res.end();
|
||
}
|
||
});
|
||
});
|
||
|
||
proxyReq.on('error', (err) => {
|
||
console.error('[InferenceSSE] Proxy error:', err.message);
|
||
if (!res.writableEnded) {
|
||
// 降级:使用本地估值问答
|
||
handleLocalValuationQA(req.body, res);
|
||
}
|
||
});
|
||
|
||
proxyReq.on('timeout', () => {
|
||
proxyReq.destroy();
|
||
if (!res.writableEnded) {
|
||
res.write(`data: ${JSON.stringify({ type: 'error', message: '推理引擎响应超时' })}\n\n`);
|
||
res.end();
|
||
}
|
||
});
|
||
|
||
proxyReq.write(body);
|
||
proxyReq.end();
|
||
|
||
req.on('close', () => {
|
||
if (!proxyReq.destroyed) proxyReq.destroy();
|
||
});
|
||
});
|
||
|
||
// ============================================================
|
||
// 本地估值问答降级处理(推理引擎不可用时)
|
||
// ============================================================
|
||
function handleLocalValuationQA(body, res) {
|
||
const question = body.question || '';
|
||
const jurisdiction = body.jurisdiction || 'HK';
|
||
const assetType = body.assetType || 'real_estate';
|
||
const mode = body.mode || 'valuation';
|
||
const lang = body.language || 'zh';
|
||
|
||
let answer = '';
|
||
|
||
if (lang === 'zh') {
|
||
if (question.includes('XTZH') || question.includes('定价') || question.includes('SDR')) {
|
||
answer = generateXTZHExplanation(jurisdiction, lang);
|
||
} else if (question.includes('上链') || question.includes('TOKEN') || question.includes('流程')) {
|
||
answer = generateOnChainGuide(jurisdiction, lang);
|
||
} else if (question.includes('方法') || question.includes('市场法') || question.includes('收益法')) {
|
||
answer = generateMethodologyExplanation(assetType, lang);
|
||
} else if (question.includes('辖区') || question.includes('对比') || question.includes('比较')) {
|
||
answer = generateJurisdictionComparison(lang);
|
||
} else {
|
||
answer = generateGeneralValuationResponse(question, jurisdiction, assetType, lang);
|
||
}
|
||
} else {
|
||
answer = generateGeneralValuationResponse(question, jurisdiction, assetType, lang);
|
||
}
|
||
|
||
// 流式输出(按行分块,确保 Markdown 表格行完整传输)
|
||
const lines = answer.split('\n');
|
||
let i = 0;
|
||
const interval = setInterval(() => {
|
||
if (i >= lines.length) {
|
||
clearInterval(interval);
|
||
res.write('data: [DONE]\n\n');
|
||
res.end();
|
||
return;
|
||
}
|
||
// 每行加上换行符发送
|
||
const lineContent = lines[i] + (i < lines.length - 1 ? '\n' : '');
|
||
res.write(`data: ${JSON.stringify({ type: 'chunk', content: lineContent })}\n\n`);
|
||
i++;
|
||
}, 40);
|
||
}
|
||
|
||
function generateXTZHExplanation(jurisdiction, lang) {
|
||
return `## XTZH 统一定价机制
|
||
|
||
**XTZH(新资产链稳定权益代币)** 采用SDR锚定篮子动态计算,确保全球资产估值的一致性和稳定性。
|
||
|
||
### SDR五货币篮子权重
|
||
|
||
| 货币 | 权重 | 说明 |
|
||
|------|------|------|
|
||
| 美元 USD | 58.13% | 全球储备主导货币 |
|
||
| 欧元 EUR | 18.86% | 欧洲区域货币 |
|
||
| 人民币 CNY | 10.42% | 新兴市场代表 |
|
||
| 日元 JPY | 8.95% | 亚太货币 |
|
||
| 英镑 GBP | 8.64% | 英联邦货币 |
|
||
|
||
### 当前定价参数
|
||
|
||
- **XTZH/USD 汇率**:$4.3944
|
||
- **黄金覆盖率**:125%(超额储备保障)
|
||
- **定价模型**:NAC_SDR_MODEL_V4
|
||
- **更新频率**:实时(每分钟)
|
||
|
||
### 80%质押规则
|
||
|
||
> 资产估值 × 80% = 可发行权益化代币数量
|
||
|
||
这确保了链上资产的超额抵押,保障代币持有者权益。`;
|
||
}
|
||
|
||
function generateOnChainGuide(jurisdiction, lang) {
|
||
return `## 资产上链完整流程
|
||
|
||
### 第一步:AI估值
|
||
1. 提交资产描述(类型、位置、面积、市值)
|
||
2. AI引擎自动识别GNACS分类
|
||
3. 多方法融合估值(市场法/收益法/成本法)
|
||
4. 输出 **finalXTZH** 和置信度
|
||
|
||
### 第二步:合规审批
|
||
- 辖区合规验证(${jurisdiction}监管要求)
|
||
- KYC/AML核查
|
||
- 资产所有权证明
|
||
- 合规评分 ≥ 80分方可上链
|
||
|
||
### 第三步:TOKEN生成
|
||
- 智能合约部署(Charter语言,NVM虚拟机)
|
||
- GNACS编码注册
|
||
- ACC-20协议代币铸造
|
||
|
||
### 第四步:权证发行
|
||
- 资产权证NFT生成
|
||
- 链上存证(CBPP共识确认)
|
||
- 权证与实物资产绑定
|
||
|
||
### 第五步:代币流通
|
||
- 80%质押规则执行
|
||
- 二级市场流通
|
||
- 持续合规监控
|
||
|
||
> **注意**:完整上链流程通常需要3-7个工作日,具体时间取决于辖区审批速度。`;
|
||
}
|
||
|
||
function generateMethodologyExplanation(assetType, lang) {
|
||
const methods = {
|
||
real_estate: `## 不动产估值方法论
|
||
|
||
### 市场比较法(权重40%)
|
||
- 选取3-5个可比交易案例
|
||
- 调整因素:位置、面积、楼层、装修
|
||
- 适用:住宅、商业地产
|
||
|
||
### 收益资本化法(权重35%)
|
||
- 净营业收入 ÷ 资本化率
|
||
- 资本化率参考:HK 3-4%,SG 3.5-4.5%,AE 6-8%
|
||
- 适用:商业地产、租赁物业
|
||
|
||
### 成本法(权重25%)
|
||
- 土地价值 + 建筑重置成本 - 折旧
|
||
- 适用:特殊用途物业、新建物业
|
||
|
||
### XTZH换算
|
||
> finalUSD ÷ XTZH价格($4.3944) = finalXTZH`,
|
||
financial: `## 金融资产估值方法论
|
||
|
||
### DCF折现现金流法(权重50%)
|
||
- 预测未来现金流
|
||
- 折现率:WACC + 风险溢价
|
||
- 适用:股权、债券、基金
|
||
|
||
### 市场乘数法(权重30%)
|
||
- P/E、P/B、EV/EBITDA
|
||
- 对标可比公司
|
||
|
||
### 净资产法(权重20%)
|
||
- 账面价值调整
|
||
- 适用:清算场景`
|
||
};
|
||
return methods[assetType] || methods.real_estate;
|
||
}
|
||
|
||
function generateJurisdictionComparison(lang) {
|
||
return `## 主要辖区估值对比
|
||
|
||
| 辖区 | 资本化率 | 辖区乘数 | 主要法规 | 合规难度 |
|
||
|------|---------|---------|---------|---------|
|
||
| 🇭🇰 香港 | 3.0-4.0% | 1.05 | HKMA/SFC | ★★★☆☆ |
|
||
| 🇸🇬 新加坡 | 3.5-4.5% | 1.03 | MAS | ★★★☆☆ |
|
||
| 🇦🇪 阿联酋 | 6.0-8.0% | 1.08 | ADGM/DIFC | ★★☆☆☆ |
|
||
| 🇺🇸 美国 | 4.0-6.0% | 0.95 | SEC/FINRA | ★★★★★ |
|
||
| 🇬🇧 英国 | 4.5-5.5% | 0.98 | FCA | ★★★★☆ |
|
||
| 🇯🇵 日本 | 3.0-4.5% | 1.00 | FSA | ★★★★☆ |
|
||
| 🇨🇳 中国 | 4.0-5.0% | 0.90 | CBIRC | ★★★★★ |
|
||
|
||
> **辖区乘数**:对最终XTZH估值的调整系数,反映各辖区市场流动性和监管环境。`;
|
||
}
|
||
|
||
function generateGeneralValuationResponse(question, jurisdiction, assetType, lang) {
|
||
return `## NAC AI 资产估值引擎
|
||
|
||
感谢您的咨询!我是NAC NewAssetChain的AI资产估值专家。
|
||
|
||
**当前配置:**
|
||
- 辖区:${jurisdiction}
|
||
- 资产类型:${assetType}
|
||
- XTZH价格:$4.3944 USD(SDR锚定)
|
||
|
||
**我可以帮您:**
|
||
|
||
1. **资产估值** - 描述您的资产(类型、位置、面积、市值),我将给出XTZH估值
|
||
2. **XTZH定价** - 解释SDR锚定机制和当前价格计算
|
||
3. **上链流程** - 从估值到TOKEN生成的完整流程
|
||
4. **辖区对比** - 不同司法辖区的估值差异
|
||
5. **方法论** - 市场法/收益法/成本法详解
|
||
|
||
请告诉我您想了解的具体问题,或直接描述您的资产信息!`;
|
||
}
|
||
|
||
// ============================================================
|
||
// 工具函数:HTTP代理请求
|
||
// ============================================================
|
||
function proxyRequest(method, target, path, body, timeout) {
|
||
return new Promise((resolve, reject) => {
|
||
const bodyStr = body ? JSON.stringify(body) : null;
|
||
const options = {
|
||
hostname: target.host,
|
||
port: target.port,
|
||
path: path,
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
|
||
},
|
||
timeout: timeout || 10000
|
||
};
|
||
|
||
const req = http.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try {
|
||
resolve(JSON.parse(data));
|
||
} catch(e) {
|
||
reject(new Error('无效的JSON响应: ' + data.substring(0, 100)));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
|
||
|
||
if (bodyStr) req.write(bodyStr);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// 静态文件服务(主页)
|
||
// ============================================================
|
||
app.get('/', (req, res) => {
|
||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||
});
|
||
|
||
// 健康检查
|
||
app.get('/health', (req, res) => {
|
||
res.json({
|
||
status: 'ok',
|
||
service: 'nac-valuation-ui',
|
||
version: '1.0.0',
|
||
timestamp: new Date().toISOString(),
|
||
engines: {
|
||
valuation: `${VALUATION_ENGINE.host}:${VALUATION_ENGINE.port}`,
|
||
inference: `${INFERENCE_ENGINE.host}:${INFERENCE_ENGINE.port}`
|
||
}
|
||
});
|
||
});
|
||
|
||
// ============================================================
|
||
// 启动
|
||
// ============================================================
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`[NAC Valuation UI] 服务启动 → http://0.0.0.0:${PORT}`);
|
||
console.log(`[NAC Valuation UI] 估值引擎 → ${VALUATION_ENGINE.host}:${VALUATION_ENGINE.port}`);
|
||
console.log(`[NAC Valuation UI] 推理引擎 → ${INFERENCE_ENGINE.host}:${INFERENCE_ENGINE.port}`);
|
||
});
|