gangvy 5 месяцев назад
Родитель
Сommit
f3e9437bbf

BIN
dist/openclaw-all.zip


BIN
dist/social-media.zip


BIN
dist/voc-core.zip


BIN
dist/workshop.zip


+ 14 - 8
scripts/deploy/install-all.js

@@ -195,15 +195,21 @@ function listWorkshop() {
 // Token 工具安装
 // ============================================
 function installTools() {
-    const setTokenSrc = path.join(SRC_DIR, 'set-voc-token.js');
-    if (!fs.existsSync(setTokenSrc)) {
-        console.log('  ⚠️  set-voc-token.js 不存在,跳过');
-        return;
+    const tools = [
+        { name: 'set-voc-token.js', desc: 'Token 写入工具' },
+        { name: 'voc-token-preflight.js', desc: 'Token 预飞检测工具(Workshop 前置检查)' }
+    ];
+
+    for (const { name, desc } of tools) {
+        const src = path.join(SRC_DIR, name);
+        if (!fs.existsSync(src)) {
+            console.log(`  ⚠️  ${name} 不存在,跳过`);
+            continue;
+        }
+        const dest = path.join(TOOLS_TARGET, name);
+        copyFile(src, dest);
+        console.log(`  ✅ ${name} → ${dest}  (${desc})`);
     }
-
-    const dest = path.join(TOOLS_TARGET, 'set-voc-token.js');
-    copyFile(setTokenSrc, dest);
-    console.log(`  ✅ set-voc-token.js → ${dest}`);
 }
 
 function installCredentialsTemplate() {

+ 286 - 62
scripts/deploy/install-workshop.js

@@ -1,96 +1,320 @@
 #!/usr/bin/env node
 /**
- * Workshop 安装脚本
- * 将 workshop.zip 解压后的文件复制到 ~/.openclaw/workspace/
- * 
+ * VOC 虾工作坊 一键安装脚本 v4.2
+ *
+ * 将 workshop.zip 解压后的全部组件(skills + playbooks + memory templates + tools + credentials)
+ * 安装到 OpenClaw 对应目录。
+ *
  * 用法:
- *   node install-workshop.js            # 安装
- *   node install-workshop.js --dry-run  # 预览模式
+ *   node install-workshop.js                 # 安装全部(默认)
+ *   node install-workshop.js --dry-run       # 预览模式
+ *   node install-workshop.js --skills-only   # 只装 skills
+ *   node install-workshop.js --playbook-only # 只装 playbooks(旧行为,兼容)
+ *   node install-workshop.js --help          # 帮助
  */
 
 const fs = require('fs');
 const os = require('os');
 const path = require('path');
 
+// ============================================
+// 参数
+// ============================================
 const args = process.argv.slice(2);
 const DRY_RUN = args.includes('--dry-run');
+const SKILLS_ONLY = args.includes('--skills-only');
+const PLAYBOOK_ONLY = args.includes('--playbook-only');
+const HELP = args.includes('--help') || args.includes('-h');
 
+// ============================================
+// 路径
+// ============================================
 const SRC_DIR = __dirname;
-const WORKSPACE_DIR = path.join(os.homedir(), '.openclaw', 'workspace');
+const HOME = os.homedir();
+const OPENCLAW_DIR = path.join(HOME, '.openclaw');
+const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
 const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
+const SKILLS_TARGET = path.join(OPENCLAW_DIR, 'skills');
+const TOOLS_TARGET = path.join(OPENCLAW_DIR, 'tools');
+const CREDENTIALS_PATH = path.join(OPENCLAW_DIR, 'voc-credentials.json');
 
-console.log('');
-console.log('🦐 VOC虾工作坊 安装脚本');
-console.log('==================================================');
-if (DRY_RUN) console.log('  📌 预览模式(不会实际复制)');
-console.log('');
+// ============================================
+// 工具函数
+// ============================================
+function ensureDir(dir) {
+    if (!DRY_RUN && !fs.existsSync(dir)) {
+        fs.mkdirSync(dir, { recursive: true });
+    }
+}
 
-// 确保目标目录存在
-if (!DRY_RUN) {
-    fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
-    fs.mkdirSync(MEMORY_DIR, { recursive: true });
+function copyFile(src, dest) {
+    if (DRY_RUN) return;
+    ensureDir(path.dirname(dest));
+    fs.copyFileSync(src, dest);
 }
 
-let copied = 0;
-let skipped = 0;
+function formatSize(bytes) {
+    if (bytes < 1024) return bytes + ' B';
+    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+    return (bytes / 1024 / 1024).toFixed(1) + ' MB';
+}
 
-// 复制 .md 文件到 workspace
-const mdFiles = fs.readdirSync(SRC_DIR).filter(f =>
-    f.endsWith('.md') && fs.statSync(path.join(SRC_DIR, f)).isFile()
-);
+// ============================================
+// Help
+// ============================================
+function showHelp() {
+    console.log(`
+🦐 VOC 虾工作坊 安装脚本 v4.2
 
-console.log('── Playbooks + Supporting Docs ──');
-for (const f of mdFiles) {
-    const src = path.join(SRC_DIR, f);
-    const dest = path.join(WORKSPACE_DIR, f);
-    const size = fs.statSync(src).size;
-    const sizeKB = (size / 1024).toFixed(1);
+Usage:
+  node install-workshop.js [options]
 
-    if (DRY_RUN) {
-        console.log(`  📄 ${f} (${sizeKB} KB) → ${dest}`);
-    } else {
-        fs.copyFileSync(src, dest);
-        console.log(`  ✅ ${f} (${sizeKB} KB)`);
-    }
-    copied++;
+Options:
+  --dry-run          预览模式(不实际复制)
+  --skills-only      只装 skills
+  --playbook-only    只装 playbooks + memory templates(旧行为)
+  --help, -h         显示帮助
+
+默认(不加参数)安装全部:
+  skills/*              → ~/.openclaw/skills/
+  *.md                  → ~/.openclaw/workspace/
+  memory-templates/*    → ~/.openclaw/workspace/memory/
+  set-voc-token.js      → ~/.openclaw/tools/
+  voc-token-preflight.js → ~/.openclaw/tools/
+  voc-credentials.template.json → ~/.openclaw/voc-credentials.json(若不存在)
+
+Token 设置流程:
+  1. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF
+  2. 登录并充值
+  3. 复制 session token (格式: r:xxxx...)
+  4. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>
+  5. 重启 OpenClaw gateway
+`);
 }
-console.log('');
 
-// 复制 memory-templates
-const memSrcDir = path.join(SRC_DIR, 'memory-templates');
-if (fs.existsSync(memSrcDir)) {
-    const jsonFiles = fs.readdirSync(memSrcDir).filter(f =>
-        f.endsWith('.json') && fs.statSync(path.join(memSrcDir, f)).isFile()
-    );
+// ============================================
+// Skills 安装
+// ============================================
+function installSkills() {
+    const skillsSrcDir = path.join(SRC_DIR, 'skills');
+    if (!fs.existsSync(skillsSrcDir)) {
+        console.log('  ⚠️  skills/ 目录不存在,跳过');
+        return 0;
+    }
+
+    const skills = fs.readdirSync(skillsSrcDir).filter(d => {
+        const p = path.join(skillsSrcDir, d);
+        return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
+    }).sort();
+
+    console.log(`── Skills (${skills.length}) → ${SKILLS_TARGET} ──`);
+
+    for (const skill of skills) {
+        const srcSkillDir = path.join(skillsSrcDir, skill);
+        const destSkillDir = path.join(SKILLS_TARGET, skill);
+        const files = fs.readdirSync(srcSkillDir).filter(f =>
+            fs.statSync(path.join(srcSkillDir, f)).isFile()
+        );
+
+        // 覆盖式安装(先删除目标)
+        if (!DRY_RUN && fs.existsSync(destSkillDir)) {
+            fs.rmSync(destSkillDir, { recursive: true, force: true });
+        }
+        ensureDir(destSkillDir);
 
-    console.log('── Memory Templates ──');
-    for (const f of jsonFiles) {
-        const src = path.join(memSrcDir, f);
-        const dest = path.join(MEMORY_DIR, f);
-        const size = fs.statSync(src).size;
-        const sizeKB = (size / 1024).toFixed(1);
+        for (const f of files) {
+            copyFile(path.join(srcSkillDir, f), path.join(destSkillDir, f));
+        }
 
         if (DRY_RUN) {
-            console.log(`  📄 ${f} (${sizeKB} KB) → ${dest}`);
+            console.log(`  📄 ${skill} (${files.length} files)`);
         } else {
-            fs.copyFileSync(src, dest);
-            console.log(`  ✅ ${f} (${sizeKB} KB)`);
+            console.log(`  ✅ ${skill}`);
+        }
+    }
+    console.log('');
+    return skills.length;
+}
+
+// ============================================
+// Playbooks + Memory Templates 安装
+// ============================================
+function installPlaybooks() {
+    let count = 0;
+
+    // .md 文件 → workspace
+    const mdFiles = fs.readdirSync(SRC_DIR).filter(f =>
+        f.endsWith('.md') && fs.statSync(path.join(SRC_DIR, f)).isFile()
+    );
+
+    if (mdFiles.length > 0) {
+        console.log(`── Playbooks (${mdFiles.length}) → ${WORKSPACE_DIR} ──`);
+        for (const f of mdFiles) {
+            const src = path.join(SRC_DIR, f);
+            const dest = path.join(WORKSPACE_DIR, f);
+            const size = formatSize(fs.statSync(src).size);
+            copyFile(src, dest);
+            console.log(DRY_RUN ? `  📄 ${f} (${size})` : `  ✅ ${f} (${size})`);
+            count++;
         }
-        copied++;
+        console.log('');
+    }
+
+    // memory-templates → workspace/memory
+    const memSrcDir = path.join(SRC_DIR, 'memory-templates');
+    if (fs.existsSync(memSrcDir)) {
+        const jsonFiles = fs.readdirSync(memSrcDir).filter(f =>
+            f.endsWith('.json') && fs.statSync(path.join(memSrcDir, f)).isFile()
+        );
+
+        if (jsonFiles.length > 0) {
+            console.log(`── Memory Templates (${jsonFiles.length}) → ${MEMORY_DIR} ──`);
+            for (const f of jsonFiles) {
+                const src = path.join(memSrcDir, f);
+                const dest = path.join(MEMORY_DIR, f);
+                const size = formatSize(fs.statSync(src).size);
+                copyFile(src, dest);
+                console.log(DRY_RUN ? `  📄 ${f} (${size})` : `  ✅ ${f} (${size})`);
+                count++;
+            }
+            console.log('');
+        }
+    }
+
+    return count;
+}
+
+// ============================================
+// Tools 安装
+// ============================================
+function installTools() {
+    const tools = [
+        { name: 'set-voc-token.js', desc: 'Token 写入工具' },
+        { name: 'voc-token-preflight.js', desc: 'Token 预飞检测工具(Workshop 前置检查)' }
+    ];
+
+    const available = tools.filter(t => fs.existsSync(path.join(SRC_DIR, t.name)));
+    if (available.length === 0) return 0;
+
+    console.log(`── Tools (${available.length}) → ${TOOLS_TARGET} ──`);
+    for (const { name, desc } of available) {
+        const src = path.join(SRC_DIR, name);
+        const dest = path.join(TOOLS_TARGET, name);
+        copyFile(src, dest);
+        console.log(DRY_RUN
+            ? `  📄 ${name} → ${dest}  (${desc})`
+            : `  ✅ ${name}  (${desc})`);
     }
     console.log('');
-} else {
-    console.log('  ⚠️  memory-templates/ 目录不存在,跳过');
+    return available.length;
+}
+
+// ============================================
+// Credentials Template 安装(不覆盖已有凭证)
+// ============================================
+function installCredentialsTemplate() {
+    const templatePath = path.join(SRC_DIR, 'voc-credentials.template.json');
+    if (!fs.existsSync(templatePath)) return false;
+
+    console.log(`── Credentials Template → ${OPENCLAW_DIR} ──`);
+
+    // 总是安装 template 本身(作参考)
+    const templateDest = path.join(OPENCLAW_DIR, 'voc-credentials.template.json');
+    copyFile(templatePath, templateDest);
+    console.log(DRY_RUN
+        ? `  📄 voc-credentials.template.json → ${templateDest}`
+        : `  ✅ voc-credentials.template.json (参考模板)`);
+
+    // 只有当 voc-credentials.json 不存在时才拷贝为工作配置
+    if (fs.existsSync(CREDENTIALS_PATH)) {
+        console.log(`  ⏭️  voc-credentials.json 已存在,保留用户配置(未覆盖)`);
+    } else {
+        copyFile(templatePath, CREDENTIALS_PATH);
+        console.log(DRY_RUN
+            ? `  📄 voc-credentials.json → ${CREDENTIALS_PATH} (新建)`
+            : `  ✅ voc-credentials.json (新建,待填写 vocToken)`);
+    }
     console.log('');
+    return true;
 }
 
-console.log('==================================================');
-if (DRY_RUN) {
-    console.log(`  📌 预览完成:${copied} 个文件待安装`);
-    console.log('  去掉 --dry-run 执行实际安装');
-} else {
-    console.log(`  ✅ 安装完成!共 ${copied} 个文件`);
-    console.log(`  📂 ${WORKSPACE_DIR}`);
-    console.log('  🔄 请重启 OpenClaw gateway 加载新文件');
+// ============================================
+// Main
+// ============================================
+function main() {
+    if (HELP) { showHelp(); return; }
+
+    console.log('');
+    console.log('══════════════════════════════════════════════════');
+    console.log('  🦐 VOC 虾工作坊 安装脚本 v4.2(自足版)');
+    console.log('══════════════════════════════════════════════════');
+    if (DRY_RUN) console.log('  📌 预览模式(不会实际复制)');
+    if (SKILLS_ONLY) console.log('  📌 只装 skills');
+    if (PLAYBOOK_ONLY) console.log('  📌 只装 playbooks + memory templates');
+    console.log('');
+
+    // 确保目标根目录
+    ensureDir(OPENCLAW_DIR);
+    ensureDir(WORKSPACE_DIR);
+    ensureDir(MEMORY_DIR);
+    ensureDir(SKILLS_TARGET);
+    ensureDir(TOOLS_TARGET);
+
+    let skillsCount = 0;
+    let playbookCount = 0;
+    let toolsCount = 0;
+    let credInstalled = false;
+
+    // Skills
+    if (!PLAYBOOK_ONLY) {
+        skillsCount = installSkills();
+    }
+
+    // Playbooks + Memory Templates
+    if (!SKILLS_ONLY) {
+        playbookCount = installPlaybooks();
+    }
+
+    // Tools + Credentials(总是安装,除非 --playbook-only)
+    if (!PLAYBOOK_ONLY && !SKILLS_ONLY) {
+        toolsCount = installTools();
+        credInstalled = installCredentialsTemplate();
+    } else if (SKILLS_ONLY) {
+        // skills-only 也带上 tools,因为 Session 0 Step 0.4 的预飞依赖它
+        toolsCount = installTools();
+    }
+
+    // Summary
+    console.log('══════════════════════════════════════════════════');
+    const prefix = DRY_RUN ? '[预览] ' : '';
+    console.log(`${prefix}✅ 安装完成`);
+    if (!PLAYBOOK_ONLY) console.log(`  🔧 Skills: ${skillsCount} 个 → ${SKILLS_TARGET}`);
+    if (!SKILLS_ONLY) console.log(`  � Playbooks + Memory: ${playbookCount} 个 → ${WORKSPACE_DIR}`);
+    if (toolsCount > 0) console.log(`  🔑 Tools: ${toolsCount} 个 → ${TOOLS_TARGET}`);
+    if (credInstalled) console.log(`  📝 Credentials: ${CREDENTIALS_PATH}`);
+    console.log('');
+
+    if (!DRY_RUN) {
+        console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
+        console.log('� 下一步:设置 Token(首次使用)');
+        console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
+        console.log('  1. 打开充值页面(扫码登录 + 充值):');
+        console.log('     https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF');
+        console.log('');
+        console.log('  2. 复制 session token(格式: r:xxxx...)');
+        console.log('');
+        console.log('  3. 运行:');
+        console.log('     node ~/.openclaw/tools/set-voc-token.js <your-token>');
+        console.log('');
+        console.log('  4. 验证(可选):');
+        console.log('     node ~/.openclaw/tools/voc-token-preflight.js');
+        console.log('     期望看到: status=valid, 余额 >= 1');
+        console.log('');
+        console.log('  5. 重启 OpenClaw gateway');
+        console.log('');
+        console.log('  6. 对话框说"开始 VOC 工作坊",Agent 会进入 Session 0');
+        console.log('');
+    }
 }
-console.log('');
+
+main();

+ 106 - 24
scripts/deploy/package-and-upload.js

@@ -32,6 +32,7 @@ const CDN_PREFIX = 'x/openclaw-skills/packages';
 const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills');
 const INSTALL_JS = path.join(__dirname, '..', '..', 'dist', 'install.js');
 const SET_VOC_TOKEN_JS = path.join(__dirname, '..', 'tools', 'set-voc-token.js');
+const VOC_TOKEN_PREFLIGHT_JS = path.join(__dirname, '..', 'tools', 'voc-token-preflight.js');
 const INSTALL_WORKSHOP_JS = path.join(__dirname, 'install-workshop.js');
 const INSTALL_ALL_JS = path.join(__dirname, 'install-all.js');
 const CREDENTIALS_TEMPLATE = path.join(__dirname, '..', '..', '__config', 'voc-credentials.template.json');
@@ -100,7 +101,7 @@ const PACKAGES = {
             // social-voc
             'instagram-brand-voc', 'social-trend-analysis', 'tiktok-brand-voc', 'tiktok-category-voc',
             // workshop
-            'brand-context-builder'
+            'brand-context-builder', 'voc-token-preflight'
         ]
     },
     'social-media': {
@@ -162,6 +163,11 @@ function buildPackage(pkgId, pkg) {
         fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
     }
 
+    // 复制 voc-token-preflight.js(Token 预飞检测工具)
+    if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
+        fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
+    }
+
     // 生成 README.txt
     const readme = [
         `OpenClaw ${pkg.label}`,
@@ -179,11 +185,12 @@ function buildPackage(pkgId, pkg) {
         '  5. 重启 OpenClaw gateway',
         '',
         '其他命令:',
-        '  node install.js --list          # 列出所有技能',
-        '  node install.js --dry-run       # 预览模式',
-        '  node install.js --only a,b,c    # 只安装指定技能',
-        '  node install.js --skip a,b,c    # 跳过指定技能',
-        '  node set-voc-token.js <token>   # 设置/更新 VOC Token',
+        '  node install.js --list              # 列出所有技能',
+        '  node install.js --dry-run           # 预览模式',
+        '  node install.js --only a,b,c        # 只安装指定技能',
+        '  node install.js --skip a,b,c        # 跳过指定技能',
+        '  node set-voc-token.js <token>       # 设置/更新 VOC Token',
+        '  node voc-token-preflight.js          # 检测 Token 有效性和余额',
         '',
         '技能列表:',
         ...pkg.skills.map(s => `  - ${s}`),
@@ -238,15 +245,30 @@ function buildPackage(pkgId, pkg) {
 function buildWorkshopPackage() {
     const pkgDir = path.join(TEMP_BASE, 'workshop');
     const memoryDir = path.join(pkgDir, 'memory-templates');
+    const skillsDir = path.join(pkgDir, 'skills');
 
     if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
     fs.mkdirSync(memoryDir, { recursive: true });
+    fs.mkdirSync(skillsDir, { recursive: true });
 
     // 复制安装脚本
     if (fs.existsSync(INSTALL_WORKSHOP_JS)) {
         fs.copyFileSync(INSTALL_WORKSHOP_JS, path.join(pkgDir, 'install-workshop.js'));
     }
 
+    // 复制 Token 工具(workshop 自足所需)
+    if (fs.existsSync(SET_VOC_TOKEN_JS)) {
+        fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
+    }
+    if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
+        fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
+    }
+
+    // 复制凭证模板
+    if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
+        fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
+    }
+
     let copied = 0;
 
     // 复制 workshop 文件(仅白名单 playbooks)
@@ -271,38 +293,92 @@ function buildWorkshopPackage() {
         }
     }
 
+    // 复制 workshop 所需的全部 skills(复用 voc-core 列表,workshop 主线+侧线都依赖它们)
+    const workshopSkills = PACKAGES['voc-core'].skills;
+    let skillsCopied = 0;
+    let skillsMissing = 0;
+    for (const skillName of workshopSkills) {
+        const srcDir = path.join(SKILLS_DIR, skillName);
+        if (!fs.existsSync(srcDir)) {
+            skillsMissing++;
+            continue;
+        }
+        const destDir = path.join(skillsDir, skillName);
+        fs.mkdirSync(destDir, { recursive: true });
+        const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
+        for (const f of files) {
+            fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
+        }
+        skillsCopied++;
+    }
+
     // 生成 README.txt
     const readme = [
-        'OpenClaw 🦐 VOC虾工作坊 v4.1',
+        'OpenClaw 🦐 VOC虾工作坊 v4.2(自足版)',
         '==================================================',
         '',
-        'VOC 工作坊完整流程文件(Side Quest 架构)',
+        '一个压缩包搞定所有!包含:',
+        `  🔧 ${skillsCopied} 个 workshop 必需的 skills(品类/评论/竞品/合成/社媒VOC/工作坊)`,
+        '  📘 11 份 playbook(入口+全局规则+6个Session+配套文档)',
+        '  📄 9 份 memory templates(记忆模板)',
+        '  🔑 2 个 Token 工具(set-voc-token.js + voc-token-preflight.js)',
+        '  📝 1 份凭证模板(voc-credentials.template.json)',
         '',
         '架构:6个Session + 17个Side Quest,主线感知 + 支线深耕',
         '',
-        '安装方法:',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '📦 一键安装:',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
         '  1. 解压本压缩包',
-        '  2. 在解压目录运行: node install-workshop.js',
-        '  3. 重启 OpenClaw gateway',
+        '  2. 运行:  node install-workshop.js',
+        '     (自动安装 skills + playbooks + memory templates + tools + 凭证模板)',
+        '  3. 设置 Token(首次使用):',
+        '     a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF',
+        '     b. 登录并充值(最低 ¥0.01 体验 1 次)',
+        '     c. 复制 session token(格式 r:xxxx...)',
+        '     d. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>',
+        '  4. 重启 OpenClaw gateway',
+        '  5. 对话框说"开始 VOC 工作坊",Agent 会自动进入 Session 0',
         '',
-        '其他命令:',
-        '  node install-workshop.js --dry-run  # 预览模式',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '🔍 验证安装:',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '  node ~/.openclaw/tools/voc-token-preflight.js',
+        '  (期望看到: status=valid, 余额 >= 1)',
+        '',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '🛠 其他命令:',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '  node install-workshop.js --dry-run         # 预览将要安装的文件',
+        '  node install-workshop.js --skills-only     # 只装 skills',
+        '  node install-workshop.js --playbook-only   # 只装 playbooks (旧行为)',
         '',
-        '文件说明:',
-        '  workshop-voc-playbook.md       ← 入口路由',
-        '  workshop-global-rules.md       ← 全局规则 + Side Quest Protocol',
-        '  workshop-session-0-intake.md   ← Session 0: 品牌建档',
-        '  workshop-session-1-category.md ← Session 1: 品类 + 4 Side Quests',
-        '  workshop-session-2-brand.md    ← Session 2: 品牌 + 3 Side Quests',
-        '  workshop-session-3-voc.md      ← Session 3: VOC + 3 Side Quests',
-        '  workshop-session-4-deep.md     ← Session 4: 深入 + 5 Side Quests',
-        '  workshop-session-5-report.md   ← Session 5: 报告 + 2 Side Quests',
-        '  memory-templates/*.json        ← 记忆模板(9个)',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '📂 文件结构:',
+        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
+        '  skills/                            ← workshop 用到的 skills',
+        '    voc-token-preflight/             ← Session 0/1 的 Token 预飞闸门',
+        '    brand-context-builder/           ← Session 0 品牌建档',
+        '    category-landscape/              ← Session 1 品类全景',
+        '    keyword-search/, category-tree/, ...  (其余 Amazon/VOC/合成 skills)',
+        '  workshop-voc-playbook.md           ← 入口路由',
+        '  workshop-global-rules.md           ← 全局规则 + Token 预飞协议',
+        '  workshop-session-0-intake.md       ← Session 0: 品牌建档 + Token 预飞',
+        '  workshop-session-1-category.md     ← Session 1: 品类 + 4 Side Quests',
+        '  workshop-session-2-brand.md        ← Session 2: 品牌 + 3 Side Quests',
+        '  workshop-session-3-voc.md          ← Session 3: VOC + 3 Side Quests',
+        '  workshop-session-4-deep.md         ← Session 4: 深入 + 5 Side Quests',
+        '  workshop-session-5-report.md       ← Session 5: 报告 + 2 Side Quests',
+        '  memory-templates/*.json            ← 记忆模板(9个)',
+        '  set-voc-token.js                   ← Token 写入工具',
+        '  voc-token-preflight.js             ← Token 预飞检测工具',
+        '  voc-credentials.template.json      ← 凭证模板',
         ''
     ].join('\n');
     fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
 
-    console.log(`    ✅ 复制 ${copied} 个文件`);
+    console.log(`    ✅ 复制 ${copied} 个 playbook/template + ${skillsCopied} 个 skills` +
+        (skillsMissing > 0 ? ` (缺失 ${skillsMissing})` : ''));
 
     // 压缩
     const zipPath = path.join(DIST_DIR, 'workshop.zip');
@@ -342,6 +418,11 @@ function buildAllPackage() {
         fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
     }
 
+    // 复制 voc-token-preflight.js
+    if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
+        fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
+    }
+
     // 复制凭证模板
     if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
         fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
@@ -418,6 +499,7 @@ function buildAllPackage() {
         '  node install-all.js --skills-only      # 只装 Skills',
         '  node install-all.js --workshop-only    # 只装 Workshop',
         '  node set-voc-token.js <token>          # 设置/更新 Token',
+        '  node voc-token-preflight.js            # 检测 Token 有效性和余额(Workshop 前置检查)',
         ''
     ].join('\n');
     fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');

+ 248 - 0
scripts/tools/voc-token-preflight.js

@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+/**
+ * VOC Token Preflight v1
+ *
+ * 检测 VOC-AI token 状态,供 OpenClaw Workshop 在进入 Session 1 之前
+ * 强制调用,确保 token 有效、余额充足,否则返回可直接展示给用户的
+ * Markdown 支付引导文案 + 专属充值链接。
+ *
+ * 用法:
+ *   node voc-token-preflight.js [apigId] [--json]
+ *
+ * 参数:
+ *   apigId       要检测的 APIG objectId。缺省 7HwdQZk55B(voc-ecom,覆盖 Session 1 的 category-landscape 等付费 skill)。
+ *   --json       只输出 JSON 到 stdout(静默 stderr 日志)。适合被其他脚本 pipe 解析。
+ *
+ * 输出(stdout 最后一行):
+ *   PREFLIGHT_RESULT={"status":"valid|missing|expired|insufficient_balance|error", ...}
+ *
+ * 退出码:
+ *   0 - valid
+ *   1 - missing / expired / insufficient_balance(预期内的业务状态)
+ *   2 - error(网络或未预期错误)
+ */
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const PARSE_BASE = 'https://server.fmode.cn/parse';
+const APP_ID = 'ncloudmaster';
+const APIG_PAY_BASE = 'https://app.fmode.cn/dev/apig-pay/';
+const FUN_ID = 'HOkkX72PMF';
+
+const args = process.argv.slice(2);
+const apigId = args.find(a => !a.startsWith('--')) || '7HwdQZk55B';
+const jsonOnly = args.includes('--json');
+
+function log(...msg) {
+  if (!jsonOnly) console.error(...msg);
+}
+
+function buildPaymentUrl(userId) {
+  const params = new URLSearchParams();
+  if (userId) params.set('user', userId);
+  params.set('apigid', apigId);
+  params.set('fun_id', FUN_ID);
+  return `${APIG_PAY_BASE}?${params.toString()}`;
+}
+
+function buildDisplayMessage(status, ctx) {
+  const { userId, balance, paymentUrl } = ctx;
+  switch (status) {
+    case 'valid':
+      return [
+        '🔐 VOC-AI 服务已就绪',
+        `   👤 账号 ${userId}`,
+        `   💰 余额 ${balance} 次`
+      ].join('\n');
+    case 'missing':
+    case 'expired':
+      return [
+        '🔐 需要开通 VOC-AI 数据服务',
+        '━━━━━━━━━━━━━━━━━━━━',
+        `👉 点击开通:${paymentUrl}`,
+        '',
+        '📋 操作步骤:',
+        '1. 打开上方链接完成手机号登录',
+        '2. 选择套餐扫码支付(最低 ¥0.01 体验 1 次)',
+        '3. 支付成功后,点击页面顶部"📋 复制 Token"',
+        '4. 将 token 粘贴到对话框发给我',
+        '',
+        '⏸️ 我会等你,粘过来就能继续。'
+      ].join('\n');
+    case 'insufficient_balance':
+      return [
+        `💰 VOC-AI 余额不足(当前 ${balance} 次)`,
+        '━━━━━━━━━━━━━━━━━━━━',
+        `👉 专属续费:${paymentUrl}`,
+        '',
+        '扫码支付后自动激活,无需重新登录。'
+      ].join('\n');
+    case 'error':
+      return `⚠️ Token 预飞遇到网络/系统错误,请稍后重试。`;
+    default:
+      return `⚠️ 未知状态: ${status}`;
+  }
+}
+
+function emit(result) {
+  if (jsonOnly) {
+    console.log(JSON.stringify(result, null, 2));
+  } else {
+    console.log(`PREFLIGHT_RESULT=${JSON.stringify(result)}`);
+  }
+  if (result.status === 'valid') process.exit(0);
+  if (result.status === 'error') process.exit(2);
+  process.exit(1);
+}
+
+(async () => {
+  log('══════════════════════════════════════════════');
+  log('  VOC Token Preflight v1');
+  log(`  APIG: ${apigId}`);
+  log('══════════════════════════════════════════════');
+
+  // Step 1: 读取本地 token
+  const configPath = path.join(os.homedir(), '.openclaw', 'voc-credentials.json');
+  let token = null;
+  if (fs.existsSync(configPath)) {
+    try {
+      const raw = fs.readFileSync(configPath, 'utf-8').trim();
+      const config = raw ? JSON.parse(raw) : {};
+      token = config.vocToken;
+    } catch (e) {
+      log(`⚠️  读取 voc-credentials.json 失败: ${e.message}`);
+    }
+  }
+
+  if (!token) {
+    log('❌ 未找到 vocToken (voc-credentials.json 不存在或为空)');
+    const paymentUrl = buildPaymentUrl();
+    return emit({
+      status: 'missing',
+      userId: null,
+      balance: 0,
+      paymentUrl,
+      displayMessage: buildDisplayMessage('missing', { paymentUrl }),
+      nextAction: 'showPaymentAndWait',
+      apigId,
+      timestamp: new Date().toISOString()
+    });
+  }
+
+  log(`✅ Token: ${token.slice(0, 6)}...${token.slice(-4)}`);
+
+  // Step 2: /users/me 校验
+  const headers = {
+    'Content-Type': 'application/json',
+    'X-Parse-Application-Id': APP_ID,
+    'X-Parse-Session-Token': token
+  };
+
+  let user;
+  try {
+    const resp = await fetch(`${PARSE_BASE}/users/me`, { headers });
+    user = await resp.json();
+    if (!resp.ok || !user.objectId) {
+      log(`❌ Token 失效: ${user.error || user.message || JSON.stringify(user)}`);
+      const paymentUrl = buildPaymentUrl();
+      return emit({
+        status: 'expired',
+        userId: null,
+        balance: 0,
+        paymentUrl,
+        displayMessage: buildDisplayMessage('expired', { paymentUrl }),
+        nextAction: 'showPaymentAndWait',
+        apigId,
+        parseError: user.error || user.message || null,
+        timestamp: new Date().toISOString()
+      });
+    }
+  } catch (e) {
+    log(`⚠️  网络错误: ${e.message}`);
+    return emit({
+      status: 'error',
+      userId: null,
+      balance: 0,
+      paymentUrl: buildPaymentUrl(),
+      displayMessage: buildDisplayMessage('error', {}),
+      nextAction: 'retry',
+      apigId,
+      error: e.message,
+      timestamp: new Date().toISOString()
+    });
+  }
+
+  log(`✅ Token 有效`);
+  log(`👤 用户: ${user.username || user.objectId} (${user.objectId})`);
+
+  // Step 3: 查询 APIGAuth 余额
+  const paymentUrl = buildPaymentUrl(user.objectId);
+  let balance = 0;
+  let authId = null;
+  try {
+    const where = encodeURIComponent(JSON.stringify({
+      user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
+      api: { __type: 'Pointer', className: 'APIG', objectId: apigId }
+    }));
+    const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${where}&limit=1`, { headers });
+    const data = await resp.json();
+    if (data.results && data.results.length > 0) {
+      const record = data.results[0];
+      balance = record.count || 0;
+      authId = record.objectId;
+      log(`💰 APIGAuth: ${authId}, 余额: ${balance} 次, 已用: ${record.used || 0} 次`);
+    } else {
+      log(`ℹ️  APIGAuth 记录不存在 (用户从未开通此 APIG)`);
+    }
+  } catch (e) {
+    log(`⚠️  APIGAuth 查询失败: ${e.message}`);
+  }
+
+  if (balance <= 0) {
+    log(`❌ 余额不足,需要充值`);
+    return emit({
+      status: 'insufficient_balance',
+      userId: user.objectId,
+      username: user.username || null,
+      authId,
+      balance,
+      paymentUrl,
+      displayMessage: buildDisplayMessage('insufficient_balance', {
+        userId: user.objectId, balance, paymentUrl
+      }),
+      nextAction: 'showPaymentAndWait',
+      apigId,
+      timestamp: new Date().toISOString()
+    });
+  }
+
+  log(`✅ 预飞通过,可进入 Session 1`);
+  return emit({
+    status: 'valid',
+    userId: user.objectId,
+    username: user.username || null,
+    authId,
+    balance,
+    paymentUrl,
+    displayMessage: buildDisplayMessage('valid', {
+      userId: user.objectId, balance, paymentUrl
+    }),
+    nextAction: 'proceed',
+    apigId,
+    timestamp: new Date().toISOString()
+  });
+})().catch(e => {
+  log(`💥 未预期错误: ${e.stack || e.message}`);
+  emit({
+    status: 'error',
+    userId: null,
+    balance: 0,
+    paymentUrl: buildPaymentUrl(),
+    displayMessage: buildDisplayMessage('error', {}),
+    nextAction: 'retry',
+    apigId,
+    error: e.message,
+    timestamp: new Date().toISOString()
+  });
+});

+ 212 - 0
workshop/voc-token-preflight/SKILL.md

@@ -0,0 +1,212 @@
+---
+name: voc-token-preflight
+description: VOC工作坊 Token 预飞检测。在 Session 1 调用任何付费 skill 之前强制执行,确保 vocToken 有效且余额充足,否则返回可直接展示给用户的支付引导文案+专属充值链接。
+version: 1.0.0
+author: nkkj-BrainHack
+---
+
+# VOC Token 预飞 (voc-token-preflight)
+
+## 技能名称
+voc-token-preflight
+
+## 技能目的
+
+在 VOC 工作坊从 Session 0 进入 Session 1 之前(或任何付费 skill 调用之前),**结构化地**检测:
+
+1. `~/.openclaw/voc-credentials.json` 是否存在且包含 `vocToken`
+2. 该 token 是否仍有效(调用 `/parse/users/me` 验证)
+3. 对应 APIG 的 `APIGAuth.count` 余额是否 >0
+
+返回明确的 `status` 和可直接输出给用户的 `displayMessage`,让 OpenClaw 不用自己组装支付引导文案,避免"仅说'暂无权限'就停止"的断崖式体验。
+
+## 核心业务场景
+
+- **Session 0 Step 0.4**:建档完成后、切换 `currentPhase` 到 `session-1` 之前强制检查
+- **Session 1 Step 1.0**:对话开场前的快检(可复用 Session 0 的 30 分钟缓存结果)
+- **跨天恢复工作坊**:用户第二天回来"继续"时,检测 token 是否仍有效
+- **付费 skill 失败兜底**:当某个付费 skill 返回 `unauthorized` 或 `balanceInsufficient` 时重跑
+
+## 调用方式
+
+这是**本地脚本型 skill**,不走 HTTP,Agent 直接运行:
+
+```bash
+node scripts/tools/voc-token-preflight.js [apigId]
+```
+
+### 参数
+
+| 参数 | 必填 | 默认 | 说明 |
+|------|------|------|------|
+| apigId | 否 | `7HwdQZk55B` | 要检测的 APIG objectId。Session 1 的 category-landscape 等付费 skill 都走 voc-ecom (`7HwdQZk55B`) |
+| --json | 否 | off | 纯 JSON 输出到 stdout(适合被其他脚本 pipe) |
+
+### 输出解析
+
+**stderr**:人类可读的日志(Agent 可选择性展示给用户做调试)
+
+**stdout**(默认模式):最后一行格式为 `PREFLIGHT_RESULT={...}`,Agent 按以下步骤解析:
+
+```js
+// 伪代码
+const output = await runCommand('node scripts/tools/voc-token-preflight.js 7HwdQZk55B');
+const lastLine = output.stdout.split('\n').filter(l => l.startsWith('PREFLIGHT_RESULT=')).pop();
+const json = lastLine.slice('PREFLIGHT_RESULT='.length);
+const result = JSON.parse(json);
+```
+
+### 退出码
+
+| 退出码 | 含义 | 对应 status |
+|--------|------|-------------|
+| 0 | 预飞通过 | `valid` |
+| 1 | 业务问题(token 缺失/过期/余额不足) | `missing` / `expired` / `insufficient_balance` |
+| 2 | 系统/网络错误 | `error` |
+
+## 返回结果结构
+
+```ts
+interface PreflightResult {
+  status: 'valid' | 'missing' | 'expired' | 'insufficient_balance' | 'error';
+  userId: string | null;        // Parse _User.objectId (valid/insufficient_balance 时有)
+  username: string | null;      // Parse _User.username
+  authId: string | null;        // APIGAuth.objectId
+  balance: number;              // APIGAuth.count 剩余次数
+  paymentUrl: string;           // 专属充值/开通链接(带 ?user=... 参数)
+  displayMessage: string;       // 已格式化好的 Markdown,Agent 可一字不差展示
+  nextAction: 'proceed' | 'showPaymentAndWait' | 'retry';
+  apigId: string;               // 检测的 APIG id
+  timestamp: string;            // ISO 时间戳
+}
+```
+
+## Agent 执行协议(重要)
+
+**严格遵循以下流程,否则视为违反 Token 预飞协议:**
+
+### 1. 调用预飞
+
+```bash
+node scripts/tools/voc-token-preflight.js 7HwdQZk55B
+```
+
+### 2. 解析最后一行 `PREFLIGHT_RESULT=...` 的 JSON
+
+### 3. 按 `status` 分支执行
+
+#### status = `valid`
+- 简短确认:"🔐 VOC-AI 服务已就绪(余额 {{balance}} 次)"
+- 更新 `workshop-progress.json`:
+  ```json
+  {
+    "preflight": {
+      "lastValidatedAt": "{timestamp}",
+      "userId": "{userId}",
+      "balance": {balance},
+      "apigId": "{apigId}",
+      "status": "valid"
+    }
+  }
+  ```
+- 继续下一步(如进入 Session 1 对话开场)
+
+#### status = `missing` 或 `expired`
+- **一字不差**输出 `displayMessage` 字段给用户
+- **HARD STOP** 等待用户粘贴 session token
+- 收到 token 后:
+  1. 运行 `node scripts/tools/set-voc-token.js <token>`
+  2. **再跑一次** 本 skill 确认 `status = valid`
+  3. 若仍非 valid → 重复展示 displayMessage
+
+#### status = `insufficient_balance`
+- **一字不差**输出 `displayMessage` 字段(paymentUrl 已带 `?user={userId}`)
+- 告诉用户:"扫码付款后回复'已支付',我会再查余额"
+- 用户回复"已支付"后,**再跑一次** 本 skill 确认 `status = valid`
+
+#### status = `error`
+- 展示 `displayMessage` + 网络错误细节
+- 建议用户检查网络,等 10 秒后重试
+- 不要自动推进工作坊
+
+### 4. 禁止行为
+
+```
+❌ 不得跳过本 skill 直接调 category-landscape / keyword-search 等付费 skill
+❌ 不得忽略 displayMessage,自己组装支付文案
+❌ 不得把 status = error 当作 missing 处理
+❌ 不得在 status != valid 时推进 workshop-progress.currentPhase
+```
+
+## 调用示例
+
+### 示例 1:完整预飞(理想路径)
+
+```bash
+$ node scripts/tools/voc-token-preflight.js 7HwdQZk55B
+# stderr:
+══════════════════════════════════════════════
+  VOC Token Preflight v1
+  APIG: 7HwdQZk55B
+══════════════════════════════════════════════
+✅ Token: r:aa5f...79a7
+✅ Token 有效
+👤 用户: 陈斌 (kI6aUwfiBE)
+💰 APIGAuth: abc123, 余额: 98 次, 已用: 2 次
+✅ 预飞通过,可进入 Session 1
+
+# stdout:
+PREFLIGHT_RESULT={"status":"valid","userId":"kI6aUwfiBE","balance":98,"paymentUrl":"https://app.fmode.cn/dev/apig-pay/?user=kI6aUwfiBE&apigid=7HwdQZk55B&fun_id=HOkkX72PMF","displayMessage":"🔐 VOC-AI 服务已就绪\n   👤 账号 kI6aUwfiBE\n   💰 余额 98 次","nextAction":"proceed","apigId":"7HwdQZk55B","timestamp":"2026-04-17T12:00:00Z"}
+
+# exit code: 0
+```
+
+### 示例 2:Token 缺失(首次开营)
+
+```bash
+$ node scripts/tools/voc-token-preflight.js 7HwdQZk55B
+# stderr:
+══════════════════════════════════════════════
+  VOC Token Preflight v1
+  APIG: 7HwdQZk55B
+══════════════════════════════════════════════
+❌ 未找到 vocToken (voc-credentials.json 不存在或为空)
+
+# stdout:
+PREFLIGHT_RESULT={"status":"missing","userId":null,"balance":0,"paymentUrl":"https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF","displayMessage":"🔐 需要开通 VOC-AI 数据服务\n━━━━━━━━━━━━━━━━━━━━\n👉 点击开通:https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF\n\n📋 操作步骤:\n1. 打开上方链接完成手机号登录\n2. 选择套餐扫码支付(最低 ¥0.01 体验 1 次)\n3. 支付成功后,点击页面顶部\"📋 复制 Token\"\n4. 将 token 粘贴到对话框发给我\n\n⏸️ 我会等你,粘过来就能继续。","nextAction":"showPaymentAndWait","apigId":"7HwdQZk55B","timestamp":"..."}
+
+# exit code: 1
+```
+
+### 示例 3:余额不足(已开通但用完了)
+
+```bash
+$ node scripts/tools/voc-token-preflight.js 7HwdQZk55B
+# stderr:
+...
+💰 APIGAuth: abc123, 余额: 0 次, 已用: 10 次
+❌ 余额不足,需要充值
+
+# stdout:
+PREFLIGHT_RESULT={"status":"insufficient_balance","userId":"kI6aUwfiBE","balance":0,"paymentUrl":"https://app.fmode.cn/dev/apig-pay/?user=kI6aUwfiBE&apigid=7HwdQZk55B&fun_id=HOkkX72PMF",...}
+
+# exit code: 1
+```
+
+## 相关文件
+
+- **执行脚本**:`scripts/tools/voc-token-preflight.js`
+- **伴侣脚本**:`scripts/tools/set-voc-token.js` (收到 token 后写入本地)
+- **全局规则**:`workshop/workshop-global-rules.md` 「🔐 Token 预飞协议」章节
+- **调用位置**:
+  - `workshop/workshop-session-0-intake.md` Step 0.4
+  - `workshop/workshop-session-1-category.md` Step 1.0
+  - 其他 Session 的付费 skill 前置检查
+
+## 版本历史
+
+- v1.0.0 (2026-04-17) — 初版
+  - 支持 status = valid / missing / expired / insufficient_balance / error
+  - 输出带 ?user=... 的专属充值链接
+  - 内置可直接展示的 Markdown displayMessage
+  - 支持 --json 模式(纯 JSON stdout)

+ 162 - 0
workshop/voc-token-preflight/api-config.json

@@ -0,0 +1,162 @@
+{
+  "name": "voc-token-preflight",
+  "displayName": "VOC Token 预飞",
+  "description": "VOC 工作坊 Token 预飞检测:在 Session 1 调用付费 skill 之前,结构化地检查 vocToken 是否有效、余额是否充足,并返回可直接展示给用户的支付引导文案+专属充值链接,避免'暂无权限'的断崖式体验。",
+  "category": "workshop",
+  "version": "1.0.0",
+  "type": "local-script",
+  "execution": {
+    "kind": "command",
+    "command": "node",
+    "args": [
+      "scripts/tools/voc-token-preflight.js",
+      "{{apigId}}"
+    ],
+    "cwd": "{{workspaceRoot}}",
+    "outputParser": {
+      "type": "stdout-prefix-json",
+      "prefix": "PREFLIGHT_RESULT=",
+      "description": "解析 stdout 中以 'PREFLIGHT_RESULT=' 开头的行,去掉前缀后即为 JSON"
+    },
+    "exitCodes": {
+      "0": "valid",
+      "1": "missing | expired | insufficient_balance",
+      "2": "error"
+    }
+  },
+  "parameters": {
+    "type": "object",
+    "required": [],
+    "properties": {
+      "apigId": {
+        "type": "string",
+        "description": "要检测的 APIG objectId。默认 7HwdQZk55B(voc-ecom),覆盖 Session 1 的 category-landscape 等付费 skill。若要检测 voc-social 相关 skill,可传 Vo3ROWEvDy。",
+        "default": "7HwdQZk55B",
+        "enum": [
+          "7HwdQZk55B",
+          "Vo3ROWEvDy"
+        ]
+      }
+    }
+  },
+  "output": {
+    "type": "object",
+    "description": "预飞检测结果,Agent 按 status 字段分支处理",
+    "properties": {
+      "status": {
+        "type": "string",
+        "enum": [
+          "valid",
+          "missing",
+          "expired",
+          "insufficient_balance",
+          "error"
+        ],
+        "description": "预飞状态。valid=通过;missing=无token;expired=token失效;insufficient_balance=余额不足;error=系统错误"
+      },
+      "userId": {
+        "type": "string",
+        "nullable": true,
+        "description": "Parse _User.objectId(token 有效时返回)"
+      },
+      "username": {
+        "type": "string",
+        "nullable": true,
+        "description": "Parse _User.username"
+      },
+      "authId": {
+        "type": "string",
+        "nullable": true,
+        "description": "APIGAuth.objectId(账号已开通该 APIG 时返回)"
+      },
+      "balance": {
+        "type": "integer",
+        "description": "APIGAuth.count 剩余调用次数"
+      },
+      "paymentUrl": {
+        "type": "string",
+        "description": "充值/开通链接。valid/insufficient_balance 时带 ?user={userId} 参数(专属),missing/expired 时为通用链接"
+      },
+      "displayMessage": {
+        "type": "string",
+        "description": "已格式化好的 Markdown 文案,Agent 应一字不差展示给用户(status=missing/expired/insufficient_balance 时特别重要)"
+      },
+      "nextAction": {
+        "type": "string",
+        "enum": [
+          "proceed",
+          "showPaymentAndWait",
+          "retry"
+        ],
+        "description": "建议的下一步动作"
+      },
+      "apigId": {
+        "type": "string",
+        "description": "检测的 APIG id(回显)"
+      },
+      "timestamp": {
+        "type": "string",
+        "description": "ISO 时间戳,用于更新 workshop-progress.preflight.lastValidatedAt"
+      }
+    }
+  },
+  "memoryFiles": {
+    "read": [
+      "~/.openclaw/voc-credentials.json"
+    ],
+    "write": [
+      "memory/workshop-progress.json"
+    ]
+  },
+  "usageExamples": [
+    {
+      "name": "Session 0 Step 0.4 默认预飞",
+      "input": {
+        "apigId": "7HwdQZk55B"
+      },
+      "description": "品牌档案建档完成后的预飞检查,验证用户可进入 Session 1 的付费分析阶段"
+    },
+    {
+      "name": "Session 1 Step 1.0 入口快检",
+      "input": {
+        "apigId": "7HwdQZk55B"
+      },
+      "description": "跨天恢复工作坊时,在调用 category-landscape 前快速检验 token 仍有效"
+    },
+    {
+      "name": "付费 skill 失败后重新预飞",
+      "input": {
+        "apigId": "7HwdQZk55B"
+      },
+      "description": "当某个付费 skill 返回 unauthorized/balanceInsufficient 时,重新预飞以确定是 token 问题还是余额问题"
+    }
+  ],
+  "agentProtocol": {
+    "description": "OpenClaw Agent 必须严格按以下顺序执行,否则视为违反 Token 预飞协议",
+    "steps": [
+      "1. 运行 `node scripts/tools/voc-token-preflight.js <apigId>`",
+      "2. 在 stdout 中找到以 'PREFLIGHT_RESULT=' 开头的最后一行,去掉前缀得到 JSON",
+      "3. 解析 JSON,读取 result.status",
+      "4. 若 status = 'valid':简短确认 + 更新 memory/workshop-progress.json.preflight 字段",
+      "5. 若 status = 'missing' 或 'expired':一字不差输出 result.displayMessage → HARD STOP 等用户粘 token → 收到后运行 set-voc-token.js + 再跑本 skill",
+      "6. 若 status = 'insufficient_balance':一字不差输出 result.displayMessage → 等用户支付后回复'已支付' → 再跑本 skill 确认 valid",
+      "7. 若 status = 'error':告知用户网络/系统错误,等待后重试",
+      "8. 除非 status = 'valid',否则绝不推进 workshop-progress.currentPhase"
+    ],
+    "forbidden": [
+      "跳过本 skill 直接调 category-landscape / keyword-search 等付费 skill",
+      "忽略 displayMessage 自己组装支付文案(易遗漏链接或步骤)",
+      "把 status = 'error' 当成 'missing' 处理(会让用户误以为需要重新支付)",
+      "在 status != 'valid' 时更新 workshop-progress.currentPhase 到 session-1"
+    ]
+  },
+  "relatedFiles": {
+    "executor": "scripts/tools/voc-token-preflight.js",
+    "companion": "scripts/tools/set-voc-token.js",
+    "protocolDocs": "workshop/workshop-global-rules.md#token-预飞协议",
+    "callers": [
+      "workshop/workshop-session-0-intake.md (Step 0.4)",
+      "workshop/workshop-session-1-category.md (Step 1.0)"
+    ]
+  }
+}

+ 122 - 0
workshop/workshop-global-rules.md

@@ -237,6 +237,128 @@ pending → in_progress → awaitingCalibration → exploring → completed
 
 ---
 
+## 🔐 Token 预飞协议(与 SESSION GATE 同等最高优先级)
+
+> **设计目的**:在 Session 1 技能调用前确保 `vocToken` 有效且余额足够,避免"暂无权限"的断崖式体验。
+>
+> **核心原则**:没有 Token 预飞通过,就不能调用任何付费 skill。Agent 不是 token 的被动接收者,而是支付引导的主动执行者。
+
+### 触发时机(满足任一即触发)
+
+1. **Session 0 Step 0.4** — `brand-context.json` 建档完成后,切换 `currentPhase` 到 `session-1` 之前
+2. **Session 1 Step 1.0** — 进入 `category-landscape` 调用前
+3. **任何付费 skill 调用前** — 若 `workshop-progress.preflight.lastValidatedAt` 距今 > 30 分钟
+4. **任何付费 skill 失败时** — 返回 `errorHandling.unauthorized` 或 `errorHandling.balanceInsufficient`
+
+### 执行方式:优先调用 `voc-token-preflight` skill
+
+Agent 必须优先调用结构化 skill(定义在 `workshop/voc-token-preflight/`),而非自己组装检测逻辑:
+
+```
+voc-token-preflight(apigId = "7HwdQZk55B")
+  → 返回: {
+      status,           // "valid" | "missing" | "expired" | "insufficient_balance"
+      userId,           // Parse 用户 objectId(valid/insufficient_balance 时有)
+      balance,          // APIGAuth.count 剩余次数
+      paymentUrl,       // 带 user 参数的专属充值/开通链接
+      displayMessage,   // 已格式化好的、可直接输出给用户的 Markdown 文案
+      nextAction        // "proceed" | "showPaymentAndWait"
+    }
+```
+
+### 各 status 的强制动作
+
+#### ✅ status = `valid`
+
+Agent 输出简短确认后继续下一步:
+```
+🔐 VOC-AI 服务已就绪(账号 {userId},余额 {balance} 次)
+```
+
+#### 🆘 status = `missing` 或 `expired`
+
+Agent **必须一字不差**地展示 `displayMessage`(由 skill 生成)。典型输出:
+
+```
+🔐 需要开通 VOC-AI 数据服务
+━━━━━━━━━━━━━━━━━━━━
+👉 点击开通:{paymentUrl}
+
+📋 操作步骤:
+1. 打开上方链接完成手机号登录
+2. 选择套餐扫码支付(最低 ¥0.01 体验 1 次)
+3. 支付成功后,点击页面顶部"📋 复制 Token"
+4. 将 token 粘贴到对话框发给我
+
+⏸️ 我会等你,粘过来就能继续。
+```
+
+然后 HARD STOP,等待用户粘贴 token。
+
+#### 💰 status = `insufficient_balance`
+
+Agent 展示专属续费链接(`paymentUrl` 已带 `?user={userId}`,扫码即续费到当前账号):
+
+```
+💰 VOC-AI 余额不足(当前 {balance} 次)
+━━━━━━━━━━━━━━━━━━━━
+👉 专属续费:{paymentUrl}
+
+扫码支付后自动激活,无需重新登录。
+```
+
+### 收到用户粘贴的 Token 后
+
+Agent 按以下顺序执行:
+
+1. 先告诉用户:"收到 token,正在帮你激活..."
+2. 运行:
+   ```bash
+   node scripts/tools/set-voc-token.js <用户粘贴的 token>
+   ```
+3. **再跑一次** `voc-token-preflight`,必须确认 `status = valid` 才能推进
+4. 若确认成功,输出激活确认并继续上次中断的流程
+5. 若再次失败(token 仍无效、余额仍为 0 等)→ 重新展示对应 `displayMessage`
+
+### 记录到 `workshop-progress.json`
+
+preflight 成功后,Agent 必须在 `workshop-progress.json` 顶层写入:
+
+```json
+{
+  "preflight": {
+    "lastValidatedAt": "2026-04-17T00:00:00Z",
+    "userId": "xxx",
+    "balance": 123,
+    "apigId": "7HwdQZk55B",
+    "status": "valid"
+  }
+}
+```
+
+Session 1+ 的 Step X.0 快检就靠这个字段判断是否可复用 Session 0 的结果。
+
+### 禁止行为
+
+```
+❌ 不得跳过 preflight 直接调用 category-landscape / keyword-search 等付费 skill
+❌ 不得仅输出"暂无权限"/"调用失败"就停止,必须给出完整支付链接 + 步骤
+❌ 不得让用户手动编辑 ~/.openclaw/voc-credentials.json
+❌ 不得在 preflight 未通过时推进 workshop-progress.currentPhase
+❌ 不得把 voc-token-preflight 自己的错误(如网络失败)当作 "missing",应显式报告并重试
+```
+
+### 允许行为
+
+```
+✅ preflight 失败时,Agent 可短暂脱离"推进主线"模式,专注协助支付
+✅ 支付激活成功后,无缝回到中断点继续(基于 currentPhase + 最近一次 stage-output)
+✅ Session 0 Step 0.4 preflight 通过后,Session 1 Step 1.0 可利用 30 分钟缓存跳过重跑
+✅ 对 insufficient_balance 场景,Agent 可直接展示 paymentUrl,无需二次确认
+```
+
+---
+
 ## Side Quest 协议(开放世界探索机制)
 
 > **设计哲学:主线感知 + 支线深耕**

+ 102 - 3
workshop/workshop-session-0-intake.md

@@ -95,10 +95,10 @@ nextSession: workshop-session-1-category.md
 ## Step 0.3: 确认 + 存储
 
 1. 调用 `brand-context-builder` 验证并存储到 `memory/brand-context.json`
-2. 初始化 `memory/workshop-progress.json`
+2. 初始化 `memory/workshop-progress.json`(此时 `currentPhase` 仍为 `"intake"`,**不要**直接切到 `session-1`)
 3. 初始化 `memory/calibration-notes.json`
 4. 展示品牌档案摘要,请用户确认
-5. 更新 `workshop-progress.json` → `currentPhase: "session-1"`
+5. 用户确认后 → **进入 Step 0.4 Token 预飞**(Token 预飞通过后才切 `currentPhase`)
 
 ```
 虾:品牌建档完成!以下是你的品牌档案,请确认:
@@ -113,5 +113,104 @@ Amazon站点:{{amazonDomain}}
 分析目标:{{workshopGoal}}
 ━━━━━━━━━━━━━━━━━━━━
 
-确认无误的话,我们就开始第一阶段——品类全景分析!
+确认无误的话,我先快速检查一下你的 VOC-AI 数据服务是否已就绪,然后我们就开始第一阶段——品类全景分析!
+```
+
+---
+
+## Step 0.4: Token 预飞(进入 Session 1 前必过)
+
+> **⚠️ 这是进入 Session 1 的唯一入口**
+>
+> 详细规则参见 `workshop-global-rules.md` 「🔐 Token 预飞协议」。
+> 本步骤在用户确认品牌档案后、切换 `currentPhase` 到 `session-1` 之前**强制执行**。
+
+### 执行
+
+```
+1. 调用 skill: voc-token-preflight(apigId = "7HwdQZk55B")
+2. 根据返回的 status 执行对应动作(见下文)
+3. 只有 status = "valid" 时,才更新 workshop-progress.json:
+   - currentPhase = "session-1"
+   - preflight = { lastValidatedAt, userId, balance, apigId, status: "valid" }
+4. status = "valid" 后才能进入"对话开场",否则停在本步骤
+```
+
+### 理想路径(老用户,token 有效)
+
+```
+虾:📋 品牌档案已保存!
+
+🔐 VOC-AI 服务已就绪:
+   👤 账号 {{userId}}
+   💰 余额 {{balance}} 次
+
+可以直接开始品类分析!我们进入阶段一:品类全景。
+```
+
+### 首次开营路径(status = missing 或 expired)
+
+Agent 按「Token 预飞协议」**一字不差**展示 `displayMessage`:
+
+```
+虾:📋 品牌档案已保存!
+
+在正式开跑品类分析之前,虾需要你开通一下 VOC-AI 数据服务
+(这是采集 Amazon 品类/评论/趋势数据的计费通道,最低 ¥0.01 体验 1 次):
+
+🔐 需要开通 VOC-AI 数据服务
+━━━━━━━━━━━━━━━━━━━━
+👉 点击开通:{{paymentUrl}}
+
+📋 操作步骤:
+1. 打开上方链接完成手机号登录
+2. 选择套餐扫码支付(最低 ¥0.01 体验 1 次)
+3. 支付成功后,点击页面顶部"📋 复制 Token"
+4. 将 token 粘贴到对话框发给我
+
+⏸️ 我会等你,粘过来就能继续。
+```
+
+然后 **HARD STOP**,等待用户粘贴 token。
+
+### 余额不足路径(status = insufficient_balance)
+
+```
+虾:📋 品牌档案已保存!
+
+🔐 VOC-AI 账号已识别({{userId}}),但余额不足(剩余 {{balance}} 次)。
+   需要续费一下才能跑品类分析。
+
+💰 专属续费链接
+━━━━━━━━━━━━━━━━━━━━
+👉 {{paymentUrl}}
+
+扫码支付后自动激活,无需重新登录。支付成功回复"已支付",我帮你确认。
+```
+
+### 收到用户 Token 后
+
+1. 输出:"收到 token,正在帮你激活..."
+2. 运行:
+   ```bash
+   node scripts/tools/set-voc-token.js <用户粘贴的 token>
+   ```
+3. **再跑一次** `voc-token-preflight` 确认 `status = valid`
+4. 展示激活确认:
+   ```
+   虾:✨ 已激活!账号 {{userId}},余额 {{balance}} 次。
+
+   现在正式开始阶段一:品类全景分析!
+   ```
+5. 更新 `workshop-progress.json`:
+   - `currentPhase: "session-1"`
+   - `preflight: { lastValidatedAt, userId, balance, apigId, status: "valid" }`
+6. 转由 `workshop-session-1-category.md` 接管
+
+### 禁止行为(重申)
+
+```
+❌ Token 未激活就调用 category-landscape 或任何付费 skill
+❌ 跳过 Step 0.4 直接切换 currentPhase 到 session-1
+❌ 把 "暂无权限" 错误吞没,不给支付链接
 ```

+ 44 - 0
workshop/workshop-session-1-category.md

@@ -13,6 +13,50 @@ nextSession: workshop-session-2-brand.md
 
 ## 前置检查
 - ✅ `brand-context.json` 已存在且 categoryKeywords 非空
+- 🔐 `workshop-progress.preflight.status === "valid"` 或 通过 Step 1.0 Token 快检
+
+---
+
+## Step 1.0: Token 快检(Session 1 入口必过)
+
+> **⚠️ 在调用任何付费 skill 之前必须执行**
+>
+> 详细规则见 `workshop-global-rules.md` 「🔐 Token 预飞协议」。
+
+### 判定逻辑
+
+```
+1. 读取 memory/workshop-progress.json 的 preflight 字段
+2. 如果 preflight.status === "valid" 且 preflight.lastValidatedAt 距今 <= 30 分钟:
+   → 跳过本步骤,直接进入"对话开场"(复用 Session 0 的 preflight 结果)
+3. 否则:
+   → 调用 voc-token-preflight(apigId = "7HwdQZk55B")
+   → 根据 status 分支处理
+```
+
+### 各 status 的处理
+
+#### status = `valid`
+更新 `workshop-progress.preflight`,简短确认后进入对话开场:
+```
+🔐 VOC-AI 服务已就绪(余额 {{balance}} 次),开始跑品类分析...
+```
+
+#### status = `missing` 或 `expired`
+按全局规则「Token 预飞协议 → status = missing / expired」展示完整支付引导,**HARD STOP** 等用户粘 token。收到后运行 `set-voc-token.js` + 再跑 preflight,然后重回本步骤。
+
+#### status = `insufficient_balance`
+按全局规则「Token 预飞协议 → status = insufficient_balance」展示专属续费链接,等用户扫码支付后回复"已支付",再跑 preflight 确认。
+
+### 禁止行为(重申)
+
+```
+❌ 忽略快检结果,直接调用 category-landscape
+❌ 把 "Token 失效" / "余额不足" 错误吞没为 "品类分析失败"
+❌ preflight 失败时强行推进到对话开场
+```
+
+---
 
 ## Side Quest 触发条件速查(主线执行时参考)