Skill: tianming3/9kvr-panorama (version 1.0.0)

--- name: 9kvr-panorama description: 全景 VR 作品管理技能，覆盖账号登录配置（uid/token）、作品、素材、场景、热点、配乐、语音讲解、评分查询与接入指引。用户提出“配置登录信息”、“创建/修改/查看 VR 作品”、“上传素材”、“配置场景与热点”、“给作品加音乐或配音”、“查看评分”、“生成接入代码”等需求时使用。 --- # VR 全景作品管理技能 ## What（是什么） `9kvr-panorama` 是一个用于全景 VR 内容生产与管理的操作技能，统一处理从素材入库到作品发布前配置的关键环节，覆盖以下能力： - 作品管理：创建作品、查看详情、读取场景、更新基础信息 - 素材管理：上传素材、查询素材、更新描述、删除素材、获取下载链接 - 场景管理：场景列表、详情、更新、删除 - 热点管理：查询热点、添加场景跳转热点、添加文本热点、删除热点 - 音乐管理：音乐标签、搜索音乐、智能匹配、挂载背景音乐 - 语音管理：主播列表、语音生成、任务轮询、语音上传、挂载语音 - 评分与诊断：读取作品评分、获取版本与全局上下文 - 账号配置：设置/更新登录 `uid` 与 `token` - 接入开发：小程序/网页/存量系统接入指南与代码生成 ## Why（为什么） 该技能的目标不是“命令大全”，而是“面向新手可复用的执行流程”。 - 降低学习成本：按业务动作组织能力，而不是按 API 字段堆砌 - 降低出错概率：强调“先查后改、改后复查”的顺序 - 提升交付效率：同类任务可直接套用流程与命令模板 - 保障可追溯性：每次关键变更都可通过 info/list 再确认 ## Where（去哪里看详细信息） 先判定“调用哪个脚本/命令”，再看参数和示例。按此顺序读取： 1. 调用路由（先看）：`references/router.md` 2. 命令与参数总览：`references/commands.md` 3. 标准操作流程（SOP）：`references/workflows.md` 4. 端到端场景示例：`references/examples.md` ## 调用原则（给 AI 的执行约束） 1. 每次收到需求先做“意图分类”，必须落到一个模块：`works/media/scenes/hotspot/music/voice/score/develop/info(含账号配置)`。 2. 根据模块去 `references/router.md` 选对应脚本与命令组。 3. 先读后写：优先 `list/info/scenes/hotspot list`，再执行 `create/update/add/delete`。 4. 写后复查：任何写入动作后，必须追加一次读取命令验证结果。 5. 涉及删除、覆盖、批量操作时，先复述目标 ID 与影响范围再执行。 ## 使用边界 - 本技能聚焦“管理与配置”，不包含沉浸式前端渲染开发细节。 - 不确定归属模块时，优先使用 `references/router.md` 的“最小调用模板”判定。

path: README.md
content: # 9kvr-panorama

`9kvr-panorama` 是芊云全景面向 AI 的全景业务技能说明，帮助 AI 助手准确理解用户意图，并完成全景内容管理相关任务。

## 产品定位

面向 AI 场景的智能全景系统。

## 关于芊云全景

芊云全景（9kvr.cn）是一个面向 AI 的全景系统，帮助个人与企业快速完成全景作品的生产、编辑、交付与传播。  
无论是展馆展示、门店导览、文旅项目还是企业介绍，芊云全景都致力于把复杂的全景制作流程变成更清晰、更高效的业务动作。

- 官网：https://9kvr.cn

- 📱 **移动端体验**：

<div align="center">
  <img src="https://img.he29.com/api/png/87fb6ae5837c2328045ab04e7b5f5a5f.png" width="200" alt="抖音小程序" />
  <img src="https://img.he29.com/api/jpg/fbabfac5c04b2e065b3cd395beaca7b1.jpg" width="200" alt="微信小程序" />
  <br/>
  <sub>抖音小程序 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 微信小程序</sub>
</div>

## 这是什么

这是一个“全景作品运营助手”技能集，核心目标是让用户用自然语言就能完成全景项目中的常见工作，例如：

- 管理作品与场景
- 管理素材与热点
- 配置背景音乐与语音讲解
- 查看作品评分与优化方向
- 获取接入指引与代码建议
- 配置账号登录信息（uid / token）

## 我们的目的

- 让新手也能快速上手全景项目管理
- 让 AI 更懂全景业务语境，减少沟通损耗
- 让高频任务标准化，提升团队协作效率
- 让内容交付从“会用工具”升级到“会做结果”

## 能力总览

### 1. 账号配置

