Skip to content

Add 心屿 Xinyu Island (xinyu-daily) - #27

Merged
MyPrototypeWhat merged 3 commits into
MiniMax-AI:mainfrom
yujiachen-y:add-xinyu-daily
Oct 19, 2026
Merged

MyPrototypeWhat merged 3 commits into
MiniMax-AI:mainfrom
yujiachen-y:add-xinyu-daily

Conversation

@yujiachen-y

@yujiachen-y yujiachen-y commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

心屿 Xinyu Island (xinyu-daily) 1.1.0

A cozy daily-mood Mini App that plays like a small indie game. A floating island grows as you tell its keeper, a cloud named Xiaoyu, more about yourself. Daily inspiration card, mood stickers and journal, joy jar, decision slot machine, Book of Answers, and 20 hidden secrets guarded by Hengheng, a grumpy crab "island bully". Interface in Chinese.

New in 1.1.0: 小屿陪伴 (optional). Xiaoyu writes you a letter each day, chats with you, keeps a small notebook about you, and answers your mood sticker, using your own model through the 3.1.2 Mini App Agent API. Xiaoyu is also redrawn to be cuter: round mochi cloud, big sparkly eyes, little arms, five mood expressions.

preview

Data & access

  • Writes one file, xinyu-state.json, inside context.dataDir (temp file + rename). A corrupt file is renamed to xinyu-state.broken-<time>.json and the app starts empty. Reads only its own client files.
  • No network requests from the app, no child processes, no surface.window.
  • Every route answers only the local page: Host must be loopback, and a browser Origin, when sent, must be this listener (403 otherwise). Writes accept only application/json (415).
  • Agent: agentAccess: ["session.run"], agentConcurrency: 1. Turns run from the Node entry only after the user turns the companion on (mailbox invitation or Settings), and ask for plain text only: the daily letter, chat replies, one line about the mood sticker. Tool, permission, and question events cancel the turn; the app never answers allowOnce. Timeouts 240/120/90 s; up to 20 chat messages and 3 mood replies a day. The README lists every field sent in the prompt.
  • The self-harm care message is fixed text in the app, not model output.
  • Card / answer content is rule-generated for fun and self-reflection; it makes no predictions and gives no advice (stated in the app footer).

Tested environment

  • MiniMax Code 3.1.1-preview.4651 on macOS, an internal preview build that already ships the 3.1.2 Mini App Agent API. Installed through the Host publish flow, opened in the right panel; the companion setting appears when the Host provides an Agent. No console errors or failed requests.
  • The companion was exercised against a mock Agent with the same run contract (55 scripted cases: invitation, letters, chat, notebook, mood replies, fallbacks, tool cancellation, timeouts, limits, session reuse). It has not yet been run against a real model on a device.
  • The review fixes were reproduced first and then verified with a Node 22 script (20 cases, 7/20 before the fix, 20/20 after), plus a headless Chrome run of the page's own writes.
  • Not exercised end to end: shooting stars / hot-air balloons (random timing), birthday cake (birthday only), holographic cards (chance). Sound not checked by ear.

Checks

  • npm run check (Node 22.22.2): 69/69 tests pass; plugins/yujiachen-y/xinyu-daily: 0 errors, 1 warning. The warning is README_MIN_VERSION_MISSING: testing was on 3.1.1-preview, so the README does not claim a 3.1.2 run; the Install section states 3.1.2 or later is required.
  • Icon, artwork, characters, and copy are original; preview uses synthetic data.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

A cozy daily-mood Mini App that plays like an indie game: inspiration cards
shaped by the user's answers, mood stickers and journal, a joy jar, a decision
slot machine, a Book of Answers, and 19 hidden island secrets.

Local only: writes one state file in context.dataDir, no network requests,
no child processes, no Agent turns.

@MyPrototypeWhat MyPrototypeWhat left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the submission — the island is lovely and clearly built with care. I ran npm run check on the merged tree (Node 22: xinyu-daily: OK, 69/69 pass), ran the Node entry in a process with fs, child_process, net, http and fetch all stubbed, and used headless Chrome to walk all 4 tabs and every write endpoint. Everything matches the README: it only writes context.dataDir/xinyu-state.json (temp file + rename), spawns no child processes, makes no outside network requests, and the page makes no outside requests either. HTML entered into the nickname, notes, joy jar and Book of Answers is never executed; esc() is used well. SSE cleanup and dispose are fine too.

