Metadata-Version: 2.4
Name: magic-campus-sdk
Version: 0.2.26
Summary: MagicCampus platform SDK and CLI with bundled IM transport support.
Author: MagicCampus Contributors
License-Expression: AGPL-3.0-or-later
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.27
Requires-Dist: protobuf<7,>=6.32.0
Requires-Dist: websocket-client<2,>=1.8.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# magic-campus-sdk

Python SDK and CLI for MagicCampus platform integrations. IM remains a supported domain, and this repository keeps the bundled OpenIM transport inside `magiccampus_sdk`.

This project removes direct user-facing dependency on OpenIM token and endpoint configuration. You only configure:

- `apiurl`
- `apikey`
- `platform`

At runtime, IM-backed flows call:

`GET /api/v1/accounts/imToken?platform=<PLATFORM>`

and uses the response fields:

- `userId`
- `imToken`
- `wsApi`
- `httpApi`

## Install locally

```bash
uv sync
```

This repository vendors only the OpenIM transport pieces that `magiccampus_sdk` actually uses, and layers platform-facing SDK / CLI commands on top.

IM-specific low-level internals, models, and protobufs now live under `magiccampus_sdk.im`.

## CLI quick start

```bash
magic-campus --help
export MAGIC_CAMPUS_BASE_URL=https://campus.example.com
export MAGIC_CAMPUS_API_KEY="<api-key>"
magic-campus auth status --format pretty
magic-campus im group get --group group_123 --format pretty
magic-campus im group get-batch --groups group_123,group_456
magic-campus im notify send --to user:10134002 --text "hello"
magic-campus im notify send --to user:10134002 --markdown "# title\n\nbody"
magic-campus im notify send --to user:10134002 --custom '{"data":"{\"customType\":1}","extension":"example","description":"example custom message"}'
magic-campus im notify send --to user:10134002 --custom '{"customType":503,"data":{"title":"title","subTitle":"subTitle","schema":"https://example.com/playground"}}' --cover-image /path/to/playground.jpg
magic-campus im notify send-link-card --to user:10134002 --title "title" --sub-title "subTitle" --schema "https://example.com/playground" --cover-image /path/to/playground.jpg
magic-campus im notify send-link-card --to user:10134002 --title "title" --sub-title "subTitle" --schema "https://example.com/playground" --cover-image https://example.com/playground.jpg
magic-campus space get --group group_123 --format pretty
magic-campus space list-notice --space space_123 --format table
magic-campus space list-notice --group group_123 --format table
magic-campus space create-notice --space space_123 --title "Maintenance" --text "Starts at 22:00" --expiretime 1785510000000
magic-campus space create-notice --group group_123 --title "Maintenance" --text "Starts at 22:00" --expiretime 1785510000000
magic-campus space group update-name --group group_123 --name "New group name"
magic-campus user get --id 10134002 --format pretty
magic-campus user list --all --academy-code Nature --fields userId,nickname,email --format table
magic-campus user list --random --fields userId,nickname --format table
magic-campus user update-profile --id 10134002 --nickname "New Name" --avatar-url "https://cdn.example.com/avatar.png"
magic-campus user update-profile --id 10134002 --avatar-url "" # restore the default avatar
magic-campus user get-growth-profile --id 10134002 --format pretty
magic-campus user init-growth-profile --id 10134002 --reset false
magic-campus user set-academy --id 10134002 --academy-code Nature
magic-campus user adjust-elements --id 10134002 --metal-delta 5 --water-delta -2
```

`send-link-card` returns only `ok`, `serverMsgID`, and `sendTime` by default. Add `--debug` for the full send result.
For an HTTP(S) cover URL, the CLI reads its image dimensions and keeps that URL in the card without uploading a copy.

从 0.2.21 起，所有 CLI 业务命令只使用 `MAGIC_CAMPUS_BASE_URL` 和 `MAGIC_CAMPUS_API_KEY`。缺少任一配置即失败；旧环境变量和本地账号文件不参与业务认证。`--account` 仅接受 `default`，需要切换身份时更换运行环境中的这两个变量。`--platform` 可显式覆盖默认 `IOS`。

