返回 Skill 列表
extension
分类: 其它需要 API Key

byteplan-analysis

从 BytePlan 平台查询数据并生成分析报告的 skill。处理流程:登录 → 生成分析任务列表(LLM) → 查询数据 → 输出 Markdown 格式数据列表。生成 PPT 或 Word 需要单独使用 byteplan-ppt 或 byteplan-word skill。

person作者: user_801e2aaahubcommunity

BytePlan Analysis Skill

概述

此 Skill 专注于从 BytePlan 平台查询数据并生成分析报告的数据层工作:

  1. 登录认证 - 通过 BytePlan 平台认证(凭证存储在 ~/.byteplan/.env
  2. 创建工作目录 - 为每个分析主题创建独立文件夹,防止干扰
  3. 生成分析任务列表 - 使用 LLM 根据分析主题生成 Analysis Task List
  4. 查询数据 - 按照任务列表依次查询 BytePlan 数据
  5. 输出数据 - 生成 Markdown 格式的数据列表
  6. 生成后续建议 - 使用 LLM 根据数据分析结果生成后续行动建议

⚠️ 核心规则:所有生成的文件(analysisPlan.json、analysis_report.md、Excel、PPT、Word 等)都必须放在独立工作目录 ~/.byteplan/workspaces/{主题}_{时间戳}/ 下,绝不能放在其他位置。

⚠️ 分析完成后必须询问:分析完成后,必须立即主动询问用户是否需要生成 Excel/PPT/Word/网页/视频 报告,不能等用户主动要求

注意:生成 Excel/PPT/Word/网页/视频 报告需要使用单独的 skill:

目录结构(重要)

~/.byteplan/
├── .env                          # 凭证和 token(全局共享)
│   ├── BP_ENV=uat
│   ├── BP_USER=手机号
│   ├── BP_PASSWORD=密码
│   ├── ACCESS_TOKEN=...
│   └── TOKEN_EXPIRES_IN=...
│
└── workspaces/                   # 分析主题工作目录
    ├── 计算机资产折旧_20260331_224600/
    │   ├── analysisPlan.json     # 分析任务列表
    │   ├── analysis_report.md    # Markdown 报告
    │   └── raw_data/             # 原始数据(可选)
    │
    ├── 计算机资产折旧_20260401_103000/  # 重新分析时创建新目录
    │   ├── analysisPlan.json
    │   └── analysis_report.md
    │
    └── 库存周转分析_20260402_090000/
    │   ├── analysisPlan.json
    │   └── analysis_report.md
    └ ...

工作目录命名规则

  • 格式:{分析主题简称}_{时间戳}
  • 时间戳:YYYYMMDD_HHMMSS
  • 示例:计算机资产折旧_20260331_224600

⚠️ 文件位置规则

| 文件类型 | 正确位置 | 错误位置 | |----------|----------|----------| | analysisPlan.json | workspaces/{主题}_{时间戳}/ ✅ | ~/.byteplan/ ❌ | | analysis_report.md | workspaces/{主题}_{时间戳}/ ✅ | ~/.openclaw/workspace/ ❌ | | Excel/PPT/Word | workspaces/{主题}_{时间戳}/ ✅ | /tmp/ ❌ | | 所有输出文件 | 同一工作目录 ✅ | 分散在不同目录 ❌ |

重新分析处理

  • 当用户说"重新分析"时,自动创建新的工作目录
  • 使用新的时间戳,保留历史分析记录
  • 例如:第一次 计算机资产折旧_20260331_224600,重新分析 计算机资产折旧_20260401_103000

核心流程

用户输入分析主题
       ↓
   【第一步】创建工作目录(~/.byteplan/workspaces/{主题}_{时间戳}/)← 重要!
       ↓
   登录认证(优先使用 ~/.byteplan/.env 缓存 token)
       ↓
   查询可用模型
       ↓
   使用 LLM 生成分析任务列表 (Analysis Task List)
       ↓
   保存 analysisPlan.json 到工作目录 ← 所有文件都在此目录!
       ↓
   依次执行任务查询数据
       ↓
   输出 analysis_report.md 到工作目录 ← 所有文件都在此目录!
       ↓
   使用 LLM 生成后续建议
       ↓
   【必须执行】询问用户:是否生成 网页/PPT/Word?
       ↓
   生成报告文件到【同一工作目录】← 所有文件都在此目录!

工作目录创建时机

  • ✅ 用户输入新的分析主题 → 创建新工作目录
  • ✅ 用户说"重新分析" → 创建新工作目录(保留历史)
  • ❌ 用户说"继续分析"或"查看上次分析" → 使用现有工作目录

依赖

本 skill 依赖 byteplan-api 提供 API 调用能力。

API 导入方式

import { login, loginWithEnv, getUserInfo, setEnvironment, queryModels, getModelData, getModelColumns } from '/Users/fudebao/.claude/skills/byteplan-api/scripts/api.js';

前置条件

凭证存储位置

系统使用 ~/.byteplan/.env 存储凭证(用户主目录下的 byteplan 文件夹):

BP_ENV=uat                        # UAT 环境
BP_USER=你的手机号
BP_PASSWORD="你的密码"
# 以下字段由系统自动管理(登录成功后自动写入)
ACCESS_TOKEN=                        # 访问令牌
REFRESH_TOKEN=                       # 刷新令牌
TOKEN_EXPIRES_IN=                    # 过期时间戳

首次使用时:如果没有凭证文件,系统会询问用户输入账号密码,登录成功后自动创建 ~/.byteplan/ 目录并保存凭证。

工作流程详解

步骤 1:创建工作目录

在开始分析之前,首先创建独立的工作目录,防止不同分析主题干扰。

import { homedir } from 'os';
import path from 'path';
import { existsSync, mkdirSync } from 'fs';

// 基础目录
const BYTEPLAN_DIR = path.join(homedir(), '.byteplan');
const WORKSPACES_DIR = path.join(BYTEPLAN_DIR, 'workspaces');

// 确保 workspaces 目录存在
if (!existsSync(WORKSPACES_DIR)) {
  mkdirSync(WORKSPACES_DIR, { recursive: true });
}

// 生成工作目录名称
function generateWorkspaceName(theme: string): string {
  // 简化主题名称(取前20个字符,去除特殊字符)
  const shortTheme = theme.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '').substring(0, 20);
  
  // 生成时间戳
  const now = new Date();
  const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
  
  return `${shortTheme}_${timestamp}`;
}