Required changes

  • handle() in miniapp/node/server.mjs doesn't check Host or Origin right now. Without the Host's loopback credential, I tested two requests: GET /api/state with Host: attacker.example:<port> returned 200 with the raw mood notes, and POST /api/reset with Origin: https://evil.example and Content-Type: text/plain also returned 200 and wiped the data. Since MiniMax Code 3.1.2 the Host adds a credential to the port, but this package can be installed on older clients (like the 3.1.1-preview you tested on), which don't have that layer. Please follow cq-viewer's approach in this repo (the loopback check in plugins/hanzijie/cq-viewer/miniapp/node/server.mjs): Host must be 127.0.0.1, localhost or [::1], otherwise reject; if Origin is present it must be http://<loopback>:<context.listen.port>, otherwise return 403. Write endpoints could also require Content-Type: application/json while you're at it.

Suggestions (optional)

  • domain.mjs only ignores ENOENT when loading state: if xinyu-state.json is truncated JSON or null, start() throws and the app can never open again, and users won't know how to find dataDir to fix it. Suggest renaming the bad file as a backup, starting from an empty state, and logging a warning.
  • mutate() changes memory before writing to disk, so a failed write leaves memory and disk out of sync. You could modify a copy and swap it in after the write succeeds.
  • The README says localStorage only holds view preferences, but app.js also stores xinyu.awoke. Either mention it in the README or use the existing flags.awoke in state.
  • This PR and #30 both add a row at the same spot in the root README table, so whichever merges first will give the other a one-line conflict; a rebase will fix it.

