#!/usr/bin/env node const assert = require('assert'); const http = require('http'); const fs = require('fs'); const os = require('os'); const path = require('path'); const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-main-smoke-')); process.env.QIWEI_OUTPUTS_DIR = smokeRoot; const { outputsRoot } = require('../mcp/src/core/output-paths'); const { loadCatalog, searchEndpoints, findEndpoint } = require('../mcp/src/core/api-catalog'); const { searchQiweiApis, getQiweiApiDoc, callQiweiApi } = require('../mcp/src/tools/qiwei-api-catalog-run'); const { qiweiLoginStatus, qiweiLoginStart, qiweiLoginCheck, qiweiLoginVerify } = require('../mcp/src/tools/qiwei-login-run'); const { stopLoginFlowServer } = require('../mcp/src/core/login-flow-server'); const { stopLoginFallbackServer } = require('../mcp/src/core/login-fallback-server'); const { qiweiSubscriptionStatus, qiweiSubscribe, qiweiSubscriptionAutoRenew } = require('../mcp/src/tools/qiwei-subscription-run'); const { qiweiCheckFriendStatus, qiweiGetCustomerProfile } = require('../mcp/src/tools/qiwei-customer-ops-run'); const { qiweiSyncExternalGroups, qiweiListExternalGroups, qiweiAnalyzeGroupMembers, qiweiConfirmExternalGroup, qiweiAddExternalGroup, qiweiConfigureGroupKeywords, qiweiRejectExternalGroup } = require('../mcp/src/tools/qiwei-group-management-run'); const { qiweiPrepareCustomerPortrait, qiweiUpdateCustomerPortrait, qiweiSaveCustomerPortrait, qiweiAddCustomerTags, qiweiListCustomerTags, qiweiSyncPersonalLabels, qiweiCreatePersonalLabel } = require('../mcp/src/tools/qiwei-portrait-tags-run'); const { qiweiDistillBroker, qiweiPrepareBrokerPlaybook, qiweiSaveBrokerPlaybook, qiweiGetBrokerPlaybook } = require('../mcp/src/tools/qiwei-broker-playbook-run'); const { qiweiPreviewTransferPackage, qiweiExecuteTransfer } = require('../mcp/src/tools/qiwei-customer-transfer-run'); const { qiweiTranscribeVoice } = require('../mcp/src/tools/qiwei-voice-run'); const { qiweiWebhookStatus, qiweiWebhookAutoSetup, connectRelayThroughFmode } = require('../mcp/src/tools/qiwei-webhook-relay-run'); const { getProductMode } = require('../mcp/src/core/product-mode'); const { qiweiProductModeStatus } = require('../mcp/src/tools/qiwei-product-mode-run'); const PRIVATE_PROVIDER_NAME = ['private', 'provider'].join('-'); const PRIVATE_PROVIDER_HOST = ['private', 'provider', 'invalid'].join('.'); const PRIVATE_CREDENTIAL_KEY = ['token', 'Id'].join(''); function assertProviderHidden(value) { const output = JSON.stringify(value); assert(!output.includes(PRIVATE_PROVIDER_NAME)); assert(!output.includes(PRIVATE_PROVIDER_HOST)); assert(!output.includes('server-only-token')); } function readBody(req) { return new Promise(resolve => { let body = ''; req.setEncoding('utf8'); req.on('data', chunk => { body += chunk; }); req.on('end', () => resolve(body ? JSON.parse(body) : undefined)); }); } async function startMockGateway({ subscribed = true } = {}) { const requests = []; let subscriptionActive = subscribed; const server = http.createServer(async (req, res) => { const body = await readBody(req); const url = new URL(req.url, 'http://localhost'); requests.push({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()), authorization: req.headers.authorization, body }); let data; if (url.pathname === '/doApi') { if (body.method === '/contact/searchContact') { data = { searchStatus: 2, wxUserInfo: { userId: 'wx-u-1', nickname: '测试客户' }, workUserInfo: null }; } else if (body.method === '/contact/getWxContactList') { data = { currentSeq: 0, contactCount: 1, hasMore: false, contactList: [{ userId: 'wx-u-1', nickname: '测试客户' }] }; } else if (body.method === '/room/getRoomList') { data = { hasMore: false, nextStartIndex: 0, roomCount: 2, roomList: [ { roomId: 'r-1', roomName: '张三买房服务群', roomMemberCount: 3 }, { roomId: 'r-2', roomName: 'random-group', roomMemberCount: 2 } ] }; } else if (body.method === '/session/getSessionPage') { data = { hasMore: false, currentSeq: 0, sessionCount: 1, sessionList: [ { roomId: 'r-3', sessionName: '潜在VIP群', sessionType: 1, roomMemberCount: 4 } ] }; } else if (body.method === '/room/batchGetRoomDetail') { data = { roomList: (body.params.roomIdList || []).map(roomId => ({ roomId, roomName: roomId === 'r-1' ? '张三买房服务群' : '其他群', roomMemberCount: roomId === 'r-1' ? 3 : 2 })) }; } else if (body.method === '/label/syncLabelList') { data = { currentSeq: 0, hasMore: false, labelCount: 1, labelList: [{ labelId: 'l-1', labelName: '高意向' }] }; } else if (body.method === '/label/editLabel') { data = { success: true }; } else if (body.method === '/label/contactEditLabel') { data = { success: true }; } else { data = { nickname: '测试账号', echoedMethod: body.method, guid: 'server-only-guid', [PRIVATE_CREDENTIAL_KEY]: 'server-only-token', provider: PRIVATE_PROVIDER_NAME, providerName: PRIVATE_PROVIDER_NAME, providerHost: PRIVATE_PROVIDER_HOST, supportUrl: `https://${PRIVATE_PROVIDER_HOST}/help`, diagnostic: `See https://${PRIVATE_PROVIDER_HOST}/diagnostic` }; } } else if (url.pathname === '/login/status') { data = { configured: true, online: true, statusCode: 2 }; } else if (url.pathname === '/login/start') { data = { uid: body.uid, guid: 'server-only-guid', loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') }; } else if (url.pathname === '/login/check') { data = { uid: body.uid, guid: 'server-only-guid', status: 2, detail: { nickname: '测试账号', userId: 'u-1', guid: 'server-only-guid' } }; } else if (url.pathname === '/login/verify') { data = { accepted: true }; } else if (url.pathname === '/subscribe/status') { data = { subscribed: subscriptionActive, price: 500, seats: 2, usedSeats: 1, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true, balance: 1000 }; } else if (url.pathname === '/subscribe') { subscriptionActive = true; data = { seats: body.seats || 1, months: body.months || 1, amount: (body.seats || 1) * (body.months || 1) * 500, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true }; } else if (url.pathname === '/subscribe/autoRenew') { data = { autoRenew: body.autoRenew }; } else if (url.pathname === '/relay/connect') { data = { connected: true, uid: body.uid }; } else if (url.pathname === '/product/enterprise/quote') { data = { sku: 'qiwei-enterprise-relay', amount: 0, currency: 'CNY', billingCycle: 'month', pricingStatus: 'placeholder', chargeOnQuote: false }; } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 404, mess: 'not found' })); return; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 200, data })); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); return { requests, apiBase: `http://127.0.0.1:${address.port}`, close: () => new Promise(resolve => server.close(resolve)) }; } async function startMockVerifyGateway() { const requests = []; let verified = false; const server = http.createServer(async (req, res) => { const body = await readBody(req); const url = new URL(req.url, 'http://localhost'); requests.push({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()), authorization: req.headers.authorization, body }); let data; if (url.pathname === '/login/status') { data = { configured: true, online: false, statusCode: -1 }; } else if (url.pathname === '/login/start') { data = { uid: body.uid, guid: 'server-only-guid', loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') }; } else if (url.pathname === '/login/check') { if (verified) { data = { uid: body.uid, status: 2, detail: { nickname: '测试账号', userId: 'u-1', guid: 'server-only-guid' } }; } else { data = { uid: body.uid, status: 10, detail: {} }; } } else if (url.pathname === '/login/verify') { verified = true; data = { accepted: true }; } else if (url.pathname === '/subscribe/status') { data = { subscribed: true, seats: 2, usedSeats: 1, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true, balance: 1000 }; } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 404, mess: 'not found' })); return; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 200, data })); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); return { requests, apiBase: `http://127.0.0.1:${address.port}`, close: () => new Promise(resolve => server.close(resolve)) }; } async function main() { const catalog = loadCatalog(); assert(catalog.endpoints.length >= 100, 'catalog should contain 100+ endpoints'); assert(catalog.gateway.baseUrl.includes('server.fmode.cn/api/qiwei')); assert(catalog.endpoints.every(endpoint => !Object.hasOwn(endpoint, 'docUrl'))); assertProviderHidden(catalog); console.log(`[ok] catalog loaded: ${catalog.endpoints.length} endpoints`); const hits = searchEndpoints({ query: '发送 文本' }); assert(hits.length > 0 && hits[0].module === 'msg'); assert(findEndpoint('/login/getLoginQrcode')); const searchResult = await searchQiweiApis({ query: '朋友圈' }); assert(searchResult.status === 'ok' && searchResult.data.endpoints.length > 0); assertProviderHidden(searchResult); const doc = await getQiweiApiDoc({ id: 'user.getProfile' }); assert(doc.status === 'ok' && doc.data.callTemplate); assertProviderHidden(doc); assert(!JSON.stringify(doc).includes('"guid"')); console.log('[ok] catalog search and docs'); const gateway = await startMockGateway(); const common = { authToken: 'sk-test-relay-token', uid: 'qiwei-smoke-device', apiBase: gateway.apiBase, productMode: 'personal', persistConfig: false }; try { const productStatus = await qiweiProductModeStatus(common); assert.strictEqual(productStatus.status, 'ok'); assert.strictEqual(productStatus.summary.mode, 'personal'); assert.strictEqual(productStatus.summary.enterpriseUpgrade.amount, 0); assert.strictEqual(productStatus.summary.enterpriseUpgrade.source, 'server'); const quoteRequest = gateway.requests.at(-1); assert.strictEqual(quoteRequest.path, '/product/enterprise/quote'); assert.strictEqual(quoteRequest.method, 'GET'); assert.strictEqual(quoteRequest.authorization, 'Bearer sk-test-relay-token'); assert.strictEqual(quoteRequest.body, undefined); console.log('[ok] product mode uses the server enterprise quote without charging'); const relayConnection = await connectRelayThroughFmode(common); assert.strictEqual(relayConnection.connected, true); const relayConnectRequest = gateway.requests.at(-1); assert.strictEqual(relayConnectRequest.path, '/relay/connect'); assert.strictEqual(relayConnectRequest.authorization, 'Bearer sk-test-relay-token'); assert.deepStrictEqual(relayConnectRequest.body, { uid: 'qiwei-smoke-device' }); assert(!JSON.stringify(relayConnectRequest.body).match(/callback|secret|guid/i)); console.log('[ok] relay connection uses the dedicated server endpoint without client callback credentials'); const call = await callQiweiApi({ ...common, id: 'user.getProfile', params: {} }); assert.strictEqual(call.status, 'ok'); const apiRequest = gateway.requests.at(-1); assert.strictEqual(apiRequest.path, '/doApi'); assert.strictEqual(apiRequest.authorization, 'Bearer sk-test-relay-token'); assert.deepStrictEqual(apiRequest.body, { uid: 'qiwei-smoke-device', method: '/user/getProfile', params: {} }); assert(!JSON.stringify(apiRequest.body).includes('tokenId')); assert(!JSON.stringify(apiRequest.body).includes('guid')); assertProviderHidden(call); console.log('[ok] qiwei_api_call uses the Fmode gateway envelope without private credentials'); const blocked = await callQiweiApi({ ...common, rawMethod: '/login/manualLogin', params: {} }); assert.strictEqual(blocked.status, 'error'); assert.strictEqual(gateway.requests.at(-1), apiRequest); const subscription = await qiweiSubscriptionStatus(common); assert.strictEqual(subscription.status, 'ok'); assert.strictEqual(subscription.summary.subscribed, true); const subscribed = await qiweiSubscribe({ ...common, seats: 3, months: 6 }); assert.strictEqual(subscribed.status, 'ok'); assert.strictEqual(gateway.requests.at(-1).body.seats, 3); assert.strictEqual(gateway.requests.at(-1).body.months, 6); const autoRenew = await qiweiSubscriptionAutoRenew({ ...common, autoRenew: false }); assert.strictEqual(autoRenew.status, 'ok'); assert.strictEqual(gateway.requests.at(-1).body.autoRenew, false); console.log('[ok] subscription tools use Fmode gateway routes'); const status = await qiweiLoginStatus(common); assert.strictEqual(status.status, 'ok'); assert.strictEqual(gateway.requests.at(-1).query.uid, common.uid); const start = await qiweiLoginStart({ ...common, openBrowser: false }); assert.strictEqual(start.status, 'ok'); assertProviderHidden(start); const check = await qiweiLoginCheck(common); assert.strictEqual(check.status, 'ok'); assert.strictEqual(check.summary.loggedIn, true); assertProviderHidden(check); const verify = await qiweiLoginVerify({ ...common, code: '123456' }); assert.strictEqual(verify.status, 'ok'); assert.deepStrictEqual(gateway.requests.at(-1).body, { uid: common.uid, code: '123456' }); console.log('[ok] login tools use uid-based Fmode gateway routes and hide internal identifiers'); stopLoginFlowServer(); const checkoutGateway = await startMockGateway({ subscribed: false }); try { const checkoutStart = await qiweiLoginStart({ ...common, apiBase: checkoutGateway.apiBase, flowPort: 4312, openBrowser: false }); assert.strictEqual(checkoutStart.status, 'ok'); const checkoutUrl = checkoutStart.summary.flowUrl; const checkoutPageRes = await fetch(checkoutUrl); assert.strictEqual(checkoutPageRes.status, 200); const checkoutPage = await checkoutPageRes.text(); assert.ok(!checkoutPage.includes(common.authToken), 'dynamic checkout page must not expose the gateway token'); assert.ok(!checkoutPage.includes(checkoutGateway.apiBase), 'dynamic checkout page must not expose the gateway URL'); const tokenMatch = checkoutPage.match(/const CHECKOUT_TOKEN = "([^"]+)";/); assert.ok(tokenMatch && tokenMatch[1], 'dynamic checkout page should carry a local one-time confirmation'); const checkoutState = await fetch(`${checkoutUrl}flow/state`).then(response => response.json()); assert.strictEqual(checkoutState.monthlyPrice, 500); const checkoutBody = { seats: 3, months: 6, idempotencyKey: 'flow-smoke-order-1', checkoutToken: tokenMatch[1] }; const rejectedCheckout = await fetch(`${checkoutUrl}flow/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(checkoutBody) }); assert.strictEqual(rejectedCheckout.status, 403); const acceptedCheckout = await fetch(`${checkoutUrl}flow/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: new URL(checkoutUrl).origin }, body: JSON.stringify(checkoutBody) }); assert.strictEqual(acceptedCheckout.status, 200); assert.deepStrictEqual(checkoutGateway.requests.at(-1).body, { seats: 3, months: 6, idempotencyKey: 'flow-smoke-order-1' }); const replayedCheckout = await fetch(`${checkoutUrl}flow/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: new URL(checkoutUrl).origin }, body: JSON.stringify(checkoutBody) }); assert.strictEqual(replayedCheckout.status, 403); console.log('[ok] dynamic checkout hides gateway credentials, submits months once and rejects cross-origin/replayed payment'); } finally { stopLoginFlowServer(); await checkoutGateway.close(); } const verifyGateway = await startMockVerifyGateway(); try { const verifyCommon = { authToken: 'sk-test-relay-token', uid: 'qiwei-verify-device', apiBase: verifyGateway.apiBase, persistConfig: false }; const fallbackStart = await qiweiLoginStart({ ...verifyCommon, flowUi: false, openBrowser: false }); assert.strictEqual(fallbackStart.status, 'ok'); assert.strictEqual(fallbackStart.summary.fallbackServer, true); assert.ok(fallbackStart.summary.flowUrl.startsWith('http://127.0.0.1:'), 'fallback server should return a local URL'); assertProviderHidden(fallbackStart); const fallbackUrl = fallbackStart.summary.flowUrl; const pageRes = await fetch(fallbackUrl); assert.strictEqual(pageRes.status, 200); const pageHtml = await pageRes.text(); assert.ok(pageHtml.includes('企业微信扫码登录')); assert.ok(!pageHtml.includes('sk-test-relay-token'), 'fallback page must not expose token'); const qrRes = await fetch(`${fallbackUrl}flow/qrcode`); assert.strictEqual(qrRes.status, 200); assert.strictEqual(qrRes.headers.get('content-type'), 'image/png'); const qrBody = await qrRes.arrayBuffer(); assert.deepStrictEqual(Buffer.from(qrBody), Buffer.from('mock-png')); const checkRes = await fetch(`${fallbackUrl}flow/check`, { method: 'POST' }); assert.strictEqual(checkRes.status, 200); const checkJson = await checkRes.json(); assert.strictEqual(checkJson.status, 10); const verifyRes = await fetch(`${fallbackUrl}flow/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: '123456' }) }); assert.strictEqual(verifyRes.status, 200); const verifyJson = await verifyRes.json(); assert.strictEqual(verifyJson.ok, true); const lastVerifyRequest = verifyGateway.requests.at(-1); assert.strictEqual(lastVerifyRequest.path, '/login/verify'); assert.deepStrictEqual(lastVerifyRequest.body, { uid: verifyCommon.uid, code: '123456' }); const checkAfterRes = await fetch(`${fallbackUrl}flow/check`, { method: 'POST' }); assert.strictEqual(checkAfterRes.status, 200); const checkAfterJson = await checkAfterRes.json(); assert.strictEqual(checkAfterJson.status, 2); assert.strictEqual(checkAfterJson.detail.userId, 'u-1'); console.log('[ok] fallback login server serves QR, polls status and accepts verify code'); } finally { await verifyGateway.close(); stopLoginFallbackServer(); } const friendStatus = await qiweiCheckFriendStatus({ ...common, guid: 'guid-1', phones: ['13800000000'] }); assert.strictEqual(friendStatus.status, 'ok'); assert.strictEqual(friendStatus.summary.confirmed, 1); assert.strictEqual(friendStatus.data.details[0].statusText, 'already_friend'); assertProviderHidden(friendStatus); const portraitFile = path.join(outputsRoot(), 'portraits', 'wx-u-1.json'); const tagFile = path.join(outputsRoot(), 'tags', 'wx-u-1.json'); try { fs.unlinkSync(portraitFile); } catch {} try { fs.unlinkSync(tagFile); } catch {} const profile = await qiweiGetCustomerProfile({ ...common, guid: 'guid-1', phone: '13800000000' }); assert.strictEqual(profile.status, 'ok'); assert.strictEqual(profile.summary.hasPortrait, false); assert.strictEqual(profile.data.contactList.length, 1); assert.strictEqual(profile.data.rooms.length, 2); assertProviderHidden(profile); console.log('[ok] customer-ops tools check friend status and query customer profile'); const groupMappingFile = path.join(outputsRoot(), 'groups', 'confirmed-mapping.json'); const keywordConfigFile = path.join(outputsRoot(), 'groups', 'customer-keywords.json'); const rejectedMappingFile = path.join(outputsRoot(), 'groups', 'rejected-mapping.json'); try { fs.unlinkSync(groupMappingFile); } catch {} try { fs.unlinkSync(keywordConfigFile); } catch {} try { fs.unlinkSync(rejectedMappingFile); } catch {} const configured = await qiweiConfigureGroupKeywords({ reset: true }); assert.strictEqual(configured.status, 'ok'); assert.strictEqual(configured.summary.matchMode, 'threshold'); assert(configured.summary.keywordCount >= 10); const synced = await qiweiSyncExternalGroups({ ...common, guid: 'guid-1', scope: 'all' }); assert.strictEqual(synced.status, 'ok'); assert.strictEqual(synced.summary.scanned, 3); assert.strictEqual(synced.summary.selfCount, 2); assert.strictEqual(synced.summary.sessionCount, 1); assert.strictEqual(synced.summary.autoConfirmed, 1); assert.strictEqual(synced.summary.suggested, 1); assert.strictEqual(synced.summary.imported, 1); assertProviderHidden(synced); const listed = await qiweiListExternalGroups({}); assert.strictEqual(listed.status, 'ok'); assert.strictEqual(listed.summary.total, 3); assert.strictEqual(listed.summary.autoConfirmed, 1); assert.strictEqual(listed.summary.suggested, 1); assert.strictEqual(listed.summary.imported, 1); assertProviderHidden(listed); const suggestedOnly = await qiweiListExternalGroups({ status: 'SUGGESTED' }); assert.strictEqual(suggestedOnly.status, 'ok'); assert.strictEqual(suggestedOnly.summary.total, 1); assert.strictEqual(suggestedOnly.data.groups[0].roomId, 'r-3'); const rejected = await qiweiRejectExternalGroup({ roomId: 'r-2', reason: '测试拒绝' }); assert.strictEqual(rejected.status, 'ok'); assert.strictEqual(rejected.summary.reviewStatus, 'REJECTED'); const listedWithRejected = await qiweiListExternalGroups({ includeRejected: true }); assert.strictEqual(listedWithRejected.status, 'ok'); assert.strictEqual(listedWithRejected.summary.rejected, 1); const analyzed = await qiweiAnalyzeGroupMembers({ ...common, guid: 'guid-1', roomIds: ['r-1', 'r-3'], updateSnapshot: true }); assert.strictEqual(analyzed.status, 'ok'); assert.strictEqual(analyzed.summary.analyzed, 2); assert.strictEqual(analyzed.summary.autoConfirmed, 1); assertProviderHidden(analyzed); const confirmed = await qiweiConfirmExternalGroup({ roomId: 'r-1', externalUserId: 'wx-u-1', customerName: '张三' }); assert.strictEqual(confirmed.status, 'ok'); assert.strictEqual(confirmed.summary.reviewStatus, 'CONFIRMED'); const added = await qiweiAddExternalGroup({ roomId: 'r-99', roomName: '手动群', externalUserId: 'wx-u-2' }); assert.strictEqual(added.status, 'ok'); assert.strictEqual(gateway.requests.some(request => request.body?.method === '/msg/syncMsg'), false, '群管理流程不应拉取历史消息'); console.log('[ok] group-management tools scan, list, analyze, configure, reject and confirm without history sync'); const scanManifestFile = path.join(outputsRoot(), 'groups', 'group-scan-manifest.json'); assert(fs.existsSync(scanManifestFile), 'group-scan-manifest.json should exist'); assert(fs.existsSync(keywordConfigFile), 'customer-keywords.json should exist'); assert(fs.existsSync(rejectedMappingFile), 'rejected-mapping.json should exist'); const messageFile = path.join(outputsRoot(), 'messages', 'room-r1-test-portrait.json'); fs.mkdirSync(path.dirname(messageFile), { recursive: true }); fs.writeFileSync(messageFile, JSON.stringify([ { fromRoomId: 'r1', seq: 2, senderId: 'wx-u-1', senderName: '张三', msgType: 1, content: '我想买房,预算200万', msgUniqueIdentifier: 'm-2', timestamp: Math.floor(Date.now() / 1000) } ])); const prepared = await qiweiPrepareCustomerPortrait({ externalUserId: 'wx-u-1', roomIds: ['r1'] }); assert.strictEqual(prepared.status, 'ok'); assert.strictEqual(prepared.summary.messageCount, 1); const updated = await qiweiUpdateCustomerPortrait({ externalUserId: 'wx-u-1', aiMode: 'keyword', updateMode: 'full', roomIds: ['r1'] }); assert.strictEqual(updated.status, 'ok'); assert.strictEqual(updated.summary.fields.length > 0, true); assert.strictEqual(updated.summary.updateMode, 'full'); fs.writeFileSync(messageFile, JSON.stringify([ { fromRoomId: 'r1', seq: 2, senderId: 'wx-u-1', senderName: '张三', msgType: 1, content: '我想买房,预算200万', msgUniqueIdentifier: 'm-2', timestamp: Math.floor(Date.now() / 1000) }, { fromRoomId: 'r1', seq: 3, senderId: 'wx-u-1', senderName: '张三', msgType: 1, content: '还想看看学区房', msgUniqueIdentifier: 'm-3', timestamp: Math.floor(Date.now() / 1000) + 1 } ])); const incremental = await qiweiUpdateCustomerPortrait({ externalUserId: 'wx-u-1', aiMode: 'keyword', updateMode: 'incremental', roomIds: ['r1'] }); assert.strictEqual(incremental.status, 'ok'); assert.strictEqual(incremental.summary.incrementalMessageCount, 1); assert(incremental.data.portrait.budgetRange, '增量更新应保留既有预算画像'); assert(incremental.data.portrait.keyConcerns, '增量更新应合并新增关注点'); const noChanges = await qiweiUpdateCustomerPortrait({ externalUserId: 'wx-u-1', aiMode: 'keyword', updateMode: 'incremental', roomIds: ['r1'] }); assert.strictEqual(noChanges.status, 'ok'); assert.strictEqual(noChanges.summary.skipped, true); const saved = await qiweiSaveCustomerPortrait({ externalUserId: 'wx-u-1', portrait: { intent: '购房', budgetRange: '200万' } }); assert.strictEqual(saved.status, 'ok'); const tagsAdded = await qiweiAddCustomerTags({ externalUserId: 'wx-u-1', tags: ['高意向', '购房'] }); assert.strictEqual(tagsAdded.status, 'ok'); assert.strictEqual(tagsAdded.summary.total, 2); const tagsListed = await qiweiListCustomerTags({ externalUserId: 'wx-u-1' }); assert.strictEqual(tagsListed.status, 'ok'); assert.strictEqual(tagsListed.data.tags.includes('高意向'), true); const labels = await qiweiSyncPersonalLabels({ ...common, guid: 'guid-1' }); assert.strictEqual(labels.status, 'ok'); assert.strictEqual(labels.summary.count, 1); const labelCreated = await qiweiCreatePersonalLabel({ ...common, guid: 'guid-1', labelName: '测试标签' }); assert.strictEqual(labelCreated.status, 'ok'); assertProviderHidden(labelCreated); try { fs.unlinkSync(messageFile); } catch {} console.log('[ok] portrait-tags tools prepare, update, save portrait and manage tags/labels'); const brokerMessageFile = path.join(outputsRoot(), 'messages', 'room-r2-test-playbook.json'); fs.writeFileSync(brokerMessageFile, JSON.stringify([ { fromRoomId: 'r2', seq: 1, senderId: 'broker-1', senderName: '顾问', msgType: 1, content: '您好,这个户型很适合您,预算也匹配', msgUniqueIdentifier: 'bm-1', timestamp: Math.floor(Date.now() / 1000) } ])); const brokerPrepared = await qiweiPrepareBrokerPlaybook({ brokerUserId: 'broker-1', roomIds: ['r2'] }); assert.strictEqual(brokerPrepared.status, 'ok'); assert.strictEqual(brokerPrepared.summary.messageCount, 1); const brokerDistilled = await qiweiDistillBroker({ brokerUserId: 'broker-1', aiMode: 'keyword', roomIds: ['r2'] }); assert.strictEqual(brokerDistilled.status, 'ok'); assert.strictEqual(brokerDistilled.summary.dimensions.length > 0, true); const brokerSaved = await qiweiSaveBrokerPlaybook({ brokerUserId: 'broker-1', playbook: { '客户分层': '精准匹配', '开场白': '您好' } }); assert.strictEqual(brokerSaved.status, 'ok'); const brokerGot = await qiweiGetBrokerPlaybook({ brokerUserId: 'broker-1' }); assert.strictEqual(brokerGot.status, 'ok'); assert.strictEqual(brokerGot.data.brokerUserId, 'broker-1'); try { fs.unlinkSync(brokerMessageFile); } catch {} console.log('[ok] broker-playbook tools prepare, distill, save and get playbook'); const preview = await qiweiPreviewTransferPackage({ fromUserId: 'old-broker', toUserId: 'new-broker', roomIds: ['r-1'] }); assert.strictEqual(preview.status, 'ok'); assert.strictEqual(preview.summary.itemCount, 1); const executed = await qiweiExecuteTransfer({ ...common, guid: 'guid-1', removeOldBroker: true }); assert.strictEqual(executed.status, 'ok'); assert.strictEqual(executed.summary.success, 1); assertProviderHidden(executed); console.log('[ok] customer-transfer tools preview and execute transfer'); const wavHeader = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAA=='; // minimal WAV header-ish base64 const voice = await qiweiTranscribeVoice({ base64Audio: wavHeader, transcribe: false }); assert.strictEqual(voice.status, 'ok'); assert.strictEqual(voice.summary.sourceExt, 'wav'); console.log('[ok] voice tool saves audio file'); const statusBefore = await qiweiWebhookStatus({}); assert.strictEqual(statusBefore.status, 'ok'); const currentProduct = getProductMode(); assert.strictEqual(statusBefore.summary.productMode, currentProduct.mode); assert.strictEqual(statusBefore.summary.localMode, currentProduct.mode === 'personal' ? 'enabled' : 'disabled'); if (!statusBefore.summary.relayEnabled) { const autoSetupWithoutRelay = await qiweiWebhookAutoSetup({ ...common, guid: 'guid-1' }); assert.strictEqual(autoSetupWithoutRelay.status, 'error'); assert.strictEqual(autoSetupWithoutRelay.assistantMessage.includes('个人版'), true); } console.log('[ok] webhook status reflects local mode and avoids external relay setup during smoke tests'); for (const request of gateway.requests) { assert.strictEqual(request.authorization, 'Bearer sk-test-relay-token'); } console.log(`[ok] ${gateway.requests.length} mock Fmode gateway requests authenticated`); } finally { await gateway.close(); } stopLoginFlowServer(); stopLoginFallbackServer(); console.log('\nAll smoke tests passed.'); } main() .finally(() => fs.rmSync(smokeRoot, { recursive: true, force: true })) .catch(error => { console.error('smoke test failed:', error); process.exit(1); });