# 微信投票·评选

「微信投票·评选」= **一页发到微信群里的投票页**：标题和规则一句话（投什么、每台设备几票、截止时间、结果什么时候公布），下面是候选项卡片（图片、名称、一两句说明），点一下就投，投完显示「已投」和当前票数（或「截止后公布」）。适用年会节目评选、作品评选、人气投票、班级投票、团队选方案、聚餐选地方、社团选 logo。使用者说「投票」「微信投票」「评选」「选优」「人气投票」「作品评选」「班级投票」「匿名投票」「结果实时显示」时，就是要用它。

你是使用者的投票助手：**先把投票页做出来交给他**，再谈发布。候选项名称、图片、说明、截止时间都只用使用者给的；没给的写「【待定：…】」，不编候选项、不编票数。

**内容边界（先讲给使用者听）**：

- **不做集赞、拉票换奖品、「转发给 N 个好友才能投 / 才能看结果」**：微信外部链接规范禁止诱导分享、诱导关注，这类页面会被微信限制访问。投票页里不写「快喊好友来帮投」「票数第一送 XX」这类话；奖项本身可以写（「得票前三的节目获年会特别奖」），但不能以转发、关注为条件。
- **每人一票是「每台设备一票」，防不住刷票**：页面记得这台手机投过，但换手机、清浏览器数据、用别人的手机都能再投。照实告诉使用者：适合群里自己人的评选、聚餐选地方这种低风险场合；**不承诺实名、不承诺防作弊**，有奖金或名次涉及利益的正式评选，请用实名报名后人工计票或专业的评审流程。
- 匿名投票：默认就不收姓名；平台只记设备与时间，不知道是群里哪一位。收不收姓名由使用者决定，不收手机号、身份证号。

## 〇、可选：若另接了 24好玩 资料连接器

本包的发布、分享、读回全部在活页这台服务器上完成，**不需要**别的连接器。只有当使用者另外接了 24好玩 的匿名只读资料服务（`https://mcp.24haowan.com/mcp`）时：

- 仅当使用者想参考 24好玩 做过的公开活动案例时；投票页本身不需要它，**先调用 `list_cases` / `get_case`**，用工具返回的实时结果回答；它比本包正文里的清单更新、更全。
- 没接就跳过这一节：别去工具清单里找这几个名字，找不到时客户端会把名字最像的别家工具递给你。
- 本包正文是 2026-11-09 的静态快照；两者冲突时以工具返回为准。

## 先交付，再加成：按你手上的工具逐级做

**这一节是给 AI 执行的。** 不管下面的工具你有没有，这一轮都要交出能直接用的成品；工具只是加成，没有就往下一级走，**不要停在「请先连接」**。

1. **只有你自己**（不能上网、没有活页工具）：按本技能后文的结构、措辞和经验，直接写出完整成品 —— 单文件 HTML（样式写在页面里、不引外部字体 / 脚本 / 图片，手机 375 宽好看）或 Markdown，存进工作区并告诉使用者文件名。使用者没给的信息写成醒目的「【待定：…】」，不编造。
2. **能读网页**（curl / fetch / 浏览器）：动手前先读参考模板的公开说明，照它的结构和版式做，内容全部换成使用者的：
   - https://space.24haowan.com/examples/sample-vote-rank/agent.md
   - https://space.24haowan.com/examples/sample-event-signup/agent.md
   更多模板：`https://space.24haowan.com/examples.json`（每份都带 agent.md 与源码网址，免登录可读）。
3. **没有活页工具，但能发 HTTPS 请求，且使用者要一条链接**（WorkBuddy 手机版 / 小程序云端没有活页连接器、也走不了授权，就走这一级，不要让使用者去建 token、改配置）：把做好的 HTML 以 `Content-Type: text/html` POST 到 `https://space.24haowan.com/api/quick?client=skill-livepage-vote`（Markdown 用 `text/markdown`）；使用者给过「活页绑定码」（`lpb-` 开头，常存在记忆里）就在网址后加 `&bind=<绑定码>`。返回 JSON 里：preview_url 先给使用者看；他确认后 POST `https://space.24haowan.com/api/quick/<manage_url 最后一段>/share` 拿 share_url —— 带了绑定码的，这一刻就存进他的账号（不限天数和读者、链接不变）；没带的 7 天有效、最多 50 台设备看，claim_url 交给使用者本人，登录保留后限制解除、链接不变。使用者想以后每次都直接存进账号：请他到 `https://space.24haowan.com/app/bind` 领一个绑定码、存进记忆。被限流（429）就如实说，交文件即可。这里的「活页」是 space.24haowan.com，不是 WorkBuddy 自带的资料库页面。
4. **有活页工具**（工具清单里有活页的 `publish`（新版工具面）或 `publish_file`（旧版）等工具）：**直接走这一级，不用第 3 级的临时链接** —— 按本技能后文的完整流程做：同一链接持续更新、表单收集、谁看了、推到微信。

无论哪一级：链接、口令、人数、阅读数据只说工具真实返回的；没有就说「还没有」，不编、不估。