// 检查是否需要创建新目录
function shouldCreateNewWorkspace(userInput: string, existingWorkspaces: string[]): boolean {
  // 用户说"重新分析" → 创建新目录
  if (userInput.includes('重新分析') || userInput.includes('重新') || userInput.includes('再分析')) {
    return true;
  }
  
  // 用户说"继续分析"或"查看上次分析" → 使用现有目录
  if (userInput.includes('继续') || userInput.includes('查看') || userInput.includes('上次')) {
    return false;
  }
  
  // 新的分析主题 → 创建新目录
  return true;
}

// 创建工作目录
const workspaceName = generateWorkspaceName(analysisTheme);
const workspacePath = path.join(WORKSPACES_DIR, workspaceName);

if (!existsSync(workspacePath)) {
  mkdirSync(workspacePath, { recursive: true });
  console.log(`✅ 创建工作目录: ${workspaceName}`);
} else {
  console.log(`📁 使用现有工作目录: ${workspaceName}`);
}

// 切换工作目录(后续文件操作都在此目录下)
process.chdir(workspacePath);
console.log(`📌 当前工作目录: ${workspacePath}`);

工作目录创建逻辑

| 用户意图 | 处理方式 | 示例 | |----------|----------|------| | 新分析主题 | 创建新目录 | "分析计算机资产" → 创建新目录 | | 重新分析 | 创建新目录(保留历史) | "重新分析" → 创建新目录 | | 继续分析 | 使用最近目录 | "继续上次的分析" → 使用现有目录 | | 查看历史 | 不创建新目录 | "查看上次的分析结果" → 使用现有目录 |

步骤 2:验证登录状态或重新登录

优化流程:先检查 ~/.byteplan/.env 中缓存的 token 是否有效,避免每次都重新登录。

  1. 使用 UAT 环境登录
  2. 尝试使用 ~/.byteplan/.env 中缓存的 token(如果存在且未过期)
  3. 如果缓存 token 有效,直接使用;否则重新登录
  4. 登录成功后必须显示当前租户信息
import { login, loginWithEnv, getUserInfo, setEnvironment, getToken } from '/Users/fudebao/.claude/skills/byteplan-api/scripts/api.js';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import path from 'path';

// 凭证文件路径
const BYTEPLAN_DIR = path.join(homedir(), '.byteplan');
const ENV_FILE = path.join(BYTEPLAN_DIR, '.env');

// 1. 设置环境为 UAT
setEnvironment('uat');

// 2. 尝试使用缓存的 token(优先)
let token;
let loginResult;
let isCachedLogin = false;

// 尝试使用 ~/.byteplan/.env 中缓存的 token
if (existsSync(ENV_FILE)) {
  loginResult = await loginWithEnv('uat', false).catch(() => null);
  if (loginResult?.access_token) {
    token = loginResult.access_token;
    isCachedLogin = loginResult._cached === true;  // 标记是否使用缓存
  }
}

// 3. 如果缓存无效,询问用户输入凭证并重新登录
if (!token) {
  // 检查是否有保存的账号密码
  const envContent = existsSync(ENV_FILE) ? readFileSync(ENV_FILE, 'utf-8') : '';
  const hasCredentials = envContent.includes('BP_USER=') || envContent.includes('USER_NAME=');

  if (hasCredentials) {
    // 有凭证但 token 过期,尝试重新登录
    loginResult = await loginWithEnv(selectedEnv, true).catch(() => null);
    if (loginResult?.access_token) {
      token = loginResult.access_token;
    }
  }

  // 仍然没有 token,需要用户输入
  if (!token) {
    // 使用 AskUserQuestion 询问用户输入账号密码
    // ... 获取 username 和 password
    loginResult = await login(username, password, selectedEnv);
    token = loginResult.access_token;
  }
}

// 4. 显示登录状态和租户信息
const userInfo = await getUserInfo(token);
const currentTenant = userInfo.user?.tenantName || '未知租户';

if (isCachedLogin) {
  console.log(`✅ 使用缓存的登录状态,无需重新登录`);
} else {
  console.log(`✅ 登录成功!Token 已缓存到 ~/.byteplan/.env`);
}
console.log(`📌 当前租户: ${currentTenant}`);
console.log(`📌 当前环境: ${selectedEnv}`);

登录状态说明

| 状态 | 说明 | 处理方式 | |------|------|----------| | 缓存有效 | ~/.byteplan/.env 中有未过期的 token | 直接使用,无需重新登录 | | 缓存过期 | token 已过期(提前5分钟判定) | 使用保存的凭证重新登录 | | 无凭证 | ~/.byteplan/.env 不存在或缺少账号密码 | 询问用户输入并登录 |

API 函数说明

| 函数 | 描述 | |------|------| | loginWithEnv(env, forceReLogin) | 使用 ~/.byteplan/.env 凭证登录,forceReLogin=false 时优先使用缓存 | | getToken() | 获取当前缓存的 token | | isTokenExpired() | 检查 token 是否过期(内部函数) | | login(username, password, env) | 手动输入凭证登录 |

步骤 3:查询可用模型

import { queryModels } from '/Users/fudebao/.claude/skills/byteplan-api/scripts/api.js';

const models = await queryModels(token);

// 筛选与分析主题相关的模型(可选)
const relatedModels = models.filter(m => {
  const name = m.modelName || '';
  const code = m.modelCode || '';
  // 根据分析主题关键词筛选
  return name.includes('资产') || code.includes('ASSET');
});

// 保存模型列表
const modelList = models.map(m => ({
  name: m.modelName,
  code: m.modelCode
}));

步骤 4:使用 LLM 生成分析任务列表

调用 LLM 生成分析任务列表 JSON

LLM Prompt 模板

# 角色
你是一个企业级数据分析规划 Agent(Analysis Planner)。

# 任务
根据用户提供的【分析主题】,生成一份【分析任务列表】,用于驱动后续的数据查询和分析。

# 可用模型列表
```json
{available_models_json}

分析主题

{analysis_theme}

你必须遵守的规则

  1. 不要进行任何数据计算
  2. 不要猜测数据内容
  3. 只输出合法 JSON,符合 AnalysisTaskList Schema
  4. 每个任务只对应一个数据查询
  5. 任务之间逻辑清晰、层级合理

AnalysisTaskList Schema

{
  "analysisPlanId": "PLAN_ID",
  "planName": "计划名称",
  "planDescription": "计划描述",
  "currentDate": "2026-03-27",
  "tasks": [
    {
      "taskId": "TASK_001",
      "taskName": "任务名称",
      "description": "任务描述",
      "modelName": "数据模型名称",
      "modelCode": "数据模型代码",
      "filters": {
        "type": "condition",
        "field": "筛选字段",
        "operator": "=",
        "value": "筛选值"
      },
      "outputFormat": "表格/卡片/列表",
      "outputFields": ["字段1", "字段2", "..."]
    }
  ],
  "summary": {
    "totalTasks": 5,
    "keyMilestones": ["任务1", "任务2", "..."]
  }
}

输出要求

只输出 JSON,不要有其他内容。


### 步骤 5:保存分析任务列表

```javascript
const analysisPlan = JSON.parse(llmResponse);
writeFileSync('analysisPlan.json', JSON.stringify(analysisPlan, null, 2));
console.log(`分析任务列表已保存,共 ${analysisPlan.tasks.length} 个任务`);

步骤 6:执行分析任务

