voc-init.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /**
  2. * VOC 鉴权 + 计费 建表云函数
  3. *
  4. * 数据库:NovaCloud (ncloudmaster)
  5. *
  6. * 支持 action:
  7. * - init : 建表 + 建索引(幂等,可重复执行)
  8. * - addKey : 添加 API Key
  9. * - listKeys : 列出所有 API Key
  10. * - disableKey : 禁用某个 API Key
  11. * - enableKey : 启用某个 API Key
  12. * - stats : 查询用量统计
  13. * - debug : 调试模式,返回接收到的所有参数
  14. */
  15. async function handler(request, response) {
  16. console.log('🔑 [VOC-Init] 云函数被调用...');
  17. try {
  18. // ==================== 0. 建表(幂等) ====================
  19. // --- VocApiKey: API Key 鉴权表 ---
  20. await Psql.query(`
  21. CREATE TABLE IF NOT EXISTS "VocApiKey" (
  22. "objectId" VARCHAR(10) PRIMARY KEY,
  23. "key" VARCHAR(255) NOT NULL UNIQUE,
  24. "clientName" VARCHAR(255) DEFAULT 'unknown',
  25. "isActive" BOOLEAN DEFAULT true,
  26. "dailyQuota" INTEGER DEFAULT 10000,
  27. "rateLimit" INTEGER DEFAULT 60,
  28. "permissions" JSONB DEFAULT '["*"]'::jsonb,
  29. "description" TEXT DEFAULT '',
  30. "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  31. "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
  32. )
  33. `);
  34. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_apikey_key ON "VocApiKey" ("key")`);
  35. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_apikey_client ON "VocApiKey" ("clientName")`);
  36. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_apikey_active ON "VocApiKey" ("isActive")`);
  37. // --- VocUsageLog: 调用计费日志表 ---
  38. await Psql.query(`
  39. CREATE TABLE IF NOT EXISTS "VocUsageLog" (
  40. "objectId" VARCHAR(10) PRIMARY KEY,
  41. "apiKey" VARCHAR(255) DEFAULT 'anonymous',
  42. "clientName" VARCHAR(255) DEFAULT 'unknown',
  43. "skillName" VARCHAR(100) DEFAULT '',
  44. "path" VARCHAR(500) DEFAULT '',
  45. "method" VARCHAR(10) DEFAULT 'POST',
  46. "requestParams" JSONB,
  47. "responseStatus" INTEGER DEFAULT 200,
  48. "responseTime" INTEGER DEFAULT 0,
  49. "dataCount" INTEGER DEFAULT 0,
  50. "cost" INTEGER DEFAULT 1,
  51. "ip" VARCHAR(50) DEFAULT '',
  52. "userAgent" VARCHAR(500) DEFAULT '',
  53. "date" VARCHAR(10) DEFAULT '',
  54. "success" BOOLEAN DEFAULT true,
  55. "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  56. "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
  57. )
  58. `);
  59. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_apikey ON "VocUsageLog" ("apiKey")`);
  60. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_client ON "VocUsageLog" ("clientName")`);
  61. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_skill ON "VocUsageLog" ("skillName")`);
  62. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_date ON "VocUsageLog" ("date")`);
  63. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_success ON "VocUsageLog" ("success")`);
  64. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_log_created ON "VocUsageLog" ("createdAt")`);
  65. // --- VocDailyStat: 按天聚合统计表(可选,加速统计查询) ---
  66. await Psql.query(`
  67. CREATE TABLE IF NOT EXISTS "VocDailyStat" (
  68. "objectId" VARCHAR(10) PRIMARY KEY,
  69. "apiKey" VARCHAR(255) NOT NULL,
  70. "clientName" VARCHAR(255) DEFAULT 'unknown',
  71. "skillName" VARCHAR(100) DEFAULT '',
  72. "date" VARCHAR(10) NOT NULL,
  73. "totalCalls" INTEGER DEFAULT 0,
  74. "totalCost" INTEGER DEFAULT 0,
  75. "totalData" INTEGER DEFAULT 0,
  76. "successCount" INTEGER DEFAULT 0,
  77. "failCount" INTEGER DEFAULT 0,
  78. "avgTime" FLOAT DEFAULT 0,
  79. "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  80. "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  81. UNIQUE ("apiKey", "skillName", "date")
  82. )
  83. `);
  84. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_stat_date ON "VocDailyStat" ("date")`);
  85. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_voc_stat_client ON "VocDailyStat" ("clientName")`);
  86. console.log('✅ [VOC-Init] 建表完成');
  87. // ==================== 解析参数 ====================
  88. let action = '';
  89. const sources = [request.params, request.body, request];
  90. const getParam = (name) => {
  91. for (const src of sources) {
  92. if (src && typeof src === 'object' && src[name] !== undefined) return src[name];
  93. }
  94. return '';
  95. };
  96. action = getParam('action');
  97. console.log(`📌 [VOC-Init] action=${action}`);
  98. // ==================== 调试模式 ====================
  99. if (action === 'debug') {
  100. response.json({
  101. code: 200, success: true,
  102. debug: {
  103. requestKeys: Object.keys(request || {}),
  104. paramsKeys: Object.keys(request.params || {}),
  105. bodyKeys: Object.keys(request.body || {}),
  106. params: request.params,
  107. body: request.body
  108. }
  109. });
  110. return;
  111. }
  112. // ==================== init: 仅建表,返回表结构 ====================
  113. if (!action || action === 'init') {
  114. const keyCount = await Psql.query(`SELECT COUNT(*) as total FROM "VocApiKey"`);
  115. const logCount = await Psql.query(`SELECT COUNT(*) as total FROM "VocUsageLog"`);
  116. response.json({
  117. code: 200,
  118. success: true,
  119. message: 'VOC 表初始化完成',
  120. tables: {
  121. VocApiKey: {
  122. count: parseInt(keyCount[0]?.total || '0'),
  123. columns: ['objectId', 'key', 'clientName', 'isActive', 'dailyQuota', 'rateLimit', 'permissions', 'description', 'createdAt', 'updatedAt']
  124. },
  125. VocUsageLog: {
  126. count: parseInt(logCount[0]?.total || '0'),
  127. columns: ['objectId', 'apiKey', 'clientName', 'skillName', 'path', 'method', 'requestParams', 'responseStatus', 'responseTime', 'dataCount', 'cost', 'ip', 'userAgent', 'date', 'success', 'createdAt', 'updatedAt']
  128. },
  129. VocDailyStat: {
  130. columns: ['objectId', 'apiKey', 'clientName', 'skillName', 'date', 'totalCalls', 'totalCost', 'totalData', 'successCount', 'failCount', 'avgTime', 'createdAt', 'updatedAt']
  131. }
  132. }
  133. });
  134. return;
  135. }
  136. // ==================== addKey: 添加 API Key ====================
  137. if (action === 'addKey') {
  138. const clientName = getParam('clientName') || 'default';
  139. const dailyQuota = parseInt(getParam('dailyQuota')) || 10000;
  140. const rateLimit = parseInt(getParam('rateLimit')) || 60;
  141. const description = getParam('description') || '';
  142. let permissions = getParam('permissions');
  143. if (!permissions) permissions = ['*'];
  144. if (typeof permissions === 'string') {
  145. try { permissions = JSON.parse(permissions); } catch(e) { permissions = ['*']; }
  146. }
  147. // 生成 API Key: voc_ + 32位随机字符
  148. const keyChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  149. let apiKey = 'voc_';
  150. for (let i = 0; i < 32; i++) {
  151. apiKey += keyChars.charAt(Math.floor(Math.random() * keyChars.length));
  152. }
  153. const objectId = generateId();
  154. await Psql.query(
  155. `INSERT INTO "VocApiKey" ("objectId", "key", "clientName", "isActive", "dailyQuota", "rateLimit", "permissions", "description")
  156. VALUES ($1, $2, $3, true, $4, $5, $6::jsonb, $7)`,
  157. [objectId, apiKey, clientName, dailyQuota, rateLimit, JSON.stringify(permissions), description]
  158. );
  159. console.log(`✅ [VOC-Init] API Key 已创建: ${clientName} -> ${apiKey}`);
  160. response.json({
  161. code: 200,
  162. success: true,
  163. message: 'API Key 已创建',
  164. data: {
  165. objectId,
  166. key: apiKey,
  167. clientName,
  168. dailyQuota,
  169. rateLimit,
  170. permissions,
  171. description
  172. }
  173. });
  174. return;
  175. }
  176. // ==================== listKeys: 列出所有 API Key ====================
  177. if (action === 'listKeys') {
  178. const results = await Psql.query(
  179. `SELECT * FROM "VocApiKey" ORDER BY "createdAt" DESC LIMIT 100`
  180. );
  181. response.json({
  182. code: 200,
  183. success: true,
  184. total: results.length,
  185. data: results.map(r => ({
  186. objectId: r.objectId,
  187. key: r.key,
  188. clientName: r.clientName,
  189. isActive: r.isActive,
  190. dailyQuota: r.dailyQuota,
  191. rateLimit: r.rateLimit,
  192. permissions: r.permissions,
  193. description: r.description,
  194. createdAt: r.createdAt
  195. }))
  196. });
  197. return;
  198. }
  199. // ==================== disableKey / enableKey ====================
  200. if (action === 'disableKey' || action === 'enableKey') {
  201. const targetKey = getParam('targetKey') || getParam('key');
  202. if (!targetKey) {
  203. response.json({ code: 400, success: false, error: '缺少 targetKey 或 key' });
  204. return;
  205. }
  206. const isActive = action === 'enableKey';
  207. await Psql.query(
  208. `UPDATE "VocApiKey" SET "isActive" = $1, "updatedAt" = NOW() WHERE "key" = $2`,
  209. [isActive, targetKey]
  210. );
  211. console.log(`🔧 [VOC-Init] API Key ${isActive ? '启用' : '禁用'}: ${targetKey}`);
  212. response.json({
  213. code: 200,
  214. success: true,
  215. message: `API Key 已${isActive ? '启用' : '禁用'}`
  216. });
  217. return;
  218. }
  219. // ==================== stats: 用量统计 ====================
  220. if (action === 'stats') {
  221. const filterApiKey = getParam('apiKey');
  222. const filterDate = getParam('date') || new Date().toISOString().slice(0, 10);
  223. const filterStartDate = getParam('startDate') || filterDate;
  224. const filterEndDate = getParam('endDate') || filterDate;
  225. let conditions = [];
  226. let params = [];
  227. let paramIdx = 1;
  228. if (filterApiKey) {
  229. conditions.push(`"apiKey" = $${paramIdx++}`);
  230. params.push(filterApiKey);
  231. }
  232. conditions.push(`"date" >= $${paramIdx++}`);
  233. params.push(filterStartDate);
  234. conditions.push(`"date" <= $${paramIdx++}`);
  235. params.push(filterEndDate);
  236. const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
  237. // 总体统计
  238. const summary = await Psql.query(
  239. `SELECT
  240. COUNT(*) as "totalCalls",
  241. COALESCE(SUM("cost"), 0) as "totalCost",
  242. COALESCE(SUM("dataCount"), 0) as "totalData",
  243. COALESCE(AVG("responseTime"), 0) as "avgTime",
  244. COUNT(CASE WHEN "success" = true THEN 1 END) as "successCount",
  245. COUNT(CASE WHEN "success" = false THEN 1 END) as "failCount"
  246. FROM "VocUsageLog" ${whereClause}`,
  247. params
  248. );
  249. // 按技能分组
  250. const bySkill = await Psql.query(
  251. `SELECT
  252. "skillName",
  253. COUNT(*) as calls,
  254. COALESCE(SUM("cost"), 0) as cost,
  255. COALESCE(SUM("dataCount"), 0) as data,
  256. COALESCE(AVG("responseTime"), 0) as "avgTime"
  257. FROM "VocUsageLog" ${whereClause}
  258. GROUP BY "skillName"
  259. ORDER BY calls DESC`,
  260. params
  261. );
  262. // 最近调用
  263. const recent = await Psql.query(
  264. `SELECT * FROM "VocUsageLog" ${whereClause} ORDER BY "createdAt" DESC LIMIT 20`,
  265. params
  266. );
  267. response.json({
  268. code: 200,
  269. success: true,
  270. period: { start: filterStartDate, end: filterEndDate },
  271. summary: summary[0] || {},
  272. bySkill: bySkill.map(r => ({
  273. skillName: r.skillName,
  274. calls: parseInt(r.calls),
  275. cost: parseInt(r.cost),
  276. data: parseInt(r.data),
  277. avgTime: Math.round(parseFloat(r.avgTime))
  278. })),
  279. recent: recent.map(r => ({
  280. objectId: r.objectId,
  281. skillName: r.skillName,
  282. clientName: r.clientName,
  283. cost: r.cost,
  284. dataCount: r.dataCount,
  285. responseTime: r.responseTime,
  286. success: r.success,
  287. date: r.date,
  288. createdAt: r.createdAt
  289. }))
  290. });
  291. return;
  292. }
  293. // ==================== 未知 action ====================
  294. response.json({
  295. code: 400,
  296. success: false,
  297. error: `未知 action: ${action}`,
  298. availableActions: ['init', 'addKey', 'listKeys', 'disableKey', 'enableKey', 'stats', 'debug']
  299. });
  300. } catch (error) {
  301. console.error('❌ [VOC-Init] 处理失败:', error.message);
  302. response.json({ code: 500, success: false, error: error.message });
  303. }
  304. }
  305. function generateId() {
  306. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  307. let result = '';
  308. for (let i = 0; i < 10; i++) {
  309. result += chars.charAt(Math.floor(Math.random() * chars.length));
  310. }
  311. return result;
  312. }