发布:学生会策划部工作包(Skills + MCP)

This commit is contained in:
gitea_admin
2026-09-29 12:35:04 +08:00
commit 2df696c390
147 changed files with 50777 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Tencent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+246
View File
@@ -0,0 +1,246 @@
---
name: tencent-survey
description: 腾讯问卷(wj.qq.com)- 在线问卷调查平台。涉及「问卷」「调查」「表单」「投票」「考试」「测评」「wj.qq.com」等操作时优先使用。支持能力:(1) 获取问卷详情(标题、设置、页面、题目、选项完整结构 + 纯文本 DSL + 自定义逻辑)(2) 使用纯文本创建问卷(text 必填,支持指定场景/指定项目)(3) 更新问卷中的单个题目(DSL 格式)(4) 获取问卷回答列表(支持游标分页)(5) 更新问卷自定义逻辑(条件显示/隐藏、跳转、替换、随机排序等)。支持场景:调查(1)、考试(3)、测评(6)、投票(8)。
description_zh: "腾讯问卷操作(创建、修改、逻辑设置、统计)"
description_en: "Tencent Survey operations (create, edit, logic settings, view statistical analysis of various surveys)\""
version: 1.1.2
homepage: https://wj.qq.com
metadata:
openclaw:
primaryEnv: TENCENT_SURVEY_TOKEN
category: tencent
tencentTokenMode: custom
emoji: "\U0001F4CB"
display_name: "腾讯问卷"
display_name_en: "Tencent Survey"
visibility: "public"
---
# SKILL 更新
每天使用 SKILL 前进行一次更新检查,按照如下的步骤执行:
1. **查看当前版本version**:读取本文件顶部 frontmatter 中的 `version` 字段;格式为 MAJOR.MINOR.PATCH
2. **查看最新版本latest**:通过命令获取最新版本信息`latest`,输入参数 `current_version` 为上一步获取的当前版本
```bash
mcporter call tencent-survey "check_skill_update" --args '{"current_version": "当前版本"}'
```
JSON 格式数据返回,返回参数示例:
- `has_update`: 是否有更新(true/false)
- `latest`: 最新版本号,格式为 MAJOR.MINOR.PATCH
- `current_version`: 当前传入的版本号
- `release_note`: 最新版本发布说明
- `instruction`: 更新指令
3. **更新版本**:如果 `has_update` 为 true(即当前版本`current_version`低于最新版本`latest`),则遵循 `instruction` 指令进行更新,或提示用户更新
# 腾讯问卷 MCP 使用指南
腾讯问卷 MCP 提供问卷查询、创建、编辑与回答查看能力,让 AI 助手可以直接操作问卷系统。
## 触发场景
### 明确触发
以下情况应直接激活本 skill:
- 用户提到「问卷」「调查」「表单」「投票」「考试」「测评」等关键词
- 用户提供了 `wj.qq.com` 链接
- 用户说「帮我做个调查」「创建一个投票」等
### 模糊场景
| 用户表述 | 判断方式 |
|---------|---------|
| 「帮我做个投票」 | 直接使用本 skill,scene=8 |
| 「做个考试」 | 直接使用本 skill,scene=3 |
| 「做个测评」 | 直接使用本 skill,scene=6 |
| 「收集一下大家的意见」 | 直接使用本 skill,scene=1(调查) |
| 「我有个问卷链接…」 | 解析链接提取 survey_id,调用 get_survey |
| 「修改问卷的第X题」 | 先 get_survey 获取 question_id,再 update_question |
| 「设置跳题逻辑」「设置显示逻辑」 | 先 get_survey 获取题目/选项 ID 和当前 survey_dsl,再 update_logic |
| 「选了A就跳到结束」 | 先 get_survey 获取 ID,用 branch 语句调用 update_logic |
| 「随机显示题目」「选项随机排序」 | 先 get_survey 获取 ID,用 random show / shuffle 调用 update_logic |
| 「看看问卷的回答」 | 调用 list_answers,注意翻页获取全部数据 |
| 「问卷收了多少份」 | 调用 list_answers 查看 total 字段 |
## 配置
**在本次会话首次调用工具前**,完成一次鉴权检查(完整流程见 `references/auth.md`):
> `${SKILL_DIR}` 为本 skill 所在目录路径(即 `SKILL.md` 所在目录)。由 AI Agent 框架在加载 skill 时自动注入;如果框架未注入,请替换为 `SKILL.md` 所在目录的绝对路径。
### 方式一:环境变量传入 Token
如果已有 Token(环境变量 `TENCENT_SURVEY_TOKEN`),直接完成配置,无需 OAuth 授权:
```bash
TENCENT_SURVEY_TOKEN=xxx bash "${SKILL_DIR}/setup.sh" wj_check_and_start_auth
```
脚本检测到 `TENCENT_SURVEY_TOKEN` 后会自动写入 mcporter 配置,输出 `READY` 即表示就绪。
### 方式二:OAuth 设备授权
未设置 `TENCENT_SURVEY_TOKEN` 时,自动进入 OAuth 授权流程:
1. 执行 `bash "${SKILL_DIR}/setup.sh" wj_check_and_start_auth`
2. 输出 `READY` → 鉴权已就绪,直接继续
3. 输出 `AUTH_REQUIRED:<url>` → 向用户展示授权链接,然后执行 `bash "${SKILL_DIR}/setup.sh" wj_wait_auth` 等待授权完成
4. 输出 `ERROR:*` → 告知用户对应错误
> 鉴权通过后,**同一会话内后续调用无需重复检查**。仅当工具返回 `invalid_token`、`token expired`、`missing_token` 等鉴权错误时,才需要重新执行上述流程。
- Token 前缀固定为 `wjpt_`,长度 70 字符
- 每个 Token 绑定一个团队,只能操作该团队下的问卷
## 工具列表与调用方式
| 工具名称 | 功能说明 | 参考文档 |
|---------|---------|---------|
| get_survey | 获取指定问卷的详细信息(标题、设置、页面、题目、选项 + 纯文本 DSL) | `references/get_survey.md` |
| create_survey | 使用纯文本创建问卷(text 必填,支持指定场景/指定项目) | `references/create_survey.md` |
| update_question | 更新问卷中的某一道题目(需先获取 question_id) | `references/update_question.md` |
| update_logic | 更新问卷的自定义逻辑设置(条件显示/隐藏、跳转、替换、随机等) | `references/update_logic.md` |
| list_answers | 获取问卷的回答列表(支持游标分页) | `references/list_answers.md` |
| check_skill_update | 检查 Skill 是否有新版本可更新 | 见上方「SKILL 更新」章节 |
调用优先级:
1. **MCP 原生调用**:如果当前 AI Agent 已通过 MCP 协议连接了 tencent-survey 服务(工具列表中可见 `get_survey`、`create_survey`、`update_question`、`update_logic`、`list_answers`),直接调用工具即可
2. **mcporter CLI 调用**:如果 AI Agent 不支持 MCP 原生调用,或工具列表中未出现 tencent-survey 工具,通过终端执行 `mcporter call tencent-survey.<tool_name> --args '{...}'`
3. **确认工具可用**:使用 `mcporter list tencent-survey` 查看已注册的工具列表和参数 Schema
> 参考文档中的参数说明应与 MCP 工具 Schema 保持一致。如有冲突,以 `mcporter list tencent-survey` 返回的 Schema 为准。
## URL 解析规则
问卷投放链接格式为 `https://wj.qq.com/s2/{survey_id}/{hash}`
当用户提供链接时,取路径第二段为 `survey_id`:
| URL 格式 | 提取方式 | 示例 |
|----------|---------|------|
| `wj.qq.com/s2/{id}/{hash}` | 取路径第二段为 survey_id | `wj.qq.com/s2/292192/abc1` → `292192` |
> 提取到 `survey_id` 后,调用 `get_survey(survey_id=...)` 获取问卷详情。
## 数据模型
```
问卷(Survey)
├── 基本信息:id, hash, title, scene, state
├── 设置:prefix(欢迎语), suffix(结束语), started_at, end_at ...
├── 项目:project { id, name }
├── 纯文本内容:text(DSL 格式,包含标题和所有题目)
├── 自定义逻辑:survey_dsl { code, errors } ← 通过 update_logic 更新
├── 页面列表(Pages[])
│ └── 题目列表(Questions[])
│ ├── 基本属性:id, type, sub_type, title, required
│ ├── 选项列表(Options[]):id, text, exclusive
│ ├── 量表属性:starBeginNum, starNum
│ ├── 矩阵子问题:subTitles[]
│ └── 联动层级:levels[], groups[]
└── 回答列表(Answers[])← 通过 list_answers 获取
├── 基本信息:answer_id, respondent_nickname, started_at, ended_at
├── 地理信息:country, province, city
└── 回答内容(answer[])
└── 页面 → 题目回答 { id, type, text, options, blanks, groups }
```
> 核心嵌套关系:`Survey → Pages[] → Questions[] → Options[]`
> 回答嵌套关系:`Answer → answer[] (Pages) → questions[]`
## 常见工作流
### 查看问卷详情
参考文档:`references/get_survey.md`
1. 执行鉴权检查(见上方「配置」节)
2. 从用户提供的链接或 ID 获取 `survey_id`(链接解析见「URL 解析规则」)
3. 调用 `get_survey(survey_id=...)` 获取问卷详情
4. 递归解析 `pages → questions → options` 嵌套结构
5. 向用户展示问卷标题、题目列表等信息
### 创建问卷
参考文档:`references/create_survey.md`
1. 执行鉴权检查(见上方「配置」节)
2. 根据用户需求判断 `scene`:调查(1, 默认)、考试(3)、测评(6)、投票(8)
3. 按问卷文本语法组织 `text` 内容(语法详见参考文档)
4. 如果用户指定了项目,传入 `project_id`
5. 调用 `create_survey` 创建问卷
6. 从返回结果中取 `survey_id` 和 `hash`,拼接投放链接 `wj.qq.com/s2/{survey_id}/{hash}` 告知用户
7. 可选:调用 `get_survey` 确认问卷结构
### 更新问卷题目
参考文档:`references/update_question.md`
1. 执行鉴权检查(见上方「配置」节)
2. 调用 `get_survey(survey_id=...)` 获取问卷详情
3. 从返回的 `pages → questions` 中找到目标题目的 `id`(格式如 `q-1-xxxx`)
4. 参考返回的 `text` 字段了解当前问卷的 DSL 格式
5. 按 DSL 语法编写新的题目文本(只写这一道题,不需要问卷标题)
6. 调用 `update_question(survey_id=..., question_id=..., text=...)` 更新题目
7. 可选:再次调用 `get_survey` 确认更新结果
### 设置问卷逻辑
参考文档:`references/update_logic.md`
1. 执行鉴权检查(见上方「配置」节)
2. 调用 `get_survey(survey_id=...)` 获取问卷详情
3. 从返回的 `survey_dsl.code` 获取当前自定义逻辑代码(如有)
4. 从返回的 `pages → questions → options` 中获取题目 ID 和选项 ID
5. 根据用户需求编写 DSL 逻辑代码(ID 用反引号包裹,如 `` `q-1-abcd::o-100-EFGH` ``)
6. 如需追加规则,在原有 `survey_dsl.code` 基础上添加新规则
7. 调用 `update_logic(survey_id=..., dsl=...)` 更新逻辑(整体覆盖)
8. 可选:再次调用 `get_survey` 确认逻辑已生效
### 查看问卷回答
参考文档:`references/list_answers.md`
1. 执行鉴权检查(见上方「配置」节)
2. 调用 `list_answers(survey_id=...)` 获取首页回答
3. **⚠️ 注意翻页**:如果 `list.length == per_page`,说明可能还有下一页,需要循环翻页:
- 将返回的 `last_answer_id` 作为下一次请求的参数
- 继续调用 `list_answers(survey_id=..., last_answer_id=...)` 获取下一页
- 直到 `list.length < per_page` 表示已到最后一页
4. 解析每条回答的 `answer` 字段(嵌套结构:`页面 → 题目回答`)
5. 向用户展示回答汇总或详情
## 注意事项
- **标题可能含 HTML 标签**:`get_survey` 返回的 `title` 字段可能包含 `<p>`、`<br>` 等标签,展示给用户前需清理
- **text 字段(DSL 格式)**:`get_survey` 返回的 `text` 字段是纯文本 DSL 格式的问卷内容,可作为 `update_question` 的参考
- **text 参数格式**:`create_survey` 和 `update_question` 的 `text` 为必填,JSON 中换行使用 `\n`,选项不需要字母前缀(写 `满意` 而非 `A. 满意`)
- **update_question 需先获取 question_id**:必须先调用 `get_survey` 获取题目列表,不能自行构造 question_id
- **update_logic 整体覆盖**:每次调用 `update_logic` 会覆盖所有已有逻辑,追加规则需先获取当前 `survey_dsl.code`
- **survey_dsl 字段**:`get_survey` 返回的 `survey_dsl` 包含当前自定义逻辑代码(`code`)和错误信息(`errors`),作为 `update_logic` 的参考
- **list_answers 需要翻页**:回答列表使用游标分页,如果回答数量超过 `per_page`(默认 20),必须循环调用直到获取完所有数据
- **非幂等的写操作**:`create_survey` 每次调用都会创建新问卷,`update_question` 每次调用都会覆盖原题目
## 问题定位指南
### 常见错误码
| 错误码 | 错误类型 | 解决方案 |
|--------|---------|---------|
| `missing_token` | 请求未携带 Token | 检查 Authorization Header 或 access_token 参数 |
| `invalid_token_prefix` | Token 前缀错误 | 确认使用 `wjpt_` 开头的 Token |
| `invalid token` | Token 不存在或已撤销 | 重新创建 Token |
| `token expired` | Token 已过期 | 重新授权,详见 `references/auth.md` |
| `claim_error` | 问卷不属于当前团队 | 确认问卷 ID 正确且属于当前 Token 绑定的团队 |
| `invalid_text_format` | 文本格式错误(create_survey / update_question) | 检查 text DSL 语法 |
| `survey_not_editable` | 问卷不可编辑(update_question) | 问卷可能正在回收中,需先暂停 |
| `paid_function_trial_no_permission` | 付费功能无权限(如自定义逻辑) | 引导用户前往[腾讯问卷购买页](https://wj.qq.com/pricing)升级为付费版本 |
### 排查步骤
1. **检查错误信息**:查看返回的 error 字段,确定错误类型
2. **检查请求参数**:确认 `survey_id` 等参数值正确
3. **阅读参考文档**:`references/` 目录下包含所有工具的参数说明
4. **获取工具列表**:使用 `mcporter list tencent-survey` 确认工具是否可用
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+22
View File
@@ -0,0 +1,22 @@
{
"name": "腾讯问卷",
"installedAt": 1787572419124,
"source": "marketplace",
"version": "1.1.2",
"skillId": "skill_2053083197624082432",
"examples_zh": [
"用纯文本帮我创建一份员工满意度腾讯问卷,包含单选、量表和开放题。",
"把这份腾讯问卷的第 5 题改成多选题,并保留原有选项含义。",
"为课程反馈问卷设置逻辑:选择\"不满意\"的用户跳转到原因追问题。"
],
"examples_en": [
"Create a Tencent Survey from plain text for employee satisfaction, with single-choice, scale, and open-ended questions.",
"Update question 5 in this Tencent Survey into a multiple-choice question while preserving the option meanings.",
"Set survey logic so respondents who choose \"Dissatisfied\" in the course feedback form jump to a follow-up reason question."
],
"description_zh": "腾讯问卷操作(创建、修改、逻辑设置、统计)",
"description_en": "Tencent Survey operations (create, edit, logic settings, view statistical analysis of various surveys)\"",
"iconSource": "https://openplatform-cdn.codebuddy.cn/public/skills/icons/tencent-survey.png",
"iconDownloadedFrom": "https://openplatform-cdn.codebuddy.cn/public/skills/icons/tencent-survey.png",
"installedContentHash": "sha256:d727d450647bb8e47cc20c3395f01862e5a8c5f6243a66328734658fa0cdbfb5"
}
+103
View File
@@ -0,0 +1,103 @@
# 腾讯问卷鉴权检查
## 何时需要鉴权
- **会话首次调用工具前**:执行一次鉴权检查
- **鉴权通过后**:同一会话内后续调用**无需重复检查**,直接调用工具即可
- **按需重试**:仅当工具调用返回以下鉴权错误时,才需重新执行鉴权流程:
- `invalid_token` / `invalid token`
- `token expired`
- `missing_token`
- `invalid_token_prefix`
腾讯问卷授权流程,**必须按以下步骤执行**:
## 快速配置:通过环境变量传入 Token
如果已有 Token,可通过 `TENCENT_SURVEY_TOKEN` 环境变量直接完成配置,跳过 OAuth 授权:
```bash
TENCENT_SURVEY_TOKEN=xxx bash "${SKILL_DIR}/setup.sh" wj_check_and_start_auth
```
| 输出 | 处理方式 |
|------|---------|
| `READY` | ✅ Token 已写入配置,直接执行用户任务 |
| `ERROR:invalid_token_prefix` | Token 格式错误,必须以 `wjpt_` 开头 |
| `ERROR:save_token_failed` | Token 写入配置失败 |
> 设置了 `TENCENT_SURVEY_TOKEN` 时,脚本会优先使用该 Token,不再发起 OAuth 流程。
## 第一步:检查状态(立即返回)
未设置 `TENCENT_SURVEY_TOKEN` 时,进入标准 OAuth 设备授权流程:
```bash
bash "${SKILL_DIR}/setup.sh" wj_check_and_start_auth
```
> `${SKILL_DIR}` 为当前 skill 所在目录路径(即 `setup.sh` 所在目录)。
| 输出 | 处理方式 |
|------|---------|
| `READY` | ✅ 直接执行用户任务,**无需第二步** |
| `NONCE:<nonce>` | 记录 nonce 值,用于展示给用户(在 `AUTH_REQUIRED` 之前输出) |
| `AUTH_REQUIRED:<url>` | **立即**向用户展示授权链接(见下方模板),**然后执行第二步** |
| `ERROR:*` | 告知用户对应错误 |
## 第二步:等待授权完成(仅 AUTH_REQUIRED 时执行)
**展示授权链接后**,立即执行:
```bash
bash "${SKILL_DIR}/setup.sh" wj_wait_auth
```
| 输出 | 处理方式 |
|------|---------|
| `TOKEN_READY:ok` | ✅ 授权成功,Token 已写入配置,继续执行用户任务 |
| `AUTH_TIMEOUT` | 告知用户:「授权超时,请重新发起请求。」 |
| `ERROR:*` | 告知用户对应错误,请重新发起请求 |
## 第三步:人工兜底(前两步都失败的情况)
🔑 **手动获取 Token**:访问 [https://wj.qq.com/claw](https://wj.qq.com/claw) 登录后创建 Token,再通过环境变量配置:
```bash
TENCENT_SURVEY_TOKEN=<your_token> bash "${SKILL_DIR}/setup.sh" wj_check_and_start_auth
```
或手动执行 mcporter 命令:
```bash
mcporter config add tencent-survey "https://wj.qq.com/api/v2/mcp" \
--header "Authorization=Bearer <your_token>" \
--transport http \
--scope home
```
> Token 以 `wjpt_` 开头,可在 [https://wj.qq.com/oauth/authorize](https://wj.qq.com/oauth/authorize) 登录后创建。
## 授权链接展示模板
当第一步输出 `AUTH_REQUIRED:<url>` 时,**立即**向用户展示:
> 🔑 **需要先完成腾讯问卷授权**
>
> 请确保在**浏览器**中打开以下链接完成授权:**[点击授权腾讯问卷]({url})**
>
> 🔑 授权码(nonce):`{nonce}`
>
> ⚠️ 请使用 **QQ 或微信** 扫码 / 登录授权
>
> _(授权后将自动继续,无需回复)_
## 错误说明
| 错误 | 含义 |
|------|------|
| `ERROR:mcporter_not_found` | 缺少依赖,请先安装 Node.js |
| `ERROR:invalid_token_prefix` | `TENCENT_SURVEY_TOKEN` 格式错误,必须以 `wjpt_` 开头 |
| `ERROR:empty_token` | 授权异常,Token 为空 |
| `ERROR:save_token_failed` | Token 写入配置失败 |
| `AUTH_TIMEOUT` | 用户未在时限内完成授权 |
@@ -0,0 +1,406 @@
# create_survey 工具参考
## 概述
使用纯文本创建问卷。系统会自动将文本解析为问卷结构。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `text` | string | **是** | 问卷内容文本,换行使用 `\n` 代替(具体语法见下文) |
| `scene` | number | 否 | 问卷场景,默认为 1(调查),见下方场景枚举 |
| `project_id` | number | 否 | 项目 ID,将问卷创建到指定项目下 |
> **注意**:`text` 为必填参数。创建空白问卷时至少需要传入问卷标题,如 `text="新建问卷"`。
## 返回值
### 成功响应
```json
{
"survey_id": 716128,
"hash": "859f"
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `survey_id` | number | 新创建的问卷 ID |
| `hash` | string | 问卷 hash,用于拼接投放链接 |
> **投放链接拼接**:`https://wj.qq.com/s2/{survey_id}/{hash}`
### 失败响应
```json
{
"code": "InvalidArgument",
"error": {
"type": "invalid_text_format"
}
}
```
## 场景枚举
| scene | 场景 | 说明 |
|-------|------|------|
| **1** | **调查** | **默认值**,通用问卷调查 |
| 3 | 考试 | 带评分的考试问卷 |
| 6 | 测评 | 测评类问卷 |
| 8 | 投票 | 投票类问卷 |
## text 文本语法详解
`text` 参数使用纯文本描述问卷结构,系统会自动解析为对应的问卷题型。
> **重要**:在 JSON 参数中,所有换行必须使用 `\n` 代替。
>
> 📖 语法详细参考:
> - [内容编辑概述](https://wj.qq.com/docs/survey-dsl/content/)
> - [语法说明](https://wj.qq.com/docs/survey-dsl/content/grammar)
> - [普通场景语法](https://wj.qq.com/docs/survey-dsl/content/scene-default)
> - [考试场景语法](https://wj.qq.com/docs/survey-dsl/content/scene-exam)
> - [测评场景语法](https://wj.qq.com/docs/survey-dsl/content/scene-assessment)
### 基础结构
```
问卷标题
问卷引导语(可选)
题目标题[题型](描述)
选项/内容
```
- 第一行为**问卷标题**
- 标题后可跟**引导语**(可选)
- 使用 `=== 分页 ===` 插入分页符
- 题目之间用空行分隔
### 题型语法列表
| 题型 | 语法 | 说明 |
|------|------|------|
| 单选题 | `标题[单选题](描述)\n选项A\n选项B` | 选项另起一行,每行一个 |
| 多选题 | `标题[多选题](描述)\n选项A\n选项B` | 选项另起一行 |
| 下拉题 | `标题[下拉题](描述)\n选项A\n选项B` | 选项另起一行 |
| 排序题 | `标题[排序题](描述)\n选项A\n选项B` | 选项另起一行 |
| 单行文本题 | `标题[单行文本题](描述)` | 无需选项 |
| 多行文本题 | `标题[多行文本题](描述)` | 无需选项 |
| 多项填空题 | `标题[多项填空题](描述)\n填空1:____` | 使用 `____` 指定填空位置 |
| 量表题 | `标题[量表题](描述)\n1~5` | 使用数字范围指定量表 |
| 日期时间题 | `标题[日期时间题](描述)` | 无需选项 |
| 地理位置题 | `标题[地理位置题](描述)` | 无需选项 |
| 附件题 | `标题[附件题](描述)` | 无需选项 |
| 手写签名题 | `标题[手写签名题](描述)` | 无需选项 |
| 段落说明 | `描述内容[段落说明]` | 用于纯文本段落说明(也称 `[文本描述题]`) |
| 矩阵单选题 | `标题[矩阵单选题](描述)\n选项A 选项B\n子问题1\n子问题2` | 选项空格分隔,子问题另起一行 |
| 矩阵多选题 | `标题[矩阵多选题](描述)\n选项A 选项B\n子问题1\n子问题2` | 同上 |
| 矩阵量表题 | `标题[矩阵量表题](描述)\n1~5\n子问题1\n子问题2` | 量表范围在前,子问题在后 |
| 联动题 | `标题[联动题](描述)\n第一层 第二层\n答案A+子答案A1+子答案A2\n答案B+子答案B1` | 层级名空格分隔,答案用 `+` 连接 |
> **提示**:`(描述)` 部分为可选的题目描述/说明文字。`[单选题]` 为默认题型,可以省略。
### 题目结构
每道题目的完整结构为:
```
标题[题型][设置](描述)
```
各部分顺序固定,不可调换。其中 `[设置]` 和 `(描述)` 都是可选的。
### 题目设置
支持在题型后追加设置标记:
| 设置 | 语法 | 说明 |
|------|------|------|
| 必答 | `[必答]` | 该题必须作答 |
| 选答 | `[选答]` | 该题可以跳过 |
示例:`您的姓名[单行文本题][必答](请填写真实姓名)`
### 考试场景专用设置
在考试场景(`scene=3`)中,题目可追加答案、分值和评分机制设置:
| 设置 | 语法 | 说明 |
|------|------|------|
| 答案 | `[答案:A、B]` | 正确答案,选项索引从 A 开始 |
| 填空答案 | `[答案:1.答案1、2.答案2]` | 各填空的正确答案 |
| 分值 | `[分数:5]` | 该题总分值 |
| 多空分值 | `[分数:2、3]` | 各空/各选项的分值 |
| 全部正确得分 | `[全部]` | 完全匹配答案才得分 |
| 部分正确得分 | `[部分]` | 部分匹配也可得分(多选题、不定项选择题) |
| 按空得分 | `[按空]` | 按填空分别计分(多项填空题) |
| 人工评分 | `[人工]` | 需人工阅卷评分(所有题型均可) |
> **考试题型**:考试场景额外支持 `[判断题]`、`[不定项选择题]`、`[问答题]` 三种题型。
### 测评场景专用题型
在测评场景(`scene=6`)中,选择题使用专用题型标记:
| 题型 | 语法 | 说明 |
|------|------|------|
| 测评单选题 | `[测评单选题]` | 替代普通场景的 `[单选题]` |
| 测评多选题 | `[测评多选题]` | 替代普通场景的 `[多选题]` |
> 测评场景也支持 `[量表题]`、`[矩阵单选题]`、`[矩阵量表题]`、`[单行文本题]`、`[多行文本题]`、`[多项填空题]`、`[文本描述题]`。
### 富文本语法
在题目标题、描述和选项中支持使用以下富文本语法:
| 类型 | 语法 | 说明 |
|------|------|------|
| 高亮 | `**高亮文本**` | 文本加粗/高亮显示 |
| 链接 | `[链接文本](https://example.com)` | 插入超链接 |
| 图片 | `![alt](https://example.com/img.png){宽度, 高度}` | 插入图片,`{宽度, 高度}` 必填,值为数字或 `auto` |
| 视频 | `!video(视频地址)` | 插入视频,仅支持腾讯视频、哔哩哔哩、优酷 |
### 完整示例(普通调查场景)
以下示例覆盖了普通调查场景(`scene=1`,默认)中的所有题型:
```
员工满意度调查
为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们。
1. 您在公司工作了多久?[单选题]
1年以下
1-3年
3-5年
5年以上
2. 您对以下哪些方面比较满意?[多选题]
工作环境
薪资福利
团队氛围
职业发展
3. 请选择您的部门[下拉题]
技术部
产品部
市场部
人力资源部
4. 请对以下福利按重要程度排序[排序题]
薪资待遇
年假天数
培训机会
弹性工作
5. 请对整体工作满意度打分[量表题](5分表示非常满意,1分表示非常不满意)
1~5
6. 您的姓名[单行文本题]
7. 您有什么建议或意见?[多行文本题]
8. 请填写以下信息[多项填空题]
姓名:____ 工号:____
9. 请选择您的入职日期[日期时间题]
10. 请选择您的办公地点[地理位置题]
11. 请上传相关材料[附件题]
12. 请签名确认[手写签名题]
本问卷到此结束,感谢您的参与![段落说明]
=== 分页 ===
13. 请对各部门的协作效率打分[矩阵单选题]
非常好 较好 一般 较差
技术部
产品部
市场部
14. 以下哪些部门您有过合作经历?[矩阵多选题]
有合作 有交流 无接触
技术部
产品部
市场部
15. 请为各方面打分[矩阵量表题](1分最低,5分最高)
1~5
工作环境
薪资福利
团队氛围
16. 请选择您的所在区域[联动题]
省份 城市
广东省+广州市+深圳市+东莞市
北京市+朝阳区+海淀区
上海市+浦东新区+徐汇区
```
### 考试场景示例
考试场景(`scene=3`)支持设置答案、分值和评分机制。语法在题型后追加 `[答案:...]`、`[分数:...]`、`[全部]`/`[部分]`/`[按空]`/`[人工]`。
```
期中考试
考生姓名:____ 班级:____ 考号:____
1. 在下列物体中,难溶于水的物体是?[单选题][答案:D][分数:5][全部]
味精
酱油
酒精
食用油
2. 地球是太阳系中最大的行星[判断题][答案:A][分数:3][全部]
错误
正确
3. 哪些颜色属于奥运五环的颜色?[多选题][答案:A、B、D][分数:5][部分]
蓝色
黑色
紫色
红色
棕色
4. 哪些是哺乳动物?[不定项选择题][答案:B、C][分数:4][部分]
鳄鱼
鲸鱼
海豚
蜥蜴
5. 古诗文默写[多项填空题][答案:1.秋风萧瑟、2.洪波涌起][分数:2、3][按空]
树木丛生,百草丰茂。____,____。(曹操《观沧海》)
6. 以"一件令人感动的事"为题写一篇小作文[问答题][分数:20][人工]
```
### 测评场景示例
测评场景(`scene=6`)使用 `[测评单选题]`、`[测评多选题]` 等专用题型:
```
职业性格测评
本测评将帮助您了解自己的职业性格类型,请根据真实感受作答。
1. 在团队合作中,您通常扮演什么角色?[测评单选题]
领导者
协调者
执行者
创意提供者
2. 以下哪些描述符合您的工作风格?[测评多选题]
注重细节
喜欢创新
善于沟通
偏好独立工作
3. 请评价您对当前工作的满意程度[量表题](1分非常不满意,5分非常满意)
1~5
4. 请对以下陈述表示您的同意程度[矩阵单选题]
非常同意 同意 中立 不同意 非常不同意
我善于处理压力
我喜欢接受新挑战
我注重工作与生活的平衡
5. 请为以下能力自评打分[矩阵量表题](1分最低,5分最高)
1~5
沟通能力
领导能力
学习能力
团队协作
6. 请简要描述您的职业目标[多行文本题]
```
### 请求体示例
```json
{
"text": "员工满意度调查\n\n为了给您提供更好的服务,希望您能抽出几分钟时间。\n\n1. 您在公司工作了多久?[单选题]\n1年以下\n1-3年\n3-5年\n5年以上\n\n2. 您对以下哪些方面比较满意?[多选题]\n工作环境\n薪资福利\n团队氛围\n职业发展\n\n3. 请对整体工作满意度打分[量表题](5分表示非常满意,1分表示非常不满意)\n1~5\n\n4. 您有什么建议或意见?[多行文本题]"
}
```
## 调用示例
### 创建带内容的问卷
```
create_survey(text="满意度调查\n\n1. 您对工作环境是否满意?[单选题]\n非常满意\n满意\n一般\n不满意\n\n2. 您有什么建议?[多行文本题]")
```
### 创建在线考试
```
create_survey(scene=3, text="期中考试\n\n1. 1+1=?[单选题][答案:B][分数:5][全部]\n1\n2\n3\n4\n\n2. 以下哪些是偶数?[多选题][答案:A、C][分数:4][部分]\n2\n3\n4\n5\n\n3. 请默写古诗[问答题][分数:10][人工]")
```
### 创建投票
```
create_survey(scene=8, text="年度最佳员工投票\n\n1. 请选择您心目中的最佳员工[单选题]\n张三\n李四\n王五")
```
### 创建测评
```
create_survey(scene=6, text="性格测评\n\n1. 遇到困难时,您通常会?[测评单选题]\n独立解决\n寻求帮助\n暂时搁置\n\n2. 请为自信程度打分[量表题]\n1~5")
```
### 指定项目下创建
```
create_survey(project_id=100, text="项目反馈问卷\n\n1. 项目进展如何?[单选题]\n按计划进行\n有延迟\n已完成")
```
### mcporter 调用
```bash
# 创建调查
mcporter call tencent-survey.create_survey --args '{"text": "满意度调查\n\n1. 满意吗?[单选题]\n满意\n不满意"}'
# 指定场景(投票)
mcporter call tencent-survey.create_survey --args '{"scene": 8, "text": "投票\n\n1. 选谁?[单选题]\n甲\n乙"}'
```
## 错误码
| error.type | 错误描述 | 解决方案 |
|------------|---------|---------|
| `permission_denied` | 无创建权限 | 确认 Token 有创建问卷的权限 |
| `invalid_text_format` | 文本内容格式错误 | 检查 text 语法是否正确,参考上方语法说明 |
| `invalid_argument` | 参数校验不通过 | 检查参数类型和值是否正确 |
| `resource_exhausted` | 创建数量超过限制 | 当前团队问卷数量已达上限 |
## 注意事项
1. **text 为必填参数**:必须提供 text 内容
2. **project_id 需要有效**:传入无效的 project_id 会导致创建失败
3. **非幂等操作**:每次调用都会创建一份新问卷,请勿重复调用
4. **换行必须用 `\n`**:JSON 中不能直接换行,必须用 `\n` 替代
5. **选项不需要字母前缀**:选项直接写内容即可(如 `满意`),不需要写 `A. 满意`
6. **题目内不允许空行**:一道题目的标题和选项之间不能有空行,否则会被识别为两道题
7. **量表范围用 `~`**:量表题使用 `1~5` 格式(全角 `~` 和半角 `~` 均支持)
8. **考试场景题型差异**:考试场景额外支持 `[判断题]`、`[不定项选择题]`、`[问答题]`,答案索引从 A 开始
9. **测评场景题型差异**:测评场景的选择题需使用 `[测评单选题]`、`[测评多选题]`
## Annotations(工具注解)
| 注解 | 值 | 说明 |
|------|---|------|
| `readOnlyHint` | false | 非只读操作 |
| `destructiveHint` | false | 非破坏性操作 |
| `idempotentHint` | false | **非幂等**,每次调用都创建新问卷 |
| `openWorldHint` | false | 内部调用 |
@@ -0,0 +1,231 @@
# get_survey 工具参考
## 概述
获取指定问卷的详细信息,包括标题、设置、页面、题目和选项。同时返回纯文本格式的问卷内容(`text` 字段,DSL 格式)。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `survey_id` | number | **是** | 问卷 ID |
## 返回值
```json
{
"id": 292192,
"hash": "abc123def",
"scene": "1",
"title": "员工满意度调查",
"prefix": "欢迎参与本次调查",
"suffix": "感谢您的参与",
"state": 2,
"page_count": 1,
"topic_count": 3,
"started_at": "2026-01-15 00:00:00",
"end_at": "2026-07-01 00:00:00",
"createTime": 1736899800,
"updateTime": 1736899900,
"creator_user_id": 60000000001,
"project": {
"id": 1234,
"name": "2026年度调查"
},
"text": "员工满意度调查\n\n欢迎参与本次调查\n\n1. 您对工作环境是否满意?[单选题]\n非常满意\n满意\n一般\n不满意\n\n2. 请对整体满意度打分[量表题]\n1~5\n\n3. 请填写您的建议[多行文本题]",
"survey_dsl": {
"code": "if `q-1-xxxx::o1` then show `q2`",
"errors": []
},
"pages": [
{
"id": "p1",
"index": "0",
"questions": [
{
"id": "q-1-xxxx",
"index": 1,
"type": "radio",
"sub_type": 0,
"title": "您对工作环境是否满意?",
"description": "",
"required": true,
"options": [
{"id": "o1", "text": "非常满意"},
{"id": "o2", "text": "满意"},
{"id": "o3", "text": "一般"},
{"id": "o4", "text": "不满意"}
]
},
{
"id": "q2",
"index": 2,
"type": "star",
"title": "请对整体满意度打分",
"required": true,
"starBeginNum": 1,
"starNum": 5
},
{
"id": "q3",
"index": 3,
"type": "textarea",
"title": "请填写您的建议",
"required": false
}
]
}
]
}
```
### Survey 对象
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | number | 问卷 ID |
| `hash` | string | 问卷 hash,用于拼接投放链接 `https://wj.qq.com/s2/{id}/{hash}` |
| `scene` | string | 问卷场景:`"1"`=调查, `"3"`=考试, `"6"`=测评, `"8"`=投票 |
| `title` | string | 问卷标题(**可能包含 HTML 标签**) |
| `prefix` | string | 欢迎语 |
| `suffix` | string | 结束语 |
| `state` | number | 状态:0=草稿, 2=回收中, 3=暂停回收 |
| `page_count` | number | 页数 |
| `topic_count` | number | 问题数 |
| `started_at` | string | 回收开始时间 |
| `end_at` | string | 回收结束时间 |
| `createTime` | number | 创建时间(时间戳) |
| `updateTime` | number | 更新时间(时间戳) |
| `creator_user_id` | number | 问卷创建者的用户 ID |
| `project` | object | 问卷所属项目 |
| `project.id` | number | 项目 ID |
| `project.name` | string | 项目名称 |
| `text` | string | 纯文本格式的问卷内容(DSL 格式),包含标题、引导语和所有题目。可直接用于 `update_question` 等工具的参考 |
| `survey_dsl` | object | 问卷自定义逻辑信息 |
| `survey_dsl.code` | string | 当前自定义逻辑代码(DSL 脚本),可作为 `update_logic` 的参考。为空字符串时表示未设置逻辑 |
| `survey_dsl.errors` | array | 逻辑代码中的语法错误列表,为空数组时表示无错误 |
| `pages` | array | 页面列表 |
### Page 对象
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 单页标识 |
| `index` | string | 序号 |
| `questions` | array | 该页面下的题目列表 |
### Question 对象
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 题目 ID |
| `index` | number | 题目编号 |
| `type` | string | 题目类型(见下方 type 枚举) |
| `sub_type` | number | 题目子类型(有 sub_type 时优先根据 sub_type 判断) |
| `title` | string | 题目标题 |
| `description` | string | 题目备注 |
| `required` | boolean | 是否必答 |
| `options` | array | 选项列表(仅选择题有) |
| `hidden` | boolean | 是否隐藏 |
| `random` | boolean | 选项是否随机 |
| `goto` | object | 答题后跳转题目 |
| `maxlength` | object | 多选题最多可选 / 文本题最大字数 |
| `starBeginNum` | number | 量表题起始值 |
| `starNum` | number | 量表范围(2~10) |
| `starShowCustomStart` | string | 量表题起始文案 |
| `starShowCustomEnd` | string | 量表题末尾文案 |
| `subTitles` | array | 矩阵题子问题列表 |
| `subTitles[].id` | string | 矩阵题子问题 ID |
| `subTitles[].text` | string | 矩阵题子问题文本 |
| `levels` | array | 联动题各级标题 |
| `groups` | array | 联动题选项列表(嵌套) |
### Option 对象
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 选项 ID |
| `text` | string | 选项文案 |
| `exclusive` | boolean | 多选题中是否为互斥选项 |
| `noRandom` | boolean | 选项随机时是否固定当前选项位置 |
| `goto` | object | 选择后跳转题目 |
| `display` | object | 选择后显示题目 |
### type 枚举
| type | 说明 |
|------|------|
| `radio` | 单选 |
| `checkbox` | 多选 |
| `select` | 下拉 |
| `text` | 单行文本 |
| `textarea` | 多行文本 |
| `blanks` | 填空 |
| `star` | 量表/NPS |
| `sort` | 排序 |
| `matrix_radio` | 矩阵单选 |
| `matrix_checkbox` | 矩阵多选 |
| `matrix_star` | 矩阵量表 |
| `matrix_blank` | 矩阵填空 |
| `chained_selects` | 联动 |
| `upload` | 图片/文件 |
| `description` | 文本描述 |
| `datetime` | 日期/时间 |
| `signature` | 手写签名 |
| `address` | 地理位置 |
| `phone` | 手机号 |
| `sheet` | 自增表格 |
### 基本设置字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `prev` | boolean | 是否允许回到上一页 |
| `titleIndex` | boolean | 是否显示题目序号 |
| `login_check` | boolean | 是否开启登录验证 |
| `answer_count` | number | 允许回答次数 |
| `whitelist_enable` | boolean | 是否开启白名单 |
| `redirect_url` | string | 答题后跳转链接 |
| `webhook_url` | string | 答题后推送数据的地址 |
| `is_allow_update_answer` | boolean | 是否允许修改答案 |
| `is_enabled_location` | boolean | 是否获取用户位置信息 |
## 调用示例
### 直接调用
```
get_survey(survey_id=292192)
```
### mcporter 调用
```bash
mcporter call tencent-survey.get_survey --args '{"survey_id": 292192}'
```
## 错误码
| error.type | 错误描述 | 解决方案 |
|------------|---------|---------|
| `invalid_auth_status` | 权限类型错误 | 检查 Token 权限 |
| `claim_error` | 权限校验错误 | 问卷不属于当前 Token 绑定的团队 |
| `get_survey_error` | 获取数据错误 | 确认 survey_id 正确且问卷存在 |
## 注意事项
1. **title 可能包含 HTML 标签**:如 `<p>标题</p>`、`<br>` 等,展示给用户前建议清理
2. **嵌套结构**:数据为 `pages[] → questions[] → options[]`,需要递归解析
3. **type 与 sub_type**:有 `sub_type` 时优先根据 `sub_type` 判断题目类型
4. **投放链接拼接**:`https://wj.qq.com/s2/{id}/{hash}`
5. **text 字段**:返回纯文本 DSL 格式的问卷内容,包含标题和所有题目。该字段可作为 `update_question` 工具的参考,了解当前问卷的文本结构
6. **survey_dsl 字段**:返回当前问卷的自定义逻辑代码(`code`)和错误信息(`errors`)。`code` 可作为 `update_logic` 工具的参考,了解当前已设置的逻辑规则。追加逻辑时需在原有 `code` 基础上修改
## Annotations(工具注解)
| 注解 | 值 | 说明 |
|------|---|------|
| `readOnlyHint` | true | 只读操作,不修改任何数据 |
| `destructiveHint` | false | 非破坏性操作 |
| `idempotentHint` | true | 幂等操作,多次调用结果一致 |
| `openWorldHint` | false | 内部调用 |
@@ -0,0 +1,254 @@
# list_answers 工具参考
## 概述
获取指定问卷的回答列表,支持游标分页。返回回答总数、回答详情列表和最后一条回答 ID(用于翻页)。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `survey_id` | number | **是** | 问卷 ID |
| `per_page` | number | 否 | 每页返回条数,默认 20,最大 1000 |
| `last_answer_id` | number | 否 | 上一页最后一条回答的 ID,用于翻页。默认 0 表示从头开始 |
## 返回值
### 成功响应
```json
{
"total": 150,
"list": [
{
"id": 1,
"survey_id": 292192,
"answer_id": 1,
"respondent_nickname": "用户A",
"started_at": "2026-01-15 10:30:00",
"ended_at": "2026-01-15 10:35:22",
"score": 0,
"country": "中国",
"province": "广东",
"city": "深圳",
"answer": [
{
"id": "p1",
"questions": [
{
"id": "q-1-xxxx",
"identity": "q-1-abcd1234",
"title": "您对工作环境是否满意?",
"type": "radio",
"sub_type": 0,
"text": "非常满意",
"options": [
{"id": "o1", "text": "非常满意", "selected": true},
{"id": "o2", "text": "满意"},
{"id": "o3", "text": "一般"},
{"id": "o4", "text": "不满意"}
]
},
{
"id": "q2",
"identity": "q-1-efgh5678",
"title": "请填写您的建议",
"type": "textarea",
"sub_type": 0,
"text": "希望改善食堂伙食"
}
]
}
]
}
],
"last_answer_id": 20
}
```
### 顶层字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `total` | number | 问卷回答**总数**(全量统计,非当前页条数) |
| `list` | array | 当前页的回答列表(`AnswerPayload[]`) |
| `last_answer_id` | number | 当前页最后一条回答的 ID,用于请求下一页 |
### AnswerPayload 对象
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | number | 回答记录 ID |
| `survey_id` | number | 问卷 ID |
| `answer_id` | number | 回答序号 |
| `respondent_id` | number | 答题者 ID |
| `respondent_nickname` | string | 答题者昵称 |
| `respondent_avatar` | string | 答题者头像 |
| `respondent_type` | string | 答题者类型 |
| `openid` | string | 答题者 OpenID |
| `started_at` | string | 开始答题时间 |
| `ended_at` | string | 提交答题时间 |
| `score` | number | 考试/测评场景下的得分 |
| `country` | string | 答题者所在国家 |
| `province` | string | 答题者所在省份 |
| `city` | string | 答题者所在城市 |
| `ua` | string | 答题者 User-Agent |
| `ip` | string | 答题者 IP 地址 |
| `answer` | array | 回答内容(按页面分组),见下方结构说明 |
### answer 嵌套结构
回答内容按 `页面 → 题目` 的层级组织:
```
answer[] ← 页面列表
├── id: "p1" ← 页面 ID
└── questions[] ← 该页面下的题目回答
├── id: "q1" ← 题目 ID
├── identity: "q-1-xxxx" ← 题目唯一标识
├── title: "题目标题" ← 题目标题
├── type: "radio" ← 题型
├── text: "用户填写的内容" ← 文本类题目的回答文本
├── options[] ← 选择题的选项(含选中状态)
├── groups[] ← 矩阵题的分组回答
└── blanks[] ← 填空题的填空内容
```
### Question(回答中的题目)字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 题目 ID |
| `identity` | string | 题目唯一标识(如 `q-1-xxxx`) |
| `title` | string | 题目标题 |
| `description` | string | 题目描述 |
| `type` | string | 题型(`radio`/`checkbox`/`text`/`textarea`/`star` 等) |
| `sub_type` | number | 题目子类型 |
| `text` | any | 回答内容(文本题为填写的文字,选择题为选中选项的文本) |
| `options` | array | 选择题的选项列表(含选中状态) |
| `groups` | array | 矩阵题的分组回答 |
| `blanks` | array | 填空题的各填空内容 |
| `id_list` | array | 选中选项的 ID 列表 |
| `text_list` | array | 选中选项的文本列表 |
| `signature_id` | string | 手写签名 ID |
| `files` | array | 附件题的文件列表 |
## 分页机制(重要)
`list_answers` 使用**游标分页**(Cursor-based Pagination),不是传统的 offset 分页。
### 工作原理
- 每次请求返回一页数据和 `last_answer_id`
- 将上一次返回的 `last_answer_id` 作为下一次请求的参数,获取下一页
- 数据按 answer_id **升序**排列
### 翻页流程
```
# 第一页:不传 last_answer_id(默认从头开始)
list_answers(survey_id=292192)
→ { total: 150, list: [...20条...], last_answer_id: 20 }
# 第二页:传入上一页返回的 last_answer_id
list_answers(survey_id=292192, last_answer_id=20)
→ { total: 150, list: [...20条...], last_answer_id: 45 }
# 第三页
list_answers(survey_id=292192, last_answer_id=45)
→ { total: 150, list: [...20条...], last_answer_id: 78 }
# ...继续翻页...
# 最后一页:返回不足 per_page 条,翻页结束
list_answers(survey_id=292192, last_answer_id=135)
→ { total: 150, list: [...10条...], last_answer_id: 150 }
```
### 判断是否还有下一页
| 条件 | 含义 |
|------|------|
| `list.length == per_page` | 可能还有下一页,继续用 `last_answer_id` 翻页 |
| `list.length < per_page` | **已到最后一页**,无需继续翻页 |
| `list` 为空数组 | **没有更多数据** |
> ⚠️ **注意**:`total` 是问卷回答总数,不是当前页的条数。判断翻页是否结束,应该看 `list.length` 是否小于 `per_page`,而不是看 `total`。
### 获取全部回答的伪代码
```python
all_answers = []
last_id = 0
while True:
result = list_answers(survey_id=292192, per_page=100, last_answer_id=last_id)
all_answers.extend(result.list)
if len(result.list) < 100: # 不足一页,已到末尾
break
last_id = result.last_answer_id
print(f"共 {result.total} 条回答,已全部获取 {len(all_answers)} 条")
```
## 调用示例
### 获取首页回答
```
list_answers(survey_id=292192)
```
### 指定每页条数
```
list_answers(survey_id=292192, per_page=50)
```
### 翻页获取下一页
```
list_answers(survey_id=292192, per_page=50, last_answer_id=45)
```
### mcporter 调用
```bash
# 获取首页(默认 20 条)
mcporter call tencent-survey.list_answers --args '{"survey_id": 292192}'
# 每页 100 条
mcporter call tencent-survey.list_answers --args '{"survey_id": 292192, "per_page": 100}'
# 翻页
mcporter call tencent-survey.list_answers --args '{"survey_id": 292192, "per_page": 100, "last_answer_id": 45}'
```
## 错误码
| error.type | 错误描述 | 解决方案 |
|------------|---------|---------|
| `invalid_auth_status` | 权限类型错误 | 检查 Token 权限 |
| `claim_error` | 权限校验错误 | 问卷不属于当前 Token 绑定的团队 |
| `invalid_argument` | 参数校验不通过 | 检查 survey_id 是否正确 |
| `get_answers_error` | 获取回答列表错误 | 确认 survey_id 正确且问卷存在 |
## 注意事项
1. **游标分页**:使用 `last_answer_id` 翻页,不支持跳页;必须按顺序逐页获取
2. **需要循环翻页**:如果回答数量多于 `per_page`,必须循环调用直到获取完所有数据
3. **per_page 上限 1000**:即使传入大于 1000 的值,也会被截断为 1000
4. **total 是全量统计**:`total` 字段表示问卷回收的总回答数,不随分页变化
5. **answer 嵌套结构**:每条回答的 `answer` 字段按 `页面 → 题目` 嵌套,需要递归解析
6. **题目回答内容**:不同题型的回答内容存储方式不同(`text` / `options` / `blanks` / `groups`),需根据 `type` 字段判断
## Annotations(工具注解)
| 注解 | 值 | 说明 |
|------|---|------|
| `readOnlyHint` | true | 只读操作,不修改任何数据 |
| `destructiveHint` | false | 非破坏性操作 |
| `idempotentHint` | true | 幂等操作,多次调用结果一致 |
| `openWorldHint` | false | 内部调用 |
@@ -0,0 +1,235 @@
# update_logic 工具参考
## 概述
更新问卷的自定义逻辑设置(DSL 代码)。可通过 `get_survey` 返回的 `survey_dsl` 字段获取当前逻辑代码,修改后整体传入。传入空字符串可清空所有逻辑。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `survey_id` | number | **是** | 问卷 ID |
| `dsl` | string | **是** | 问卷自定义逻辑代码(DSL 脚本语法) |
> **重要**:`dsl` 中引用题目和选项时需使用反引号包裹的 ID 格式(如 `` `q-1-abcd::o-100-ABCD` ``),ID 可从 `get_survey` 返回的题目和选项信息中获取。
## 返回值
### 成功响应
```json
{
"survey_id": 716128,
"result": "success"
}
```
### 失败响应
```json
{
"survey_id": 716128,
"result": "failed",
"error": "invalid_dsl_syntax: ..."
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `survey_id` | number | 问卷 ID |
| `result` | string | `"success"` 表示成功,`"failed"` 表示失败 |
| `error` | string | 仅失败时返回,错误描述信息 |
## DSL 语法说明
自定义逻辑使用 DSL(Domain-Specific Language)脚本语法。多条规则用换行分隔。
> 📖 完整语法文档:https://wj.qq.com/docs/dsl/grammar
### 引用格式
在通过 MCP 工具编写 DSL 时,题目和选项 ID 需要使用**反引号包裹**:
| 元素 | 格式 | 示例 |
|------|------|------|
| 题目 | `` `q-1-xxxx` `` | `` `q-1-abcd` `` |
| 选项 | `` `q-1-xxxx::o-100-ABCD` `` | `` `q-1-abcd::o-100-EFGH` `` |
| 矩阵子问题 | `` `q-1-xxxx::s-200-IJKL` `` | `` `q-1-abcd::s-200-IJKL` `` |
| 填空项 | `` `q-1-xxxx::b-300-MNOP` `` | `` `q-1-abcd::b-300-MNOP` `` |
> **注意**:ID 值必须从 `get_survey` 返回的数据中获取,不能自行构造。
### 条件操作符
| 操作符 | 说明 | 示例 |
|--------|------|------|
| `and` | 逻辑与 | `if Q1A1 and Q2A2 then show Q3` |
| `or` | 逻辑或 | `if Q1A1 or Q1A2 then show Q2` |
| `not` | 逻辑非 | `if Q1 and not Q1A3 then show Q2` |
| `()` | 分组 | `if (Q1A1 or Q1A2) and Q2A1 then show Q3` |
| `>` `>=` `==` `<` `<=` | 比较 | `if Q1 > 5 then show Q2` |
| `+` `-` `*` `/` | 算数运算 | `if Q1 + Q2 > 10 then show Q3` |
### 行为控制语句
| 语句类型 | 语法格式 | 说明 |
|---------|---------|------|
| **条件显示** | `if <条件> then show <目标>` | 满足条件时显示题目或选项 |
| **条件隐藏** | `if <条件> then hide <目标>` | 满足条件时隐藏题目或选项 |
| **直接隐藏** | `hide <目标>` | 直接隐藏题目或选项(无条件) |
| **连线跳转** | `if <条件> then branch from <题目> to <目标>` | 满足条件时从某题跳转到指定题目或 `END` |
| **内容替换** | `replace "<文本>" in <题目> title with <来源>` | 将题目标题中的指定文本替换为另一题的回答 |
| **随机排序** | `shuffle <范围>` | 对题目或选项进行随机排序 |
| **随机抽取** | `random show <数量> from <范围>` | 从题目或选项中随机抽取指定数量显示 |
| **自动圈选** | `set <选项>` | 自动选中指定选项 |
| **自动填充** | `set <题目> = <来源>` | 自动填充文本题内容 |
### 特殊关键字
| 关键字 | 说明 | 示例 |
|--------|------|------|
| `END` | 结束页,用于跳转到问卷结尾 | `branch from Q1 to END` |
| `len` | 答案个数 | `if len Q1 > 2 then show Q3` |
| `index` | 答案排在第几位 | `if index Q1A1 == 1 then show Q2` |
| `RANDBETWEEN(min, max)` | 随机生成整数 | `if RANDBETWEEN(1, 10) > 5 then show Q2` |
| `LANG()` | 获取答题者语言 | `if LANG() == "zhs" then show Q1` |
| `PARAMS("key")` | 获取自定义参数 | `if PARAMS("source") == "wechat" then show Q2` |
| `#` | 注释 | `# 这是注释,不会被执行` |
### 范围表示
| 格式 | 说明 | 示例 |
|------|------|------|
| `Q1~3` | 题目 Q1 到 Q3 | `shuffle Q1~3` |
| `Q1A1~4` | 题目 Q1 的选项 A1 到 A4 | `shuffle Q1A1~4` |
| `Q1,Q3,Q5` | 不连续的题目 | `random show 1 from Q1,Q3,Q5` |
> 当连续 3 个以上序号时,系统会自动合并为 `~` 格式。
### weight 权重
`random show` 支持设置权重:
```
random show 1 from Q1~3 weight by 1:1:2
```
表示 Q3 被抽取的概率是 Q1、Q2 的两倍。
## 调用示例
### 典型工作流
```
# 1. 先获取问卷详情,确认题目和选项 ID
get_survey(survey_id=716128)
# 返回中查看 survey_dsl.code 获取当前逻辑代码
# 从 pages → questions → options 中获取题目和选项的 ID
# 2. 编写并更新逻辑
update_logic(survey_id=716128, dsl="if `q-1-abcd::o-100-EFGH` then show `q-1-ijkl`")
```
### 条件显示逻辑
```
# 选择了第1题的第1个选项时,显示第2题
update_logic(survey_id=716128, dsl="if `q-1-abcd::o-100-EFGH` then show `q-1-ijkl`")
```
### 多条件组合
```
# 第1题选了A 且 第2题选了B 时,显示第3题
update_logic(survey_id=716128, dsl="if `q-1-abcd::o-100-EFGH` and `q-1-ijkl::o-200-MNOP` then show `q-1-qrst`")
```
### 连线跳转(甄别题)
```
# 选择了第1题某选项时,跳转到问卷结束
update_logic(survey_id=716128, dsl="if `q-1-abcd::o-100-EFGH` then branch from `q-1-abcd` to END")
```
### 内容替换
```
# 将第3题标题中的"XXX"替换为第1题的回答
update_logic(survey_id=716128, dsl="replace \"XXX\" in `q-1-qrst` title with `q-1-abcd`")
```
### 随机排序与随机抽取
```
# 随机排序第1题的选项1~4
update_logic(survey_id=716128, dsl="shuffle `q-1-abcd::o-100-A`~`q-1-abcd::o-100-D`")
# 从题目1~3中随机抽取1道显示
update_logic(survey_id=716128, dsl="random show 1 from `q-1-abcd`~`q-1-qrst`")
```
### 多条规则组合
```
# 多条规则用换行分隔
update_logic(survey_id=716128, dsl="if `q-1-abcd::o-100-EFGH` then show `q-1-ijkl`\nif `q-1-abcd::o-100-MNOP` then show `q-1-qrst`\nreplace \"XXX\" in `q-1-uvwx` title with `q-1-abcd`")
```
### 清空所有逻辑
```
update_logic(survey_id=716128, dsl="")
```
### mcporter 调用
```bash
# 设置条件显示逻辑
mcporter call tencent-survey.update_logic --args '{"survey_id": 716128, "dsl": "if `q-1-abcd::o-100-EFGH` then show `q-1-ijkl`"}'
# 清空所有逻辑
mcporter call tencent-survey.update_logic --args '{"survey_id": 716128, "dsl": ""}'
```
## 权限要求
调用此接口需要满足以下权限条件(逐级校验):
| 校验层 | 说明 |
|--------|------|
| `WithSurveyClaims` | 问卷归属校验:问卷必须属于当前 Token 绑定的团队 |
| `WithSurveyEditorClaims` | 编辑权限校验:当前用户需具有该问卷的编辑权限 |
| `WithSurveyEditableClaims` | 可编辑状态校验:问卷必须处于可编辑状态(如草稿状态) |
## 错误码
| error.type | 错误描述 | 解决方案 |
|------------|---------|---------|
| `invalid_dsl_syntax` | DSL 语法错误 | 检查 DSL 代码语法是否正确,参考语法文档 |
| `invalid_reference` | 引用的题目/选项 ID 不存在 | 确认 ID 从 get_survey 返回数据中获取 |
| `claim_error` | 权限校验错误 | 问卷不属于当前 Token 绑定的团队,或无编辑权限 |
| `survey_not_editable` | 问卷不可编辑 | 问卷可能正在回收中,需先暂停回收 |
| `invalid_argument` | 参数校验不通过 | 检查 survey_id、dsl 是否正确 |
## 注意事项
1. **先获取再更新**:必须先调用 `get_survey` 获取问卷详情,从返回数据中获取正确的题目 ID 和选项 ID
2. **整体覆盖**:`dsl` 参数为问卷的完整逻辑代码,每次调用会覆盖所有已有逻辑。如需追加规则,需先获取当前 `survey_dsl.code`,在其基础上修改后整体传入
3. **非幂等操作**:每次调用都会覆盖原有逻辑配置
4. **ID 使用反引号**:通过 MCP 工具编写 DSL 时,题目和选项 ID 需使用反引号包裹(如 `` `q-1-abcd::o-100-EFGH` ``)
5. **多条规则换行分隔**:在 JSON 参数中,多条规则之间使用 `\n` 分隔
6. **清空逻辑**:传入空字符串(`""`)可清空所有自定义逻辑
7. **问卷状态要求**:问卷必须处于可编辑状态,正在回收中的问卷需先暂停才能编辑
8. **与基础逻辑共存**:自定义逻辑与基础逻辑可同时设置,执行顺序为先执行基础逻辑、再执行自定义逻辑
9. **付费功能**:自定义逻辑为付费高级功能。如调用时返回 `paid_function_trial_no_permission` 错误,说明当前团队未购买该功能权限,需引导用户前往[腾讯问卷购买页](https://wj.qq.com/pricing)进行付费升级(升级为付费版本)后再使用
## Annotations(工具注解)
| 注解 | 值 | 说明 |
|------|---|------|
| `readOnlyHint` | false | 非只读操作,会修改问卷逻辑配置 |
| `destructiveHint` | false | 非破坏性操作(更新而非删除) |
| `idempotentHint` | false | **非幂等**,每次调用都覆盖逻辑配置 |
| `openWorldHint` | false | 内部调用 |
@@ -0,0 +1,154 @@
# update_question 工具参考
## 概述
更新问卷中的某一道题目。需要先用 `get_survey` 获取问卷详情以确认 `question_id`,再调用此接口更新指定题目的内容。只传入目标题目的纯文本(DSL 格式),系统会自动解析并覆盖该题。
## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `survey_id` | number | **是** | 问卷 ID |
| `question_id` | string | **是** | 要更新的题目 ID,格式形如 `q-1-xxxx`(从 `get_survey` 返回的题目列表中获取) |
| `text` | string | **是** | 该题目的新文本内容(DSL 格式),只需包含这一道题 |
> **重要**:`question_id` 必须从 `get_survey` 返回的题目列表中获取,不能自行构造。
## 返回值
### 成功响应
```json
{
"survey_id": 716128,
"question_id": "q-1-abcd1234",
"result": "success"
}
```
### 失败响应
```json
{
"survey_id": 716128,
"question_id": "q-1-abcd1234",
"result": "failed",
"error": "invalid_text_format: ..."
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `survey_id` | number | 问卷 ID |
| `question_id` | string | 更新的题目 ID |
| `result` | string | `"success"` 表示成功,`"failed"` 表示失败 |
| `error` | string | 仅失败时返回,错误描述信息 |
## text 语法说明
`text` 参数使用与 `create_survey` 相同的 DSL 语法,但**只需写一道题**(不需要写问卷标题)。
### 语法格式
```
题目标题[题型](描述)
选项A
选项B
```
> **关键规则**:`[题型]` 标签**必须紧跟在标题末尾**,后面不能再有文字。`(描述)` 部分为可选。
### 各题型示例
| 题型 | text 示例 |
|------|----------|
| 单选题 | `"您的性别[单选题]\n男\n女"` |
| 多选题 | `"您感兴趣的领域[多选题]\n技术\n设计\n产品\n运营"` |
| 下拉题 | `"请选择部门[下拉题]\n研发部\n市场部\n财务部"` |
| 排序题 | `"请排列优先级[排序题]\n功能\n性能\n安全\n体验"` |
| 单行文本题 | `"您的姓名[单行文本题]"` |
| 多行文本题 | `"您的建议[多行文本题]"` |
| 量表题 | `"请打分[量表题](5分非常满意)\n1~5"` |
| 多项填空题 | `"联系方式[多项填空题]\n手机号:____\n邮箱:____"` |
| 矩阵单选题 | `"满意度评价[矩阵单选题]\n非常满意 满意 一般 不满意\n服务态度\n响应速度"` |
| 段落说明 | `"以下为附加问题[段落说明]"` |
> 完整语法参考:`references/create_survey.md` 中的「text 文本语法详解」章节。
## 调用示例
### 典型工作流
```
# 1. 先获取问卷详情,确认题目 ID
get_survey(survey_id=716128)
# 返回中找到目标题目,例如 id="q-1-abcd1234",类型为单选题
# 2. 更新该题目
update_question(survey_id=716128, question_id="q-1-abcd1234", text="您的性别[单选题]\n男\n女\n其他")
```
### 更新量表题
```
update_question(survey_id=716128, question_id="q-1-efgh5678", text="请对服务打分[量表题](1分最低,10分最高)\n1~10")
```
### 更新多选题
```
update_question(survey_id=716128, question_id="q-1-ijkl9012", text="您常用的编程语言[多选题]\nGo\nPython\nJava\nTypeScript\nRust")
```
### mcporter 调用
```bash
# 更新单选题
mcporter call tencent-survey.update_question --args '{"survey_id": 716128, "question_id": "q-1-abcd1234", "text": "您的性别[单选题]\n男\n女\n其他"}'
# 更新多行文本题
mcporter call tencent-survey.update_question --args '{"survey_id": 716128, "question_id": "q-1-efgh5678", "text": "请填写您的建议[多行文本题]"}'
```
## 权限要求
调用此接口需要满足以下权限条件(逐级校验):
| 校验层 | 说明 |
|--------|------|
| `WithSurveyClaims` | 问卷归属校验:问卷必须属于当前 Token 绑定的团队 |
| `WithSurveyEditorClaims` | 编辑权限校验:当前用户需具有该问卷的编辑权限 |
| `WithSurveyEditableClaims` | 可编辑状态校验:问卷必须处于可编辑状态(如草稿状态) |
## 错误码
| error.type | 错误描述 | 解决方案 |
|------------|---------|---------|
| `invalid_text_format` | 文本内容格式错误 | 检查 text 语法是否正确,`[题型]` 需紧跟标题末尾 |
| `no_question_parsed` | 未能解析出任何题目 | 确认 text 包含了完整的题目内容 |
| `unknown_question_type` | 无法识别的题型 | 检查 `[题型]` 标签是否使用了支持的题型名称 |
| `save_question_failed` | 保存题目失败 | 服务端异常,请稍后重试 |
| `claim_error` | 权限校验错误 | 问卷不属于当前 Token 绑定的团队,或无编辑权限 |
| `survey_not_editable` | 问卷不可编辑 | 问卷可能正在回收中,需先暂停回收 |
| `invalid_argument` | 参数校验不通过 | 检查 survey_id、question_id、text 是否正确 |
## 注意事项
1. **先获取再更新**:必须先调用 `get_survey` 获取问卷详情,从返回的题目列表中获取正确的 `question_id`
2. **单题更新**:`text` 只需包含一道题目的内容,不要写问卷标题或多道题
3. **非幂等操作**:每次调用都会覆盖原题目内容
4. **题型标签位置**:`[题型]` 必须紧跟标题末尾,不能放在其他位置
5. **问卷状态要求**:问卷必须处于可编辑状态,正在回收中的问卷需先暂停才能编辑
6. **换行用 `\n`**:在 JSON 参数中,所有换行必须使用 `\n` 代替
7. **选项无需字母前缀**:选项直接写内容即可(如 `满意`),不需要写 `A. 满意`
## Annotations(工具注解)
| 注解 | 值 | 说明 |
|------|---|------|
| `readOnlyHint` | false | 非只读操作,会修改问卷内容 |
| `destructiveHint` | false | 非破坏性操作(更新而非删除) |
| `idempotentHint` | false | **非幂等**,每次调用都覆盖题目内容 |
| `openWorldHint` | false | 内部调用 |
+637
View File
@@ -0,0 +1,637 @@
#!/bin/bash
#
# Setup script for 腾讯问卷 MCP Skill (内部 OpenClaw 版本) 一体化配置与授权脚本
#
# 功能:
# 1. 检查 mcporter 是否已配置 tencent-survey(含 Authorization 可用)
# 2. 未配置或 Token 失效时,展示授权链接
# 3. 前台轮询等待用户完成授权,Token 获取后自动写入 mcporter 并继续
# 4. 对超时、过期、错误等场景给出友好提示
#
# 用法(供 AI Agent 调用):
# 第一步:检查状态(立即返回,不阻塞)
# bash ./setup.sh wj_check_and_start_auth
# 输出:
# READY → 服务已就绪,直接执行用户任务,无需第二步
# NONCE:<nonce> → nonce 值(在 AUTH_REQUIRED 之前输出)
# AUTH_REQUIRED:<url> → 立即向用户展示授权链接,然后执行第二步
# ERROR:* → 告知用户对应错误
#
# 第二步:等待授权完成(仅 AUTH_REQUIRED 时执行,阻塞最多约 300s)
# bash ./setup.sh wj_wait_auth
# 输出:
# TOKEN_READY:ok → 授权成功,Token 已写入配置,继续执行用户任务
# AUTH_TIMEOUT → 告知用户:授权超时,请重新发起请求
# ERROR:* → 告知用户对应错误
#
# 直接执行(排查问题):
# bash ./setup.sh
#
# ── 全局配置 ──────────────────────────────────────────────────────────────────
_WJ_API_BASE="${WJ_API_BASE_URL:-https://wj.qq.com}"
_WJ_AUTH_PAGE="${WJ_AUTH_PAGE_URL:-https://wj.qq.com/oauth/authorize}"
# 从 WJ_AUTH_PAGE_URL 中提取查询参数(如 _tde_id=2952),附加到 API 请求
_WJ_EXTRA_QUERY=""
if [[ "$_WJ_AUTH_PAGE" == *"?"* ]]; then
_WJ_EXTRA_QUERY="${_WJ_AUTH_PAGE#*\?}"
fi
# 构造 API URL,如有额外参数则附加
_WJ_MCP_URL="${_WJ_API_BASE}/api/v2/mcp"
_WJ_TOKEN_POLL_URL="${_WJ_API_BASE}/api/v2/account/tokens/device-auth/poll"
if [[ -n "$_WJ_EXTRA_QUERY" ]]; then
_WJ_MCP_URL="${_WJ_MCP_URL}?${_WJ_EXTRA_QUERY}"
_WJ_TOKEN_POLL_URL="${_WJ_TOKEN_POLL_URL}?${_WJ_EXTRA_QUERY}"
fi
_WJ_SERVICE_NAME="tencent-survey"
# 轮询参数:每 2s 一次,最多 150 次(约 300s)
_WJ_POLL_INTERVAL=2
_WJ_POLL_MAX=150
# 临时文件:使用基于 UID 的私有目录,避免 symlink 攻击和多用户冲突
_WJ_TMP_DIR="${TMPDIR:-/tmp}/.wj_auth_$(id -u)"
mkdir -p "$_WJ_TMP_DIR" 2>/dev/null
chmod 700 "$_WJ_TMP_DIR"
_WJ_CODE_FILE="${_WJ_TMP_DIR}/code"
_WJ_TOKEN_FILE="${_WJ_TMP_DIR}/token"
_WJ_URL_FILE="${_WJ_TMP_DIR}/url"
_WJ_NONCE_FILE="${_WJ_TMP_DIR}/nonce"
# ── 安全写入函数(拒绝写入符号链接)────────────────────────────────────────
_wj_safe_write() {
local file="$1" content="$2"
if [[ -L "$file" ]]; then
echo "ERROR:security - 检测到符号链接,拒绝写入: $file" >&2
return 1
fi
echo "$content" > "$file"
chmod 600 "$file"
}
# ── 清理函数 ──────────────────────────────────────────────────────────────────
_wj_cleanup() {
[[ -d "$_WJ_TMP_DIR" ]] && rm -rf "$_WJ_TMP_DIR"
# 重建私有目录,确保后续写入可用
mkdir -p "$_WJ_TMP_DIR" 2>/dev/null
chmod 700 "$_WJ_TMP_DIR"
}
# ── 检查 mcporter 是否已安装 ──────────────────────────────────────────────────
_wj_check_mcporter() {
if ! command -v mcporter &> /dev/null; then
echo "⚠️ 未找到 mcporter,正在安装..."
if command -v npm &>/dev/null; then
npm install -g mcporter 2>&1 | tail -3
echo "✅ mcporter 安装完成"
else
echo "ERROR:no_npm"
return 1
fi
fi
return 0
}
# ── 检查必要系统依赖 ──────────────────────────────────────────────────────────
_wj_check_dependencies() {
local missing=()
command -v curl &>/dev/null || missing+=("curl")
command -v jq &>/dev/null || missing+=("jq")
if [[ ${#missing[@]} -gt 0 ]]; then
echo "ERROR:missing_dependencies - 缺少必要依赖: ${missing[*]}"
return 1
fi
return 0
}
# ── JSON 字段提取(使用 jq)──────────────────────────────────────────────────
# 提取 JSON 顶层字段
# 用法:_wj_json_get '{"code":"Ok","data":{...}}' "code" → Ok
_wj_json_get() {
local json="$1" key="$2"
echo "$json" | jq -r --arg k "$key" '.[$k] // empty'
}
# 提取 JSON "data" 子对象中的字段
# 用法:_wj_json_get_data '{"code":"Ok","data":{"token":"xxx"}}' "token" → xxx
_wj_json_get_data() {
local json="$1" key="$2"
echo "$json" | jq -r --arg k "$key" '.data[$k] // empty'
}
# 从 mcporter config get 读取当前 Authorization Token
# 输出:token 字符串(空则表示服务未注册或 Token 未配置)
_wj_get_token() {
local output
output=$(mcporter config get "$_WJ_SERVICE_NAME" 2>/dev/null) || return 1
# 从输出中提取 Authorization 头的值
local token
token=$(echo "$output" | grep -i '^\s*Authorization:' | sed 's/.*Authorization:[[:space:]]*//' | tr -d '[:space:]')
echo "$token"
}
# ── 将 Token 写入 mcporter 配置 ───────────────────────────────────────────────
# 用法:_wj_save_token <token>
_wj_save_token() {
# 添加 MCP 配置
echo "🔧 配置 mcporter..."
local token="$1"
[[ -z "$token" ]] && return 1
# 使用传入的 token 写入 mcporter 配置(tencent-survey)
mcporter config add "$_WJ_SERVICE_NAME" "$_WJ_MCP_URL" \
--header "Authorization=Bearer $token" \
--transport http \
--scope home
echo ""
echo "✅ 配置完成!"
echo ""
echo "🧪 验证配置..."
if mcporter list 2>&1 | grep -q "$_WJ_SERVICE_NAME"; then
echo "✅ tencent-survey 配置验证成功!"
echo ""
mcporter list | grep -A 1 "$_WJ_SERVICE_NAME" || true
else
echo "⚠️ tencent-survey 配置验证失败,请检查网络或 Token 是否有效"
fi
echo ""
echo "─────────────────────────────────────"
echo "🎉 设置完成!"
echo ""
echo "📖 使用方法:"
echo " mcporter call ${_WJ_SERVICE_NAME}.get_survey --args '{\"survey_id\": 12345}'"
echo " mcporter call ${_WJ_SERVICE_NAME}.create_survey --args '{\"text\": \"问卷标题\\n\\n1. 题目\"}'"
echo ""
echo "🏠 腾讯问卷主页:${_WJ_API_BASE}"
echo ""
echo "📖 更多信息请查看 SKILL.md"
echo ""
return 0
}
# ── 检查 tencent-survey 服务状态 ────────────────────────────────────────────────
# 返回值:
# 0 = 服务正常可用(有 Token)
# 1 = 服务未注册(mcporter config get 失败)
# 2 = Token 为空或未配置
_wj_check_service() {
if ! mcporter list 2>/dev/null | grep -q "$_WJ_SERVICE_NAME"; then
return 1
fi
local token
token=$(_wj_get_token)
local rc=$?
# mcporter config get 返回非 0 表示服务未注册
if [[ $rc -ne 0 ]]; then
return 1
fi
# Token 为空表示服务已注册但未配置 Authorization
if [[ -z "$token" ]]; then
return 2
fi
return 0
}
# ── 生成授权链接(调用后端 /init API 获取 code)─────────────────────────────
# 输出:auth_url 字符串
# code 由后端 crypto/rand 生成,保证密码学安全性
_wj_generate_auth_url() {
# 构造 init API URL
local init_url="${_WJ_API_BASE}/api/v2/account/tokens/device-auth/init"
if [[ -n "$_WJ_EXTRA_QUERY" ]]; then
init_url="${init_url}?${_WJ_EXTRA_QUERY}"
fi
# 调用后端 init 端点
local response
response=$(curl -s -f -X POST "$init_url" 2>/dev/null) || {
echo "ERROR:init_request_failed - 无法连接到服务器 ${init_url}"
return 1
}
# 解析响应中的 code
local resp_code
resp_code=$(_wj_json_get "$response" "code")
local resp_upper
resp_upper=$(echo "$resp_code" | tr '[:lower:]' '[:upper:]')
if [[ "$resp_upper" != "OK" ]]; then
echo "ERROR:init_failed - 服务端返回错误: $resp_code"
return 1
fi
local code
code=$(_wj_json_get_data "$response" "code")
local nonce
nonce=$(_wj_json_get_data "$response" "nonce")
# 防御性校验
if [[ ! "$code" =~ ^[0-9a-f]{16,64}$ ]]; then
echo "ERROR:invalid_code - 服务端返回的 code 格式非法: $code"
return 1
fi
if [[ -z "$nonce" ]]; then
echo "ERROR:invalid_nonce - 服务端未返回 nonce"
return 1
fi
_wj_safe_write "$_WJ_CODE_FILE" "$code" || return 1
_wj_safe_write "$_WJ_NONCE_FILE" "$nonce" || return 1
# 如果 AUTH_PAGE 已包含 ?,则用 & 拼接;否则用 ?
local sep="?"
[[ "$_WJ_AUTH_PAGE" == *"?"* ]] && sep="&"
echo "${_WJ_AUTH_PAGE}${sep}code=${code}&nonce=${nonce}"
}
# ── 前台轮询 Token(通用函数)─────────────────────────────────────────────────
# 用法:_wj_poll_token [log_prefix]
# log_prefix: 日志前缀(默认 "⏳"),用于区分调用场景的输出
#
# 前置条件:$_WJ_CODE_FILE 已存在且包含有效的 code
#
# 输出(最后一行为结构化结果):
# TOKEN_READY:<token> 授权成功
# AUTH_TIMEOUT 超时
# ERROR:empty_token Token 为空异常
# ERROR:no_code 未找到授权码
# ERROR:empty_code 授权码为空
_wj_poll_token() {
local prefix="${1:-⏳}"
# 检查 code 文件
if [[ ! -f "$_WJ_CODE_FILE" ]]; then
echo "ERROR:no_code - 未找到授权码,请先执行 wj_check_and_start_auth"
return 1
fi
local code
code=$(cat "$_WJ_CODE_FILE")
if [[ -z "$code" ]]; then
echo "ERROR:empty_code - 授权码为空"
return 1
fi
local poll_sep="?"
[[ "$_WJ_TOKEN_POLL_URL" == *"?"* ]] && poll_sep="&"
local url="${_WJ_TOKEN_POLL_URL}${poll_sep}code=${code}"
local i
for ((i=1; i<=_WJ_POLL_MAX; i++)); do
sleep "$_WJ_POLL_INTERVAL"
local response
response=$(curl -s -f -L "$url" 2>/dev/null) || {
echo " ${prefix} [$i/$_WJ_POLL_MAX] curl 请求失败"
continue
}
local resp_code
resp_code=$(_wj_json_get "$response" "code")
# 兼容 "Ok" 和 "OK"
local resp_upper
resp_upper=$(echo "$resp_code" | tr '[:lower:]' '[:upper:]')
if [[ "$resp_upper" != "OK" && -n "$resp_upper" ]]; then
echo " ${prefix} [$i/$_WJ_POLL_MAX] resp_code=$resp_code (非Ok)"
continue
fi
local status
status=$(_wj_json_get_data "$response" "status")
local token
token=$(_wj_json_get_data "$response" "token")
case "$status" in
"completed")
echo " ${prefix} [$i/$_WJ_POLL_MAX] status=completed ✅"
if [[ -n "$token" ]]; then
echo "TOKEN_READY:$token"
return 0
fi
echo " ${prefix} [$i/$_WJ_POLL_MAX] ⚠️ status=completed 但 token 为空"
echo "ERROR:empty_token"
return 1
;;
"pending")
echo " ${prefix} [$i/$_WJ_POLL_MAX] status=pending"
continue
;;
*)
echo " ${prefix} [$i/$_WJ_POLL_MAX] status=$status (未知状态)"
continue
;;
esac
done
echo "AUTH_TIMEOUT"
return 2
}
# ── 执行授权流程(第一阶段):生成链接(立即返回,不阻塞)─────────────────────
# 输出:
# AUTH_REQUIRED:<url> 立即输出到 stdout,同时写入 $_WJ_URL_FILE
_wj_do_auth_start() {
_wj_cleanup
# 生成授权链接(同时写入 code 和 nonce 文件)
local auth_url
auth_url=$(_wj_generate_auth_url)
local rc=$?
# 检查生成是否成功
if [[ $rc -ne 0 ]]; then
echo "$auth_url" # 透传 ERROR:xxx 消息
return 1
fi
# 将 URL 写入文件,供后续阶段读取
_wj_safe_write "$_WJ_URL_FILE" "$auth_url" || return 1
# 读取 nonce 并显示
local nonce=""
[[ -f "$_WJ_NONCE_FILE" ]] && nonce=$(cat "$_WJ_NONCE_FILE")
if [[ -n "$nonce" ]]; then
echo "NONCE:$nonce"
fi
# ★ 立即输出授权链接(调用方可立即展示给用户)
echo "AUTH_REQUIRED:$auth_url"
return 0
}
# ── 主入口函数 A:检查状态 / 生成授权链接(立即返回,不阻塞)────────────────
#
# AI Agent 第一步调用此函数,命令执行完毕后立即拿到输出:
# READY 服务已就绪,直接执行用户任务,无需第二步
# NONCE:<nonce> nonce 值(在 AUTH_REQUIRED 之前输出)
# AUTH_REQUIRED:<url> 需要授权:立即展示链接给用户,然后执行第二步
# ERROR:* 错误信息
#
wj_check_and_start_auth() {
_wj_check_mcporter || {
echo "ERROR:mcporter_not_found - 请先安装 Node.js 和 npm 后重试"
return 1
}
_wj_check_dependencies || return 1
# ★ 如果设置了 TENCENT_SURVEY_TOKEN 环境变量,直接写入配置
if [[ -n "$TENCENT_SURVEY_TOKEN" ]]; then
if [[ ! "$TENCENT_SURVEY_TOKEN" =~ ^wjpt_ ]]; then
echo "ERROR:invalid_token_prefix - TENCENT_SURVEY_TOKEN 必须以 wjpt_ 开头"
return 1
fi
if _wj_save_token "$TENCENT_SURVEY_TOKEN" >/dev/null 2>&1; then
echo "READY"
return 0
else
echo "ERROR:save_token_failed - Token 写入配置失败"
return 1
fi
fi
_wj_check_service
local status=$?
case $status in
0)
echo "READY"
return 0
;;
1|2)
_wj_do_auth_start || return 1
return 0
;;
esac
}
# ── 主入口函数 B:等待授权完成(阻塞,最多约 300s)────────────────────────────
#
# AI Agent 在展示授权链接后调用此函数,等待用户完成授权:
# TOKEN_READY:ok 授权成功,Token 已写入配置,直接执行用户任务
# AUTH_TIMEOUT 超时,告知用户重新发起请求
# ERROR:* 错误信息
#
wj_wait_auth() {
# 前台轮询 API 等待授权完成
local result
result=$(_wj_poll_token "⏳")
local rc=$?
# 提取最后一行作为结构化结果
local last_line
last_line=$(echo "$result" | tail -1)
# 输出过程日志(去掉最后一行结果)
echo "$result" | sed '$d'
case "$last_line" in
TOKEN_READY:*)
local token="${last_line#TOKEN_READY:}"
if _wj_save_token "$token"; then
_wj_cleanup
echo "TOKEN_READY:ok"
return 0
else
_wj_cleanup
echo "ERROR:save_token_failed"
return 1
fi
;;
AUTH_TIMEOUT)
_wj_cleanup
echo "AUTH_TIMEOUT"
return 2
;;
ERROR:empty_token*)
_wj_cleanup
echo "ERROR:empty_token - 授权异常,Token 为空"
return 1
;;
ERROR:*)
_wj_cleanup
echo "$last_line"
return 1
;;
esac
}
# ── 直接执行时的交互式安装流程 ───────────────────────────────────────────────
_wj_interactive_setup() {
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ 腾讯问卷 MCP Skill 配置向导 ║"
echo "╚══════════════════════════════════════════════╝"
echo ""
# 检查 mcporter
echo "🔍 检查 mcporter..."
if ! _wj_check_mcporter; then
echo "❌ mcporter 安装失败,请先安装 Node.js (https://nodejs.org) 后重试"
exit 1
fi
echo "✅ mcporter 已就绪"
echo ""
# 检查系统依赖
echo "🔍 检查系统依赖..."
if ! _wj_check_dependencies; then
echo "❌ 缺少必要依赖,请先安装后重试"
exit 1
fi
echo "✅ 系统依赖已就绪"
echo ""
# 检查服务状态
echo "🔍 检查 tencent-survey 服务配置..."
_wj_check_service
local status=$?
case $status in
0)
echo "✅ tencent-survey 服务已配置且运行正常!"
echo ""
echo "🎉 无需重新配置,您可以直接使用腾讯问卷功能。"
echo ""
echo "📖 使用示例:"
echo " mcporter call tencent-survey.get_survey --args '{\"survey_id\": 12345}'"
return 0
;;
1|2)
echo "⚠️ Token 未配置,需要授权..."
;;
esac
echo ""
echo "🔐 需要完成腾讯问卷授权"
echo ""
# 清理旧状态
_wj_cleanup
# 生成授权链接(同时写入 code 和 nonce 文件)
local auth_url
auth_url=$(_wj_generate_auth_url)
if [[ $? -ne 0 ]]; then
echo "❌ 生成授权链接失败:$auth_url"
exit 1
fi
# 读取 nonce
local nonce=""
[[ -f "$_WJ_NONCE_FILE" ]] && nonce=$(cat "$_WJ_NONCE_FILE")
echo "┌─────────────────────────────────────────────────────────┐"
echo "│ 请在浏览器中打开以下链接完成授权: │"
echo "│ │"
printf "│ %s\n" "$auth_url"
echo "│ │"
if [[ -n "$nonce" ]]; then
printf "│ 🔑 nonce: %s\n" "$nonce"
echo "│ │"
fi
echo "│ ⚠️ 请使用 QQ 或微信 扫码 / 登录授权 │"
echo "└─────────────────────────────────────────────────────────┘"
echo ""
echo "⏳ 正在等待您完成授权,无需任何额外操作..."
echo " (最多等待 $((_WJ_POLL_MAX * _WJ_POLL_INTERVAL)) 秒)"
echo ""
# ★ 前台轮询等待授权完成
local result
result=$(_wj_poll_token "🔄")
local rc=$?
# 提取最后一行作为结构化结果
local last_line
last_line=$(echo "$result" | tail -1)
# 输出过程日志(去掉最后一行结果)
echo "$result" | sed '$d'
case "$last_line" in
TOKEN_READY:*)
local token="${last_line#TOKEN_READY:}"
echo ""
echo "✅ 授权成功!正在保存配置..."
if _wj_save_token "$token"; then
_wj_cleanup
echo "✅ Token 已写入 mcporter 配置"
echo ""
echo "🎉 配置完成!现在可以直接使用腾讯问卷功能了。"
echo ""
echo "📖 使用示例:"
echo " mcporter call ${_WJ_SERVICE_NAME}.get_survey --args '{\"survey_id\": 12345}'"
echo ""
echo "🏠 腾讯问卷主页:${_WJ_API_BASE}"
else
# Token 写入临时文件,避免在终端明文打印
local token_file="${_WJ_TMP_DIR}/token_backup"
_wj_safe_write "$token_file" "$token"
echo "⚠️ Token 写入配置失败"
echo " Token 已保存到临时文件: $token_file"
echo " 请手动运行:"
echo " mcporter config add ${_WJ_SERVICE_NAME} ${_WJ_MCP_URL} --header \"Authorization=Bearer \$(cat $token_file)\" --transport http --scope home"
fi
;;
AUTH_TIMEOUT)
echo ""
echo "⏳ 授权超时(未在时限内完成授权)"
echo " 请重新运行:bash ./setup.sh"
exit 1
;;
ERROR:*)
echo ""
echo "❌ 授权失败:$last_line"
echo " 如问题持续,请联系腾讯问卷支持"
exit 1
;;
esac
return 0
}
# ── 脚本入口 ──────────────────────────────────────────────────────────────────
# 直接执行时:
# bash ./setup.sh wj_check_and_start_auth → 第一步:检查状态 / 生成授权链接
# bash ./setup.sh wj_wait_auth → 第二步:等待授权完成
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
if [[ -n "$1" ]]; then
# 参数分发:将第一个参数作为函数名执行
case "$1" in
wj_check_and_start_auth|wj_wait_auth)
"$1"
exit $?
;;
setup)
echo "🚀 腾讯问卷 MCP Skill 人工配置向导"
echo ""
_wj_interactive_setup
;;
*)
echo "ERROR:unknown_command - 未知命令: $1"
echo "可用命令: wj_check_and_start_auth, wj_wait_auth, setup"
exit 1
;;
esac
else
echo "用法:"
echo " bash ./setup.sh wj_check_and_start_auth # 第一步:检查状态 / 生成授权链接"
echo " bash ./setup.sh wj_wait_auth # 第二步:等待授权完成"
fi
fi
+6
View File
@@ -0,0 +1,6 @@
{
"display_name": "腾讯问卷",
"display_name_en": "Tencent Survey",
"description_zh": "腾讯问卷操作(创建、修改、逻辑设置、统计)",
"description_en": "Tencent Survey operations (create, edit, logic settings, view statistical analysis of various surveys)\""
}