import { getModelData, getModelColumns } from '/Users/fudebao/.claude/skills/byteplan-api/scripts/api.js';

const plan = JSON.parse(readFileSync('analysisPlan.json'));
const results = [];

// 依次执行每个任务
for (const task of plan.tasks) {
  console.log(`\n▶ 执行任务: ${task.taskName}`);

  try {
    const params = {
      pageNum: 1,
      pageSize: 1000
    };

    // 添加筛选条件
    if (task.filters) {
      params.customQuery = task.filters;
    }

    const data = await getModelData(token, task.modelCode, params);

    results.push({
      taskId: task.taskId,
      taskName: task.taskName,
      description: task.description,
      modelName: task.modelName,
      data: data.data || [],
      total: data.total || data.data?.length || 0
    });

    console.log(`  ✅ 获取 ${data.data?.length || 0} 条数据`);
  } catch (error) {
    console.log(`  ❌ 查询失败: ${error.message}`);
    results.push({
      taskId: task.taskId,
      taskName: task.taskName,
      description: task.description,
      modelName: task.modelName,
      data: [],
      error: error.message
    });
  }
}

步骤 7:生成 Markdown 格式数据

import { writeFileSync } from 'fs';

function generateMarkdownReport(plan, results) {
  let md = `# ${plan.planName}\n\n`;
  md += `> 分析主题: ${plan.planDescription}\n`;
  md += `> 生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;

  md += `---\n\n`;

  // 执行摘要
  md += `## 📊 执行摘要\n\n`;
  md += `| 指标 | 数值 |\n`;
  md += `|------|------|\n`;
  md += `| 总任务数 | ${plan.tasks.length} |\n`;
  md += `| 成功 | ${results.filter(r => !r.error).length} |\n`;
  md += `| 失败 | ${results.filter(r => r.error).length} |\n\n`;

  // 任务详情
  md += `## 📋 任务详情\n\n`;

  for (const result of results) {
    md += `### ${result.taskId}: ${result.taskName}\n\n`;
    md += `**描述**: ${result.description}\n`;
    md += `**数据模型**: ${result.modelName}\n\n`;

    if (result.error) {
      md += `❌ **查询失败**: ${result.error}\n\n`;
      continue;
    }

    md += `✅ 获取数据 **${result.total}** 条\n\n`;

    // 如果有数据,生成表格
    if (result.data && result.data.length > 0) {
      const firstItem = result.data[0];
      const fields = Object.keys(firstItem);

      md += `| ${fields.join(' | ') } |\n`;
      md += `| ${fields.map(() => '---').join(' | ') } |\n`;

      const maxRows = Math.min(result.data.length, 20);  // 最多显示20行
      for (let i = 0; i < maxRows; i++) {
        const row = result.data[i];
        md += `| ${fields.map(f => {
          let val = row[f];
          if (typeof val === 'object' && val !== null) {
            val = val.name || val.code || JSON.stringify(val);
          }
          return String(val || '').replace(/\n/g, ' ').substring(0, 50);
        }).join(' | ')} |\n`;
      }

      if (result.data.length > maxRows) {
        md += `\n*... 共 ${result.data.length} 条数据*\n`;
      }
    }

    md += `\n---\n\n`;
  }

  // 数据洞察(基于数据结构)
  md += `## 💡 数据洞察\n\n`;
  md += `> 基于查询结果的数据特征分析\n\n`;

  for (const result of results) {
    if (result.data && result.data.length > 0) {
      const firstItem = result.data[0];

      // 统计数值字段
      const numericFields = Object.keys(firstItem).filter(f => {
        const val = firstItem[f];
        return typeof val === 'number';
      });

      if (numericFields.length > 0) {
        md += `### ${result.taskName} - 数值统计\n\n`;
        md += `| 字段 | 类型 | 说明 |\n`;
        md += `|------|------|------|\n`;
        numericFields.slice(0, 5).forEach(f => {
          md += `| ${f} | 数值 | ${result.taskName}相关指标 |\n`;
        });
        md += `\n`;
      }
    }
  }

  // 后续建议(基于数据分析)
  // 注意:此处只是占位,实际建议内容由步骤 7.5 的 LLM 生成后追加
  md += `## 🎯 后续建议\n\n`;
  md += `> 基于以上数据分析,以下是建议的后续行动\n\n`;
  md += `> (后续建议将在 LLM 分析后追加,详见步骤 7.5)\n\n`;

  // 数据来源
  md += `## 📚 数据来源\n\n`;
  md += `- 租户: ${currentTenant}\n`;
  md += `- 数据模型: ${results.map(r => r.modelName).join(', ')}\n`;
  md += `- 查询时间: ${new Date().toISOString()}\n`;

  return md;
}