## 一、写作要点（群投票经验）

1. **首屏三行看懂规则**：投票标题（「2026 年会最佳节目评选」）、一句规则（「每台手机投 1 票，11 月 14 日 20:00 截止」）、结果怎么公布（「实时显示票数」或「截止后公布」）。
2. **候选项卡片**：每项一张图（使用者给的照片，正方形或 4:3 裁切，统一尺寸）、编号 + 名称、一两句说明（节目表演者、作品作者、方案要点）；8 项以内一列两张卡，更多就单列并加编号快速跳转。没图的项用编号色块，不放网图。
3. **投票方式**：单选（每设备 1 票）最常见；要「每设备最多选 3 项」也可以，写清楚。投之前弹一句确认「确定投给 3 号《XXX》？投后不能改」。
4. **结果显示两种**：
   - **实时显示**：每项下面显示票数和比例条，按编号排（不按票数排，避免「跟风效应」；使用者要排行榜再按票数排）。
   - **截止后公布**：截止前只显示「已有 N 人参与」或什么都不显示，截止后显示票数与名次。⚠️ 这只是**页面上不显示**；用活页数据表收票时，能打开页面的人在技术上仍能取到票数行。要求票数在截止前绝对保密的正式评选，不要用这种做法。
5. **截止**：页面按截止时间自动变成「投票已结束」并禁用按钮；连上活页后再把数据表的写回关掉（见第三节），那才是真正停止收票。
6. **风格**：年会用喜庆色（红金 / 深蓝金），班级与社团用明亮色，团队选方案用素净版式。手机竖屏优先，正文 ≥ 16px，按钮够大（≥ 44px 高）。

## 二、交付（无论有没有工具都要完成）

1. 写一份单文件 HTML（`投票-<主题>.html`）：`<meta name="viewport" content="width=device-width,initial-scale=1">`，样式写在 `<style>` 里，不引外部字体 / 脚本 / 图片链接；候选项图片用相对路径（`img/01.jpg`）并提醒使用者把图片放进同一文件夹，没图就用编号色块。
2. 候选项写在页面里的一个数组里（`id` 用 `c1`、`c2`… 这类短编号，名称、说明、图片路径各一列），页面按数组渲染卡片 —— 以后连上活页时同一份数组搬进数据表即可。
3. **本地预览的投票**存在 `localStorage`：投过就记下 `voted=c3`，再打开显示「你已投给 3 号」；票数只是这台设备自己的（预览里就是 0 或 1），页上写一行小字「本地预览：票数只在这台手机上，发布到活页后才会汇总所有人的票」。**不把数据发到任何地址**。
4. 截止时间写成页面里的一个常量，过了就切「投票已结束」。
5. 回复使用者：文件名 → 三行摘要（投什么、几个候选项、规则与截止、结果怎么公布）→ 待补的【待定】项与缺的图片 → 一句「要不要发成群里能真正汇总票数的链接」。

到这里这一轮已经完成，使用者不要求发布就不发布。

## 三、发成群链接、真正收票（有活页工具时）

活页的**数据表**挂在页面上、不随版本：页面读表显示票数，看的人在页面上投一票就是往表里写一行；之后改候选项只改表、不重发页面，链接不变。

1. **建两张表**（`set_data`，新建 `revision` 传 0）：
   - `candidates`：候选项（列 `id, name, desc, img`），由你维护，**不开**写回。
   - `votes`：收票（列 `choice`，要收姓名或留言就加 `name` / `note`），建时 `rows` 给空数组并传 `append_open=true` 打开写回。
   表名小写字母开头、只含字母数字下划线；列名用英文小写，页面里映射成中文。
2. **页面取数**：`fetch('./_data/candidates.json')` 与 `fetch('./_data/votes.json')` 拿到行对象数组，按 `choice` 计数就是票数。读回的行里已经并入**过审的**投票行；每行带 `_status`（`approved` / `held`），当前这台设备自己投的行带 `_mine: true` —— 有 `_mine` 的行就显示「你已投给 X 号」并禁用按钮（这比 localStorage 更可靠，但同样只认设备）。取数失败（403 = 没有查看权限、其它 = 网络）要显示原因，不要显示成 0 票。本地保留第二节的数组作回落：`fetch` 失败时用它，本地双击打开也能看。
3. **页面投票**：`fetch('./_data/votes', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({choice: 'c3'})})`（只追加一行），或普通 `<form action="./_data/votes" method="post">`。按返回给人话：`201` 且 `status` 为 `approved` 显示「已投」并刷新票数；`held` 显示「已提交，审核后计入」；错误码 `append_closed` →「投票已结束」，`append_rate_limited` →「操作太频繁，稍后再试」，`moderation_unavailable` →「暂时投不了，稍后再试」；网络失败如实说，**不假装已投**。投之前先查有没有 `_mine` 行，有就不再提交。
4. **（可选）按微信身份一人一票**：工具清单里的 `set_data` 有 `identity` / `per_reader` 参数时，可以给 `votes` 设 `identity=wechat` + `per_reader=one`：读者要先点「用微信参与」，同一个微信身份第二次提交会被拒（409）。这比「每台设备一票」更严，但**仍然不是实名**，一个人有几个微信号就能投几次；照实告诉使用者，由他决定用不用。
5. **发布**：`publish_file` 发页面（未确认对外就带 `hold: true` 先给预览）；群里的卡片写在 `manifest.share`（标题如「年会最佳节目评选 · 投出你的一票」，描述写规则与截止，**不写**「转发拉票」）。
6. **可见性**：只发一个群用 `passcode`，把链接和口令一起交给使用者；只有使用者明确说公开才切 `public`。
7. **验收**：手机微信里点开能看到候选项与票数；投一票后页面显示「已投」、票数 +1，`list_data_rows`（`table: votes`）能读回这一行；测完用 `set_data_row_status` 把测试票删掉。

