gangvy 5 месяцев назад
Родитель
Сommit
e71ad8c475
34 измененных файлов с 2218 добавлено и 11 удалено
  1. 6 0
      __config/voc-credentials.template.json
  2. 7 11
      payment/apig-pay.html
  3. 108 0
      scripts/add-token-config.ps1
  4. 33 0
      scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.html
  5. 389 0
      scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.scss
  6. 25 0
      scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.spec.ts
  7. 209 0
      scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.ts
  8. 22 0
      scripts/debug-single.ps1
  9. 69 0
      scripts/fix-token-config.ps1
  10. BIN
      scripts/simple-apig-auth.zip
  11. 66 0
      scripts/test-apig-cloudfunction.ps1
  12. 132 0
      scripts/test-apig-flow.ps1
  13. 92 0
      scripts/test-apig-flow2.ps1
  14. 90 0
      scripts/test-apig-real-user.ps1
  15. 52 0
      scripts/test-apig-recharge.ps1
  16. 36 0
      scripts/test-apig-routes.ps1
  17. 36 0
      scripts/test-cloudfunc-name.ps1
  18. 54 0
      scripts/test-cloudfunc-name2.ps1
  19. 33 0
      scripts/test-cloudfunc.ps1
  20. 65 0
      scripts/test-cloudfunc3.ps1
  21. 42 0
      scripts/test-cloudfunc4.ps1
  22. 17 0
      scripts/test-find-apigauth.ps1
  23. 40 0
      scripts/test-find-user.ps1
  24. 9 0
      scripts/test-getapig-authid.ps1
  25. 51 0
      scripts/test-getapig-parse.ps1
  26. 6 0
      scripts/test-getapig-raw.ps1
  27. 27 0
      scripts/test-read.ps1
  28. 54 0
      scripts/test-video-api.ps1
  29. 78 0
      scripts/test-video-auth.ps1
  30. 57 0
      scripts/test-video-direct.ps1
  31. 122 0
      scripts/test-video-full-flow.ps1
  32. 61 0
      scripts/test-video-isolation.ps1
  33. 46 0
      scripts/token-config-injection.txt
  34. 84 0
      scripts/update-payment-urls.ps1

+ 6 - 0
__config/voc-credentials.template.json

@@ -0,0 +1,6 @@
+{
+  "_comment": "VOC-AI Token 配置文件。充值后将获得的 Token 填入 vocToken 字段,保存为 ~/.openclaw/voc-credentials.json",
+  "_format": "Token 格式为 r:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(32位十六进制)",
+  "_usage": "所有 VOC Skills 会自动读取此文件的 vocToken 字段,优先级高于内置默认 Token",
+  "vocToken": ""
+}

+ 7 - 11
payment/apig-pay.html

@@ -830,11 +830,9 @@ async function createOrder(tier) {
       oldCount: apig.count || 0,
       count: tier.count
     };
