server.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146
  1. const http = require('http');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { URL } = require('url');
  5. const { outputsRoot, categoryDir } = require('../core/output-paths');
  6. const { resolveWorkspaceRoot, workspaceIdentity } = require('../core/runtime-context');
  7. const {
  8. qiweiSyncExternalGroups,
  9. qiweiListExternalGroups,
  10. qiweiAnalyzeGroupMembers,
  11. qiweiConfirmExternalGroup,
  12. qiweiAddExternalGroup,
  13. qiweiConfigureGroupKeywords,
  14. qiweiRejectExternalGroup,
  15. qiweiSyncGroupMessages
  16. } = require('../tools/qiwei-group-management-run');
  17. const {
  18. qiweiGroupOpsGetContext,
  19. qiweiGroupOpsGeneratePlan,
  20. qiweiGroupOpsBatchGeneratePlans,
  21. qiweiGroupOpsReviewMessages,
  22. qiweiGroupOpsManagePlaybook,
  23. qiweiGroupOpsExecuteItem,
  24. qiweiGroupOpsUpdateFinding,
  25. qiweiGroupOpsInsights,
  26. qiweiGroupOpsManageTask,
  27. qiweiGroupOpsAutomation
  28. } = require('../tools/qiwei-group-operations-run');
  29. const {
  30. qiweiLoginStatus,
  31. qiweiLoginStart,
  32. qiweiLoginCheck,
  33. qiweiLoginVerify
  34. } = require('../tools/qiwei-login-run');
  35. const { qiweiSubscriptionStatus } = require('../tools/qiwei-subscription-run');
  36. const { createAccountConnectionMonitor } = require('../core/account-connection-monitor');
  37. const {
  38. qiweiBatchAddFriends,
  39. qiweiCheckFriendStatus,
  40. qiweiGetCustomerProfile,
  41. qiweiAutoCreateGroup,
  42. qiweiUpdateCustomerInfo
  43. } = require('../tools/qiwei-customer-ops-run');
  44. const {
  45. qiweiUpdateCustomerPortrait,
  46. qiweiSaveCustomerPortrait,
  47. qiweiBatchUpdateCustomerPortrait,
  48. qiweiExportCustomerPortraits,
  49. qiweiListCustomerTags,
  50. qiweiListAllTags,
  51. qiweiAddCustomerTags,
  52. qiweiRemoveCustomerTags,
  53. qiweiSyncPersonalLabels,
  54. qiweiCreatePersonalLabel,
  55. qiweiUpdatePersonalLabel,
  56. qiweiDeletePersonalLabel,
  57. qiweiApplyPersonalLabels
  58. } = require('../tools/qiwei-portrait-tags-run');
  59. const {
  60. qiweiPreviewTransferPackage,
  61. qiweiExecuteTransfer
  62. } = require('../tools/qiwei-customer-transfer-run');
  63. const {
  64. getStateSection,
  65. rebuildStateFromOutputs
  66. } = require('../core/dashboard-state');
  67. const {
  68. switchActiveAccount,
  69. getAgentStatus,
  70. getAllowlistCandidates,
  71. updateAllowlist,
  72. getConversations,
  73. getResponseMonitor,
  74. getGroupAgents,
  75. changeGroupMode,
  76. generateGroupReply,
  77. approveGroupDraft,
  78. rejectGroupDraft,
  79. regenerateGroupDraft,
  80. syncConversations,
  81. changeGlobalMode,
  82. changeConversationMode,
  83. approveReply,
  84. approveDraft,
  85. rejectDraft,
  86. regenerateDraft,
  87. generateLatestDraft,
  88. manualSend,
  89. getVoiceStatus,
  90. enrollVoice,
  91. revokeVoiceProfile,
  92. sendClonedVoice,
  93. getSentVoiceAudio,
  94. updateCustomerTask,
  95. syncCustomerTaskToOfficialTodo,
  96. updateCustomerAlert,
  97. getAudit,
  98. startListener,
  99. stopListener
  100. } = require('./agent-service');
  101. const {
  102. listKnowledgeTree,
  103. readKnowledgeFile,
  104. listProperties,
  105. getProperty,
  106. listSkillRegistry,
  107. getSkillDetail
  108. } = require('./workspace-library-service');
  109. const {
  110. getMeetingKnowledgeHub,
  111. syncMeetingKnowledge,
  112. analyzeMeetingKnowledge,
  113. getMeetingKnowledge
  114. } = require('./meeting-knowledge-service');
  115. const {
  116. getDocumentKnowledgeHub,
  117. importDocumentKnowledge,
  118. createDocumentKnowledge,
  119. analyzeDocumentKnowledge,
  120. refreshDocumentKnowledge,
  121. getDocumentKnowledge,
  122. getTodoKnowledgeHub,
  123. searchTodoUsers,
  124. syncTodoKnowledge,
  125. refreshTodoKnowledgeDetails,
  126. createTodoKnowledge,
  127. completeTodoKnowledge
  128. } = require('./official-office-knowledge-service');
  129. const {
  130. getUnifiedTaskHub,
  131. createUnifiedLocalTask,
  132. createUnifiedDiagnosisCandidate,
  133. updateUnifiedDiagnosisOutcome,
  134. updateUnifiedTask
  135. } = require('./unified-task-service');
  136. const {
  137. getCustomerMasterHub,
  138. updateCustomerMaster,
  139. updateCustomerRecommendationFeedback
  140. } = require('./customer-master-service');
  141. const { qiweiBusinessDiagnosis } = require('../tools/qiwei-business-diagnosis-run');
  142. const DASHBOARD_PORT = process.env.QIWEI_DASHBOARD_PORT || 4320;
  143. const STATIC_DIR = path.join(__dirname);
  144. const TMP_DIR = path.join(outputsRoot(), 'tmp');
  145. const WORKSPACE_ID = workspaceIdentity(resolveWorkspaceRoot());
  146. const jobs = new Map();
  147. const accountConnectionMonitor = createAccountConnectionMonitor({
  148. checkStatus: () => qiweiLoginStatus({}),
  149. recoverLogin: () => qiweiLoginCheck({ manual: true, persistConfig: false }),
  150. // 后台每 15 秒主动探测一次;页面读取复用最近结果,避免再次阻塞远端登录状态接口。
  151. cacheTtlMs: 20000
  152. });
  153. accountConnectionMonitor.start();
  154. void accountConnectionMonitor.getStatus().catch(() => {});
  155. const SUBSCRIPTION_STATUS_CACHE_TTL_MS = 2 * 60 * 1000;
  156. let subscriptionStatusCache = null;
  157. let subscriptionStatusCachedAt = 0;
  158. let subscriptionStatusInFlight = null;
  159. async function getCachedSubscriptionStatus({ force = false } = {}) {
  160. const fresh = subscriptionStatusCache && Date.now() - subscriptionStatusCachedAt < SUBSCRIPTION_STATUS_CACHE_TTL_MS;
  161. if (!force && fresh) return subscriptionStatusCache;
  162. if (subscriptionStatusInFlight) return subscriptionStatusInFlight;
  163. subscriptionStatusInFlight = qiweiSubscriptionStatus({})
  164. .then(result => {
  165. subscriptionStatusCache = result;
  166. subscriptionStatusCachedAt = Date.now();
  167. return result;
  168. })
  169. .finally(() => { subscriptionStatusInFlight = null; });
  170. return subscriptionStatusInFlight;
  171. }
  172. // Dashboard 启动后立即预热较慢的远程订阅状态,用户进入账号页时通常可直接命中缓存。
  173. void getCachedSubscriptionStatus().catch(() => {});
  174. const CUSTOMER_DIRECTORY_CACHE_TTL_MS = 60 * 1000;
  175. let customerDirectoryCache = null;
  176. let customerDirectoryCachedAt = 0;
  177. let customerDirectoryInFlight = null;
  178. async function buildDashboardCustomerDirectory() {
  179. const merged = new Map();
  180. const mergeCustomer = customer => {
  181. const externalUserId = String(customer.externalUserId || customer.customerId || '').trim();
  182. if (!externalUserId) return;
  183. const current = merged.get(externalUserId) || {};
  184. merged.set(externalUserId, {
  185. ...current,
  186. ...customer,
  187. externalUserId,
  188. customerId: customer.customerId || current.customerId || externalUserId,
  189. name: customer.name || customer.displayName || current.name || '',
  190. phone: customer.phone || current.phone || '',
  191. tags: Array.isArray(customer.tags) ? customer.tags : (current.tags || []),
  192. hasPortrait: customer.hasPortrait !== undefined
  193. ? Boolean(customer.hasPortrait)
  194. : Boolean((customer.fields || current.fields || []).length || current.hasPortrait),
  195. });
  196. };
  197. try {
  198. const allowlist = await getAllowlistCandidates();
  199. for (const contact of allowlist.data?.contacts || []) {
  200. mergeCustomer({
  201. externalUserId: contact.id,
  202. name: contact.displayName,
  203. selected: contact.selected === true,
  204. friendRequestStatus: 'ACCEPTED',
  205. source: 'qiwei-contact',
  206. });
  207. }
  208. } catch {
  209. // Local and conversation-backed customers remain available during a remote outage.
  210. }
  211. const local = getStateSection('customers');
  212. for (const customer of Object.values(local.customers || {})) mergeCustomer(customer);
  213. const master = getCustomerMasterHub();
  214. for (const customer of master.data?.customers || []) {
  215. mergeCustomer({
  216. ...customer,
  217. name: customer.displayName,
  218. hasPortrait: Array.isArray(customer.fields) && customer.fields.length > 0,
  219. });
  220. }
  221. return Array.from(merged.values());
  222. }
  223. async function getDashboardCustomerDirectory({ force = false } = {}) {
  224. const fresh = customerDirectoryCache && Date.now() - customerDirectoryCachedAt < CUSTOMER_DIRECTORY_CACHE_TTL_MS;
  225. if (!force && fresh) return customerDirectoryCache;
  226. if (customerDirectoryInFlight) return customerDirectoryInFlight;
  227. customerDirectoryInFlight = buildDashboardCustomerDirectory()
  228. .then(customers => {
  229. customerDirectoryCache = customers;
  230. customerDirectoryCachedAt = Date.now();
  231. return customers;
  232. })
  233. .finally(() => { customerDirectoryInFlight = null; });
  234. return customerDirectoryInFlight;
  235. }
  236. // 预热共享客户目录,画像、交接和客户选择器进入时可直接复用。
  237. void getDashboardCustomerDirectory().catch(() => {});
  238. function ensureTmpDir() {
  239. fs.mkdirSync(TMP_DIR, { recursive: true });
  240. }
  241. function readBody(req, maxBytes = 30 * 1024 * 1024) {
  242. return new Promise((resolve, reject) => {
  243. const chunks = [];
  244. let size = 0;
  245. let exceeded = false;
  246. req.on('data', chunk => {
  247. if (exceeded) return;
  248. size += chunk.length;
  249. if (size > maxBytes) {
  250. exceeded = true;
  251. reject(new Error('请求内容过大'));
  252. return;
  253. }
  254. chunks.push(chunk);
  255. });
  256. req.on('end', () => {
  257. if (exceeded) return;
  258. try {
  259. resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
  260. } catch {
  261. resolve({});
  262. }
  263. });
  264. req.on('error', reject);
  265. });
  266. }
  267. function json(res, status, body) {
  268. res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  269. res.end(JSON.stringify(body));
  270. }
  271. function createJob(runFn) {
  272. const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
  273. const job = { id, status: 'running', progress: 0, result: null, error: null, startedAt: Date.now() };
  274. jobs.set(id, job);
  275. runFn()
  276. .then(r => { job.status = 'done'; job.progress = 100; job.result = r; })
  277. .catch(e => { job.status = 'error'; job.error = e.message || String(e); });
  278. return id;
  279. }
  280. function getJob(id) {
  281. const job = jobs.get(id);
  282. if (!job) return null;
  283. if (job.status === 'running') {
  284. const elapsed = Date.now() - job.startedAt;
  285. job.progress = Math.min(95, Math.floor((elapsed / 30000) * 100));
  286. }
  287. return job;
  288. }
  289. function serveStatic(req, res, filePath, contentType) {
  290. fs.readFile(filePath, (err, data) => {
  291. if (err) {
  292. res.writeHead(404);
  293. res.end('not found');
  294. return;
  295. }
  296. res.writeHead(200, {
  297. 'Content-Type': contentType,
  298. 'Cache-Control': 'no-store'
  299. });
  300. res.end(data);
  301. });
  302. }
  303. function saveUploadBody(body) {
  304. const base64 = String(body.data || '');
  305. const requestedName = path.basename(String(body.name || `upload-${Date.now()}`)).replace(/[^A-Za-z0-9._\-\u4e00-\u9fa5]/g, '_');
  306. const name = !requestedName || requestedName === '.' || requestedName === '..' ? `upload-${Date.now()}` : requestedName;
  307. if (!base64) throw new Error('缺少文件数据');
  308. const maxBytes = 20 * 1024 * 1024;
  309. if (base64.length > Math.ceil(maxBytes / 3) * 4 + 16) throw new Error('上传文件不能超过 20MB');
  310. ensureTmpDir();
  311. const buffer = Buffer.from(base64, 'base64');
  312. if (buffer.length > maxBytes) throw new Error('上传文件不能超过 20MB');
  313. const filePath = path.join(TMP_DIR, name);
  314. fs.writeFileSync(filePath, buffer);
  315. return { path: filePath };
  316. }
  317. async function handleUpload(req) {
  318. return saveUploadBody(await readBody(req));
  319. }
  320. function handleOutputsDownload(req, res, query) {
  321. const requested = String(query.path || '');
  322. if (!requested) {
  323. json(res, 400, { status: 'error', message: '缺少 path 参数' });
  324. return;
  325. }
  326. const resolved = path.resolve(requested);
  327. const root = path.resolve(outputsRoot());
  328. if (!resolved.startsWith(root)) {
  329. json(res, 403, { status: 'error', message: '禁止访问 outputs 目录之外的文件' });
  330. return;
  331. }
  332. if (!fs.existsSync(resolved)) {
  333. json(res, 404, { status: 'error', message: '文件不存在' });
  334. return;
  335. }
  336. const data = fs.readFileSync(resolved);
  337. const ext = path.extname(resolved).toLowerCase();
  338. const contentType = {
  339. '.json': 'application/json',
  340. '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  341. '.xls': 'application/vnd.ms-excel',
  342. '.png': 'image/png',
  343. '.jpg': 'image/jpeg',
  344. '.jpeg': 'image/jpeg',
  345. '.mp3': 'audio/mpeg',
  346. '.mp4': 'video/mp4'
  347. }[ext] || 'application/octet-stream';
  348. res.writeHead(200, {
  349. 'Content-Type': contentType,
  350. 'Content-Disposition': `attachment; filename="${path.basename(resolved)}"`
  351. });
  352. res.end(data);
  353. }
  354. function serveSentVoiceAudio(req, res, requested) {
  355. const resolved = path.resolve(String(requested || ''));
  356. const root = path.resolve(categoryDir('voice'));
  357. const relative = path.relative(root, resolved);
  358. if (!relative || relative.startsWith('..') || path.isAbsolute(relative)
  359. || path.basename(resolved).toLowerCase() !== 'speech.wav'
  360. || !relative.split(path.sep).some(part => /^\d{6}-clone-[a-z0-9-]+$/i.test(part))
  361. || !fs.existsSync(resolved)) {
  362. json(res, 404, { status: 'error', message: '已发送语音文件不存在' });
  363. return;
  364. }
  365. const size = fs.statSync(resolved).size;
  366. const range = String(req.headers.range || '').match(/^bytes=(\d*)-(\d*)$/);
  367. let start = 0;
  368. let end = size - 1;
  369. if (range) {
  370. if (range[1]) start = Number(range[1]);
  371. if (range[2]) end = Number(range[2]);
  372. if (!range[1] && range[2]) start = Math.max(0, size - Number(range[2]));
  373. if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= size) {
  374. res.writeHead(416, { 'Content-Range': `bytes */${size}` });
  375. res.end();
  376. return;
  377. }
  378. end = Math.min(end, size - 1);
  379. }
  380. const headers = {
  381. 'Content-Type': 'audio/wav',
  382. 'Content-Length': end - start + 1,
  383. 'Accept-Ranges': 'bytes',
  384. 'Cache-Control': 'private, no-store',
  385. 'Content-Disposition': 'inline; filename="speech.wav"',
  386. };
  387. if (range) headers['Content-Range'] = `bytes ${start}-${end}/${size}`;
  388. res.writeHead(range ? 206 : 200, headers);
  389. fs.createReadStream(resolved, { start, end }).pipe(res);
  390. }
  391. function getContentType(filePath) {
  392. const ext = path.extname(filePath).toLowerCase();
  393. const map = {
  394. '.html': 'text/html; charset=utf-8',
  395. '.css': 'text/css; charset=utf-8',
  396. '.js': 'application/javascript; charset=utf-8',
  397. '.png': 'image/png',
  398. '.jpg': 'image/jpeg',
  399. '.jpeg': 'image/jpeg',
  400. '.svg': 'image/svg+xml'
  401. };
  402. return map[ext] || 'application/octet-stream';
  403. }
  404. async function combinedStatus({ fast = false } = {}) {
  405. const statusPromise = Promise.all([
  406. accountConnectionMonitor.getStatus(),
  407. getCachedSubscriptionStatus()
  408. ]);
  409. let login;
  410. let subscription;
  411. if (fast) {
  412. const timeout = Symbol('status-timeout');
  413. const result = await Promise.race([
  414. statusPromise,
  415. new Promise(resolve => setTimeout(() => resolve(timeout), 180))
  416. ]);
  417. if (result === timeout) {
  418. login = accountConnectionMonitor.state.lastOutput || { status: 'pending', summary: {}, data: {} };
  419. subscription = subscriptionStatusCache || { status: 'pending', summary: {}, data: {} };
  420. } else {
  421. [login, subscription] = result;
  422. }
  423. } else {
  424. [login, subscription] = await statusPromise;
  425. }
  426. const loginPending = login?.status === 'pending';
  427. const subscriptionPending = subscription?.status === 'pending';
  428. return {
  429. status: 'ok',
  430. summary: {
  431. authConfigured: loginPending ? null : login.status !== 'needs_auth',
  432. online: loginPending ? null : !!login.summary?.online,
  433. subscribed: subscriptionPending ? null : !!subscription.summary?.subscribed,
  434. loginPending,
  435. subscriptionPending
  436. },
  437. data: { login, subscription }
  438. };
  439. }
  440. async function handleRequest(req, res) {
  441. const url = new URL(req.url, 'http://localhost');
  442. const pathname = url.pathname;
  443. try {
  444. if (pathname === '/' || pathname === '/index.html') {
  445. serveStatic(req, res, path.join(STATIC_DIR, 'index.html'), 'text/html; charset=utf-8');
  446. return;
  447. }
  448. if (pathname.startsWith('/dashboard/')) {
  449. const fileName = pathname.slice('/dashboard/'.length).replace(/\.{2,}/g, '');
  450. const filePath = path.join(STATIC_DIR, fileName);
  451. if (!filePath.startsWith(STATIC_DIR)) {
  452. json(res, 403, { status: 'error', message: '禁止访问' });
  453. return;
  454. }
  455. serveStatic(req, res, filePath, getContentType(filePath));
  456. return;
  457. }
  458. if (pathname === '/api/health') {
  459. json(res, 200, { status: 'ok', data: { workspaceId: WORKSPACE_ID, port: Number(DASHBOARD_PORT) } });
  460. return;
  461. }
  462. if (pathname === '/api/status' && req.method === 'GET') {
  463. json(res, 200, await combinedStatus({ fast: url.searchParams.get('fast') === 'true' }));
  464. return;
  465. }
  466. if (pathname === '/api/skills' && req.method === 'GET') {
  467. json(res, 200, listSkillRegistry());
  468. return;
  469. }
  470. if (pathname === '/api/knowledge/tree' && req.method === 'GET') {
  471. json(res, 200, listKnowledgeTree());
  472. return;
  473. }
  474. if (pathname === '/api/knowledge/file' && req.method === 'GET') {
  475. json(res, 200, readKnowledgeFile(url.searchParams.get('id')));
  476. return;
  477. }
  478. if (pathname === '/api/knowledge/meetings' && req.method === 'GET') {
  479. json(res, 200, await getMeetingKnowledgeHub());
  480. return;
  481. }
  482. if (pathname === '/api/knowledge/meetings/sync' && req.method === 'POST') {
  483. json(res, 200, await syncMeetingKnowledge(await readBody(req)));
  484. return;
  485. }
  486. const meetingAnalyzeRoute = pathname.match(/^\/api\/knowledge\/meetings\/([^/]+)\/analyze$/);
  487. if (meetingAnalyzeRoute && req.method === 'POST') {
  488. json(res, 200, await analyzeMeetingKnowledge(decodeURIComponent(meetingAnalyzeRoute[1])));
  489. return;
  490. }
  491. const meetingDetailRoute = pathname.match(/^\/api\/knowledge\/meetings\/([^/]+)$/);
  492. if (meetingDetailRoute && req.method === 'GET') {
  493. json(res, 200, getMeetingKnowledge(decodeURIComponent(meetingDetailRoute[1])));
  494. return;
  495. }
  496. if (pathname === '/api/knowledge/docs' && req.method === 'GET') {
  497. json(res, 200, await getDocumentKnowledgeHub());
  498. return;
  499. }
  500. if (pathname === '/api/knowledge/docs/import' && req.method === 'POST') {
  501. json(res, 200, await importDocumentKnowledge(await readBody(req)));
  502. return;
  503. }
  504. if (pathname === '/api/knowledge/docs/create' && req.method === 'POST') {
  505. json(res, 200, await createDocumentKnowledge(await readBody(req)));
  506. return;
  507. }
  508. const documentAnalyzeRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)\/analyze$/);
  509. if (documentAnalyzeRoute && req.method === 'POST') {
  510. json(res, 200, await analyzeDocumentKnowledge(decodeURIComponent(documentAnalyzeRoute[1])));
  511. return;
  512. }
  513. const documentRefreshRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)\/refresh$/);
  514. if (documentRefreshRoute && req.method === 'POST') {
  515. json(res, 200, await refreshDocumentKnowledge(decodeURIComponent(documentRefreshRoute[1])));
  516. return;
  517. }
  518. const documentDetailRoute = pathname.match(/^\/api\/knowledge\/docs\/([^/]+)$/);
  519. if (documentDetailRoute && req.method === 'GET') {
  520. json(res, 200, getDocumentKnowledge(decodeURIComponent(documentDetailRoute[1])));
  521. return;
  522. }
  523. if (pathname === '/api/knowledge/todos' && req.method === 'GET') {
  524. json(res, 200, await getTodoKnowledgeHub());
  525. return;
  526. }
  527. if (pathname === '/api/knowledge/tasks' && req.method === 'GET') {
  528. json(res, 200, getUnifiedTaskHub());
  529. return;
  530. }
  531. if (pathname === '/api/knowledge/tasks/create' && req.method === 'POST') {
  532. json(res, 200, createUnifiedLocalTask(await readBody(req)));
  533. return;
  534. }
  535. if (pathname === '/api/knowledge/tasks/diagnosis-candidate' && req.method === 'POST') {
  536. json(res, 200, createUnifiedDiagnosisCandidate(await readBody(req)));
  537. return;
  538. }
  539. if (pathname === '/api/knowledge/tasks/diagnosis-feedback' && req.method === 'POST') {
  540. json(res, 200, updateUnifiedDiagnosisOutcome(await readBody(req)));
  541. return;
  542. }
  543. if (pathname === '/api/knowledge/tasks/update' && req.method === 'POST') {
  544. json(res, 200, await updateUnifiedTask(await readBody(req)));
  545. return;
  546. }
  547. if (pathname === '/api/knowledge/todos/search-users' && req.method === 'POST') {
  548. json(res, 200, await searchTodoUsers(await readBody(req)));
  549. return;
  550. }
  551. if (pathname === '/api/knowledge/todos/sync' && req.method === 'POST') {
  552. json(res, 200, await syncTodoKnowledge(await readBody(req)));
  553. return;
  554. }
  555. if (pathname === '/api/knowledge/todos/details' && req.method === 'POST') {
  556. json(res, 200, await refreshTodoKnowledgeDetails(await readBody(req)));
  557. return;
  558. }
  559. if (pathname === '/api/knowledge/todos/create' && req.method === 'POST') {
  560. json(res, 200, await createTodoKnowledge(await readBody(req)));
  561. return;
  562. }
  563. const todoCompleteRoute = pathname.match(/^\/api\/knowledge\/todos\/([^/]+)\/complete$/);
  564. if (todoCompleteRoute && req.method === 'POST') {
  565. json(res, 200, await completeTodoKnowledge(decodeURIComponent(todoCompleteRoute[1])));
  566. return;
  567. }
  568. if (pathname === '/api/knowledge/properties' && req.method === 'GET') {
  569. json(res, 200, listProperties(Object.fromEntries(url.searchParams.entries())));
  570. return;
  571. }
  572. const propertyRoute = pathname.match(/^\/api\/knowledge\/properties\/([^/]+)$/);
  573. if (propertyRoute && req.method === 'GET') {
  574. json(res, 200, getProperty(decodeURIComponent(propertyRoute[1])));
  575. return;
  576. }
  577. const skillRoute = pathname.match(/^\/api\/skills\/(.+)$/);
  578. if (skillRoute && req.method === 'GET') {
  579. json(res, 200, getSkillDetail(decodeURIComponent(skillRoute[1])));
  580. return;
  581. }
  582. if (pathname === '/api/agent/status' && req.method === 'GET') {
  583. json(res, 200, await getAgentStatus());
  584. return;
  585. }
  586. if (pathname === '/api/agent/voice/status' && req.method === 'GET') {
  587. json(res, 200, getVoiceStatus());
  588. return;
  589. }
  590. if (pathname === '/api/agent/voice/profile' && req.method === 'POST') {
  591. const body = await readBody(req);
  592. const upload = saveUploadBody(body);
  593. try {
  594. json(res, 200, await enrollVoice({
  595. filePath: upload.path,
  596. originalName: body.name,
  597. mime: body.type,
  598. }));
  599. } finally {
  600. fs.rmSync(upload.path, { force: true });
  601. }
  602. return;
  603. }
  604. if (pathname === '/api/agent/voice/profile' && req.method === 'DELETE') {
  605. json(res, 200, revokeVoiceProfile());
  606. return;
  607. }
  608. if (pathname === '/api/agent/allowlist' && req.method === 'GET') {
  609. json(res, 200, await getAllowlistCandidates());
  610. return;
  611. }
  612. if (pathname === '/api/agent/allowlist' && req.method === 'POST') {
  613. json(res, 200, updateAllowlist(await readBody(req)));
  614. return;
  615. }
  616. if (pathname === '/api/accounts/switch' && req.method === 'POST') {
  617. json(res, 200, await switchActiveAccount(await readBody(req)));
  618. return;
  619. }
  620. if (pathname === '/api/agent/conversations' && req.method === 'GET') {
  621. json(res, 200, getConversations());
  622. return;
  623. }
  624. if (pathname === '/api/agent/response-monitor' && req.method === 'GET') {
  625. json(res, 200, getResponseMonitor());
  626. return;
  627. }
  628. if (pathname === '/api/agent/groups' && req.method === 'GET') {
  629. json(res, 200, getGroupAgents());
  630. return;
  631. }
  632. const groupAgentModeRoute = pathname.match(/^\/api\/agent\/groups\/([^/]+)\/mode$/);
  633. if (groupAgentModeRoute && req.method === 'POST') {
  634. const body = await readBody(req);
  635. json(res, 200, changeGroupMode(decodeURIComponent(groupAgentModeRoute[1]), body.mode, body.confirmation));
  636. return;
  637. }
  638. const groupAgentGenerateRoute = pathname.match(/^\/api\/agent\/groups\/([^/]+)\/generate$/);
  639. if (groupAgentGenerateRoute && req.method === 'POST') {
  640. json(res, 200, await generateGroupReply(decodeURIComponent(groupAgentGenerateRoute[1]), await readBody(req)));
  641. return;
  642. }
  643. const groupAgentDraftRoute = pathname.match(/^\/api\/agent\/groups\/([^/]+)\/drafts\/([^/]+)\/(approve|reject|regenerate)$/);
  644. if (groupAgentDraftRoute && req.method === 'POST') {
  645. const [, roomId, draftId, action] = groupAgentDraftRoute;
  646. const body = await readBody(req);
  647. if (action === 'approve') json(res, 200, await approveGroupDraft(decodeURIComponent(roomId), decodeURIComponent(draftId), body.content));
  648. else if (action === 'reject') json(res, 200, rejectGroupDraft(decodeURIComponent(roomId), decodeURIComponent(draftId), body.reason));
  649. else json(res, 200, await regenerateGroupDraft(decodeURIComponent(roomId), decodeURIComponent(draftId)));
  650. return;
  651. }
  652. if (pathname === '/api/agent/conversations/sync' && req.method === 'POST') {
  653. json(res, 200, await syncConversations());
  654. return;
  655. }
  656. if (pathname === '/api/agent/audit' && req.method === 'GET') {
  657. json(res, 200, getAudit(url.searchParams.get('limit')));
  658. return;
  659. }
  660. if (pathname === '/api/agent/mode' && req.method === 'POST') {
  661. const body = await readBody(req);
  662. json(res, 200, changeGlobalMode(body.mode));
  663. return;
  664. }
  665. if (pathname === '/api/agent/listener/start' && req.method === 'POST') {
  666. json(res, 200, await startListener());
  667. return;
  668. }
  669. if (pathname === '/api/agent/listener/stop' && req.method === 'POST') {
  670. json(res, 200, stopListener());
  671. return;
  672. }
  673. const customerTaskSyncRoute = pathname.match(/^\/api\/agent\/tasks\/([^/]+)\/sync-official$/);
  674. if (customerTaskSyncRoute && req.method === 'POST') {
  675. const body = await readBody(req);
  676. json(res, 200, await syncCustomerTaskToOfficialTodo(customerTaskSyncRoute[1], body));
  677. return;
  678. }
  679. const customerTaskRoute = pathname.match(/^\/api\/agent\/tasks\/([^/]+)$/);
  680. if (customerTaskRoute && req.method === 'POST') {
  681. const body = await readBody(req);
  682. json(res, 200, await updateCustomerTask(customerTaskRoute[1], body));
  683. return;
  684. }
  685. const customerAlertRoute = pathname.match(/^\/api\/agent\/alerts\/([^/]+)$/);
  686. if (customerAlertRoute && req.method === 'POST') {
  687. const body = await readBody(req);
  688. json(res, 200, updateCustomerAlert(customerAlertRoute[1], body));
  689. return;
  690. }
  691. const agentConversationRoute = pathname.match(/^\/api\/agent\/conversations\/([^/]+)\/(takeover|resume|pause|auto|approve-reply|generate|manual-send)$/);
  692. if (agentConversationRoute && req.method === 'POST') {
  693. const [, conversationId, action] = agentConversationRoute;
  694. const body = await readBody(req);
  695. if (action === 'takeover') json(res, 200, changeConversationMode(conversationId, 'human'));
  696. else if (action === 'resume') json(res, 200, changeConversationMode(conversationId, 'review'));
  697. else if (action === 'pause') json(res, 200, changeConversationMode(conversationId, 'paused'));
  698. else if (action === 'auto') json(res, 200, changeConversationMode(conversationId, 'auto'));
  699. else if (action === 'generate') json(res, 200, await generateLatestDraft(conversationId));
  700. else if (action === 'manual-send') json(res, 200, await manualSend(conversationId, body.content));
  701. else json(res, 200, await approveReply(conversationId, body.content));
  702. return;
  703. }
  704. const voiceConversationRoute = pathname.match(/^\/api\/agent\/conversations\/([^/]+)\/voice-send$/);
  705. if (voiceConversationRoute && req.method === 'POST') {
  706. const [, conversationId] = voiceConversationRoute;
  707. const body = await readBody(req);
  708. json(res, 200, await sendClonedVoice(conversationId, body));
  709. return;
  710. }
  711. const voiceAudioRoute = pathname.match(/^\/api\/agent\/messages\/([^/]+)\/voice-audio$/);
  712. if (voiceAudioRoute && req.method === 'GET') {
  713. const audio = getSentVoiceAudio(decodeURIComponent(voiceAudioRoute[1]));
  714. serveSentVoiceAudio(req, res, audio.filePath);
  715. return;
  716. }
  717. const agentDraftRoute = pathname.match(/^\/api\/agent\/drafts\/([^/]+)\/(approve|reject|regenerate)$/);
  718. if (agentDraftRoute && req.method === 'POST') {
  719. const [, draftId, action] = agentDraftRoute;
  720. const body = await readBody(req);
  721. if (action === 'approve') json(res, 200, await approveDraft(draftId, body.content));
  722. else if (action === 'reject') json(res, 200, rejectDraft(draftId, body.reason));
  723. else json(res, 200, await regenerateDraft(draftId));
  724. return;
  725. }
  726. if (pathname === '/api/dashboard/summary' && req.method === 'GET') {
  727. json(res, 200, { status: 'ok', data: getStateSection('summary') });
  728. return;
  729. }
  730. if (pathname === '/api/business-diagnosis' && req.method === 'POST') {
  731. json(res, 200, await qiweiBusinessDiagnosis(await readBody(req)));
  732. return;
  733. }
  734. if (pathname === '/api/dashboard/customers/search' && req.method === 'GET') {
  735. const keyword = String(url.searchParams.get('keyword') || '').trim().toLowerCase();
  736. const hasPortrait = url.searchParams.has('hasPortrait') ? url.searchParams.get('hasPortrait') === 'true' : undefined;
  737. const scope = String(url.searchParams.get('scope') || '').trim();
  738. const forceRefresh = url.searchParams.get('refresh') === '1';
  739. let customers = (await getDashboardCustomerDirectory({ force: forceRefresh })).filter(customer => {
  740. if (scope === 'portrait' && customer.selected !== true && customer.discoveredFromGroups !== true && customer.groupStatus !== 'IN_GROUP') return false;
  741. if (keyword) {
  742. const text = `${customer.name || ''} ${customer.phone || ''} ${customer.externalUserId || ''}`.toLowerCase();
  743. if (!text.includes(keyword)) return false;
  744. }
  745. if (hasPortrait !== undefined && Boolean(customer.hasPortrait) !== hasPortrait) return false;
  746. return true;
  747. });
  748. const sortBy = url.searchParams.get('sortBy') || 'default';
  749. if (sortBy === 'lastActive') {
  750. customers.sort((a, b) => String(b.lastSeenInGroupAt || b.updatedAt || '').localeCompare(String(a.lastSeenInGroupAt || a.updatedAt || '')));
  751. } else if (sortBy === 'portraitDesc') {
  752. customers.sort((a, b) => Number(Boolean(b.hasPortrait)) - Number(Boolean(a.hasPortrait)));
  753. } else if (sortBy === 'portraitAsc') {
  754. customers.sort((a, b) => Number(Boolean(a.hasPortrait)) - Number(Boolean(b.hasPortrait)));
  755. } else if (sortBy === 'nameAsc') {
  756. customers.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN'));
  757. }
  758. const total = customers.length;
  759. const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit')) || 20));
  760. const offset = Math.max(0, Number(url.searchParams.get('offset')) || 0);
  761. customers = customers.slice(offset, offset + limit);
  762. json(res, 200, { status: 'ok', data: { customers, total, limit, offset } });
  763. return;
  764. }
  765. if (pathname === '/api/dashboard/customers' && req.method === 'GET') {
  766. const filter = {
  767. keyword: url.searchParams.get('keyword') || '',
  768. hasPortrait: url.searchParams.has('hasPortrait') ? url.searchParams.get('hasPortrait') === 'true' : undefined,
  769. friendRequestStatus: url.searchParams.get('friendRequestStatus') || '',
  770. tag: url.searchParams.get('tag') || ''
  771. };
  772. json(res, 200, { status: 'ok', data: getStateSection('customers', { filter }) });
  773. return;
  774. }
  775. if (pathname === '/api/dashboard/portraits' && req.method === 'GET') {
  776. json(res, 200, { status: 'ok', data: getStateSection('portraits') });
  777. return;
  778. }
  779. if (pathname === '/api/dashboard/tags' && req.method === 'GET') {
  780. json(res, 200, { status: 'ok', data: getStateSection('tags') });
  781. return;
  782. }
  783. if (pathname === '/api/dashboard/transfers' && req.method === 'GET') {
  784. json(res, 200, { status: 'ok', data: getStateSection('transfers', { limit: url.searchParams.get('limit') }) });
  785. return;
  786. }
  787. if (pathname === '/api/dashboard/groups' && req.method === 'GET') {
  788. json(res, 200, { status: 'ok', data: getStateSection('groups') });
  789. return;
  790. }
  791. if (pathname === '/api/dashboard/operations' && req.method === 'GET') {
  792. json(res, 200, { status: 'ok', data: getStateSection('operations', { limit: url.searchParams.get('limit') }) });
  793. return;
  794. }
  795. if (pathname === '/api/dashboard/rebuild' && req.method === 'POST') {
  796. const state = rebuildStateFromOutputs();
  797. json(res, 200, { status: 'ok', data: { message: '索引已重建', summary: getStateSection('summary'), updatedAt: state.updatedAt } });
  798. return;
  799. }
  800. if (pathname === '/api/login/start' && req.method === 'POST') {
  801. const body = await readBody(req);
  802. const result = await qiweiLoginStart({
  803. flowUi: false,
  804. openBrowser: false,
  805. ...body
  806. });
  807. json(res, 200, result);
  808. return;
  809. }
  810. if (pathname === '/api/login/check' && req.method === 'POST') {
  811. const body = await readBody(req);
  812. const result = await qiweiLoginCheck(body);
  813. json(res, 200, result);
  814. return;
  815. }
  816. if (pathname === '/api/login/verify' && req.method === 'POST') {
  817. const body = await readBody(req);
  818. const result = await qiweiLoginVerify(body);
  819. json(res, 200, result);
  820. return;
  821. }
  822. if (pathname === '/api/groups/sync' && req.method === 'POST') {
  823. const body = await readBody(req);
  824. const id = createJob(() => qiweiSyncExternalGroups(body));
  825. json(res, 200, { status: 'ok', data: { jobId: id } });
  826. return;
  827. }
  828. if (pathname === '/api/groups/list' && req.method === 'GET') {
  829. const params = {};
  830. if (url.searchParams.has('keyword')) params.keyword = url.searchParams.get('keyword');
  831. if (url.searchParams.has('status')) params.status = url.searchParams.get('status');
  832. if (url.searchParams.has('source')) params.source = url.searchParams.get('source');
  833. if (url.searchParams.has('includeRejected')) params.includeRejected = url.searchParams.get('includeRejected') === 'true';
  834. json(res, 200, await qiweiListExternalGroups(params));
  835. return;
  836. }
  837. if (pathname === '/api/groups/analyze' && req.method === 'POST') {
  838. const body = await readBody(req);
  839. const id = createJob(() => qiweiAnalyzeGroupMembers(body));
  840. json(res, 200, { status: 'ok', data: { jobId: id } });
  841. return;
  842. }
  843. if (pathname === '/api/groups/confirm' && req.method === 'POST') {
  844. const body = await readBody(req);
  845. json(res, 200, await qiweiConfirmExternalGroup(body));
  846. return;
  847. }
  848. if (pathname === '/api/groups/add' && req.method === 'POST') {
  849. const body = await readBody(req);
  850. json(res, 200, await qiweiAddExternalGroup(body));
  851. return;
  852. }
  853. if (pathname === '/api/groups/reject' && req.method === 'POST') {
  854. const body = await readBody(req);
  855. json(res, 200, await qiweiRejectExternalGroup(body));
  856. return;
  857. }
  858. if (pathname === '/api/groups/keywords' && req.method === 'POST') {
  859. const body = await readBody(req);
  860. json(res, 200, await qiweiConfigureGroupKeywords(body));
  861. return;
  862. }
  863. if (pathname === '/api/groups/messages/sync' && req.method === 'POST') {
  864. const body = await readBody(req);
  865. const id = createJob(() => qiweiSyncGroupMessages(body));
  866. json(res, 200, { status: 'ok', data: { jobId: id } });
  867. return;
  868. }
  869. // Group Operations
  870. if (pathname === '/api/group-ops/overview' && req.method === 'GET') {
  871. json(res, 200, await qiweiGroupOpsGetContext({
  872. accountKey: url.searchParams.get('accountKey') || undefined,
  873. date: url.searchParams.get('date') || undefined
  874. }));
  875. return;
  876. }
  877. if (pathname === '/api/group-ops/playbooks' && req.method === 'GET') {
  878. json(res, 200, await qiweiGroupOpsManagePlaybook({ accountKey: url.searchParams.get('accountKey') || undefined, action: 'list' }));
  879. return;
  880. }
  881. if (pathname === '/api/group-ops/playbooks' && req.method === 'POST') {
  882. json(res, 200, await qiweiGroupOpsManagePlaybook({ ...(await readBody(req)), action: 'create' }));
  883. return;
  884. }
  885. const groupOpsPreviewRoute = pathname.match(/^\/api\/group-ops\/playbooks\/([^/]+)\/(preview|versions)$/);
  886. if (groupOpsPreviewRoute && req.method === 'GET') {
  887. json(res, 200, await qiweiGroupOpsManagePlaybook({ accountKey: url.searchParams.get('accountKey') || undefined, playbookId: groupOpsPreviewRoute[1], version: url.searchParams.get('version') ? Number(url.searchParams.get('version')) : undefined, action: groupOpsPreviewRoute[2] }));
  888. return;
  889. }
  890. const groupOpsVersionRoute = pathname.match(/^\/api\/group-ops\/playbooks\/([^/]+)\/versions$/);
  891. if (groupOpsVersionRoute && req.method === 'POST') {
  892. json(res, 200, await qiweiGroupOpsManagePlaybook({ ...(await readBody(req)), playbookId: groupOpsVersionRoute[1], action: 'new_version' }));
  893. return;
  894. }
  895. const groupOpsPublishRoute = pathname.match(/^\/api\/group-ops\/playbooks\/([^/]+)\/(publish|rollback)$/);
  896. if (groupOpsPublishRoute && req.method === 'POST') {
  897. json(res, 200, await qiweiGroupOpsManagePlaybook({ ...(await readBody(req)), playbookId: groupOpsPublishRoute[1], action: groupOpsPublishRoute[2] }));
  898. return;
  899. }
  900. const groupOpsPlanRoute = pathname.match(/^\/api\/group-ops\/groups\/([^/]+)\/generate-plan$/);
  901. if (groupOpsPlanRoute && req.method === 'POST') {
  902. json(res, 200, await qiweiGroupOpsGeneratePlan({ ...(await readBody(req)), roomId: decodeURIComponent(groupOpsPlanRoute[1]) }));
  903. return;
  904. }
  905. if (pathname === '/api/group-ops/plans/batch' && req.method === 'POST') {
  906. json(res, 200, await qiweiGroupOpsBatchGeneratePlans(await readBody(req)));
  907. return;
  908. }
  909. const groupOpsReviewRoute = pathname.match(/^\/api\/group-ops\/groups\/([^/]+)\/review$/);
  910. if (groupOpsReviewRoute && req.method === 'POST') {
  911. json(res, 200, await qiweiGroupOpsReviewMessages({ ...(await readBody(req)), roomId: decodeURIComponent(groupOpsReviewRoute[1]) }));
  912. return;
  913. }
  914. const groupOpsItemRoute = pathname.match(/^\/api\/group-ops\/plan-items\/([^/]+)\/(approve|reject|skip|mark-sent)$/);
  915. if (groupOpsItemRoute && req.method === 'POST') {
  916. const action = groupOpsItemRoute[2] === 'mark-sent' ? 'mark_sent' : groupOpsItemRoute[2];
  917. json(res, 200, await qiweiGroupOpsExecuteItem({ ...(await readBody(req)), itemId: groupOpsItemRoute[1], action }));
  918. return;
  919. }
  920. const groupOpsFindingRoute = pathname.match(/^\/api\/group-ops\/findings\/([^/]+)$/);
  921. if (groupOpsFindingRoute && req.method === 'POST') {
  922. json(res, 200, await qiweiGroupOpsUpdateFinding({ ...(await readBody(req)), findingId: groupOpsFindingRoute[1] }));
  923. return;
  924. }
  925. if (pathname === '/api/group-ops/quality-settings' && req.method === 'POST') {
  926. json(res, 200, await qiweiGroupOpsInsights({ ...(await readBody(req)), action: 'update_settings' }));
  927. return;
  928. }
  929. if (pathname === '/api/group-ops/tasks' && req.method === 'GET') {
  930. json(res, 200, await qiweiGroupOpsManageTask({ accountKey: url.searchParams.get('accountKey') || undefined, status: url.searchParams.get('status') || undefined, action: 'list' }));
  931. return;
  932. }
  933. if (pathname === '/api/group-ops/tasks' && req.method === 'POST') {
  934. json(res, 200, await qiweiGroupOpsManageTask({ ...(await readBody(req)), action: 'create' }));
  935. return;
  936. }
  937. const groupOpsTaskRoute = pathname.match(/^\/api\/group-ops\/tasks\/([^/]+)\/(update|sync-official)$/);
  938. if (groupOpsTaskRoute && req.method === 'POST') {
  939. json(res, 200, await qiweiGroupOpsManageTask({ ...(await readBody(req)), taskId: groupOpsTaskRoute[1], action: groupOpsTaskRoute[2] === 'sync-official' ? 'sync_official' : 'update' }));
  940. return;
  941. }
  942. if (pathname === '/api/group-ops/automation' && req.method === 'POST') {
  943. json(res, 200, await qiweiGroupOpsAutomation(await readBody(req)));
  944. return;
  945. }
  946. // Customer Operations
  947. if (pathname === '/api/customers' && req.method === 'GET') {
  948. json(res, 200, getCustomerMasterHub());
  949. return;
  950. }
  951. const customerMasterRoute = pathname.match(/^\/api\/customers\/([^/]+)\/profile$/);
  952. if (customerMasterRoute && req.method === 'POST') {
  953. json(res, 200, updateCustomerMaster(decodeURIComponent(customerMasterRoute[1]), await readBody(req)));
  954. return;
  955. }
  956. const customerRecommendationRoute = pathname.match(/^\/api\/customers\/([^/]+)\/recommendations\/([^/]+)$/);
  957. if (customerRecommendationRoute && req.method === 'POST') {
  958. json(res, 200, updateCustomerRecommendationFeedback(decodeURIComponent(customerRecommendationRoute[1]), decodeURIComponent(customerRecommendationRoute[2]), await readBody(req)));
  959. return;
  960. }
  961. if (pathname === '/api/customer-ops/batch-add-friends' && req.method === 'POST') {
  962. const body = await readBody(req);
  963. const id = createJob(() => qiweiBatchAddFriends(body));
  964. json(res, 200, { status: 'ok', data: { jobId: id } });
  965. return;
  966. }
  967. if (pathname === '/api/customer-ops/check-friend-status' && req.method === 'POST') {
  968. const body = await readBody(req);
  969. const id = createJob(() => qiweiCheckFriendStatus(body));
  970. json(res, 200, { status: 'ok', data: { jobId: id } });
  971. return;
  972. }
  973. if (pathname === '/api/customer-ops/customer-profile' && req.method === 'POST') {
  974. const body = await readBody(req);
  975. json(res, 200, await qiweiGetCustomerProfile(body));
  976. return;
  977. }
  978. if (pathname === '/api/customer-ops/update' && req.method === 'POST') {
  979. json(res, 200, await qiweiUpdateCustomerInfo(await readBody(req)));
  980. return;
  981. }
  982. if (pathname === '/api/customer-ops/auto-create-group' && req.method === 'POST') {
  983. const body = await readBody(req);
  984. const id = createJob(() => qiweiAutoCreateGroup(body));
  985. json(res, 200, { status: 'ok', data: { jobId: id } });
  986. return;
  987. }
  988. // Portraits & Tags
  989. if (pathname === '/api/portraits/update' && req.method === 'POST') {
  990. const body = await readBody(req);
  991. const id = createJob(() => qiweiUpdateCustomerPortrait(body));
  992. json(res, 200, { status: 'ok', data: { jobId: id } });
  993. return;
  994. }
  995. if (pathname === '/api/portraits/save' && req.method === 'POST') {
  996. const body = await readBody(req);
  997. json(res, 200, await qiweiSaveCustomerPortrait(body));
  998. return;
  999. }
  1000. if (pathname === '/api/portraits/batch' && req.method === 'POST') {
  1001. const body = await readBody(req);
  1002. const id = createJob(() => qiweiBatchUpdateCustomerPortrait(body));
  1003. json(res, 200, { status: 'ok', data: { jobId: id } });
  1004. return;
  1005. }
  1006. if (pathname === '/api/portraits/export' && req.method === 'POST') {
  1007. const body = await readBody(req);
  1008. const id = createJob(() => qiweiExportCustomerPortraits(body));
  1009. json(res, 200, { status: 'ok', data: { jobId: id } });
  1010. return;
  1011. }
  1012. if (pathname === '/api/tags/list' && req.method === 'POST') {
  1013. const body = await readBody(req);
  1014. json(res, 200, await qiweiListCustomerTags(body));
  1015. return;
  1016. }
  1017. if (pathname === '/api/tags/all' && req.method === 'POST') {
  1018. const body = await readBody(req);
  1019. json(res, 200, await qiweiListAllTags(body));
  1020. return;
  1021. }
  1022. if (pathname === '/api/tags/add' && req.method === 'POST') {
  1023. const body = await readBody(req);
  1024. json(res, 200, await qiweiAddCustomerTags(body));
  1025. return;
  1026. }
  1027. if (pathname === '/api/tags/remove' && req.method === 'POST') {
  1028. const body = await readBody(req);
  1029. json(res, 200, await qiweiRemoveCustomerTags(body));
  1030. return;
  1031. }
  1032. if (pathname === '/api/personal-labels/sync' && req.method === 'POST') {
  1033. const body = await readBody(req);
  1034. json(res, 200, await qiweiSyncPersonalLabels(body));
  1035. return;
  1036. }
  1037. if (pathname === '/api/personal-labels/create' && req.method === 'POST') {
  1038. const body = await readBody(req);
  1039. json(res, 200, await qiweiCreatePersonalLabel(body));
  1040. return;
  1041. }
  1042. if (pathname === '/api/personal-labels/update' && req.method === 'POST') {
  1043. const body = await readBody(req);
  1044. json(res, 200, await qiweiUpdatePersonalLabel(body));
  1045. return;
  1046. }
  1047. if (pathname === '/api/personal-labels/delete' && req.method === 'POST') {
  1048. const body = await readBody(req);
  1049. json(res, 200, await qiweiDeletePersonalLabel(body));
  1050. return;
  1051. }
  1052. if (pathname === '/api/personal-labels/apply' && req.method === 'POST') {
  1053. const body = await readBody(req);
  1054. json(res, 200, await qiweiApplyPersonalLabels(body));
  1055. return;
  1056. }
  1057. // Customer Transfers
  1058. if (pathname === '/api/transfers/preview' && req.method === 'POST') {
  1059. const body = await readBody(req);
  1060. json(res, 200, await qiweiPreviewTransferPackage(body));
  1061. return;
  1062. }
  1063. if (pathname === '/api/transfers/execute' && req.method === 'POST') {
  1064. const body = await readBody(req);
  1065. const id = createJob(() => qiweiExecuteTransfer(body));
  1066. json(res, 200, { status: 'ok', data: { jobId: id } });
  1067. return;
  1068. }
  1069. if (pathname === '/api/upload' && req.method === 'POST') {
  1070. const result = await handleUpload(req);
  1071. json(res, 200, { status: 'ok', data: result });
  1072. return;
  1073. }
  1074. if (pathname === '/api/outputs' && req.method === 'GET') {
  1075. handleOutputsDownload(req, res, url.searchParams);
  1076. return;
  1077. }
  1078. if (pathname.startsWith('/api/jobs/') && req.method === 'GET') {
  1079. const id = pathname.slice('/api/jobs/'.length);
  1080. const job = getJob(id);
  1081. if (!job) {
  1082. json(res, 404, { status: 'error', message: '任务不存在' });
  1083. return;
  1084. }
  1085. json(res, 200, { status: 'ok', data: job });
  1086. return;
  1087. }
  1088. json(res, 404, { status: 'error', message: 'not found' });
  1089. } catch (error) {
  1090. json(res, 500, { status: 'error', message: error.message || String(error) });
  1091. }
  1092. }
  1093. function startServer(port = DASHBOARD_PORT) {
  1094. const server = http.createServer(handleRequest);
  1095. return new Promise((resolve, reject) => {
  1096. server.once('error', reject);
  1097. server.listen(port, '127.0.0.1', () => {
  1098. console.log(`Qiwei Dashboard 已启动:http://127.0.0.1:${port}/`);
  1099. resolve({ server, url: `http://127.0.0.1:${port}/`, port });
  1100. });
  1101. });
  1102. }
  1103. if (require.main === module) {
  1104. startServer().catch(error => {
  1105. console.error('Dashboard 启动失败:', error);
  1106. process.exit(1);
  1107. });
  1108. }
  1109. module.exports = { startServer };