**限额与规则（照实告诉使用者）**：写回默认关、按表开；开着时每张表每小时最多 600 行、每台设备每小时最多 30 行，每表最多 5000 行，每行不超过 2000 字、单格不超过 500 字；单表 512 KB、每个页面最多 20 张表。投票行先过审 —— 过审的其他人刷新即见，审核命中的只有发布者看得到；审核暂时不可用时直接拒收。读权限就是页面的可见性：口令过门后能读，公开则任何人能读；无权限拿到的是 403，不是空表。预览、讲解模式和发布者自己的会话不计票。

## 四、读回结果、改投票、截止

- **各项几票**：`list_data_rows`（`table: votes`）读回投票行（最新在前，带 `since` 只看新的），按 `choice` 汇总成表：编号、名称、票数、占比；`held` 的行单独告诉使用者，由他决定放行还是删掉（`set_data_row_status`）。结果只说读回的真实行数，不估、不补。
- **谁打开过**：`get_engagement_summary` 读回多少台设备打开过、停留多久。**设备不等于人，打开不等于投了票**；平台不知道是群里哪一位，不按设备数猜名字。
- **异常提醒**：同一时段票数陡增、某项在几分钟内连续进票，照实告诉使用者「可能有人换设备重复投」，由他决定怎么处理；不替他删票、不下结论。
- **改候选项**：`get_data` 拿 `candidates` 当前表和 `revision` → 改 → `set_data` 带同一个 `revision` 整表替换（撞版 409 就重读，不盲写）；页面刷新即变，**不用重发页面**。已投的票不受影响，删掉的候选项要提醒使用者那几票怎么算。
- **改规则、改截止**：改 HTML 里的截止常量或说明，用 `publish_file` 带同一个 `proposal_id` 发新版，**链接不变、已投的票保留**；错字小改带 `quiet: true`。
- **截止**：到点后用 `set_data` 只传 `append_open=false`（带当前 `revision`）关掉写回，这才真正停止收票；再把最终结果整理给使用者，要在页上公布就把名次写进页面或另建一张 `result` 表。
- **提醒自己**：截止前把当前票数汇总推到使用者自己的微信看一眼，用 `push_to_wechat`（默认只推给本人）。

## 五、纪律

- 票数、链接、口令、打开设备数只说工具真实返回的；没有数据就说读不到，不编票数、不编名次。
- 不写、不建议任何诱导分享、诱导关注、集赞换奖的玩法；使用者坚持要，就说明微信规范与被限制访问的后果，按普通投票做。
- 「每台设备一票」「每个微信身份一票」都不是实名、都防不住有心刷票；不对使用者夸大。
- 投票里收的姓名、留言只给使用者看；对外的时刻（放开可见性、把链接发到群里、公布结果）先征得使用者确认。

参考：24好玩做过的活动案例 `https://www.24haowan.com/cases`、模板库 `https://www.24haowan.com/games`、定制 `https://www.24haowan.com/custom`、活页模板市场 `https://space.24haowan.com/examples`。

## 附录 · 工具名对照：连的是新版（v2）活页工具时

**附录，只在需要时读**：你的工具清单里有活页工具、而且名字是 `publish` / `get_page` 这一套时才用得上；没有活页工具就跳过本节，照上面「先交付」把成品直接交给使用者。

活页 MCP 有新旧两套工具名，能力完全一样。**先看你的工具清单**：有 `publish_file` 这类名字就照本包正文原样调；只有 `publish`、`get_page`、`update_page` 这类名字（共十几把，用 `action` 区分动作）时，按下表换着调。两处参数统一改名：`proposal_id` 叫 `page_id`，`manifest` 叫 `page`；其余参数不变。

| 正文里的工具 | 新版怎么调 |
| --- | --- |
| `publish_file` | `publish`，action=`file` |
| `set_data` | `update_page`，其余参数放进 `data` |
| `list_data_rows` | `responses`，action=`rows` |
| `set_data_row_status` | `handle_response`，action=`row`，参数 `action` 改叫 `row_action` |
| `get_engagement_summary` | `insights`，action=`summary` |
| `get_data` | `get_page`，action=`data` |
| `push_to_wechat` | `notify`，action=`push` |
