|
@@ -54,10 +54,81 @@ interface ApigData {
|
|
|
[key: string]: any;
|
|
[key: string]: any;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface TokenCopyProfile {
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ buttonText: string;
|
|
|
|
|
+ copiedText: string;
|
|
|
|
|
+ checkoutNote: string;
|
|
|
|
|
+ successHint: string;
|
|
|
|
|
+ buildCopyText: (token: string, companyId?: string) => string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// VOC 技能体系相关的 APIG — 一键切换
|
|
// VOC 技能体系相关的 APIG — 一键切换
|
|
|
const SOCIAL_APIG_ID = 'Vo3ROWEvDy';
|
|
const SOCIAL_APIG_ID = 'Vo3ROWEvDy';
|
|
|
const ECOMMERCE_APIG_ID = '7HwdQZk55B';
|
|
const ECOMMERCE_APIG_ID = '7HwdQZk55B';
|
|
|
-const VOC_APIG_IDS = [SOCIAL_APIG_ID, ECOMMERCE_APIG_ID];
|
|
|
|
|
|
|
+const TIHAO_ECOMMERCE_APIG_ID = 'G0vmsUI44d';
|
|
|
|
|
+const TIHAO_ECOMMERCE_APIG_PRICE_STEP: PriceTier[] = [
|
|
|
|
|
+ { count: 1000, price: 300 },
|
|
|
|
|
+ { count: 3000, price: 750 },
|
|
|
|
|
+ { count: 10000, price: 2000 }
|
|
|
|
|
+];
|
|
|
|
|
+const APIG_ALIASES: Record<string, string> = {
|
|
|
|
|
+ 'voc-e-commerce': TIHAO_ECOMMERCE_APIG_ID,
|
|
|
|
|
+ '/apig/voc-e-commerce': TIHAO_ECOMMERCE_APIG_ID
|
|
|
|
|
+};
|
|
|
|
|
+const VOC_APIG_IDS = [SOCIAL_APIG_ID, ECOMMERCE_APIG_ID, TIHAO_ECOMMERCE_APIG_ID];
|
|
|
|
|
+
|
|
|
|
|
+const SOCIAL_TOKEN_COPY_PROFILE: TokenCopyProfile = {
|
|
|
|
|
+ title: '复制社媒数据中台配置话术',
|
|
|
|
|
+ buttonText: '复制社媒配置话术',
|
|
|
|
|
+ copiedText: '已复制社媒话术',
|
|
|
|
|
+ checkoutNote: '支付成功后,点击“复制社媒配置话术”完成社媒技能包工作区配置。',
|
|
|
|
|
+ successHint: '下一步:点击右上角「复制社媒配置话术」,将 VOC_TOKEN 写入当前工作区 .env.local 后,再运行小红书或抖音 live 采集。',
|
|
|
|
|
+ buildCopyText: (token: string) => [
|
|
|
|
|
+ '请把下面内容保存到当前工作区的 .env.local,用于社媒数据中台技能包的小红书/抖音 live 采集。',
|
|
|
|
|
+ '完成后重启或刷新 Claude Code,再运行 live 采集任务。',
|
|
|
|
|
+ '不要提交到 Git,也不要放进截图、报告或聊天记录。',
|
|
|
|
|
+ '',
|
|
|
|
|
+ `VOC_TOKEN=${token}`,
|
|
|
|
|
+ `VOC_SOCIAL_TOKEN=${token}`
|
|
|
|
|
+ ].join('\n')
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const TIHAO_TOKEN_COPY_PROFILE: TokenCopyProfile = {
|
|
|
|
|
+ title: '复制提号技能包配置话术',
|
|
|
|
|
+ buttonText: '复制提号配置话术',
|
|
|
|
|
+ copiedText: '已复制提号话术',
|
|
|
|
|
+ checkoutNote: '支付成功后,点击“复制提号配置话术”完成提号技能包工作区配置。',
|
|
|
|
|
+ successHint: '下一步:点击右上角「复制提号配置话术」,将 TIHAO_SESSION_TOKEN 写入提号项目工作区 .env.local 后,再运行提号或找博主任务。',
|
|
|
|
|
+ buildCopyText: (token: string, companyId?: string) => [
|
|
|
|
|
+ '请把下面内容保存到当前工作区的 .env.local,用于提号技能包调用国内电商数据中台 live 模式。',
|
|
|
|
|
+ '完成后重启或刷新 Claude Code,再运行“提号 / 找博主 / 读取 brief”的 live 任务。',
|
|
|
|
|
+ '不要提交到 Git,也不要放进截图、报告或聊天记录。',
|
|
|
|
|
+ '',
|
|
|
|
|
+ `TIHAO_SESSION_TOKEN=${token}`,
|
|
|
|
|
+ ...(companyId ? [`TIHAO_COMPANY=${companyId}`] : [])
|
|
|
|
|
+ ].join('\n')
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_TOKEN_COPY_PROFILE: TokenCopyProfile = {
|
|
|
|
|
+ title: '复制 API 服务配置话术',
|
|
|
|
|
+ buttonText: '复制服务配置话术',
|
|
|
|
|
+ copiedText: '已复制配置话术',
|
|
|
|
|
+ checkoutNote: '支付成功后,点击“复制服务配置话术”完成对应工作区配置。',
|
|
|
|
|
+ successHint: '下一步:点击右上角「复制服务配置话术」,按对应技能包要求写入当前工作区 .env.local。',
|
|
|
|
|
+ buildCopyText: (token: string) => [
|
|
|
|
|
+ '请把下面内容保存到当前工作区的 .env.local,并按对应技能包要求使用这个 APIG 授权 token。',
|
|
|
|
|
+ '不要提交到 Git,也不要放进截图、报告或聊天记录。',
|
|
|
|
|
+ '',
|
|
|
|
|
+ `FMODE_APIG_SESSION_TOKEN=${token}`
|
|
|
|
|
+ ].join('\n')
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const TOKEN_COPY_PROFILES: Record<string, TokenCopyProfile> = {
|
|
|
|
|
+ [SOCIAL_APIG_ID]: SOCIAL_TOKEN_COPY_PROFILE,
|
|
|
|
|
+ [ECOMMERCE_APIG_ID]: TIHAO_TOKEN_COPY_PROFILE,
|
|
|
|
|
+ [TIHAO_ECOMMERCE_APIG_ID]: TIHAO_TOKEN_COPY_PROFILE
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
const WORKSHOP_PACKAGES: Record<string, WorkshopPackage> = {
|
|
const WORKSHOP_PACKAGES: Record<string, WorkshopPackage> = {
|
|
|
'19-9': {
|
|
'19-9': {
|
|
@@ -77,18 +148,17 @@ const WORKSHOP_PACKAGES: Record<string, WorkshopPackage> = {
|
|
|
},
|
|
},
|
|
|
'29-9': {
|
|
'29-9': {
|
|
|
slug: '29-9',
|
|
slug: '29-9',
|
|
|
- title: '课程加量体验包',
|
|
|
|
|
- badge: '课程专享',
|
|
|
|
|
- subtitle: '给课程学员使用的加量练习包,适合课上多跑几轮真实案例。',
|
|
|
|
|
- scene: '课程学习',
|
|
|
|
|
|
|
+ title: '轻量级工作坊',
|
|
|
|
|
+ badge: '轻量入门',
|
|
|
|
|
+ subtitle: '适合轻量级工作坊和短课实操,只开通社媒数据中台额度。',
|
|
|
|
|
+ scene: '轻量工作坊',
|
|
|
price: 29.9,
|
|
price: 29.9,
|
|
|
- visible: false,
|
|
|
|
|
|
|
+ visible: true,
|
|
|
primaryApigId: SOCIAL_APIG_ID,
|
|
primaryApigId: SOCIAL_APIG_ID,
|
|
|
allocations: [
|
|
allocations: [
|
|
|
- { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 150 },
|
|
|
|
|
- { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 150 }
|
|
|
|
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 300 }
|
|
|
],
|
|
],
|
|
|
- includes: ['课堂练习额度', '基础情报官模板', '不含复盘服务']
|
|
|
|
|
|
|
+ includes: ['社媒数据额度', '工作坊轻量练习', '不含电商数据中台额度']
|
|
|
},
|
|
},
|
|
|
'99': {
|
|
'99': {
|
|
|
slug: '99',
|
|
slug: '99',
|
|
@@ -162,6 +232,56 @@ interface OrderData {
|
|
|
[key: string]: any;
|
|
[key: string]: any;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface LauncherDownload {
|
|
|
|
|
+ os: 'Windows' | 'macOS' | 'Linux';
|
|
|
|
|
+ arch: string;
|
|
|
|
|
+ label: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ primary?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const LAUNCHER_DOWNLOADS: LauncherDownload[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'Windows',
|
|
|
|
|
+ arch: 'x64',
|
|
|
|
|
+ label: 'Windows x64',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/FmodeStudioLauncher-win-x64.exe',
|
|
|
|
|
+ primary: true
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'Windows',
|
|
|
|
|
+ arch: 'ARM64',
|
|
|
|
|
+ label: 'Windows ARM64',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/FmodeStudioLauncher-win-arm64.exe'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'macOS',
|
|
|
|
|
+ arch: 'Apple Silicon',
|
|
|
|
|
+ label: 'macOS Apple Silicon',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/fmode-studio-launcher-macos-arm64',
|
|
|
|
|
+ primary: true
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'macOS',
|
|
|
|
|
+ arch: 'Intel',
|
|
|
|
|
+ label: 'macOS Intel',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/fmode-studio-launcher-macos-x64'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'Linux',
|
|
|
|
|
+ arch: 'x64',
|
|
|
|
|
+ label: 'Linux x64',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/fmode-studio-launcher-linux-x64'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ os: 'Linux',
|
|
|
|
|
+ arch: 'ARM64',
|
|
|
|
|
+ label: 'Linux ARM64',
|
|
|
|
|
+ url: 'https://repos.fmode.cn/x/launcher/fmode-studio-launcher-linux-arm64'
|
|
|
|
|
+ }
|
|
|
|
|
+];
|
|
|
|
|
+const LAUNCHER_URL_LIST = 'https://repos.fmode.cn/x/launcher/url.txt';
|
|
|
|
|
+
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-root',
|
|
selector: 'app-root',
|
|
|
imports: [CommonModule, LoginModalComponent],
|
|
imports: [CommonModule, LoginModalComponent],
|
|
@@ -182,6 +302,7 @@ export class App implements OnInit {
|
|
|
workshopPackage: WorkshopPackage | null = null;
|
|
workshopPackage: WorkshopPackage | null = null;
|
|
|
workshopPortal = false;
|
|
workshopPortal = false;
|
|
|
workshopPreparing = false;
|
|
workshopPreparing = false;
|
|
|
|
|
+ isLauncherPage = false;
|
|
|
|
|
|
|
|
// Login state
|
|
// Login state
|
|
|
needLogin = false;
|
|
needLogin = false;
|
|
@@ -214,6 +335,8 @@ export class App implements OnInit {
|
|
|
orderError = '';
|
|
orderError = '';
|
|
|
hasMoreOrders = false;
|
|
hasMoreOrders = false;
|
|
|
orderSkip = 0;
|
|
orderSkip = 0;
|
|
|
|
|
+ launcherDownloads = LAUNCHER_DOWNLOADS;
|
|
|
|
|
+ launcherUrlList = LAUNCHER_URL_LIST;
|
|
|
|
|
|
|
|
get selectedTier(): PriceTier | null {
|
|
get selectedTier(): PriceTier | null {
|
|
|
if (this.workshopPackage) {
|
|
if (this.workshopPackage) {
|
|
@@ -227,6 +350,16 @@ export class App implements OnInit {
|
|
|
return this.workshopPackage?.allocations.reduce((sum, item) => sum + item.count, 0) || 0;
|
|
return this.workshopPackage?.allocations.reduce((sum, item) => sum + item.count, 0) || 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ get workshopQuotaLabel(): string {
|
|
|
|
|
+ if (!this.workshopPackage) return '数据中台运行额度';
|
|
|
|
|
+ return this.workshopPackage.allocations.length > 1 ? '双数据中台运行额度' : this.workshopPackage.allocations[0].title + '额度';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get workshopArrivalLabel(): string {
|
|
|
|
|
+ if (!this.workshopPackage) return '数据中台到账';
|
|
|
|
|
+ return this.workshopPackage.allocations.length > 1 ? '两个数据中台分别到账' : this.workshopPackage.allocations[0].title + '到账';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
get visibleWorkshopPackages(): WorkshopPackage[] {
|
|
get visibleWorkshopPackages(): WorkshopPackage[] {
|
|
|
return Object.values(WORKSHOP_PACKAGES).filter(pkg => pkg.visible);
|
|
return Object.values(WORKSHOP_PACKAGES).filter(pkg => pkg.visible);
|
|
|
}
|
|
}
|
|
@@ -236,6 +369,7 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get pageSubtitle(): string {
|
|
get pageSubtitle(): string {
|
|
|
|
|
+ if (this.isLauncherPage) return 'Studio 启动器下载';
|
|
|
return this.isWorkshopMode ? '情报官技能套餐' : 'API 服务充值';
|
|
return this.isWorkshopMode ? '情报官技能套餐' : 'API 服务充值';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -244,15 +378,88 @@ export class App implements OnInit {
|
|
|
return (this.apig.objectId === 'MYM5zJBKgw' || this.apig.objectId === 'FQtTgjcqIZ') ? 'token' : '次';
|
|
return (this.apig.objectId === 'MYM5zJBKgw' || this.apig.objectId === 'FQtTgjcqIZ') ? 'token' : '次';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ get currentApigFromList(): ApigData | null {
|
|
|
|
|
+ return this.apigList.find(item => this.isCurrentApig(item.objectId)) || this.apig;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get selectedUnitPrice(): string {
|
|
|
|
|
+ const tier = this.selectedTier;
|
|
|
|
|
+ if (!tier?.count) return '0.000';
|
|
|
|
|
+ return (tier.price / tier.count).toFixed(3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get tokenCopyProfile(): TokenCopyProfile {
|
|
|
|
|
+ const apigId = this.normalizeApigId(this.apig?.objectId || this.apigId || this.workshopPackage?.primaryApigId || '');
|
|
|
|
|
+ return TOKEN_COPY_PROFILES[apigId] || DEFAULT_TOKEN_COPY_PROFILE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get tokenCopyTitle(): string {
|
|
|
|
|
+ return this.tokenCopyProfile.title;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get tokenCopyButtonText(): string {
|
|
|
|
|
+ return this.tokenCopied ? this.tokenCopyProfile.copiedText : this.tokenCopyProfile.buttonText;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get tokenCopyCheckoutNote(): string {
|
|
|
|
|
+ return this.tokenCopyProfile.checkoutNote;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get tokenCopySuccessHint(): string {
|
|
|
|
|
+ return this.tokenCopyProfile.successHint;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ normalizeApigId(apigId: string): string {
|
|
|
|
|
+ return APIG_ALIASES[apigId] || apigId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 支付回调云函数 id(fun_id)必须是充值云函数 apigRechargeOnPay 的 Parse objectId
|
|
|
|
|
+ // (HOkkX72PMF)。历史链接曾把 APIG 的别名 / path / objectId(如 voc-e-commerce)误当作
|
|
|
|
|
+ // fun_id 传入,导致微信回调按 fun_id 找不到云函数、静默不入账。统一在此回退默认值。
|
|
|
|
|
+ resolveFunId(rawFunId: string | null): string {
|
|
|
|
|
+ const value = (rawFunId || '').trim();
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ return DEFAULT_FUN_ID;
|
|
|
|
|
+ }
|
|
|
|
|
+ const isApigAlias = Object.prototype.hasOwnProperty.call(APIG_ALIASES, value);
|
|
|
|
|
+ const isApigId = VOC_APIG_IDS.includes(value);
|
|
|
|
|
+ const looksLikeApigPath = value.startsWith('/apig/') || value.startsWith('apig/') || value.includes('voc-');
|
|
|
|
|
+ if (isApigAlias || isApigId || looksLikeApigPath) {
|
|
|
|
|
+ console.warn('[PAY] 非法 fun_id(疑似 APIG 别名/path/objectId),回退默认充值云函数:', value, '->', DEFAULT_FUN_ID);
|
|
|
|
|
+ return DEFAULT_FUN_ID;
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ decorateApig(apig: ApigData): ApigData {
|
|
|
|
|
+ const normalizedId = this.normalizeApigId(apig.objectId);
|
|
|
|
|
+ if (normalizedId === TIHAO_ECOMMERCE_APIG_ID) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...apig,
|
|
|
|
|
+ objectId: TIHAO_ECOMMERCE_APIG_ID,
|
|
|
|
|
+ title: '国内电商数据中台',
|
|
|
|
|
+ content: '电商与达人数据接口,适合提号选品、达人筛选和电商情报检索。',
|
|
|
|
|
+ priceStep: TIHAO_ECOMMERCE_APIG_PRICE_STEP.map(item => ({ ...item }))
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return apig;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
ngOnInit(): void {
|
|
ngOnInit(): void {
|
|
|
|
|
+ this.isLauncherPage = window.location.pathname.includes('/launcher') || window.location.hash === '#/launcher';
|
|
|
|
|
+ if (this.isLauncherPage) {
|
|
|
|
|
+ this.loadLauncherDownloads();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
this.syncWorkshopRouteFromUrl(false);
|
|
this.syncWorkshopRouteFromUrl(false);
|
|
|
window.addEventListener('hashchange', this.routeChangeHandler);
|
|
window.addEventListener('hashchange', this.routeChangeHandler);
|
|
|
window.addEventListener('popstate', this.routeChangeHandler);
|
|
window.addEventListener('popstate', this.routeChangeHandler);
|
|
|
this.authId = params.get('authid') || '';
|
|
this.authId = params.get('authid') || '';
|
|
|
this.userId = params.get('user') || params.get('userid') || '';
|
|
this.userId = params.get('user') || params.get('userid') || '';
|
|
|
- this.apigId = params.get('apigid') || this.workshopPackage?.primaryApigId || SOCIAL_APIG_ID;
|
|
|
|
|
- this.funId = params.get('fun_id') || DEFAULT_FUN_ID;
|
|
|
|
|
|
|
+ this.apigId = this.normalizeApigId(params.get('apigid') || this.workshopPackage?.primaryApigId || SOCIAL_APIG_ID);
|
|
|
|
|
+ this.funId = this.resolveFunId(params.get('fun_id'));
|
|
|
const urlToken = params.get('token') || '';
|
|
const urlToken = params.get('token') || '';
|
|
|
|
|
|
|
|
// 0. URL 携带 token → 使用 Parse become 登录
|
|
// 0. URL 携带 token → 使用 Parse become 登录
|
|
@@ -293,6 +500,66 @@ export class App implements OnInit {
|
|
|
window.removeEventListener('popstate', this.routeChangeHandler);
|
|
window.removeEventListener('popstate', this.routeChangeHandler);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ async loadLauncherDownloads(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(LAUNCHER_URL_LIST, { cache: 'no-store' });
|
|
|
|
|
+ if (!resp.ok) return;
|
|
|
|
|
+ const text = await resp.text();
|
|
|
|
|
+ const parsed = this.parseLauncherDownloads(text);
|
|
|
|
|
+ if (parsed.length > 0) {
|
|
|
|
|
+ this.launcherDownloads = parsed;
|
|
|
|
|
+ this.cdr.detectChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ console.warn('[LAUNCHER] 下载链接清单加载失败,使用内置链接:', e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ parseLauncherDownloads(text: string): LauncherDownload[] {
|
|
|
|
|
+ const downloads = text
|
|
|
|
|
+ .split(/\r?\n/)
|
|
|
|
|
+ .map(line => line.trim())
|
|
|
|
|
+ .filter(line => line.startsWith('http'))
|
|
|
|
|
+ .map(url => this.toLauncherDownload(url))
|
|
|
|
|
+ .filter((item): item is LauncherDownload => !!item);
|
|
|
|
|
+
|
|
|
|
|
+ const rank = (item: LauncherDownload): number => {
|
|
|
|
|
+ const key = item.os + '-' + item.arch;
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'Windows-x64': 1,
|
|
|
|
|
+ 'Windows-ARM64': 2,
|
|
|
|
|
+ 'macOS-Apple Silicon': 3,
|
|
|
|
|
+ 'macOS-Intel': 4,
|
|
|
|
|
+ 'Linux-x64': 5,
|
|
|
|
|
+ 'Linux-ARM64': 6
|
|
|
|
|
+ }[key] || 99;
|
|
|
|
|
+ };
|
|
|
|
|
+ return downloads.sort((a, b) => rank(a) - rank(b));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toLauncherDownload(url: string): LauncherDownload | null {
|
|
|
|
|
+ const lower = url.toLowerCase();
|
|
|
|
|
+ if (lower.includes('win-x64')) {
|
|
|
|
|
+ return { os: 'Windows', arch: 'x64', label: 'Windows x64', url, primary: true };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lower.includes('win-arm64')) {
|
|
|
|
|
+ return { os: 'Windows', arch: 'ARM64', label: 'Windows ARM64', url };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lower.includes('macos-arm64')) {
|
|
|
|
|
+ return { os: 'macOS', arch: 'Apple Silicon', label: 'macOS Apple Silicon', url, primary: true };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lower.includes('macos-x64')) {
|
|
|
|
|
+ return { os: 'macOS', arch: 'Intel', label: 'macOS Intel', url };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lower.includes('linux-x64')) {
|
|
|
|
|
+ return { os: 'Linux', arch: 'x64', label: 'Linux x64', url };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lower.includes('linux-arm64')) {
|
|
|
|
|
+ return { os: 'Linux', arch: 'ARM64', label: 'Linux ARM64', url };
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 通过 URL 中的 token 调用 Parse Server 的 become(fetch /parse/users/me)完成登录
|
|
* 通过 URL 中的 token 调用 Parse Server 的 become(fetch /parse/users/me)完成登录
|
|
|
*/
|
|
*/
|
|
@@ -422,7 +689,9 @@ export class App implements OnInit {
|
|
|
headers: { 'X-Parse-Application-Id': APP_ID }
|
|
headers: { 'X-Parse-Application-Id': APP_ID }
|
|
|
});
|
|
});
|
|
|
const data = await resp.json();
|
|
const data = await resp.json();
|
|
|
- const apigs: ApigData[] = data.results || [];
|
|
|
|
|
|
|
+ const apigs: ApigData[] = (data.results || [])
|
|
|
|
|
+ .map((item: ApigData) => this.decorateApig(item))
|
|
|
|
|
+ .sort((a: ApigData, b: ApigData) => VOC_APIG_IDS.indexOf(a.objectId) - VOC_APIG_IDS.indexOf(b.objectId));
|
|
|
|
|
|
|
|
// 2. 合并当前用户的 APIGAuth(如果已登录)
|
|
// 2. 合并当前用户的 APIGAuth(如果已登录)
|
|
|
if (this.userId && this.sessionToken) {
|
|
if (this.userId && this.sessionToken) {
|
|
@@ -476,29 +745,31 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
isCurrentApig(apigId: string): boolean {
|
|
isCurrentApig(apigId: string): boolean {
|
|
|
- return !!this.apigId && this.apigId === apigId;
|
|
|
|
|
|
|
+ return !!this.apigId && this.normalizeApigId(this.apigId) === this.normalizeApigId(apigId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
navigateToApig(apigId: string): void {
|
|
navigateToApig(apigId: string): void {
|
|
|
if (this.isCurrentApig(apigId)) return;
|
|
if (this.isCurrentApig(apigId)) return;
|
|
|
const url = new URL(window.location.href);
|
|
const url = new URL(window.location.href);
|
|
|
- url.searchParams.set('apigid', apigId);
|
|
|
|
|
|
|
+ url.searchParams.set('apigid', this.normalizeApigId(apigId));
|
|
|
// 切换 APIG 时清空 authid(因为换了 APIG 就要重新解析新的 APIGAuth)
|
|
// 切换 APIG 时清空 authid(因为换了 APIG 就要重新解析新的 APIGAuth)
|
|
|
url.searchParams.delete('authid');
|
|
url.searchParams.delete('authid');
|
|
|
window.location.href = url.toString();
|
|
window.location.href = url.toString();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ onApigSelect(value: string): void {
|
|
|
|
|
+ this.navigateToApig(value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
copyToken(): void {
|
|
copyToken(): void {
|
|
|
if (!this.sessionToken) return;
|
|
if (!this.sessionToken) return;
|
|
|
- // 复制一整条可直接粘贴到终端执行的命令,把 token 写入 OpenClaw 凭证文件
|
|
|
|
|
- const cmd = `node set-voc-token.js ${this.sessionToken}`;
|
|
|
|
|
- navigator.clipboard.writeText(cmd).then(() => {
|
|
|
|
|
|
|
+ const tokenText = this.tokenCopyProfile.buildCopyText(this.sessionToken, this.companyId);
|
|
|
|
|
+ navigator.clipboard.writeText(tokenText).then(() => {
|
|
|
this.tokenCopied = true;
|
|
this.tokenCopied = true;
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
|
setTimeout(() => { this.tokenCopied = false; this.cdr.detectChanges(); }, 2500);
|
|
setTimeout(() => { this.tokenCopied = false; this.cdr.detectChanges(); }, 2500);
|
|
|
}).catch(() => {
|
|
}).catch(() => {
|
|
|
- // 剪贴板失败时兜底:把命令显示出来让用户手动复制
|
|
|
|
|
- this.errorMsg = '复制失败,请手动复制命令:' + cmd;
|
|
|
|
|
|
|
+ this.errorMsg = '复制失败,请手动复制:' + tokenText;
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -523,7 +794,7 @@ export class App implements OnInit {
|
|
|
const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
|
|
const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
|
|
|
console.log('[getApig] response:', JSON.stringify(resp).substring(0, 500));
|
|
console.log('[getApig] response:', JSON.stringify(resp).substring(0, 500));
|
|
|
if (resp.code === 200 && resp.data) {
|
|
if (resp.code === 200 && resp.data) {
|
|
|
- this.apig = resp.data;
|
|
|
|
|
|
|
+ this.apig = this.decorateApig(resp.data);
|
|
|
this.applyTestTier();
|
|
this.applyTestTier();
|
|
|
this.selectTier(0);
|
|
this.selectTier(0);
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
@@ -542,7 +813,7 @@ export class App implements OnInit {
|
|
|
if (cfResp?.result) {
|
|
if (cfResp?.result) {
|
|
|
const cfData = cfResp.result.data || cfResp.result;
|
|
const cfData = cfResp.result.data || cfResp.result;
|
|
|
if (cfData?.objectId) {
|
|
if (cfData?.objectId) {
|
|
|
- this.apig = cfData;
|
|
|
|
|
|
|
+ this.apig = this.decorateApig(cfData);
|
|
|
this.applyTestTier();
|
|
this.applyTestTier();
|
|
|
this.selectTier(0);
|
|
this.selectTier(0);
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
@@ -565,6 +836,7 @@ export class App implements OnInit {
|
|
|
|
|
|
|
|
// ─── 通过 user+apigid 查询或创建 APIGAuth ───
|
|
// ─── 通过 user+apigid 查询或创建 APIGAuth ───
|
|
|
async resolveAuthId(user: string, apig: string): Promise<string | null> {
|
|
async resolveAuthId(user: string, apig: string): Promise<string | null> {
|
|
|
|
|
+ apig = this.normalizeApigId(apig);
|
|
|
const authHeaders: any = { 'X-Parse-Application-Id': APP_ID };
|
|
const authHeaders: any = { 'X-Parse-Application-Id': APP_ID };
|
|
|
if (this.sessionToken) authHeaders['X-Parse-Session-Token'] = this.sessionToken;
|
|
if (this.sessionToken) authHeaders['X-Parse-Session-Token'] = this.sessionToken;
|
|
|
|
|
|
|
@@ -618,9 +890,11 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
applyTestTier(): void {
|
|
applyTestTier(): void {
|
|
|
- // 仅在 URL 带 test=1 时追加测试套餐(¥0.01 / 1 次),不再对正式价格做任何改写。
|
|
|
|
|
|
|
+ // 仅本地开发时允许 URL 带 test=1 追加测试套餐(¥0.01 / 1 次),线上不开放。
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
- if (params.get('test') === '1' && this.apig?.priceStep && !this.apig.priceStep.some(tier => tier.isTest)) {
|
|
|
|
|
|
|
+ const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
|
|
+ const isTihaoEcommerce = this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
|
|
|
+ if (isLocalhost && isTihaoEcommerce && params.get('test') === '1' && this.apig?.priceStep && !this.apig.priceStep.some(tier => tier.isTest)) {
|
|
|
this.apig.priceStep.unshift({ count: 1, price: 0.01, isTest: true });
|
|
this.apig.priceStep.unshift({ count: 1, price: 0.01, isTest: true });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -629,6 +903,17 @@ export class App implements OnInit {
|
|
|
this.selectedIndex = idx;
|
|
this.selectedIndex = idx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ isLocalTestTier(tier: PriceTier | null = this.selectedTier): boolean {
|
|
|
|
|
+ const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
|
|
+ return !!tier?.isTest && isLocalhost && this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isStrictCallbackTest(): boolean {
|
|
|
|
|
+ const params = new URLSearchParams(window.location.search);
|
|
|
|
|
+ const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
|
|
+ return isLocalhost && params.get('callback_test') === '1' && this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
syncWorkshopRouteFromUrl(shouldDetectChanges = true): void {
|
|
syncWorkshopRouteFromUrl(shouldDetectChanges = true): void {
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
const nextPackage = this.resolveWorkshopPackage(params);
|
|
const nextPackage = this.resolveWorkshopPackage(params);
|
|
@@ -707,6 +992,32 @@ export class App implements OnInit {
|
|
|
this.syncWorkshopRouteFromUrl();
|
|
this.syncWorkshopRouteFromUrl();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ closeWorkshopPortal(): void {
|
|
|
|
|
+ const url = new URL(window.location.href);
|
|
|
|
|
+ url.searchParams.delete('workshop');
|
|
|
|
|
+ url.searchParams.delete('packages');
|
|
|
|
|
+ url.searchParams.delete('package');
|
|
|
|
|
+ url.searchParams.delete('pkg');
|
|
|
|
|
+ url.hash = '';
|
|
|
|
|
+ window.history.pushState({}, '', url.toString());
|
|
|
|
|
+ this.workshopPackage = null;
|
|
|
|
|
+ this.workshopPortal = false;
|
|
|
|
|
+ this.showSuccess = false;
|
|
|
|
|
+ this.errorMsg = '';
|
|
|
|
|
+ this.cdr.detectChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ backToWorkshopPortal(): void {
|
|
|
|
|
+ const url = new URL(window.location.href);
|
|
|
|
|
+ url.searchParams.delete('workshop');
|
|
|
|
|
+ url.searchParams.delete('packages');
|
|
|
|
|
+ url.searchParams.delete('package');
|
|
|
|
|
+ url.searchParams.delete('pkg');
|
|
|
|
|
+ url.hash = '/workshop';
|
|
|
|
|
+ window.history.pushState({}, '', url.toString());
|
|
|
|
|
+ this.syncWorkshopRouteFromUrl();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
syncWorkshopAllocationsFromList(): void {
|
|
syncWorkshopAllocationsFromList(): void {
|
|
|
if (!this.workshopPackage || this.apigList.length === 0) return;
|
|
if (!this.workshopPackage || this.apigList.length === 0) return;
|
|
|
for (const allocation of this.workshopPackage.allocations) {
|
|
for (const allocation of this.workshopPackage.allocations) {
|
|
@@ -724,7 +1035,7 @@ export class App implements OnInit {
|
|
|
|
|
|
|
|
getPaymentBody(): string {
|
|
getPaymentBody(): string {
|
|
|
if (this.workshopPackage) {
|
|
if (this.workshopPackage) {
|
|
|
- return this.workshopPackage.title + '(双数据中台额度)';
|
|
|
|
|
|
|
+ return this.workshopPackage.title + '(' + this.workshopQuotaLabel + ')';
|
|
|
}
|
|
}
|
|
|
return this.apig!.title + ' 接口充值';
|
|
return this.apig!.title + ' 接口充值';
|
|
|
}
|
|
}
|
|
@@ -944,6 +1255,13 @@ export class App implements OnInit {
|
|
|
const tier = this.apig!.priceStep![this.selectedIndex];
|
|
const tier = this.apig!.priceStep![this.selectedIndex];
|
|
|
console.log('═══ [RECHARGE] 开始 ═══ oldCount:', oldCount, 'addCount:', tier.count, 'fun_id:', this.funId);
|
|
console.log('═══ [RECHARGE] 开始 ═══ oldCount:', oldCount, 'addCount:', tier.count, 'fun_id:', this.funId);
|
|
|
|
|
|
|
|
|
|
+ if (this.isLocalTestTier(tier) && !this.isStrictCallbackTest()) {
|
|
|
|
|
+ console.log('[RECHARGE] 本地 0.01 测试套餐,跳过后端回调等待,直接调用 saveRecharge。');
|
|
|
|
|
+ await this.saveRechargeFallback(oldCount, tier);
|
|
|
|
|
+ await this.confirmRechargeResult(oldCount);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 步骤1: 等待后端微信回调自动执行云函数
|
|
// 步骤1: 等待后端微信回调自动执行云函数
|
|
|
for (let i = 1; i <= 8; i++) {
|
|
for (let i = 1; i <= 8; i++) {
|
|
|
console.log('[RECHARGE] 步骤1: 等待后端回调... 第' + i + '/8次 (3秒后)');
|
|
console.log('[RECHARGE] 步骤1: 等待后端回调... 第' + i + '/8次 (3秒后)');
|
|
@@ -957,8 +1275,26 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (this.isStrictCallbackTest()) {
|
|
|
|
|
+ console.warn('[RECHARGE] 严格回调测试模式:后端回调未自动加余额,不调用 saveRecharge 兜底。');
|
|
|
|
|
+ this.successDetailHtml =
|
|
|
|
|
+ '支付已成功,但严格回调测试未观察到后端自动充值。<br>' +
|
|
|
|
|
+ '<span style="color:var(--text-dim);margin-top:8px;display:inline-block;">当前余额仍为 <strong style="color:var(--neon);">' +
|
|
|
|
|
+ this.apig!.count + ' ' + this.unitLabel + '</strong>,请检查支付回调/订单充值逻辑。</span>';
|
|
|
|
|
+ this.cdr.detectChanges();
|
|
|
|
|
+ this.loadOrderHistory();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 步骤2: 后端回调未生效,调用 saveRecharge 保底
|
|
// 步骤2: 后端回调未生效,调用 saveRecharge 保底
|
|
|
console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
|
|
console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
|
|
|
|
|
+ await this.saveRechargeFallback(oldCount, tier);
|
|
|
|
|
+
|
|
|
|
|
+ // 步骤3: 再轮询确认
|
|
|
|
|
+ await this.confirmRechargeResult(oldCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async saveRechargeFallback(oldCount: number, tier: PriceTier): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
const rechargeBody: any = {
|
|
const rechargeBody: any = {
|
|
|
user: this.userId,
|
|
user: this.userId,
|
|
@@ -975,8 +1311,9 @@ export class App implements OnInit {
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
|
console.warn('[RECHARGE] saveRecharge 失败:', e.message);
|
|
console.warn('[RECHARGE] saveRecharge 失败:', e.message);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 步骤3: 再轮询确认
|
|
|
|
|
|
|
+ async confirmRechargeResult(oldCount: number): Promise<void> {
|
|
|
for (let i = 1; i <= 3; i++) {
|
|
for (let i = 1; i <= 3; i++) {
|
|
|
await this.sleep(2000);
|
|
await this.sleep(2000);
|
|
|
await this.refreshBalance();
|
|
await this.refreshBalance();
|
|
@@ -1006,7 +1343,7 @@ export class App implements OnInit {
|
|
|
await this.prepareWorkshopPackage(false);
|
|
await this.prepareWorkshopPackage(false);
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
|
this.successDetailHtml =
|
|
this.successDetailHtml =
|
|
|
- '支付已完成,但双数据中台额度开通需要人工确认。<br>' +
|
|
|
|
|
|
|
+ '支付已完成,但' + this.workshopQuotaLabel + '开通需要人工确认。<br>' +
|
|
|
'<span style="color:var(--text-dim);">原因:' + (e.message || '账套确认失败') + '</span>';
|
|
'<span style="color:var(--text-dim);">原因:' + (e.message || '账套确认失败') + '</span>';
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
|
return;
|
|
return;
|