* @param {import('node:http').ServerResponse} response
*/
async function handle(request, response) {
const url = new URL(request.url ?? '/', 'http://miniapp.local');

@MyPrototypeWhat MyPrototypeWhat Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check here that request.headers.host is a loopback address and that Origin (if present) equals http://<loopback>:${context.listen.port}, returning 403 otherwise. Without Host credentials (e.g. on a 3.1.1 client), a cross-site POST /api/reset (text/plain) returned 200 and wiped the data in my test, and GET /api/state with Host: attacker.example could read the raw notes.

if (!state.secrets || typeof state.secrets !== 'object') state.secrets = {};
if (!state.flags || typeof state.flags !== 'object') state.flags = {};
} catch (error) {
if (/** @type {any} */ (error)?.code !== 'ENOENT') throw error;

@MyPrototypeWhat MyPrototypeWhat Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: when the JSON is corrupt or the content is null, this makes start() fail (SyntaxError and TypeError respectively in my test), and the app will never open. Suggest renaming the bad file as a backup and starting with emptyState().

/** 串行化所有写操作:修改 → 落盘 → 广播 */
function mutate(fn) {
const run = queue.then(async () => {
const result = fn();

@MyPrototypeWhat MyPrototypeWhat Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: fn() modifies memory first, so if persist() fails, memory and disk are out of sync. Consider modifying a copy and swapping it in after persist succeeds.

- Agent turns: none. No `agentAccess`.
- State stored in `xinyu-state.json`: nickname, birthday, daily mood check-ins and notes, drawn
cards and favourites, small joys, Book of Answers history, quiz result, about-you answers,
discovered secrets, and one-time onboarding flags. The browser's `localStorage` keeps only view

@MyPrototypeWhat MyPrototypeWhat Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: app.js also writes xinyu.awoke to localStorage; worth adding here.

Optional daily letter, chat, notebook, and mood replies written by the user's own model through the
3.1.2 Mini App Agent API (agentAccess session.run, agentConcurrency 1). Off until the user opts in
from the mailbox invitation or Settings; tool, permission, and question events cancel the turn.
Redrawn mascot, 20 secrets, new icon and preview.
…om a corrupt state file

Addresses the review on MiniMax-AI#27:
- Every route checks that Host is loopback and that a browser Origin, when sent, is this listener
  (same check as cq-viewer); anything else gets 403. Writes require application/json (415).
- A truncated, null, or non-object xinyu-state.json is renamed to xinyu-state.broken-<time>.json
  and the app starts empty with a warn log; top-level fields of the wrong type fall back to defaults.
- mutate() edits a copy and swaps it in only after the write succeeds, so a failed write leaves
  memory and disk in step.
- The page sends the JSON content type on every write, including bodyless ones.
@yujiachen-y

Copy link
Copy Markdown
Contributor Author

@MyPrototypeWhat 感谢这么细的审查,几条都改了,在 b5dad3d(同时推上来的 76543d2 是 1.1.0 的新功能:可选的小屿陪伴和重画的小屿形象)。

需要修改:Host / Origin 检查

  • handle() 开头对所有路由做 cq-viewer 同款检查:Host 必须是 127.0.0.1、localhost 或 [::1];带了 Origin 时必须是 http://<回环地址>:${context.listen.port},否则返回 403。
  • 写接口一律要求 Content-Type: application/json,否则返回 415。页面这边不带正文的写请求也改成带这个头。
  • 先用你描述的两个请求在改动前的代码上复现了(Host: attacker.example 的 GET /api/state 读到备注;跨站 text/plain 的 POST /api/reset 清空数据),修复后两者都返回 403,同源的 text/plain 返回 415,数据不变。另外覆盖了 localhost / [::1]、别的端口、Origin: null、https、页面和 SSE 路由。

可选建议

  • 存档损坏:截断的 JSON、null、数组会改名为 xinyu-state.broken-<时间>.json,从空状态启动并记一条 warn;顶层字段类型不对时换回默认值。实测改动前分别是 SyntaxError、TypeError,以及 GET /api/state 500。
  • mutate():先在副本上改,写盘成功才换上。写盘期间别处读到的仍是旧状态;写盘失败时内存不变。用只读目录模拟了写盘失败,改动前那条失败的写入会留在内存里,并在下一次写盘时被一起写进去。
  • localStorage:1.1.0 的 README 已经写了“开场是否播放过”(xinyu.awoke)。它用来在状态加载前决定要不要盖上开场画面,避免闪一下,所以保留在 localStorage。
  • 根 README 冲突:Add 仓库透视 Repo X-Ray (repo-xray) #30 先合的话我这边 rebase。

npm run check:69/69,xinyu-daily: 0 errors, 1 warning(README_MIN_VERSION_MISSING:测试用的是 3.1.1-preview,README 没有写 3.1.2 实测,安装说明里写了需要 3.1.2 或更高版本)。README 的 Data & access 和 Tested environment 已同步更新。

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up. I re-reviewed b5dad3d (the 1.1.0 companion commit plus the review-fix commit on top of 11ada93). Locally on Node 22, npm run check passes 69/69, with xinyu-daily at 0 errors and the 1 known warning.

The Host/Origin gap from last round is fixed: every request now goes through isLoopbackRequest first. Retested without Host credentials: a read with a forged Host: attacker.example gets 403; POST /api/reset with a cross-site Origin gets 403 and the data is untouched; text/plain writes get 415. A corrupt xinyu-state.json (truncated / null / array) is renamed to xinyu-state.broken-*.json and the app starts empty, and mutate() now edits a copy and only swaps it in after the write succeeds. The companion also looks careful: turns are cancelled on tool, permission and question events, allowOnce is never answered, and output is parsed and length-capped.

Optional

  • npm run check still reports README_MIN_VERSION_MISSING. Naming MiniMax Code 3.1.2 in ## Tested environment (e.g. "requires ≥3.1.2; tested on 3.1.1-preview, which already ships the Agent API") would match Install and clear the warning.
  • The companion has only been run against a mock Agent, so a quick smoke test on a real 3.1.2+ client with a real model (letter / chat / toggling it off) would be good after this lands.
  • When Host is missing, isLoopbackRequest only checks Origin. Low risk in practice, but you could require a loopback Host.
  • The root README row will conflict with #30; whichever lands second needs a rebase.

@MyPrototypeWhat
MyPrototypeWhat merged commit fa836a3 into MiniMax-AI:main Oct 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants