server.js 44 KB

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