feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -10,9 +10,17 @@ const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..
|
||||
const conciergePagePath = path.join(adminRoot, 'src/pages/ai/concierge/index.tsx');
|
||||
const conciergeLocalePath = path.join(adminRoot, 'src/pages/ai/concierge/locale/index.ts');
|
||||
|
||||
function readPage(): string {
|
||||
return fs.readFileSync(conciergePagePath, 'utf-8');
|
||||
}
|
||||
|
||||
function readLocale(): string {
|
||||
return fs.readFileSync(conciergeLocalePath, 'utf-8');
|
||||
}
|
||||
|
||||
test('1. ai/concierge 页面必须使用 Arco Form/Select/Slider/Switch/Card/Radio 并接入 API', () => {
|
||||
assert.ok(fs.existsSync(conciergePagePath), 'AI 顾问配置页面必须存在');
|
||||
const content = fs.readFileSync(conciergePagePath, 'utf-8');
|
||||
const content = readPage();
|
||||
|
||||
assert.ok(
|
||||
content.includes('admin/ai/concierge'),
|
||||
@@ -38,7 +46,7 @@ test('1. ai/concierge 页面必须使用 Arco Form/Select/Slider/Switch/Card/Rad
|
||||
});
|
||||
|
||||
test('2. ai/concierge 必须遵循设计 Token,禁止 emoji 与硬编码颜色,支持预设人设与阈值', () => {
|
||||
const content = fs.readFileSync(conciergePagePath, 'utf-8');
|
||||
const content = readPage();
|
||||
|
||||
assert.ok(content.includes("from '@/types/omni'"), '必须复用 admin/src/types/omni.ts');
|
||||
assert.ok(content.includes('lightColors'), '颜色必须引用 lightColors token');
|
||||
@@ -75,7 +83,7 @@ test('3. ai/concierge 路由与国际化配置必须齐备', () => {
|
||||
|
||||
// 检查语言包
|
||||
assert.ok(fs.existsSync(conciergeLocalePath), '页面语言包必须存在');
|
||||
const localeContent = fs.readFileSync(conciergeLocalePath, 'utf-8');
|
||||
const localeContent = readLocale();
|
||||
assert.ok(localeContent.includes("'en-US'"), '必须包含英文案');
|
||||
assert.ok(localeContent.includes("'zh-CN'"), '必须包含中文案');
|
||||
assert.ok(localeContent.includes("'menu.ai.concierge'"), '页面语言包必须包含 menu.ai.concierge');
|
||||
@@ -91,3 +99,116 @@ test('3. ai/concierge 路由与国际化配置必须齐备', () => {
|
||||
'src/locale/index.ts 必须包含 zh-CN menu.ai.concierge'
|
||||
);
|
||||
});
|
||||
|
||||
test('4. 保存 / 人设 / DeepSeek 等操作仅在 code===1000 且有 data 时 Message.success', () => {
|
||||
const content = readPage();
|
||||
const localeContent = readLocale();
|
||||
|
||||
assert.ok(content.includes('Message.success'), '成功时仍应使用 Message.success');
|
||||
assert.ok(content.includes('Message.error'), '失败必须使用 Message.error');
|
||||
|
||||
// 每个 Message.success 必须出现在 code === 1000 成功分支内,且附近要求 data
|
||||
const successMatches = [...content.matchAll(/Message\.success\s*\(/g)];
|
||||
assert.ok(successMatches.length > 0, '至少应有一处成功提示');
|
||||
|
||||
for (const match of successMatches) {
|
||||
const idx = match.index ?? 0;
|
||||
const windowStart = Math.max(0, idx - 360);
|
||||
const preceding = content.slice(windowStart, idx);
|
||||
assert.ok(
|
||||
/code\s*===\s*1000/.test(preceding),
|
||||
`Message.success 必须位于 code === 1000 分支内,附近片段: ${preceding.slice(-100)}`
|
||||
);
|
||||
assert.ok(
|
||||
/\.data\b|data\s*&&|&&\s*.*data/.test(preceding),
|
||||
`Message.success 前须校验 data 存在,附近片段: ${preceding.slice(-100)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 保存接口必须同时校验 code 与 data
|
||||
assert.ok(
|
||||
/saveByCardId[\s\S]{0,800}code\s*===\s*1000[\s\S]{0,120}data/.test(content) ||
|
||||
/SAVE_CONFIG_API[\s\S]{0,800}code\s*===\s*1000[\s\S]{0,120}data/.test(
|
||||
content
|
||||
),
|
||||
'保存顾问配置须 code === 1000 且有 data'
|
||||
);
|
||||
|
||||
// 人设切换仅填充表单预览,不得 Message.success 假装已落库
|
||||
assert.ok(
|
||||
content.includes('handlePersonaChange'),
|
||||
'必须存在人设切换处理'
|
||||
);
|
||||
assert.ok(
|
||||
!/handlePersonaChange[\s\S]{0,500}Message\.success/.test(content),
|
||||
'人设切换不得 Message.success 假装已保存,须经保存接口'
|
||||
);
|
||||
|
||||
// FAQ:无独立保存接口时禁止假成功,须提示暂不可用
|
||||
const hasFaqApi =
|
||||
/admin\/ai\/concierge\/(faq|saveFaq|upsertFaq)/i.test(content) ||
|
||||
/FAQ_SAVE_API|SAVE_FAQ_API/i.test(content);
|
||||
assert.ok(
|
||||
localeContent.includes("'aiConcierge.faq.unavailable'"),
|
||||
'locale 必须包含 aiConcierge.faq.unavailable'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes("t['aiConcierge.faq.unavailable']") ||
|
||||
content.includes('aiConcierge.faq.unavailable'),
|
||||
'页面必须引用 FAQ 暂不可用文案'
|
||||
);
|
||||
if (!hasFaqApi) {
|
||||
assert.ok(
|
||||
!/Message\.success[\s\S]{0,80}faq|Message\.success[\s\S]{0,80}FAQ/i.test(
|
||||
content
|
||||
),
|
||||
'无 FAQ 接口时禁止 Message.success 假装 FAQ 已保存'
|
||||
);
|
||||
assert.ok(
|
||||
/disabled[\s\S]{0,160}faq|faq[\s\S]{0,200}disabled|aiConcierge\.faq[\s\S]{0,200}disabled/i.test(
|
||||
content
|
||||
) ||
|
||||
content.includes("t['aiConcierge.faq.unavailable']"),
|
||||
'无 FAQ 接口时须禁用相关操作或提示暂不可用'
|
||||
);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
localeContent.includes("'aiConcierge.request.success'"),
|
||||
'locale 须含保存成功文案'
|
||||
);
|
||||
assert.ok(
|
||||
localeContent.includes("'aiConcierge.request.saveFail'"),
|
||||
'locale 须含保存失败文案'
|
||||
);
|
||||
});
|
||||
|
||||
test('5. 失败分支不得 Message.success,且不得本地假装已保存', () => {
|
||||
const content = readPage();
|
||||
|
||||
// catch / else 失败分支附近不得出现 Message.success
|
||||
const failBlocks = [
|
||||
...content.matchAll(
|
||||
/catch\s*(?:\([^)]*\))?\s*\{[\s\S]{0,280}\}|else\s*\{[\s\S]{0,280}\}/g
|
||||
),
|
||||
];
|
||||
for (const block of failBlocks) {
|
||||
const text = block[0];
|
||||
if (/Message\.error/.test(text) || /setLoading|setSaving|setDeepSeek/.test(text)) {
|
||||
assert.ok(
|
||||
!/Message\.success/.test(text),
|
||||
`失败分支内不得 Message.success: ${text.slice(0, 120)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存失败后不得用本地假数据冒充已保存成功提示
|
||||
assert.ok(
|
||||
!/catch[\s\S]{0,200}Message\.success/.test(content),
|
||||
'catch 失败后禁止 Message.success'
|
||||
);
|
||||
assert.ok(
|
||||
!/code\s*!==\s*1000[\s\S]{0,120}Message\.success/.test(content),
|
||||
'code 非 1000 时禁止 Message.success'
|
||||
);
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户