server.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. const http = require('http');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { URL } = require('url');
  5. const { outputsRoot } = require('../core/output-paths');
  6. const {
  7. qiweiSyncExternalGroups,
  8. qiweiListExternalGroups,
  9. qiweiAnalyzeGroupMembers,
  10. qiweiConfirmExternalGroup,
  11. qiweiAddExternalGroup,
  12. qiweiConfigureGroupKeywords,
  13. qiweiRejectExternalGroup,
  14. qiweiSyncGroupMessages
  15. } = require('../tools/qiwei-group-management-run');
  16. const {
  17. qiweiLoginStatus,
  18. qiweiLoginStart,
  19. qiweiLoginCheck,
  20. qiweiLoginVerify
  21. } = require('../tools/qiwei-login-run');
  22. const { qiweiSubscriptionStatus } = require('../tools/qiwei-subscription-run');
  23. const { createAccountConnectionMonitor } = require('../core/account-connection-monitor');
  24. const {
  25. qiweiBatchAddFriends,
  26. qiweiCheckFriendStatus,
  27. qiweiGetCustomerProfile,
  28. qiweiAutoCreateGroup,
  29. qiweiUpdateCustomerInfo
  30. } = require('../tools/qiwei-customer-ops-run');
  31. const {
  32. qiweiUpdateCustomerPortrait,
  33. qiweiSaveCustomerPortrait,
  34. qiweiBatchUpdateCustomerPortrait,
  35. qiweiExportCustomerPortraits,
  36. qiweiListCustomerTags,
  37. qiweiListAllTags,
  38. qiweiAddCustomerTags,
  39. qiweiRemoveCustomerTags,
  40. qiweiSyncPersonalLabels,
  41. qiweiCreatePersonalLabel,
  42. qiweiUpdatePersonalLabel,
  43. qiweiDeletePersonalLabel,
  44. qiweiApplyPersonalLabels
  45. } = require('../tools/qiwei-portrait-tags-run');
  46. const {
  47. qiweiPreviewTransferPackage,
  48. qiweiExecuteTransfer
  49. } = require('../tools/qiwei-customer-transfer-run');
  50. const {
  51. getStateSection,
  52. rebuildStateFromOutputs
  53. } = require('../core/dashboard-state');
  54. const {
  55. switchActiveAccount,
  56. getAgentStatus,
  57. getConversations,
  58. syncConversations,
  59. changeGlobalMode,
  60. changeConversationMode,
  61. approveReply,
  62. approveDraft,
  63. rejectDraft,
  64. regenerateDraft,
  65. generateLatestDraft,
  66. manualSend,
  67. updateCustomerTask,
  68. syncCustomerTaskToOfficialTodo,
  69. updateCustomerAlert,
  70. getAudit,
  71. startListener,
  72. stopListener
  73. } = require('./agent-service');
  74. const {
  75. listKnowledgeTree,
  76. readKnowledgeFile,
  77. listProperties,
  78. getProperty,
  79. listSkillRegistry,
  80. getSkillDetail
  81. } = require('./workspace-library-service');
  82. const {
  83. getMeetingKnowledgeHub,
  84. syncMeetingKnowledge,
  85. analyzeMeetingKnowledge,
  86. getMeetingKnowledge
  87. } = require('./meeting-knowledge-service');
  88. const {
  89. getDocumentKnowledgeHub,
  90. importDocumentKnowledge,
  91. createDocumentKnowledge,
  92. analyzeDocumentKnowledge,
  93. refreshDocumentKnowledge,
  94. getDocumentKnowledge,
  95. getTodoKnowledgeHub,
  96. searchTodoUsers,
  97. syncTodoKnowledge,
  98. refreshTodoKnowledgeDetails,
  99. createTodoKnowledge,
  100. completeTodoKnowledge
  101. } = require('./official-office-knowledge-service');
  102. const {
  103. getUnifiedTaskHub,
  104. createUnifiedLocalTask,
  105. updateUnifiedTask
  106. } = require('./unified-task-service');
  107. const {
  108. getCustomerMasterHub,
  109. updateCustomerMaster,
  110. updateCustomerRecommendationFeedback
  111. } = require('./customer-master-service');
  112. const DASHBOARD_PORT = process.env.QIWEI_DASHBOARD_PORT || 4320;
  113. const STATIC_DIR = path.join(__dirname);
  114. const TMP_DIR = path.join(outputsRoot(), 'tmp');
  115. const jobs = new Map();
  116. const accountConnectionMonitor = createAccountConnectionMonitor({
  117. checkStatus: () => qiweiLoginStatus({}),
  118. recoverLogin: () => qiweiLoginCheck({ manual: true, persistConfig: false })
  119. });
  120. accountConnectionMonitor.start();
  121. function ensureTmpDir() {
  122. fs.mkdirSync(TMP_DIR, { recursive: true });
  123. }
  124. function readBody(req) {
  125. return new Promise((resolve, reject) => {
  126. const chunks = [];
  127. req.on('data', chunk => chunks.push(chunk));
  128. req.on('end', () => {
  129. try {
  130. resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
  131. } catch {
  132. resolve({});
  133. }
  134. });
  135. req.on('error', reject);
  136. });
  137. }
  138. function json(res, status, body) {
  139. res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  140. res.end(JSON.stringify(body));
  141. }
  142. function createJob(runFn) {
  143. const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
  144. const job = { id, status: 'running', progress: 0, result: null, error: null, startedAt: Date.now() };
  145. jobs.set(id, job);
  146. runFn()
  147. .then(r => { job.status = 'done'; job.progress = 100; job.result = r; })
  148. .catch(e => { job.status = 'error'; job.error = e.message || String(e); });
  149. return id;
  150. }
  151. function getJob(id) {
  152. const job = jobs.get(id);
  153. if (!job) return null;
  154. if (job.status === 'running') {
  155. const elapsed = Date.now() - job.startedAt;
  156. job.progress = Math.min(95, Math.floor((elapsed / 30000) * 100));
  157. }
  158. return job;
  159. }
  160. function serveStatic(req, res, filePath, contentType) {
  161. fs.readFile(filePath, (err, data) => {
  162. if (err) {
  163. res.writeHead(404);
  164. res.end('not found');
  165. return;
  166. }
  167. res.writeHead(200, { 'Content-Type': contentType });
  168. res.end(data);
  169. });
  170. }
  171. async function handleUpload(req) {
  172. const body = await readBody(req);
  173. const base64 = String(body.data || '');
  174. const name = String(body.name || `upload-${Date.now()}`).replace(/[\\/]/g, '_');
  175. if (!base64) throw new Error('缺少文件数据');
  176. ensureTmpDir();
  177. const buffer = Buffer.from(base64, 'base64');
  178. const filePath = path.join(TMP_DIR, name);
  179. fs.writeFileSync(filePath, buffer);
  180. return { path: filePath };
  181. }
  182. function handleOutputsDownload(req, res, query) {
  183. const requested = String(query.path || '');
  184. if (!requested) {
  185. json(res, 400, { status: 'error', message: '缺少 path 参数' });
  186. return;
  187. }
  188. const resolved = path.resolve(requested);
  189. const root = path.resolve(outputsRoot());
  190. if (!resolved.startsWith(root)) {
  191. json(res, 403, { status: 'error', message: '禁止访问 outputs 目录之外的文件' });
  192. return;
  193. }
  194. if (!fs.existsSync(resolved)) {
  195. json(res, 404, { status: 'error', message: '文件不存在' });
  196. return;
  197. }
  198. const data = fs.readFileSync(resolved);
  199. const ext = path.extname(resolved).toLowerCase();
  200. const contentType = {
  201. '.json': 'application/json',
  202. '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  203. '.xls': 'application/vnd.ms-excel',
  204. '.png': 'image/png',
  205. '.jpg': 'image/jpeg',
  206. '.jpeg': 'image/jpeg',
  207. '.mp3': 'audio/mpeg',
  208. '.mp4': 'video/mp4'
  209. }[ext] || 'application/octet-stream';
  210. res.writeHead(200, {
  211. 'Content-Type': contentType,
  212. 'Content-Disposition': `attachment; filename="${path.basename(resolved)}"`
  213. });
  214. res.end(data);
  215. }
  216. function getContentType(filePath) {
  217. const ext = path.extname(filePath).toLowerCase();
  218. const map = {
  219. '.html': 'text/html; charset=utf-8',
  220. '.css': 'text/css; charset=utf-8',
  221. '.js': 'application/javascript; charset=utf-8',
  222. '.png': 'image/png',
  223. '.jpg': 'image/jpeg',
  224. '.jpeg': 'image/jpeg',
  225. '.svg': 'image/svg+xml'
  226. };
  227. return map[ext] || 'application/octet-stream';
  228. }
  229. async function combinedStatus() {
  230. const [login, subscription] = await Promise.all([
  231. accountConnectionMonitor.getStatus(),
  232. qiweiSubscriptionStatus({})
  233. ]);
  234. return {
  235. status: 'ok',
  236. summary: {
  237. authConfigured: login.status !== 'needs_auth',
  238. online: !!login.summary?.online,
  239. subscribed: !!subscription.summary?.subscribed
  240. },
  241. data: { login, subscription }
  242. };
  243. }
  244. async function handleRequest(req, res) {
  245. const url = new URL(req.url, 'http://localhost');
  246. const pathname = url.pathname;
  247. try {
  248. if (pathname === '/' || pathname === '/index.html') {
  249. serveStatic(req, res, path.join(STATIC_DIR, 'index.html'), 'text/html; charset=utf-8');
  250. return;
  251. }
  252. if (pathname.startsWith('/dashboard/')) {
  253. const fileName = pathname.slice('/dashboard/'.length).replace(/\.{2,}/g, '');
  254. const filePath = path.join(STATIC_DIR, fileName);
  255. if (!filePath.startsWith(STATIC_DIR)) {
  256. json(res, 403, { status: 'error', message: '禁止访问' });
  257. return;
  258. }
  259. serveStatic(req, res, filePath, getContentType(filePath));
  260. return;
  261. }
  262. if (pathname === '/api/health') {
  263. json(res, 200, { status: 'ok' });
  264. return;
  265. }
  266. if (pathname === '/api/status' && req.method === 'GET') {
  267. json(res, 200, await combinedStatus());
  268. return;
  269. }
  270. if (pathname === '/api/skills' && req.method === 'GET') {
  271. json(res, 200, listSkillRegistry());
  272. return;
  273. }
  274. if (pathname === '/api/knowledge/tree' && req.method === 'GET') {
  275. json(res, 200, listKnowledgeTree());
  276. return;
  277. }
  278. if (pathname === '/api/knowledge/file' && req.method === 'GET') {
  279. json(res, 200, readKnowledgeFile(url.searchParams.get('id')));
  280. return;
  281. }
  282. if (pathname === '/api/knowledge/meetings' && req.method === 'GET') {
  283. json(res, 200, await getMeetingKnowledgeHub());
  284. return;
  285. }
  286. if (pathname === '/api/knowledge/meetings/sync' && req.method === 'POST') {
  287. json(res, 200, await syncMeetingKnowledge(await readBody(req)));
  288. return;
  289. }
  290. const meetingAnalyzeRoute = pathname.match(/^\/api\/knowledge\/meetings\/([^/]+)\/analyze$/);
  291. if (meetingAnalyzeRoute && req.method === 'POST') {
  292. json(res, 200, await analyzeMeetingKnowledge(decodeURIComponent(meetingAnalyzeRoute[1])));
  293. return;
  294. }
  295. const meetingDetailRoute = pathname.match(/^\/api\/knowledge\/meetings\/([^/]+)$/);
  296. if (meetingDetailRoute && req.method === 'GET') {
  297. json(res, 200, getMeetingKnowledge(decodeURIComponent(meetingDetailRoute[1])));
  298. return;
  299. }
  300. if (pathname === '/api/knowledge/docs' && req.method === 'GET') {
  301. json(res, 200, await getDocumentKnowledgeHub());
  302. return;
  303. }
  304. if (pathname === '/api/knowledge/docs/import' && req.method === 'POST') {
  305. json(res, 200, await importDocumentKnowledge(await readBody(req)));
  306. return;
  307. }
  308. if (pathname === '/api/knowledge/docs/create' && req.method === 'POST') {
  309. json(res, 200, await createDocumentKnowledge(await readBody(req)));
  310. return;
  311. }
  312. const documentAnalyzeRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)\/analyze$/);
  313. if (documentAnalyzeRoute && req.method === 'POST') {
  314. json(res, 200, await analyzeDocumentKnowledge(decodeURIComponent(documentAnalyzeRoute[1])));
  315. return;
  316. }
  317. const documentRefreshRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)\/refresh$/);
  318. if (documentRefreshRoute && req.method === 'POST') {
  319. json(res, 200, await refreshDocumentKnowledge(decodeURIComponent(documentRefreshRoute[1])));
  320. return;
  321. }
  322. const documentDetailRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)$/);
  323. if (documentDetailRoute && req.method === 'GET') {
  324. json(res, 200, getDocumentKnowledge(decodeURIComponent(documentDetailRoute[1])));
  325. return;
  326. }
  327. if (pathname === '/api/knowledge/todos' && req.method === 'GET') {
  328. json(res, 200, await getTodoKnowledgeHub());
  329. return;
  330. }
  331. if (pathname === '/api/knowledge/tasks' && req.method === 'GET') {
  332. json(res, 200, getUnifiedTaskHub());
  333. return;
  334. }
  335. if (pathname === '/api/knowledge/tasks/create' && req.method === 'POST') {
  336. json(res, 200, createUnifiedLocalTask(await readBody(req)));
  337. return;
  338. }
  339. if (pathname === '/api/knowledge/tasks/update' && req.method === 'POST') {
  340. json(res, 200, await updateUnifiedTask(await readBody(req)));
  341. return;
  342. }
  343. if (pathname === '/api/knowledge/todos/search-users' && req.method === 'POST') {
  344. json(res, 200, await searchTodoUsers(await readBody(req)));
  345. return;
  346. }
  347. if (pathname === '/api/knowledge/todos/sync' && req.method === 'POST') {
  348. json(res, 200, await syncTodoKnowledge(await readBody(req)));
  349. return;
  350. }
  351. if (pathname === '/api/knowledge/todos/details' && req.method === 'POST') {
  352. json(res, 200, await refreshTodoKnowledgeDetails(await readBody(req)));
  353. return;
  354. }
  355. if (pathname === '/api/knowledge/todos/create' && req.method === 'POST') {
  356. json(res, 200, await createTodoKnowledge(await readBody(req)));
  357. return;
  358. }
  359. const todoCompleteRoute = pathname.match(/^\/api\/knowledge\/todos\/([^/]+)\/complete$/);
  360. if (todoCompleteRoute && req.method === 'POST') {
  361. json(res, 200, await completeTodoKnowledge(decodeURIComponent(todoCompleteRoute[1])));
  362. return;
  363. }
  364. if (pathname === '/api/knowledge/properties' && req.method === 'GET') {
  365. json(res, 200, listProperties(Object.fromEntries(url.searchParams.entries())));
  366. return;
  367. }
  368. const propertyRoute = pathname.match(/^\/api\/knowledge\/properties\/([^/]+)$/);
  369. if (propertyRoute && req.method === 'GET') {
  370. json(res, 200, getProperty(decodeURIComponent(propertyRoute[1])));
  371. return;
  372. }
  373. const skillRoute = pathname.match(/^\/api\/skills\/(.+)$/);
  374. if (skillRoute && req.method === 'GET') {
  375. json(res, 200, getSkillDetail(decodeURIComponent(skillRoute[1])));
  376. return;
  377. }
  378. if (pathname === '/api/agent/status' && req.method === 'GET') {
  379. json(res, 200, await getAgentStatus());
  380. return;
  381. }
  382. if (pathname === '/api/accounts/switch' && req.method === 'POST') {
  383. json(res, 200, await switchActiveAccount(await readBody(req)));
  384. return;
  385. }
  386. if (pathname === '/api/agent/conversations' && req.method === 'GET') {
  387. json(res, 200, getConversations());
  388. return;
  389. }
  390. if (pathname === '/api/agent/conversations/sync' && req.method === 'POST') {
  391. json(res, 200, await syncConversations());
  392. return;
  393. }
  394. if (pathname === '/api/agent/audit' && req.method === 'GET') {
  395. json(res, 200, getAudit(url.searchParams.get('limit')));
  396. return;
  397. }
  398. if (pathname === '/api/agent/mode' && req.method === 'POST') {
  399. const body = await readBody(req);
  400. json(res, 200, changeGlobalMode(body.mode));
  401. return;
  402. }
  403. if (pathname === '/api/agent/listener/start' && req.method === 'POST') {
  404. json(res, 200, await startListener());
  405. return;
  406. }
  407. if (pathname === '/api/agent/listener/stop' && req.method === 'POST') {
  408. json(res, 200, stopListener());
  409. return;
  410. }
  411. const customerTaskSyncRoute = pathname.match(/^\/api\/agent\/tasks\/([^/]+)\/sync-official$/);
  412. if (customerTaskSyncRoute && req.method === 'POST') {
  413. const body = await readBody(req);
  414. json(res, 200, await syncCustomerTaskToOfficialTodo(customerTaskSyncRoute[1], body));
  415. return;
  416. }
  417. const customerTaskRoute = pathname.match(/^\/api\/agent\/tasks\/([^/]+)$/);
  418. if (customerTaskRoute && req.method === 'POST') {
  419. const body = await readBody(req);
  420. json(res, 200, await updateCustomerTask(customerTaskRoute[1], body));
  421. return;
  422. }
  423. const customerAlertRoute = pathname.match(/^\/api\/agent\/alerts\/([^/]+)$/);
  424. if (customerAlertRoute && req.method === 'POST') {
  425. const body = await readBody(req);
  426. json(res, 200, updateCustomerAlert(customerAlertRoute[1], body));
  427. return;
  428. }
  429. const agentConversationRoute = pathname.match(/^\/api\/agent\/conversations\/([^/]+)\/(takeover|resume|pause|auto|approve-reply|generate|manual-send)$/);
  430. if (agentConversationRoute && req.method === 'POST') {
  431. const [, conversationId, action] = agentConversationRoute;
  432. const body = await readBody(req);
  433. if (action === 'takeover') json(res, 200, changeConversationMode(conversationId, 'human'));
  434. else if (action === 'resume') json(res, 200, changeConversationMode(conversationId, 'review'));
  435. else if (action === 'pause') json(res, 200, changeConversationMode(conversationId, 'paused'));
  436. else if (action === 'auto') json(res, 200, changeConversationMode(conversationId, 'auto'));
  437. else if (action === 'generate') json(res, 200, await generateLatestDraft(conversationId));
  438. else if (action === 'manual-send') json(res, 200, await manualSend(conversationId, body.content));
  439. else json(res, 200, await approveReply(conversationId, body.content));
  440. return;
  441. }
  442. const agentDraftRoute = pathname.match(/^\/api\/agent\/drafts\/([^/]+)\/(approve|reject|regenerate)$/);
  443. if (agentDraftRoute && req.method === 'POST') {
  444. const [, draftId, action] = agentDraftRoute;
  445. const body = await readBody(req);
  446. if (action === 'approve') json(res, 200, await approveDraft(draftId, body.content));
  447. else if (action === 'reject') json(res, 200, rejectDraft(draftId, body.reason));
  448. else json(res, 200, await regenerateDraft(draftId));
  449. return;
  450. }
  451. if (pathname === '/api/dashboard/summary' && req.method === 'GET') {
  452. json(res, 200, { status: 'ok', data: getStateSection('summary') });
  453. return;
  454. }
  455. if (pathname === '/api/dashboard/customers' && req.method === 'GET') {
  456. const filter = {
  457. keyword: url.searchParams.get('keyword') || '',
  458. hasPortrait: url.searchParams.has('hasPortrait') ? url.searchParams.get('hasPortrait') === 'true' : undefined,
  459. friendRequestStatus: url.searchParams.get('friendRequestStatus') || '',
  460. tag: url.searchParams.get('tag') || ''
  461. };
  462. json(res, 200, { status: 'ok', data: getStateSection('customers', { filter }) });
  463. return;
  464. }
  465. if (pathname === '/api/dashboard/portraits' && req.method === 'GET') {
  466. json(res, 200, { status: 'ok', data: getStateSection('portraits') });
  467. return;
  468. }
  469. if (pathname === '/api/dashboard/tags' && req.method === 'GET') {
  470. json(res, 200, { status: 'ok', data: getStateSection('tags') });
  471. return;
  472. }
  473. if (pathname === '/api/dashboard/transfers' && req.method === 'GET') {
  474. json(res, 200, { status: 'ok', data: getStateSection('transfers', { limit: url.searchParams.get('limit') }) });
  475. return;
  476. }
  477. if (pathname === '/api/dashboard/groups' && req.method === 'GET') {
  478. json(res, 200, { status: 'ok', data: getStateSection('groups') });
  479. return;
  480. }
  481. if (pathname === '/api/dashboard/operations' && req.method === 'GET') {
  482. json(res, 200, { status: 'ok', data: getStateSection('operations', { limit: url.searchParams.get('limit') }) });
  483. return;
  484. }
  485. if (pathname === '/api/dashboard/rebuild' && req.method === 'POST') {
  486. const state = rebuildStateFromOutputs();
  487. json(res, 200, { status: 'ok', data: { message: '索引已重建', summary: getStateSection('summary'), updatedAt: state.updatedAt } });
  488. return;
  489. }
  490. if (pathname === '/api/login/start' && req.method === 'POST') {
  491. const body = await readBody(req);
  492. const result = await qiweiLoginStart({
  493. flowUi: false,
  494. openBrowser: false,
  495. ...body
  496. });
  497. json(res, 200, result);
  498. return;
  499. }
  500. if (pathname === '/api/login/check' && req.method === 'POST') {
  501. const body = await readBody(req);
  502. const result = await qiweiLoginCheck(body);
  503. json(res, 200, result);
  504. return;
  505. }
  506. if (pathname === '/api/login/verify' && req.method === 'POST') {
  507. const body = await readBody(req);
  508. const result = await qiweiLoginVerify(body);
  509. json(res, 200, result);
  510. return;
  511. }
  512. if (pathname === '/api/groups/sync' && req.method === 'POST') {
  513. const body = await readBody(req);
  514. const id = createJob(() => qiweiSyncExternalGroups(body));
  515. json(res, 200, { status: 'ok', data: { jobId: id } });
  516. return;
  517. }
  518. if (pathname === '/api/groups/list' && req.method === 'GET') {
  519. const params = {};
  520. if (url.searchParams.has('keyword')) params.keyword = url.searchParams.get('keyword');
  521. if (url.searchParams.has('status')) params.status = url.searchParams.get('status');
  522. if (url.searchParams.has('source')) params.source = url.searchParams.get('source');
  523. if (url.searchParams.has('includeRejected')) params.includeRejected = url.searchParams.get('includeRejected') === 'true';
  524. json(res, 200, await qiweiListExternalGroups(params));
  525. return;
  526. }
  527. if (pathname === '/api/groups/analyze' && req.method === 'POST') {
  528. const body = await readBody(req);
  529. const id = createJob(() => qiweiAnalyzeGroupMembers(body));
  530. json(res, 200, { status: 'ok', data: { jobId: id } });
  531. return;
  532. }
  533. if (pathname === '/api/groups/confirm' && req.method === 'POST') {
  534. const body = await readBody(req);
  535. json(res, 200, await qiweiConfirmExternalGroup(body));
  536. return;
  537. }
  538. if (pathname === '/api/groups/add' && req.method === 'POST') {
  539. const body = await readBody(req);
  540. json(res, 200, await qiweiAddExternalGroup(body));
  541. return;
  542. }
  543. if (pathname === '/api/groups/reject' && req.method === 'POST') {
  544. const body = await readBody(req);
  545. json(res, 200, await qiweiRejectExternalGroup(body));
  546. return;
  547. }
  548. if (pathname === '/api/groups/keywords' && req.method === 'POST') {
  549. const body = await readBody(req);
  550. json(res, 200, await qiweiConfigureGroupKeywords(body));
  551. return;
  552. }
  553. if (pathname === '/api/groups/messages/sync' && req.method === 'POST') {
  554. const body = await readBody(req);
  555. const id = createJob(() => qiweiSyncGroupMessages(body));
  556. json(res, 200, { status: 'ok', data: { jobId: id } });
  557. return;
  558. }
  559. // Customer Operations
  560. if (pathname === '/api/customers' && req.method === 'GET') {
  561. json(res, 200, getCustomerMasterHub());
  562. return;
  563. }
  564. const customerMasterRoute = pathname.match(/^\/api\/customers\/([^/]+)\/profile$/);
  565. if (customerMasterRoute && req.method === 'POST') {
  566. json(res, 200, updateCustomerMaster(decodeURIComponent(customerMasterRoute[1]), await readBody(req)));
  567. return;
  568. }
  569. const customerRecommendationRoute = pathname.match(/^\/api\/customers\/([^/]+)\/recommendations\/([^/]+)$/);
  570. if (customerRecommendationRoute && req.method === 'POST') {
  571. json(res, 200, updateCustomerRecommendationFeedback(decodeURIComponent(customerRecommendationRoute[1]), decodeURIComponent(customerRecommendationRoute[2]), await readBody(req)));
  572. return;
  573. }
  574. if (pathname === '/api/customer-ops/batch-add-friends' && req.method === 'POST') {
  575. const body = await readBody(req);
  576. const id = createJob(() => qiweiBatchAddFriends(body));
  577. json(res, 200, { status: 'ok', data: { jobId: id } });
  578. return;
  579. }
  580. if (pathname === '/api/customer-ops/check-friend-status' && req.method === 'POST') {
  581. const body = await readBody(req);
  582. const id = createJob(() => qiweiCheckFriendStatus(body));
  583. json(res, 200, { status: 'ok', data: { jobId: id } });
  584. return;
  585. }
  586. if (pathname === '/api/customer-ops/customer-profile' && req.method === 'POST') {
  587. const body = await readBody(req);
  588. json(res, 200, await qiweiGetCustomerProfile(body));
  589. return;
  590. }
  591. if (pathname === '/api/customer-ops/update' && req.method === 'POST') {
  592. json(res, 200, await qiweiUpdateCustomerInfo(await readBody(req)));
  593. return;
  594. }
  595. if (pathname === '/api/customer-ops/auto-create-group' && req.method === 'POST') {
  596. const body = await readBody(req);
  597. const id = createJob(() => qiweiAutoCreateGroup(body));
  598. json(res, 200, { status: 'ok', data: { jobId: id } });
  599. return;
  600. }
  601. // Portraits & Tags
  602. if (pathname === '/api/portraits/update' && req.method === 'POST') {
  603. const body = await readBody(req);
  604. const id = createJob(() => qiweiUpdateCustomerPortrait(body));
  605. json(res, 200, { status: 'ok', data: { jobId: id } });
  606. return;
  607. }
  608. if (pathname === '/api/portraits/save' && req.method === 'POST') {
  609. const body = await readBody(req);
  610. json(res, 200, await qiweiSaveCustomerPortrait(body));
  611. return;
  612. }
  613. if (pathname === '/api/portraits/batch' && req.method === 'POST') {
  614. const body = await readBody(req);
  615. const id = createJob(() => qiweiBatchUpdateCustomerPortrait(body));
  616. json(res, 200, { status: 'ok', data: { jobId: id } });
  617. return;
  618. }
  619. if (pathname === '/api/portraits/export' && req.method === 'POST') {
  620. const body = await readBody(req);
  621. const id = createJob(() => qiweiExportCustomerPortraits(body));
  622. json(res, 200, { status: 'ok', data: { jobId: id } });
  623. return;
  624. }
  625. if (pathname === '/api/tags/list' && req.method === 'POST') {
  626. const body = await readBody(req);
  627. json(res, 200, await qiweiListCustomerTags(body));
  628. return;
  629. }
  630. if (pathname === '/api/tags/all' && req.method === 'POST') {
  631. const body = await readBody(req);
  632. json(res, 200, await qiweiListAllTags(body));
  633. return;
  634. }
  635. if (pathname === '/api/tags/add' && req.method === 'POST') {
  636. const body = await readBody(req);
  637. json(res, 200, await qiweiAddCustomerTags(body));
  638. return;
  639. }
  640. if (pathname === '/api/tags/remove' && req.method === 'POST') {
  641. const body = await readBody(req);
  642. json(res, 200, await qiweiRemoveCustomerTags(body));
  643. return;
  644. }
  645. if (pathname === '/api/personal-labels/sync' && req.method === 'POST') {
  646. const body = await readBody(req);
  647. json(res, 200, await qiweiSyncPersonalLabels(body));
  648. return;
  649. }
  650. if (pathname === '/api/personal-labels/create' && req.method === 'POST') {
  651. const body = await readBody(req);
  652. json(res, 200, await qiweiCreatePersonalLabel(body));
  653. return;
  654. }
  655. if (pathname === '/api/personal-labels/update' && req.method === 'POST') {
  656. const body = await readBody(req);
  657. json(res, 200, await qiweiUpdatePersonalLabel(body));
  658. return;
  659. }
  660. if (pathname === '/api/personal-labels/delete' && req.method === 'POST') {
  661. const body = await readBody(req);
  662. json(res, 200, await qiweiDeletePersonalLabel(body));
  663. return;
  664. }
  665. if (pathname === '/api/personal-labels/apply' && req.method === 'POST') {
  666. const body = await readBody(req);
  667. json(res, 200, await qiweiApplyPersonalLabels(body));
  668. return;
  669. }
  670. // Customer Transfers
  671. if (pathname === '/api/transfers/preview' && req.method === 'POST') {
  672. const body = await readBody(req);
  673. json(res, 200, await qiweiPreviewTransferPackage(body));
  674. return;
  675. }
  676. if (pathname === '/api/transfers/execute' && req.method === 'POST') {
  677. const body = await readBody(req);
  678. const id = createJob(() => qiweiExecuteTransfer(body));
  679. json(res, 200, { status: 'ok', data: { jobId: id } });
  680. return;
  681. }
  682. if (pathname === '/api/upload' && req.method === 'POST') {
  683. const result = await handleUpload(req);
  684. json(res, 200, { status: 'ok', data: result });
  685. return;
  686. }
  687. if (pathname === '/api/outputs' && req.method === 'GET') {
  688. handleOutputsDownload(req, res, url.searchParams);
  689. return;
  690. }
  691. if (pathname.startsWith('/api/jobs/') && req.method === 'GET') {
  692. const id = pathname.slice('/api/jobs/'.length);
  693. const job = getJob(id);
  694. if (!job) {
  695. json(res, 404, { status: 'error', message: '任务不存在' });
  696. return;
  697. }
  698. json(res, 200, { status: 'ok', data: job });
  699. return;
  700. }
  701. json(res, 404, { status: 'error', message: 'not found' });
  702. } catch (error) {
  703. json(res, 500, { status: 'error', message: error.message || String(error) });
  704. }
  705. }
  706. function startServer(port = DASHBOARD_PORT) {
  707. const server = http.createServer(handleRequest);
  708. return new Promise((resolve, reject) => {
  709. server.once('error', reject);
  710. server.listen(port, '127.0.0.1', () => {
  711. console.log(`Qiwei Dashboard 已启动:http://127.0.0.1:${port}/`);
  712. resolve({ server, url: `http://127.0.0.1:${port}/`, port });
  713. });
  714. });
  715. }
  716. if (require.main === module) {
  717. startServer().catch(error => {
  718. console.error('Dashboard 启动失败:', error);
  719. process.exit(1);
  720. });
  721. }
  722. module.exports = { startServer };