- 配置登录信息（uid、token）
- 校验当前账号状态
- 支持“先配置再执行后续任务”的工作流

### 2. 作品管理

- 查看作品列表
- 查询作品详情
- 更新作品基础信息
- 基于素材创建作品

### 3. 素材管理

- 上传与整理素材
- 查看素材信息
- 更新素材描述
- 删除或下载素材

### 4. 场景与热点管理

- 查看作品场景
- 添加场景跳转热点
- 添加文本热点
- 删除热点并回查结果

### 5. 音频能力

- 搜索或智能匹配背景音乐
- 挂载背景音乐到作品
- 生成语音讲解并挂载

### 6. 评分与诊断

- 查看作品评分
- 结合评分结果做优化动作

### 7. 接入指导

- 小程序接入建议
- Web 接入建议
- 现有系统接入建议
- 常见接入场景的示例指引

## 如何触发（提示词示例）

以下是推荐给 AI 助手的自然语言提示词写法。  
不需要刻意说出技能名，直接描述你的目标即可。

### 账号配置类

- 帮我配置登录信息，uid 是 `<UID>`，token 是 `<TOKEN>`
- 帮我登录全景账号，uid=`<UID>` token=`<TOKEN>`

### 作品管理类

- 帮我查一下我的全景作品列表
- 把作品 `<WORK_ID>` 的名称改成“春季展厅”

### 素材与场景类

- 帮我上传这张全景图并创建作品
- 给作品 `<WORK_ID>` 增加一个跳转热点

### 音频类

- 给作品 `<WORK_ID>` 匹配一段背景音乐
- 把这段讲解词生成语音并挂到作品 `<WORK_ID>`

### 评分与优化类

- 查看作品 `<WORK_ID>` 的评分，并告诉我优先优化什么

### 接入指导类

- 给我一份 React 项目接入全景的建议
- 小程序接入全景需要准备什么

## 这套技能有什么用

- 降低沟通成本：用户只需描述目标，不必记复杂操作流程
- 提升任务成功率：AI 能更准确匹配业务场景，减少误操作
- 提高执行效率：同类任务可复用同一套提示词模板
- 适合新手用户：将复杂流程转换为清晰、可理解的业务动作

## 适用人群

- 全景内容运营人员
- 项目交付与实施人员
- 需要快速配置全景内容的产品/运营同学
- 希望通过 AI 完成日常全景管理任务的新手用户

## 使用建议

- 先说明目标，再给关键标识（如作品 ID）
- 涉及删除、覆盖时，先让 AI 复述目标再执行
- 多步骤任务可拆分为“先查询、再修改、后验证”三段描述

## 许可证

MIT


path: src/config.py
content: """配置管理模块"""

import json
import os
from typing import Any, Dict, Optional


class ConfigService:
    """配置服务类（仅认证与超时）"""

    def __init__(self):
        # 不再维护域名配置，域名仅由 VRAPI 网络服务器内置控制
        self._config: Dict[str, Any] = {
            "api": {
                "timeout": 5000,
            },
            "auth": {
                "token": None,
                "uid": None,
            },
        }

        self._load_from_session()
        self._load_from_env()

    def _load_from_env(self) -> None:
        """从环境变量加载配置（最高优先级）"""
        if os.getenv("API_TIMEOUT"):
            try:
                self._config["api"]["timeout"] = int(os.getenv("API_TIMEOUT"))
            except ValueError:
                pass

        if os.getenv("AUTH_TOKEN"):
            self._config["auth"]["token"] = os.getenv("AUTH_TOKEN")
        if os.getenv("AUTH_UID"):
            self._config["auth"]["uid"] = os.getenv("AUTH_UID")

    def _load_from_session(self) -> None:
        """从 vr-api session 文件加载认证信息"""
        session_path = os.path.join(self.auth_dir, "vr-session.json")
        if not os.path.exists(session_path):
            return
        try:
            with open(session_path, "r", encoding="utf-8") as f:
                session = json.load(f)
                if not self._config["auth"]["uid"] and session.get("uid"):
                    self._config["auth"]["uid"] = session["uid"]
                if not self._config["auth"]["token"] and session.get("token"):
                    self._config["auth"]["token"] = session["token"]
        except (FileNotFoundError, json.JSONDecodeError):
            pass

    @property
    def timeout(self) -> int:
        """请求超时时间（毫秒）"""
        return self._config.get("api", {}).get("timeout", 5000)

    @property
    def token(self) -> Optional[str]:
        """认证 Token"""
        return self._config.get("auth", {}).get("token")

    @property
    def uid(self) -> Optional[str]:
        """用户 UID"""
        return self._config.get("auth", {}).get("uid")

    @property
    def cache_dir(self) -> str:
        """Skills/MCP 缓存目录 - ~/.9kvr/skills/cache"""
        return os.path.expanduser("~/.9kvr/skills/cache")

    @property
    def client_dir(self) -> str:
        """VRAPI 网络服务器目录 - ~/.9kvr/client"""
        return os.path.expanduser("~/.9kvr/client")

    @property
    def auth_dir(self) -> str:
        """认证信息目录 - ~/.9kvr/auth"""
        return os.path.expanduser("~/.9kvr/auth")

    def get(self, *keys: str, default: Any = None) -> Any:
        """获取配置值"""
        value = self._config
        for key in keys:
            if isinstance(value, dict) and key in value:
                value = value[key]
            else:
                return default
        return value

    def set(self, value: Any, *keys: str) -> None:
        """设置配置值（仅运行时内存生效）"""
        if len(keys) == 0:
            return
        target = self._config
        for key in keys[:-1]:
            if key not in target:
                target[key] = {}
            target = target[key]
        target[keys[-1]] = value

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典"""
        return self._config.copy()


_config_instance: Optional[ConfigService] = None


def get_config() -> ConfigService:
    """获取全局配置实例"""
    global _config_instance
    if _config_instance is None:
        _config_instance = ConfigService()
    return _config_instance


def reset_config() -> None:
    """重置全局配置"""
    global _config_instance
    _config_instance = None



path: src/__init__.py
content: # VR Skills Package


path: src/utils/__init__.py
content: # Utils Package


path: src/utils/response.py
content: """响应格式化工具模块

