Skill: yzdame/headteacher (version 2.1.0)

--- name: headteacher-workbench description: "Bootstrap and operate an AI-native headteacher workspace. Guide users through backend selection, environment-aware Feishu Base access routing, schema installation, data operations, and artifact generation. Use for class management setup, OpenClaw Feishu plugin onboarding, Feishu CLI onboarding, student records, grades, conduct logs, parent communication, schedules, and Word/Excel/PPT outputs. | 搭建并运行班主任 AI Native 工作台：引导用户选择后端、按运行环境路由飞书多维表格接入方式、初始化班级数据库、持续处理学生信息、成绩、德育、家校沟通与文件生成。" argument-hint: "[task-or-class-name]" version: "2.1.0" user-invocable: true allowed-tools: Read, Write, Edit, Bash --- > **Language / 语言**: Detect the user's language from their first message and keep using it. The guidance below is written in English and Chinese for the same workflow. # Headteacher Workbench ## When this skill should trigger Trigger this skill when the user wants to do any of the following: - Set up a headteacher workspace for the first time - Install or verify `lark-cli` - Install or verify the official OpenClaw Feishu plugin - Connect Feishu Base, Notion, or Obsidian - Bootstrap a class-management schema - Inspect an existing Feishu Base and decide whether it is reusable - Import student rosters or update grades, conduct records, parent communication, seat plans, duty schedules, or committee assignments - Generate `.docx`, `.xlsx`, or `.pptx` artifacts from structured data - Query an existing class workspace and update or summarize it Do **not** treat this skill as: - A persona simulator - A teacher role-play prompt - A colleague distillation workflow ## Default operating mode This skill is **setup-first**. On first use, do not jump straight into task execution. 1. Check whether a local workspace manifest exists at: - `./.headteacher-skill/workspace_manifest.json` 2. If the manifest does not exist or is incomplete, enter setup mode. 3. Default-recommend `feishu_base` as the backend. 4. Only after setup is complete should normal runtime task routing begin. After setup, treat the skill as two cooperating subsystems: 1. **Data record and retrieval** - write mode: - one-time import - dynamic append / update - read mode: - longitudinal read: follow one student across a timeline - horizontal read: inspect a cohort or the whole class at one time slice 2. **Artifact generation** - generate Office outputs from structured data - typical cases: - seat plan / duty schedule arranged by attributes - parent meeting PPT generated from scores plus daily records ## Setup workflow ### Step 1: Environment doctor Run: ```bash python3 tools/setup_doctor.py --format markdown ``` Use the result to decide: - which agent runtime is currently hosting the skill - whether Feishu should be accessed through the OpenClaw official plugin or through `lark-cli` - whether `lark-cli` is installed - whether Feishu is configured - whether office artifact generation dependencies are present - whether Notion MCP is available in the current agent environment - whether Obsidian CLI is available - whether the user still needs the official Obsidian skill / connector setup ### Step 2: Backend selection Read [prompts/backend-selector.md](prompts/backend-selector.md). Default recommendation order: 1. `feishu_base` 2. `notion` 3. `obsidian` 4. `local_only` Use Feishu as the default unless the user explicitly prefers otherwise. ### Step 3: Workspace bootstrap Read: - [prompts/setup.md](prompts/setup.md) - [prompts/schema-installer.md](prompts/schema-installer.md) - [references/schema-manifest.md](references/schema-manifest.md) - [references/backend-contract.md](references/backend-contract.md) If backend is `feishu_base`, also read: - [prompts/feishu-bootstrap.md](prompts/feishu-bootstrap.md) - [references/feishu-model.md](references/feishu-model.md) Then choose the Feishu access path: 1. If `tools/setup_doctor.py` reports `agent_runtime.runtime = openclaw`: - check whether the official OpenClaw plugin `openclaw-lark` is installed - if missing, guide installation first - then use the plugin's Feishu Base tools / API capabilities to create the base, tables, fields, views, and records - do **not** require `lark-cli` in this branch 2. If runtime is `codex`, `claude_code`, or another local agent: - use the existing local toolchain - run: ```bash python3 tools/feishu_bootstrap.py bootstrap --workspace-name "<class-name>" ``` If the user provides an existing Base, inspect it first: ```bash python3 tools/migration_inspector.py feishu --base-token "<base-token>" --format markdown ``` ### Step 4: Runtime routing Once setup is complete, read [prompts/runtime-router.md](prompts/runtime-router.md) and route the user's request into one of these intents: - `setup workspace` - `connect backend` - `bootstrap schema` - `inspect existing workspace` - `migrate from subject-teacher base` - `append records` - `query student/class data` - `generate artifact` - `sync artifact` ## Runtime rules ### Capability split Treat all runtime work as belonging to one of two families: 1. `data operations` - import existing roster / score / conduct material - append or update new records - read one student longitudinally - read multiple students horizontally 2. `artifact generation` - produce `.docx`, `.xlsx`, `.pptx` outputs from structured data - never treat Office files as the source of truth ### Data model All runtime work should use the unified semantic model described in [references/schema-manifest.md](references/schema-manifest.md), not backend-specific ad hoc field guesses. Core entities: - student master - exam batch - score detail - growth event - parent communication - seat assignment - duty assignment - committee assignment - artifact registry The model is intentionally object-event based: - `student master` is the stable object layer - scores, conduct, duties, observations, and parent communication are event or assignment layers - artifacts are downstream products generated from those layers ### Backend rules #### Feishu Base The only fully supported backend in v1. Always route access by runtime first: - `openclaw` -> official OpenClaw Lark/Feishu plugin (`openclaw-lark`) + Feishu Base API tools - `codex`, `claude_code`, or local agent -> `lark-cli` + local Python tools in this repository Use local tools when the runtime is **not** OpenClaw: - `python3 tools/setup_doctor.py` - `python3 tools/feishu_bootstrap.py` - `python3 tools/migration_inspector.py` - `python3 tools/artifact_registry.py` #### Notion Supported as a planning target only in v1. Read: - [references/notion-model.md](references/notion-model.md) Treat Notion as an external dependency: - verify that Notion MCP is already connected - if it is not connected, guide the user to install or connect Notion MCP first - do not attempt to bundle Notion capability into this repository You may produce the mapping plan and minimal bootstrap instructions, but do not claim full runtime parity with Feishu in v1. #### Obsidian Supported as a local-first planning target only in v1. Read: - [references/obsidian-model.md](references/obsidian-model.md) Treat Obsidian as an external dependency: - verify whether `obsidian` CLI is installed locally - if missing, guide the user to install Obsidian CLI - recommend that the user also installs the official Obsidian-related skill exposed by the agent environment - do not attempt to bundle the Obsidian CLI or official skill into this repository You may generate folder and note templates plus schema mapping guidance, but do not claim a full structured database experience in v1. ## Artifact generation Use [prompts/artifact-generator.md](prompts/artifact-generator.md) and [references/artifact-spec.md](references/artifact-spec.md). Supported artifact kinds in v1: - `.docx`: parent visit records, class notices, student talk records - `.xlsx`: seat plans, duty schedules, committee tables, deduction summaries - `.pptx`: parent meeting slides Before generating artifacts: 1. Confirm the workspace has already been initialized 2. Query structured data first 3. Choose a template or explain that a template is missing 4. Register the result with: ```bash python3 tools/artifact_registry.py register ... ``` ## Safety and change control - Never overwrite an existing Feishu Base by default. - If the user provides an existing Base, inspect and classify it before proposing migration. - Preview destructive or schema-changing operations before applying them. - Treat student contact details, addresses, and IDs as sensitive fields. - Do not claim Notion or Obsidian parity that v1 does not implement. - Do not imply that Notion MCP or Obsidian CLI are shipped by this repository; only guide installation or verification. ## Resource map ### Prompts - [prompts/setup.md](prompts/setup.md) - [prompts/backend-selector.md](prompts/backend-selector.md) - [prompts/feishu-bootstrap.md](prompts/feishu-bootstrap.md) - [prompts/schema-installer.md](prompts/schema-installer.md) - [prompts/runtime-router.md](prompts/runtime-router.md) - [prompts/artifact-generator.md](prompts/artifact-generator.md) - [prompts/migration-guide.md](prompts/migration-guide.md) ### References - [references/schema-manifest.md](references/schema-manifest.md) - [references/backend-contract.md](references/backend-contract.md) - [references/feishu-model.md](references/feishu-model.md) - [references/notion-model.md](references/notion-model.md) - [references/obsidian-model.md](references/obsidian-model.md) - [references/artifact-spec.md](references/artifact-spec.md) ### Tools - `tools/setup_doctor.py` - `tools/schema_planner.py` - `tools/feishu_bootstrap.py` - `tools/migration_inspector.py` - `tools/artifact_registry.py`

path: INSTALL.md
content: # 班主任.Skill 安装说明

## 安装到 Claude Code

```bash
mkdir -p .claude/skills
git clone https://github.com/YZDame/headteacher-skill .claude/skills/headteacher-workbench
```

全局安装：

```bash
git clone https://github.com/YZDame/headteacher-skill ~/.claude/skills/headteacher-workbench
```

## 安装到 OpenClaw

```bash
git clone https://github.com/YZDame/headteacher-skill ~/.openclaw/workspace/skills/headteacher-workbench
```

如果你准备在 OpenClaw 中使用飞书多维表格，额外安装 OpenClaw 官方飞书插件：

- 插件名：`openclaw-lark`
- Skill 在 OpenClaw 中应优先检测并复用这个官方插件
- 安装好插件后，应通过插件提供的飞书 Base API 能力完成多维表格搭建，而不是要求用户再走 `lark-cli`

## Python 依赖

```bash
pip3 install -r requirements.txt
```

## 飞书后端前置条件

若你打算使用飞书多维表格作为工作台后端：

1. 如果你是通过 OpenClaw 接入：
   - 优先安装官方飞书插件 `openclaw-lark`
   - 由 Skill 检测 OpenClaw 运行环境
   - 通过插件的飞书 API 能力完成 Base 初始化
2. 如果你是通过 Codex、Claude Code 或其他本地 Agent 接入：
   - 安装 `lark-cli`
   - 在首次运行 Skill 时完成 `lark-cli config init --new`
   - 让 Skill 使用 `tools/setup_doctor.py` 和 `tools/feishu_bootstrap.py` 完成检查与初始化

Skill 的默认建议顺序是：

1. 先做环境检查
2. 再选后端
3. 最后初始化工作台

## 其他后端

### Notion

第一版只提供结构映射和最小初始化说明，不承诺完整自动写入链路。

### Obsidian

第一版只提供目录骨架、模板和数据规范说明，不承诺完整结构化数据库体验。


path: README.md
content: <div align="center">

# 班主任.Skill

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue.svg)](https://python.org)
[![AgentSkills](https://img.shields.io/badge/AgentSkills-Standard-green)](https://agentskills.io)

一个面向教师和班主任的工作台 Skill。  
它的重点不是“模仿老师说话”，而是帮助教师持续管理学生数据，并按需生成日常需要的文件。

[English](README_EN.md)

[安装](#安装) · [使用](#使用) · [工作台模型](#工作台模型)

</div>

---

## 这是什么

`班主任.Skill` 的核心是两种能力：

1. 数据的记录、读写以及动态读写。  
   包括一次性导入、日常动态添加、纵向读取某个学生、横向读取部分学生或全班。
2. 按需生成特定的 Office 文件。  
   包括排座表、值日表、家长会 PPT，以及其他通知或记录文档。

第一版默认首选接入 **飞书多维表格**。  
`Notion` 和 `Obsidian` 暂时作为后续开发方向保留。

飞书接入方式会按运行环境自动分流：

- 如果 Skill 运行在 OpenClaw 中，优先检测并使用 OpenClaw 官方飞书插件 `openclaw-lark`
- 如果 Skill 运行在 Codex、Claude Code 或其他本地 Agent 中，沿用原有 `lark-cli` 方案

## 第一版范围

### 现在可用

- 飞书多维表格工作台初始化
- 标准班主任数据模型
- 已有飞书多维表格的迁移检查
- 本地产物登记

### 后续开发

- Notion：等待后续开发，计划通过 MCP 的方式连接
- Obsidian：等待后续开发，计划使用 Obsidian 的 CLI

## 工作台模型

第一版围绕这些核心对象展开：

- 学生主档
- 考试批次
- 成绩明细
- 成长事件
- 家校沟通
- 座位安排
- 值日安排
- 班委任命
- 产物索引

飞书多维表格是第一后端，但 Skill 本身真正关心的是班级数据和文件生成能力。

## 安装

不需要手动记复杂命令。  
如果你在使用 Agent、OpenClaw 或其他类似工具，直接发送下面这句提示词即可：

```bash
请帮我安装这个技能：https://github.com/YZDame/headteacher-skill
```

安装完成后，再告诉你的 Agent：

```bash
请帮我启用班主任.Skill，并先带我完成飞书多维表格的初始化。
```

如果你是在 OpenClaw 中使用本 Skill，飞书多维表格初始化应优先通过 OpenClaw 官方飞书插件完成，而不是默认要求你安装 `lark-cli`。

如果你后续要使用“按需生成文件”这项能力，还需要本地具备 Office 相关的 Skill 套装，用来处理：

- `.docx`
- `.xlsx`
- `.pptx`

具体参考: Anthropic Skills 仓库中的 Office 相关 Skill  
  [https://github.com/anthropics/skills/tree/main/skills](https://github.com/anthropics/skills/tree/main/skills)

## 使用

触发 Skill 后，默认按以下顺序进入：

1. 环境检查
2. 后端选型
3. 工作台初始化
4. 正式运行时任务

默认首选接入：

- 飞书多维表格

其中飞书多维表格的接入会按环境自动切换：

- OpenClaw -> 官方飞书插件 `openclaw-lark` + 飞书 API
- Codex / Claude Code / 本地 Agent -> `lark-cli`

后续计划支持：

- Notion
- Obsidian

典型请求：

- “帮我初始化一个高一 3 班的班主任工作台”
- “把这份学生名单和历史成绩一次性导入进去”
- “记一条学生表现：张三今天课后主动来问题”
- “按时间轴整理一下张三这学期的成绩和表现”
- “看一下这次月考后全班哪些学生需要重点跟进”
- “按成绩、性别和身高重新排一个座位表”
- “根据最近一次考试和这段时间的日常表现生成家长会 PPT”

## 仓库结构

```text
headteacher-skill/
├── SKILL.md
├── prompts/
├── references/
├── tools/
├── docs/
└── paper/
```

其中：

- `SKILL.md` 负责触发条件、主流程和运行时路由
- `prompts/` 负责 setup、迁移、runtime 和文件生成的操作指引
- `references/` 负责标准模型与规则说明
- `tools/` 负责初始化、迁移检查和产物登记

## 注意

- 这个 Skill 不再以“角色扮演班主任”为目标
- 不会默认覆盖已有的飞书多维表格
- 如果用户提供已有工作台，会先做检查与迁移建议
- 敏感字段默认按“少展示、先确认”处理


path: README_EN.md
content: <div align="center">

# Headteacher.Skill

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue.svg)](https://python.org)
[![AgentSkills](https://img.shields.io/badge/AgentSkills-Standard-green)](https://agentskills.io)

A skill for teachers and headteachers who want to manage class data and generate working documents with AI.

[中文](README.md)

</div>

## What it is

`Headteacher.Skill` focuses on two core abilities:

1. Record, read, and continuously update class data.
2. Generate specific Office files on demand.

Typical data work includes:

- one-time import of student rosters and historical records
- daily updates for scores, behavior, parent communication, and class duties
- longitudinal reading for one student across time
- horizontal reading for part of a class or the whole class at one time slice

Typical file outputs include:

- seat plans
- duty schedules
- parent meeting slides
- other Word / Excel / PowerPoint files based on class data

The default v1 backend is **Feishu Base**.  
`Notion` and `Obsidian` are reserved for later development.

Feishu access is routed by runtime:

- In OpenClaw, prefer the official OpenClaw Lark/Feishu plugin `openclaw-lark`
- In Codex, Claude Code, or other local agents, keep using the existing `lark-cli` path

## v1 scope

### Available now

- Feishu Base as the default backend
- standard headteacher data model
- migration inspection for existing Feishu Bases
- local artifact registry

### Planned later

- Notion
- Obsidian

## Install

You do not need to remember shell commands.

Just send this prompt to your agent tool:

```bash
Please install this skill for me: https://github.com/YZDame/headteacher-skill
```

Then tell the agent:

```bash
Please enable Headteacher.Skill and guide me through initializing a Feishu Base workspace first.
```

If the skill is running inside OpenClaw, Feishu Base setup should prefer the official OpenClaw Feishu plugin path instead of requiring `lark-cli` by default.

## Office skills

If you want this skill to generate Office files, your local agent environment should also have Office-related skills for:

- `.docx`
- `.xlsx`
- `.pptx`

Reference:

- Anthropic skills repository: [https://github.com/anthropics/skills/tree/main/skills](https://github.com/anthropics/skills/tree/main/skills)

## Example requests

- “Help me initialize a class workspace for Senior 1 Class 3.”
- “Import this student roster and historical scores.”
- “Show me Zhang San’s scores and daily performance in time order.”
- “Show me which students need attention after this monthly exam.”
- “Generate a new seat plan based on scores, gender, and height.”
- “Generate parent meeting slides from recent scores and daily records.”


path: README_ZH.md
content: <div align="center">

# 班主任.Skill

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue.svg)](https://python.org)
[![AgentSkills](https://img.shields.io/badge/AgentSkills-Standard-green)](https://agentskills.io)

一个面向教师和班主任的工作台 Skill。  
它的重点不是“模仿老师说话”，而是帮助教师持续管理学生数据，并按需生成日常需要的文件。

[安装](#安装) · [使用](#使用) · [工作台模型](#工作台模型)

</div>

---

## 这是什么

`班主任.Skill` 的核心是两种能力：

1. 数据的记录、读写以及动态读写。  
   包括一次性导入、日常动态添加、纵向读取某个学生、横向读取部分学生或全班。
2. 按需生成特定的 Office 文件。  
   包括排座表、值日表、家长会 PPT，以及其他通知或记录文档。

第一版默认首选接入 **飞书多维表格**。  
`Notion` 和 `Obsidian` 暂时作为后续开发方向保留。

飞书接入方式会按运行环境自动分流：

- 如果 Skill 运行在 OpenClaw 中，优先检测并使用 OpenClaw 官方飞书插件 `openclaw-lark`
- 如果 Skill 运行在 Codex、Claude Code 或其他本地 Agent 中，沿用原有 `lark-cli` 方案

## 第一版范围

### 现在可用

- 飞书多维表格工作台初始化
- 标准班主任数据模型
- 已有飞书多维表格的迁移检查
- 本地产物登记

### 后续开发

- Notion：等待后续开发，计划通过 MCP 的方式连接
- Obsidian：等待后续开发，计划使用 Obsidian 的 CLI

## 工作台模型

第一版围绕这些核心对象展开：

- 学生主档
- 考试批次
- 成绩明细
- 成长事件
- 家校沟通
- 座位安排
- 值日安排
- 班委任命
- 产物索引

飞书多维表格是第一后端，但 Skill 本身真正关心的是班级数据和文件生成能力。

## 安装

不需要手动记复杂命令。  
如果你在使用 Agent、OpenClaw 或其他类似工具，直接发送下面这句提示词即可：

```bash
请帮我安装这个技能：https://github.com/YZDame/headteacher-skill
```

安装完成后，再告诉你的 Agent：

```bash
请帮我启用班主任.Skill，并先带我完成飞书多维表格的初始化。
```

如果你是在 OpenClaw 中使用本 Skill，飞书多维表格初始化应优先通过 OpenClaw 官方飞书插件完成，而不是默认要求你安装 `lark-cli`。

如果你后续要使用“按需生成文件”这项能力，还需要本地具备 Office 相关的 Skill 套装，用来处理：

- `.docx`
- `.xlsx`
- `.pptx`

具体参考: Anthropic Skills 仓库中的 Office 相关 Skill  
  [https://github.com/anthropics/skills/tree/main/skills](https://github.com/anthropics/skills/tree/main/skills)

## 使用

触发 Skill 后，默认按以下顺序进入：

1. 环境检查
2. 后端选型
3. 工作台初始化
4. 正式运行时任务

默认首选接入：

- 飞书多维表格

其中飞书多维表格的接入会按环境自动切换：

- OpenClaw -> 官方飞书插件 `openclaw-lark` + 飞书 API
- Codex / Claude Code / 本地 Agent -> `lark-cli`

后续计划支持：

- Notion
- Obsidian

典型请求：

- “帮我初始化一个高一 3 班的班主任工作台”
- “把这份学生名单和历史成绩一次性导入进去”
- “记一条学生表现：张三今天课后主动来问题”
- “按时间轴整理一下张三这学期的成绩和表现”
- “看一下这次月考后全班哪些学生需要重点跟进”
- “按成绩、性别和身高重新排一个座位表”
- “根据最近一次考试和这段时间的日常表现生成家长会 PPT”

## 仓库结构

```text
headteacher-skill/
├── SKILL.md
├── prompts/
├── references/
├── tools/
├── docs/
└── paper/
```

其中：

- `SKILL.md` 负责触发条件、主流程和运行时路由
- `prompts/` 负责 setup、迁移、runtime 和文件生成的操作指引
- `references/` 负责标准模型与规则说明
- `tools/` 负责初始化、迁移检查和产物登记


path: docs/PRD.md
content: # 班主任.Skill PRD 摘要

## 产品定位

`班主任.Skill` 是一个 AI Native 班主任工作台 Skill。

它不是班主任角色卡，也不是人格蒸馏器。  
它负责引导用户完成工作台 setup，并在工作台建立后持续处理结构化数据和文件生成任务。

## 第一版目标

- 默认首选飞书多维表格
- 提供 setup 向导
- 提供统一语义层
- 提供 Feishu bootstrap 与迁移检查能力
- 预留 Notion / Obsidian 占位适配

## 核心对象

- 学生主档
- 考试批次
- 成绩明细
- 成长事件
- 家校沟通
- 座位安排
- 值日安排
- 班委任命
- 产物索引

## 两类核心能力

### 1. 数据记录与读取

- 写入分为：
  - 一次性导入
  - 动态添加或更新
- 读取分为：
  - 纵向读取：沿时间轴查看某个学生
  - 横向读取：在某个时间截面查看部分学生或全班

### 2. 自动化文件生成

- 排座表 / 值日表
  - 可按成绩、性别、身高、行为约束等属性排列
- 家长会 PPT
  - 依据学生成绩记录与日常表现记录自动生成

## 第一版不做

- 班主任人格扮演
- 多后端完整并进
- 高级权限与复杂仪表盘
- 心理健康自动判定


path: prompts/artifact-generator.md
content: # Artifact Generator

Use this prompt when the user asks for a concrete output file.

## Supported outputs in v1

- `.docx`
  - 家访记录
  - 班级通知
  - 学生谈话记录
- `.xlsx`
  - 座位表
  - 值日表
  - 班委表
  - 扣分汇总表
- `.pptx`
  - 家长会 PPT

## Workflow

1. Confirm the workspace is initialized.
2. Query structured data first.
3. Identify the artifact family:
   - arrangement artifact
   - presentation / communication artifact
3. Confirm which template or source materials should be used.
4. Generate the local file.
5. Register it:

```bash
python3 tools/artifact_registry.py register ...
```

6. If the user wants cloud sync, then sync and update the registry entry.

## Parent meeting PPT guidance

For 家长会 PPT in v1:

- use a text-defined outline instead of depending on a bundled `.pptx` template
- pull both score records and daily performance / conduct records into the slide plan
- prefer sections such as:
  - 封面与会议主题
  - 教师与班级概况
  - 班委/科代表/班级文化
  - 纪律、作业、宿舍等运行现状
  - 考试与成绩分析
  - 学习方法建议
  - 给家长的建议
  - 学科教师补充建议
- if the user has prior local slides, treat them as outline references only
- do not keep the original Office file inside the skill directory

## Arrangement artifact guidance

For 排座表 or 值日表:

- gather the arrangement goal first
- confirm which attributes should drive the arrangement, such as:
  - score
  - gender
  - height
  - dorm or group
  - conduct or discipline constraints
- generate from structured student data instead of manual spreadsheet edits

## Rules

- do not generate artifacts directly from vague chat context if structured data is missing
- call out missing templates or missing data explicitly
- the registry must record local path, type, timestamp, and optional remote link


path: prompts/backend-selector.md
content: # Backend Selector

Use this prompt when the user has not yet chosen a backend or asks about backend tradeoffs.

## Recommendation order

1. `feishu_base`
2. `notion`
3. `obsidian`
4. `local_only`

## Decision rules

### Prefer `feishu_base` when:

- the user wants structured records
- the user wants dashboards or views
- the user wants the most complete v1 support
- the user can use either:
  - the official OpenClaw Lark/Feishu plugin in OpenClaw
  - or `lark-cli` in Codex / Claude Code / local agent environments

### Allow `notion` when:

- the user already organizes class work in Notion
- the user can connect Notion MCP in the current agent environment
- they accept that v1 is a planning and mapping adapter, not a full automation backend

### Allow `obsidian` when:

- the user wants local-first knowledge management
- they can install or verify Obsidian CLI locally
- they accept markdown templates and folder scaffolding instead of full structured runtime parity

### Use `local_only` when:

- the user wants to defer backend choice
- the user only wants local manifests and templates for now

## Response requirements

- always recommend a default, do not present all options as equal
- state clearly that Feishu is the only fully supported v1 backend
- state limitations for Notion and Obsidian instead of overstating support
- state clearly that Notion MCP and Obsidian CLI are external prerequisites, not bundled parts of this skill


path: prompts/feishu-bootstrap.md
content: # Feishu Bootstrap

Use this prompt when the selected backend is `feishu_base`.

## Prerequisites

Run:

```bash
python3 tools/setup_doctor.py --backend feishu_base --format markdown
```

Read the reported `agent runtime` and `Feishu access mode` first.

### If access mode is `openclaw_plugin`

- verify whether the official OpenClaw Lark/Feishu plugin `openclaw-lark` is installed
- if missing, guide the user to install that plugin first
- once installed, create the workspace through the plugin's Feishu Base API tools
- use API-level operations to create:
  - the base
  - tables
  - fields
  - relation fields
  - views
- do not require `lark-cli` in this branch

### If access mode is `lark_cli`

If `lark-cli` is missing, guide installation first.

If `lark-cli` is not configured, guide:

```bash
lark-cli config init --new
```

## Bootstrap path

### New workspace

If access mode is `lark_cli`, use:

```bash
python3 tools/feishu_bootstrap.py bootstrap --workspace-name "<class-name>" --format markdown
```

If access mode is `openclaw_plugin`, use the official OpenClaw Feishu plugin's Base APIs to perform the same schema bootstrap based on this repository's schema definition.

### Existing Base provided by user

If access mode is `lark_cli`, inspect first:

```bash
python3 tools/migration_inspector.py feishu --base-token "<base-token>" --format markdown
```

If access mode is `openclaw_plugin`, inspect the existing Base through the plugin's Base APIs first, then classify it before deciding whether to connect or migrate.

Then decide:

- connect existing headteacher base
- or copy-and-refactor a subject-teacher base

## Rules

- do not overwrite an existing Base by default
- classify first, migrate second
- write the local workspace manifest after bootstrap succeeds
- keep the user informed of created tables, views, and local manifest path
- when running in OpenClaw, prefer the official plugin's Feishu Base tools over local shell-based Feishu tooling


path: prompts/migration-guide.md
content: # Migration Guide

Use this prompt when the user provides an existing Feishu Base.

## Goal

Determine whether the Base is:

- already a headteacher workspace
- a subject-teacher database
- or a generic Base that needs manual mapping

## Required command

```bash
python3 tools/migration_inspector.py feishu --base-token "<base-token>" --format markdown
```

## Interpretation rules

### Subject-teacher base

Typical signals:

- a mixed `学生信息` table with score columns
- a generic `学生记录` table with text student references
- subject-specific assessment columns such as quizzes, stages, or ranking summaries

Recommendation:

- copy and refactor
- do not overwrite in place

### Headteacher workspace

Typical signals:

- separate student master, score, event, communication, schedule, and artifact tables
- relation-based student references instead of plain name text

Recommendation:

- connect directly

### Generic or unknown

Recommendation:

- inspect manually
- generate a schema mapping before migration


path: prompts/runtime-router.md
content: # Runtime Router

Use this prompt after setup is complete.

The runtime has two top-level capability families:

1. `data operations`
2. `generate artifact`

Always identify the family first, then choose the primary intent.

## Intent routing

Route each user request into exactly one primary intent:

- `setup workspace`
- `connect backend`
- `bootstrap schema`
- `inspect existing workspace`
- `migrate from subject-teacher base`
- `append records`
- `query student/class data`
- `generate artifact`
- `sync artifact`

## Routing rules

### `setup workspace`

Use when no workspace manifest exists or setup is incomplete.

### `connect backend`

Use when the user wants to switch or add Feishu / Notion / Obsidian.

### `inspect existing workspace`

Use when the user provides a Base token, Base link, or existing workspace path.

### `migrate from subject-teacher base`

Use when the existing Feishu Base looks like a subject-teacher database.

### `append records`

Use for student info, grades, conduct logs, parent communication, seat plans, duty schedules, committee updates.

Within this intent, distinguish:

- one-time import: bulk initialize or migrate existing material
- dynamic append: add new exam, conduct, contact, or schedule records during daily use

### `query student/class data`

Use for summaries, statistics, dashboards, follow-up questions, and class status reports.

Within this intent, distinguish:

- longitudinal read: one student across time
  - examples: timeline, growth path, repeated contacts, behavior trend
- horizontal read: many students at one time slice
  - examples: current class snapshot, partial cohort comparison, one exam or one week view

### `generate artifact`

Use for `.docx`, `.xlsx`, `.pptx` outputs only after structured data exists.

Typical subtypes:

- arrangement artifacts
  - seat plan
  - duty schedule
  - committee table
- presentation / communication artifacts
  - parent meeting PPT
  - class notice
  - parent visit record

If the request is for a seat plan or duty schedule, ask which arrangement attributes matter most, such as:

- score
- gender
- height
- dorm or group
- behavior or conduct constraints

### `sync artifact`

Use when the user wants generated files uploaded or linked back to the backend.


path: prompts/schema-installer.md
content: # Schema Installer

Use this prompt whenever you need to explain or apply the standard headteacher schema.

## Source of truth

The schema source of truth is:

- [references/schema-manifest.md](../references/schema-manifest.md)
- `python3 tools/schema_planner.py --backend <backend> --format <format>`

## Rules

- always start from the unified semantic model
- do not invent backend-specific tables or fields without checking the schema manifest
- if the backend is not fully implemented, emit a mapping plan and limitations instead of pretending to install it

## Typical commands

Feishu summary:

```bash
python3 tools/schema_planner.py --backend feishu_base --format markdown
```

Notion summary:

```bash
python3 tools/schema_planner.py --backend notion --format markdown
```

Obsidian summary:

```bash
python3 tools/schema_planner.py --backend obsidian --format markdown
```


path: prompts/setup.md
content: # Setup Workflow

Use this prompt when the workspace has not been initialized yet.

## Goal

Turn a fresh installation into a usable headteacher workspace.

## Default behavior

1. Check whether `./.headteacher-skill/workspace_manifest.json` exists.
2. If missing, enter setup mode immediately.
3. Run:

```bash
python3 tools/setup_doctor.py --format markdown
```

4. Use the doctor result to identify the agent runtime and Feishu access mode.
5. Recommend `feishu_base` unless the user explicitly prefers another backend.
6. If the user accepts Feishu and the access mode is `openclaw_plugin`, guide them through:
   - verifying whether the official OpenClaw Lark/Feishu plugin (`openclaw-lark`) is installed
   - installing that plugin first if missing
   - using the plugin's Feishu Base API tools to create the workspace, tables, fields, views, and records
   - not requiring `lark-cli` in this branch
7. If the user accepts Feishu and the access mode is `lark_cli`, guide them through:
   - installing or verifying `lark-cli`
   - running `lark-cli config init --new` if needed
   - bootstrapping the workspace with `tools/feishu_bootstrap.py`
8. If the user selects Notion, verify that Notion MCP is connected. If not, stop at external setup guidance.
9. If the user selects Obsidian, verify that Obsidian CLI is installed and remind them to install the official Obsidian skill if needed.
10. After bootstrap, explain what tasks the user can run next.

## Required outputs

- environment status
- agent runtime
- Feishu access mode
- chosen backend
- whether the workspace is initialized
- next available actions

## Do not do

- do not role-play a teacher
- do not ask persona questions
- do not jump into file generation before setup is complete
- do not pretend this repository contains Notion MCP or Obsidian CLI capabilities
- do not force `lark-cli` when the skill is running inside OpenClaw with the official Feishu plugin path available


path: references/artifact-spec.md
content: # Artifact Spec

Artifact generation is a downstream subsystem. It should only start after structured data already exists.

## Supported artifact kinds

### `.docx`

- 家访记录
- 班级通知
- 学生谈话记录

### `.xlsx`

- 座位表
- 值日表
- 班委表
- 扣分汇总表

### `.pptx`

- 家长会 PPT

These artifacts belong to two main generation families:

1. arrangement artifacts
   - seat plans
   - duty schedules
   - committee tables
2. presentation or communication artifacts
   - parent meeting slides
   - notices
   - visit records
   - student talk records

## Template policy

- Do not keep real `.docx`, `.xlsx`, or `.pptx` files inside the published skill directory.
- Existing Office files may be used only as temporary local references when extracting structure or outline.
- The skill should preserve reusable outlines, field mappings, and generation rules as text documentation, not as bundled Office assets.

## Artifact request contract

An artifact request should include:

- `artifact_type`
- `template_name` or template source
- `context_ref`
- `output_path`
- `sync_policy`

For arrangement artifacts, also include:

- `arrangement_constraints`
- `priority_attributes`

Examples of useful arrangement attributes:

- score
- gender
- height
- dorm or group
- conduct constraints

## Parent meeting PPT outline

When generating a 家长会 PPT in v1, prefer a text-defined outline instead of a bundled `.pptx` template.

Its data sources should come from both:

- score records
- daily performance or growth records

Suggested default outline:

1. 封面：时间、班级、家校协同主题
2. 教师与班级概况：任课教师、班级人数、班级基本面
3. 班级组织与文化：班委、科代表、班级文化建设
4. 班级运行现状：纪律、宿舍、作业、卫生、待改进点
5. 考试与成绩：阶段考试结果、学科表现、成绩表彰
6. 学习方法建议：学习兴趣、学习策略、考试观
7. 给家长的建议：家校协同、沟通方式、支持策略
8. 学科教师建议：可选附录部分

This outline may be adapted from prior local materials, but the Office source file itself should not be retained in the repository.

## Registry rules

Every generated artifact should be recorded with:

- title
- type
- related entity
- local path
- timestamp
- optional remote URL
- sync status
- parameter summary

## Guardrails

- refuse to fabricate structured data that does not exist
- call out missing templates explicitly
- do not sync externally unless the user asks for it


path: references/backend-contract.md
content: # Backend Contract

The skill operates against a unified backend adapter contract. Each backend implementation or stub should expose the same conceptual methods.

## Adapter interface

- `check_prerequisites()`
  - Verify local tools, connectors, or configuration
- `authenticate()`
  - Guide or verify backend authentication
- `bootstrap_workspace(schema_manifest)`
  - Create the initial workspace structure
- `read_entities(query_spec)`
  - Read normalized entities from the backend
- `write_entities(change_set)`
  - Apply normalized writes to the backend
- `materialize_views(view_manifest)`
  - Create or refresh derived views
- `register_artifact(record)`
  - Record generated artifact metadata
- `describe_limitations()`
  - Return backend-specific support boundaries

## v1 adapter status

- `feishu_base_adapter`
  - fully implemented
- `notion_adapter_stub`
  - mapping + setup guidance only
- `obsidian_adapter_stub`
  - folder/template scaffolding guidance only

## Shared rules

- All writes should originate from the unified semantic model.
- All destructive operations should be previewed or clearly acknowledged.
- Existing backends must be inspected before migration.
- Artifact generation is downstream of structured data, not a substitute for it.


path: references/feishu-model.md
content: # Feishu Model

Feishu Base is the default v1 backend.

## Standard tables

The Feishu bootstrap should create these tables:

- `班级配置`
- `学生主档`
- `考试批次`
- `成绩明细`
- `成长事件`
- `家校沟通`
- `座位安排`
- `值日安排`
- `班委任命`
- `产物索引`

## Field design principles

- keep `学生主档` as the source of truth for student identity
- use link fields such as `student_ref` and `exam_ref` instead of plain name text
- keep score data in long-form rows inside `成绩明细`
- keep conduct, duty, and learning observations in `成长事件`
- never default to overwriting an existing Base

## Existing Base inspection

When a user provides an existing Base:

1. inspect with `tools/migration_inspector.py`
2. classify it
3. if it is a subject-teacher base, recommend copy-and-refactor

Typical subject-teacher signals:

- `学生信息` mixes profile fields and score columns
- `学生记录` stores generic activity notes
- student references are plain text instead of relation fields

## Views

Create at least one grid view per table. Recommended named views:

- `学生总览`
- `全科成绩`
- `德育与班务`
- `家校沟通`
- `当前座位`
- `当前值日`
- `当前班委`
- `产物归档`

## Dashboard stance

Dashboard creation is optional in v1.

- It is acceptable to create empty dashboard shells
- It is not required to fully configure chart blocks in the first bootstrap pass


path: references/notion-model.md
content: # Notion Model

Notion is a v1 placeholder backend.

## What v1 supports

- explaining the unified schema in Notion terms
- generating a database plan
- generating minimal setup instructions
- optionally creating a lightweight workspace skeleton if the agent has Notion access

## External prerequisite

This repository does not ship Notion connectivity itself.

Before using the Notion path, the user should:

- connect Notion MCP in their agent environment
- verify that Notion tools are actually available in the session

If Notion MCP is not connected, the skill should stop at installation or verification guidance rather than pretending Notion is ready.

## What v1 does not promise

- full runtime parity with Feishu Base
- automatic dashboards
- full artifact registration and sync automation

## Suggested Notion layout

- `班级配置` database
- `学生主档` database
- `考试批次` database
- `成绩明细` database
- `成长事件` database
- `家校沟通` database
- `产物索引` database
- one overview page linking the databases

## Adapter stance

If the user chooses Notion in v1:

- state that it is a planning-first adapter
- generate the mapping cleanly
- treat Notion MCP as a prerequisite that must already exist outside this repo
- do not claim a fully automated runtime


path: references/obsidian-model.md
content: # Obsidian Model

Obsidian is a v1 local-first placeholder backend.

## What v1 supports

- folder scaffolding
- markdown templates
- schema documentation
- local artifact indexing guidance

## External prerequisite

This repository does not ship Obsidian connectivity itself.

Before using the Obsidian path, the user should:

- install the Obsidian CLI locally
- keep Obsidian available if the CLI requires a running app
- install or enable the official Obsidian-related skill in the agent environment when needed

If these prerequisites are missing, the skill should stop at installation or verification guidance.

## What v1 does not promise

- full structured database behavior
- full parity with Feishu Base relations and views
- automated dashboards

## Suggested vault structure

```text
班主任工作台/
├── 00-配置/
├── 01-学生主档/
├── 02-考试批次/
├── 03-成绩明细/
├── 04-成长事件/
├── 05-家校沟通/
├── 06-班级安排/
└── 07-产物索引/
```

## Adapter stance

If the user chooses Obsidian in v1:

- generate folders and markdown templates
- emit a schema mapping
- treat Obsidian CLI and the official Obsidian skill as external prerequisites
- explain that this is a local knowledge-workbench mode, not a full structured runtime backend


path: references/schema-manifest.md
content: # Schema Manifest

This file defines the unified semantic model used by `班主任.Skill`.

## System stance

The skill is built around two core capability families:

1. data record and retrieval
2. automated artifact generation

The underlying modeling principle is:

- separate stable objects from ongoing events
- use structured data as the source of truth
- treat Office outputs as generated artifacts, not primary storage

For the headteacher domain, this means:

- `student_master` is the stable object layer
- scores, conduct, daily observations, parent communication, and schedules are event or assignment layers
- artifacts such as seat plans, duty schedules, or parent meeting slides are generated from those layers

## Data operation modes

### Write modes

- one-time import
  - initialize a workspace from rosters, existing spreadsheets, legacy bases, or notes
- dynamic append / update
  - record new exams, observations, discipline events, contacts, and schedule changes

### Read modes

- longitudinal read
  - follow one student across time
  - useful for growth tracking, interventions, and parent communication
- horizontal read
  - inspect a set of students or the whole class at one time slice
  - useful for one exam, one week, one seating cycle, or one duty cycle

## Core entities

### 1. `class_profile`

One record per class workspace.

Suggested fields:

- class_name
- academic_year
- current_term
- headteacher
- default_backend
- workspace_status
- notes

### 2. `student_master`

The authoritative student roster and profile table.

Suggested fields:

- name
- student_id
- student_number
- gender
- class_name
- term
- elective_subjects
- previous_school
- dorm_building
- dorm_room
- dorm_bed
- guardian_1_name
- guardian_1_relation
- guardian_1_phone
- guardian_2_name
- guardian_2_relation
- guardian_2_phone
- national_id
- address
- focus_tags
- notes

### 3. `exam_batch`

Metadata about each assessment batch.

Suggested fields:

- exam_name
- exam_id
- exam_date
- term
- exam_type
- subject_scope
- source
- notes

### 4. `score_detail`

Long-form score storage. One row should represent one student, one exam batch, one subject.

Suggested fields:

- student_ref
- exam_ref
- subject
- score
- class_rank
- grade_rank
- grade_level
- absent_flag
- source_column
- notes

### 5. `growth_event`

A unified event stream for learning follow-up, conduct, class duties, and positive recognition.

Suggested fields:

- title
- student_ref
- event_date
- domain
- event_type
- evaluation
- tags
- score_delta
- description
- evidence
- recorder
- status

### 6. `parent_contact`

For calls, messages, visits, and follow-up tasks.

Suggested fields:

- subject
- student_ref
- contact_date
- channel
- counterpart
- summary
- next_action
- status
- attachments

### 7. `seat_assignment`

Suggested fields:

- version
- student_ref
- effective_date
- row
- column
- zone
- deskmate
- reason
- active_flag

### 8. `duty_assignment`

Suggested fields:

- title
- student_ref
- cycle_start
- cycle_end
- role
- group_name
- leader_flag
- replacement_note
- makeup_status

### 9. `committee_assignment`

Suggested fields:

- role_name
- student_ref
- start_date
- end_date
- active_flag
- notes

### 10. `artifact_record`

Suggested fields:

- artifact_title
- artifact_type
- related_entity
- template_name
- local_path
- remote_url
- created_at
- sync_status
- params_summary

## Migration note

If the user provides an existing subject-teacher Base:

- treat mixed score columns as import material, not as the final model
- unpivot subject-specific score columns into `score_detail`
- migrate generic activity logs into `growth_event`

## Backend mapping

- Feishu Base: fully implemented in v1
- Notion: database/page structure only in v1
- Obsidian: folders/templates only in v1

## Artifact generation note

The standard model should support at least these downstream generation tasks:

- seat plans or duty schedules arranged by selected student attributes
- parent meeting PPT generated from score records plus daily performance records
- notices, visit records, and talk records generated from structured context


path: requirements.txt
content: # Core runtime helpers
requests>=2.28.0

# Office artifact generation
python-docx>=1.1.0
openpyxl>=3.1.0
python-pptx>=0.6.23


path: tools/artifact_registry.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List


STATE_DIR = Path(".headteacher-skill")
REGISTRY_PATH = STATE_DIR / "artifact_registry.json"


def ensure_state_dir() -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)


def load_registry(path: Path = REGISTRY_PATH) -> List[Dict[str, Any]]:
    if not path.exists():
        return []
    return json.loads(path.read_text(encoding="utf-8"))


def save_registry(records: List[Dict[str, Any]], path: Path = REGISTRY_PATH) -> None:
    ensure_state_dir()
    path.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8")


def register_artifact(args: argparse.Namespace) -> Dict[str, Any]:
    records = load_registry()
    record = {
        "artifact_id": f"artifact-{uuid.uuid4().hex[:10]}",
        "title": args.title,
        "artifact_type": args.artifact_type,
        "related_entity": args.related_entity,
        "template_name": args.template_name,
        "local_path": args.local_path,
        "remote_url": args.remote_url,
        "sync_status": args.sync_status,
        "params_summary": args.params_summary,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    records.append(record)
    save_registry(records)
    return record


def list_artifacts(limit: int) -> List[Dict[str, Any]]:
    records = load_registry()
    return records[-limit:]


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Manage local artifact metadata for the headteacher workbench.")
    subparsers = parser.add_subparsers(dest="command", required=True)

    register = subparsers.add_parser("register", help="Register a generated artifact.")
    register.add_argument("--title", required=True)
    register.add_argument("--artifact-type", required=True, choices=["docx", "xlsx", "pptx"])
    register.add_argument("--related-entity", default="")
    register.add_argument("--template-name", default="")
    register.add_argument("--local-path", required=True)
    register.add_argument("--remote-url", default="")
    register.add_argument("--sync-status", default="仅本地")
    register.add_argument("--params-summary", default="")

    listing = subparsers.add_parser("list", help="List recent artifact records.")
    listing.add_argument("--limit", type=int, default=20)
    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    if args.command == "register":
        record = register_artifact(args)
        print(json.dumps(record, ensure_ascii=False, indent=2))
        return 0
    if args.command == "list":
        print(json.dumps(list_artifacts(args.limit), ensure_ascii=False, indent=2))
        return 0
    parser.error("Unknown command")
    return 2


if __name__ == "__main__":
    raise SystemExit(main())


path: tools/feishu_bootstrap.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from schema_planner import (
    build_schema_manifest,
    build_table_specs,
    build_workspace_manifest_template,
)
from setup_doctor import check_feishu_backend


STATE_DIR = Path(".headteacher-skill")
WORKSPACE_MANIFEST_PATH = STATE_DIR / "workspace_manifest.json"


def find_first_key(data: Any, target_key: str) -> Optional[Any]:
    if isinstance(data, dict):
        if target_key in data:
            return data[target_key]
        for value in data.values():
            found = find_first_key(value, target_key)
            if found is not None:
                return found
    if isinstance(data, list):
        for item in data:
            found = find_first_key(item, target_key)
            if found is not None:
                return found
    return None


def run_json_command(cmd: List[str]) -> Dict[str, Any]:
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        message = result.stderr.strip() or result.stdout.strip()
        raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{message}")
    return json.loads(result.stdout)


def ensure_state_dir() -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)


class FeishuBaseAdapter:
    def check_prerequisites(self) -> Dict[str, Any]:
        return check_feishu_backend()

    def authenticate(self) -> Dict[str, Any]:
        status = self.check_prerequisites()
        if status["installed"] and status["configured"]:
            return {"status": "ready", "message": "lark-cli is configured."}
        if not status["installed"]:
            return {"status": "missing_cli", "message": "Install lark-cli first."}
        return {"status": "needs_config", "message": "Run `lark-cli config init --new`."}

    def describe_limitations(self) -> Dict[str, Any]:
        return {
            "backend": "feishu_base",
            "limitations": [
                "v1 creates schema, relation fields, base views, and optional empty dashboards",
                "v1 does not fully configure dashboard blocks",
                "v1 does not overwrite an existing Base by default",
            ],
        }

    def read_entities(self, query_spec: Dict[str, Any]) -> Dict[str, Any]:
        return {"status": "not_implemented", "query_spec": query_spec}

    def write_entities(self, change_set: Dict[str, Any]) -> Dict[str, Any]:
        return {"status": "not_implemented", "change_set": change_set}

    def materialize_views(self, view_manifest: Dict[str, Any]) -> Dict[str, Any]:
        return {"status": "not_implemented", "view_manifest": view_manifest}

    def register_artifact(self, record: Dict[str, Any]) -> Dict[str, Any]:
        return {"status": "delegate_to_artifact_registry", "record": record}

    def _create_base(self, workspace_name: str, folder_token: str, time_zone: str) -> Dict[str, Any]:
        cmd = ["lark-cli", "base", "+base-create", "--name", workspace_name, "--time-zone", time_zone]
        if folder_token:
            cmd.extend(["--folder-token", folder_token])
        response = run_json_command(cmd)
        base = find_first_key(response, "base") or {}
        base_token = base.get("base_token") or base.get("app_token") or find_first_key(response, "base_token")
        return {
            "base_token": base_token,
            "name": base.get("name", workspace_name),
            "url": base.get("url", ""),
            "raw": response,
        }

    def _create_table(self, base_token: str, table_name: str, fields: List[Dict[str, Any]], views: List[Dict[str, Any]]) -> Dict[str, Any]:
        cmd = [
            "lark-cli",
            "base",
            "+table-create",
            "--base-token",
            base_token,
            "--name",
            table_name,
            "--fields",
            json.dumps(fields, ensure_ascii=False),
        ]
        if views:
            cmd.extend(["--view", json.dumps(views, ensure_ascii=False)])
        response = run_json_command(cmd)
        table_id = find_first_key(response, "table_id")
        return {"table_id": table_id, "raw": response}

    def _create_link_field(self, base_token: str, table_id: str, field_json: Dict[str, Any]) -> Dict[str, Any]:
        cmd = [
            "lark-cli",
            "base",
            "+field-create",
            "--base-token",
            base_token,
            "--table-id",
            table_id,
            "--json",
            json.dumps(field_json, ensure_ascii=False),
        ]
        response = run_json_command(cmd)
        return {"field_id": find_first_key(response, "field_id"), "raw": response}

    def _create_dashboard(self, base_token: str, name: str, theme_style: str) -> Dict[str, Any]:
        cmd = [
            "lark-cli",
            "base",
            "+dashboard-create",
            "--base-token",
            base_token,
            "--name",
            name,
            "--theme-style",
            theme_style,
        ]
        response = run_json_command(cmd)
        dashboard_id = response.get("dashboard_id") or find_first_key(response, "dashboard_id")
        return {"dashboard_id": dashboard_id, "name": name, "raw": response}

    def build_execution_plan(self, workspace_name: str, base_token: str = "", create_dashboards: bool = False) -> Dict[str, Any]:
        manifest = build_schema_manifest()
        table_specs = build_table_specs()
        plan: Dict[str, Any] = {
            "workspace_name": workspace_name,
            "backend": "feishu_base",
            "base_action": "reuse_existing" if base_token else "create_new",
            "tables": [],
            "dashboards": manifest["dashboards"] if create_dashboards else [],
        }
        for table in table_specs:
            plan["tables"].append(
                {
                    "name": table.name,
                    "create_fields": table.feishu_table_create_fields(),
                    "link_fields": [field.public_dict() for field in table.fields if field.field_type == "link"],
                    "views": [view.public_dict() for view in table.views],
                }
            )
        return plan

    def bootstrap_workspace(
        self,
        workspace_name: str,
        base_token: str = "",
        folder_token: str = "",
        time_zone: str = "Asia/Shanghai",
        create_dashboards: bool = False,
        dry_run: bool = False,
    ) -> Dict[str, Any]:
        prerequisites = self.check_prerequisites()
        if not prerequisites["installed"]:
            raise RuntimeError("lark-cli is not installed.")
        if not prerequisites["configured"]:
            raise RuntimeError("lark-cli is not configured. Run `lark-cli config init --new` first.")

        plan = self.build_execution_plan(workspace_name, base_token=base_token, create_dashboards=create_dashboards)
        if dry_run:
            return {"status": "dry_run", "plan": plan}

        base_info = {"base_token": base_token, "name": workspace_name, "url": ""}
        if not base_token:
            base_info = self._create_base(workspace_name, folder_token, time_zone)
            if not base_info["base_token"]:
                raise RuntimeError("Unable to resolve the new base token from lark-cli output.")

        table_specs = build_table_specs()
        table_lookup: Dict[str, str] = {}
        created_tables: List[Dict[str, Any]] = []
        for table in table_specs:
            response = self._create_table(
                base_token=base_info["base_token"],
                table_name=table.name,
                fields=table.feishu_table_create_fields(),
                views=[view.public_dict() for view in table.views],
            )
            if not response["table_id"]:
                raise RuntimeError(f"Unable to resolve table_id for table {table.name}.")
            table_lookup[table.name] = response["table_id"]
            created_tables.append({"name": table.name, "table_id": response["table_id"]})

        created_link_fields: List[Dict[str, Any]] = []
        for table in table_specs:
            table_id = table_lookup[table.name]
            for link_field in table.feishu_link_fields(table_lookup):
                response = self._create_link_field(base_info["base_token"], table_id, link_field)
                created_link_fields.append(
                    {
                        "table_name": table.name,
                        "field_name": link_field["name"],
                        "field_id": response["field_id"],
                    }
                )

        created_dashboards: List[Dict[str, Any]] = []
        if create_dashboards:
            for dashboard in build_schema_manifest()["dashboards"]:
                created_dashboards.append(
                    self._create_dashboard(base_info["base_token"], dashboard["name"], dashboard["theme_style"])
                )

        workspace_manifest = build_workspace_manifest_template(workspace_name, "feishu_base")
        workspace_manifest.update(
            {
                "status": "ready",
                "created_at": datetime.now(timezone.utc).isoformat(),
                "base": {
                    "base_token": base_info["base_token"],
                    "name": base_info["name"],
                    "url": base_info["url"],
                },
                "tables": created_tables,
                "dashboards": [
                    {"name": item["name"], "dashboard_id": item["dashboard_id"]} for item in created_dashboards
                ],
                "capabilities": {
                    "setup_complete": True,
                    "artifact_generation": True,
                    "remote_sync": True,
                },
            }
        )
        ensure_state_dir()
        WORKSPACE_MANIFEST_PATH.write_text(
            json.dumps(workspace_manifest, ensure_ascii=False, indent=2), encoding="utf-8"
        )

        return {
            "status": "ready",
            "base": workspace_manifest["base"],
            "tables": created_tables,
            "link_fields": created_link_fields,
            "dashboards": workspace_manifest["dashboards"],
            "workspace_manifest_path": str(WORKSPACE_MANIFEST_PATH),
        }


def render_markdown(result: Dict[str, Any]) -> str:
    if "limitations" in result:
        lines = ["# Feishu Backend Limitations", ""]
        for item in result["limitations"]:
            lines.append(f"- {item}")
        return "\n".join(lines)

    if result["status"] == "dry_run":
        plan = result["plan"]
        lines = ["# Feishu Bootstrap Plan", ""]
        lines.append(f"- Workspace: `{plan['workspace_name']}`")
        lines.append(f"- Base action: `{plan['base_action']}`")
        lines.append("")
        lines.append("## Tables")
        lines.append("")
        for table in plan["tables"]:
            lines.append(f"### {table['name']}")
            lines.append(f"- Create fields: {', '.join(field['name'] for field in table['create_fields'])}")
            if table["link_fields"]:
                lines.append(f"- Link fields: {', '.join(field['name'] for field in table['link_fields'])}")
            lines.append(f"- Views: {', '.join(view['name'] for view in table['views'])}")
            lines.append("")
        if plan["dashboards"]:
            lines.append("## Dashboards")
            lines.append("")
            for dashboard in plan["dashboards"]:
                lines.append(f"- {dashboard['name']}")
        return "\n".join(lines)

    lines = ["# Feishu Bootstrap Result", ""]
    lines.append(f"- Status: `{result['status']}`")
    lines.append(f"- Base token: `{result['base']['base_token']}`")
    if result["base"].get("url"):
        lines.append(f"- Base URL: {result['base']['url']}")
    lines.append(f"- Workspace manifest: `{result['workspace_manifest_path']}`")
    lines.append("")
    lines.append("## Tables")
    lines.append("")
    for table in result["tables"]:
        lines.append(f"- {table['name']} (`{table['table_id']}`)")
    if result["dashboards"]:
        lines.append("")
        lines.append("## Dashboards")
        lines.append("")
        for dashboard in result["dashboards"]:
            lines.append(f"- {dashboard['name']} (`{dashboard['dashboard_id']}`)")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description="Bootstrap the Feishu Base workspace for the headteacher skill.")
    subparsers = parser.add_subparsers(dest="command", required=True)

    bootstrap = subparsers.add_parser("bootstrap", help="Create or reuse a Feishu Base and write the standard schema.")
    bootstrap.add_argument("--workspace-name", required=True)
    bootstrap.add_argument("--base-token", default="")
    bootstrap.add_argument("--folder-token", default="")
    bootstrap.add_argument("--time-zone", default="Asia/Shanghai")
    bootstrap.add_argument("--create-dashboards", action="store_true")
    bootstrap.add_argument("--dry-run", action="store_true")
    bootstrap.add_argument("--format", default="json", choices=["json", "markdown"])

    plan = subparsers.add_parser("plan", help="Emit a dry-run bootstrap plan.")
    plan.add_argument("--workspace-name", required=True)
    plan.add_argument("--base-token", default="")
    plan.add_argument("--create-dashboards", action="store_true")
    plan.add_argument("--format", default="json", choices=["json", "markdown"])

    limitations = subparsers.add_parser("describe-limitations", help="Show Feishu backend limitations.")
    limitations.add_argument("--format", default="json", choices=["json", "markdown"])

    args = parser.parse_args()
    adapter = FeishuBaseAdapter()

    if args.command == "describe-limitations":
        result = adapter.describe_limitations()
    elif args.command == "plan":
        result = {"status": "dry_run", "plan": adapter.build_execution_plan(args.workspace_name, args.base_token, args.create_dashboards)}
    else:
        result = adapter.bootstrap_workspace(
            workspace_name=args.workspace_name,
            base_token=args.base_token,
            folder_token=args.folder_token,
            time_zone=args.time_zone,
            create_dashboards=args.create_dashboards,
            dry_run=args.dry_run,
        )

    output_format = getattr(args, "format", "json")
    if output_format == "json":
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(render_markdown(result))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: tools/migration_inspector.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from schema_planner import build_schema_manifest


SUBJECT_SCORE_HINTS = (
    "小测",
    "阶段",
    "期中",
    "期末",
    "复数",
    "向量",
    "数学",
    "竞赛",
    "平均排名",
)


def parse_json_output(text: str) -> Dict[str, Any]:
    return json.loads(text)


def run_json_command(cmd: List[str]) -> Dict[str, Any]:
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(result.stderr.strip() or result.stdout.strip())
    return parse_json_output(result.stdout)


def fetch_feishu_structure(base_token: str) -> Dict[str, Any]:
    tables = run_json_command(
        ["lark-cli", "base", "+table-list", "--base-token", base_token, "--offset", "0", "--limit", "100"]
    )["data"]["items"]
    table_details = []
    for table in tables:
        fields = run_json_command(
            [
                "lark-cli",
                "base",
                "+field-list",
                "--base-token",
                base_token,
                "--table-id",
                table["table_id"],
                "--offset",
                "0",
                "--limit",
                "200",
            ]
        )["data"]["items"]
        table_details.append(
            {
                "table_id": table["table_id"],
                "table_name": table["table_name"],
                "fields": fields,
            }
        )
    return {"tables": table_details}


def classify_structure(structure: Dict[str, Any]) -> Dict[str, Any]:
    tables = structure["tables"]
    table_names = {table["table_name"] for table in tables}
    standard_tables = {table["name"] for table in build_schema_manifest()["tables"]}

    if len(standard_tables.intersection(table_names)) >= 6:
        return {
            "classification": "headteacher_workspace",
            "confidence": "high",
            "reason": "Most standard headteacher tables are already present.",
            "recommendation": "connect_existing",
        }

    subject_evidence: List[str] = []
    for table in tables:
        field_names = [field["field_name"] for field in table["fields"]]
        if table["table_name"] == "学生信息":
            score_like = [name for name in field_names if any(hint in name for hint in SUBJECT_SCORE_HINTS)]
            if score_like:
                subject_evidence.append(f"`学生信息` mixes profile fields and score columns: {', '.join(score_like[:6])}")
        if table["table_name"] == "学生记录":
            required = {"时间", "评价", "类型", "学生"}
            if required.issubset(set(field_names)):
                subject_evidence.append("`学生记录` looks like a generic subject-teacher event log with plain-text student references")

    if {"学生信息", "学生记录"}.issubset(table_names) and subject_evidence:
        return {
            "classification": "subject_teacher_base",
            "confidence": "high",
            "reason": "The structure matches a subject-teacher roster plus generic record log.",
            "recommendation": "copy_and_refactor",
            "evidence": subject_evidence,
        }

    generic_notes = []
    for table in tables:
        field_names = [field["field_name"] for field in table["fields"]]
        generic_notes.append(f"{table['table_name']}: {', '.join(field_names[:8])}")
    return {
        "classification": "generic_base",
        "confidence": "medium",
        "reason": "The structure does not match the standard headteacher model or the known subject-teacher template exactly.",
        "recommendation": "inspect_and_map",
        "evidence": generic_notes,
    }


def render_markdown(result: Dict[str, Any]) -> str:
    lines = ["# Migration Inspection", ""]
    lines.append(f"- Classification: `{result['classification']}`")
    lines.append(f"- Confidence: `{result['confidence']}`")
    lines.append(f"- Recommendation: `{result['recommendation']}`")
    lines.append(f"- Reason: {result['reason']}")
    evidence = result.get("evidence", [])
    if evidence:
        lines.append("")
        lines.append("## Evidence")
        lines.append("")
        for item in evidence:
            lines.append(f"- {item}")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description="Inspect an existing Feishu Base and classify it for migration.")
    subparsers = parser.add_subparsers(dest="backend", required=True)

    feishu = subparsers.add_parser("feishu", help="Inspect a Feishu Base.")
    feishu.add_argument("--base-token", required=True)
    feishu.add_argument("--format", default="json", choices=["json", "markdown"])

    args = parser.parse_args()
    structure = fetch_feishu_structure(args.base_token)
    result = classify_structure(structure)
    result["base_token"] = args.base_token
    result["tables"] = [
        {
            "table_id": table["table_id"],
            "table_name": table["table_name"],
            "fields": [field["field_name"] for field in table["fields"]],
        }
        for table in structure["tables"]
    ]

    if args.format == "json":
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(render_markdown(result))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: tools/schema_planner.py
content: #!/usr/bin/env python3
from __future__ import annotations

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


SCHEMA_VERSION = "2026-04-v1"


def select_option(name: str, hue: str = "Blue", lightness: str = "Light") -> Dict[str, str]:
    return {"name": name, "hue": hue, "lightness": lightness}


@dataclass
class FieldSpec:
    name: str
    field_type: str
    description: str = ""
    multiple: Optional[bool] = None
    options: List[Dict[str, str]] = field(default_factory=list)
    precision: Optional[int] = None
    link_table: Optional[str] = None
    bidirectional: bool = False

    def public_dict(self) -> Dict[str, Any]:
        data: Dict[str, Any] = {
            "name": self.name,
            "type": self.field_type,
            "description": self.description,
        }
        if self.multiple is not None:
            data["multiple"] = self.multiple
        if self.options:
            data["options"] = self.options
        if self.precision is not None:
            data["precision"] = self.precision
        if self.link_table:
            data["link_table"] = self.link_table
            data["bidirectional"] = self.bidirectional
        return data

    def to_feishu_json(self, table_lookup: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
        data: Dict[str, Any] = {"name": self.name, "type": self.field_type}
        if self.description:
            data["description"] = self.description
        if self.field_type == "number" and self.precision is not None:
            data["precision"] = self.precision
        if self.field_type == "select":
            data["multiple"] = bool(self.multiple)
            data["options"] = self.options
        if self.field_type == "link":
            if not self.link_table:
                raise ValueError(f"Field {self.name} is missing link_table")
            target = self.link_table
            if table_lookup:
                target = table_lookup.get(self.link_table, self.link_table)
            data["link_table"] = target
            data["bidirectional"] = self.bidirectional
        return data


@dataclass
class ViewSpec:
    name: str
    view_type: str = "grid"

    def public_dict(self) -> Dict[str, str]:
        return {"name": self.name, "type": self.view_type}


@dataclass
class TableSpec:
    name: str
    description: str
    primary_field: FieldSpec
    fields: List[FieldSpec]
    views: List[ViewSpec]

    def public_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "primary_field": self.primary_field.public_dict(),
            "fields": [field.public_dict() for field in self.fields],
            "views": [view.public_dict() for view in self.views],
        }

    def feishu_table_create_fields(self) -> List[Dict[str, Any]]:
        fields = [self.primary_field.to_feishu_json()]
        for field_spec in self.fields:
            if field_spec.field_type == "link":
                continue
            fields.append(field_spec.to_feishu_json())
        return fields

    def feishu_link_fields(self, table_lookup: Dict[str, str]) -> List[Dict[str, Any]]:
        items: List[Dict[str, Any]] = []
        for field_spec in self.fields:
            if field_spec.field_type == "link":
                items.append(field_spec.to_feishu_json(table_lookup))
        return items


def build_table_specs() -> List[TableSpec]:
    yes_no_options = [
        select_option("是", "Green", "Light"),
        select_option("否", "Red", "Light"),
    ]
    sync_status_options = [
        select_option("仅本地", "Grey", "Light"),
        select_option("待同步", "Orange", "Light"),
        select_option("已同步", "Green", "Light"),
        select_option("同步失败", "Red", "Light"),
    ]
    term_options = [
        select_option("上学期", "Blue", "Light"),
        select_option("下学期", "Purple", "Light"),
        select_option("暑期", "Orange", "Light"),
    ]

    return [
        TableSpec(
            name="班级配置",
            description="单班级的工作台配置与运行状态。",
            primary_field=FieldSpec("班级名称", "text", "班级工作台的显示名称。"),
            fields=[
                FieldSpec("学年", "text", "如 2025-2026。"),
                FieldSpec("当前学期", "select", "当前使用学期。", multiple=False, options=term_options),
                FieldSpec("班主任", "text", "班主任姓名或代号。"),
                FieldSpec(
                    "默认后端",
                    "select",
                    "工作台默认后端。",
                    multiple=False,
                    options=[
                        select_option("feishu_base", "Blue", "Light"),
                        select_option("notion", "Purple", "Light"),
                        select_option("obsidian", "Green", "Light"),
                        select_option("local_only", "Grey", "Light"),
                    ],
                ),
                FieldSpec(
                    "工作台状态",
                    "select",
                    "初始化与可用性状态。",
                    multiple=False,
                    options=[
                        select_option("draft", "Grey", "Light"),
                        select_option("ready", "Green", "Light"),
                        select_option("needs_attention", "Orange", "Light"),
                    ],
                ),
                FieldSpec("备注", "text", "工作台初始化说明或例外。"),
            ],
            views=[ViewSpec("配置总览")],
        ),
        TableSpec(
            name="学生主档",
            description="学生主数据与联系方式。",
            primary_field=FieldSpec("姓名", "text", "学生姓名。"),
            fields=[
                FieldSpec("student_id", "text", "工作台内部稳定 ID。"),
                FieldSpec("学号", "text", "学校或班级学号。"),
                FieldSpec(
                    "性别",
                    "select",
                    "学生性别。",
                    multiple=False,
                    options=[
                        select_option("男", "Blue", "Light"),
                        select_option("女", "Pink", "Light"),
                        select_option("其他", "Grey", "Light"),
                    ],
                ),
                FieldSpec("班级", "text", "班级名。"),
                FieldSpec("学期", "select", "当前学期。", multiple=False, options=term_options),
                FieldSpec(
                    "选科",
                    "select",
                    "学生选科。",
                    multiple=True,
                    options=[
                        select_option("语文"),
                        select_option("数学"),
                        select_option("英语"),
                        select_option("物理"),
                        select_option("化学"),
                        select_option("生物"),
                        select_option("历史"),
                        select_option("地理"),
                        select_option("政治"),
                    ],
                ),
                FieldSpec("毕业学校", "text", "升学前学校。"),
                FieldSpec("宿舍楼", "text", "宿舍楼栋。"),
                FieldSpec("宿舍房间", "text", "宿舍房间号。"),
                FieldSpec("宿舍床位", "text", "宿舍床位号。"),
                FieldSpec("家长1姓名", "text", "主要家长联系人。"),
                FieldSpec(
                    "家长1关系",
                    "select",
                    "与学生关系。",
                    multiple=False,
                    options=[
                        select_option("父亲"),
                        select_option("母亲"),
                        select_option("监护人"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec("家长1电话", "text", "主要联系电话。"),
                FieldSpec("家长2姓名", "text", "备用家长联系人。"),
                FieldSpec(
                    "家长2关系",
                    "select",
                    "与学生关系。",
                    multiple=False,
                    options=[
                        select_option("父亲"),
                        select_option("母亲"),
                        select_option("监护人"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec("家长2电话", "text", "备用联系电话。"),
                FieldSpec("身份证号", "text", "敏感字段，只面向班主任。"),
                FieldSpec("住址", "text", "家庭住址。"),
                FieldSpec(
                    "重点关注标签",
                    "select",
                    "班主任关注标签。",
                    multiple=True,
                    options=[
                        select_option("学业"),
                        select_option("纪律"),
                        select_option("宿舍"),
                        select_option("家校沟通"),
                        select_option("心理支持"),
                    ],
                ),
                FieldSpec("备注", "text", "补充说明。"),
            ],
            views=[ViewSpec("学生总览"), ViewSpec("重点关注")],
        ),
        TableSpec(
            name="考试批次",
            description="考试或阶段测评的元信息。",
            primary_field=FieldSpec("考试名称", "text", "例如 2025 秋期末。"),
            fields=[
                FieldSpec("exam_id", "text", "考试批次稳定 ID。"),
                FieldSpec("考试日期", "datetime", "考试日期。"),
                FieldSpec("学期", "select", "所属学期。", multiple=False, options=term_options),
                FieldSpec(
                    "考试类型",
                    "select",
                    "考试批次类型。",
                    multiple=False,
                    options=[
                        select_option("小测"),
                        select_option("阶段考"),
                        select_option("期中"),
                        select_option("期末"),
                        select_option("模拟考"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec("学科范围", "text", "全科或单科描述。"),
                FieldSpec(
                    "来源",
                    "select",
                    "数据来源。",
                    multiple=False,
                    options=[
                        select_option("班主任录入"),
                        select_option("科任老师导入"),
                        select_option("系统导入"),
                    ],
                ),
                FieldSpec("备注", "text", "补充说明。"),
            ],
            views=[ViewSpec("考试批次")],
        ),
        TableSpec(
            name="成绩明细",
            description="长表成绩记录，一行表示一次考试中的单个学科成绩。",
            primary_field=FieldSpec("记录标题", "text", "建议由脚本生成：学生-考试-学科。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("exam_ref", "link", "关联考试批次。", link_table="考试批次"),
                FieldSpec(
                    "学科",
                    "select",
                    "单科成绩学科。",
                    multiple=False,
                    options=[
                        select_option("语文"),
                        select_option("数学"),
                        select_option("英语"),
                        select_option("物理"),
                        select_option("化学"),
                        select_option("生物"),
                        select_option("历史"),
                        select_option("地理"),
                        select_option("政治"),
                    ],
                ),
                FieldSpec("分数", "number", "原始分数。", precision=1),
                FieldSpec("班排", "number", "班级排名。", precision=0),
                FieldSpec("级排", "number", "年级排名。", precision=0),
                FieldSpec(
                    "等级",
                    "select",
                    "等级或档位。",
                    multiple=False,
                    options=[
                        select_option("A"),
                        select_option("B"),
                        select_option("C"),
                        select_option("D"),
                        select_option("未评级", "Grey", "Light"),
                    ],
                ),
                FieldSpec("缺考", "select", "是否缺考。", multiple=False, options=yes_no_options),
                FieldSpec("来源列名", "text", "迁移时保留原成绩列名。"),
                FieldSpec("备注", "text", "补充说明。"),
            ],
            views=[ViewSpec("全科成绩"), ViewSpec("成绩录入")],
        ),
        TableSpec(
            name="成长事件",
            description="统一事件流：学习、德育、纪律、值日、宿舍、表扬等。",
            primary_field=FieldSpec("事件标题", "text", "事件简述。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("事件日期", "datetime", "事件发生日期。"),
                FieldSpec(
                    "事件域",
                    "select",
                    "事件所属大类。",
                    multiple=False,
                    options=[
                        select_option("学习跟进"),
                        select_option("德育"),
                        select_option("纪律"),
                        select_option("值日"),
                        select_option("宿舍"),
                        select_option("表扬"),
                        select_option("班务"),
                    ],
                ),
                FieldSpec(
                    "事件类型",
                    "select",
                    "事件具体类型。",
                    multiple=False,
                    options=[
                        select_option("作业"),
                        select_option("课堂"),
                        select_option("迟到"),
                        select_option("缺勤"),
                        select_option("卫生"),
                        select_option("扣分"),
                        select_option("表扬"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec(
                    "评价",
                    "select",
                    "结果评价。",
                    multiple=False,
                    options=[
                        select_option("优秀", "Green", "Light"),
                        select_option("一般", "Blue", "Light"),
                        select_option("待改进", "Orange", "Light"),
                        select_option("严重", "Red", "Light"),
                    ],
                ),
                FieldSpec(
                    "标签",
                    "select",
                    "可多选标签。",
                    multiple=True,
                    options=[
                        select_option("作业"),
                        select_option("纪律"),
                        select_option("卫生"),
                        select_option("宿舍"),
                        select_option("课堂"),
                        select_option("家校"),
                    ],
                ),
                FieldSpec("分值变化", "number", "奖惩或评分增减。", precision=1),
                FieldSpec("描述", "text", "详细描述。"),
                FieldSpec("证据附件", "attachment", "相关截图或附件。"),
                FieldSpec("记录人", "text", "谁录入了这条记录。"),
                FieldSpec(
                    "状态",
                    "select",
                    "事件处理状态。",
                    multiple=False,
                    options=[
                        select_option("已记录"),
                        select_option("待跟进", "Orange", "Light"),
                        select_option("已完成", "Green", "Light"),
                    ],
                ),
            ],
            views=[ViewSpec("德育与班务"), ViewSpec("学习跟进"), ViewSpec("成长日历", "calendar")],
        ),
        TableSpec(
            name="家校沟通",
            description="家长电话、面谈、家访、微信等沟通记录。",
            primary_field=FieldSpec("沟通主题", "text", "沟通主题或标题。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("沟通日期", "datetime", "沟通日期。"),
                FieldSpec(
                    "沟通方式",
                    "select",
                    "沟通渠道。",
                    multiple=False,
                    options=[
                        select_option("电话"),
                        select_option("面谈"),
                        select_option("家访"),
                        select_option("微信"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec(
                    "沟通对象",
                    "select",
                    "沟通对象。",
                    multiple=False,
                    options=[
                        select_option("父亲"),
                        select_option("母亲"),
                        select_option("监护人"),
                        select_option("学生本人"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec("摘要", "text", "沟通要点。"),
                FieldSpec("后续动作", "text", "下一步跟进。"),
                FieldSpec(
                    "状态",
                    "select",
                    "处理状态。",
                    multiple=False,
                    options=[
                        select_option("已记录"),
                        select_option("待回访", "Orange", "Light"),
                        select_option("已完成", "Green", "Light"),
                    ],
                ),
                FieldSpec("附件", "attachment", "附件或记录文件。"),
            ],
            views=[ViewSpec("家校沟通"), ViewSpec("待回访")],
        ),
        TableSpec(
            name="座位安排",
            description="结构化座位表数据。",
            primary_field=FieldSpec("座位版本", "text", "座位调整版本。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("生效日期", "datetime", "生效时间。"),
                FieldSpec("行", "number", "座位行。", precision=0),
                FieldSpec("列", "number", "座位列。", precision=0),
                FieldSpec(
                    "区域",
                    "select",
                    "座位区域。",
                    multiple=False,
                    options=[
                        select_option("前排"),
                        select_option("中排"),
                        select_option("后排"),
                        select_option("窗边"),
                        select_option("过道"),
                    ],
                ),
                FieldSpec("同桌", "text", "同桌姓名或说明。"),
                FieldSpec("调整原因", "text", "调整理由。"),
                FieldSpec("当前有效", "select", "是否当前版本。", multiple=False, options=yes_no_options),
            ],
            views=[ViewSpec("当前座位"), ViewSpec("座位日历", "calendar")],
        ),
        TableSpec(
            name="值日安排",
            description="结构化值日表数据。",
            primary_field=FieldSpec("值日标题", "text", "如 2025-09-01 周值日。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("周期开始", "datetime", "值日周期开始。"),
                FieldSpec("周期结束", "datetime", "值日周期结束。"),
                FieldSpec(
                    "岗位",
                    "select",
                    "值日岗位。",
                    multiple=False,
                    options=[
                        select_option("讲台"),
                        select_option("黑板"),
                        select_option("扫地"),
                        select_option("功能室"),
                        select_option("机动"),
                        select_option("其他"),
                    ],
                ),
                FieldSpec("组别", "text", "值日组或任务组。"),
                FieldSpec("组长", "select", "是否组长。", multiple=False, options=yes_no_options),
                FieldSpec("替班说明", "text", "调班或请假说明。"),
                FieldSpec(
                    "补做状态",
                    "select",
                    "是否需要补做。",
                    multiple=False,
                    options=[
                        select_option("正常"),
                        select_option("需补做", "Orange", "Light"),
                        select_option("已补做", "Green", "Light"),
                    ],
                ),
            ],
            views=[ViewSpec("当前值日"), ViewSpec("值日日历", "calendar")],
        ),
        TableSpec(
            name="班委任命",
            description="班委岗位与任命信息。",
            primary_field=FieldSpec("岗位名称", "text", "班委岗位。"),
            fields=[
                FieldSpec("student_ref", "link", "关联学生主档。", link_table="学生主档"),
                FieldSpec("开始日期", "datetime", "任命开始。"),
                FieldSpec("结束日期", "datetime", "任命结束。"),
                FieldSpec("当前有效", "select", "是否当前有效。", multiple=False, options=yes_no_options),
                FieldSpec("备注", "text", "岗位说明。"),
            ],
            views=[ViewSpec("当前班委"), ViewSpec("班委历史")],
        ),
        TableSpec(
            name="产物索引",
            description="生成文件的元数据索引。",
            primary_field=FieldSpec("产物标题", "text", "生成文件名称。"),
            fields=[
                FieldSpec(
                    "产物类型",
                    "select",
                    "文件类型。",
                    multiple=False,
                    options=[
                        select_option("docx"),
                        select_option("xlsx"),
                        select_option("pptx"),
                    ],
                ),
                FieldSpec("关联对象", "text", "相关学生、考试或班级对象。"),
                FieldSpec("模板名称", "text", "使用的模板。"),
                FieldSpec("本地路径", "text", "本地生成路径。"),
                FieldSpec("飞书链接", "text", "同步后写回的飞书链接。"),
                FieldSpec("生成时间", "datetime", "生成时间。"),
                FieldSpec("同步状态", "select", "同步状态。", multiple=False, options=sync_status_options),
                FieldSpec("参数摘要", "text", "渲染参数摘要。"),
            ],
            views=[ViewSpec("产物归档"), ViewSpec("最近生成")],
        ),
    ]


def build_schema_manifest() -> Dict[str, Any]:
    tables = build_table_specs()
    return {
        "schema_version": SCHEMA_VERSION,
        "workspace_backend": ["feishu_base", "notion", "obsidian", "local_only"],
        "tables": [table.public_dict() for table in tables],
        "dashboards": [
            {"name": "班级总览", "theme_style": "default"},
            {"name": "全科成绩看板", "theme_style": "SimpleBlue"},
            {"name": "德育与班务看板", "theme_style": "DarkGreen"},
            {"name": "家校沟通看板", "theme_style": "summerBreeze"},
        ],
        "artifacts": {
            "docx": ["家访记录", "班级通知", "学生谈话记录"],
            "xlsx": ["座位表", "值日表", "班委表", "扣分汇总表"],
            "pptx": ["家长会PPT"],
        },
    }


def build_backend_projection(backend: str) -> Dict[str, Any]:
    manifest = build_schema_manifest()
    if backend == "feishu_base":
        return manifest
    if backend == "notion":
        return {
            "schema_version": SCHEMA_VERSION,
            "backend": "notion",
            "databases": [
                {"name": table["name"], "fields": [table["primary_field"]["name"]] + [field["name"] for field in table["fields"]]}
                for table in manifest["tables"]
            ],
            "limitations": [
                "v1 only guarantees mapping and minimal bootstrap guidance",
                "full runtime parity with Feishu is not implemented",
            ],
        }
    if backend == "obsidian":
        return {
            "schema_version": SCHEMA_VERSION,
            "backend": "obsidian",
            "folders": [
                "00-配置",
                "01-学生主档",
                "02-考试批次",
                "03-成绩明细",
                "04-成长事件",
                "05-家校沟通",
                "06-班级安排",
                "07-产物索引",
            ],
            "limitations": [
                "v1 provides folder and template guidance only",
                "full structured runtime behavior is not implemented",
            ],
        }
    return {
        "schema_version": SCHEMA_VERSION,
        "backend": "local_only",
        "notes": [
            "Use the unified semantic model locally.",
            "Defer backend selection until the user is ready.",
        ],
    }


def render_markdown(data: Dict[str, Any], backend: str) -> str:
    lines: List[str] = [f"# Headteacher Schema ({backend})", ""]
    lines.append(f"- Schema version: `{data.get('schema_version', SCHEMA_VERSION)}`")
    lines.append("")
    if backend == "feishu_base":
        lines.append("## Tables")
        lines.append("")
        for table in data["tables"]:
            lines.append(f"### {table['name']}")
            lines.append(table["description"])
            lines.append("")
            lines.append(f"- Primary field: `{table['primary_field']['name']}`")
            lines.append(f"- Fields: {', '.join(field['name'] for field in table['fields'])}")
            lines.append(f"- Views: {', '.join(view['name'] for view in table['views'])}")
            lines.append("")
        lines.append("## Dashboards")
        lines.append("")
        for dashboard in data["dashboards"]:
            lines.append(f"- {dashboard['name']} ({dashboard['theme_style']})")
    elif backend == "notion":
        lines.append("## Databases")
        lines.append("")
        for database in data["databases"]:
            lines.append(f"- {database['name']}: {', '.join(database['fields'])}")
        lines.append("")
        lines.append("## Limitations")
        lines.append("")
        for item in data["limitations"]:
            lines.append(f"- {item}")
    elif backend == "obsidian":
        lines.append("## Vault folders")
        lines.append("")
        for folder_name in data["folders"]:
            lines.append(f"- {folder_name}")
        lines.append("")
        lines.append("## Limitations")
        lines.append("")
        for item in data["limitations"]:
            lines.append(f"- {item}")
    else:
        lines.append("## Notes")
        lines.append("")
        for note in data["notes"]:
            lines.append(f"- {note}")
    return "\n".join(lines)


def render_summary(data: Dict[str, Any], backend: str) -> str:
    if backend == "feishu_base":
        table_names = ", ".join(table["name"] for table in data["tables"])
        return f"Feishu Base schema with {len(data['tables'])} tables: {table_names}"
    if backend == "notion":
        return f"Notion mapping with {len(data['databases'])} suggested databases"
    if backend == "obsidian":
        return f"Obsidian mapping with {len(data['folders'])} folders"
    return "Local-only schema plan"


def build_workspace_manifest_template(workspace_name: str, backend: str) -> Dict[str, Any]:
    return {
        "schema_version": SCHEMA_VERSION,
        "workspace_name": workspace_name,
        "workspace_backend": backend,
        "status": "draft",
        "capabilities": {
            "setup_complete": False,
            "artifact_generation": False,
            "remote_sync": backend == "feishu_base",
        },
        "tables": [],
        "dashboards": [],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Emit the unified headteacher schema manifest.")
    parser.add_argument(
        "--backend",
        default="feishu_base",
        choices=["feishu_base", "notion", "obsidian", "local_only"],
        help="Target backend projection.",
    )
    parser.add_argument(
        "--format",
        default="json",
        choices=["json", "markdown", "summary"],
        help="Output format.",
    )
    parser.add_argument("--workspace-name", default="班主任工作台", help="Workspace name for manifest templates.")
    parser.add_argument("--include-workspace-template", action="store_true", help="Include a workspace manifest template in JSON output.")
    args = parser.parse_args()

    data = build_backend_projection(args.backend)
    if args.include_workspace_template:
        data = dict(data)
        data["workspace_manifest_template"] = build_workspace_manifest_template(args.workspace_name, args.backend)

    if args.format == "json":
        print(json.dumps(data, ensure_ascii=False, indent=2))
    elif args.format == "markdown":
        print(render_markdown(data, args.backend))
    else:
        print(render_summary(data, args.backend))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: tools/setup_doctor.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import importlib
import json
import os
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional


WORKSPACE_MANIFEST = Path(".headteacher-skill/workspace_manifest.json")
OPENCLAW_PLUGIN_PATH = Path.home() / ".openclaw" / "extensions" / "openclaw-lark"


def parse_embedded_json(text: str) -> Optional[Dict[str, Any]]:
    match = re.search(r"\{[\s\S]*\}", text)
    if not match:
        return None
    try:
        return json.loads(match.group(0))
    except json.JSONDecodeError:
        return None


def run_command(cmd: list[str]) -> Dict[str, Any]:
    result = subprocess.run(cmd, capture_output=True, text=True)
    return {
        "returncode": result.returncode,
        "stdout": result.stdout.strip(),
        "stderr": result.stderr.strip(),
    }


def check_python_module(module_name: str) -> bool:
    try:
        importlib.import_module(module_name)
        return True
    except Exception:
        return False


def detect_agent_runtime() -> Dict[str, Any]:
    env = os.environ
    evidence: list[str] = []

    if env.get("OPENCLAW") or env.get("OPENCLAW_WORKSPACE") or env.get("OPENCLAW_AGENT_ID"):
        evidence.append("openclaw_env")
    if env.get("CODEX_THREAD_ID") or env.get("CODEX_INTERNAL_ORIGINATOR_OVERRIDE"):
        evidence.append("codex_env")
    if env.get("CLAUDECODE") or env.get("CLAUDE_CODE_ENTRYPOINT") or env.get("CLAUDE_PROJECT_DIR"):
        evidence.append("claude_code_env")

    cwd_parts = {part.lower() for part in Path.cwd().parts}
    if ".openclaw" in cwd_parts or "openclaw" in cwd_parts:
        evidence.append("openclaw_cwd")
    if ".claude" in cwd_parts or "claude" in cwd_parts:
        evidence.append("claude_cwd")

    if "openclaw_env" in evidence or "openclaw_cwd" in evidence:
        runtime = "openclaw"
    elif "codex_env" in evidence:
        runtime = "codex"
    elif "claude_code_env" in evidence or "claude_cwd" in evidence:
        runtime = "claude_code"
    else:
        runtime = "local_agent"

    return {
        "runtime": runtime,
        "evidence": evidence or ["no_known_agent_markers"],
    }


def check_openclaw_feishu_plugin() -> Dict[str, Any]:
    installed = OPENCLAW_PLUGIN_PATH.exists()
    return {
        "plugin_id": "openclaw-lark",
        "installed": installed,
        "install_path": str(OPENCLAW_PLUGIN_PATH),
        "status": "ready" if installed else "missing_plugin",
        "recommendation": (
            "Use the official OpenClaw Lark/Feishu plugin and Feishu Base API tools."
            if installed
            else "Install the official OpenClaw Lark/Feishu plugin (`openclaw-lark`) first."
        ),
    }


def resolve_feishu_access(runtime: Dict[str, Any], feishu_backend: Dict[str, Any]) -> Dict[str, Any]:
    if runtime["runtime"] == "openclaw":
        plugin = check_openclaw_feishu_plugin()
        return {
            "mode": "openclaw_plugin",
            "status": plugin["status"],
            "plugin": plugin,
            "recommendation": plugin["recommendation"],
        }

    status = "ready" if feishu_backend["installed"] and feishu_backend["configured"] else feishu_backend["status"]
    return {
        "mode": "lark_cli",
        "status": status,
        "binary": feishu_backend.get("binary"),
        "recommendation": (
            "Use `lark-cli` for Feishu bootstrap in Codex / Claude Code / local agent environments."
        ),
    }


def check_feishu_backend() -> Dict[str, Any]:
    path = shutil.which("lark-cli")
    if not path:
        return {
            "backend": "feishu_base",
            "installed": False,
            "configured": False,
            "status": "missing_cli",
            "recommendation": "Install lark-cli first.",
        }

    config = run_command(["lark-cli", "config", "show"])
    parsed = parse_embedded_json(config["stdout"])
    configured = bool(parsed and parsed.get("appId"))
    users = parsed.get("users") if parsed else None
    status = "ready" if configured else "needs_config"

    return {
        "backend": "feishu_base",
        "installed": True,
        "configured": configured,
        "status": status,
        "binary": path,
        "config_path": str(Path.home() / ".lark-cli" / "config.json"),
        "brand": parsed.get("brand") if parsed else None,
        "users": users,
        "recommendation": "Run `lark-cli config init --new`." if not configured else "Feishu backend is available.",
    }


def check_obsidian_backend() -> Dict[str, Any]:
    path = shutil.which("obsidian")
    return {
        "backend": "obsidian",
        "installed": bool(path),
        "status": "placeholder_ready" if path else "missing_cli",
        "binary": path,
        "recommendation": "Install the Obsidian CLI and the official Obsidian-related skill, then verify local access."
        if not path
        else "Obsidian CLI is installed; also verify the official Obsidian-related skill in the agent environment.",
    }


def check_notion_backend() -> Dict[str, Any]:
    return {
        "backend": "notion",
        "installed": None,
        "status": "manual_verification_required",
        "recommendation": "Verify that Notion MCP is connected and available in the current agent environment.",
    }


def check_artifact_stack() -> Dict[str, Any]:
    return {
        "python_docx": check_python_module("docx"),
        "openpyxl": check_python_module("openpyxl"),
        "python_pptx": check_python_module("pptx"),
    }


def build_report(focus_backend: Optional[str]) -> Dict[str, Any]:
    runtime = detect_agent_runtime()
    feishu_backend = check_feishu_backend()
    backends = {
        "feishu_base": feishu_backend,
        "notion": check_notion_backend(),
        "obsidian": check_obsidian_backend(),
        "local_only": {
            "backend": "local_only",
            "status": "always_available",
            "recommendation": "Use the local schema plan and defer backend setup.",
        },
    }
    if focus_backend:
        selected = {focus_backend: backends[focus_backend]}
    else:
        selected = backends

    recommended_backend = "feishu_base" if runtime["runtime"] == "openclaw" or feishu_backend["installed"] else "local_only"
    return {
        "workspace_manifest_exists": WORKSPACE_MANIFEST.exists(),
        "workspace_manifest_path": str(WORKSPACE_MANIFEST),
        "agent_runtime": runtime,
        "feishu_access": resolve_feishu_access(runtime, feishu_backend),
        "recommended_backend": recommended_backend,
        "artifact_stack": check_artifact_stack(),
        "backends": selected,
    }


def render_markdown(report: Dict[str, Any]) -> str:
    lines = ["# Setup Doctor", ""]
    lines.append(f"- Workspace manifest present: `{report['workspace_manifest_exists']}`")
    lines.append(f"- Workspace manifest path: `{report['workspace_manifest_path']}`")
    lines.append(f"- Agent runtime: `{report['agent_runtime']['runtime']}`")
    lines.append(f"- Runtime evidence: `{', '.join(report['agent_runtime']['evidence'])}`")
    lines.append(f"- Recommended Feishu access: `{report['feishu_access']['mode']}`")
    lines.append(f"- Recommended backend: `{report['recommended_backend']}`")
    lines.append("")
    lines.append("## Feishu access")
    lines.append("")
    lines.append(f"- mode: `{report['feishu_access']['mode']}`")
    lines.append(f"- status: `{report['feishu_access']['status']}`")
    if report["feishu_access"]["mode"] == "openclaw_plugin":
        plugin = report["feishu_access"]["plugin"]
        lines.append(f"- plugin_id: `{plugin['plugin_id']}`")
        lines.append(f"- plugin_installed: `{plugin['installed']}`")
        lines.append(f"- plugin_path: `{plugin['install_path']}`")
    elif report["feishu_access"].get("binary"):
        lines.append(f"- binary: `{report['feishu_access']['binary']}`")
    lines.append(f"- recommendation: {report['feishu_access']['recommendation']}")
    lines.append("")
    lines.append("## Artifact stack")
    lines.append("")
    for name, available in report["artifact_stack"].items():
        lines.append(f"- {name}: `{available}`")
    lines.append("")
    lines.append("## Backends")
    lines.append("")
    for backend_name, data in report["backends"].items():
        lines.append(f"### {backend_name}")
        for key in ["status", "installed", "configured", "binary", "users", "recommendation"]:
            if key in data and data[key] is not None:
                if key == "recommendation":
                    lines.append(f"- {key}: {data[key]}")
                else:
                    lines.append(f"- {key}: `{data[key]}`")
        lines.append("")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description="Check local prerequisites for the headteacher workbench.")
    parser.add_argument(
        "--backend",
        choices=["feishu_base", "notion", "obsidian", "local_only"],
        help="Focus on a single backend.",
    )
    parser.add_argument("--format", default="json", choices=["json", "markdown"], help="Output format.")
    args = parser.parse_args()

    report = build_report(args.backend)
    if args.format == "json":
        print(json.dumps(report, ensure_ascii=False, indent=2))
    else:
        print(render_markdown(report))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


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