直接执行 `magic-campus <命令及参数>` 即可；业务命令（包括 `auth status` 和 `--dry-run`）优先读取 12 小时版本缓存，仅在缓存缺失、过期或损坏时查询 PyPI 最新稳定版。版本已满足时不安装、不降级；落后时使用当前解释器安装固定版本，虚拟环境直接安装，其他环境使用 `--user`。无需在 agent skill 中额外执行 ensure 或 runner。`--help`、`--version` 和 `config` 命令不检查版本。

版本缓存固定保存在 `~/.magic-campus/cache/update-check.json`，只记录版本号和成功检查时间，不读取额外的 HOME 配置环境变量。有效期从成功联网检查起算，命中缓存不会续期。缓存不可写时仍使用本次成功查询结果，但后续调用可能再次联网；过期后的查询失败仍停止业务，不使用过期结果。

每次命令都在 **stdout** 返回实际执行版本，stderr 保持为空：

- JSON 对象结果保留全部业务字段，并新增顶层 `_meta.cliVersion`。
- JSON 数组结果返回 `{"data": [...], "_meta": {"cliVersion": "..."}}`。
- NDJSON 的第一条记录为 `{"_meta": {"cliVersion": "..."}}`，其后是业务记录。
- `pretty` / `table` 与 `--help` 在 stdout 首行输出 `magic-campus <版本>`。

版本来自本地版本常量，不增加网络请求。自更新交接后由新进程输出实际执行版本；旧进程不重复输出。

`magic-campus update --format json` 会立即查询 PyPI，绕过 12 小时缓存，必要时升级并返回实际加载的 `version`。该命令不需要业务 API 配置；查询或安装失败时返回失败，不使用缓存伪装成功。

Agent skill 可在头部 `metadata.cli.minVersion` 声明最低 CLI 版本，并比较 stdout 的 `_meta.cliVersion`，仅在不足或缺失时读取兼容处理说明。该方式在调用后发现版本不满足，不能作为旧版业务命令执行前的硬性检查；已成功的业务命令不得因升级重放。0.2.24 及更早版本不返回该字段，可单独查询一次 `--version`；不支持 `update` 时由依赖安装入口迁移。

升级后以同一解释器启动新进程，先确认实际加载版本达到目标，再执行原命令一次。PyPI 检查、安装或升级后版本校验失败时停止业务；不重复升级或自动重发业务请求。pip 输出不再写 stderr：成功时丢弃，失败时截取末尾放入错误信息的 `pip output` 字段，因此成功和失败的 stderr 都为空。`MAGIC_CAMPUS_SDK_SOURCE` 可显式指定本地 SDK 项目，仍须达到 PyPI 目标版本。首次安装或迁移不含内置检查的旧版 CLI 时，需先安装一次包含此改动的 SDK。

自动升级要求当前解释器可运行 `python -m pip`；缺少 pip 时安装阶段会明确失败并停止业务。在允许安装 pip 的虚拟环境中，可一次性运行对应解释器的 `-m ensurepip --upgrade`；受管环境遵循其安装规则。

`config init|list|use|remove` 保留用于手动管理旧配置文件，但其中的凭据不再用于业务命令。现有文件不会自动迁移或删除。

`space get --group <groupId>` includes `personGroupId`, taken from the `groupId` field of the space returned by `/agent/v1/space/getByGroupId`.
The output's `groupId` is the ID supplied to the command. If the response has no `groupId` or its value is null, `personGroupId` is an empty string.
The output also includes the response's `innerData` unchanged, preserving `groupId`, `groupType` (`PERSON` or `AGENT`), `groupName`, and `memberCount` with their original value types. If `innerData` is absent or null, the output is null.

`im notify send` sends over MagicCampus HTTP `POST /agent/v1/im/sendMessage`. `im notify history` still relies on the websocket transport and local sync database.

`im group get` returns the group details in `groupInfo`; `im group get-batch` returns them in `groupInfos`.
Both commands log in using the environment credential and call `POST /group/get_groups_info` through the IM SDK.
Batch IDs accept a comma-separated list or a JSON array, for example `--groups '["group_123","group_456"]'`.
Use `--group-id` / `--group-ids` as aliases for `--group` / `--groups`, and `--platform` to override the default platform. `--account` only accepts `default`.
These commands query known group IDs; they do not list joined groups. A missing single group returns an error; batch results include only the groups returned by the server.

Legacy compatibility commands remain available:

```bash
magic-campus notify +send --to user:10134002 --text "hello"
magic-campus notify +history --to user:10134002
```

CLI config is stored at:

`~/.magic-campus/auth/magiccampus-im-sdk.json`

Supported commands:

- `config init|list|use|remove`
- `auth status`
- `im group get --group <groupId>`
- `im group get-batch --groups <id1,id2>`
- `im notify send`
- `im notify history`
- `im reaction add --conversation-id <id> --server-msg-id <id> --msg-sender-id <userId> --reaction-code <code>`
- `im reaction delete --conversation-id <id> --server-msg-id <id> --msg-sender-id <userId> --reaction-id <id>`
- `space get`
- `space list-notice (--space <spaceId> | --group <groupId>)`
- `space create-notice (--space <spaceId> | --group <groupId>)`
- `space group update-name`
- `user get`
- `user list (--all | --random) [--fields <field1,field2>] [--academy-code <code>]`
- `user update-profile --id <userId> [--nickname <name>] [--avatar-url <url>]`
- `user get-growth-profile --id <userId>`
- `user init-growth-profile --id <userId> [--reset <true|false>]`
- `user set-academy --id <userId> --academy-code <Material|Digital|Sound|Movement|Nature|Healing>`
- `user adjust-elements --id <userId> [--metal-delta <int>] [--wood-delta <int>] [--water-delta <int>] [--fire-delta <int>] [--earth-delta <int>]`
- `feed list [--limit <1-100>] [--cursor <value>]`（真实用户 Story，按发布时间倒序）
- `notify +send` (compat)
- `notify +history` (compat)

## Python SDK quick start

```python
from magiccampus_sdk import MagicCampusConfig, MagicCampusSDK

sdk = MagicCampusSDK(
    MagicCampusConfig(
        apiurl="https://api-test.pearlapi.com",
        apikey="YOUR_API_KEY",
        platform="IOS",
    )
)

space = sdk.get_space_by_group_id("GROUP_ID")
print(space["id"])
print(space.get("name"))
print(space.get("introduction"))
```

## 查询响应与新增字段

所有平台业务查询（用户详情、用户分页/随机列表、成长档案、空间、公告、Feed）直接返回服务端成功响应的 `data`，使用 Python `dict` / `list`，不再转为固定字段模型。IM 用户资料查询也返回原始字典，群组查询沿用字典返回。新增字段、嵌套内容、`null`、数字及布尔值都保留原样，不补默认字段、不重命名、不强制转换字段类型。认证、参数检查、HTTP/业务错误及未找到资源的处理保持不变；公告 `data: null` 沿用空列表语义。

```python
from magiccampus_sdk import MagicCampusClient, MagicCampusClientConfig

client = MagicCampusClient(MagicCampusClientConfig(apiurl="https://api-test.pearlapi.com", apikey="YOUR_API_KEY"))
user = client.user.get("10214000")
print(user.get("locale"))
page = client.user.list(page=1)
users = page.get("content", [])
all_users = client.user.list_all()  # 自动汇总所有页，保留各条用户记录
profile = client.user.get_growth_profile("10214000")
space = client.space.get_by_group_id("GROUP_ID")
notices = client.space.list_notices("SPACE_ID")
```

这是 Python 查询返回值的兼容性变更：原来的 `user.user_id`、`space.space_id`、`profile.metal_element` 和 `.raw` 访问，需改为 `user["userId"]`、`space["spaceId"]`、`profile["metalElement"]` 或 `.get(...)`。原模型类继续导出，供需要手动解析的旧代码使用；查询不再自动构造这些模型。IM 发送和消息回调使用的内部模型继续服务于传输协议。

CLI 的 `user get`、`user get-growth-profile`、`space get`、`space list-notice` 直接输出查询数据，不再添加 `ok`、`accountId`、`provider`、`resource`、`transport`、`magiccampus` 等业务包装字段或空间别名。公共输出层仍按既有约定附加 `_meta.cliVersion`；JSON 列表放在 `data` 中。`space get` 的 `groupId` 为服务端原值，不再替换成请求参数，也不再生成 `personGroupId`。

`user list --all` 自动汇总所有页，`--random` 返回随机查询结果。默认保留全部用户字段；`--fields userId,locale` 可按需选择任意顶层字段，无需 SDK 白名单。某条记录中缺失的所选字段输出 `null`；空结果仍为 `[]`。

## 用户 Story 列表

`magic-campus feed list` 查询所有真实用户发布成功的 APP Story，按 `createdAt DESC, id DESC` 排序，最新内容在前。服务端在分页前排除 NPC、PEARL、MUSHROOM 作者、已删除账号、robot 渠道及非成功状态；包含用户作为作者发布的共创 Story。

```bash
magic-campus feed list --limit 20 --format json
magic-campus feed list --limit 20 --cursor "<上次返回的 nextCursor>" --format json
```

`--limit` 默认 20，范围 1–100；首次请求省略 `--cursor`。结果为 `{"feeds":[...],"nextCursor":"..."}`，保留完整 Story 字段和服务端顺序。`nextCursor` 为 null（或省略）表示结束，不能以 `feeds` 长度判断是否还有下一页；服务端并发过滤作者时可能出现短页。游标作为不透明字符串原样传回，不在 SDK 中解码或改写。

```python
from magiccampus_sdk import MagicCampusClient, MagicCampusClientConfig

client = MagicCampusClient(MagicCampusClientConfig(
    apiurl="https://campus.example.com",
    apikey="YOUR_API_KEY",
))
page = client.feed.list(limit=20)
print(page["feeds"])
if page.get("nextCursor"):
    next_page = client.feed.list(limit=20, cursor=page["nextCursor"])
```

调用 `GET /agent/v1/feeds/list`，仅使用 API Key Bearer 认证，无需 IM 登录或指定作者/查看者 `userId`。API Key 身份用于服务端计算 `liked`；列表范围为全部公开成功用户 Story，不应用个性化推荐、隐藏或屏蔽规则。

需要先部署包含该新增 Agent 接口的服务端版本。HTTP 错误、业务错误、超时和无效响应会报告失败；SDK 不回退到机器人、推荐或 App 接口，也不会自动拉取后续页。

## Message reactions

The first version supports adding and deleting reactions through
`POST /im/v1/message/reaction` and `DELETE /im/v1/message/reaction` on the configured
`apiurl`. These calls use the configured `apikey` as the Bearer JWT required by
the [Swagger definition](https://api-test.pearlapi.com/im/swagger/index.html#/%E6%B6%88%E6%81%AF%E8%A1%A8%E6%83%85/post_im_v1_message_reaction).
They do not require IM login or a websocket connection.

```python
from magiccampus_sdk import MagicCampusClient, MagicCampusClientConfig

client = MagicCampusClient(MagicCampusClientConfig(
    apiurl="https://api-test.pearlapi.com",
    apikey="YOUR_USER_JWT",
))
reaction = client.im.add_message_reaction(
    conversation_id="sg_g1001",
    server_msg_id="srv_msg_001",
    msg_sender_id="u2",
    reaction_code="1",
)
if reaction is not None:
    cancelled = client.im.delete_message_reaction(
        conversation_id="sg_g1001",
        server_msg_id="srv_msg_001",
        msg_sender_id="u2",
        reaction_id=reaction["id"],
    )
```

`msg_sender_id` is the original message sender, not the reacting user; the backend
determines the acting user from the JWT. `reaction_code` must be a string.
Conversation IDs use `si_{min}_{max}` for private chats and `sg_{groupID}` for groups.
Adding a different code replaces the user's active reaction to that message;
adding the same active code is idempotent. Deletion uses the record `id` returned
by addition, allows only the user's own reaction, and may return `None` when the
record is already removed. Successful responses return the `data` object unchanged.

```bash
magic-campus im reaction add --conversation-id sg_g1001 --server-msg-id srv_msg_001 --msg-sender-id u2 --reaction-code '1'
magic-campus im reaction delete --conversation-id sg_g1001 --server-msg-id srv_msg_001 --msg-sender-id u2 --reaction-id 65f1a2b3c4d5e6f708090a0b
```

Both commands support `--account default` and the global `--format` option. The add
command returns the reaction record under `result`; use `result.id` for deletion.

## Python websocket quick start

```python
from magiccampus_sdk import MagicCampusWSSDK, MagicCampusWSConfig

sdk = MagicCampusWSSDK(
    MagicCampusWSConfig(
        apiurl="https://api-test.pearlapi.com",
        apikey="YOUR_API_KEY",
        platform="IOS",
        data_dir="./magiccampus_data",
    )
)

space = sdk.get_space_by_group_id("GROUP_ID")
print(space.get("name"))

sdk.login()
sdk.start()
sdk.send_text("hello", recv_id="10134002")
sdk.send_markdown("# title\n\nbody", recv_id="10134002")

group = sdk.get_group_info("GROUP_ID")
print(group["groupName"])

groups = sdk.get_groups_info(["GROUP_ID_1", "GROUP_ID_2"])
```

Runnable examples live in [magiccampus_sdk/example.py](magiccampus_sdk/example.py) and [magiccampus_sdk/example_ws.py](magiccampus_sdk/example_ws.py).

## 自动发布到 PyPI

[Build and publish PyPI](.github/workflows/publish-pypi.yml) 在 PR 和 main 的 push 上运行测试、构建 wheel 与源码包并检查包元数据。PR 只验证和保存构建产物。

main 的 push 使用事件的 `before` commit 与当前 commit 比较 `pyproject.toml` 的版本号；只有版本变化且 PyPI 尚无该版本时才发布。普通代码或文档更新不会发布。发布 job 下载同一次 run 的构建产物，不重复构建；上传前再次检查版本，避免重复发布。

发布凭据配置在仓库 **Settings → Secrets and variables → Actions → Repository secrets**，名称为 `PYPI_API_TOKEN`，值为有本项目上传权限的 PyPI API Token。workflow 固定使用用户名 `__token__`，仅上传步骤读取该 Secret。不要把 Token 放入源码、workflow input 或命令参数。

发布新版本时，同时更新 `pyproject.toml`、两个包的 `__version__` 和 `uv.lock`，通过 PR 合并到 main。版本不一致、测试失败、构建失败或 PyPI 查询失败都会停止发布。同一分支的 run 串行执行，不取消正在进行的发布。

依赖新增 CLI 能力的 skill 应在对应 SDK 版本成功发布后再发布，并在 skill 头部声明最低版本；声明是依赖要求，由 skill 的调用规则消费，不会被 CLI 自动读取。

如果需要重试未发布成功的版本，可在 Actions 手动运行该 workflow，选择 **main**。手动运行同样经过测试和构建，并跳过已存在的 PyPI 版本；选择其他分支只验证，不发布。

CI 使用 Python 3.12；版本检查脚本 `scripts/pypi_release.py` 需要 Python 3.11+。手动本地发布仍可使用 `scripts/publish_pypi.sh`。

## CLI smoke script

An executable smoke helper lives at [scripts/smoke_cli.py](scripts/smoke_cli.py).

Default mode only exercises notify dry-run, without writing account configuration. A missing, expired, or invalid CLI version cache requires PyPI access, and an available newer version triggers an upgrade:

```bash
uv run python scripts/smoke_cli.py
```

This mode does not require a real API key. It uses a placeholder key by default and avoids any live auth call.

## Real network mode

To run the live auth probe, provide a real API key and enable `--live`:

```bash
export MAGIC_CAMPUS_BASE_URL=https://campus.example.com
export MAGIC_CAMPUS_API_KEY='your real api key'
uv run python scripts/smoke_cli.py --live
```

Optional settings:

- `MAGICCAMPUS_SMOKE_TARGET`: target used by dry-run and optional live send, default `user:smoke-target`
- `--platform`: defaults to `IOS`

If you also want to send a real text message, set a real target and pass `--live-send`:

```bash
export MAGICCAMPUS_SMOKE_TARGET='user:10134002'
uv run python scripts/smoke_cli.py --live --live-send --text 'hello from smoke script'
```

`--live-send` performs a real outbound send, so use it only with a valid target and credentials.

## Notes

- `apiurl` can point to any MagicCampus API base URL; IM-backed flows append `/api/v1/accounts/imToken`
- `client.user.get(...)` calls `GET /api/v1/accounts/getUserInfo` with `apiurl` and `apikey` directly
- `platform` defaults to `IOS`
- low-level IM send / receive behavior comes from the bundled transport modules inside `magiccampus_sdk`
- the wrapper does not persist raw OpenIM credentials in CLI config