提供统一的响应格式
"""

import json
from dataclasses import dataclass, field, asdict
from typing import Any, Optional, List, Dict


@dataclass
class Response:
    """标准响应格式"""
    code: int = 0
    message: str = "success"
    data: Any = None

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    def to_json(self, indent: Optional[int] = None) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)

    @staticmethod
    def success(data: Any = None, message: str = "success") -> "Response":
        """成功响应"""
        return Response(code=0, message=message, data=data)

    @staticmethod
    def error(message: str, code: int = 1) -> "Response":
        """错误响应"""
        return Response(code=code, message=message, data=None)


@dataclass
class ListResponse:
    """列表响应格式"""
    items: List[Any] = field(default_factory=list)
    total: int = 0
    page: int = 1
    page_size: int = 20

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    def to_json(self, indent: Optional[int] = None) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)


@dataclass
class ErrorResponse:
    """错误响应格式"""
    code: int
    message: str
    details: Optional[Dict[str, Any]] = None

    def to_dict(self) -> Dict[str, Any]:
        result = {"code": self.code, "message": self.message}
        if self.details:
            result["details"] = self.details
        return result

    def to_json(self, indent: Optional[int] = None) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)


def format_tool_response(
    success: bool = True,
    data: Any = None,
    error_message: Optional[str] = None,
    error_code: int = 1,
    **extra_fields
) -> Dict[str, Any]:
    """格式化工具响应

    统一的响应格式：
    - success: 是否成功
    - data: 响应数据（成功时）
    - error_message: 错误信息（失败时）
    - error_code: 错误码（失败时）
    - 其他额外字段

    Args:
        success: 是否成功
        data: 响应数据
        error_message: 错误信息
        error_code: 错误码
        **extra_fields: 额外字段

    Returns:
        格式化的响应字典
    """
    if success:
        result = {
            "success": True,
            "code": 0,
            "message": "success",
            "data": data
        }
    else:
        result = {
            "success": False,
            "code": error_code,
            "message": error_message or "Unknown error",
            "data": None
        }

    # 添加额外字段
    result.update(extra_fields)

    return result


def format_success(data: Any = None, message: str = "success", **kwargs) -> Dict[str, Any]:
    """格式化成功响应为字典"""
    result = {
        "success": True,
        "code": 0,
        "message": message,
        "data": data
    }
    result.update(kwargs)
    return result


def format_error(message: str, code: int = 1, **kwargs) -> Dict[str, Any]:
    """格式化错误响应为字典"""
    result = {
        "success": False,
        "code": code,
        "message": message,
        "data": None
    }
    result.update(kwargs)
    return result


def format_list(items: List[Any], total: int = 0, **kwargs) -> Dict[str, Any]:
    """格式化列表响应为字典"""
    result = {
        "success": True,
        "code": 0,
        "message": "success",
        "data": {
            "items": items,
            "total": total,
        }
    }
    result["data"].update(kwargs)
    return result


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: stale | SkillSpector: suspicious (CRITICAL)