-    // Use user field if available, fallback to fcompany
-    if (userId) {
-      body.user = userId;
-      body.fcompany = userId;
-    }
+    // Server requires fcompany or user; fallback to authId if userId is empty
+    body.user = userId || authId;
+    body.fcompany = userId || authId;
 
     const resp = await postJSON(API_BASE + '/api/apig/created-apigorder', body);
     if (resp.code === 200 && resp.data) {
@@ -885,12 +883,10 @@ async function doRecharge() {
       count: tier.count,
       orderid: order ? order.objectId : ''
     };
-    if (userId) {
-      body.user = userId;
-      body.authComp = userId;
-    }
-    // Also pass fcompany for compatibility
-    body.fcompany = userId || '';
+    // Server requires fcompany or user; fallback to authId if userId is empty
+    body.user = userId || authId;
+    body.authComp = userId || authId;
+    body.fcompany = userId || authId;
     console.log('saveRecharge body:', JSON.stringify(body));
     // Use AbortController for 15s timeout
     const controller = new AbortController();

+ 108 - 0
scripts/add-token-config.ps1

@@ -0,0 +1,108 @@
+# add-token-config.ps1
+# 批量给所有 api-config.json 添加 tokenConfig + errorHandling
+# 并将 endpoint-based skill 的 Authorization 改为 {{vocToken}} 动态引用
+
+param(
+    [string]$SkillsRoot = (Split-Path $PSScriptRoot -Parent),
+    [switch]$DryRun
+)
+
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+$hardcodedToken = 'r:858b3ee92314d5447d1fc3cdc10462d7'
+
+# ============================================================
+#  tokenConfig + errorHandling 块(插入到最后一个 } 之前)
+# ============================================================
+$injection = @'
+,
+  "tokenConfig": {
+    "type": "bearer",
+    "configFile": "~/.openclaw/voc-credentials.json",
+    "tokenField": "vocToken",
+    "currentToken": "r:858b3ee92314d5447d1fc3cdc10462d7",
+    "resolutionOrder": ["configFile", "currentToken"],
+    "onMissing": {
+      "action": "showPaymentPrompt",
+      "paymentUrl": "",
+      "title": "需要配置 VOC-AI Token",
+      "message": "此 Skill 需要有效的 VOC-AI Token 才能调用数据接口。",
+      "setupInstructions": "充值成功后,请将获得的 Token(格式: r:xxxxxxxx)告知 AI,AI 将自动创建或更新 ~/.openclaw/voc-credentials.json,写入 {\"vocToken\": \"您的Token值\"} 并保存,后续所有 VOC Skills 将自动使用此 Token。"
+    },
+    "onBalanceInsufficient": {
+      "action": "showPaymentPrompt",
+      "paymentUrl": "",
+      "title": "VOC-AI Token 余额不足",
+      "message": "当前 Token 余额已不足以完成本次请求,请充值后告知 AI 更新 ~/.openclaw/voc-credentials.json 中的 vocToken 字段。"
+    }
+  },
+  "errorHandling": {
+    "balanceInsufficient": {
+      "conditions": [
+        { "responseField": "code", "operator": "in", "value": [-2, -3, -10, 402, 429] },
+        { "responseField": "msg", "operator": "contains", "value": ["余额不足", "insufficient", "balance", "quota"] },
+        { "responseField": "message", "operator": "contains", "value": ["余额不足", "insufficient", "balance"] }
+      ],
+      "matchMode": "any",
+      "trigger": "tokenConfig.onBalanceInsufficient"
+    },
+    "unauthorized": {
+      "conditions": [
+        { "responseField": "code", "operator": "in", "value": [401, 403] },
+        { "responseField": "msg", "operator": "contains", "value": ["unauthorized", "token", "invalid", "auth"] }
+      ],
+      "matchMode": "any",
+      "trigger": "tokenConfig.onMissing"
+    }
+  }
+'@
+
+$categories = @("voc","social-media","competitor-analysis","review-analysis","synthesis","social-voc","douyin")
+$updated = 0
+$skipped = 0
+
+foreach ($cat in $categories) {
+    $catPath = Join-Path $SkillsRoot $cat
+    if (-not (Test-Path $catPath)) { continue }
+
+    Get-ChildItem -Recurse -Filter "api-config.json" -Path $catPath | ForEach-Object {
+        $filePath = $_.FullName
+        $content = [System.IO.File]::ReadAllText($filePath, [System.Text.Encoding]::UTF8)
+
+        # 跳过已处理的文件
+        if ($content -match '"tokenConfig"') {
+            Write-Host "  [SKIP] already patched: $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor DarkGray
+            $skipped++
+            return
+        }
+
+        # --- Step A: Authorization 改为 {{vocToken}}(仅 endpoint-based skill)---
+        $hasEndpoint = $content -match '"endpoint"'
+        if ($hasEndpoint) {
+            $content = $content.Replace(
+                "Bearer $hardcodedToken",
+                "Bearer {{vocToken}}"
+            )
+        }
+
+        # --- Step B: 插入 tokenConfig + errorHandling(最后一个 } 之前)---
+        $lastBrace = $content.LastIndexOf('}')
+        if ($lastBrace -lt 0) {
+            Write-Host "  [ERROR] malformed JSON: $filePath" -ForegroundColor Red
+            return
+        }
+
+        $newContent = $content.Substring(0, $lastBrace) + $injection + "`n}"
+
+        if ($DryRun) {
+            Write-Host "  [DRY] $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor Yellow
+        } else {
+            [System.IO.File]::WriteAllText($filePath, $newContent, $utf8NoBom)
+            Write-Host "  [OK] $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor Green
+        }
+        $updated++
+    }
+}
+
+Write-Host ""
+Write-Host "Done: $updated updated, $skipped skipped"
+if ($DryRun) { Write-Host "Remove -DryRun to apply." -ForegroundColor Yellow }

Разница между файлами не показана из-за своего большого размера
+ 33 - 0
scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.html


+ 389 - 0
scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.scss

@@ -0,0 +1,389 @@
+.apig-auth-page{
+  height: 100%;
+}
+.width1200 {
+  width: 1200px;
+  margin: 0 auto;
+  position: relative;
+}
+
+.cBlue {
+  color: #00bdff;
+}
+
+.ellipsis {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.apig-header{
+  padding: 20px 40px;
+  background: white;
+  font-size: 18px;
+}
+.dark-apig-header{
+  padding: 20px 40px;
+  background: #494949;
+  font-size: 18px;
+  color: white;
+}
+@media screen and (min-width: 800px) {
+  body {
+    background: #000;
+  }
+
+  .api-infos .title-api {
+    line-height: 40px;
+  }
+
+  .api-infos .title-api h1 {
+    font-size: 22px;
+    color: #555;
+    padding-right: 8px;
+    float: left;
+  }
+
+  .api-infos .title-api p {
+    float: left;
+  }
+
+  .api-infos .title-api p span {
+    margin-left: 5px;
+    border: 1px solid #00BDFF;
+    font-size: 12px;
+    padding: 2px 6px;
+    color: #00BDFF;
+    font-weight: normal;
+    vertical-align: top;
+    border-radius: 2px;
+  }
+
+  .api-infos .user-info {
+    line-height: 26px;
+    font-size: 14px;
+    font-weight: bold;
+    color: #f9a42a;
+    padding: 4px 0 6px;
+  }
+
+  .api-infos .user-info span {
+    padding-right: 30px;
+  }
+
+  .api-infos .user-info i {
+    font-size: 24px;
+    vertical-align: middle;
+    margin-right: 5px;
+    font-weight: normal;
+  }
+
+  .redfont {
+    color: #fb5b5b;
+  }
+
+  .api-infos .api-des-info {
+    line-height: 22px;
+    color: #666;
+    padding-bottom: 13px;
+    font-size: 14px;
+  }
+
+  #api-package-side {
+    min-height: 130px;
+  }
+}
+
+.api-mains-content {
+  margin: 20px auto;
+  border-radius: 6px;
+  position: relative;
+  z-index: 2;
+  // background-image:url('/src/assets/img/api/star.jpg') 0 0  no-repeat;
+  // background-size: 100% 100%;
+  background-color: #fff;
+  max-width: 1300px;
+  min-height: 50%;
+  .api-mains {
+    display: flex;
+    padding: 40px 0;
+
+    .red {
+      color: #fb5b5b;
+    }
+    .orange{
+      color: #f9a42a;
+    }
+    .api-img-des {
+      width: 260px;
+      height: 270px;
+      padding: 25px;
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      align-items: center;
+      border-radius: 2px;
+      background: #fff;
+      box-shadow: 0 0 10px 0 rgb(36 36 36 / 10%);
+      border: 1px solid #eee \9;
+
+      .idcard {
+        font-size: 120px;
+        margin-top: 20px;
+        width: 120px;
+        height: 120px;
+      }
+
+      .api-img-grid {
+        width: 100%;
+        text-align: center;
+        color: #bbb;
+      }
+
+    }
+
+    .api-infos {
+      padding: 10px 40px;
+
+      .title-api {
+        display: flex;
+        align-items: center;
+        h1 {
+          font-size: 22px;
+          color: #555;
+          padding-right: 8px;
+          float: left;
+          height: 40px;
+          line-height: normal;
+        }
+
+        div {
+          float: left;
+
+          span {
+            margin-left: 5px;
+            border: 1px solid #00BDFF;
+            font-size: 12px;
+            padding: 2px 6px;
+            color: #00BDFF;
+            font-weight: normal;
+            vertical-align: top;
+            border-radius: 2px;
+          }
+        }
+
+
+      }
+
+      .user-info {
+        line-height: 26px;
+        font-size: 14px;
+        font-weight: bold;
+        color: #f9a42a;
+        padding: 4px 0 6px;
+
+        span {
+          padding-right: 30px;
+
+        }
+
+        img {
+          width: 18px;
+          height: 18px;
+          vertical-align: sub;
+          margin-right: 5px;
+          font-weight: normal;
+        }
+
+
+      }
+
+      .api-des-info {
+        line-height: 22px;
+        color: #666;
+        padding-bottom: 13px;
+        font-size: 14px;
+      }
+
+      .api-buy-radio {
+        position: relative;
+
+        .show-demo-qrucode {
+          border: 1px solid #00aeff;
+          color: #00aeff;
+          background: #fff;
+          position: relative;
+          float: left;
+          cursor: pointer;
+          min-width: 100px;
+          height: 28px;
+          line-height: 28px;
+          text-align: center;
+          font-size: 12px;
+          border-radius: 2px;
+        }
+
+        .code-box {
+          position: absolute;
+          top: 30px;
+          left: -10px;
+          display: none;
+          width: 120px;
+          height: 150px;
+          padding: 5px 0;
+          margin-top: 0px;
+          margin-bottom: 0px;
+          background-color: #fff;
+          text-align: center;
+          z-index: 4;
+
+          img {
+            width: 100%;
+
+          }
+        }
+
+        .show-demo-qrucode:hover+.code-box {
+          display: block;
+        }
+        .buy-block{
+          display: flex;
+          .title-des{
+            color: #999;
+            width: 70px;
+            flex-shrink: 0;
+          }
+          .buy-list{
+            display: flex;
+            flex-wrap: wrap;
+          }
+          .buy-radio {
+            display: inline-block;
+            min-width: 74px;
+            font-weight: normal;
+            margin-left: 10px;
+            color: #666;
+            // height: 28px;
+            // line-height: 28px;
+            width: 200px;
+            font-size: 12px;
+            background: none;
+            text-align: center;
+            border-radius: 2px;
+            border: 1px solid #e9e9e9;
+            padding: 10px;
+            cursor: pointer;
+            margin-bottom: 10px;
+            .item-price{
+              display: flex;
+              align-items: center;
+              font-size: 20px;
+              color: #f9a42a;
+              font-weight: 600;
+              .price-tag{
+                color: #4c4c4c;
+                background-color: #f0f0f0;
+                font-size: 12px;
+                margin-left: 10px;
+                padding: 2px 4px;
+                border-radius: 4px;
+              }
+            }
+            .item-des{
+              color: #606060;
+              font-size: 14px;
+              text-align: left;
+              font-weight: 600;
+            }
+          }
+          .active-radio {
+            color: #00BDFF;
+            border: 1px solid #00BDFF;
+            background: url('https://www.juhe.cn/images/v6/docs//select.png') right -1px bottom 0 no-repeat;
+            box-shadow: 0 0 12px 0px #b8ddff;
+          }
+        }
+      }
+
+      .last-price {
+        height: 20px;
+        line-height: 20px;
+        padding: 10px 0 0;
+        font-size: 20px;
+        color: #666;
+        display: flex;
+        // border-bottom: 1px solid #dfdfdf;
+        .num-title{
+          color: #999;
+          // width: 70px;
+          flex-shrink: 0;
+          font-size: 14px;
+        }
+        span {
+          font-size: 14px;
+          color: #000;
+          font-weight: 600;
+        }
+      }
+      .bar{
+        border-bottom: 1px solid #e1e1e1;
+        margin: 20px auto;
+      }
+      .end-price{
+        display: flex;
+        align-items: center;
+        margin: 20px auto;
+        .price-title{
+          color: #000;
+          width: 70px;
+          flex-shrink: 0;
+          font-size: 14px;
+        }
+        .all-price{
+          font-size: 20px;
+          font-weight: 600;
+        }
+      }
+      .buy-apply-div {
+        margin-top: 10px;
+
+        .now-btn {
+          display: inline-block;
+          width: 166px;
+          height: 40px;
+          line-height: 40px;
+          background: #00BDFF;
+          color: #fff;
+          font-size: 16px;
+          font-weight: bold;
+          text-align: center;
+          border-radius: 2px;
+          margin-right: 10px;
+        }
+
+        .now-btn2 {
+          display: inline-block;
+          width: 100px;
+          font-weight: normal;
+          margin-left: 5px;
+          margin-right: 10px;
+          color: #00bdff;
+          height: 38px;
+          line-height: 38px;
+          font-size: 14px;
+          border: 1px solid #00bdff;
+          background: none;
+          text-align: center;
+          border-radius: 2px;
+        }
+      }
+
+
+    }
+  }
+}
+.code-wrapper {
+  width: 220px;
+  padding: 10px 0;
+  border: 6px double #00bdff;
+  box-shadow: 0px 0px 2px 2px #eee;
+}

+ 25 - 0
scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.spec.ts

@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SimpleApigAuthComponent } from './simple-apig-auth.component';
+
+describe('SimpleApigAuthComponent', () => {
+  let component: SimpleApigAuthComponent;
+  let fixture: ComponentFixture<SimpleApigAuthComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ SimpleApigAuthComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(SimpleApigAuthComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 209 - 0
scripts/apig-auth-ref/simple-apig-auth/simple-apig-auth.component.ts

@@ -0,0 +1,209 @@
+import { Component, OnInit, ViewChild } from "@angular/core";
+import { ActivatedRoute, Router } from "@angular/router";
+import Parse from "parse";
+import { HttpClient } from "@angular/common/http";
+import { NzMessageService } from "ng-zorro-antd/message";
+import { DeveloperServer } from "../../../service/developer";
+
+@Component({
+  selector: "app-simple-apig-auth",
+  templateUrl: "./simple-apig-auth.component.html",
+  styleUrls: ["./simple-apig-auth.component.scss"],
+})
+export class SimpleApigAuthComponent implements OnInit {
+  apigAuthId: any;
+  apig: any;
+  codeLink: string;
+  company: any;
+  // department: any;
+  user: any;
+  tradeNo: string;
+
+  index: number = 0;
+  price: number = 0;
+
+  count: number;
+  oldCount: number;
+
+  codeModal: boolean;
+  sucessModal: boolean = false;
+  timer: any; //定时器
+  order: any; // 创建的待支付订单
+
+  @ViewChild("custom", { static: false }) custom: any;
+  com:Parse.Object
+
+  constructor(
+    private activRoute: ActivatedRoute,
+    private http: HttpClient,
+    private message: NzMessageService,
+    public developerSer: DeveloperServer
+  ) {}
+  ngOnInit(): void {
+    this.activRoute.paramMap.subscribe((params) => {
+      this.apigAuthId = params.get("PobjectId");
+      this.company = params.get("company") || this.developerSer.companyId
+      // this.company = localStorage.getItem("company");
+      console.log(this.company);
+      this.getCom()
+      this.user = Parse.User.current();
+      this.getAuth();
+    });
+  }
+  async getCom(){
+    let query = new Parse.Query('Company')
+    query.equalTo('objectId',this.company)
+    query.select('name','title')
+    let com = await query.first()
+    if(com?.id){
+      this.com = com
+    }
+  }
+  async getAuth() {
+    let url = "https://server.fmode.cn/api/apig/getApig";
+    this.http.post(url, { authid: this.apigAuthId }).subscribe(
+      (data: any) => {
+        console.log(data);
+        this.apig = data.data;
+      },
+      (err) => {
+        this.message.error("网络错误,请稍后重试");
+      }
+    );
+  }
+
+  changeRadio(index) {
+    this.index = index;
+  }
+
+  async showCodeModal(type) {
+    switch (type) {
+      case "wxpay":
+        this.wxPay();
+        break;
+      default:
+        break;
+    }
+  }
+
+  async wxPay() {
+    this.tradeNo = this.setTradeNo();
+    console.log(this.company, this.tradeNo);
+    let title = this.apig.title;
+    let params = {
+      company: "1AiWpTEDH9",
+      out_trade_no: this.tradeNo,
+      total_fee: +this.apig.priceStep[this.index].price,
+      body: title + "接口充值",
+    };
+    try {
+      let _this = this;
+      Parse.Cloud.run("pay_code2", params).then(async (res) => {
+        await this.setOrder("wxpay", params);
+        let nonce_str = res.nonce_str;
+        this.codeLink = res.code_url[0];
+        this.codeModal = true;
+        // 定时查询订单支付状态
+        _this.timer = setInterval(function () {
+          let info = {
+            out_trade_no: _this.tradeNo,
+            nonce_str: nonce_str,
+            company: "1AiWpTEDH9",
+          };
+          console.log(info);
+          Parse.Cloud.run("order_status2", info)
+            .then(async (res) => {
+              if (res.status && res.status[0] == "SUCCESS") {
+                clearInterval(_this.timer);
+                await _this.saveRecharge();
+                _this.message.success("支付成功");
+                _this.codeModal = false;
+                _this.sucessModal = true;
+              }
+            })
+            .catch((err) => {});
+        }, 3000);
+      });
+    } catch (error) {
+      alert(error);
+    }
+  }
+
+  // 存支付记录
+  async setOrder(type, params) {
+    let url = "https://server.fmode.cn/api/apig/created-apigorder";
+    this.http
+      .post(url, {
+        fcompany: this.company,
+        type: type,
+        authid: this.apigAuthId,
+        params: params,
+        apigid: this.apig.objectId,
+        oldCount: this.apig.count,
+        count: this.apig.priceStep[this.index].count,
+      })
+      .subscribe(
+        (data: any) => {
+          console.log(data);
+          this.order = data.data;
+        },
+        (err) => {
+          this.message.error("网络错误,订单创建失败");
+        }
+      );
+  }
+
+  setTradeNo() {
+    let now = new Date();
+    let tradeNo =
+      "C" +
+      this.company +
+      String(now.getFullYear()) +
+      (now.getMonth() + 1) +
+      now.getDate() +
+      now.getHours() +
+      now.getMinutes() +
+      now.getSeconds() +
+      now.getMilliseconds();
+    return tradeNo;
+  }
+
+  cancelPush(type) {
+    switch (type) {
+      case "code":
+        this.codeModal = false;
+        clearInterval(this.timer);
+        break;
+
+      default:
+        this.sucessModal = false;
+        break;
+    }
+  }
+
+  async saveRecharge() {
+    let url = "https://server.fmode.cn/api/apig/saveRecharge";
+    console.log(this.order);
+    this.http
+      .post(url, {
+        authComp: this.company,
+        authid: this.apigAuthId,
+        apigid: this.apig.objectId,
+        oldCount: this.apig.count,
+        count: this.apig.priceStep[this.index].count,
+        orderid: this.order.objectId,
+      })
+      .subscribe(
+        (data: any) => {
+          console.log(data);
+          // this.apig = data.data;
+        },
+        (err) => {
+          this.message.error("网络错误,请稍后重试");
+        }
+      );
+  }
+  back(){
+    history.back()
+  }
+}

+ 22 - 0
scripts/debug-single.ps1

@@ -0,0 +1,22 @@
+$SkillsRoot = "e:\workspace\openclaw-voc-skill"
+$catPath = Join-Path $SkillsRoot "voc"
+
+Get-ChildItem -Recurse -Filter "api-config.json" -Path $catPath | Select-Object -First 1 | ForEach-Object {
+    $filePath = $_.FullName
+    Write-Host "File: $filePath"
+
+    $content = Get-Content -Path $filePath -Raw -Encoding UTF8
+    Write-Host "Content null: $($null -eq $content)"
+    Write-Host "Content length: $($content.Length)"
+
+    $search = '"tokenConfig"'
+    Write-Host "Search string length: $($search.Length)"
+    Write-Host "Search string chars: $(($search.ToCharArray() | ForEach-Object { [int]$_ }) -join ',')"
+
+    $idx = $content.IndexOf($search)
+    Write-Host "IndexOf result: $idx"
+
+    # Also try without quotes
+    $idx2 = $content.IndexOf("tokenConfig")
+    Write-Host "IndexOf no-quotes: $idx2"
+}

+ 69 - 0
scripts/fix-token-config.ps1

@@ -0,0 +1,69 @@
+# fix-token-config.ps1 (ASCII-only comments to avoid PS5 encoding issues)
+# Strategy: find "tokenConfig" key, cut back to last } before it, re-inject from UTF-8 file
+
+param(
+    [string]$SkillsRoot    = (Split-Path $PSScriptRoot -Parent),
+    [string]$InjectionFile = (Join-Path $PSScriptRoot "token-config-injection.txt"),
+    [switch]$DryRun
+)
+
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+if (-not (Test-Path $InjectionFile)) {
+    Write-Error "Injection file not found: $InjectionFile"
+    exit 1
+}
+
+$injection = [System.IO.File]::ReadAllText($InjectionFile, [System.Text.Encoding]::UTF8)
+$injection = $injection.TrimEnd()
+
+$categories = @("voc","social-media","competitor-analysis","review-analysis","synthesis","social-voc","douyin")
+$fixed   = 0
+$skipped = 0
+
+foreach ($cat in $categories) {
+    $catPath = Join-Path $SkillsRoot $cat
+    if (-not (Test-Path $catPath)) { continue }
+
+    Get-ChildItem -Recurse -Filter "api-config.json" -Path $catPath | ForEach-Object {
+        $filePath = $_.FullName
+        $relPath  = $filePath.Replace($SkillsRoot + '\', '')
+
+        $content = Get-Content -Path $filePath -Raw -Encoding UTF8
+        if ([string]::IsNullOrEmpty($content)) {
+            Write-Warning "  [WARN] empty: $relPath"
+            $skipped++
+            return
+        }
+
+        $searchKey = [string]'"tokenConfig"'
+        $tcIdx = $content.IndexOf($searchKey)
+        if ($tcIdx -lt 0) {
+            Write-Host "  [SKIP] no tokenConfig: $relPath" -ForegroundColor DarkGray
+            $skipped++
+            return
+        }
+
+        $cutPos = $content.LastIndexOf([string]'}', $tcIdx - 1)
+        if ($cutPos -lt 0) {
+            Write-Warning "  [WARN] no cut point: $relPath"
+            $skipped++
+            return
+        }
+
+        $base       = $content.Substring(0, $cutPos + 1).TrimEnd()
+        $newContent = $base + ",`r`n" + $injection + "`r`n}"
+
+        if ($DryRun) {
+            Write-Host "  [DRY] $relPath" -ForegroundColor Yellow
+        } else {
+            [System.IO.File]::WriteAllText($filePath, $newContent, $utf8NoBom)
+            Write-Host "  [OK] $relPath" -ForegroundColor Green
+        }
+        $fixed++
+    }
+}
+
+Write-Host ""
+Write-Host "Done: $fixed fixed, $skipped skipped"
+if ($DryRun) { Write-Host "Remove -DryRun to apply." -ForegroundColor Yellow }

BIN
scripts/simple-apig-auth.zip


+ 66 - 0
scripts/test-apig-cloudfunction.ps1

@@ -0,0 +1,66 @@
+# Test different cloud function approaches to get APIG data
+$headers = @{ "Content-Type" = "application/json" }
+$appId = "ncloudmaster"
+
+# Try 1: getApig with id field
+Write-Host "=== Try getApig with id ===" -ForegroundColor Cyan
+try {
+    $body = @{ id = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+
+Write-Host ""
+
+# Try 2: getApig with apigId
+Write-Host "=== Try getApig with apigId ===" -ForegroundColor Cyan
+try {
+    $body = @{ apigId = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+
+Write-Host ""
+
+# Try 3: getApig with objectId
+Write-Host "=== Try getApig with objectId ===" -ForegroundColor Cyan
+try {
+    $body = @{ objectId = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+
+Write-Host ""
+
+# Try 4: Cloud function approach
+Write-Host "=== Try cloud function getApig ===" -ForegroundColor Cyan
+try {
+    $body = @{ _ApplicationId = $appId; objectId = "Vo3ROWEvDy"; className = "APIG" } | ConvertTo-Json -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/getApig" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+
+Write-Host ""
+
+# Try 5: Generic getObject cloud function
+Write-Host "=== Try cloud function getObject ===" -ForegroundColor Cyan
+try {
+    $body = @{ _ApplicationId = $appId; className = "APIG"; objectId = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/getObject" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+
+Write-Host ""
+
+# Try 6: REST fetch-style approach (Query endpoint)
+Write-Host "=== Try REST query with where ===" -ForegroundColor Cyan
+try {
+    $body = @{
+        _ApplicationId = $appId
+        className = "APIG"
+        where = @{ objectId = "Vo3ROWEvDy" }
+        limit = 1
+    } | ConvertTo-Json -Depth 3 -Compress
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/find" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5)
+} catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }

+ 132 - 0
scripts/test-apig-flow.ps1

@@ -0,0 +1,132 @@
+# Test APIG billing flow - all endpoints
+$utf8 = [System.Text.Encoding]::UTF8
+
+# Social media APIG id
+$apigId = "Vo3ROWEvDy"
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "  APIG Billing Flow - Connectivity Test" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host ""
+
+# --- Step 1: getApig ---
+Write-Host "=== Step 1: getApig (authid=$apigId) ===" -ForegroundColor Cyan
+$body1 = @{ authid = $apigId } | ConvertTo-Json -Compress
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}
+
+Write-Host ""
+
+# --- Step 2: Try pay_code2 (generate QR code) ---
+Write-Host "=== Step 2: pay_code2 (generate QR code) ===" -ForegroundColor Cyan
+$now = Get-Date
+$tradeNo = "CTEST" + $now.ToString("yyyyMMddHHmmssfff")
+Write-Host "TradeNo: $tradeNo"
+
+# pay_code2 is a Parse Cloud Function
+$payBody = @{
+    _ApplicationId = "ncloudmaster"
+    company = "1AiWpTEDH9"
+    out_trade_no = $tradeNo
+    total_fee = 0.01
+    body = "VOC Social API Test"
+} | ConvertTo-Json -Compress
+
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/pay_code2" -Method POST -ContentType "application/json" -Body $payBody -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}
+
+Write-Host ""
+
+# --- Step 3: created-apigorder (with user field per Chen Bin's update) ---
+Write-Host "=== Step 3: created-apigorder ===" -ForegroundColor Cyan
+$orderBody = @{
+    user = "test-openclaw-user"
+    type = "wxpay"
+    authid = $apigId
+    params = @{
+        out_trade_no = $tradeNo
+        total_fee = 0.01
+        body = "VOC Social API Test"
+    }
+    apigid = $apigId
+    oldCount = 0
+    count = 100
+} | ConvertTo-Json -Depth 3 -Compress
+
+try {
+    $r3 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/created-apigorder" -Method POST -ContentType "application/json" -Body $orderBody -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r3 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}
+
+Write-Host ""
+
+# --- Step 4: order_status2 (check payment status) ---
+Write-Host "=== Step 4: order_status2 (check payment status) ===" -ForegroundColor Cyan
+$statusBody = @{
+    _ApplicationId = "ncloudmaster"
+    out_trade_no = $tradeNo
+    nonce_str = "test_nonce"
+    company = "1AiWpTEDH9"
+} | ConvertTo-Json -Compress
+
+try {
+    $r4 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/order_status2" -Method POST -ContentType "application/json" -Body $statusBody -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r4 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}
+
+Write-Host ""
+
+# --- Step 5: saveRecharge (just test endpoint, won't actually save) ---
+Write-Host "=== Step 5: saveRecharge (endpoint test) ===" -ForegroundColor Cyan
+$rechargeBody = @{
+    user = "test-openclaw-user"
+    authid = $apigId
+    apigid = "test-apig-auth-id"
+    oldCount = 0
+    count = 100
+    orderid = "test-order-id"
+} | ConvertTo-Json -Compress
+
+try {
+    $r5 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/saveRecharge" -Method POST -ContentType "application/json" -Body $rechargeBody -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r5 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}

+ 92 - 0
scripts/test-apig-flow2.ps1

@@ -0,0 +1,92 @@
+# Retry timed-out endpoints with longer timeout + get full getApig data
+
+$apigId = "Vo3ROWEvDy"
+
+# --- Step 1: getApig full response ---
+Write-Host "=== Step 1: getApig (full) ===" -ForegroundColor Cyan
+$body1 = @{ authid = $apigId } | ConvertTo-Json -Compress
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 30
+    $json1 = $r1 | ConvertTo-Json -Depth 10
+    Write-Host $json1
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+Write-Host "---" 
+Write-Host ""
+
+# --- Step 3 retry: created-apigorder with fcompany (original field) ---
+Write-Host "=== Step 3a: created-apigorder (fcompany, original) ===" -ForegroundColor Cyan
+$now = Get-Date
+$tradeNo = "CTEST" + $now.ToString("yyyyMMddHHmmssfff")
+
+$orderBody3a = @{
+    fcompany = "test-openclaw-user"
+    type = "wxpay"
+    authid = $apigId
+    params = @{
+        out_trade_no = $tradeNo
+        total_fee = 0.01
+        body = "test"
+    }
+    apigid = $apigId
+    oldCount = 0
+    count = 100
+} | ConvertTo-Json -Depth 3 -Compress
+
+try {
+    $r3a = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/created-apigorder" -Method POST -ContentType "application/json" -Body $orderBody3a -TimeoutSec 60
+    Write-Host "Response (fcompany):" -ForegroundColor Green
+    $r3a | ConvertTo-Json -Depth 8
+} catch {
+    Write-Host "Error (fcompany): $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# --- Step 3b: created-apigorder with user (new field per Chen Bin) ---
+Write-Host "=== Step 3b: created-apigorder (user, new) ===" -ForegroundColor Cyan
+$orderBody3b = @{
+    user = "test-openclaw-user"
+    type = "wxpay"
+    authid = $apigId
+    params = @{
+        out_trade_no = $tradeNo
+        total_fee = 0.01
+        body = "test"
+    }
+    apigid = $apigId
+    oldCount = 0
+    count = 100
+} | ConvertTo-Json -Depth 3 -Compress
+
+try {
+    $r3b = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/created-apigorder" -Method POST -ContentType "application/json" -Body $orderBody3b -TimeoutSec 60
+    Write-Host "Response (user):" -ForegroundColor Green
+    $r3b | ConvertTo-Json -Depth 8
+} catch {
+    Write-Host "Error (user): $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# --- Step 5 retry: saveRecharge ---
+Write-Host "=== Step 5: saveRecharge (endpoint availability) ===" -ForegroundColor Cyan
+$rechargeBody = @{
+    authComp = "test"
+    authid = $apigId
+    apigid = "test"
+    oldCount = 0
+    count = 100
+    orderid = "test"
+} | ConvertTo-Json -Compress
+
+try {
+    $r5 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/saveRecharge" -Method POST -ContentType "application/json" -Body $rechargeBody -TimeoutSec 60
+    Write-Host "Response:" -ForegroundColor Green
+    $r5 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 90 - 0
scripts/test-apig-real-user.ps1

@@ -0,0 +1,90 @@
+# Test the full payment flow with a real userId
+# Step 1: Create order with real userId
+# Step 2: Generate QR code
+# Step 3: Check if saveRecharge works with real userId
+
+$realUserId = "nd7NOCmFiE"
+$apigId = "Vo3ROWEvDy"
+$ct = @{ "Content-Type" = "application/json" }
+
+Write-Host "=== Test with real userId: $realUserId ===" -ForegroundColor Cyan
+Write-Host ""
+
+# Step 1: Create order with real userId (using fcompany which works)
+$now = Get-Date
+$tradeNo = "CREAL" + $now.ToString("yyyyMMddHHmmssfff")
+Write-Host "TradeNo: $tradeNo"
+
+$orderBody = @{
+    fcompany = $realUserId
+    user = $realUserId
+    type = "wxpay"
+    authid = $apigId
+    params = @{
+        out_trade_no = $tradeNo
+        total_fee = 0.01
+        body = "test recharge"
+    }
+    apigid = $apigId
+    oldCount = 99999
+    count = 1
+} | ConvertTo-Json -Depth 3 -Compress
+
+Write-Host "=== Creating order ===" -ForegroundColor Cyan
+try {
+    $orderResp = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/created-apigorder" -Method POST -ContentType "application/json" -Body $orderBody -TimeoutSec 60
+    Write-Host "Order response:" -ForegroundColor Green
+    $orderResp | ConvertTo-Json -Depth 8
+    $orderId = $orderResp.data.objectId
+    $apigAuthId = $orderResp.data.detail.apigAuth.objectId
+    Write-Host ""
+    Write-Host "orderId: $orderId" -ForegroundColor Yellow
+    Write-Host "apigAuthId: $apigAuthId" -ForegroundColor Yellow
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    $orderId = ""
+    $apigAuthId = ""
+}
+
+Write-Host ""
+
+# Step 2: Test saveRecharge with real userId and order
+if ($orderId) {
+    Write-Host "=== Testing saveRecharge ===" -ForegroundColor Cyan
+    $rechargeBody = @{
+        user = $realUserId
+        authComp = $realUserId
+        fcompany = $realUserId
+        authid = $apigId
+        apigid = $apigAuthId
+        oldCount = 99999
+        count = 1
+        orderid = $orderId
+    } | ConvertTo-Json -Compress
+    
+    Write-Host "Recharge body: $rechargeBody"
+    
+    try {
+        $rechargeResp = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/saveRecharge" -Method POST -ContentType "application/json" -Body $rechargeBody -TimeoutSec 60
+        Write-Host "Recharge response:" -ForegroundColor Green
+        $rechargeResp | ConvertTo-Json -Depth 5
+    } catch {
+        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+        if ($_.Exception.Response) {
+            $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+            Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+        }
+    }
+}
+
+Write-Host ""
+
+# Step 3: Check if getApig now returns data (APIGAuth might have been created)
+Write-Host "=== Check getApig after order ===" -ForegroundColor Cyan
+$body3 = @{ authid = $apigId } | ConvertTo-Json -Compress
+try {
+    $r3 = Invoke-WebRequest -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body3 -TimeoutSec 15
+    Write-Host "getApig response: $($r3.Content)" -ForegroundColor Green
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 52 - 0
scripts/test-apig-recharge.ps1

@@ -0,0 +1,52 @@
+# Test saveRecharge with real order data from step 3
+# Order created: objectId=IqV8pb4PXw, detail.apigAuth.objectId=Vo3ROWEvDy
+
+$apigId = "Vo3ROWEvDy"
+
+# Test with fcompany (original field)
+Write-Host "=== saveRecharge (authComp, original) ===" -ForegroundColor Cyan
+$body1 = @{
+    authComp = "test-openclaw-user"
+    authid = $apigId
+    apigid = $apigId
+    oldCount = 0
+    count = 100
+    orderid = "IqV8pb4PXw"
+} | ConvertTo-Json -Compress
+
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/saveRecharge" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 60
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}
+
+Write-Host ""
+
+# Test with user (new field)
+Write-Host "=== saveRecharge (user, new) ===" -ForegroundColor Cyan
+$body2 = @{
+    user = "test-openclaw-user"
+    authid = $apigId
+    apigid = $apigId
+    oldCount = 0
+    count = 100
+    orderid = "IqV8pb4PXw"
+} | ConvertTo-Json -Compress
+
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/saveRecharge" -Method POST -ContentType "application/json" -Body $body2 -TimeoutSec 60
+    Write-Host "Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    }
+}

+ 36 - 0
scripts/test-apig-routes.ps1

@@ -0,0 +1,36 @@
+# Test alternative API routes for getting APIG data
+$ct = @{ "Content-Type" = "application/json" }
+$id = "Vo3ROWEvDy"
+
+# Try different REST route patterns
+$routes = @(
+    @{ url = "https://server.fmode.cn/api/apig/getApigById"; body = @{ objectId = $id } },
+    @{ url = "https://server.fmode.cn/api/apig/detail"; body = @{ id = $id } },
+    @{ url = "https://server.fmode.cn/api/apig/info"; body = @{ id = $id } },
+    @{ url = "https://server.fmode.cn/api/apig/get"; body = @{ id = $id } },
+    @{ url = "https://server.fmode.cn/api/apig/getApig"; body = @{ authid = $id; apigId = $id } }
+)
+
+foreach ($r in $routes) {
+    Write-Host "=== $($r.url) ===" -ForegroundColor Cyan
+    $jsonBody = $r.body | ConvertTo-Json -Compress
+    try {
+        $resp = Invoke-RestMethod -Uri $r.url -Method POST -Headers $ct -Body $jsonBody -TimeoutSec 8
+        Write-Host "OK:" ($resp | ConvertTo-Json -Depth 3 -Compress) -ForegroundColor Green
+    } catch {
+        $code = ""
+        if ($_.Exception.Response) { $code = $_.Exception.Response.StatusCode }
+        Write-Host "Error: $code $($_.Exception.Message)" -ForegroundColor Red
+    }
+    Write-Host ""
+}
+
+# Also list API routes by requesting root
+Write-Host "=== Try /api/apig ===" -ForegroundColor Cyan
+try {
+    $resp = Invoke-WebRequest -Uri "https://server.fmode.cn/api/apig" -Method GET -TimeoutSec 8
+    Write-Host "Status:" $resp.StatusCode
+    Write-Host $resp.Content.Substring(0, [Math]::Min(500, $resp.Content.Length))
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 36 - 0
scripts/test-cloudfunc-name.ps1

@@ -0,0 +1,36 @@
+# Query CloudFunction class to find actual function names
+$headers = @{ "X-Parse-Application-Id" = "ncloudmaster" }
+
+# Try to get the CloudFunction record by objectId
+Write-Host "=== CloudFunction t012qy4XVe ===" -ForegroundColor Cyan
+try {
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/CloudFunction/t012qy4XVe" -Method GET -Headers $headers -TimeoutSec 10
+    Write-Host ($r | ConvertTo-Json -Depth 5) -ForegroundColor Green
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+Write-Host "=== CloudFunction H7X7y9Wfc0 ===" -ForegroundColor Cyan
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/CloudFunction/H7X7y9Wfc0" -Method GET -Headers $headers -TimeoutSec 10
+    Write-Host ($r2 | ConvertTo-Json -Depth 5) -ForegroundColor Green
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# Also try FunctionDeploy or other class names
+foreach ($cls in @("FunctionDeploy", "ServerFunction", "ParseFunction", "Function")) {
+    Write-Host "=== $cls/t012qy4XVe ===" -ForegroundColor Cyan
+    try {
+        $r3 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/$cls/t012qy4XVe" -Method GET -Headers $headers -TimeoutSec 5
+        Write-Host ($r3 | ConvertTo-Json -Depth 3) -ForegroundColor Green
+    } catch {
+        $code = ""
+        if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode }
+        Write-Host "$code" -ForegroundColor Yellow
+    }
+}

+ 54 - 0
scripts/test-cloudfunc-name2.ps1

@@ -0,0 +1,54 @@
+# Test cloud functions by actual name (not ID)
+$ct = "application/json"
+$appId = "ncloudmaster"
+
+# Test getApigInfo with X-Parse-Application-Id header
+Write-Host "=== getApigInfo (header auth) ===" -ForegroundColor Cyan
+try {
+    $headers = @{ "X-Parse-Application-Id" = $appId; "Content-Type" = $ct }
+    $body = '{"apigId":"Vo3ROWEvDy"}'
+    $r = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/getApigInfo" -Method POST -Headers $headers -Body $body -TimeoutSec 15
+    Write-Host "Status: $($r.StatusCode)  Body: $($r.Content)" -ForegroundColor Green
+} catch {
+    $code = ""; $body = ""
+    if ($_.Exception.Response) {
+        $code = [int]$_.Exception.Response.StatusCode
+        try { $stream = $_.Exception.Response.GetResponseStream(); $reader = New-Object System.IO.StreamReader($stream); $body = $reader.ReadToEnd() } catch {}
+    }
+    Write-Host "Status: $code  Body: $body" -ForegroundColor Yellow
+}
+
+Write-Host ""
+
+# Test getApigInfo with _ApplicationId in body
+Write-Host "=== getApigInfo (body auth) ===" -ForegroundColor Cyan
+try {
+    $body = '{"_ApplicationId":"ncloudmaster","apigId":"Vo3ROWEvDy"}'
+    $r2 = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/getApigInfo" -Method POST -ContentType $ct -Body $body -TimeoutSec 15
+    Write-Host "Status: $($r2.StatusCode)  Body: $($r2.Content)" -ForegroundColor Green
+} catch {
+    $code = ""; $body = ""
+    if ($_.Exception.Response) {
+        $code = [int]$_.Exception.Response.StatusCode
+        try { $stream = $_.Exception.Response.GetResponseStream(); $reader = New-Object System.IO.StreamReader($stream); $body = $reader.ReadToEnd() } catch {}
+    }
+    Write-Host "Status: $code  Body: $body" -ForegroundColor Yellow
+}
+
+Write-Host ""
+
+# Test getApigAuth
+Write-Host "=== getApigAuth (header auth) ===" -ForegroundColor Cyan
+try {
+    $headers = @{ "X-Parse-Application-Id" = $appId; "Content-Type" = $ct }
+    $body = '{"userId":"nd7NOCmFiE","apigId":"Vo3ROWEvDy"}'
+    $r3 = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/getApigAuth" -Method POST -Headers $headers -Body $body -TimeoutSec 15
+    Write-Host "Status: $($r3.StatusCode)  Body: $($r3.Content)" -ForegroundColor Green
+} catch {
+    $code = ""; $body = ""
+    if ($_.Exception.Response) {
+        $code = [int]$_.Exception.Response.StatusCode
+        try { $stream = $_.Exception.Response.GetResponseStream(); $reader = New-Object System.IO.StreamReader($stream); $body = $reader.ReadToEnd() } catch {}
+    }
+    Write-Host "Status: $code  Body: $body" -ForegroundColor Yellow
+}

+ 33 - 0
scripts/test-cloudfunc.ps1

@@ -0,0 +1,33 @@
+# Test the deployed cloud functions
+$ct = @{ "Content-Type" = "application/json" }
+
+# Test getApigInfo (t012qy4XVe)
+Write-Host "=== getApigInfo ===" -ForegroundColor Cyan
+$body1 = @{
+    _ApplicationId = "ncloudmaster"
+    apigId = "Vo3ROWEvDy"
+} | ConvertTo-Json -Compress
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/t012qy4XVe" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# Test getApigAuth (H7X7y9Wfc0)
+Write-Host "=== getApigAuth ===" -ForegroundColor Cyan
+$body2 = @{
+    _ApplicationId = "ncloudmaster"
+    userId = "nd7NOCmFiE"
+    apigId = "Vo3ROWEvDy"
+} | ConvertTo-Json -Compress
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/functions/H7X7y9Wfc0" -Method POST -ContentType "application/json" -Body $body2 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 65 - 0
scripts/test-cloudfunc3.ps1

@@ -0,0 +1,65 @@
+# Test cloud functions - get detailed error info
+$ct = "application/json"
+
+# Test 1: getApigInfo without _ApplicationId
+Write-Host "=== Test 1: No AppId ===" -ForegroundColor Cyan
+try {
+    $body = '{"apigId":"Vo3ROWEvDy"}'
+    $r = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/t012qy4XVe" -Method POST -ContentType $ct -Body $body -TimeoutSec 10
+    Write-Host "Status: $($r.StatusCode)  Body: $($r.Content)"
+} catch {
+    if ($_.Exception.Response) {
+        $stream = $_.Exception.Response.GetResponseStream()
+        $reader = New-Object System.IO.StreamReader($stream)
+        Write-Host "Status: $($_.Exception.Response.StatusCode)  Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    } else { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+}
+
+Write-Host ""
+
+# Test 2: With _ApplicationId in body
+Write-Host "=== Test 2: AppId in body ===" -ForegroundColor Cyan
+try {
+    $body = '{"_ApplicationId":"ncloudmaster","apigId":"Vo3ROWEvDy"}'
+    $r = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/t012qy4XVe" -Method POST -ContentType $ct -Body $body -TimeoutSec 10
+    Write-Host "Status: $($r.StatusCode)  Body: $($r.Content)"
+} catch {
+    if ($_.Exception.Response) {
+        $stream = $_.Exception.Response.GetResponseStream()
+        $reader = New-Object System.IO.StreamReader($stream)
+        Write-Host "Status: $($_.Exception.Response.StatusCode)  Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    } else { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+}
+
+Write-Host ""
+
+# Test 3: With X-Parse-Application-Id header
+Write-Host "=== Test 3: AppId in header ===" -ForegroundColor Cyan
+try {
+    $headers = @{ "X-Parse-Application-Id" = "ncloudmaster"; "Content-Type" = $ct }
+    $body = '{"apigId":"Vo3ROWEvDy"}'
+    $r = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/t012qy4XVe" -Method POST -Headers $headers -Body $body -TimeoutSec 10
+    Write-Host "Status: $($r.StatusCode)  Body: $($r.Content)"
+} catch {
+    if ($_.Exception.Response) {
+        $stream = $_.Exception.Response.GetResponseStream()
+        $reader = New-Object System.IO.StreamReader($stream)
+        Write-Host "Status: $($_.Exception.Response.StatusCode)  Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    } else { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+}
+
+Write-Host ""
+
+# Test 4: Same as pay_code2 pattern (which works in the page)
+Write-Host "=== Test 4: pay_code2 pattern (reference) ===" -ForegroundColor Cyan
+try {
+    $body = '{"trade_no":"TEST123","total_fee":0.01,"body":"test","company":"1AiWpTEDH9"}'
+    $r = Invoke-WebRequest -Uri "https://server.fmode.cn/parse/functions/pay_code2" -Method POST -ContentType $ct -Body $body -TimeoutSec 10
+    Write-Host "Status: $($r.StatusCode)  Body: $($r.Content.Substring(0,200))" -ForegroundColor Green
+} catch {
+    if ($_.Exception.Response) {
+        $stream = $_.Exception.Response.GetResponseStream()
+        $reader = New-Object System.IO.StreamReader($stream)
+        Write-Host "Status: $($_.Exception.Response.StatusCode)  Body: $($reader.ReadToEnd())" -ForegroundColor Yellow
+    } else { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red }
+}

+ 42 - 0
scripts/test-cloudfunc4.ps1

@@ -0,0 +1,42 @@
+# Explore FmodeParse cloud function REST endpoint patterns
+$ct = "application/json"
+$appId = "ncloudmaster"
+$funcId = "t012qy4XVe"
+$headers = @{ "X-Parse-Application-Id" = $appId; "Content-Type" = $ct }
+
+$patterns = @(
+    @{ name = "cloudRun"; url = "https://server.fmode.cn/parse/functions/cloudRun"; body = @{ functionId = $funcId; params = @{ apigId = "Vo3ROWEvDy" } } },
+    @{ name = "cloudFunction"; url = "https://server.fmode.cn/parse/functions/cloudFunction"; body = @{ id = $funcId; apigId = "Vo3ROWEvDy" } },
+    @{ name = "runFunction"; url = "https://server.fmode.cn/parse/functions/runFunction"; body = @{ id = $funcId; apigId = "Vo3ROWEvDy" } },
+    @{ name = "api/function"; url = "https://server.fmode.cn/api/function/$funcId"; body = @{ apigId = "Vo3ROWEvDy" } },
+    @{ name = "api/cloudfunction"; url = "https://server.fmode.cn/api/cloudfunction/$funcId"; body = @{ apigId = "Vo3ROWEvDy" } },
+    @{ name = "api/cloudfunction/run"; url = "https://server.fmode.cn/api/cloudfunction/run"; body = @{ id = $funcId; apigId = "Vo3ROWEvDy" } },
+    @{ name = "parse/functions/id+header"; url = "https://server.fmode.cn/parse/functions/$funcId"; body = @{ apigId = "Vo3ROWEvDy" }; useHeaders = $true },
+    @{ name = "api/cql/run"; url = "https://server.fmode.cn/api/cql/run"; body = @{ id = $funcId; params = @{ apigId = "Vo3ROWEvDy" } } }
+)
+
+foreach ($p in $patterns) {
+    Write-Host "=== $($p.name): $($p.url) ===" -ForegroundColor Cyan
+    $jsonBody = $p.body | ConvertTo-Json -Depth 3 -Compress
+    try {
+        if ($p.useHeaders) {
+            $r = Invoke-WebRequest -Uri $p.url -Method POST -Headers $headers -Body $jsonBody -TimeoutSec 8
+        } else {
+            $r = Invoke-WebRequest -Uri $p.url -Method POST -ContentType $ct -Body $jsonBody -TimeoutSec 8
+        }
+        Write-Host "  Status: $($r.StatusCode)  Body: $($r.Content.Substring(0, [Math]::Min(300, $r.Content.Length)))" -ForegroundColor Green
+    } catch {
+        $code = "?"
+        $body = ""
+        if ($_.Exception.Response) {
+            $code = [int]$_.Exception.Response.StatusCode
+            try {
+                $stream = $_.Exception.Response.GetResponseStream()
+                $reader = New-Object System.IO.StreamReader($stream)
+                $body = $reader.ReadToEnd()
+            } catch {}
+        }
+        Write-Host "  $code : $body" -ForegroundColor Yellow
+    }
+    Write-Host ""
+}

+ 17 - 0
scripts/test-find-apigauth.ps1

@@ -0,0 +1,17 @@
+# Find APIGAuth records linked to APIG Vo3ROWEvDy
+$headers = @{ "X-Parse-Application-Id" = "ncloudmaster" }
+$where = '{"api":{"__type":"Pointer","className":"APIG","objectId":"Vo3ROWEvDy"}}'
+$encoded = [System.Uri]::EscapeDataString($where)
+$url = "https://server.fmode.cn/parse/classes/APIGAuth?where=$encoded"
+
+Write-Host "=== APIGAuth records for APIG Vo3ROWEvDy ===" -ForegroundColor Cyan
+try {
+    $r = Invoke-RestMethod -Uri $url -Method GET -Headers $headers -TimeoutSec 15
+    Write-Host "Found $($r.results.Count) records" -ForegroundColor Green
+    foreach ($rec in $r.results) {
+        Write-Host "  objectId: $($rec.objectId)  count: $($rec.count)  user: $($rec.user.objectId)  company: $($rec.company.objectId)" -ForegroundColor Yellow
+    }
+    $r.results | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 40 - 0
scripts/test-find-user.ps1

@@ -0,0 +1,40 @@
+# Query Parse _User class to find a real user ID for testing
+$headers = @{ "X-Parse-Application-Id" = "ncloudmaster" }
+
+# List a few users (limit 5)
+Write-Host "=== Query _User (limit 5) ===" -ForegroundColor Cyan
+try {
+    $encoded = [System.Uri]::EscapeDataString('{"_created_at":{"$gt":{"__type":"Date","iso":"2025-01-01T00:00:00.000Z"}}}')
+    $url = "https://server.fmode.cn/parse/classes/_User?limit=5&order=-createdAt&keys=objectId,username,email,createdAt&where=$encoded"
+    $r = Invoke-RestMethod -Uri $url -Method GET -Headers $headers -TimeoutSec 15
+    Write-Host "Users found:" -ForegroundColor Green
+    $r.results | ForEach-Object {
+        Write-Host "  objectId=$($_.objectId)  username=$($_.username)  created=$($_.createdAt)"
+    }
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    # Try without where clause
+    Write-Host "Trying without filter..." -ForegroundColor Yellow
+    try {
+        $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/_User?limit=3&order=-createdAt&keys=objectId,username,createdAt" -Method GET -Headers $headers -TimeoutSec 15
+        $r2.results | ForEach-Object {
+            Write-Host "  objectId=$($_.objectId)  username=$($_.username)  created=$($_.createdAt)"
+        }
+    } catch {
+        Write-Host "Also failed: $($_.Exception.Message)" -ForegroundColor Red
+    }
+}
+
+Write-Host ""
+
+# Also check APIGAuth for any existing records linked to Vo3ROWEvDy
+Write-Host "=== Query APIGAuth linked to APIG Vo3ROWEvDy ===" -ForegroundColor Cyan
+try {
+    $where2 = [System.Uri]::EscapeDataString('{"apig":{"__type":"Pointer","className":"APIG","objectId":"Vo3ROWEvDy"}}')
+    $url2 = "https://server.fmode.cn/parse/classes/APIGAuth?limit=5&where=$where2"
+    $r3 = Invoke-RestMethod -Uri $url2 -Method GET -Headers $headers -TimeoutSec 15
+    Write-Host "APIGAuth records:" -ForegroundColor Green
+    $r3 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 9 - 0
scripts/test-getapig-authid.ps1

@@ -0,0 +1,9 @@
+# Test getApig with real APIGAuth objectId
+$body = '{"authid":"T0iOotHcDX"}'
+Write-Host "=== getApig with APIGAuth ID T0iOotHcDX ===" -ForegroundColor Cyan
+try {
+    $r = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body -TimeoutSec 15
+    Write-Host ($r | ConvertTo-Json -Depth 5) -ForegroundColor Green
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 51 - 0
scripts/test-getapig-parse.ps1

@@ -0,0 +1,51 @@
+# Query Parse classes directly to find the APIG record
+$appId = "ncloudmaster"
+$headers = @{ "X-Parse-Application-Id" = $appId }
+
+# 1. Query APIG class for Vo3ROWEvDy
+Write-Host "=== Query APIG class ===" -ForegroundColor Cyan
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/APIG/Vo3ROWEvDy" -Method GET -Headers $headers -TimeoutSec 15
+    Write-Host "APIG record:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "APIG not found: $($_.Exception.Message)" -ForegroundColor Yellow
+}
+
+Write-Host ""
+
+# 2. Query APIGAuth class for Vo3ROWEvDy
+Write-Host "=== Query APIGAuth class ===" -ForegroundColor Cyan
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/parse/classes/APIGAuth/Vo3ROWEvDy" -Method GET -Headers $headers -TimeoutSec 15
+    Write-Host "APIGAuth record:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "APIGAuth not found: $($_.Exception.Message)" -ForegroundColor Yellow
+}
+
+Write-Host ""
+
+# 3. Try getApig with different param names
+Write-Host "=== getApig with apigid ===" -ForegroundColor Cyan
+$body3 = @{ apigid = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+try {
+    $r3 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body3 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r3 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# 4. Try getApig with objectId
+Write-Host "=== getApig with objectId ===" -ForegroundColor Cyan
+$body4 = @{ objectId = "Vo3ROWEvDy" } | ConvertTo-Json -Compress
+try {
+    $r4 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body4 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r4 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 6 - 0
scripts/test-getapig-raw.ps1

@@ -0,0 +1,6 @@
+# Get raw response from getApig
+$body = '{"authid":"Vo3ROWEvDy"}'
+$resp = Invoke-WebRequest -Uri "https://server.fmode.cn/api/apig/getApig" -Method POST -ContentType "application/json" -Body $body -TimeoutSec 15
+Write-Host "Status:" $resp.StatusCode
+Write-Host "Raw content:"
+Write-Host $resp.Content

+ 27 - 0
scripts/test-read.ps1

@@ -0,0 +1,27 @@
+$f = "e:\workspace\openclaw-voc-skill\voc\asin-sales-volume\api-config.json"
+$c1 = Get-Content $f -Raw -Encoding UTF8
+$c2 = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8)
+
+Write-Host "GC null: $($null -eq $c1)"
+Write-Host "RT null: $($null -eq $c2)"
+Write-Host "GC len: $($c1.Length)"
+Write-Host "RT len: $($c2.Length)"
+
+$token = "tokenConfig"
+Write-Host "GC IndexOf: $($c1.IndexOf($token))"
+Write-Host "RT IndexOf: $($c2.IndexOf($token))"
+
+# Show first 200 chars raw bytes around the expected position
+$bytes = [System.IO.File]::ReadAllBytes($f)
+Write-Host "File byte count: $($bytes.Length)"
+# Show bytes 2000-2010 (rough position of "tokenConfig" based on line ~99)
+$near = $bytes[2900..2950]
+Write-Host "Bytes near pos 2900: $($near -join ' ')"
+
+# Check with quotes
+$token2 = '"tokenConfig"'
+Write-Host "IndexOf with quotes: $($c1.IndexOf($token2))"
+
+# Show 10 chars around position 1972
+Write-Host "Chars 1965-1995: $(($c1[1965..1995] | ForEach-Object { [int]$_ }) -join ' ')"
+Write-Host "String around: $($c1.Substring(1965, 30))"

+ 54 - 0
scripts/test-video-api.ps1

@@ -0,0 +1,54 @@
+# Test video-creation skills connectivity
+
+# 1. Test kuaizi-video-library (list)
+Write-Host "=== Test kuaizi-video-library (list) ===" -ForegroundColor Cyan
+$body1 = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "list"
+    page = 1
+    pageSize = 3
+} | ConvertTo-Json -Compress
+
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# 2. Test kuaizi-video-query (with a dummy taskId)
+Write-Host "=== Test kuaizi-video-query ===" -ForegroundColor Cyan
+$body2 = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "query"
+    taskId = "test123"
+} | ConvertTo-Json -Compress
+
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $body2 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# 3. Test kuaizi-video-create relay endpoint (dry ping, no real materials)
+Write-Host "=== Test kuaizi-video-create relay endpoint ===" -ForegroundColor Cyan
+$body3 = @{
+    action = "ping"
+} | ConvertTo-Json -Compress
+
+try {
+    $r3 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions/cut/onemerge" -Method POST -ContentType "application/json" -Body $body3 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r3 | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 78 - 0
scripts/test-video-auth.ps1

@@ -0,0 +1,78 @@
+# Test auth signing and relay via both endpoints
+
+$appKey = "ZmNmOGRhNjYzZTAx"
+$appSecretB64 = "ZWFhMTIxNTRjMjQ4Y2FkOTE1OWE5ZDZlYThiZWRmNDY="
+$appSecret = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($appSecretB64))
+
+Write-Host "appKey: $appKey"
+Write-Host "appSecret (decoded): $appSecret"
+
+$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString()
+$signStr = "$timestamp#$appSecret"
+Write-Host "signStr: $signStr"
+
+$md5 = [System.Security.Cryptography.MD5]::Create()
+$signBytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signStr))
+$sign = ($signBytes | ForEach-Object { $_.ToString("x2") }) -join ''
+Write-Host "sign: $sign"
+Write-Host ""
+
+# Test 1: Direct call to kuaizi API (bypass relay)
+Write-Host "=== Test 1: Direct kuaizi API call ===" -ForegroundColor Cyan
+$headers1 = @{
+    "Content-Type" = "application/json"
+    "APP-KEY" = $appKey
+    "AUTH-TIMESTAMP" = $timestamp
+    "AUTH-SIGN" = $sign
+}
+
+$directBody = @{
+    account_id = "12859_117409"
+} | ConvertTo-Json -Compress
+
+try {
+    # Try a lightweight endpoint to check auth - /v2/video/vlog/list
+    $r1 = Invoke-RestMethod -Uri "https://openapi.kuaizi.co/v2/video/vlog/list" -Method POST -Headers $headers1 -Body $directBody -TimeoutSec 15
+    Write-Host "openapi.kuaizi.co Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 3
+} catch {
+    Write-Host "openapi.kuaizi.co Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# Try open.kuaizi.co instead
+try {
+    $r1b = Invoke-RestMethod -Uri "https://open.kuaizi.co/v2/video/vlog/list" -Method POST -Headers $headers1 -Body $directBody -TimeoutSec 15
+    Write-Host "open.kuaizi.co Response:" -ForegroundColor Green
+    $r1b | ConvertTo-Json -Depth 3
+} catch {
+    Write-Host "open.kuaizi.co Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# Test 2: Via Parse cloud function with relay
+Write-Host "=== Test 2: Relay via Parse function ===" -ForegroundColor Cyan
+$relayData = @{
+    apiPath = "/v2/video/vlog/list"
+    apiBody = @{ account_id = "12859_117409" }
+    appKey = $appKey
+    timestamp = $timestamp
+    sign = $sign
+} | ConvertTo-Json -Depth 3 -Compress
+
+$parseBody = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "relay"
+    relayData = $relayData
+} | ConvertTo-Json -Compress
+
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $parseBody -TimeoutSec 15
+    Write-Host "Parse relay Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 3
+} catch {
+    Write-Host "Parse relay Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 57 - 0
scripts/test-video-direct.ps1

@@ -0,0 +1,57 @@
+# Direct test: create video on open.kuaizi.co (bypassing relay)
+
+$appKey = "ZmNmOGRhNjYzZTAx"
+$appSecretB64 = "ZWFhMTIxNTRjMjQ4Y2FkOTE1OWE5ZDZlYThiZWRmNDY="
+$appSecret = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($appSecretB64))
+
+$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString()
+$signStr = "$timestamp#$appSecret"
+$md5 = [System.Security.Cryptography.MD5]::Create()
+$signBytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signStr))
+$sign = ($signBytes | ForEach-Object { $_.ToString("x2") }) -join ''
+
+$headers = @{
+    "Content-Type" = "application/json"
+    "APP-KEY" = $appKey
+    "AUTH-TIMESTAMP" = $timestamp
+    "AUTH-SIGN" = $sign
+}
+
+Write-Host "Auth headers ready: timestamp=$timestamp" -ForegroundColor Cyan
+
+# Use existing cover images as test materials
+$preId = $timestamp
+$body = @{
+    account_id = "12859_117409"
+    callback_url = "https://server.fmode.cn/api/functions/cut/onemerge"
+    material_list = @(
+        @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172925/749/1080x1920_5000_96/b65038bd93c1214e3ffb99e964379a8a.jpg" },
+        @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172918/749/1080x1920_5000_96/71d10a2e42c548492410d3f1f466320b.jpg" },
+        @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172872/749/1080x1920_5000_96/70f54baeb92a44d35f022b31068311dd.jpg" }
+    )
+    tags = "test,openclaw"
+    proportion = "9:16"
+    video_duration = @{ min = 10; max = 15 }
+    pre_id = $preId
+    compose_number = 1
+    ai_voice = 1
+    ai_bgm = 1
+    ai_flower = 1
+    ai_subtitle = 0
+    original_voice = 0
+} | ConvertTo-Json -Depth 3 -Compress
+
+# Test on open.kuaizi.co
+Write-Host "=== POST open.kuaizi.co/v2/video/vlog/create ===" -ForegroundColor Cyan
+try {
+    $r = Invoke-RestMethod -Uri "https://open.kuaizi.co/v2/video/vlog/create" -Method POST -Headers $headers -Body $body -TimeoutSec 30
+    Write-Host "Response:" -ForegroundColor Green
+    $r | ConvertTo-Json -Depth 5
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        $respBody = $reader.ReadToEnd()
+        Write-Host "Body: $respBody" -ForegroundColor Yellow
+    }
+}

+ 122 - 0
scripts/test-video-full-flow.ps1

@@ -0,0 +1,122 @@
+# Full flow test: create video -> poll results
+# Uses cover images from existing videos as test materials
+
+# --- Config ---
+$appKey = "ZmNmOGRhNjYzZTAx"
+$appSecretB64 = "ZWFhMTIxNTRjMjQ4Y2FkOTE1OWE5ZDZlYThiZWRmNDY="
+$appSecret = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($appSecretB64))
+$accountId = "12859_117409"
+$callbackUrl = "https://server.fmode.cn/api/functions/cut/onemerge"
+
+# --- Generate timestamp and sign ---
+$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString()
+$signStr = "$timestamp#$appSecret"
+$md5 = [System.Security.Cryptography.MD5]::Create()
+$signBytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signStr))
+$sign = ($signBytes | ForEach-Object { $_.ToString("x2") }) -join ''
+
+Write-Host "Timestamp: $timestamp"
+Write-Host "Sign: $sign"
+Write-Host ""
+
+# --- Build materials (use existing cover images as test) ---
+$materials = @(
+    @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172925/749/1080x1920_5000_96/b65038bd93c1214e3ffb99e964379a8a.jpg" },
+    @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172918/749/1080x1920_5000_96/71d10a2e42c548492410d3f1f466320b.jpg" },
+    @{ type = "pic"; value = "https://img.kuaizi.co/video/12859/12172872/749/1080x1920_5000_96/70f54baeb92a44d35f022b31068311dd.jpg" }
+)
+
+$preId = $timestamp
+
+$apiBody = @{
+    account_id = $accountId
+    callback_url = $callbackUrl
+    material_list = $materials
+    tags = "test,openclaw,skill"
+    proportion = "9:16"
+    video_duration = @{ min = 10; max = 15 }
+    pre_id = $preId
+    compose_number = 1
+    ai_voice = 1
+    ai_bgm = 1
+    ai_flower = 1
+    ai_subtitle = 0
+    original_voice = 0
+}
+
+$relayData = @{
+    apiPath = "/v2/video/vlog/create"
+    apiBody = $apiBody
+    appKey = $appKey
+    timestamp = $timestamp
+    sign = $sign
+} | ConvertTo-Json -Depth 5 -Compress
+
+$requestBody = @{
+    action = "relay"
+    relayData = $relayData
+} | ConvertTo-Json -Compress
+
+# --- Step 1: Create video ---
+Write-Host "=== Step 1: kuaizi-video-create ===" -ForegroundColor Cyan
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions/cut/onemerge" -Method POST -ContentType "application/json" -Body $requestBody -TimeoutSec 30
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 5
+    
+    # Extract task_id
+    $taskId = ""
+    if ($r1.data -and $r1.data.task_id) {
+        $taskId = $r1.data.task_id
+    } elseif ($r1.task_id) {
+        $taskId = $r1.task_id
+    }
+    
+    if ($taskId) {
+        Write-Host ""
+        Write-Host "task_id: $taskId" -ForegroundColor Yellow
+        
+        # --- Step 2: Poll for results ---
+        Write-Host ""
+        Write-Host "=== Step 2: kuaizi-video-query (polling) ===" -ForegroundColor Cyan
+        Write-Host "Polling every 15s, max 4 attempts for this test..."
+        
+        $pollBody = @{
+            id = "sWvRr8RvPT"
+            _ApplicationId = "ncloudmaster"
+            action = "query"
+            taskId = $taskId
+        } | ConvertTo-Json -Compress
+        
+        for ($i = 1; $i -le 4; $i++) {
+            Write-Host "  Poll #$i..." -NoNewline
+            $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $pollBody -TimeoutSec 15
+            
+            $dataCount = 0
+            if ($r2.data) { $dataCount = $r2.data.Count }
+            
+            if ($dataCount -gt 0) {
+                Write-Host " Got $dataCount video(s)!" -ForegroundColor Green
+                $r2 | ConvertTo-Json -Depth 5
+                break
+            } else {
+                Write-Host " No results yet (still generating)" -ForegroundColor DarkYellow
+                if ($i -lt 4) {
+                    Write-Host "  Waiting 15s..."
+                    Start-Sleep -Seconds 15
+                }
+            }
+        }
+        
+        Write-Host ""
+        Write-Host "Note: Video generation typically takes 2-5 min. Use taskId=$taskId to check later." -ForegroundColor Yellow
+    } else {
+        Write-Host "No task_id in response" -ForegroundColor Red
+    }
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+    if ($_.Exception.Response) {
+        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
+        Write-Host "Response body: $($reader.ReadToEnd())" -ForegroundColor Red
+    }
+}

+ 61 - 0
scripts/test-video-isolation.ps1

@@ -0,0 +1,61 @@
+# Test storeId isolation without changing cloud function
+
+# 1. Test register action (bind a dummy taskId to storeId="openclaw")
+Write-Host "=== Test register (storeId isolation) ===" -ForegroundColor Cyan
+$body1 = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "register"
+    taskId = "openclaw-test-001"
+    storeId = "openclaw"
+    caseId = "test-case"
+    caseName = "OpenClaw Test"
+} | ConvertTo-Json -Compress
+
+try {
+    $r1 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $body1 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r1 | ConvertTo-Json -Depth 3
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# 2. Test list with storeId filter
+Write-Host "=== Test list with storeId=openclaw ===" -ForegroundColor Cyan
+$body2 = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "list"
+    storeId = "openclaw"
+    page = 1
+    pageSize = 5
+} | ConvertTo-Json -Compress
+
+try {
+    $r2 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $body2 -TimeoutSec 15
+    Write-Host "Response:" -ForegroundColor Green
+    $r2 | ConvertTo-Json -Depth 3
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+
+# 3. Test list WITHOUT storeId (should see all videos)
+Write-Host "=== Test list without storeId (all videos) ===" -ForegroundColor Cyan
+$body3 = @{
+    id = "sWvRr8RvPT"
+    _ApplicationId = "ncloudmaster"
+    action = "list"
+    page = 1
+    pageSize = 3
+} | ConvertTo-Json -Compress
+
+try {
+    $r3 = Invoke-RestMethod -Uri "https://server.fmode.cn/api/functions" -Method POST -ContentType "application/json" -Body $body3 -TimeoutSec 15
+    Write-Host "Total (all): $($r3.total)" -ForegroundColor Green
+} catch {
+    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
+}

+ 46 - 0
scripts/token-config-injection.txt

@@ -0,0 +1,46 @@
+  "tokenConfig": {
+    "type": "bearer",
+    "configFile": "~/.openclaw/voc-credentials.json",
+    "tokenField": "vocToken",
+    "currentToken": "r:858b3ee92314d5447d1fc3cdc10462d7",
+    "resolutionOrder": ["configFile", "currentToken"],
+    "onMissing": {
+      "action": "showPaymentQR",
+      "qrCodeUrl": "",
+      "paymentCheckEndpoint": "",
+      "pollingIntervalMs": 3000,
+      "pollingTimeoutMs": 300000,
+      "onPaymentSuccess": "retrySkillWithNewToken",
+      "title": "扫码开通 VOC-AI 数据服务",
+      "message": "请扫描下方二维码完成支付,支付成功后将自动获取 Token 并继续执行。"
+    },
+    "onBalanceInsufficient": {
+      "action": "showPaymentQR",
+      "qrCodeUrl": "",
+      "paymentCheckEndpoint": "",
+      "pollingIntervalMs": 3000,
+      "pollingTimeoutMs": 300000,
+      "onPaymentSuccess": "retrySkillWithNewToken",
+      "title": "VOC-AI Token 余额不足,请扫码充值",
+      "message": "当前 Token 余额不足,请扫描二维码充值,支付完成后将自动继续执行。"
+    }
+  },
+  "errorHandling": {
+    "balanceInsufficient": {
+      "conditions": [
+        { "responseField": "code", "operator": "in", "value": [-2, -3, -10, 402, 429] },
+        { "responseField": "msg", "operator": "contains", "value": ["余额不足", "insufficient", "balance", "quota"] },
+        { "responseField": "message", "operator": "contains", "value": ["余额不足", "insufficient", "balance"] }
+      ],
+      "matchMode": "any",
+      "trigger": "tokenConfig.onBalanceInsufficient"
+    },
+    "unauthorized": {
+      "conditions": [
+        { "responseField": "code", "operator": "in", "value": [401, 403] },
+        { "responseField": "msg", "operator": "contains", "value": ["unauthorized", "token", "invalid", "auth"] }
+      ],
+      "matchMode": "any",
+      "trigger": "tokenConfig.onMissing"
+    }
+  }

+ 84 - 0
scripts/update-payment-urls.ps1

@@ -0,0 +1,84 @@
+# update-payment-urls.ps1
+# Batch update all api-config.json files to fill in payment page URLs
+# Run with -DryRun to preview changes
+
+param(
+    [string]$SkillsRoot = (Split-Path $PSScriptRoot -Parent),
+    [string]$PaymentDomain = "DEPLOY_DOMAIN_HERE",
+    [string]$AuthId = "T0iOotHcDX",
+    [switch]$DryRun
+)
+
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+$paymentBaseUrl = "https://$PaymentDomain/apig-pay.html?authid=$AuthId"
+$balanceCheckUrl = "https://server.fmode.cn/api/apig/getApig"
+
+Write-Host "Payment URL: $paymentBaseUrl" -ForegroundColor Cyan
+Write-Host "Balance Check: $balanceCheckUrl" -ForegroundColor Cyan
+Write-Host ""
+
+$categories = @("voc","social-media","competitor-analysis","review-analysis","synthesis","social-voc","douyin","video-creation")
+$updated = 0
+$skipped = 0
+
+foreach ($cat in $categories) {
+    $catPath = Join-Path $SkillsRoot $cat
+    if (-not (Test-Path $catPath)) { continue }
+
+    Get-ChildItem -Recurse -Filter "api-config.json" -Path $catPath | ForEach-Object {
+        $filePath = $_.FullName
+        $content = [System.IO.File]::ReadAllText($filePath, [System.Text.Encoding]::UTF8)
+
+        # Skip files without tokenConfig
+        if ($content -notmatch '"tokenConfig"') {
+            Write-Host "  [SKIP] no tokenConfig: $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor DarkGray
+            $skipped++
+            return
+        }
+
+        # Skip files already with payment URL filled
+        if ($content -match '"qrCodeUrl":\s*"https://') {
+            Write-Host "  [SKIP] already has URL: $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor DarkGray
+            $skipped++
+            return
+        }
+
+        $changed = $false
+
+        # Replace empty qrCodeUrl with payment page URL
+        if ($content -match '"qrCodeUrl":\s*""') {
+            $content = $content -replace '"qrCodeUrl":\s*""', "`"qrCodeUrl`": `"$paymentBaseUrl`""
+            $changed = $true
+        }
+
+        # Replace empty paymentCheckEndpoint with getApig balance check
+        if ($content -match '"paymentCheckEndpoint":\s*""') {
+            $content = $content -replace '"paymentCheckEndpoint":\s*""', "`"paymentCheckEndpoint`": `"$balanceCheckUrl`""
+            $changed = $true
+        }
+
+        # Replace empty paymentUrl if present
+        if ($content -match '"paymentUrl":\s*""') {
+            $content = $content -replace '"paymentUrl":\s*""', "`"paymentUrl`": `"$paymentBaseUrl`""
+            $changed = $true
+        }
+
+        if ($changed) {
+            if ($DryRun) {
+                Write-Host "  [DRY] $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor Yellow
+            } else {
+                [System.IO.File]::WriteAllText($filePath, $content, $utf8NoBom)
+                Write-Host "  [OK] $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor Green
+            }
+            $updated++
+        } else {
+            Write-Host "  [SKIP] nothing to update: $($filePath.Replace($SkillsRoot+'\',''))" -ForegroundColor DarkGray
+            $skipped++
+        }
+    }
+}
+
+Write-Host ""
+Write-Host "Done: $updated updated, $skipped skipped"
+if ($DryRun) { Write-Host "Remove -DryRun to apply." -ForegroundColor Yellow }

Некоторые файлы не были показаны из-за большого количества измененных файлов