const markdown = generateMarkdownReport(plan, results);
writeFileSync('analysis_report.md', markdown);
console.log('\n✅ Markdown 报告已生成: analysis_report.md');

步骤 7.5:生成后续建议(LLM 分析)

在生成 Markdown 报告后,需要调用 LLM 根据数据分析结果生成后续建议。

LLM Prompt 模板

# 角色
你是一个企业级数据分析顾问(Analysis Consultant)。

# 任务
根据以下数据分析结果,生成后续行动建议。

# 分析主题
{plan_name}: {plan_description}

# 数据分析结果
{analysis_results_json}

# 你必须遵守的规则

1. 建议必须基于实际数据,不得臆造
2. 建议要具体、可执行,避免泛泛而谈
3. 如果发现数据异常,明确指出并给出排查建议
4. 建议要分优先级,区分"立即行动"和"后续关注"
5. 如果数据不足以得出结论,明确说明需要补充的数据

# 建议结构

请按以下结构输出建议(Markdown 格式):

## 🔴 需要立即关注的问题
- 列出数据中发现的问题或异常
- 每个问题给出可能的原因和影响

## 🟡 建议深入分析的方向
- 列出值得进一步探索的数据维度
- 说明每个方向的分析价值

## 🟢 可执行的业务建议
- 给出具体的行动建议
- 说明预期效果和实施要点

## 📋 后续数据收集建议
- 如需补充数据,说明需要收集的数据
- 说明数据用途和收集方式

# 输出要求

只输出 Markdown 格式的建议内容,不要有其他内容。

实现代码

import { writeFileSync, readFileSync } from 'fs';

async function generateFollowUpSuggestions(plan, results) {
  // 准备数据摘要(避免数据量过大)
  const dataSummary = results.map(r => ({
    taskId: r.taskId,
    taskName: r.taskName,
    description: r.description,
    modelName: r.modelName,
    totalRecords: r.total,
    hasError: !!r.error,
    sampleData: r.data?.slice(0, 5) || [],  // 只取前5条作为样本
    numericStats: extractNumericStats(r.data)
  }));

  // 调用 LLM 生成建议
  const prompt = `
# 角色
你是一个企业级数据分析顾问(Analysis Consultant)。

# 任务
根据以下数据分析结果,生成后续行动建议。

# 分析主题
${plan.planName}: ${plan.planDescription}

# 数据分析结果
\`\`\`json
${JSON.stringify(dataSummary, null, 2)}
\`\`\`

# 你必须遵守的规则

1. 建议必须基于实际数据,不得臆造
2. 建议要具体、可执行,避免泛泛而谈
3. 如果发现数据异常,明确指出并给出排查建议
4. 建议要分优先级,区分"立即行动"和"后续关注"
5. 如果数据不足以得出结论,明确说明需要补充的数据

# 建议结构

请按以下结构输出建议(Markdown 格式):

## 🔴 需要立即关注的问题
- 列出数据中发现的问题或异常
- 每个问题给出可能的原因和影响

## 🟡 建议深入分析的方向
- 列出值得进一步探索的数据维度
- 说明每个方向的分析价值

## 🟢 可执行的业务建议
- 给出具体的行动建议
- 说明预期效果和实施要点

## 📋 后续数据收集建议
- 如需补充数据,说明需要收集的数据
- 说明数据用途和收集方式

# 输出要求

只输出 Markdown 格式的建议内容,不要有其他内容。
`;

  // 这里调用 Claude 或其他 LLM API
  // const suggestions = await callLLM(prompt);
  // return suggestions;

  // 临时返回示例结构(实际应调用 LLM)
  return generateDefaultSuggestions(results);
}

// 提取数值字段的统计信息
function extractNumericStats(data) {
  if (!data || data.length === 0) return {};

  const numericFields = {};
  const firstItem = data[0];

  for (const field of Object.keys(firstItem)) {
    if (typeof firstItem[field] === 'number') {
      const values = data.map(d => d[field]).filter(v => typeof v === 'number');
      if (values.length > 0) {
        numericFields[field] = {
          min: Math.min(...values),
          max: Math.max(...values),
          avg: values.reduce((a, b) => a + b, 0) / values.length,
          count: values.length
        };
      }
    }
  }

  return numericFields;
}

// 生成默认建议结构(当 LLM 不可用时)
function generateDefaultSuggestions(results) {
  let suggestions = `## 🔴 需要立即关注的问题\n\n`;
  suggestions += `> 请根据实际数据分析结果填写\n\n`;

  // 检查是否有查询失败的任务
  const failedTasks = results.filter(r => r.error);
  if (failedTasks.length > 0) {
    suggestions += `- **数据查询异常**: ${failedTasks.length} 个任务查询失败,建议检查数据源连接\n`;
  }

  suggestions += `\n## 🟡 建议深入分析的方向\n\n`;
  suggestions += `> 请根据分析主题和数据特征填写\n\n`;
  suggestions += `- 建议进一步分析数据的趋势变化\n`;
  suggestions += `- 建议对比不同维度的数据分布\n`;

  suggestions += `\n## 🟢 可执行的业务建议\n\n`;
  suggestions += `> 请根据数据分析结论填写\n\n`;
  suggestions += `- 建议定期更新数据,保持分析的时效性\n`;
  suggestions += `- 建议建立数据监控机制,及时发现异常\n`;

  suggestions += `\n## 📋 后续数据收集建议\n\n`;
  suggestions += `> 请根据分析缺口填写\n\n`;
  suggestions += `- 如需更全面的分析,建议补充历史数据\n`;

  return suggestions;
}

// 更新报告,追加后续建议
function appendSuggestionsToReport(reportPath, suggestions) {
  const originalContent = readFileSync(reportPath, 'utf-8');
  const lines = originalContent.split('\n');

  // 找到"数据来源"章节的位置
  const sourceIndex = lines.findIndex(line => line.includes('## 📚 数据来源'));

  if (sourceIndex !== -1) {
    // 在数据来源之前插入后续建议
    lines.splice(sourceIndex, 0, suggestions + '\n');
  } else {
    // 追加到末尾
    lines.push('\n' + suggestions);
  }

  writeFileSync(reportPath, lines.join('\n'));
}

// 使用示例
const suggestions = await generateFollowUpSuggestions(plan, results);
appendSuggestionsToReport('analysis_report.md', suggestions);
console.log('✅ 后续建议已追加到报告');

建议内容要求

生成的后续建议应包含以下维度:

| 建议类型 | 说明 | 示例 | |----------|------|------| | 🔴 立即关注 | 数据异常、风险点、紧急问题 | 发现资产折旧异常、数据缺失严重 | | 🟡 深入分析 | 值得进一步探索的方向 | 细分品类分析、时间趋势对比 | | 🟢 业务建议 | 可执行的行动建议 | 优化采购策略、调整折旧政策 | | 📋 数据收集 | 需要补充的数据 | 历史数据、外部对标数据 |

⚠️ 步骤 8:询问用户是否生成报告文档(工作流必须执行)

🔴 这是工作流的必要环节,必须执行,不能跳过!

🔴 不能等用户主动要求,分析完成后必须立即主动询问!

分析数据生成完成后,立即主动询问用户下一步操作,输出类似以下内容:

✅ 分析完成!
   工作目录: ~/.byteplan/workspaces/边际分析_20260331_230800/
   - analysis_report.md 已生成(含后续建议)
   - analysisPlan.json 已保存

请问是否需要生成其他格式的报告?
- Excel 报告(结构化数据表格)
- PPT 报告(演示文稿格式)
- Word 报告(文档格式)
- 网页报告(支持演示模式)
- 视频报告(数据可视化视频)

提供的选项

| 选项 | 说明 | 执行动作 | |------|------|----------| | 生成 Excel 报告 | 结构化数据表格,多 Sheet | 调用 byteplan-excel | | 生成 PPT 报告 | PowerPoint 演示格式 | 调用 byteplan-ppt | | 生成 Word 报告 | Word 文档格式 | 调用 byteplan-word | | 生成网页报告 | 支持演示模式 | 调用 byteplan-html | | 生成视频报告 | 数据可视化视频 | 调用 byteplan-video | | 查看 Markdown | 直接查看已生成的 md 文件 | 展示文件内容 | | 不需要 | 结束本次分析 | 结束对话 |

用户选择后自动执行

根据用户选择,直接调用对应的 skill

  • 用户选择 Excel → 执行 byteplan-excel skill
  • 用户选择 PPT → 执行 byteplan-ppt skill
  • 用户选择 Word → 执行 byteplan-word skill
  • 用户选择网页 → 执行 byteplan-html skill
  • 用户选择 Video → 执行 byteplan-video skill
  • 用户选择 Markdown → 展示 analysis_report.md 内容

注意

  1. 不要只是告诉用户"请使用 xxx skill",而是直接执行对应的 skill
  2. 不能等用户主动要求,分析完成后必须立即主动询问
  3. 所有生成的报告文件必须输出到同一工作目录

API 参考

| 函数 | 描述 | |------|------| | setEnvironment(env) | 设置登录环境(仅支持 'uat') | | getEnvironment() | 获取当前环境 | | login(username, password, env?) | 使用凭证登录,token 自动缓存到 ~/.byteplan/.env | | loginWithEnv(env?, forceReLogin?) | 使用 ~/.byteplan/.env 凭证登录,默认优先使用缓存 token | | getToken() | 获取当前缓存的 access_token | | getUserInfo(token) | 获取用户和租户信息 | | queryModels(token, options?) | 列出可用模型 | | getModelData(token, modelCode, params) | 查询模型数据 | | getModelColumns(token, modelCodes) | 获取模型字段定义 |

loginWithEnv 参数说明

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | env | 'uat' | 'uat' | 登录环境 | | forceReLogin | boolean | false | 是否强制重新登录(忽略缓存) |

返回值中 _cached: true 表示使用了缓存的 token。

查询语法

条件结构

{
  type: 'condition',
  field: '字段名',
  operator: '=',  // 支持: =, !=, >, <, >=, <=, LIKE, IN, BETWEEN
  value: '值'
}

条件组结构(多条件)

{
  type: 'group',
  logic: 'AND',  // AND 或 OR
  children: [
    { type: 'condition', field: '字段1', operator: '=', value: '值1' },
    { type: 'condition', field: '字段2', operator: '>', value: '值2' }
  ]
}

完整示例

用户请求:"分析计算机类实物资产的折旧趋势"

1. 创建工作目录

import { homedir } from 'os';
import path from 'path';
import { mkdirSync } from 'fs';

const BYTEPLAN_DIR = path.join(homedir(), '.byteplan');
const WORKSPACES_DIR = path.join(BYTEPLAN_DIR, 'workspaces');
const workspaceName = '计算机资产折旧_20260331_224600';
const workspacePath = path.join(WORKSPACES_DIR, workspaceName);

mkdirSync(workspacePath, { recursive: true });
process.chdir(workspacePath);
console.log(`✅ 工作目录: ${workspacePath}`);

2. 登录并获取模型

const models = await queryModels(token);
const modelList = models.map(m => ({ name: m.modelName, code: m.modelCode }));

3. 调用 LLM 生成任务列表

将模型列表和分析主题发送给 LLM:

分析主题:分析计算机类实物资产的折旧趋势

可用模型列表:
[
  {"name": "资产基础信息表", "code": "CASMN_ASSET_BASE_INFO"},
  {"name": "资产明细表", "code": "CASMN_ASSET_INFO_DETAIL"},
  {"name": "资产类别", "code": "CASMN_ASSET_CATEGORY"},
  {"name": "已到期未报废资产明细", "code": "CASMN_ASSET_EXPIRED_NOT_SCRAPPED"}
]

4. 保存并执行任务

const plan = JSON.parse(llmResponse);
writeFileSync('analysisPlan.json', JSON.stringify(plan, null, 2));

// 执行每个任务...
const results = [];
for (const task of plan.tasks) {
  const data = await getModelData(token, task.modelCode, { pageNum: 1, pageSize: 1000 });
  results.push({ ...task, data: data.data });
}

5. 生成 Markdown 报告

const markdown = generateMarkdownReport(plan, results);
writeFileSync('analysis_report.md', markdown);

6. 生成后续建议

const suggestions = await generateFollowUpSuggestions(plan, results);
appendSuggestionsToReport('analysis_report.md', suggestions);
console.log('✅ 后续建议已追加到报告');

7. 询问用户下一步操作(工作流必须执行)

分析完成后,立即询问用户下一步操作

✅ 分析完成!
   工作目录: ~/.byteplan/workspaces/计算机资产折旧_20260331_224600/
   - analysis_report.md 已生成(含后续建议)
   - analysisPlan.json 已保存

请问是否需要生成其他格式的报告?
- 网页报告(支持演示模式)
- PPT 报告(演示格式)
- Word 报告(文档格式)
- 仅查看 Markdown

根据用户选择直接调用对应 skill:

  • 网页 → 执行 byteplan-html
  • PPT → 执行 byteplan-ppt
  • Word → 执行 byteplan-word
  • Markdown → 展示 analysis_report.md 内容

输出文件

| 文件 | 描述 | 存储位置 | 必须 | |------|------|----------|------| | analysisPlan.json | 分析任务列表 JSON | 工作目录 | ✅ | | analysis_report.md | Markdown 格式的数据报告(含后续建议) | 工作目录 | ✅ | | report_data.json | 报告数据文件(供下游 Excel/PPT/Word/HTML skill 消费) | 工作目录 | ✅ |

report_data.json 规范

⚠️ 此文件是下游报告 skill(html/ppt/word/excel)的标准输入,必须生成!

分析完成后,必须根据查询结果生成 report_data.json,格式如下:

{
  "title": "报告标题",
  "subtitle": "副标题",
  "period": "2026年Q1",
  "analysisGoal": "分析目标关键词",
  "sections": ["章节1", "章节2"],
  "summary": [
    { "rank": "🥇 第一名", "name": "项目名", "type": "类型", "value": "¥1,000万", "rate": "利润率 90%" }
  ],
  "kpis": [
    { "icon": "💰", "label": "指标名", "value": "数值", "unit": "单位" }
  ],
  "charts": [
    {
      "title": "图表标题",
      "type": "bar|line|pie|horizontalBar",
      "data": [{ "name": "分类名", "value": 100 }]
    }
  ],
  "tableData": {
    "title": "明细表标题",
    "columns": ["列1", "列2"],
    "rows": [["值1", "值2"]]
  },
  "insights": [
    { "title": "洞察标题", "content": "洞察内容", "warning": false }
  ],
  "recommendations": [
    { "title": "建议标题", "content": "建议内容" }
  ],
  "source": "BytePlan 数据平台"
}

关键规则

  • charts 数组中每个图表的 type 可省略,下游脚本会根据标题关键词自动选择
  • summary 最多 3 条,kpis 最多 4 个,insightsrecommendations 各最多 4 条
  • 所有数值型字段使用数字类型(非字符串),方便图表渲染
  • 文件必须保存在工作目录根路径下(与其他输出文件同级)

输出文件位置:所有分析结果文件保存在当前工作目录 ~/.byteplan/workspaces/{主题}_{时间戳}/

⚠️ 重要规则:文件必须放在独立工作目录

这是本 skill 最核心的规则,必须严格遵守!

所有输出文件的位置

~/.byteplan/workspaces/{主题}_{时间戳}/
├── analysisPlan.json      ← 分析任务列表(必须)
├── analysis_report.md     ← Markdown 报告(必须)
├── 边际分析报告.xlsx      ← Excel 报告(可选)
├── 边际分析报告.pptx      ← PPT 报告(可选)
├── 边际分析报告.docx      ← Word 报告(可选)
└── raw_data/              ← 原始数据(可选)

❌ 错误做法

| 错误位置 | 说明 | |----------|------| | ~/.openclaw/workspace/ | ❌ 不能放在 OpenClaw workspace 根目录 | | ~/.byteplan/ | ❌ 不能放在 byteplan 根目录 | | /tmp/ | ❌ 不能放在临时目录 | | 当前目录 | ❌ 不能放在任意当前工作目录 |

✅ 正确做法

  1. 第一步就是创建工作目录~/.byteplan/workspaces/{主题}_{时间戳}/
  2. 切换到该目录process.chdir(workspacePath)
  3. 所有后续文件操作都在此目录下
  4. 生成的 Excel/PPT/Word 也必须放在同一目录

目录切换代码示例

// 创建并切换到工作目录(这是第一步!)
const workspacePath = path.join(WORKSPACES_DIR, workspaceName);
mkdirSync(workspacePath, { recursive: true });
process.chdir(workspacePath);  // ← 关键:切换当前工作目录

// 之后所有 writeFileSync 都会写入此目录
writeFileSync('analysisPlan.json', ...);     // → workspacePath/analysisPlan.json
writeFileSync('analysis_report.md', ...);    // → workspacePath/analysis_report.md
writeFileSync('边际分析报告.xlsx', ...);     // → workspacePath/边际分析报告.xlsx

注意事项

  • ⚠️ 工作目录隔离(最重要):所有文件必须放在 ~/.byteplan/workspaces/{主题}_{时间戳}/,绝不能放在其他位置
  • 🔴 分析后必须询问(最重要):分析完成后,必须立即主动询问用户是否需要生成 Excel/PPT/Word/网页/视频,不能等用户主动要求
  • 重新分析处理:用户说"重新分析"时创建新目录,保留历史分析记录
  • 登录状态检查(重要):优先使用 loginWithEnv(env, false) 检查 ~/.byteplan/.env 中缓存的 token,避免每次重新登录
  • 首次使用时:如果没有 ~/.byteplan/.env 文件或缺少凭证,必须询问用户输入账号密码
  • 登录成功后:必须调用 getUserInfo 获取并显示当前租户信息
  • 生成任务列表:必须使用 LLM 生成,不要自行编造
  • 执行任务时:严格按照任务列表中的定义执行查询
  • 数据分析:只输出数据结构,不做计算和结论
  • 生成后续建议:必须使用 LLM 根据实际数据生成,建议要具体可执行
  • Token 会自动缓存到 ~/.byteplan/.env 文件,有效期通常为 24 小时(提前 5 分钟判定过期)