|
|
@@ -26,9 +26,14 @@ interface ApigData {
|
|
|
content?: string;
|
|
|
count?: number;
|
|
|
priceStep?: PriceTier[];
|
|
|
+ userBalance?: number; // 当前登录用户在该 APIG 的余额(合并 APIGAuth)
|
|
|
+ authId?: string; // 当前用户的 APIGAuth objectId
|
|
|
[key: string]: any;
|
|
|
}
|
|
|
|
|
|
+// VOC 技能体系相关的 APIG — 一键切换
|
|
|
+const VOC_APIG_IDS = ['Vo3ROWEvDy', '7HwdQZk55B'];
|
|
|
+
|
|
|
interface OrderData {
|
|
|
objectId: string;
|
|
|
createdAt: string;
|
|
|
@@ -60,6 +65,7 @@ export class App implements OnInit {
|
|
|
needLogin = false;
|
|
|
loggedInUser: string = '';
|
|
|
sessionToken: string = '';
|
|
|
+ companyId: string = '';
|
|
|
apigList: ApigData[] = [];
|
|
|
apigListLoading = false;
|
|
|
tokenCopied = false;
|
|
|
@@ -132,7 +138,7 @@ export class App implements OnInit {
|
|
|
|
|
|
async validateAndLoad(token: string, userId: string): Promise<void> {
|
|
|
try {
|
|
|
- const resp = await fetch(`${API_BASE}/parse/users/me`, {
|
|
|
+ const resp = await fetch(`${API_BASE}/parse/users/me?include=company`, {
|
|
|
headers: {
|
|
|
'X-Parse-Application-Id': APP_ID,
|
|
|
'X-Parse-Session-Token': token
|
|
|
@@ -160,7 +166,9 @@ export class App implements OnInit {
|
|
|
this.userId = data.objectId;
|
|
|
localStorage.setItem('apig_user_id', data.objectId);
|
|
|
}
|
|
|
- console.log('[AUTH] token 验证通过, user:', data.objectId, data.username || '');
|
|
|
+ // 提取 companyId(可选:后端扣费只要 user 指针即可,Company 存在时才附带)
|
|
|
+ this.companyId = data.company?.objectId || '';
|
|
|
+ console.log('[AUTH] token 验证通过, user:', data.objectId, data.username || '', 'company:', this.companyId || '(用户无 Company,仅用 user 指针扣费)');
|
|
|
this.cdr.detectChanges();
|
|
|
this.loadApig();
|
|
|
} catch (e) {
|
|
|
@@ -170,7 +178,7 @@ export class App implements OnInit {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- onLoginSuccess(event: { userId: string; sessionToken: string; displayName: string }): void {
|
|
|
+ async onLoginSuccess(event: { userId: string; sessionToken: string; displayName: string }): Promise<void> {
|
|
|
console.log('[AUTH] 登录成功, userId:', event.userId, 'display:', event.displayName);
|
|
|
this.userId = event.userId;
|
|
|
this.loggedInUser = event.displayName || event.userId;
|
|
|
@@ -178,7 +186,8 @@ export class App implements OnInit {
|
|
|
this.needLogin = false;
|
|
|
this.errorMsg = '';
|
|
|
|
|
|
- this.loadApig();
|
|
|
+ // 通过 validateAndLoad 统一处理 Company 解析 + 数据加载
|
|
|
+ await this.validateAndLoad(event.sessionToken, event.userId);
|
|
|
}
|
|
|
|
|
|
logout(): void {
|
|
|
@@ -189,6 +198,7 @@ export class App implements OnInit {
|
|
|
this.userId = '';
|
|
|
this.loggedInUser = '';
|
|
|
this.sessionToken = '';
|
|
|
+ this.companyId = '';
|
|
|
this.authId = '';
|
|
|
this.apig = null;
|
|
|
this.orders = [];
|
|
|
@@ -198,13 +208,14 @@ export class App implements OnInit {
|
|
|
this.cdr.detectChanges();
|
|
|
}
|
|
|
|
|
|
- // ─── Load available APIG list (when no apigId) ───
|
|
|
+ // ─── Load available APIG list + merge user's APIGAuth balance ───
|
|
|
async loadApigList(): Promise<void> {
|
|
|
this.apigListLoading = true;
|
|
|
try {
|
|
|
+ // 1. 拉 VOC 相关 APIG 基础信息
|
|
|
const query: any = {
|
|
|
where: JSON.stringify({
|
|
|
- objectId: { $in: ['Vo3ROWEvDy'] }
|
|
|
+ objectId: { $in: VOC_APIG_IDS }
|
|
|
}),
|
|
|
keys: 'title,content,priceStep',
|
|
|
limit: '10'
|
|
|
@@ -213,32 +224,83 @@ export class App implements OnInit {
|
|
|
headers: { 'X-Parse-Application-Id': APP_ID }
|
|
|
});
|
|
|
const data = await resp.json();
|
|
|
- if (data.results?.length > 0) {
|
|
|
- this.apigList = data.results;
|
|
|
- console.log('[APIG] 获取到', data.results.length, '个可用 API');
|
|
|
- } else {
|
|
|
- console.log('[APIG] 未查询到可用 API');
|
|
|
+ const apigs: ApigData[] = data.results || [];
|
|
|
+
|
|
|
+ // 2. 合并当前用户的 APIGAuth(如果已登录)
|
|
|
+ if (this.userId && this.sessionToken) {
|
|
|
+ try {
|
|
|
+ const authQuery = {
|
|
|
+ where: JSON.stringify({
|
|
|
+ user: { __type: 'Pointer', className: '_User', objectId: this.userId },
|
|
|
+ api: {
|
|
|
+ $inQuery: {
|
|
|
+ where: { objectId: { $in: VOC_APIG_IDS } },
|
|
|
+ className: 'APIG'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ keys: 'api,count,objectId',
|
|
|
+ limit: '10'
|
|
|
+ };
|
|
|
+ const authResp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(authQuery as any), {
|
|
|
+ headers: {
|
|
|
+ 'X-Parse-Application-Id': APP_ID,
|
|
|
+ 'X-Parse-Session-Token': this.sessionToken
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const authData = await authResp.json();
|
|
|
+ const authMap: Record<string, { count: number; objectId: string }> = {};
|
|
|
+ for (const a of (authData.results || [])) {
|
|
|
+ const apigPtrId = a.api?.objectId;
|
|
|
+ if (apigPtrId) authMap[apigPtrId] = { count: a.count || 0, objectId: a.objectId };
|
|
|
+ }
|
|
|
+ for (const apig of apigs) {
|
|
|
+ const rec = authMap[apig.objectId];
|
|
|
+ apig.userBalance = rec ? rec.count : 0;
|
|
|
+ apig.authId = rec ? rec.objectId : '';
|
|
|
+ }
|
|
|
+ console.log('[APIG] 用户余额合并完成:', authMap);
|
|
|
+ } catch (e: any) {
|
|
|
+ console.warn('[APIG] 合并用户 APIGAuth 失败:', e.message);
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ this.apigList = apigs;
|
|
|
+ console.log('[APIG] 获取到', apigs.length, '个可用 API');
|
|
|
this.cdr.detectChanges();
|
|
|
} catch (e: any) {
|
|
|
console.warn('[APIG] 查询 API 列表失败:', e.message);
|
|
|
} finally {
|
|
|
this.apigListLoading = false;
|
|
|
+ this.cdr.detectChanges();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ isCurrentApig(apigId: string): boolean {
|
|
|
+ return !!this.apigId && this.apigId === apigId;
|
|
|
+ }
|
|
|
+
|
|
|
navigateToApig(apigId: string): void {
|
|
|
+ if (this.isCurrentApig(apigId)) return;
|
|
|
const url = new URL(window.location.href);
|
|
|
url.searchParams.set('apigid', apigId);
|
|
|
+ // 切换 APIG 时清空 authid(因为换了 APIG 就要重新解析新的 APIGAuth)
|
|
|
+ url.searchParams.delete('authid');
|
|
|
window.location.href = url.toString();
|
|
|
}
|
|
|
|
|
|
copyToken(): void {
|
|
|
if (!this.sessionToken) return;
|
|
|
- navigator.clipboard.writeText(this.sessionToken).then(() => {
|
|
|
+ // 复制一整条可直接粘贴到终端执行的命令,把 token 写入 OpenClaw 凭证文件
|
|
|
+ const cmd = `node set-voc-token.js ${this.sessionToken}`;
|
|
|
+ navigator.clipboard.writeText(cmd).then(() => {
|
|
|
this.tokenCopied = true;
|
|
|
this.cdr.detectChanges();
|
|
|
- setTimeout(() => { this.tokenCopied = false; this.cdr.detectChanges(); }, 2000);
|
|
|
+ setTimeout(() => { this.tokenCopied = false; this.cdr.detectChanges(); }, 2500);
|
|
|
+ }).catch(() => {
|
|
|
+ // 剪贴板失败时兜底:把命令显示出来让用户手动复制
|
|
|
+ this.errorMsg = '复制失败,请手动复制命令:' + cmd;
|
|
|
+ this.cdr.detectChanges();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -267,6 +329,8 @@ export class App implements OnInit {
|
|
|
this.selectTier(0);
|
|
|
this.cdr.detectChanges();
|
|
|
this.loadOrderHistory();
|
|
|
+ // 同时加载其它 VOC APIG 余额以显示切换器
|
|
|
+ this.loadApigList();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -329,25 +393,12 @@ export class App implements OnInit {
|
|
|
try {
|
|
|
console.log('[resolveAuthId] 未找到该用户的 APIGAuth,创建新记录(余额=0)...');
|
|
|
|
|
|
- // 获取用户的 company 指针(ensureCompany 已在登录时创建)
|
|
|
- let companyPointer: any = undefined;
|
|
|
- try {
|
|
|
- const meResp = await fetch(API_BASE + '/parse/users/me', { headers: authHeaders });
|
|
|
- const meData = await meResp.json();
|
|
|
- if (meData.company?.objectId) {
|
|
|
- companyPointer = { __type: 'Pointer', className: 'Company', objectId: meData.company.objectId };
|
|
|
- }
|
|
|
- } catch (e: any) {
|
|
|
- console.warn('[resolveAuthId] 获取 company 失败:', e.message);
|
|
|
- }
|
|
|
-
|
|
|
const body: any = {
|
|
|
api: { __type: 'Pointer', className: 'APIG', objectId: apig },
|
|
|
user: { __type: 'Pointer', className: '_User', objectId: user },
|
|
|
count: 0,
|
|
|
used: 0
|
|
|
};
|
|
|
- if (companyPointer) body.company = companyPointer;
|
|
|
|
|
|
const createResp = await fetch(API_BASE + '/parse/classes/APIGAuth', {
|
|
|
method: 'POST',
|
|
|
@@ -367,13 +418,7 @@ export class App implements OnInit {
|
|
|
}
|
|
|
|
|
|
applyTestTier(): void {
|
|
|
- if (this.apig?.priceStep) {
|
|
|
- // 套餐价格翻倍
|
|
|
- this.apig.priceStep = this.apig.priceStep.map(t => ({
|
|
|
- ...t,
|
|
|
- price: t.price * 2
|
|
|
- }));
|
|
|
- }
|
|
|
+ // 仅在 URL 带 test=1 时追加测试套餐(¥0.01 / 1 次),不再对正式价格做任何改写
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
if (params.get('test') === '1' && this.apig?.priceStep) {
|
|
|
this.apig.priceStep.unshift({ count: 1, price: 0.01 });
|
|
|
@@ -485,9 +530,10 @@ export class App implements OnInit {
|
|
|
apigid: this.apig!.objectId,
|
|
|
oldCount: this.apig!.count || 0,
|
|
|
count: tier.count,
|
|
|
- user: this.userId || this.authId,
|
|
|
- fcompany: this.userId || this.authId
|
|
|
+ user: this.userId
|
|
|
};
|
|
|
+ // 仅在拿到真实 Company objectId 时才传 fcompany,避免把 userId 误写成 Company 指针
|
|
|
+ if (this.companyId) body.fcompany = this.companyId;
|
|
|
const resp = await this.postJSON(API_BASE + '/api/apig/created-apigorder', body);
|
|
|
if (resp.code === 200 && resp.data) {
|
|
|
this.order = resp.data;
|
|
|
@@ -544,15 +590,15 @@ export class App implements OnInit {
|
|
|
// 步骤2: 后端回调未生效,调用 saveRecharge 保底
|
|
|
console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
|
|
|
try {
|
|
|
- const rechargeBody = {
|
|
|
- user: this.userId || this.authId,
|
|
|
- authComp: this.userId || this.authId,
|
|
|
+ const rechargeBody: any = {
|
|
|
+ user: this.userId,
|
|
|
authid: this.authId,
|
|
|
apigid: this.apig!.objectId,
|
|
|
oldCount: oldCount,
|
|
|
count: tier.count,
|
|
|
orderid: this.order?.objectId || ''
|
|
|
};
|
|
|
+ if (this.companyId) rechargeBody.authComp = this.companyId;
|
|
|
console.log('[RECHARGE] saveRecharge 参数:', JSON.stringify(rechargeBody));
|
|
|
const rechargeResp = await this.postJSON(API_BASE + '/api/apig/saveRecharge', rechargeBody);
|
|
|
console.log('[RECHARGE] saveRecharge 响应:', JSON.stringify(rechargeResp));
|
|
|
@@ -647,15 +693,24 @@ export class App implements OnInit {
|
|
|
this.orderSkip = 0;
|
|
|
this.orders = [];
|
|
|
}
|
|
|
+ // 在 detectChanges 之后触发异步工作,避免在父级 CD 周期内改变 [disabled] 绑定
|
|
|
+ this.cdr.detectChanges();
|
|
|
|
|
|
try {
|
|
|
- const userVal = this.userId || this.authId;
|
|
|
- const where = JSON.stringify({
|
|
|
- '$or': [
|
|
|
- { fromCompany: { __type: 'Pointer', className: 'Company', objectId: userVal } },
|
|
|
- { fromUser: { __type: 'Pointer', className: '_User', objectId: userVal } }
|
|
|
- ]
|
|
|
- });
|
|
|
+ const orClauses: any[] = [];
|
|
|
+ if (this.userId) {
|
|
|
+ orClauses.push({ fromUser: { __type: 'Pointer', className: '_User', objectId: this.userId } });
|
|
|
+ }
|
|
|
+ if (this.companyId) {
|
|
|
+ orClauses.push({ fromCompany: { __type: 'Pointer', className: 'Company', objectId: this.companyId } });
|
|
|
+ }
|
|
|
+ if (orClauses.length === 0) {
|
|
|
+ this.orders = [];
|
|
|
+ this.ordersLoading = false;
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const where = JSON.stringify(orClauses.length === 1 ? orClauses[0] : { '$or': orClauses });
|
|
|
const qs = new URLSearchParams({
|
|
|
where,
|
|
|
order: '-createdAt',
|