Compare commits

..

4 Commits

Author SHA1 Message Date
cdf44afc5b 修复 MCP 清单项更新并完善中文部署文档
Some checks failed
Translate / translate (push) Has been cancelled
2026-09-04 11:22:02 +08:00
8f5bf806e1 修正中文看板术语翻译
Some checks failed
Translate / translate (push) Has been cancelled
2026-09-02 23:46:23 +08:00
0f57279c77 修复卡片右键菜单底部定位 2026-08-25 22:02:43 +08:00
ed5b8d1644 fix(mcp): expose board lists and labels 2026-08-24 20:51:22 +08:00
7 changed files with 350 additions and 341 deletions

490
README.md
View File

@@ -1,369 +1,315 @@
![github-background](https://github.com/user-attachments/assets/f728f52e-bf67-4357-9ba2-c24c437488e3)
# Kan
<div align="center">
<h3 align="center">Kan</h3>
<p>The open-source project management alternative to Trello.</p>
</div>
Kan 是一个开源的看板式项目管理工具,可作为 Trello 的自托管替代方案。它支持工作区、看板、列表、卡片、标签、成员、评论、检查清单和活动记录。
<p align="center">
<a href="https://kan.bn/kan/roadmap">Roadmap</a>
·
<a href="https://kan.bn">Website</a>
·
<a href="https://docs.kan.bn">Docs</a>
·
<a href="https://discord.gg/e6ejRb6CmT">Discord</a>
</p>
Kan 使用 AGPLv3 协议发布。
<div align="center">
<a href="https://github.com/kanbn/kan/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-purple"></a>
</div>
- [项目主页](https://kan.bn)
- [在线文档](https://docs.kan.bn)
- [路线图](https://kan.bn/kan/roadmap)
- [Discord 社区](https://discord.gg/e6ejRb6CmT)
## Features 💫
![Kan 截图](https://github.com/user-attachments/assets/8490104a-cd5d-49de-afc2-152fd8a93119)
- 👁️ **Board Visibility**: Control who can view and edit your boards
- 🤝 **Workspace Members**: Invite members and collaborate with your team
- 🚀 **Trello Imports**: Easily import your Trello boards
- 🔍 **Labels & Filters**: Organise and find cards quickly
- 💬 **Comments**: Discuss and collaborate with your team
- 📝 **Activity Log**: Track all card changes with detailed activity history
- 🎨 **Templates** : Save time with reusable custom board templates
- ⚡️ **Integrations (coming soon)** : Connect your favourite tools
## 功能
See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features.
- 看板可见性控制
- 工作区成员和协作
- Trello 看板导入
- 卡片标签和筛选
- 评论、活动记录和检查清单
- 可复用的看板模板
- 卡片截止日期、成员和附件
- 内置 REST API 和 MCP 服务
## Screenshot 👁️
## 使用 Docker Compose 自托管
<img width="1507" alt="hero-dark" src="https://github.com/user-attachments/assets/8490104a-cd5d-49de-afc2-152fd8a93119" />
### 环境要求
## Made With 🛠️
- Docker Engine
- Docker Compose v2
- [Next.js](https://nextjs.org/?ref=kan.bn)
- [tRPC](https://trpc.io/?ref=kan.bn)
- [Better Auth](https://better-auth.com/?ref=kan.bn)
- [Tailwind CSS](https://tailwindcss.com/?ref=kan.bn)
- [Drizzle ORM](https://orm.drizzle.team/?ref=kan.bn)
- [React Email](https://react.email/?ref=kan.bn)
### 启动
## Self Hosting 🐳
在仓库根目录执行:
### One-click Deployments
```bash
cp .env.example .env
```
The easiest way to deploy Kan is through Railway. We've partnered with Railway to maintain an official template that supports the development of the project.
编辑 `.env`,至少设置以下变量:
<a href="https://railway.com/deploy/kan?referralCode=bZPsr2&utm_medium=integration&utm_source=template&utm_campaign=generic">
<img src="https://railway.app/button.svg" alt="Deploy on Railway" height="40" />
</a>
```dotenv
NEXT_PUBLIC_BASE_URL=http://localhost:3000
POSTGRES_PASSWORD=请填写数据库密码
POSTGRES_URL=postgresql://kan:请填写数据库密码@postgres:5432/kan_db
BETTER_AUTH_SECRET=请填写一个随机的 32 位以上密钥
```
### Docker Compose
构建本地镜像并启动:
Alternatively, you can self-host Kan with Docker Compose. This will set up everything for you including your postgres database and automatically run migrations.
```bash
docker compose up -d --build
```
1. Create a `.env` file with your environment variables (see [Environment Variables](#environment-variables-) section below)
2. Use the provided `docker-compose.yml` file or create your own with the following configuration:
仓库自带的 `docker-compose.yml` 使用本地镜像标签:
```yaml
services:
migrate:
image: ghcr.io/kanbn/kan-migrate:latest
container_name: kan-migrate
networks:
- kan-network
environment:
- POSTGRES_URL=${POSTGRES_URL}
depends_on:
postgres:
condition: service_healthy
restart: "no"
image: kan-migrate:local
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: migrate
web:
image: ghcr.io/kanbn/kan:latest
container_name: kan-web
ports:
- "${WEB_PORT:-3000}:3000"
networks:
- kan-network
env_file:
- .env
environment:
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- POSTGRES_URL=${POSTGRES_URL}
- NEXT_PUBLIC_ALLOW_CREDENTIALS=true
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
postgres:
image: postgres:15
container_name: kan-db
environment:
- POSTGRES_DB=kan_db
- POSTGRES_USER=kan
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- 5432:5432
volumes:
- kan_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kan -d kan_db"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks:
- kan-network
networks:
kan-network:
volumes:
kan_postgres_data:
image: kan:local
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: web
```
3. Start the containers in detached mode:
不会拉取远端 Web 镜像。数据库迁移会在 Web 服务启动前自动执行。启动完成后访问 <http://localhost:3000>。如修改端口,请同步修改 `WEB_PORT``NEXT_PUBLIC_BASE_URL`
### 常用命令
```bash
docker compose up -d
# 查看状态
docker compose ps
# 查看 Web 日志
docker compose logs -f web
# 查看迁移日志
docker compose logs -f migrate
# 停止服务
docker compose down
# 停止服务并删除数据库卷(会删除本地数据)
docker compose down -v
```
The `migrate` service will automatically run database migrations before the web service starts. The application will be available at http://localhost:3000 (or the port specified in `WEB_PORT`).
检查 API 健康状态:
**Managing containers:**
```bash
curl http://localhost:3000/api/v1/health
```
- To stop the containers: `docker compose down`
- To view logs: `docker compose logs -f`
- To view logs for a specific service: `docker compose logs -f web` or `docker compose logs -f migrate`
- To restart the containers: `docker compose restart`
- To rebuild after code changes: `docker compose up -d --build`
预期返回:
For the complete Docker Compose configuration with all optional features, see [docker-compose.yml](./docker-compose.yml) in the repository.
```json
{ "status": "ok", "database": "ok", "storage": "not_configured" }
```
## Local Development 🧑‍💻
默认文件存储状态为 `not_configured`。如需上传头像和附件,请按照 `.env.example` 配置 S3 兼容存储。
1. Clone the repository (or fork)
## 本地开发
项目是 pnpm monorepo需要 Node.js 20.18.1 或更高版本,以及 pnpm 9.14.2。
```bash
git clone https://github.com/kanbn/kan.git
```
2. Install dependencies
```bash
cd kan
pnpm install
```
3. Copy `.env.example` to `.env` and configure your environment variables
4. Migrate database
```bash
cp .env.example .env
pnpm db:migrate
```
5. Start the development server
```bash
pnpm dev
```
## Environment Variables 🔐
开发服务器默认地址为 <http://localhost:3000>。常用检查命令:
| Variable | Description | Required | Example |
| ----------------------------------------- | --------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------- |
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
| `REDIS_URL` | Redis connection URL | For rate limiting (optional) | `redis://localhost:6379` or `redis://redis:6379` (Docker) |
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
| `SMTP_PORT` | SMTP server port | For Email | `465` |
| `SMTP_USER` | SMTP username/email | No | `resend` |
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` |
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` |
| `SMTP_REJECT_UNAUTHORIZED` | Reject invalid certificates (defaults to true if not set) | For Email | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL` | To disable all email features | For Email | `true` |
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
| `NEXT_API_BODY_SIZE_LIMIT` | Maximum API request body size (defaults to 1mb) | No | `50mb` |
| `BETTER_AUTH_ALLOWED_DOMAINS` | Comma-separated list of allowed domains for OIDC logins | For OIDC/Social login | `example.com,subsidiary.com` |
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
| `OIDC_CLIENT_ID` | Generic OIDC client ID | For OIDC login | `xxx` |
| `OIDC_CLIENT_SECRET` | Generic OIDC client secret | For OIDC login | `xxx` |
| `OIDC_DISCOVERY_URL` | OIDC discovery URL | For OIDC login | `https://auth.example.com/.well-known/openid-configuration` |
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads (optional with IRSA) | `xxx` |
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads (optional with IRSA) | `xxx` |
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
| `S3_AVATAR_UPLOAD_LIMIT` | Maximum avatar file size in bytes | For file uploads | `2097152` (2MB) |
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
| `NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS` | Use virtual-hosted style URLs (bucket.domain.com) | For file uploads (optional) | `true` |
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
| `NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME` | S3 bucket name for attachments | For file uploads | `attachments` |
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
| `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `true` |
| `KAN_ADMIN_API_KEY` | Admin API key for stats and admin endpoints | For admin/monitoring | `your-secret-admin-key` |
| `LOG_LEVEL` | Log verbosity level (debug, info, warn, error) | No (defaults to debug in dev, info in prod) | `info` |
```bash
pnpm typecheck
pnpm lint
pnpm format
```
See `.env.example` for a complete list of supported environment variables.
## 环境变量
## MCP Server (AI Control) 🤖
完整变量列表见 [`.env.example`](.env.example)。
Kan ships with a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets any MCP-compatible AI client — GitHub Copilot, Claude Desktop, Cursor, Codex, and others — read and control your Kan instance using natural language.
| 变量 | 用途 | 示例 |
| ------------------------------- | ---------------------------------- | -------------------------------------------- |
| `NEXT_PUBLIC_BASE_URL` | 当前 Kan 实例的访问地址 | `http://localhost:3000` |
| `POSTGRES_URL` | PostgreSQL 连接地址 | `postgresql://kan:密码@postgres:5432/kan_db` |
| `POSTGRES_PASSWORD` | Compose 创建 PostgreSQL 使用的密码 | `change-me` |
| `BETTER_AUTH_SECRET` | 登录会话加密密钥 | 随机 32 位以上字符串 |
| `WEB_PORT` | 宿主机映射端口 | `3000` |
| `REDIS_URL` | 可选的限流 Redis 地址 | `redis://redis:6379` |
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | 是否允许账号密码登录 | `true` |
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | 是否关闭注册 | `false` |
| `KAN_ADMIN_API_KEY` | 管理和监控接口密钥 | 自定义密钥 |
### Prerequisites
## MCP 服务
- Node.js 18+
- A running Kan instance (self-hosted or cloud)
- A Kan API key (Settings → API Keys → Create key)
Kan 仓库内置了基于 Model Context Protocol 的 MCP 服务。连接后Claude Desktop、Cursor、VS Code、Codex 等 MCP 客户端可以通过自然语言读取和管理 Kan 数据。
### Installation
### 先看这里
You do **not** need to clone this repository. The recommended way is to use `npx`, which runs the server on-demand and always uses the latest version — no global install required:
MCP 源码位于 `packages/mcp`,目前是仓库内的工作区包,并未发布为 npm 公共包。因此以下命令不能使用:
```bash
npx -y @kan/mcp
bunx kan-mcp
```
Alternatively, install it globally:
请从当前仓库构建 MCP 服务,再让客户端启动生成的文件。
### 获取 API 密钥
1. 登录 Kan。
2. 打开设置中的 API 密钥页面,地址通常为 `/settings/api`
3. 创建一个 API 密钥并立即保存。密钥只会完整显示一次。
MCP 使用用户 API 密钥,不是 `KAN_ADMIN_API_KEY`。请求会以 Bearer Token 方式访问 `${KAN_BASE_URL}/api/v1`
### 构建
在仓库根目录执行:
```bash
npm install -g @kan/mcp
kan-mcp
pnpm install
pnpm --filter @kan/mcp build
```
### Configuration
构建产物为 `packages/mcp/dist/index.js`。MCP 服务使用 stdio 通信,应由 MCP 客户端启动,不需要单独暴露 HTTP 端口。
The server is configured via two environment variables:
### 环境变量
| Variable | Description | Example |
| --------------- | ----------------------------------- | ------------------------------ |
| `KAN_BASE_URL` | Base URL of your Kan instance | `https://your-kan.example.com` |
| `KAN_API_TOKEN` | API key from your Kan user settings | `kan_xxxxxxxxxxxx` |
| 变量 | 说明 |
| --------------- | ---------------------------------- |
| `KAN_BASE_URL` | Kan 实例根地址,不要追加 `/api/v1` |
| `KAN_API_TOKEN` | Kan 设置中创建的用户 API 密钥 |
#### GitHub Copilot (VS Code)
本地 Kan 使用 Docker Compose 时:
Add the following to your VS Code `mcp.json` (open it via **MCP: Open User MCP Configuration** from the Command Palette):
```text
KAN_BASE_URL=http://localhost:3000
KAN_API_TOKEN=kan_你的_api_key
```
### Claude Desktop 和 Cursor
Claude Desktop 使用 `claude_desktop_config.json`Cursor 使用项目内的 `.cursor/mcp.json`。将 `args` 中的路径替换为本机仓库的绝对路径:
```json
{
"mcpServers": {
"kan": {
"command": "node",
"args": ["/绝对路径/kan/packages/mcp/dist/index.js"],
"env": {
"KAN_BASE_URL": "http://localhost:3000",
"KAN_API_TOKEN": "kan_你的_api_key"
}
}
}
}
```
Windows 路径示例:
```json
{
"mcpServers": {
"kan": {
"command": "node",
"args": ["C:\\workspace\\kan\\packages\\mcp\\dist\\index.js"],
"env": {
"KAN_BASE_URL": "http://localhost:3000",
"KAN_API_TOKEN": "kan_你的_api_key"
}
}
}
}
```
VS Code 的 `mcp.json` 使用 `servers` 作为顶层键,并增加 `type`
```json
{
"servers": {
"kan": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@kan/mcp"],
"command": "node",
"args": ["/绝对路径/kan/packages/mcp/dist/index.js"],
"env": {
"KAN_BASE_URL": "https://your-kan-instance.com",
"KAN_API_TOKEN": "kan_your_api_key_here"
"KAN_BASE_URL": "http://localhost:3000",
"KAN_API_TOKEN": "kan_你的_api_key"
}
}
}
}
```
Then use Copilot in **Agent mode** to interact with Kan.
### Codex
#### Claude Desktop
Codex 可通过命令添加 stdio MCP 服务。将脚本路径替换为本机绝对路径:
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"kan": {
"command": "npx",
"args": ["-y", "@kan/mcp"],
"env": {
"KAN_BASE_URL": "https://your-kan-instance.com",
"KAN_API_TOKEN": "kan_your_api_key_here"
}
}
}
}
```bash
codex mcp add kan \
--env KAN_BASE_URL=http://localhost:3000 \
--env KAN_API_TOKEN=kan_你的_api_key \
-- node /绝对路径/kan/packages/mcp/dist/index.js
```
#### Cursor / Codex / other clients
Windows PowerShell
Use the same `command` + `args` + `env` pattern above — all MCP stdio clients follow the same format.
```powershell
codex mcp add kan `
--env KAN_BASE_URL=http://localhost:3000 `
--env KAN_API_TOKEN=kan_你的_api_key `
-- node C:\workspace\kan\packages\mcp\dist\index.js
```
### Example prompts
使用 `codex mcp get kan` 检查配置。
Once connected, you can ask your AI assistant things like:
### 手动启动和排错
**Browsing**
Linux 或 macOS
- _"List all my workspaces"_
- _"Show me all boards in the Marketing workspace"_
- _"What cards are in the Backlog list of the Q3 Planning board?"_
- _"Get the full details of card X including comments and checklists"_
```bash
KAN_BASE_URL=http://localhost:3000 \
KAN_API_TOKEN=kan_你的_api_key \
node packages/mcp/dist/index.js
```
**Managing cards**
PowerShell
- _"Create a card called 'Fix login bug' in the To Do list of the Dev board"_
- _"Move the 'API redesign' card to the In Progress list"_
- _"Set a due date of next Friday on the 'Write docs' card"_
- _"Add a comment to the 'Deploy to prod' card saying the deployment is blocked"_
- _"Duplicate the 'Sprint template' card into the new Sprint 4 list"_
- _"Mark the 'Setup CI' checklist item as complete"_
```powershell
$env:KAN_BASE_URL = "http://localhost:3000"
$env:KAN_API_TOKEN = "kan_你的_api_key"
node .\packages\mcp\dist\index.js
```
**Organisation**
如果出现 `Connection closed`,检查以下内容:
- _"Add the 'urgent' label to all cards assigned to me in the Backend board"_
- _"Create a 'Release checklist' checklist on the v2.0 card with items: smoke test, update changelog, tag release"_
- _"What tasks are assigned to @alice in the Mechanics Rework board?"_
1. 已执行 `pnpm --filter @kan/mcp build`
2. `args` 指向真实存在的 `packages/mcp/dist/index.js`
3. `KAN_BASE_URL` 只填写实例根地址。
4. `KAN_API_TOKEN` 是 Kan 用户 API 密钥。
5. `http://localhost:3000/api/v1/health` 返回数据库正常。
**Workspace management**
### 工具范围
- _"Create a new workspace called 'Client Projects'"_
- _"Invite bob@example.com to the Marketing workspace as a member"_
- _"Create a new board called 'Sprint 5' in the Dev workspace with lists: Backlog, In Progress, Done"_
- _"Search for anything related to 'authentication' across the Dev workspace"_
当前 MCP 服务提供 46 个工具,覆盖工作区、看板、列表、卡片、评论、标签、检查清单和成员管理。连接后可以直接提出以下请求:
### Available tools
- 列出我的工作区。
- 查看研发工作区中的所有看板。
- 创建一个包含待办、进行中、已完成列表的新看板。
- 将登录问题卡片移动到进行中列表。
- 给版本发布卡片添加检查清单和评论。
The MCP server exposes 46 tools across 7 resource types:
## 技术栈
| Resource | Tools |
| ----------------- | ------------------------------------------------------------------- |
| Workspaces | list, find by name, get, create, update, delete, search, check slug |
| Boards | list, find by name, get, get by slug, create, update, delete |
| Lists | create, update, delete |
| Cards | create, get, update, delete, duplicate, get activities |
| Card interactions | add/update/delete comment, toggle label, toggle member |
| Checklists | create, update, delete, create item, update item, delete item |
| Labels | get, create, update, delete |
| Members | invite, remove, update role, manage invite links |
Next.js、React、tRPC、Better Auth、Tailwind CSS、Drizzle ORM、PostgreSQL 和 Model Context Protocol SDK。
## Contributing 🤝
## 参与贡献
We welcome contributions! Please read our [contribution guidelines](CONTRIBUTING.md) before submitting a pull request.
欢迎提交 Issue 和 Pull Request。提交代码前请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)并运行类型检查、Lint 和格式检查。
## Contributors 👥
## 许可证与联系
<a href="https://github.com/kanbn/kan/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kanbn/kan" />
</a>
## Sponsors ❤️
[<img height="100" alt="image" src="https://github.com/user-attachments/assets/e331c71f-ac86-46a6-bceb-ce276de094b0" />](https://www.testmuai.com)
Proudly sponsored by [TestMu AI (formerly LambdaTest)](https://www.testmuai.com) - an AI-native testing cloud platform built for modern engineering teams. Covering everything from autonomous test creation and fast execution to testing AI agents like chatbots and voice assistants. If you're serious about testing, go check them out.
## License 📝
Kan is licensed under the [AGPLv3 license](LICENSE).
## Contact 📧
For support or to get in touch, please email [henry@kan.bn](mailto:henry@kan.bn) or join our [Discord server](https://discord.gg/e6ejRb6CmT).
项目使用 [AGPLv3](LICENSE) 协议。如需支持,请发送邮件至 [henry@kan.bn](mailto:henry@kan.bn),或加入 [Discord 社区](https://discord.gg/e6ejRb6CmT)。

View File

@@ -1127,7 +1127,7 @@
115
]
],
"translation": "档案委员会"
"translation": "归档看板"
},
"TdfEV7": {
"message": "Archived",
@@ -1552,7 +1552,7 @@
125
]
],
"translation": "董事会"
"translation": "看板"
},
"YhvWXb": {
"message": "Board analytics",
@@ -1564,7 +1564,7 @@
85
]
],
"translation": "板分析"
"translation": "板分析"
},
"i0ZMQl": {
"message": "Board archived",
@@ -1576,7 +1576,7 @@
47
]
],
"translation": "委员会存档"
"translation": "看板已归档"
},
"wE3hGS": {
"message": "Board moved",
@@ -1588,7 +1588,7 @@
33
]
],
"translation": "移动"
"translation": "看板已移动"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
@@ -1600,7 +1600,7 @@
23
]
],
"translation": "板名称不能超过100个字符"
"translation": "板名称不能超过 100 个字符"
},
"R1c3Mq": {
"message": "Board name is required",
@@ -1612,7 +1612,7 @@
22
]
],
"translation": "董事会名称是必需的"
"translation": "看板名称是必需的"
},
"jwI32v": {
"message": "Board not found",
@@ -1628,7 +1628,7 @@
181
]
],
"translation": "找不到板"
"translation": "找不到板"
},
"XNxYxq": {
"message": "Board templates",
@@ -1640,7 +1640,7 @@
116
]
],
"translation": "板模板"
"translation": "板模板"
},
"tbNcu4": {
"message": "Board unarchived",
@@ -1652,7 +1652,7 @@
47
]
],
"translation": "董事会从未归档"
"translation": "看板已取消归档"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
@@ -1664,7 +1664,7 @@
46
]
],
"translation": "“单板URL只能包含字母、数字和连字符"
"translation": "看板 URL 只能包含字母、数字和连字符"
},
"gv5kbo": {
"message": "Board URL cannot exceed 60 characters",
@@ -1676,7 +1676,7 @@
44
]
],
"translation": "“单板URL不能超过60个字符"
"translation": "看板 URL 不能超过 60 个字符"
},
"8+BY11": {
"message": "Board URL copied to clipboard",
@@ -1720,7 +1720,7 @@
132
]
],
"translation": "董事会的可见性"
"translation": "看板可见性"
},
"PTzsxH": {
"message": "Board visibility updated",
@@ -1732,7 +1732,7 @@
50
]
],
"translation": "董事会可见性更新"
"translation": "看板可见性更新"
},
"DaBhdY": {
"translation": "看板",
@@ -1844,7 +1844,7 @@
20
]
],
"translation": "会制作板子"
"translation": "可以创建看板"
},
"6zeSBB": {
"message": "Can create cards",
@@ -1892,7 +1892,7 @@
22
]
],
"translation": "可以删除板"
"translation": "可以删除板"
},
"jNuzV/": {
"message": "Can delete cards",
@@ -1972,7 +1972,7 @@
21
]
],
"translation": "会编辑版面"
"translation": "可以编辑看板"
},
"I+AROe": {
"message": "Can edit cards",
@@ -2116,7 +2116,7 @@
19
]
],
"translation": "可以查看板"
"translation": "可以查看板"
},
"zoEbXq": {
"message": "Can view cards",
@@ -2893,7 +2893,7 @@
149
]
],
"translation": "Trello帐户连接到导入板。"
"translation": "连接您的 Trello 账户以导入板。"
},
"jfC/xh": {
"message": "Contact",
@@ -3009,7 +3009,7 @@
86
]
],
"translation": "控制谁可以查看和编辑您的板。"
"translation": "控制谁可以查看和编辑您的板。"
},
"zQPhSa": {
"message": "Convert to link",
@@ -3033,7 +3033,7 @@
87
]
],
"translation": "板链接"
"translation": "复制看板链接"
},
"F0YmUY": {
"message": "Copy card link",
@@ -3447,7 +3447,7 @@
38
]
],
"translation": "自定义板模板"
"translation": "自定义板模板"
},
"A42Dqn": {
"message": "Custom branding",
@@ -3704,7 +3704,7 @@
148
]
],
"translation": "删除板"
"translation": "删除板"
},
"nabda1": {
"message": "Delete card",
@@ -4245,7 +4245,7 @@
116
]
],
"translation": "编辑URL"
"translation": "编辑看板 URL"
},
"rF8SEQ": {
"message": "Edit comment",
@@ -4838,7 +4838,7 @@
33
]
],
"translation": "你需要的一切,永远免费。无限的棋盘,无限的列表,无限的牌。随时升级。"
"translation": "你需要的一切,永远免费。无限的看板、列表和卡片。随时升级。"
},
"t+R8+P": {
"message": "Execution",
@@ -4902,7 +4902,7 @@
94
]
],
"translation": "创建板失败"
"translation": "创建板失败"
},
"o4sQAg": {
"message": "Failed to create webhook",
@@ -5531,7 +5531,7 @@
44
]
],
"translation": "到董事会"
"translation": "前往看板"
},
"XEoGkQ": {
"message": "Go to homepage",
@@ -5707,7 +5707,7 @@
46
]
],
"translation": "如何导入我的Trello板"
"translation": "如何导入我的 Trello板?"
},
"nol/Fb": {
"message": "How do I invite team members?",
@@ -5871,7 +5871,7 @@
96
]
],
"translation": "导入您的Trello板并开始运行。"
"translation": "导入您的 Trello 看板,立即开始使用。"
},
"c/IAuR": {
"message": "Important",
@@ -5907,7 +5907,7 @@
49
]
],
"translation": "将Trello板导入Kan很容易。您可以按照我们的步指<0>here</0>。"
"translation": "将 Trello板导入 Kan 很容易。您可以按照我们的步指<0>此处</0>操作。"
},
"02i3WU": {
"message": "Imports",
@@ -6881,7 +6881,7 @@
107
]
],
"translation": "移动板"
"translation": "移动板"
},
"VvCMyU": {
"message": "Move board to another workspace",
@@ -6893,7 +6893,7 @@
64
]
],
"translation": "把板子移到另一个工作区"
"translation": "将看板移动到另一个工作区"
},
"51UCsN": {
"message": "Move to another list",
@@ -7314,7 +7314,7 @@
432
]
],
"translation": "未发现板子"
"translation": "未找到看板"
},
"OpNhRn": {
"message": "No credit card required",
@@ -9013,7 +9013,7 @@
116
]
],
"translation": "使用可重复使用的板模板节省时间。"
"translation": "使用可用的板模板节省时间。"
},
"cOh+MI": {
"message": "Screening",
@@ -9037,7 +9037,7 @@
105
]
],
"translation": "搜索板和卡片……"
"translation": "搜索板和卡片……"
},
"66Jmqh": {
"message": "Seat limit reached",
@@ -10173,7 +10173,7 @@
49
]
],
"translation": "董事会已存档。"
"translation": "看板已归档。"
},
"ZWk38w": {
"message": "The board has been moved to {0}.",
@@ -10189,7 +10189,7 @@
34
]
],
"translation": "板已移至{0}。"
"translation": "板已移至{0}。"
},
"C6gv54": {
"message": "The board has been unarchived.",
@@ -10201,7 +10201,7 @@
50
]
],
"translation": "这块板没有存档。"
"translation": "看板已取消归档。"
},
"nKBUeF": {
"message": "The card has been duplicated.",
@@ -10237,7 +10237,7 @@
33
]
],
"translation": "KanTrello的主要区别在于Kan是开源的任何人都可以查看、修改和贡献我们的代码。我们的云产品对个人使用的功能也没有限制而Trello锁定了基本功能,比如你可以在付费墙后面创建的电路板数量。"
"translation": "KanTrello 的主要区别在于Kan 是开源的,任何人都可以查看、修改和贡献代码。我们的云服务对个人使用的功能也不设限制,而 Trello 会将一些基本功能(例如可创建的板数量)设置为付费项目。"
},
"6tDFBe": {
"message": "The member's permissions have been updated.",
@@ -10305,7 +10305,7 @@
50
]
],
"translation": "您的板可见性已设置为{0}。",
"translation": "您的板可见性已设置为{0}。",
"obsolete": true
},
"7ENRJu": {
@@ -10410,7 +10410,7 @@
184
]
],
"translation": "此板是私有的或不存在"
"translation": "此板是私有的或不存在"
},
"wY+6Ye": {
"message": "This board URL has already been taken",
@@ -10970,7 +10970,7 @@
45
]
],
"translation": "无法移动板"
"translation": "无法移动板"
},
"K7k9u3": {
"message": "Unable to move card",
@@ -11070,7 +11070,7 @@
68
]
],
"translation": "无法更新板"
"translation": "无法更新板"
},
"XpcjLO": {
"message": "Unable to update board URL",
@@ -11082,7 +11082,7 @@
72
]
],
"translation": "无法更新URL"
"translation": "无法更新看板 URL"
},
"Wy6pcF": {
"message": "Unable to update board visibility",
@@ -11094,7 +11094,7 @@
57
]
],
"translation": "无法更新电路板可见性"
"translation": "无法更新板可见性"
},
"o9WLwO": {
"message": "Unable to update card",
@@ -11250,7 +11250,7 @@
115
]
],
"translation": "Unarchive董事会"
"translation": "取消归档看板"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
@@ -11334,7 +11334,7 @@
36
]
],
"translation": "无限的董事会"
"translation": "无限的看板"
},
"ZU5IYI": {
"message": "Unlimited boards, unlimited lists, unlimited cards. No credit card required.",
@@ -11346,7 +11346,7 @@
57
]
],
"translation": "无限的棋盘,无限的列表,无限的牌。不需要信用卡。"
"translation": "无限的看板、列表和卡片。不需要信用卡。"
},
"Zz1qkk": {
"message": "Unlimited cards",
@@ -12529,7 +12529,7 @@
68
]
],
"translation": "没有任何其他工作区来移动这个板。"
"translation": "没有其他工作区可供移动此看板。"
},
"h2FKMV": {
"message": "You don't have permission",
@@ -12731,7 +12731,7 @@
382
]
],
"translation": "您的看板已导入。"
"translation": "您的看板已导入。"
},
"LqWW5F": {
"message": "Your display name has been updated.",

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,17 @@
import { t } from "@lingui/core/macro";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import {
HiLink,
HiOutlineArrowRightCircle,
HiOutlineCalendar,
HiOutlineDocumentDuplicate,
HiOutlineTag,
HiOutlineTrash,
HiOutlineUserGroup,
HiOutlineArrowRightCircle,
} from "react-icons/hi2";
import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect";
export type CardContextMenuAction =
| "members"
| "move"
@@ -85,6 +87,55 @@ export function CardContextMenu({
canEdit,
}: CardContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x, y });
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
useIsomorphicLayoutEffect(() => {
const menu = menuRef.current;
if (!menu) return;
const gutter = 8;
const updatePosition = () => {
const { width, height } = menu.getBoundingClientRect();
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
const viewportHeight =
window.visualViewport?.height ?? window.innerHeight;
const maxX = Math.max(gutter, viewportWidth - width - gutter);
const maxY = Math.max(gutter, viewportHeight - height - gutter);
const nextPosition = {
x: Math.min(Math.max(x, gutter), maxX),
y: Math.min(Math.max(y, gutter), maxY),
};
setPosition((currentPosition) =>
currentPosition.x === nextPosition.x &&
currentPosition.y === nextPosition.y
? currentPosition
: nextPosition,
);
};
updatePosition();
const resizeObserver =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(updatePosition)
: null;
resizeObserver?.observe(menu);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
window.visualViewport?.addEventListener("resize", updatePosition);
window.visualViewport?.addEventListener("scroll", updatePosition);
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
window.visualViewport?.removeEventListener("resize", updatePosition);
window.visualViewport?.removeEventListener("scroll", updatePosition);
};
}, [x, y, canEdit, items.length]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -103,13 +154,11 @@ export function CardContextMenu({
};
}, [onClose]);
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
return (
<div
ref={menuRef}
className="fixed z-[200] min-w-[200px] rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: x, top: y }}
className="fixed z-[200] max-h-[calc(100dvh-1rem)] min-w-[min(200px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] overflow-y-auto overflow-x-hidden rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: position.x, top: position.y }}
>
{items.map(({ action, label, icon }) => (
<button
@@ -119,7 +168,7 @@ export function CardContextMenu({
onAction(action);
onClose();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
className="flex w-full items-center gap-2 break-words px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
{icon}
{label}

View File

@@ -1,6 +1,6 @@
services:
migrate:
image: ghcr.io/kanbn/kan-migrate:latest
image: kan-migrate:local
container_name: ${CONTAINER_NAME:-kan-migrate}
networks:
- kan-network
@@ -16,7 +16,7 @@ services:
restart: "no"
web:
image: ghcr.io/kanbn/kan:latest
image: kan:local
container_name: ${CONTAINER_NAME:-kan-web}
ports:
- "${WEB_PORT:-3000}:3000"

View File

@@ -91,21 +91,35 @@ export function registerBoardTools(server: McpServer): void {
server.tool(
"create_board",
"Create a new board in a workspace",
"Create a new board in a workspace, optionally with initial lists and labels",
{
workspacePublicId: z.string().describe("The workspace's public ID"),
name: z.string().describe("Board name"),
slug: z.string().optional().describe("URL-friendly slug (auto-generated if omitted)"),
visibility: z
.enum(["public", "private"])
lists: z
.array(z.string().min(1))
.default([])
.describe("Initial list names, for example [\"Backlog\", \"In Progress\", \"Done\"]"),
labels: z
.array(z.string().min(1))
.default([])
.describe("Initial label names, for example [\"Bug\", \"Feature\"]"),
type: z
.enum(["regular", "template"])
.optional()
.describe("Board visibility (default: private)"),
.describe("Board type (defaults to regular)"),
sourceBoardPublicId: z
.string()
.min(12)
.optional()
.describe("Source board public ID when cloning an existing board"),
},
async ({ workspacePublicId, name, slug, visibility }) => {
async ({ workspacePublicId, name, lists, labels, type, sourceBoardPublicId }) => {
const data = await kanRequest("POST", `/workspaces/${workspacePublicId}/boards`, {
name,
slug,
visibility,
lists,
labels,
type,
sourceBoardPublicId,
});
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},

View File

@@ -64,7 +64,7 @@ export function registerChecklistTools(server: McpServer): void {
async ({ checklistItemPublicId, title, isCompleted, index }) => {
const data = await kanRequest("PATCH", `/checklists/items/${checklistItemPublicId}`, {
title,
isCompleted,
completed: isCompleted,
index,
});
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };