smoke-test.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. #!/usr/bin/env node
  2. const assert = require('assert');
  3. const http = require('http');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const { loadCatalog, searchEndpoints, findEndpoint } = require('../mcp/src/core/api-catalog');
  7. const { searchQiweiApis, getQiweiApiDoc, callQiweiApi } = require('../mcp/src/tools/qiwei-api-catalog-run');
  8. const {
  9. qiweiLoginStatus,
  10. qiweiLoginStart,
  11. qiweiLoginCheck,
  12. qiweiLoginVerify
  13. } = require('../mcp/src/tools/qiwei-login-run');
  14. const { stopLoginFlowServer } = require('../mcp/src/core/login-flow-server');
  15. const { stopLoginFallbackServer } = require('../mcp/src/core/login-fallback-server');
  16. const {
  17. qiweiSubscriptionStatus,
  18. qiweiSubscribe,
  19. qiweiSubscriptionAutoRenew
  20. } = require('../mcp/src/tools/qiwei-subscription-run');
  21. const {
  22. qiweiCheckFriendStatus,
  23. qiweiGetCustomerProfile
  24. } = require('../mcp/src/tools/qiwei-customer-ops-run');
  25. const {
  26. qiweiSyncExternalGroups,
  27. qiweiListExternalGroups,
  28. qiweiAnalyzeGroupMembers,
  29. qiweiConfirmExternalGroup,
  30. qiweiAddExternalGroup,
  31. qiweiConfigureGroupKeywords,
  32. qiweiRejectExternalGroup,
  33. qiweiSyncGroupMessages
  34. } = require('../mcp/src/tools/qiwei-group-management-run');
  35. const {
  36. qiweiPrepareCustomerPortrait,
  37. qiweiUpdateCustomerPortrait,
  38. qiweiSaveCustomerPortrait,
  39. qiweiAddCustomerTags,
  40. qiweiListCustomerTags,
  41. qiweiSyncPersonalLabels,
  42. qiweiCreatePersonalLabel
  43. } = require('../mcp/src/tools/qiwei-portrait-tags-run');
  44. const {
  45. qiweiDistillBroker,
  46. qiweiPrepareBrokerPlaybook,
  47. qiweiSaveBrokerPlaybook,
  48. qiweiGetBrokerPlaybook
  49. } = require('../mcp/src/tools/qiwei-broker-playbook-run');
  50. const {
  51. qiweiPreviewTransferPackage,
  52. qiweiExecuteTransfer
  53. } = require('../mcp/src/tools/qiwei-customer-transfer-run');
  54. const { qiweiTranscribeVoice } = require('../mcp/src/tools/qiwei-voice-run');
  55. const {
  56. qiweiWebhookStatus,
  57. qiweiWebhookDiscover,
  58. qiweiWebhookServerStart,
  59. qiweiWebhookServerStop,
  60. qiweiWebhookAutoSetup
  61. } = require('../mcp/src/tools/qiwei-webhook-relay-run');
  62. const PRIVATE_PROVIDER_NAME = ['private', 'provider'].join('-');
  63. const PRIVATE_PROVIDER_HOST = ['private', 'provider', 'invalid'].join('.');
  64. const PRIVATE_CREDENTIAL_KEY = ['token', 'Id'].join('');
  65. function assertProviderHidden(value) {
  66. const output = JSON.stringify(value);
  67. assert(!output.includes(PRIVATE_PROVIDER_NAME));
  68. assert(!output.includes(PRIVATE_PROVIDER_HOST));
  69. assert(!output.includes('server-only-token'));
  70. }
  71. function readBody(req) {
  72. return new Promise(resolve => {
  73. let body = '';
  74. req.setEncoding('utf8');
  75. req.on('data', chunk => {
  76. body += chunk;
  77. });
  78. req.on('end', () => resolve(body ? JSON.parse(body) : undefined));
  79. });
  80. }
  81. async function startMockGateway() {
  82. const requests = [];
  83. const server = http.createServer(async (req, res) => {
  84. const body = await readBody(req);
  85. const url = new URL(req.url, 'http://localhost');
  86. requests.push({
  87. method: req.method,
  88. path: url.pathname,
  89. query: Object.fromEntries(url.searchParams.entries()),
  90. authorization: req.headers.authorization,
  91. body
  92. });
  93. let data;
  94. if (url.pathname === '/doApi') {
  95. if (body.method === '/contact/searchContact') {
  96. data = {
  97. searchStatus: 2,
  98. wxUserInfo: { userId: 'wx-u-1', nickname: '测试客户' },
  99. workUserInfo: null
  100. };
  101. } else if (body.method === '/contact/getWxContactList') {
  102. data = {
  103. currentSeq: 0,
  104. contactCount: 1,
  105. hasMore: false,
  106. contactList: [{ userId: 'wx-u-1', nickname: '测试客户' }]
  107. };
  108. } else if (body.method === '/room/getRoomList') {
  109. data = {
  110. hasMore: false,
  111. nextStartIndex: 0,
  112. roomCount: 2,
  113. roomList: [
  114. { roomId: 'r-1', roomName: '张三买房服务群', roomMemberCount: 3 },
  115. { roomId: 'r-2', roomName: 'random-group', roomMemberCount: 2 }
  116. ]
  117. };
  118. } else if (body.method === '/session/getSessionPage') {
  119. data = {
  120. hasMore: false,
  121. currentSeq: 0,
  122. sessionCount: 1,
  123. sessionList: [
  124. { roomId: 'r-3', sessionName: '潜在VIP群', sessionType: 1, roomMemberCount: 4 }
  125. ]
  126. };
  127. } else if (body.method === '/room/batchGetRoomDetail') {
  128. data = {
  129. roomList: (body.params.roomIdList || []).map(roomId => ({
  130. roomId,
  131. roomName: roomId === 'r-1' ? '张三买房服务群' : '其他群',
  132. roomMemberCount: roomId === 'r-1' ? 3 : 2
  133. }))
  134. };
  135. } else if (body.method === '/msg/syncMsg') {
  136. data = {
  137. hasMore: false,
  138. travelSyncKey: 1,
  139. syncMsgList: [
  140. { fromRoomId: 'r-1', seq: 1, senderId: 's-1', senderName: '张三', msgType: 1, content: '你好', msgUniqueIdentifier: 'm-1', timestamp: Math.floor(Date.now() / 1000) }
  141. ]
  142. };
  143. } else if (body.method === '/label/syncLabelList') {
  144. data = { currentSeq: 0, hasMore: false, labelCount: 1, labelList: [{ labelId: 'l-1', labelName: '高意向' }] };
  145. } else if (body.method === '/label/editLabel') {
  146. data = { success: true };
  147. } else if (body.method === '/label/contactEditLabel') {
  148. data = { success: true };
  149. } else {
  150. data = {
  151. nickname: '测试账号',
  152. echoedMethod: body.method,
  153. guid: 'server-only-guid',
  154. [PRIVATE_CREDENTIAL_KEY]: 'server-only-token',
  155. provider: PRIVATE_PROVIDER_NAME,
  156. providerName: PRIVATE_PROVIDER_NAME,
  157. providerHost: PRIVATE_PROVIDER_HOST,
  158. supportUrl: `https://${PRIVATE_PROVIDER_HOST}/help`,
  159. diagnostic: `See https://${PRIVATE_PROVIDER_HOST}/diagnostic`
  160. };
  161. }
  162. } else if (url.pathname === '/login/status') {
  163. data = { configured: true, online: true, statusCode: 2 };
  164. } else if (url.pathname === '/login/start') {
  165. data = { uid: body.uid, guid: 'server-only-guid', loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') };
  166. } else if (url.pathname === '/login/check') {
  167. data = { uid: body.uid, guid: 'server-only-guid', status: 2, detail: { nickname: '测试账号', userId: 'u-1', guid: 'server-only-guid' } };
  168. } else if (url.pathname === '/login/verify') {
  169. data = { accepted: true };
  170. } else if (url.pathname === '/subscribe/status') {
  171. data = {
  172. subscribed: true,
  173. seats: 2,
  174. usedSeats: 1,
  175. expireAt: '2026-08-10T00:00:00.000Z',
  176. autoRenew: true,
  177. balance: 1000
  178. };
  179. } else if (url.pathname === '/subscribe') {
  180. data = { seats: body.seats || 1, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true };
  181. } else if (url.pathname === '/subscribe/autoRenew') {
  182. data = { autoRenew: body.autoRenew };
  183. } else {
  184. res.writeHead(404, { 'Content-Type': 'application/json' });
  185. res.end(JSON.stringify({ code: 404, mess: 'not found' }));
  186. return;
  187. }
  188. res.writeHead(200, { 'Content-Type': 'application/json' });
  189. res.end(JSON.stringify({ code: 200, data }));
  190. });
  191. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  192. const address = server.address();
  193. return {
  194. requests,
  195. apiBase: `http://127.0.0.1:${address.port}`,
  196. close: () => new Promise(resolve => server.close(resolve))
  197. };
  198. }
  199. async function startMockVerifyGateway() {
  200. const requests = [];
  201. let verified = false;
  202. const server = http.createServer(async (req, res) => {
  203. const body = await readBody(req);
  204. const url = new URL(req.url, 'http://localhost');
  205. requests.push({
  206. method: req.method,
  207. path: url.pathname,
  208. query: Object.fromEntries(url.searchParams.entries()),
  209. authorization: req.headers.authorization,
  210. body
  211. });
  212. let data;
  213. if (url.pathname === '/login/status') {
  214. data = { configured: true, online: false, statusCode: -1 };
  215. } else if (url.pathname === '/login/start') {
  216. data = { uid: body.uid, guid: 'server-only-guid', loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') };
  217. } else if (url.pathname === '/login/check') {
  218. if (verified) {
  219. data = { uid: body.uid, status: 2, detail: { nickname: '测试账号', userId: 'u-1', guid: 'server-only-guid' } };
  220. } else {
  221. data = { uid: body.uid, status: 10, detail: {} };
  222. }
  223. } else if (url.pathname === '/login/verify') {
  224. verified = true;
  225. data = { accepted: true };
  226. } else if (url.pathname === '/subscribe/status') {
  227. data = { subscribed: true, seats: 2, usedSeats: 1, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true, balance: 1000 };
  228. } else {
  229. res.writeHead(404, { 'Content-Type': 'application/json' });
  230. res.end(JSON.stringify({ code: 404, mess: 'not found' }));
  231. return;
  232. }
  233. res.writeHead(200, { 'Content-Type': 'application/json' });
  234. res.end(JSON.stringify({ code: 200, data }));
  235. });
  236. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  237. const address = server.address();
  238. return {
  239. requests,
  240. apiBase: `http://127.0.0.1:${address.port}`,
  241. close: () => new Promise(resolve => server.close(resolve))
  242. };
  243. }
  244. async function main() {
  245. const catalog = loadCatalog();
  246. assert(catalog.endpoints.length >= 100, 'catalog should contain 100+ endpoints');
  247. assert(catalog.gateway.baseUrl.includes('server.fmode.cn/api/qiwei'));
  248. assert(catalog.endpoints.every(endpoint => !Object.hasOwn(endpoint, 'docUrl')));
  249. assertProviderHidden(catalog);
  250. console.log(`[ok] catalog loaded: ${catalog.endpoints.length} endpoints`);
  251. const hits = searchEndpoints({ query: '发送 文本' });
  252. assert(hits.length > 0 && hits[0].module === 'msg');
  253. assert(findEndpoint('/login/getLoginQrcode'));
  254. const searchResult = await searchQiweiApis({ query: '朋友圈' });
  255. assert(searchResult.status === 'ok' && searchResult.data.endpoints.length > 0);
  256. assertProviderHidden(searchResult);
  257. const doc = await getQiweiApiDoc({ id: 'user.getProfile' });
  258. assert(doc.status === 'ok' && doc.data.callTemplate);
  259. assertProviderHidden(doc);
  260. assert(!JSON.stringify(doc).includes('"guid"'));
  261. console.log('[ok] catalog search and docs');
  262. const gateway = await startMockGateway();
  263. const common = {
  264. authToken: 'sk-test-relay-token',
  265. uid: 'qiwei-smoke-device',
  266. apiBase: gateway.apiBase,
  267. persistConfig: false
  268. };
  269. try {
  270. const call = await callQiweiApi({ ...common, id: 'user.getProfile', params: {} });
  271. assert.strictEqual(call.status, 'ok');
  272. const apiRequest = gateway.requests.at(-1);
  273. assert.strictEqual(apiRequest.path, '/doApi');
  274. assert.strictEqual(apiRequest.authorization, 'Bearer sk-test-relay-token');
  275. assert.deepStrictEqual(apiRequest.body, {
  276. uid: 'qiwei-smoke-device',
  277. method: '/user/getProfile',
  278. params: {}
  279. });
  280. assert(!JSON.stringify(apiRequest.body).includes('tokenId'));
  281. assert(!JSON.stringify(apiRequest.body).includes('guid'));
  282. assertProviderHidden(call);
  283. console.log('[ok] qiwei_api_call uses the Fmode gateway envelope without private credentials');
  284. const blocked = await callQiweiApi({ ...common, rawMethod: '/login/manualLogin', params: {} });
  285. assert.strictEqual(blocked.status, 'error');
  286. assert.strictEqual(gateway.requests.at(-1), apiRequest);
  287. const subscription = await qiweiSubscriptionStatus(common);
  288. assert.strictEqual(subscription.status, 'ok');
  289. assert.strictEqual(subscription.summary.subscribed, true);
  290. const subscribed = await qiweiSubscribe({ ...common, seats: 3 });
  291. assert.strictEqual(subscribed.status, 'ok');
  292. assert.strictEqual(gateway.requests.at(-1).body.seats, 3);
  293. const autoRenew = await qiweiSubscriptionAutoRenew({ ...common, autoRenew: false });
  294. assert.strictEqual(autoRenew.status, 'ok');
  295. assert.strictEqual(gateway.requests.at(-1).body.autoRenew, false);
  296. console.log('[ok] subscription tools use Fmode gateway routes');
  297. const status = await qiweiLoginStatus(common);
  298. assert.strictEqual(status.status, 'ok');
  299. assert.strictEqual(gateway.requests.at(-1).query.uid, common.uid);
  300. const start = await qiweiLoginStart({ ...common, openBrowser: false });
  301. assert.strictEqual(start.status, 'ok');
  302. assertProviderHidden(start);
  303. const check = await qiweiLoginCheck(common);
  304. assert.strictEqual(check.status, 'ok');
  305. assert.strictEqual(check.summary.loggedIn, true);
  306. assertProviderHidden(check);
  307. const verify = await qiweiLoginVerify({ ...common, code: '123456' });
  308. assert.strictEqual(verify.status, 'ok');
  309. assert.deepStrictEqual(gateway.requests.at(-1).body, { uid: common.uid, code: '123456' });
  310. console.log('[ok] login tools use uid-based Fmode gateway routes and hide internal identifiers');
  311. const verifyGateway = await startMockVerifyGateway();
  312. try {
  313. const verifyCommon = {
  314. authToken: 'sk-test-relay-token',
  315. uid: 'qiwei-verify-device',
  316. apiBase: verifyGateway.apiBase,
  317. persistConfig: false
  318. };
  319. const fallbackStart = await qiweiLoginStart({ ...verifyCommon, flowUi: false, openBrowser: false });
  320. assert.strictEqual(fallbackStart.status, 'ok');
  321. assert.strictEqual(fallbackStart.summary.fallbackServer, true);
  322. assert.ok(fallbackStart.summary.flowUrl.startsWith('http://127.0.0.1:'), 'fallback server should return a local URL');
  323. assertProviderHidden(fallbackStart);
  324. const fallbackUrl = fallbackStart.summary.flowUrl;
  325. const pageRes = await fetch(fallbackUrl);
  326. assert.strictEqual(pageRes.status, 200);
  327. const pageHtml = await pageRes.text();
  328. assert.ok(pageHtml.includes('企业微信扫码登录'));
  329. assert.ok(!pageHtml.includes('sk-test-relay-token'), 'fallback page must not expose token');
  330. const qrRes = await fetch(`${fallbackUrl}flow/qrcode`);
  331. assert.strictEqual(qrRes.status, 200);
  332. assert.strictEqual(qrRes.headers.get('content-type'), 'image/png');
  333. const qrBody = await qrRes.arrayBuffer();
  334. assert.deepStrictEqual(Buffer.from(qrBody), Buffer.from('mock-png'));
  335. const checkRes = await fetch(`${fallbackUrl}flow/check`, { method: 'POST' });
  336. assert.strictEqual(checkRes.status, 200);
  337. const checkJson = await checkRes.json();
  338. assert.strictEqual(checkJson.status, 10);
  339. const verifyRes = await fetch(`${fallbackUrl}flow/verify`, {
  340. method: 'POST',
  341. headers: { 'Content-Type': 'application/json' },
  342. body: JSON.stringify({ code: '123456' })
  343. });
  344. assert.strictEqual(verifyRes.status, 200);
  345. const verifyJson = await verifyRes.json();
  346. assert.strictEqual(verifyJson.ok, true);
  347. const lastVerifyRequest = verifyGateway.requests.at(-1);
  348. assert.strictEqual(lastVerifyRequest.path, '/login/verify');
  349. assert.deepStrictEqual(lastVerifyRequest.body, { uid: verifyCommon.uid, code: '123456' });
  350. const checkAfterRes = await fetch(`${fallbackUrl}flow/check`, { method: 'POST' });
  351. assert.strictEqual(checkAfterRes.status, 200);
  352. const checkAfterJson = await checkAfterRes.json();
  353. assert.strictEqual(checkAfterJson.status, 2);
  354. assert.strictEqual(checkAfterJson.detail.userId, 'u-1');
  355. console.log('[ok] fallback login server serves QR, polls status and accepts verify code');
  356. } finally {
  357. await verifyGateway.close();
  358. stopLoginFallbackServer();
  359. }
  360. const friendStatus = await qiweiCheckFriendStatus({ ...common, guid: 'guid-1', phones: ['13800000000'] });
  361. assert.strictEqual(friendStatus.status, 'ok');
  362. assert.strictEqual(friendStatus.summary.confirmed, 1);
  363. assert.strictEqual(friendStatus.data.details[0].statusText, 'already_friend');
  364. assertProviderHidden(friendStatus);
  365. const portraitFile = path.join(__dirname, '..', 'outputs', 'portraits', 'wx-u-1.json');
  366. const tagFile = path.join(__dirname, '..', 'outputs', 'tags', 'wx-u-1.json');
  367. try { fs.unlinkSync(portraitFile); } catch {}
  368. try { fs.unlinkSync(tagFile); } catch {}
  369. const profile = await qiweiGetCustomerProfile({ ...common, guid: 'guid-1', phone: '13800000000' });
  370. assert.strictEqual(profile.status, 'ok');
  371. assert.strictEqual(profile.summary.hasPortrait, false);
  372. assert.strictEqual(profile.data.contactList.length, 1);
  373. assert.strictEqual(profile.data.rooms.length, 2);
  374. assertProviderHidden(profile);
  375. console.log('[ok] customer-ops tools check friend status and query customer profile');
  376. const groupMappingFile = path.join(__dirname, '..', 'outputs', 'groups', 'confirmed-mapping.json');
  377. const keywordConfigFile = path.join(__dirname, '..', 'outputs', 'groups', 'customer-keywords.json');
  378. const rejectedMappingFile = path.join(__dirname, '..', 'outputs', 'groups', 'rejected-mapping.json');
  379. try { fs.unlinkSync(groupMappingFile); } catch {}
  380. try { fs.unlinkSync(keywordConfigFile); } catch {}
  381. try { fs.unlinkSync(rejectedMappingFile); } catch {}
  382. const configured = await qiweiConfigureGroupKeywords({ reset: true });
  383. assert.strictEqual(configured.status, 'ok');
  384. assert.strictEqual(configured.summary.matchMode, 'threshold');
  385. assert(configured.summary.keywordCount >= 10);
  386. const synced = await qiweiSyncExternalGroups({ ...common, guid: 'guid-1', scope: 'all' });
  387. assert.strictEqual(synced.status, 'ok');
  388. assert.strictEqual(synced.summary.scanned, 3);
  389. assert.strictEqual(synced.summary.selfCount, 2);
  390. assert.strictEqual(synced.summary.sessionCount, 1);
  391. assert.strictEqual(synced.summary.autoConfirmed, 1);
  392. assert.strictEqual(synced.summary.suggested, 1);
  393. assert.strictEqual(synced.summary.imported, 1);
  394. assertProviderHidden(synced);
  395. const listed = await qiweiListExternalGroups({});
  396. assert.strictEqual(listed.status, 'ok');
  397. assert.strictEqual(listed.summary.total, 3);
  398. assert.strictEqual(listed.summary.autoConfirmed, 1);
  399. assert.strictEqual(listed.summary.suggested, 1);
  400. assert.strictEqual(listed.summary.imported, 1);
  401. assertProviderHidden(listed);
  402. const suggestedOnly = await qiweiListExternalGroups({ status: 'SUGGESTED' });
  403. assert.strictEqual(suggestedOnly.status, 'ok');
  404. assert.strictEqual(suggestedOnly.summary.total, 1);
  405. assert.strictEqual(suggestedOnly.data.groups[0].roomId, 'r-3');
  406. const rejected = await qiweiRejectExternalGroup({ roomId: 'r-2', reason: '测试拒绝' });
  407. assert.strictEqual(rejected.status, 'ok');
  408. assert.strictEqual(rejected.summary.reviewStatus, 'REJECTED');
  409. const listedWithRejected = await qiweiListExternalGroups({ includeRejected: true });
  410. assert.strictEqual(listedWithRejected.status, 'ok');
  411. assert.strictEqual(listedWithRejected.summary.rejected, 1);
  412. const analyzed = await qiweiAnalyzeGroupMembers({ ...common, guid: 'guid-1', roomIds: ['r-1', 'r-3'], previewMessages: 5, updateSnapshot: true });
  413. assert.strictEqual(analyzed.status, 'ok');
  414. assert.strictEqual(analyzed.summary.analyzed, 2);
  415. assert.strictEqual(analyzed.summary.autoConfirmed, 1);
  416. assertProviderHidden(analyzed);
  417. const confirmed = await qiweiConfirmExternalGroup({ roomId: 'r-1', externalUserId: 'wx-u-1', customerName: '张三' });
  418. assert.strictEqual(confirmed.status, 'ok');
  419. assert.strictEqual(confirmed.summary.reviewStatus, 'CONFIRMED');
  420. const added = await qiweiAddExternalGroup({ roomId: 'r-99', roomName: '手动群', externalUserId: 'wx-u-2' });
  421. assert.strictEqual(added.status, 'ok');
  422. const messages = await qiweiSyncGroupMessages({ ...common, guid: 'guid-1', roomIds: ['r-1'] });
  423. assert.strictEqual(messages.status, 'ok');
  424. assert.strictEqual(messages.summary.totalNew, 1);
  425. assertProviderHidden(messages);
  426. console.log('[ok] group-management tools sync, list, analyze, configure, reject, confirm and sync messages');
  427. const scanManifestFile = path.join(__dirname, '..', 'outputs', 'groups', 'group-scan-manifest.json');
  428. assert(fs.existsSync(scanManifestFile), 'group-scan-manifest.json should exist');
  429. assert(fs.existsSync(keywordConfigFile), 'customer-keywords.json should exist');
  430. assert(fs.existsSync(rejectedMappingFile), 'rejected-mapping.json should exist');
  431. const messageFile = path.join(__dirname, '..', 'outputs', 'messages', 'room-r1-test-portrait.json');
  432. fs.mkdirSync(path.dirname(messageFile), { recursive: true });
  433. fs.writeFileSync(messageFile, JSON.stringify([
  434. { fromRoomId: 'r1', seq: 2, senderId: 'wx-u-1', senderName: '张三', msgType: 1, content: '我想买房,预算200万', msgUniqueIdentifier: 'm-2', timestamp: Math.floor(Date.now() / 1000) }
  435. ]));
  436. const prepared = await qiweiPrepareCustomerPortrait({ externalUserId: 'wx-u-1', roomIds: ['r1'] });
  437. assert.strictEqual(prepared.status, 'ok');
  438. assert.strictEqual(prepared.summary.messageCount, 1);
  439. const updated = await qiweiUpdateCustomerPortrait({ externalUserId: 'wx-u-1', aiMode: 'keyword', roomIds: ['r1'] });
  440. assert.strictEqual(updated.status, 'ok');
  441. assert.strictEqual(updated.summary.fields.length > 0, true);
  442. const saved = await qiweiSaveCustomerPortrait({ externalUserId: 'wx-u-1', portrait: { intent: '购房', budgetRange: '200万' } });
  443. assert.strictEqual(saved.status, 'ok');
  444. const tagsAdded = await qiweiAddCustomerTags({ externalUserId: 'wx-u-1', tags: ['高意向', '购房'] });
  445. assert.strictEqual(tagsAdded.status, 'ok');
  446. assert.strictEqual(tagsAdded.summary.total, 2);
  447. const tagsListed = await qiweiListCustomerTags({ externalUserId: 'wx-u-1' });
  448. assert.strictEqual(tagsListed.status, 'ok');
  449. assert.strictEqual(tagsListed.data.tags.includes('高意向'), true);
  450. const labels = await qiweiSyncPersonalLabels({ ...common, guid: 'guid-1' });
  451. assert.strictEqual(labels.status, 'ok');
  452. assert.strictEqual(labels.summary.count, 1);
  453. const labelCreated = await qiweiCreatePersonalLabel({ ...common, guid: 'guid-1', labelName: '测试标签' });
  454. assert.strictEqual(labelCreated.status, 'ok');
  455. assertProviderHidden(labelCreated);
  456. try { fs.unlinkSync(messageFile); } catch {}
  457. console.log('[ok] portrait-tags tools prepare, update, save portrait and manage tags/labels');
  458. const brokerMessageFile = path.join(__dirname, '..', 'outputs', 'messages', 'room-r2-test-playbook.json');
  459. fs.writeFileSync(brokerMessageFile, JSON.stringify([
  460. { fromRoomId: 'r2', seq: 1, senderId: 'broker-1', senderName: '顾问', msgType: 1, content: '您好,这个户型很适合您,预算也匹配', msgUniqueIdentifier: 'bm-1', timestamp: Math.floor(Date.now() / 1000) }
  461. ]));
  462. const brokerPrepared = await qiweiPrepareBrokerPlaybook({ brokerUserId: 'broker-1', roomIds: ['r2'] });
  463. assert.strictEqual(brokerPrepared.status, 'ok');
  464. assert.strictEqual(brokerPrepared.summary.messageCount, 1);
  465. const brokerDistilled = await qiweiDistillBroker({ brokerUserId: 'broker-1', aiMode: 'keyword', roomIds: ['r2'] });
  466. assert.strictEqual(brokerDistilled.status, 'ok');
  467. assert.strictEqual(brokerDistilled.summary.dimensions.length > 0, true);
  468. const brokerSaved = await qiweiSaveBrokerPlaybook({ brokerUserId: 'broker-1', playbook: { '客户分层': '精准匹配', '开场白': '您好' } });
  469. assert.strictEqual(brokerSaved.status, 'ok');
  470. const brokerGot = await qiweiGetBrokerPlaybook({ brokerUserId: 'broker-1' });
  471. assert.strictEqual(brokerGot.status, 'ok');
  472. assert.strictEqual(brokerGot.data.brokerUserId, 'broker-1');
  473. try { fs.unlinkSync(brokerMessageFile); } catch {}
  474. console.log('[ok] broker-playbook tools prepare, distill, save and get playbook');
  475. const preview = await qiweiPreviewTransferPackage({ fromUserId: 'old-broker', toUserId: 'new-broker', roomIds: ['r-1'] });
  476. assert.strictEqual(preview.status, 'ok');
  477. assert.strictEqual(preview.summary.itemCount, 1);
  478. const executed = await qiweiExecuteTransfer({ ...common, guid: 'guid-1', removeOldBroker: true });
  479. assert.strictEqual(executed.status, 'ok');
  480. assert.strictEqual(executed.summary.success, 1);
  481. assertProviderHidden(executed);
  482. console.log('[ok] customer-transfer tools preview and execute transfer');
  483. const wavHeader = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAA=='; // minimal WAV header-ish base64
  484. const voice = await qiweiTranscribeVoice({ base64Audio: wavHeader, transcribe: false });
  485. assert.strictEqual(voice.status, 'ok');
  486. assert.strictEqual(voice.summary.sourceExt, 'wav');
  487. console.log('[ok] voice tool saves audio file');
  488. const statusBefore = await qiweiWebhookStatus({});
  489. assert.strictEqual(statusBefore.status, 'ok');
  490. assert.strictEqual(statusBefore.summary.running, false);
  491. const started = await qiweiWebhookServerStart({ port: 0 });
  492. assert.strictEqual(started.status, 'ok');
  493. assert.strictEqual(typeof started.summary.port, 'number');
  494. const discovered = await qiweiWebhookDiscover({});
  495. assert.strictEqual(discovered.status, 'ok');
  496. assert.strictEqual(discovered.summary.running, true);
  497. const autoSetup = await qiweiWebhookAutoSetup({ ...common, guid: 'guid-1' });
  498. assert.strictEqual(autoSetup.status, 'ok');
  499. assert.strictEqual(gateway.requests.at(-1).body.method, '/client/setCallback');
  500. const setCallbackParams = gateway.requests.at(-1).body.params;
  501. assert.strictEqual(setCallbackParams.callbackUrl.startsWith('http://127.0.0.1:'), true);
  502. assert.strictEqual(setCallbackParams.authSecret && setCallbackParams.authSecret.length > 0, true);
  503. assert.strictEqual(setCallbackParams.guid, undefined);
  504. assertProviderHidden(autoSetup);
  505. const localUrl = discovered.data.localUrl;
  506. const callbackRes = await fetch(localUrl, {
  507. method: 'POST',
  508. headers: { 'Content-Type': 'application/json' },
  509. body: JSON.stringify({ msgType: 1, content: 'test', fromRoomId: 'r-1' })
  510. });
  511. assert.strictEqual(callbackRes.status, 200);
  512. const stopped = await qiweiWebhookServerStop({});
  513. assert.strictEqual(stopped.status, 'ok');
  514. assert.strictEqual(stopped.data.stopped, true);
  515. console.log('[ok] webhook-relay tools start server, auto-setup and ingest callback');
  516. for (const request of gateway.requests) {
  517. assert.strictEqual(request.authorization, 'Bearer sk-test-relay-token');
  518. }
  519. console.log(`[ok] ${gateway.requests.length} mock Fmode gateway requests authenticated`);
  520. } finally {
  521. await gateway.close();
  522. }
  523. stopLoginFlowServer();
  524. stopLoginFallbackServer();
  525. console.log('\nAll smoke tests passed.');
  526. }
  527. main().catch(error => {
  528. console.error('smoke test failed:', error);
  529. process.exit(1);
  530. });