# Làm app với AI

> Trang này dành cho người viết app bằng công cụ AI (Claude Code, Cursor, Codex, Copilot…) và cho chính công cụ AI đọc.

## Cho AI hiểu POS Apps

| Cách | Dùng khi | AI làm được |
|---|---|---|
| **MCP server `pos-app`** (khuyên dùng) | Công cụ AI hỗ trợ MCP | Tra tài liệu, xem mã thật của shop, kiểm tra app, chạy thử trong POS, đọc lỗi, deploy, báo lỗi cho Pancake |
| **`AGENTS.md`** | Mọi dự án tạo bằng `create-pos-app` | Đọc quy tắc bắt buộc mỗi lần sửa code |
| **`llms-full.txt`** | Công cụ không hỗ trợ MCP, hoặc chat trên web | Đọc toàn bộ tài liệu một lần |

Ba cách dùng chung được: dự án tạo bằng `create-pos-app` đã có `AGENTS.md`, chỉ cần thêm MCP server.

## MCP

`pos-app mcp` chạy một [MCP](https://modelcontextprotocol.io) server để công cụ AI tự tra tài liệu, xem dữ liệu nền thật của shop, kiểm tra app, đọc lỗi khi chạy và deploy ngay trong cuộc trò chuyện. Server chạy trên máy của bạn (stdio), trong thư mục dự án app, bằng tài khoản đã đăng nhập qua `pos-app login`.

### Kết nối MCP server

1. Đăng nhập CLI một lần: trong POS vào **Cấu hình › Nâng cao › Ứng dụng tuỳ biến**, bấm **Kết nối CLI › Tạo token** rồi chạy lệnh hiện ra (xem [Quản lý ứng dụng › Kết nối CLI](/developers/apps/manage-apps#ket-noi-cli)).
2. Thêm server vào công cụ AI, trong thư mục dự án app (thay `123` bằng mã shop):

```bash
# Claude Code
claude mcp add pos-app -- npx @pancake-pos/app-cli mcp --shop 123
```

Cursor (`.cursor/mcp.json` trong dự án):

```json
{
  "mcpServers": {
    "pos-app": { "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123"] }
  }
}
```

VS Code với Copilot (`.vscode/mcp.json` trong dự án):

```json
{
  "servers": {
    "pos-app": { "type": "stdio", "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123"] }
  }
}
```

```toml
# Codex: ~/.codex/config.toml
[mcp_servers.pos-app]
command = "npx"
args = ["@pancake-pos/app-cli", "mcp", "--shop", "123"]
```

Claude Desktop (`claude_desktop_config.json`) không chạy trong thư mục dự án, nên thêm `--cwd`:

```json
{
  "mcpServers": {
    "pos-app": { "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123", "--cwd", "/đường/dẫn/tới/my-app"] }
  }
}
```

3. Mở lại công cụ AI và hỏi "Liệt kê tool của pos-app". Trong Claude Code, lệnh `/mcp` cho biết server đã kết nối chưa.

### Tool

| Tool | Đầu vào | Trả về |
|---|---|---|
| `search_docs` | `query` | Các đoạn tài liệu khớp, dạng Markdown |
| `get_target` | `target`, ví dụ `purchase.details.action` | Schema dữ liệu ngữ cảnh, giá trị cần trả về, trường manifest và ví dụ ngắn |
| `list_shop_meta` | `resource` | Mã và tên dùng được trong `conditions` và code: trạng thái, thẻ, kho, danh mục sản phẩm, nguồn đơn |
| `sample_context` | `target`, mã bản ghi (không bắt buộc) | `pos.data` mẫu dựng từ dữ liệu thật của shop, đã lọc theo `access` của app và quyền của người đăng nhập |
| `check_app` | không có | Kết quả `pos-app check`: từng lỗi kèm file, dòng, mã luật và gợi ý sửa |
| `dev_start`, `dev_status` | không có | Chạy `pos-app dev`; trả URL local, link POS cần mở ở chế độ phát triển và lỗi build |
| `get_errors` | `version` (mặc định `dev`), `since` | Lỗi khi chạy mà POS ghi lại cho app: target, nội dung, stack trace, thời điểm |
| `deploy` | `message` | Chạy `check` và `build`, rồi upload version mới **chưa kích hoạt**; trả về version và link tới trang quản lý ứng dụng |
| `send_feedback` | `kind` (`bug` hoặc `feature`), `title`, `description`, `include_errors` | Gửi báo lỗi hoặc yêu cầu tính năng về POS Apps cho đội hỗ trợ POS của Pancake; trả mã `FB-…` |

- `deploy` không bao giờ kích hoạt version. Quản trị viên duyệt quyền và kích hoạt trên trang [Ứng dụng tuỳ biến](/developers/apps/manage-apps#duyet-va-kich-hoat-version), giống hệt `pos-app deploy`.
- Nếu `version` trong `pos-app.json` đã tồn tại, `deploy` báo lỗi `duplicate_version`; tăng `version` rồi deploy lại.
- Tool chạy dưới danh nghĩa người đã đăng nhập, trên shop đã chọn. Tool không trả app token hay API key và không ghi dữ liệu shop: `list_shop_meta` và `sample_context` chỉ đọc. Code của app vẫn đọc/ghi dữ liệu qua `pos.api`.

### Vòng làm việc

1. `get_target` và `search_docs` để chọn đúng target và kiểu dữ liệu.
2. `list_shop_meta` và `sample_context` để viết `conditions` và code theo mã, trường thật.
3. Viết code, chạy `check_app` đến khi hết lỗi.
4. `dev_start`, thử app trong POS, rồi `get_errors` để sửa lỗi khi chạy.
5. `deploy` và nhờ quản trị viên kích hoạt version mới.

### Câu lệnh mẫu

```text
Tạo app POS "Nhắc công nợ": block trong form đơn hiện tổng công nợ của khách,
nút "Gửi nhắc nợ" gọi https://crm.example.com/api/remind. Chỉ cần quyền đọc.
Dùng sample_context để lấy dữ liệu đơn thật, check_app đến khi hết lỗi, rồi dev_start để tôi thử trong POS.
```

```text
Nút "Đẩy sang ERP" của version 1.2.0 đang báo lỗi. Đọc get_errors của version đó, tìm nguyên nhân, sửa,
tăng version rồi deploy kèm ghi chú.
```

```text
Chỉ hiện action "Duyệt đơn" với đơn ở trạng thái Chờ xác nhận và nguồn Facebook.
Lấy mã trạng thái và nguồn đơn bằng list_shop_meta, không viết cứng theo tên.
```

## Báo lỗi và đề xuất tính năng

Gặp lỗi của nền tảng (runtime, component, SDK, CLI, MCP, tài liệu, Open API) hoặc cần tính năng chưa có, nhờ công cụ AI gửi cho Pancake bằng tool `send_feedback`:

```text
Báo cho Pancake: pos-table không hiện cột ngày khi format là "date".
```

1. AI soạn tiêu đề và mô tả (lỗi gì, mong đợi gì, các bước tái hiện; hoặc nhu cầu và tình huống dùng), cho bạn xem trước.
2. Bạn đồng ý thì AI mới gửi. Báo cáo vào thẳng kênh hỗ trợ POS của Pancake; AI trả lại mã `FB-…` để bạn dẫn khi trao đổi tiếp.

Báo cáo tự kèm phiên bản CLI và runtime, hệ điều hành, Node, handle và version của app, 5 lỗi gần nhất của app (bỏ bằng `include_errors: false`). Không gửi code hay dữ liệu shop; token lỡ dán vào nội dung bị che trước khi gửi. Mỗi shop gửi tối đa 20 báo cáo mỗi ngày.

## Không dùng MCP

- Toàn bộ tài liệu dạng Markdown: `/developers/apps/llms-full.txt`. Mục lục: `/developers/apps/llms.txt`. Mỗi trang có bản Markdown tại cùng đường dẫn thêm `.md`, ví dụ `/developers/apps/targets.md`.
- Các file cho LLM mặc định là tiếng Anh. Bản tiếng Việt nằm ở `/developers/apps/vi/`, ví dụ `/developers/apps/vi/llms-full.txt`.
- Template `create-pos-app` sinh sẵn `AGENTS.md` với nội dung ở [mục cuối](#agents-md-mau). Giữ file này trong dự án để AI luôn đọc quy tắc.

Câu lệnh mẫu:

```text
Đọc https://pos.pages.fm/developers/apps/llms-full.txt.
Tạo app POS "Nhắc công nợ": block trong form đơn hiện tổng công nợ của khách,
nút "Gửi nhắc nợ" gọi https://crm.example.com/api/remind. Chỉ cần quyền đọc.
```

## Quy tắc bắt buộc

### Môi trường chạy

1. Code trong `extensions/` chạy trong **Web Worker**, không có DOM thật.
   - Không dùng `window`, `document.querySelector`, `document.getElementById`, `localStorage`, `sessionStorage`, `alert`, `confirm`, `canvas`, `getBoundingClientRect`.
   - Không import thư viện UI (antd, MUI, Tailwind, thư viện chart, thư viện kéo thả) và file CSS.
   - Dùng Preact (khuyến nghị) hoặc JS thuần để render element `pos-*` vào `document.body`. Không dùng React trong `extensions/`.
   - Không cài thư viện nào để kết nối hay vẽ giao diện với POS; runtime POS đã có sẵn trong worker.
2. Trang iframe (`page/`) là ứng dụng web bình thường: được dùng thư viện tuỳ ý, nhưng phải lấy `pos` bằng `connect()`, không truy cập `window.top`/`window.parent` và không dùng route bắt đầu bằng `/_pos/` (dành riêng cho POS).

### Giao diện

3. Extension Worker UI **chỉ** dùng element trong [Component giao diện](/developers/apps/components). Không dùng `div`, `span`, `p`, `img`, `table`, `button`, `input`.
4. Text phải nằm trong `pos-text`, `pos-heading`, `pos-badge`, `pos-link`, `pos-button` hoặc `pos-banner`.
5. Không dùng `style`, `className`. Giá trị sự kiện lấy từ `event.detail`.
6. Số tiền hiển thị bằng `pos-money` hoặc cột `format: 'money'`, không tự định dạng.

### Dữ liệu

7. Dữ liệu POS chỉ lấy qua `pos.api` với đường dẫn `/shops/${pos.shop.id}/...` và endpoint có trong [Open API](/developers/api).
8. Không viết cứng shop id, API key, token; không gửi app token ra ngoài, trừ backend của [trang tự host](/developers/apps/runtime-api#trang-tu-host); không đặt secret trong code.
9. Mọi method dùng trong code (`POST`, `PUT`, `DELETE`) phải có trong `access.methods`. Mọi domain ngoài (kể cả ảnh của `pos-image`) phải có trong `network.allowed_origins`.
10. Đọc bản ghi hiện tại từ `data.record` (`data.records` ở action hàng loạt). Gọi Open API bằng `record.system_id` với đơn hàng, `record.id` với sản phẩm, phiếu nhập, phiếu xuất, phiếu chuyển kho.

### Module và manifest

11. Mỗi module có `export default async function (data)`.
    - Block, action `modal`/`fullscreen`, trang `render: "worker"`: render vào `document.body`.
    - Action `headless`: trả `{ message?, refresh? }` hoặc ném lỗi.
    - `should_render`: trả `boolean`, chạy dưới 300 ms, không gọi API chậm.
    - `*.before_submit`: trả `{ ok: true }` hoặc `{ ok: false, message }`, chạy dưới `timeout_ms`.
12. Mỗi extension trong `pos-app.json` có `id` duy nhất; `target` phải có trong [Extension point](/developers/apps/targets).
13. Nên bọc module bằng `defineExtension(target, fn)` từ `@pancake-pos/ui-extensions` để type của `data` và giá trị trả về khớp target.

### Chất lượng

14. Bọc mọi lời gọi API trong `try/catch`; hiện lỗi bằng `pos-banner` (trong giao diện) hoặc `pos.toast.show(..., { tone: 'error' })`.
15. Xử lý `403` (người dùng không đủ quyền) và `429` (thử lại sau, tăng dần thời gian chờ).
16. Không gọi API trong mỗi lần render; dùng `useEffect` với dependency rõ ràng, phân trang và `AbortController`.
17. Viết giao diện bằng ngôn ngữ của người dùng (`pos.user.locale`); mặc định tiếng Việt.
18. Sau mỗi lần sửa, chạy `npx eslint .` (hoặc tool `check_app`) và sửa hết lỗi. Không thêm `eslint-disable` để né lỗi; `pos-app check` vẫn chặn deploy.

## Lỗi thường gặp

| Sai | Đúng |
|---|---|
| `<div className="box">…</div>` | `<pos-section heading="…">…</pos-section>` |
| `<input onChange={(e) => set(e.target.value)} />` | `<pos-text-field onChange={(e) => set(e.detail)} />` |
| `fetch('https://pos.pages.fm/api/v1/shops/123/orders?api_key=...')` | `` pos.api.get(`/shops/${pos.shop.id}/orders`) `` |
| `localStorage.setItem(...)` trong extension | Lưu qua Open API (bảng CRM) hoặc backend riêng |
| `alert('Xong')` | `pos.toast.show('Xong')` |
| `data.order`, `data.purchase` | `data.record` (`data.resource` cho biết loại bản ghi) |
| `` `/shops/${shopId}/purchases/${record.display_id}` `` | `` `/shops/${shopId}/purchases/${record.id}` `` |
| `import { Table } from 'antd'` trong extension | `<pos-table columns={…} rows={…} />` |
| Gọi `PUT` khi `access.methods` chỉ có `GET` | Thêm `"PUT"` vào `access.methods` |
| `new Intl.NumberFormat(...).format(total) + 'đ'` | `<pos-money value={total} />` |

## Checklist trước khi deploy

- [ ] `pos-app.json` hợp lệ: `handle`, `version` mới, `api_version`, `id` không trùng.
- [ ] `access.methods` đủ cho mọi request ghi; không xin quyền thừa.
- [ ] Mọi domain ngoài nằm trong `network.allowed_origins`.
- [ ] Module trong `extensions/` không dùng DOM thật, thư viện UI hay CSS.
- [ ] Không có token, API key, secret trong code.
- [ ] Có xử lý lỗi và trạng thái đang tải.
- [ ] `npx pos-app check` không còn lỗi.
- [ ] Chạy thử bằng `npx pos-app dev` (tài khoản có quyền Cấu hình), rồi deploy lên shop test và thử với tài khoản nhân viên quyền hẹp.

## AGENTS.md mẫu

````markdown
# POS App

Dự án này là app tuỳ biến chạy trong Pancake POS.
Tài liệu: https://pos.pages.fm/developers/apps/llms-full.txt (đọc trước khi sửa code).

## Cấu trúc
- pos-app.json: manifest (quyền, domain ngoài, extension).
- extensions/: module chạy trong Web Worker, render element pos-* bằng Preact.
- page/: trang iframe (React + Vite), lấy `pos` bằng `connect()` từ `@pancake-pos/ui-extensions/page`; route bắt đầu bằng /_pos/ dành riêng cho POS.

## Quy tắc
- extensions/: không dùng DOM thật, window, localStorage, React, thư viện UI, CSS. Chỉ dùng element pos-* có trong tài liệu.
- Text nằm trong pos-text/pos-heading/pos-badge/pos-link/pos-button/pos-banner. Giá trị sự kiện ở event.detail.
- Dữ liệu POS chỉ qua pos.api với đường dẫn /shops/${pos.shop.id}/... và endpoint Open API.
- Không viết cứng shop id, API key, token, secret.
- Method ghi phải có trong access.methods; domain ngoài phải có trong network.allowed_origins.
- Module: export default async function (data). Headless trả { message }, before_submit trả { ok } hoặc { ok: false, message }.
- Bắt lỗi mọi lời gọi API; xử lý 403 và 429.

## Lệnh
- npx pos-app dev: chạy thử với POS (bật Chế độ phát triển trong trang quản lý ứng dụng tuỳ biến); POS tự nạp lại khi sửa code.
- npx eslint .: kiểm tra code sau mỗi lần sửa; không thêm eslint-disable để né lỗi.
- npx pos-app check: kiểm tra manifest và code; sửa hết lỗi trước khi deploy.
- npx pos-app deploy: build, kiểm tra manifest, upload version mới.
- Có MCP server pos-app: gọi get_target/list_shop_meta/sample_context trước khi viết code, check_app sau mỗi lần sửa, get_errors sau khi thử trong POS, deploy khi xong (upload version chưa kích hoạt; admin kích hoạt). Gặp lỗi của nền tảng hoặc thiếu tính năng: soạn báo cáo, cho người dùng xem, chỉ gọi send_feedback khi họ đồng ý.
````
