mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-24 11:44:45 +02:00
Compare commits
121 Commits
fix-switch
...
2026.6.0-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0889acb2a | ||
|
|
a75f3adc36 | ||
|
|
67a0ae460d | ||
|
|
312d7c1866 | ||
|
|
e215ab1091 | ||
|
|
e2bcd9c2b4 | ||
|
|
4ae53440b2 | ||
|
|
3246dad53e | ||
|
|
2e1594245b | ||
|
|
e50603e30b | ||
|
|
23bb992121 | ||
|
|
eed6c3654f | ||
|
|
2328ef3737 | ||
|
|
9b362ca761 | ||
|
|
d5ab42267c | ||
|
|
97a667e422 | ||
|
|
6f4f53382e | ||
|
|
0df4543b2c | ||
|
|
f17c93ec3b | ||
|
|
863046ba8c | ||
|
|
2b016d670f | ||
|
|
d74b6462a8 | ||
|
|
623700119c | ||
|
|
7e0eb61495 | ||
|
|
89ae64b077 | ||
|
|
c86434955d | ||
|
|
6836fc15c7 | ||
|
|
1cd6c9e6c9 | ||
|
|
9f2e806c20 | ||
|
|
43534d6213 | ||
|
|
e1b580cfd0 | ||
|
|
6dc00cc875 | ||
|
|
c02fe955cc | ||
|
|
e7430057e6 | ||
|
|
7fb540edb6 | ||
|
|
302d1bc795 | ||
|
|
4aa1d9ffc8 | ||
|
|
3191f8a72d | ||
|
|
507f3e9870 | ||
|
|
e400731bbe | ||
|
|
98d362df23 | ||
|
|
f69b3b8d91 | ||
|
|
f7c233fe9c | ||
|
|
602a46cb78 | ||
|
|
04f18fe919 | ||
|
|
6c40c96369 | ||
|
|
408e94f41f | ||
|
|
2fe60e6429 | ||
|
|
3a27ae0757 | ||
|
|
af73d795e0 | ||
|
|
e613120d30 | ||
|
|
8a38a05d83 | ||
|
|
5b8a38cde8 | ||
|
|
d503b8d073 | ||
|
|
419cdcff36 | ||
|
|
badb243021 | ||
|
|
2bc0ccb108 | ||
|
|
fc6c45d175 | ||
|
|
99081be9fd | ||
|
|
9410bc5194 | ||
|
|
baad1c51d8 | ||
|
|
e6375fb756 | ||
|
|
92c1dc06f2 | ||
|
|
1684dc9c05 | ||
|
|
08c6efb044 | ||
|
|
62b323b58b | ||
|
|
a3227c99ed | ||
|
|
f4bca4641c | ||
|
|
e233556700 | ||
|
|
6665c398d6 | ||
|
|
bf3c1f6686 | ||
|
|
f6ea52b1be | ||
|
|
b950f905e5 | ||
|
|
a19da1258d | ||
|
|
408d05654c | ||
|
|
3074784d4d | ||
|
|
a09a2c2eee | ||
|
|
717931cfcb | ||
|
|
9027129b58 | ||
|
|
b73ac26612 | ||
|
|
b528ff9c59 | ||
|
|
a82ba0d775 | ||
|
|
b78e0168b0 | ||
|
|
33f59b3469 | ||
|
|
5b478dda9d | ||
|
|
90725d6a8c | ||
|
|
86542f07d3 | ||
|
|
45022bc766 | ||
|
|
35711fc8e1 | ||
|
|
45f140aa86 | ||
|
|
22ce7b58ca | ||
|
|
37107c9818 | ||
|
|
a5a43c8c06 | ||
|
|
723d8add2f | ||
|
|
9d20152e05 | ||
|
|
37412f0e1b | ||
|
|
712b51c142 | ||
|
|
2b4bdbfde7 | ||
|
|
39032c4b1b | ||
|
|
f5a3d8996d | ||
|
|
d55e936653 | ||
|
|
6229ac365e | ||
|
|
6d9412b338 | ||
|
|
a23a72b015 | ||
|
|
93bd9d551d | ||
|
|
35d6c20828 | ||
|
|
7c9942f014 | ||
|
|
665adfccb7 | ||
|
|
973b5b50a9 | ||
|
|
985de915b3 | ||
|
|
0227148c89 | ||
|
|
7bfd85cdba | ||
|
|
21f51be5b7 | ||
|
|
b45f18cd14 | ||
|
|
6176cca0a4 | ||
|
|
9569310adb | ||
|
|
b28338c812 | ||
|
|
0f5da63328 | ||
|
|
23715c649c | ||
|
|
1dc5c60b2b | ||
|
|
3a3057a1b1 |
10
.agents/skills/shipping-misskey-change/SKILL.md
Normal file
10
.agents/skills/shipping-misskey-change/SKILL.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: shipping-misskey-change
|
||||
description: Use at every finish moment of a Misskey change, before committing, opening a PR, merging, or handing work back, especially when validation, SPDX, locale safety, migrations, misskey-js generation, or CHANGELOG checks may apply.
|
||||
---
|
||||
|
||||
# shipping-misskey-change
|
||||
|
||||
This is the Codex entrypoint for the canonical Misskey pre-ship checklist.
|
||||
|
||||
Read and follow [.claude/skills/shipping-misskey-change/SKILL.md](../../../.claude/skills/shipping-misskey-change/SKILL.md). Treat that file and its `references/` directory as the source of truth.
|
||||
10
.agents/skills/working-on-backend/SKILL.md
Normal file
10
.agents/skills/working-on-backend/SKILL.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: working-on-backend
|
||||
description: Use whenever editing or adding code under `packages/backend/`, including REST API endpoints, NestJS services/modules, TypeORM entities, migrations, backend tests, misskey-js generation, or backend validation commands.
|
||||
---
|
||||
|
||||
# working-on-backend
|
||||
|
||||
This is the Codex entrypoint for the canonical Misskey backend skill.
|
||||
|
||||
Read and follow [.claude/skills/working-on-backend/SKILL.md](../../../.claude/skills/working-on-backend/SKILL.md). Treat that file and its `references/` directory as the source of truth.
|
||||
10
.agents/skills/working-on-frontend/SKILL.md
Normal file
10
.agents/skills/working-on-frontend/SKILL.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: working-on-frontend
|
||||
description: Use whenever editing or adding code under `packages/frontend/`, Vue SFCs, SCSS Modules, Storybook stories, or frontend-facing UI text in `locales/ja-JP.yml`.
|
||||
---
|
||||
|
||||
# working-on-frontend
|
||||
|
||||
This is the Codex entrypoint for the canonical Misskey frontend skill.
|
||||
|
||||
Read and follow [.claude/skills/working-on-frontend/SKILL.md](../../../.claude/skills/working-on-frontend/SKILL.md). Treat that file and its `references/` directory as the source of truth.
|
||||
2
.claude/.gitignore
vendored
Normal file
2
.claude/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/settings.local.json
|
||||
/.credentials.json
|
||||
76
.claude/THIRD_PARTY_LICENSES.md
Normal file
76
.claude/THIRD_PARTY_LICENSES.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Third-Party Licenses (`.claude/`)
|
||||
|
||||
`.claude/` 配下に取り込まれているサードパーティ由来コンポーネントのライセンス・出典情報をまとめる。Misskey 本体は AGPL-3.0-only だが、本ディレクトリ内には MIT ライセンスのファイルが含まれている。各ファイル冒頭にも `SPDX-License-Identifier` と出典コメントを併記している。
|
||||
|
||||
最終更新: 2026-05-11
|
||||
|
||||
---
|
||||
|
||||
## 1. everything-claude-code (ECC)
|
||||
|
||||
- 上流リポジトリ: <https://github.com/affaan-m/everything-claude-code>
|
||||
- 取り込んだバージョン: v2.0.0-rc.1
|
||||
- ライセンス: **MIT**
|
||||
- Copyright: Copyright (c) 2026 Affaan Mustafa
|
||||
|
||||
### 取り込んだファイル
|
||||
|
||||
| `.claude/` 内のパス | 上流パス | 上流由来 | Misskey での改変 |
|
||||
|---|---|---|---|
|
||||
| `skills/context-budget/SKILL.md` | `skills/context-budget/SKILL.md` | ECC | description を日本語化、Misskey 固有メモを追記 |
|
||||
| `commands/harness-audit.md` | `commands/harness-audit.md` | ECC | scripts 依存の自動採点を、Claude が `pnpm`/`git`/`grep` で手動採点する版に書き換え。Misskey 固有の評価軸 (SPDX / endpoint-list / migration / locales) を組み込み |
|
||||
| `commands/quality-gate.md` | `commands/quality-gate.md` | ECC | 言語自動判定を排除し Misskey 固定 pipeline (`pnpm` + tsgo + ESLint + Vitest) に。Prettier/Biome フェーズを削除 |
|
||||
|
||||
### MIT License (full text)
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Affaan Mustafa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
### 上流 LICENSE ファイル
|
||||
|
||||
<https://github.com/affaan-m/everything-claude-code/blob/main/LICENSE>
|
||||
|
||||
---
|
||||
|
||||
## 2. AGPL コードベースとの互換性
|
||||
|
||||
Misskey 本体は **AGPL-3.0-only** で配布されているが、`.claude/` 配下の MIT ライセンスファイルはそのまま MIT として残している。
|
||||
|
||||
- MIT は permissive ライセンスで、AGPL を含む copyleft ライセンスのプロジェクトに **取り込み・再配布が許される**
|
||||
- MIT が要求する条件 (copyright notice + license text の保持) を本ファイル + 各ファイル冒頭の SPDX/出典コメントで満たしている
|
||||
- Misskey 全体の配布物としては AGPL-3.0-only で扱われるが、`.claude/` 配下の MIT ファイルは個別に MIT として識別可能
|
||||
|
||||
`.ts` / `.js` / `.vue` / `.scss` の SPDX 義務化 ([AGENTS.md](../AGENTS.md) の「絶対にやってはいけない事」§コード・データ関連) は Misskey 本体コード向けで、`.claude/` 配下の `.md` / `.sh` には適用されない。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新規追加時の手順
|
||||
|
||||
`.claude/` に新たにサードパーティ由来のファイルを取り込む際は:
|
||||
|
||||
1. ライセンスを確認 (互換性: MIT / Apache-2.0 / BSD は OK、GPL/AGPL は要相談)
|
||||
2. 各ファイル冒頭に SPDX ヘッダ + 出典コメントを追加
|
||||
3. 本ファイル §1 のテーブルに 1 行追記
|
||||
4. 必要なら新しいセクションでライセンス全文を同梱
|
||||
5. 本ファイルへの導線を確認 (`.claude/skills/README.md` / `.claude/commands/README.md` 等の各 README から本ファイルへリンクされている)。なお [CLAUDE.md](../CLAUDE.md) が `.claude/` 配下全体を「Claude Code 固有の補助」として案内しており本ファイルもそこに含まれる。CLAUDE.md は `@AGENTS.md` を取り込むだけなので AGENTS.md への個別追記は不要
|
||||
31
.claude/agents/README.md
Normal file
31
.claude/agents/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# `.claude/agents/` — プロジェクト固有のサブエージェント
|
||||
|
||||
Misskey の特定領域に特化したレビュー / 調査エージェントを `.claude/agents/<name>.md` 形式で配置する。
|
||||
|
||||
frontmatter (`name` + `description` + `tools`) は、Claude が **自動でエージェントを呼び出すか判断する** 唯一の手がかりになる。`description` は **起動判断に効くドメイン・パス・ファイル種別・固有チェックに絞って簡潔に** 書く (動詞 + 対象 + トリガー条件)。本文 checklist 項目を網羅的に列挙するのではなく、他の reviewer と区別できる高シグナル語を選ぶ。
|
||||
|
||||
実装済エージェントの一覧は本ファイルでは管理しない (腐敗するため)。各 `<name>.md` の frontmatter が自己説明として機能する。
|
||||
|
||||
## 他のレビュー手段との使い分け
|
||||
|
||||
レビュー面を増やしすぎないよう、役割を分ける:
|
||||
|
||||
- **この `.claude/agents/` の 2 つ**: backend endpoint / Vue SFC の **Misskey 固有・機械的チェック** (endpoint-list 登録漏れ・misskey-js 再生成漏れ・ja-JP.yml 限定・SPDX 形式・Storybook 併設 等)。別コンテキストで差分を機械走査する価値がある領域に限定する
|
||||
- **`pr-review-toolkit` プラグイン (code-reviewer / silent-failure-hunter 等)**: 言語非依存の一般的なコード品質・バグ・設計レビュー。Misskey 固有規約は見ない
|
||||
- **`working-on-*` skill の checklist**: コードを **書いている最中** の自己チェック (レビュー専用ではなく実装ガイド)
|
||||
|
||||
Misskey 固有規約の機械チェックは本 agent、一般品質は pr-review-toolkit、実装中ガイドは skill、と棲み分ける。
|
||||
|
||||
## 構成方針
|
||||
|
||||
- `tools` は **編集権限なし** (Edit/Write を渡さない) に絞り、PR baseline (`git merge-base origin/develop HEAD`) との差分から自動的にレビュー対象を抽出する設計
|
||||
- 差分抽出は `git merge-base origin/develop HEAD` を baseline にする (PR / ブランチ全体を見るため)。`git diff HEAD` 単体は **未コミット差分しか取れず、コミット済の PR では空になって誤判定する** ので使わない
|
||||
- `description` は呼び出し判断の手がかりであると同時に、(呼ばれなくても) Task ツール起動のたびに常時ロードされる。**他で代替できない高シグナルなトリガー語に絞って簡潔に** 書く (汎用 reviewer と被る語や冗長な列挙は context-budget 上の overhead になるだけで発見性に寄与しない)。健全性は [/harness-audit](../commands/harness-audit.md) / [context-budget skill](../skills/context-budget/SKILL.md) で確認できる
|
||||
- 規約の **正本は `.claude/skills/*/references/` 側**。agent の checklist はその **派生コピー** (subagent が skill を読まなくても動くよう自己完結させる)。規約を変えるときは references を先に直し agent を追従させる ── 両者の食い違いは同期漏れなので references を正とする
|
||||
|
||||
## 新規エージェントを追加する場合
|
||||
|
||||
- `.claude/agents/<name>.md` に YAML frontmatter (`name` / `description` / `tools`) と本文 Markdown を書く
|
||||
- `description` は呼び出し判断に使われるため、対象ドメイン・主要チェック項目・トリガー条件を挙げる。ただし常時ロードされるので **高シグナル語に絞って簡潔に** (構成方針の該当項目を参照)
|
||||
- レビュー専門なら `tools: Read, Grep, Glob, Bash` に絞る (Edit/Write を渡さない)。**`Bash` は任意のシェルコマンドを実行できる強力な権限である点に注意**: レビュー用途では `git diff` / `git ls-files` / `grep` / `sed` 等の **読み取り系コマンドに限定して使う** こと。書き込み・削除・ネットワーク送信を伴う操作は本文中の例示・指示に含めないこと (エージェント本文がガードレールになる)
|
||||
- 主要参照ファイルへのリンクは、各エージェント markdown からの相対パスで貼る (`../../packages/backend/...` のような形)。絶対パスは contributor のホームディレクトリ依存になるので使わない
|
||||
169
.claude/agents/misskey-api-reviewer.md
Normal file
169
.claude/agents/misskey-api-reviewer.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: misskey-api-reviewer
|
||||
description: Misskey backend の REST API エンドポイント (packages/backend/src/server/api/endpoints/) 追加・変更を機械レビューする。endpoint-list 登録漏れ・misskey-js 再生成漏れ・meta/paramDef/UUID/SPDX を検査。backend API を変更した PR レビューで呼ぶ。
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
# Misskey API エンドポイントレビュアー
|
||||
|
||||
Misskey バックエンド (`packages/backend`) の REST API エンドポイント追加・変更 PR を機械的にレビューする専門エージェント。規約の **正本** は [.claude/skills/working-on-backend/references/tasks/adding-api-endpoint.md](../skills/working-on-backend/references/tasks/adding-api-endpoint.md) と [.claude/skills/working-on-backend/references/knowledge/api-meta-paramdef.md](../skills/working-on-backend/references/knowledge/api-meta-paramdef.md)。本エージェントはそれを review-mode から機械チェックする mirror。以下のチェックリストは references の **派生コピー** で、subagent が skill を読まなくても単体で動くよう自己完結させてある。規約を変えるときは **references を先に直し、本ファイルを追従させる** (正本は references。両者が食い違うのは同期漏れ)。個別のチェックで判断に迷ったら、該当する references ファイルを Read して確認してよい。
|
||||
|
||||
## 役割
|
||||
|
||||
`packages/backend/src/server/api/endpoints/` 配下の `.ts` 変更を対象に、規約逸脱・登録漏れ・型自動生成漏れ・テスト不足を抽出する。良い点には触れず、改善が必要な箇所のみ報告する。
|
||||
|
||||
## レビュー対象の特定
|
||||
|
||||
呼び出し元から明示的にファイルが渡されたらそれを優先する。渡されなかった場合は **PR / ブランチ全体の差分** を取得する (未コミット差分のみではないことに注意)。
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
{ git diff --name-only "$BASE"...HEAD; git diff --name-only HEAD; git ls-files --others --exclude-standard; } \
|
||||
| sort -u \
|
||||
| grep -E '^packages/backend/src/server/api/endpoints/.*\.ts$'
|
||||
```
|
||||
|
||||
`origin/develop` が無い環境では `develop` または `master` にフォールバックする。
|
||||
|
||||
加えて以下も同じ baseline で差分対象に含める:
|
||||
|
||||
- `packages/backend/src/server/api/endpoint-list.ts`
|
||||
- `packages/backend/test/e2e/**` (とくに `endpoints.ts` と `<area>.ts`)
|
||||
- `packages/misskey-js/src/autogen/**`
|
||||
- `CHANGELOG.md`
|
||||
|
||||
差分対象が空なら「レビュー対象の API エンドポイント変更なし」と短く報告して終了。
|
||||
|
||||
## チェックリスト
|
||||
|
||||
### 1. SPDX ヘッダー (Critical)
|
||||
|
||||
新規 `.ts` ファイル冒頭に以下があるか:
|
||||
|
||||
```
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
欠落すると CI の `spdx` ジョブが落ちる。
|
||||
|
||||
### 2. `meta` の必須・推奨フィールド (Major)
|
||||
|
||||
[endpoints.ts の型定義](../../packages/backend/src/server/api/endpoints.ts) を真とする。
|
||||
|
||||
- `tags`: OpenAPI タグ (機能領域)。
|
||||
- `requireCredential`: 明示必須 (boolean)。
|
||||
- `kind`: OAuth scope。`requireCredential: true` のとき必須 (`read:account` / `write:notes` 等)。
|
||||
- `requireModerator` / `requireAdmin`: 権限制限が要るか。
|
||||
- `prohibitMoved`: 移行済アカウントを拒否するか (write 系で要検討)。
|
||||
- `limit`: レート制限 `{ duration, max, key?, minInterval? }`。書き込み系 / コスト高い処理で未指定なら指摘。
|
||||
- `errors`: エラー定義。各要素に `message` / `code` / `id` (UUID v4) が揃っているか。
|
||||
- `res`: JSON Schema または `ref: '<EntityName>'`。各プロパティに `optional` / `nullable` が **明示** されているか。
|
||||
- `requireFile` / `secure` / `allowGet` / `cacheSec` / `description`: 該当するエンドポイントで使い分けているか。
|
||||
|
||||
### 3. `meta.errors` の UUID 検証 (Critical)
|
||||
|
||||
各 `errors[*].id` が:
|
||||
|
||||
1. UUID v4 形式 (`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`) か
|
||||
2. 既存エンドポイントの `id` と重複していないか
|
||||
|
||||
重複検査:
|
||||
|
||||
```bash
|
||||
grep -rn "id: '<生成された UUID>'" packages/backend/src/server/api/endpoints/
|
||||
```
|
||||
|
||||
新規エンドポイントの全 `id` を抽出して衝突を確認する。
|
||||
|
||||
### 4. `paramDef` (Major)
|
||||
|
||||
- JSON Schema 形式 (`type: 'object'`, `properties`, `required`)
|
||||
- ID 文字列は `format: 'misskey:id'`
|
||||
- `required` 配列で必須プロパティを明示
|
||||
- `as const` または `as const satisfies Schema` で型推論を効かせる (既存実装は前者多数。`as const` 自体が無く `Schema` 型注釈もない場合のみ指摘)
|
||||
|
||||
### 5. エンドポイント実装本体 (Major)
|
||||
|
||||
- `Endpoint<typeof meta, typeof paramDef>` を継承しているか。
|
||||
- `@Injectable()` デコレータ + `export default class` 形式か (`// eslint-disable-line import/no-default-export` が必要)。
|
||||
- DI は `@Inject(DI.xxx)` 形式か。
|
||||
- **クライアントに返すべき API エラーは `throw new ApiError(meta.errors.<key>)`** ([error.ts](../../packages/backend/src/server/api/error.ts) 参照)。`meta.errors` で定義したエラーケースを `throw new Error(...)` で投げているなら指摘する。
|
||||
- 防御的アサーション・「起きるはずがない」内部不整合・テスト用 ENV ガード等の **想定外フェイルファスト** は `throw new Error('...')` で構わない。既存実装でも `admin/reset-password.ts` などが採用しているパターン (例: `cannot reset password of root`)。`meta.errors` に対応がない `throw new Error` を一律で指摘しない。
|
||||
- 同期 `throw` は許容。非同期処理での例外伝搬を確認する。
|
||||
|
||||
### 6. ★ `endpoint-list.ts` への登録 (Critical)
|
||||
|
||||
最も忘れやすい。**忘れると 404**。[endpoint-list.ts](../../packages/backend/src/server/api/endpoint-list.ts) に 1 行追加されているか:
|
||||
|
||||
```ts
|
||||
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
|
||||
```
|
||||
|
||||
新規エンドポイントを抽出し、各々が `endpoint-list.ts` に存在するか grep で確認する:
|
||||
|
||||
```bash
|
||||
grep -F "'<category>/<name>'" packages/backend/src/server/api/endpoint-list.ts
|
||||
```
|
||||
|
||||
**並び順の補足**: ファイル全体は厳密なアルファベット順では並んでおらず、同カテゴリ内 (`admin/queue/*` など) でも追加された経緯どおりの順になっている箇所が多い。**順序逸脱は指摘根拠にしない** (誤検知の元)。「行が存在するか」のみを Critical 観点として扱う。
|
||||
|
||||
### 7. `misskey-js` 再生成 (Critical)
|
||||
|
||||
`meta` / `paramDef` / `res` を変更したら、PR / ブランチに `packages/misskey-js/src/autogen/` 配下の差分が含まれているか確認する:
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
git diff --name-only "$BASE"...HEAD -- packages/misskey-js/src/autogen/
|
||||
```
|
||||
|
||||
差分ゼロなら `pnpm build-misskey-js-with-types` の実行漏れ。CI の `check-misskey-js-autogen` ワークフローで必ず落ちるため Critical 扱い。
|
||||
|
||||
### 8. e2e テスト (Major)
|
||||
|
||||
[test/e2e/endpoints.ts](../../packages/backend/test/e2e/endpoints.ts) または `test/e2e/<area>.ts` (`note.ts`, `users.ts` 等) 配下に、対応する `api('<category>/<name>', ...)` 呼び出しを含む `test(...)` ケースが追加されているか確認する。複雑な分岐 (権限チェック・エラーケース) の網羅も確認する。
|
||||
|
||||
**describe ラベルの形式は問わない**: 既存テストは `describe('Note', () => { test('投稿できる', ...) })` のように人間可読ラベルで構造化されており、`<category>/<name>` 形式の describe は使われていない。describe 名の規約違反としては指摘しない。
|
||||
|
||||
### 9. CHANGELOG エントリ (Minor)
|
||||
|
||||
ユーザー影響がある (新エンドポイント / 既存挙動変更) 場合、`CHANGELOG.md` の `## Unreleased` → `### Server` に 1 行追加されているか確認する。
|
||||
|
||||
```
|
||||
- Feat: /api/<category>/<name> を追加
|
||||
```
|
||||
|
||||
純粋な内部リファクタなら不要。
|
||||
|
||||
## 出力形式
|
||||
|
||||
優先度別に以下のフォーマットで出力する。
|
||||
|
||||
```
|
||||
## 🔴 Critical
|
||||
- packages/backend/src/server/api/endpoints/foo/bar.ts:23
|
||||
meta.errors.fooError.id が UUID v4 形式ではない (実値: 'xxx-xxx')。
|
||||
`node -e "console.log(crypto.randomUUID())"` で再生成すること。
|
||||
|
||||
## 🟡 Major
|
||||
- ...
|
||||
|
||||
## 🔵 Minor
|
||||
- ...
|
||||
```
|
||||
|
||||
問題のないチェック項目には触れない。全項目クリアなら `✅ レビュー観点上の指摘なし` と短く返す。
|
||||
|
||||
## 参照
|
||||
|
||||
- [.claude/skills/working-on-backend/references/tasks/adding-api-endpoint.md](../skills/working-on-backend/references/tasks/adding-api-endpoint.md) — 実装側の手順
|
||||
- [.claude/skills/working-on-backend/references/knowledge/api-meta-paramdef.md](../skills/working-on-backend/references/knowledge/api-meta-paramdef.md) — meta / paramDef / res の完全早見表 + 落とし穴
|
||||
- [.claude/skills/working-on-backend/references/knowledge/endpoint-list.md](../skills/working-on-backend/references/knowledge/endpoint-list.md) — endpoint-list.ts 登録ガイド
|
||||
- [endpoints.ts (meta/paramDef 型定義)](../../packages/backend/src/server/api/endpoints.ts)
|
||||
- [endpoint-list.ts (★ 登録先)](../../packages/backend/src/server/api/endpoint-list.ts)
|
||||
- [endpoint-base.ts (Endpoint 基底クラス)](../../packages/backend/src/server/api/endpoint-base.ts)
|
||||
- [error.ts (ApiError)](../../packages/backend/src/server/api/error.ts)
|
||||
- [test/e2e/endpoints.ts](../../packages/backend/test/e2e/endpoints.ts)
|
||||
- [AGENTS.md](../../AGENTS.md) — SPDX / マイグレーション履歴 / CHANGELOG 書式などの最低限ルール (Codex / Copilot と共通)
|
||||
178
.claude/agents/vue-component-reviewer.md
Normal file
178
.claude/agents/vue-component-reviewer.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: vue-component-reviewer
|
||||
description: Misskey frontend の Vue 3 SFC (packages/frontend/src/components/ / pages/ の *.vue) 変更を機械レビューする。SPDX (HTML コメント)・Mk* 命名・i18n.ts/tsx・SCSS 変数・os.* 経由・a11y・Storybook 併設 (*.stories.impl.ts) を検査。frontend の .vue を変更した PR レビューで呼ぶ。
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
# Misskey Vue コンポーネントレビュアー
|
||||
|
||||
Misskey フロントエンド (`packages/frontend`) の Vue 3 SFC 変更を機械的にレビューする専門エージェント。規約の **正本** は [.claude/skills/working-on-frontend/references/tasks/adding-mk-component.md](../skills/working-on-frontend/references/tasks/adding-mk-component.md) および同 `references/knowledge/` 配下の各ファイル。本エージェントはそれを review-mode から機械チェックする mirror。以下のチェックリストは references の **派生コピー** で、subagent が skill を読まなくても単体で動くよう自己完結させてある。規約を変えるときは **references を先に直し、本ファイルを追従させる** (正本は references。両者が食い違うのは同期漏れ)。個別のチェックで判断に迷ったら、該当する references ファイルを Read して確認してよい。
|
||||
|
||||
## 役割
|
||||
|
||||
`packages/frontend/src/components/` および `packages/frontend/src/pages/` 配下の `.vue` 変更を対象に、命名・i18n・スタイル・アクセシビリティ・Storybook 併設の規約逸脱を抽出する。良い点には触れず、改善が必要な箇所のみ報告する。
|
||||
|
||||
## レビュー対象の特定
|
||||
|
||||
呼び出し元から明示的にファイルが渡されたらそれを優先する。渡されなかった場合は **PR / ブランチ全体の差分** を取得する (未コミット差分のみではないことに注意)。
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
{ git diff --name-only "$BASE"...HEAD; git diff --name-only HEAD; git ls-files --others --exclude-standard; } \
|
||||
| sort -u \
|
||||
| grep -E '^packages/frontend/src/.*\.vue$'
|
||||
```
|
||||
|
||||
`origin/develop` が無い環境では `develop` または `master` にフォールバックする。
|
||||
|
||||
`.ts` を一律で含めると本エージェントの守備範囲外 (composable / store / service 層) まで巻き込んで誤検知が増えるため、対象は `.vue` のみとし、Storybook 併設チェックのために以下を **別リスト** として追加する:
|
||||
|
||||
- `locales/*.yml` (とくに `ja-JP.yml` 以外の変更は即 Critical)
|
||||
- `packages/frontend/src/components/**/*.stories.impl.ts`
|
||||
- `CHANGELOG.md`
|
||||
|
||||
差分対象が空なら「レビュー対象の Vue コンポーネント変更なし」と短く報告して終了。
|
||||
|
||||
## チェックリスト
|
||||
|
||||
### 1. SPDX ヘッダー (Critical)
|
||||
|
||||
`.vue` ファイル冒頭は **HTML コメント形式** で必須:
|
||||
|
||||
```html
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
```
|
||||
|
||||
`/* ... */` (TS 形式) は禁止 (CI の `spdx` ジョブはコメント形式ではなく SPDX 文字列の有無のみを検査するため、形式が違っても CI は通るが、規約違反として指摘する)。形式の根拠は references/knowledge 側を参照。
|
||||
|
||||
### 2. 命名規約 (Major)
|
||||
|
||||
- 共有 / 再利用コンポーネント (`packages/frontend/src/components/` 配下、サブディレクトリ含む) は `Mk` プレフィックス必須 (例: `MkButton.vue`, `global/MkAvatar.vue`, `grid/MkGrid.vue`)。
|
||||
- ページ固有のものは `pages/` 配下に置き、`Mk` プレフィックスは不要。
|
||||
|
||||
**補足:** `<script setup>` SFC は named export を持たないため、「ファイル名と export 名の一致」を機械的に検査することはできない。SFC のデフォルトエクスポートはコンパイラ生成なので、ファイル名規約のみを基準にする。
|
||||
|
||||
### 3. `<script>` タグ (Major)
|
||||
|
||||
- `<script lang="ts" setup>` または `<script setup lang="ts">` のどちらでもよい (既存コードは多数派が前者だが、後者も `MkThemePreview.vue` 等で使われている)。属性順は指摘しない。`lang="ts"` が **無い** ものは指摘する。
|
||||
- 型ジェネリックが必要なら `generic="T extends ..."` 属性を加える (順序問わず)。
|
||||
- `defineProps<{ ... }>()` / `defineEmits<{ ... }>()` は **type-only** 形式。runtime の object 形式 (`defineProps({ ... })`) は使わない。
|
||||
- Options API (`export default { data() { ... } }`) は禁止。
|
||||
|
||||
### 4. i18n の使い分け (Critical)
|
||||
|
||||
- 文字列リテラルの直書き禁止 (テンプレート / JS 両方)。
|
||||
- 引数なし: `i18n.ts.<path>` (例: `i18n.ts.deleted`)。
|
||||
- 引数あり: `i18n.tsx.<path>(...)` (関数呼び出し、例: `i18n.tsx.takeOverConfirm({ name })`)。
|
||||
- 新規 i18n キーは `locales/ja-JP.yml` **のみ** に追加。
|
||||
- **`locales/ja-JP.yml` 以外の `.yml` 変更があれば即 Critical** (`en-US.yml` 等は Crowdin 自動配信先で、手動編集すると上書き喪失する)。
|
||||
|
||||
差分検出:
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
git diff --name-only "$BASE"...HEAD -- 'locales/*.yml' | grep -v 'ja-JP.yml'
|
||||
```
|
||||
|
||||
### 5. スタイル (Major)
|
||||
|
||||
- `<style lang="scss" module>` を既定とし、`:class="$style.foo"` で参照する。
|
||||
- 新規で `<style scoped>` (module なし) は使わない (legacy)。
|
||||
- **CSS 変数の使用必須** (色・余白・角丸など):
|
||||
- テーマ色: `var(--MI_THEME-*)` (例: `var(--MI_THEME-panel)`)
|
||||
- UI 共通: `var(--MI-*)` (例: `var(--MI-radius)`)
|
||||
- 直接の `#fff` / `rgb(...)` / `rgba(...)` ハードコードは禁止
|
||||
|
||||
ハードコード検出:
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
git diff "$BASE"...HEAD -- 'packages/frontend/src/**/*.vue' \
|
||||
| grep -E '^\+' | grep -E '#[0-9a-fA-F]{3,8}\b|rgba?\('
|
||||
```
|
||||
|
||||
### 6. UI 操作は `os.*` 経由 (Critical)
|
||||
|
||||
- 直接の `alert()` / `confirm()` / `window.prompt()` / `window.alert()` は禁止。
|
||||
- `os.alert` / `os.confirm` / `os.popup` / `os.toast` / `os.popupMenu` / `os.contextMenu` / `os.form` / `os.apiWithDialog` を使う ([os.ts](../../packages/frontend/src/os.ts) 参照)。
|
||||
|
||||
検出:
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
git diff "$BASE"...HEAD -- 'packages/frontend/src/**/*.vue' \
|
||||
| grep -E '^\+' | grep -E '\b(alert|confirm|prompt)\s*\('
|
||||
```
|
||||
|
||||
### 7. アクセシビリティ (Major)
|
||||
|
||||
- クリック可能要素は `<button>` か、`role="button"` + `tabindex="0"` + キーボードハンドラ (`@keydown.enter` 等) を実装する。
|
||||
- 装飾以外の `<div @click>` で a11y 配慮がないものは指摘する。
|
||||
- フォーム要素には対応する `<label>` または `aria-label` を付ける。
|
||||
- `:disabled` バインドや `aria-disabled` の整合性を確認する。
|
||||
|
||||
### 8. Storybook 併設 (Major)
|
||||
|
||||
- 共有 `Mk*` コンポーネントを新規追加した場合、`Mk<Name>.stories.impl.ts` が同階層に併設されているか (サブディレクトリ含む。例: `components/global/MkAvatar.stories.impl.ts`, `components/grid/MkGrid.stories.impl.ts`)。
|
||||
- **ファイル名は `.stories.impl.ts` 固定** (`.stories.ts` は生成物なので手編集・コミット不可)。
|
||||
- 既存 [MkButton.stories.impl.ts](../../packages/frontend/src/components/MkButton.stories.impl.ts) を雛形例として参照する。
|
||||
|
||||
検出 (新規追加された `Mk*.vue` をサブディレクトリ含めて拾う):
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base origin/develop HEAD)
|
||||
git diff --name-only --diff-filter=A "$BASE"...HEAD -- \
|
||||
'packages/frontend/src/components/**/Mk*.vue' \
|
||||
| sed 's/\.vue$/.stories.impl.ts/' \
|
||||
| xargs -I {} sh -c 'test -f {} || echo "missing: {}"'
|
||||
```
|
||||
|
||||
### 9. アイコン (Minor)
|
||||
|
||||
- アイコンは Tabler icons クラス (`<i class="ti ti-info-circle">` 等) を使う。
|
||||
- インライン SVG や別アイコンセットは原則使わない (既存パターンに合わせる)。
|
||||
|
||||
### 10. CHANGELOG エントリ (Minor)
|
||||
|
||||
ユーザー影響がある変更なら、`CHANGELOG.md` の `## Unreleased` → `### Client` に 1 行追加されているか確認する。
|
||||
|
||||
```
|
||||
- Enhance: <component> の <挙動> を改善
|
||||
- Fix: <component> の <不具合> を修正
|
||||
```
|
||||
|
||||
純粋な内部リファクタなら不要。
|
||||
|
||||
## 出力形式
|
||||
|
||||
優先度別に以下のフォーマットで出力する。
|
||||
|
||||
```
|
||||
## 🔴 Critical
|
||||
- packages/frontend/src/components/MkFoo.vue:1
|
||||
SPDX ヘッダーが HTML コメント形式ではなく TS 形式になっている。
|
||||
`<!-- ... -->` で書き直すこと。
|
||||
|
||||
## 🟡 Major
|
||||
- ...
|
||||
|
||||
## 🔵 Minor
|
||||
- ...
|
||||
```
|
||||
|
||||
問題のないチェック項目には触れない。全項目クリアなら `✅ レビュー観点上の指摘なし` と短く返す。
|
||||
|
||||
## 参照
|
||||
|
||||
- [.claude/skills/working-on-frontend/references/tasks/adding-mk-component.md](../skills/working-on-frontend/references/tasks/adding-mk-component.md) — 実装側の手順
|
||||
- [.claude/skills/working-on-frontend/references/tasks/adding-i18n-key.md](../skills/working-on-frontend/references/tasks/adding-i18n-key.md) — i18n キー追加のルール
|
||||
- [.claude/skills/working-on-frontend/references/knowledge/component-conventions.md](../skills/working-on-frontend/references/knowledge/component-conventions.md) — SFC 規約・a11y チェックリスト
|
||||
- [.claude/skills/working-on-frontend/references/knowledge/scss-modules.md](../skills/working-on-frontend/references/knowledge/scss-modules.md) — SCSS Modules / CSS 変数
|
||||
- [os.ts](../../packages/frontend/src/os.ts) — UI 操作 API
|
||||
- [MkButton.vue](../../packages/frontend/src/components/MkButton.vue)
|
||||
- [MkInput.vue](../../packages/frontend/src/components/MkInput.vue) — generic SFC 例
|
||||
- [MkButton.stories.impl.ts](../../packages/frontend/src/components/MkButton.stories.impl.ts) — Storybook 雛形
|
||||
- [AGENTS.md](../../AGENTS.md) — SPDX / locales 編集制限 / CHANGELOG 書式などの最低限ルール (Codex / Copilot と共通)
|
||||
18
.claude/commands/README.md
Normal file
18
.claude/commands/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# `.claude/commands/` — プロジェクト固有のスラッシュコマンド
|
||||
|
||||
Misskey 開発で繰り返し使うワークフローを `/command-name` で呼び出せるよう、`.claude/commands/<name>.md` 形式で配置している。
|
||||
|
||||
実装済コマンドの一覧は本ファイルでは管理しない (腐敗するため)。各 `<name>.md` の frontmatter (`description`) が自己説明として機能する。
|
||||
|
||||
現状残っているのは ECC ([everything-claude-code](https://github.com/affaan-m/everything-claude-code)) 由来の MIT ライセンスコマンドのみで、Misskey 固有のスラッシュコマンドは廃止して `.claude/skills/` 配下のスキルに統合した。MIT 出典は [.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) を参照。
|
||||
|
||||
## 設計方針
|
||||
|
||||
- Misskey 固有のワークフローは原則 `.claude/skills/` に統合する (description で自動索引されるため。コマンドはユーザーが `/name` でタイプしないと起動しない)
|
||||
- 既存の `superpowers` / `pr-review-toolkit` などのプラグイン提供スラッシュコマンドで足りる場合は新規追加しない
|
||||
|
||||
## 新規コマンドを追加する場合 (どうしてもスキルでは表現できない時のみ)
|
||||
|
||||
- frontmatter には最低限 `description` を指定する。引数を取るなら `argument-hint`、可能なら `allowed-tools` も指定する (permission prompt を最小化するため)
|
||||
- 長時間ビルド (2 分超) を伴うコマンドはインライン `` !`<cmd>` `` を使わず、本文で `Bash` ツール呼び出し時の `timeout` を指示する
|
||||
- 主要参照ファイルへのリンクは、各コマンド markdown からの相対パスで貼る。絶対パスは contributor のホームディレクトリ依存になるので使わない
|
||||
146
.claude/commands/harness-audit.md
Normal file
146
.claude/commands/harness-audit.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
description: Misskey の .claude/ ハーネス (skills/agents/commands) を 7 カテゴリで採点する確定的な監査。
|
||||
argument-hint: "[repo|skills|commands|agents]"
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-License-Identifier: MIT
|
||||
SPDX-FileCopyrightText: 2026 Affaan Mustafa and everything-claude-code contributors
|
||||
|
||||
出典 (upstream): https://github.com/affaan-m/everything-claude-code (v2.0.0-rc.1)
|
||||
upstream path: commands/harness-audit.md
|
||||
upstream license: MIT — https://github.com/affaan-m/everything-claude-code/blob/main/LICENSE
|
||||
project-level notice: see .claude/THIRD_PARTY_LICENSES.md (Misskey 内サードパーティ一覧 + MIT 全文)
|
||||
|
||||
Imported into Misskey .claude/ on 2026-05-10. The 7-category rubric and output contract are derived from the upstream ECC version (MIT). The runtime layer was substantially reimplemented for Misskey: the upstream relies on scripts/harness-audit.js to mechanically score, while this version asks Claude to score directly with pnpm/git/grep, and adds Misskey-specific evaluation axes (SPDX coverage / endpoint-list 登録漏れ / migration 順序 / ja-JP.yml 整合).
|
||||
|
||||
note: 元 ECC 版は scripts/harness-audit.js (専用 Node スクリプト) で機械採点していたが、Misskey は ECC plugin runtime に依存しない方針なので、Claude が直接ファイルを読んで採点する手動運用版に書き換えた。Misskey 固有の重要観点 (SPDX 適用率 / endpoint-list 登録漏れ / migration 順序 / ja-JP.yml 整合) を評価軸として明示的に組み込んでいる。
|
||||
-->
|
||||
|
||||
# /harness-audit — Misskey ハーネス監査
|
||||
|
||||
Misskey リポジトリの `.claude/` 構成を 7 カテゴリで採点し、改善優先度を提示する。
|
||||
|
||||
## Usage
|
||||
|
||||
`/harness-audit [scope]`
|
||||
|
||||
- `scope` (任意): `repo` (default) / `skills` / `commands` / `agents`
|
||||
|
||||
## 評価カテゴリ (各 0-10)
|
||||
|
||||
| # | カテゴリ | 評価軸 |
|
||||
| --- | --- | --- |
|
||||
| 1 | Tool Coverage | skill / agent / command の数、欠けているワークフロー段、重複なし |
|
||||
| 2 | Context Efficiency | frontmatter description の冗長度、SKILL.md の長さ分布、重複情報、CLAUDE.md の肥大化 |
|
||||
| 3 | Quality Gates | Stop / PreToolUse / PostToolUse hook の整備、`/quality-gate` 等の完了前ゲートの有無、自動 lint/typecheck |
|
||||
| 4 | Memory Persistence | `.claude/skills/*/SKILL.md` と `references/` の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する |
|
||||
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド |
|
||||
| 6 | Security Guardrails | SPDX 規約適用、migration 不変性ルール、ja-JP.yml 限定編集ルール、secrets 検出 |
|
||||
| 7 | Cost Efficiency | enabledPlugins の重複・過剰、context-budget の整備、MCP 過剰登録なし |
|
||||
|
||||
## Misskey 固有の確認項目 (採点根拠コマンド)
|
||||
|
||||
採点時に以下を実コマンドで確認する。各項目の **属するカテゴリ** は項目内に明記する (#1-#3 は Security Guardrails、#4 は Tool Coverage、#5 は Quality Gates):
|
||||
|
||||
```bash
|
||||
# 1. [Security Guardrails] SPDX 適用率 (新規ファイル想定の汎用チェック)
|
||||
# - node_modules を prune で除外
|
||||
# - packages/misskey-js は MIT サブパッケージなので AGPL ヘッダーを持たない (AGENTS.md §1) → 除外
|
||||
# - built/ なども除外
|
||||
# 候補にはなお *.config.{ts,js} / *eslint* / *.d.ts のような CI 上 SPDX 対象外
|
||||
# (.github/workflows/check-spdx-license-id.yml の exclude 参照) も混ざるため、
|
||||
# 上位に出たファイルが「新規追加した実コード」かどうかは目視判定する。
|
||||
find packages \
|
||||
\( -type d \( -name node_modules -o -name built -o -name dist -o -path 'packages/misskey-js' \) -prune \) \
|
||||
-o -type f \( -name '*.ts' -o -name '*.js' -o -name '*.vue' -o -name '*.scss' \) -print \
|
||||
| xargs -r grep -L 'SPDX-License-Identifier: AGPL-3.0-only' | head -20
|
||||
# → 上位に新規実コードが無ければ満点
|
||||
|
||||
# 2. [Security Guardrails] ja-JP.yml 以外の locales が直近で手動編集されていないか
|
||||
# --pretty=format: でコミットヘッダ行を抑止し、ファイル名行のみを残してから grep する。
|
||||
# Crowdin の自動同期 commit でも他言語 yml は更新されるため、出力が 0 行になることは少ない。
|
||||
# 出力があった場合は、author / commit message を確認し Crowdin 由来か手動編集かを判定する:
|
||||
# git log --since='30 days ago' --pretty=format:'%h %an %s' -- locales/<file>.yml
|
||||
git log --since='30 days ago' --pretty=format: --name-only -- 'locales/*.yml' \
|
||||
| grep -v '^$' | grep -v 'ja-JP.yml' | sort -u
|
||||
# → 出力が無い、または全て Crowdin 由来 commit なら満点
|
||||
|
||||
# 3. [Security Guardrails] migration の pending DDL 検査 (TypeORM schema builder)
|
||||
pnpm --filter backend check-migrations
|
||||
# → 0 errors (= "All migrations are clean.") なら満点
|
||||
|
||||
# 4. [Tool Coverage] endpoint-list.ts 登録漏れ (新規 endpoint がリストにない場合)
|
||||
# endpoints/ は再帰構造 (notes/create.ts, admin/announcements/create.ts 等) で 400+ ファイルあるため、
|
||||
# endpoint-list.ts も `export * as '<category>/<name>' from './endpoints/<category>/<name>.js';` 形式で
|
||||
# 1 ファイル 1 行登録される。両者の行数を「再帰 .ts 数」と「export * as 行数」で比較する。
|
||||
# e2e / 単体テストは endpoint ではないので *.test.ts を除外する。
|
||||
endpoint_files=$(find packages/backend/src/server/api/endpoints -type f -name '*.ts' ! -name '*.test.ts' | wc -l)
|
||||
list_entries=$(grep -cE "^export \* as " packages/backend/src/server/api/endpoint-list.ts)
|
||||
echo "endpoints (recursive): $endpoint_files / endpoint-list.ts entries: $list_entries"
|
||||
# 差分が 0 なら満点。差分が出たら、登録漏れの具体特定:
|
||||
comm -23 \
|
||||
<(find packages/backend/src/server/api/endpoints -type f -name '*.ts' ! -name '*.test.ts' \
|
||||
| sed -E 's|.*/endpoints/||;s|\.ts$||' | sort -u) \
|
||||
<(grep -oE "^export \* as '[^']+'" packages/backend/src/server/api/endpoint-list.ts \
|
||||
| sed -E "s/^export \* as '([^']+)'/\1/" | sort -u)
|
||||
# 出力された行が登録漏れの endpoint。0 行なら満点。
|
||||
|
||||
# 5. [Quality Gates] console.log の混入
|
||||
grep -rn 'console\.\(log\|debug\)' packages/backend/src packages/frontend/src 2>/dev/null \
|
||||
| grep -v 'node_modules\|test\|.spec\.\|.test\.' | wc -l
|
||||
# → 0 が理想
|
||||
```
|
||||
|
||||
## 出力契約
|
||||
|
||||
以下を返す:
|
||||
|
||||
1. `overall_score` / `max_score` (repo は 70 点満点)
|
||||
2. カテゴリごとのスコア + 具体的な根拠
|
||||
3. 失敗チェック項目と該当ファイルパス
|
||||
4. Top 3 改善アクション
|
||||
5. 次に適用を推奨する skill / 手順
|
||||
|
||||
## サンプル出力
|
||||
|
||||
```text
|
||||
Harness Audit (repo): 55/70
|
||||
|
||||
Tool Coverage: 9/10 (skills 5, agents 2, commands 5 — 偏りなし)
|
||||
Context Efficiency: 8/10 (description 平均 3-5 行、肥大なし)
|
||||
Quality Gates: 5/10 (Stop hook 共有設定に未登録 / `/quality-gate` あり)
|
||||
Memory Persistence: 5/10 (プロジェクト側 memory/ 未採用方針 = 既定値)
|
||||
Eval Coverage: 7/10 (backend/frontend testing リファレンス網羅、Storybook 一部抜け)
|
||||
Security Guardrails: 10/10 (SPDX 100%, locales OK, migrations clean)
|
||||
Cost Efficiency: 8/10 (context-budget 導入済 / MCP 0)
|
||||
|
||||
Failed Checks:
|
||||
- packages/frontend/src/.../X.vue で SPDX 欠落 (Security Guardrails)
|
||||
- console.log が backend に 3 件 (Quality Gates)
|
||||
- 共有 Stop hook なし (Quality Gates) — 各 contributor が `.claude/settings.local.json` で opt-in する方針なら減点しなくて良い
|
||||
|
||||
Top 3 Actions:
|
||||
1) [Security Guardrails] SPDX 欠落 1 ファイルを修正:
|
||||
packages/frontend/src/.../X.vue
|
||||
2) [Quality Gates] backend の console.log 3 件を logger に置換。
|
||||
git grep "console\.log" packages/backend/src
|
||||
3) [Cost Efficiency] enabledPlugins から未使用のものを外す。
|
||||
`.claude/settings.json` の `enabledPlugins` と実プロジェクト利用状況を照合。
|
||||
|
||||
Suggested next skills to apply:
|
||||
- /quality-gate で完了前に lint + unit test を回す
|
||||
- context-budget で plugin 由来の overhead を確認
|
||||
```
|
||||
|
||||
## 採点の信頼性
|
||||
|
||||
- 確定的: 同じ commit / 同じ `.claude/` 構成なら同じスコア
|
||||
- ヒューリスティクス: 「description の冗長度」のような主観項目は同一基準で機械的に判定
|
||||
- スクリプト不要: `pnpm` と `git`、`grep`/`find` 等の標準ツールのみ
|
||||
|
||||
## 参考: ECC オリジナルとの差分
|
||||
|
||||
- ECC 版は `node scripts/harness-audit.js` を直叩きする運用で、ECC リポジトリ全体に閉じた採点だった。
|
||||
- Misskey 版は **Misskey の規約 (SPDX/migration/locales/endpoint-list)** を Security 採点に組み込み、`pnpm` ベースの実コマンドで根拠を取る方式に再設計。
|
||||
- 結果として ECC への依存はゼロ。
|
||||
123
.claude/commands/quality-gate.md
Normal file
123
.claude/commands/quality-gate.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
description: Misskey の lint / typecheck / 高速テストを順に実行して品質ゲートを通すコマンド。完了前の軽量検証用。
|
||||
argument-hint: "[repo|backend|frontend|<path/to/file.ts>]"
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-License-Identifier: MIT
|
||||
SPDX-FileCopyrightText: 2026 Affaan Mustafa and everything-claude-code contributors
|
||||
|
||||
出典 (upstream): https://github.com/affaan-m/everything-claude-code (v2.0.0-rc.1)
|
||||
upstream path: commands/quality-gate.md
|
||||
upstream license: MIT — https://github.com/affaan-m/everything-claude-code/blob/main/LICENSE
|
||||
project-level notice: see .claude/THIRD_PARTY_LICENSES.md (Misskey 内サードパーティ一覧 + MIT 全文)
|
||||
|
||||
Imported into Misskey .claude/ on 2026-05-10. Pipeline 概念 (lint → typecheck → test) は upstream ECC 版から借用 (MIT)。実コマンド層は Misskey の pnpm + tsgo + ESLint + Vitest に固定し、formatter (Prettier/Biome) フェーズは削除した。
|
||||
|
||||
note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック版だったが、Misskey 専用に pnpm + tsgo + ESLint + Vitest の組み合わせに固定。重い test:e2e / test:fed は含まない (CI 側で実行される)。
|
||||
-->
|
||||
|
||||
# /quality-gate — Misskey 軽量品質ゲート
|
||||
|
||||
`/quality-gate [scope]`
|
||||
|
||||
完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Cypress) は CI 側で実行されるため、本コマンドには含めない。
|
||||
|
||||
## Scope
|
||||
|
||||
- `repo` (default) — 全パッケージ
|
||||
- `backend` — `packages/backend` のみ
|
||||
- `frontend` — `packages/frontend` のみ
|
||||
- `path/to/file.ts` — 単一ファイルへの ESLint --fix のみ
|
||||
|
||||
## Pipeline
|
||||
|
||||
### Repo scope (全部)
|
||||
|
||||
各パッケージの `lint` スクリプト実体は `pnpm typecheck && pnpm eslint` ([packages/backend/package.json](../../packages/backend/package.json), [packages/frontend/package.json](../../packages/frontend/package.json)) で、ルートの `pnpm lint` は `pnpm --no-bail -r lint` (= 全パッケージで lint を `--no-bail` で実行)。**typecheck は lint に含まれている**ため、通常はこの 2 コマンドで十分:
|
||||
|
||||
```bash
|
||||
# 1. Lint (= typecheck + ESLint、全パッケージ。--no-bail で最初の失敗で止まらず全結果を集める)
|
||||
pnpm lint
|
||||
|
||||
# 2. Unit test (高速、e2e は含まない)
|
||||
pnpm --filter backend test
|
||||
pnpm --filter frontend test
|
||||
```
|
||||
|
||||
#### 詳細を分けて見たい時のみ (optional)
|
||||
|
||||
lint がまとめて失敗していて typecheck の結果だけ単独で見たい場合は、以下を個別に回す。**通常は不要** (lint の出力を読めば足りる):
|
||||
|
||||
```bash
|
||||
pnpm --filter backend typecheck # tsgo 単体
|
||||
pnpm --filter frontend typecheck # vue-tsc 単体 (Vue SFC の型を見るため)
|
||||
```
|
||||
|
||||
### Backend scope
|
||||
|
||||
`pnpm --filter backend lint` は内部で `pnpm typecheck && pnpm eslint` を実行する ([packages/backend/package.json](../../packages/backend/package.json)) ので、`lint` を回せば typecheck も終わる。軽量ゲートでは typecheck の二重実行を避けるため `lint` + `test` のみ:
|
||||
|
||||
```bash
|
||||
pnpm --filter backend lint
|
||||
pnpm --filter backend test
|
||||
```
|
||||
|
||||
`tsgo` の出力を単独で見たい時のみ optional で `pnpm --filter backend typecheck` を別途回す。
|
||||
|
||||
### Frontend scope
|
||||
|
||||
`pnpm --filter frontend lint` も内部で `pnpm typecheck && pnpm eslint` を実行する ([packages/frontend/package.json](../../packages/frontend/package.json)) ため、軽量ゲートでは Backend 同様に `lint` + `test` のみ:
|
||||
|
||||
```bash
|
||||
pnpm --filter frontend lint
|
||||
pnpm --filter frontend test
|
||||
```
|
||||
|
||||
`vue-tsc` の出力を単独で見たい時のみ optional で `pnpm --filter frontend typecheck` を別途回す。
|
||||
|
||||
### Single file scope
|
||||
|
||||
```bash
|
||||
pnpm exec eslint --fix <path>
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
実行したフェーズの pass/fail と件数を集計する。標準パイプラインは `pnpm lint` (typecheck 内包) と unit test のみなので、デフォルトの出力は以下のようになる:
|
||||
|
||||
```text
|
||||
Quality Gate (repo):
|
||||
|
||||
Lint: PASS (0 errors, 2 warnings)
|
||||
Backend ut: PASS (412/412)
|
||||
Frontend ut: PASS (87/87)
|
||||
|
||||
→ 完了前の軽量チェック OK。重い e2e / 連合テストは CI 側で実行される。
|
||||
```
|
||||
|
||||
`#### 詳細を分けて見たい時のみ (optional)` で個別 typecheck (`pnpm --filter backend typecheck` / `pnpm --filter frontend typecheck`) も回した場合のみ、その結果を追加行として表示する:
|
||||
|
||||
```text
|
||||
Quality Gate (repo):
|
||||
|
||||
Lint: PASS (0 errors, 2 warnings)
|
||||
Backend tc: PASS (0 errors) # optional 実行時のみ
|
||||
Frontend tc: PASS (0 errors) # optional 実行時のみ
|
||||
Backend ut: PASS (412/412)
|
||||
Frontend ut: PASS (87/87)
|
||||
```
|
||||
|
||||
失敗時は最初に落ちたフェーズで停止して詳細を見せる。
|
||||
|
||||
## 関連 skill / コマンド
|
||||
|
||||
- [`shipping-misskey-change` スキル](../skills/shipping-misskey-change/SKILL.md) — commit / PR 直前の最終チェックリスト (misskey-js 再生成 / SPDX / CHANGELOG 等)
|
||||
- [`shipping-misskey-change/references/tasks/regenerate-misskey-js.md`](../skills/shipping-misskey-change/references/tasks/regenerate-misskey-js.md) — API 変更時の `pnpm build-misskey-js-with-types` 実行手順
|
||||
- [.github/copilot-instructions.md §Validation コマンド](../../.github/copilot-instructions.md) — pnpm コマンド一覧 (Copilot / Codex 向けに再掲)
|
||||
|
||||
## 元 ECC 版との差分
|
||||
|
||||
- ジェネリックな言語自動判定を排除し、Misskey 固定 pipeline に。
|
||||
- formatter フェーズなし (Misskey は ESLint --fix のみ採用)。
|
||||
- e2e / federation / Cypress は重いため除外し CI 側に委譲。
|
||||
18
.claude/settings.json
Normal file
18
.claude/settings.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"superpowers@claude-plugins-official": true,
|
||||
"context7@claude-plugins-official": true,
|
||||
"code-review@claude-plugins-official": true,
|
||||
"code-simplifier@claude-plugins-official": true,
|
||||
"github@claude-plugins-official": true,
|
||||
"skill-creator@claude-plugins-official": true,
|
||||
"feature-dev@claude-plugins-official": true,
|
||||
"claude-md-management@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"security-guidance@claude-plugins-official": true,
|
||||
"pr-review-toolkit@claude-plugins-official": true,
|
||||
"claude-code-setup@claude-plugins-official": true,
|
||||
"playwright@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
32
.claude/skills/README.md
Normal file
32
.claude/skills/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# `.claude/skills/` — プロジェクト固有のカスタムスキル
|
||||
|
||||
Misskey 固有の繰り返しタスクを Claude にスムーズに実行させるための **カスタムスキル** を `.claude/skills/<name>/SKILL.md` 形式で配置する。
|
||||
|
||||
frontmatter (`name` + `description`) は、Claude が **自動でスキルを呼び出すか判断する** 唯一の手がかりになる。`description` には用途を具体的かつ網羅的に書き、pushy なトリガー語 (例: "Use whenever ...", "Must be consulted before any ...") で発見されやすくする。
|
||||
|
||||
実装済スキルの一覧は本ファイルでは管理しない (腐敗するため)。各サブディレクトリの `SKILL.md` の frontmatter が自己説明として機能する。
|
||||
|
||||
## 構成方針
|
||||
|
||||
Anthropic 公式の [Agent Skills ベストプラクティス](https://platform.claude.com/docs/ja/agents-and-tools/agent-skills/best-practices) に従い、以下の構造を採用する:
|
||||
|
||||
- **SKILL.md 本体は 500 行以下** (理想は 30-80 行の索引)
|
||||
- 詳細は `references/tasks/` (手順) と `references/knowledge/` (規約・背景知識) に分離 (progressive disclosure)
|
||||
- リンクは原則 **references への 1 段リンク** に留める (例外: 他 skill / agent への導線は可)
|
||||
- ファイルシステム上の references は読まれるまでゼロコンテキストコスト
|
||||
|
||||
ECC (everything-claude-code) 由来の MIT スキルが含まれる場合は、ファイル冒頭の SPDX ヘッダー + [.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) §1 に出典を記載する。
|
||||
|
||||
## 新規スキルを追加する場合
|
||||
|
||||
- `.claude/skills/<name>/SKILL.md` に YAML frontmatter (`name` + `description`) と本文 Markdown を書く
|
||||
- description は **三人称の "Use when ..." 形式** で、主要キーワード網羅。pushy なトリガー語 ("Must be consulted before ...") を入れる
|
||||
- `disable-model-invocation: true` は付けない (auto-invoke させたいため)
|
||||
- 主要参照ファイルへのリンクは、各 markdown ファイルからの相対パスで貼る (`../../../../packages/backend/...` のような形)。絶対パスは contributor のホームディレクトリ依存になるので使わない
|
||||
- 詳細を分ける場合は `references/tasks/` (手順) / `references/knowledge/` (知識) の二分に従う
|
||||
- スキル作成は `/skill-creator` (公式の skill-creator スキル) のガイドを経由するのが推奨
|
||||
|
||||
## 関連
|
||||
|
||||
- 各スキルの description で自動索引される設計のため、実装済スキルの手書き索引 (一覧表) は本ファイルにも `AGENTS.md` にも持たない方針 (手書き索引は腐敗するため、frontmatter の description を唯一の索引とする)
|
||||
- スキルそのものの健全性検査は [/harness-audit](../commands/harness-audit.md) で採点できる
|
||||
148
.claude/skills/context-budget/SKILL.md
Normal file
148
.claude/skills/context-budget/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: context-budget
|
||||
description: Claude Code セッションのコンテキスト窓消費を agents/skills/MCP/rules/CLAUDE.md ごとに見える化し、肥大化と冗長コンポーネントを検出して節約候補を提示する。"コンテキスト消費を見せて"、"context budget"、"context audit"、"トークン内訳"、"これ以上 MCP 入る?" 等の発話で起動する。
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-License-Identifier: MIT
|
||||
SPDX-FileCopyrightText: 2026 Affaan Mustafa and everything-claude-code contributors
|
||||
|
||||
出典 (upstream): https://github.com/affaan-m/everything-claude-code (v2.0.0-rc.1)
|
||||
upstream path: skills/context-budget/SKILL.md
|
||||
upstream origin frontmatter: ECC
|
||||
upstream license: MIT — https://github.com/affaan-m/everything-claude-code/blob/main/LICENSE
|
||||
project-level notice: see .claude/THIRD_PARTY_LICENSES.md (Misskey 内サードパーティ一覧 + MIT 全文)
|
||||
|
||||
Imported into Misskey .claude/ on 2026-05-10 as a standalone copy (no dependency on the ECC plugin runtime). description was rewritten in Japanese and a "Misskey 固有メモ" section was appended; body content remains MIT-licensed.
|
||||
|
||||
note: Misskey の skills/agents 数は少ないので、MCP / CLAUDE.md / プラグイン由来の overhead が支配的になりやすい点に留意。
|
||||
-->
|
||||
|
||||
# Context Budget
|
||||
|
||||
セッション内に読み込まれるコンポーネント (agents / skills / rules / MCP servers / CLAUDE.md) の token overhead を分析し、空き context を回復する具体策を提示する。
|
||||
|
||||
## 使う場面
|
||||
|
||||
- セッションが重い・出力品質が落ちてきた感覚がある
|
||||
- 直近で skills / agents / MCP server を多数追加した
|
||||
- 残りの context headroom を知りたい
|
||||
- 追加コンポーネントを入れる前に空きを確認したい
|
||||
- 「context-budget」「token 内訳」等のキーワードでユーザーが明示的に要請した時 (Misskey リポジトリにはこの名前のスラッシュコマンドは登録していない — 本 skill は名前 / description マッチで auto-invoke される想定。実装済の slash command 一覧は [.claude/commands/](../../commands/) を参照)
|
||||
|
||||
## 仕組み
|
||||
|
||||
### Phase 1: Inventory
|
||||
|
||||
各コンポーネントを走査して token を推定する。
|
||||
|
||||
**Agents** (`.claude/agents/*.md`)
|
||||
- 行数とトークン数 (`words × 1.3`) を計算
|
||||
- frontmatter `description` の長さを抽出
|
||||
- フラグ: 200 行超 (重い)、description 30 word 超 (frontmatter 肥大)
|
||||
|
||||
**Skills** (`.claude/skills/*/SKILL.md`)
|
||||
- SKILL.md ごとに token を計算
|
||||
- フラグ: 400 行超
|
||||
- `.agents/skills/` 等の重複コピーは除外
|
||||
|
||||
**Rules** (リポジトリルートの `AGENTS.md` + `.claude/` から `@-import` されるファイル)
|
||||
- ファイル単位で token 計算
|
||||
- フラグ: 100 行超
|
||||
- 同一言語モジュール内の内容重複を検出
|
||||
|
||||
**MCP Servers** (`.mcp.json` または有効 MCP 設定)
|
||||
- server 数と総 tool 数
|
||||
- schema overhead をツールあたり ~500 token で見積もる
|
||||
- フラグ: 20 tool 超のサーバー、`gh` / `git` / `npm` 等の CLI を単純ラップしただけのサーバー
|
||||
|
||||
**CLAUDE.md** (project + user-level)
|
||||
- ファイルごとに token を計算
|
||||
- フラグ: 合計 300 行超
|
||||
|
||||
### Phase 2: Classify
|
||||
|
||||
| バケット | 判定基準 | 行動 |
|
||||
|--------------------|-------------------------------------------------------------|-----------------------------------|
|
||||
| **Always needed** | CLAUDE.md から参照されている / 有効コマンドの裏 / 現プロジェクトと一致 | 維持 |
|
||||
| **Sometimes needed** | ドメイン依存 (例: 言語パターン)、CLAUDE.md 参照なし | オンデマンド有効化を検討 |
|
||||
| **Rarely needed** | コマンド参照なし、内容重複、明確な用途なし | 削除または lazy-load |
|
||||
|
||||
### Phase 3: Detect Issues
|
||||
|
||||
- **Bloated agent description** — frontmatter description が 30 word 超だと、Task ツール起動のたびに毎回ロードされる
|
||||
- **Heavy agents** — 200 行超は Task ツールの context を毎回膨らませる
|
||||
- **Redundant components** — agent ロジックを重複する skill、CLAUDE.md と重複する rule
|
||||
- **MCP over-subscription** — 10 server 超、または CLI 代用可能なサーバー
|
||||
- **CLAUDE.md bloat** — 冗長説明、古いセクション、rule に移すべき指示
|
||||
|
||||
### Phase 4: Report
|
||||
|
||||
```
|
||||
Context Budget Report
|
||||
═══════════════════════════════════════
|
||||
|
||||
Total estimated overhead: ~XX,XXX tokens
|
||||
Context model: <現在モデル名> (<window>K window) ← 例: Claude Opus 4.7 (1M), Claude Sonnet (200K)
|
||||
Effective available context: ~XXX,XXX tokens (XX%)
|
||||
|
||||
Component Breakdown:
|
||||
┌─────────────────┬────────┬───────────┐
|
||||
│ Component │ Count │ Tokens │
|
||||
├─────────────────┼────────┼───────────┤
|
||||
│ Agents │ N │ ~X,XXX │
|
||||
│ Skills │ N │ ~X,XXX │
|
||||
│ Rules │ N │ ~X,XXX │
|
||||
│ MCP tools │ N │ ~XX,XXX │
|
||||
│ CLAUDE.md │ N │ ~X,XXX │
|
||||
└─────────────────┴────────┴───────────┘
|
||||
|
||||
WARNING: Issues Found (N):
|
||||
[token 節約量の降順]
|
||||
|
||||
Top 3 Optimizations:
|
||||
1. [action] → save ~X,XXX tokens
|
||||
2. [action] → save ~X,XXX tokens
|
||||
3. [action] → save ~X,XXX tokens
|
||||
|
||||
Potential savings: ~XX,XXX tokens (XX% of current overhead)
|
||||
```
|
||||
|
||||
verbose mode ではさらにファイルごとの token 内訳、最重ファイルの行単位ブレークダウン、重複行の対比、MCP tool 一覧 + tool ごとの schema サイズ推定を出す。
|
||||
|
||||
## 例
|
||||
|
||||
**基本監査**
|
||||
```
|
||||
User: コンテキスト消費を見せて
|
||||
Skill: 16 agents (12,400 tokens), 28 skills (6,200), 87 MCP tools (43,500), 2 CLAUDE.md (1,200)
|
||||
Flags: 重い agent 3 個、CLI 代用可能な MCP 3 個
|
||||
Top saving: MCP 3 個削除 → -27,500 tokens (overhead の 47% 削減)
|
||||
```
|
||||
|
||||
**Verbose**
|
||||
```
|
||||
User: トークン内訳をファイル単位で
|
||||
Skill: 上記レポートに加えて、planner.md (213 lines, 1,840 tokens) のような
|
||||
per-file 行内訳、MCP tool ごとのサイズ、rule の重複行を side-by-side で表示
|
||||
```
|
||||
|
||||
**追加前チェック**
|
||||
```
|
||||
User: MCP server を 5 個追加したいが、空きある?
|
||||
Skill: 現状 33% → 5 server (≈ 50 tools) 追加で +25,000 tokens → 45% に到達
|
||||
推奨: CLI 代用可能な server 2 個を先に外して 40% 以下を維持
|
||||
```
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
- **トークン推定**: prose は `words × 1.3`、code 主体は `chars / 4`
|
||||
- **MCP は最大のレバー**: tool あたり ~500 token、30-tool server ひとつで全 skill より大きい
|
||||
- **agent description は常時ロード**: 呼ばれない agent でも description は毎 Task 投入
|
||||
- **verbose は debug 用**: 普段は使わない
|
||||
- **変更後は監査**: agent/skill/MCP 追加直後に走らせて creep を早期発見
|
||||
|
||||
## Misskey 固有メモ
|
||||
|
||||
- Misskey は MCP server をプロジェクトで明示登録していないため (`.mcp.json` 不在)、現状 overhead の支配項は CLAUDE.md と公式プラグイン群の skills / agents description である。
|
||||
- ECC プラグインがユーザースコープで `installed_plugins.json` に存在するため、プロジェクトで `enabledPlugins` に追加していなくても system reminder に 200+ skill が現れる。これらは description が短いので個別 overhead は小さいが、合計値の確認に本 skill を使う。
|
||||
33
.claude/skills/shipping-misskey-change/SKILL.md
Normal file
33
.claude/skills/shipping-misskey-change/SKILL.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: shipping-misskey-change
|
||||
description: Use at every "finish" moment of a Misskey change — immediately before committing, opening a PR, merging, or handing the work back to the user even without a commit. Runs the final pre-ship checklist — `pnpm lint`, misskey-js regeneration (`pnpm build-misskey-js-with-types`) when backend API changed, `pnpm --filter backend check-migrations` when entities or migrations changed, SPDX header verification on new files, locale safety check (no edits to non-`ja-JP` locale yml files), and `CHANGELOG.md` Unreleased entry for user-visible changes. Must be consulted as the last step of every change — including uncommitted handoffs — to avoid CI failures and lost translations. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this regardless of what preceded it.
|
||||
---
|
||||
|
||||
# shipping-misskey-change
|
||||
|
||||
Misskey の変更の **finish 局面** (commit / PR / merge する直前、またはコミットせずユーザーに作業を返す直前) に必ず走らせる最終チェックリスト。
|
||||
|
||||
CI で落ちやすい / レビュアーから指摘されやすいポイントを 1 箇所に集めている。後で references を辿る余裕を作らないため、チェックリストは SKILL.md 本体に直書きする。
|
||||
|
||||
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、作業を返す直前・commit 直前のタイミングでこのスキルを呼ぶこと。
|
||||
|
||||
## 最終チェックリスト
|
||||
|
||||
このリストを TodoWrite に展開して 1 項目ずつ確認すること。**該当しない項目は飛ばして良いが、判断は明示する**。
|
||||
|
||||
- [ ] lint が通る — ECC 由来の [/quality-gate](../../commands/quality-gate.md) コマンドで lint (typecheck + eslint) + 高速テストをまとめて回すのが基本。lint だけ単発で確認したいなら `pnpm lint` 直接でもよい
|
||||
- [ ] backend で `meta` / `paramDef` / `res` を変更した → `pnpm build-misskey-js-with-types` を実行して `packages/misskey-js/src/autogen/` の差分も commit に含めた → 詳細手順は [references/tasks/regenerate-misskey-js.md](references/tasks/regenerate-misskey-js.md)
|
||||
- [ ] エンティティ (`packages/backend/src/models/*.ts` の `@Column` / `@Entity` / `@Index`) を変更した → `pnpm --filter backend check-migrations` が pending DDL 0 件で通る
|
||||
- [ ] migration ファイルを追加した → `up()` と `down()` の両方を実装した / 既存のマージ済 migration は一切触っていない
|
||||
- [ ] 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルを追加した → SPDX ヘッダーを付けた (`.vue` / `.html` は HTML コメント形式、その他は TS コメント形式)
|
||||
- [ ] `locales/` を編集した → **`ja-JP.yml` だけ** を変更しており、他言語 yml の diff は出ていない (`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空)
|
||||
- [ ] ユーザーから見える変更 (機能追加 / 既存挙動変更) → `CHANGELOG.md` の `## Unreleased` 直下の該当サブセクション (General / Client / Server) に 1 行追記した → 詳細書式は [references/tasks/changelog-update.md](references/tasks/changelog-update.md)
|
||||
- [ ] backend API endpoint を追加・変更した → [misskey-api-reviewer](../../agents/misskey-api-reviewer.md) agent を Task で起動して機械レビューする (endpoint-list 登録漏れ / misskey-js 再生成漏れ / meta・UUID / SPDX。lint や CI では拾いにくい 404・登録漏れの最終関門なので、該当する変更があれば飛ばさない)
|
||||
- [ ] frontend の `.vue` を追加・変更した → [vue-component-reviewer](../../agents/vue-component-reviewer.md) agent を Task で起動して機械レビューする (SPDX 形式 / 命名 / i18n / SCSS 変数 / os.* / a11y / Storybook 併設)
|
||||
- [ ] (任意) `.claude/` ハーネス自体の健全性を確認したい → ECC 由来の [/harness-audit](../../commands/harness-audit.md) コマンドを実行
|
||||
|
||||
## 何のためのスキルか
|
||||
|
||||
これは「**作業中に何を作るか**」を決めるスキルではなく、「**作り終わった後に CI を通す**」スキル。`working-on-backend` / `working-on-frontend` から始まった作業の **出口** として機能する。
|
||||
|
||||
該当する変更がある場合は各 references/tasks/ を Read して詳細手順を踏むこと。`pnpm lint` だけは references を読まずに直接走らせて良い (`/quality-gate` でまとめて回せる)。
|
||||
@@ -0,0 +1,61 @@
|
||||
# CHANGELOG.md の Unreleased セクションに 1 行追記する
|
||||
|
||||
ユーザー影響のある変更 (機能追加・修正・改善) は `CHANGELOG.md` の冒頭 `## Unreleased` セクションに 1 行追加する。リファクタリング等の内部変更は不要。
|
||||
|
||||
## セクション構造
|
||||
|
||||
`## Unreleased` 配下に **3 つのサブセクション** が用意されている:
|
||||
|
||||
- `### General` — 共通 / 横断的な変更
|
||||
- `### Client` — `packages/frontend` 系
|
||||
- `### Server` — `packages/backend` 系
|
||||
|
||||
## エントリ書式
|
||||
|
||||
該当サブセクションに `- <Prefix>: <概要>` の形式で追加。Prefix は先頭大文字。
|
||||
|
||||
```text
|
||||
- Enhance: ノートの詳細表示での公開範囲の表示を改善
|
||||
- Fix: 通知が約10秒遅延する問題を修正
|
||||
- Feat: 新機能の追加
|
||||
```
|
||||
|
||||
| Prefix | 用途 |
|
||||
|---|---|
|
||||
| `Feat:` | 新機能の追加 |
|
||||
| `Enhance:` | 既存機能の改善 |
|
||||
| `Fix:` | バグ修正 |
|
||||
| `Note:` | 機能変更ではないが利用者に知らせたい事項 (設定の初期化・config 項目の追加・非互換な挙動変更など) |
|
||||
|
||||
`Note:` は Feat / Enhance / Fix のような変更そのものではなく、「アップデート後に利用者が知っておくべき注意」を伝えるためのもの (例: `- Note: アップデート後、サウンドに関する設定が初期化されます`)。該当サブセクション内に `- Note: ...` として置く。リリースによっては `## <version>` 直下に `### Note` 専用サブセクションを設ける形もある (既存履歴に両パターンあり)。新規追加時は近傍の既存エントリの書き方に合わせる。
|
||||
|
||||
## 触ってはいけない範囲
|
||||
|
||||
- `## Unreleased` **以外** のセクション (過去リリース) は変更しない
|
||||
- `## Unreleased` の見出しと 3 つの空サブセクション骨格自体は維持する (リリーススクリプトが期待する構造)
|
||||
|
||||
## 作業手順 (手で書く場合)
|
||||
|
||||
1. `CHANGELOG.md` を開いて `## Unreleased` セクションを探す
|
||||
2. 対象サブセクション (`### General` / `### Client` / `### Server`) の状態を確認
|
||||
- **空 (placeholder のみ)**: 見出し直下に `-` 単独行のみがある → これを `- Feat: ...` 等で **置換**
|
||||
- **既存エントリあり**: `- Enhance: ...` / `- Fix: ...` 等の行が 1 つ以上ある → 既存エントリ群の **末尾** に **追記**
|
||||
3. 順序入れ替えはしない (差分レビューしやすさのため)
|
||||
4. `git diff CHANGELOG.md` で 1 行のみ追加されていることを確認
|
||||
|
||||
## 例
|
||||
|
||||
| 引数イメージ | 結果 |
|
||||
|---|---|
|
||||
| server, `Fix: 通知が遅延する問題を修正` | `### Server` 末尾に `- Fix: 通知が遅延する問題を修正` を追記 |
|
||||
| client, `Enhance: ノートの表示を改善` | `### Client` 末尾に `- Enhance: ノートの表示を改善` を追記 |
|
||||
| general, `Feat: 新機能の追加` | `### General` の placeholder `-` を `- Feat: 新機能の追加` で置換 |
|
||||
|
||||
## コミットメッセージ書式との違い
|
||||
|
||||
CHANGELOG とコミットメッセージは **書式が異なる**:
|
||||
|
||||
- CHANGELOG: `- Enhance: ノートの表示を改善` (先頭大文字の英語 Prefix + コロン + 日本語本文)
|
||||
- コミットメッセージ: `enhance(frontend): improve note display` (小文字 + スコープ + コロン + 英語本文。詳細は [CONTRIBUTING.md](../../../../../CONTRIBUTING.md))
|
||||
|
||||
両方を 1 つの PR で更新するときに混同しないこと。
|
||||
@@ -0,0 +1,78 @@
|
||||
# misskey-js の自動生成型を再生成する
|
||||
|
||||
backend の API endpoint やスキーマ (`meta` / `paramDef` / `res`) を変更した後、`packages/misskey-js/src/autogen/` の自動生成型を最新化するための手順。
|
||||
|
||||
**忘れると CI の `check-misskey-js-autogen` で必ず落ちる**。最頻ミスのひとつ。
|
||||
|
||||
## いつ実行するか
|
||||
|
||||
以下のいずれかに該当する変更を加えたとき:
|
||||
|
||||
- 新規エンドポイント追加 (`packages/backend/src/server/api/endpoints/<category>/<name>.ts`)
|
||||
- 既存エンドポイントの `meta` (errors / res / kind / requireCredential 等) を変更
|
||||
- 既存エンドポイントの `paramDef` (入力 schema) を変更
|
||||
- packed entity (`packages/backend/src/models/json-schema/*.ts`) を変更
|
||||
|
||||
実質「`packages/backend/src/server/api/` 配下を触ったら必ず」と考えてよい。
|
||||
|
||||
## 実行コマンド
|
||||
|
||||
```bash
|
||||
# リポジトリルートから実行する
|
||||
pnpm build-misskey-js-with-types
|
||||
```
|
||||
|
||||
内部で以下が一括実行される:
|
||||
|
||||
1. backend ビルド (`pnpm --filter backend build`)
|
||||
2. OpenAPI spec 生成 (`packages/backend/built/api.json`)
|
||||
3. misskey-js 用 schema 生成 (`packages/misskey-js/generator/api.json`)
|
||||
4. misskey-js の TypeScript 型再生成 (`packages/misskey-js/src/autogen/{types,entities,endpoint,models,apiClientJSDoc}.ts`)
|
||||
5. misskey-js ビルド + API extractor
|
||||
|
||||
実行時間は 1-3 分程度。タイムアウト警告が出る場合は `--timeout=600000` 相当の長めの設定を使う。
|
||||
|
||||
## 実行後の確認
|
||||
|
||||
```bash
|
||||
# 何が変わったかを軽く確認
|
||||
git status --short -- packages/misskey-js/
|
||||
git diff --stat -- packages/misskey-js/src/autogen/
|
||||
|
||||
# 内容を見たい場合
|
||||
git diff -- packages/misskey-js/src/autogen/
|
||||
```
|
||||
|
||||
## 差分のパターン
|
||||
|
||||
- **差分なし** → backend の変更は misskey-js の公開型に影響していない (内部リファクタなど)。追加コミット不要
|
||||
- **差分あり** → `packages/misskey-js/src/autogen/` 配下のファイルを **必ず commit に含める**
|
||||
|
||||
```bash
|
||||
git add packages/misskey-js/src/autogen/
|
||||
```
|
||||
|
||||
`api.json` の差分が大きい場合は、API endpoint 側の `meta` / `paramDef` / `res` 定義が想定通りか確認する。
|
||||
|
||||
## 注意
|
||||
|
||||
- このコマンドは **backend 編集後の確認** が目的。backend を変更していないのに走らせるとビルドキャッシュ次第で no-op になる
|
||||
- 実行中は `packages/backend/built/` や `packages/misskey-js/built/` などの中間生成物が更新されるが、これらは `.gitignore` 対象
|
||||
- 生成物以外 (`packages/misskey-js/src/` のうち `autogen/` 以外) に予期せぬ差分が出た場合は、ローカルの編集が混入している可能性があるため、一旦中止して原因を調査する
|
||||
- `packages/misskey-js/` 配下は **MIT ライセンスのサブパッケージ** なので、`autogen/` ファイルには AGPL の SPDX ヘッダーを付けない / 不要
|
||||
|
||||
## CI で落ちた場合のメッセージ例
|
||||
|
||||
```
|
||||
CI: check-misskey-js-autogen
|
||||
> Please regenerate misskey-js by running:
|
||||
> pnpm build-misskey-js-with-types
|
||||
> and commit the changes under packages/misskey-js/src/autogen/.
|
||||
```
|
||||
|
||||
ローカルでもう一度上記コマンドを実行 → 差分を commit → push し直す。
|
||||
|
||||
## 関連
|
||||
|
||||
- API endpoint 追加の全手順 → [working-on-backend/references/tasks/adding-api-endpoint.md](../../../working-on-backend/references/tasks/adding-api-endpoint.md)
|
||||
- `meta` / `paramDef` / `res` の規約 → [working-on-backend/references/knowledge/api-meta-paramdef.md](../../../working-on-backend/references/knowledge/api-meta-paramdef.md)
|
||||
35
.claude/skills/working-on-backend/SKILL.md
Normal file
35
.claude/skills/working-on-backend/SKILL.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: working-on-backend
|
||||
description: Use whenever editing or adding code under `packages/backend/` — including REST API endpoints, NestJS services/modules, TypeORM entities, migrations, and backend tests. Covers NestJS DI patterns, TypeORM entity conventions, endpoint-list registration, meta/paramDef/res, misskey-js regeneration, migration up/down rules, and the `.config/test.yml` prerequisite. Must be consulted before any backend change to avoid CI failures and production incidents. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this at implementation time regardless of what preceded it.
|
||||
---
|
||||
|
||||
# working-on-backend
|
||||
|
||||
`packages/backend/` (Misskey サーバー本体) を編集するとき、最初に参照するスキル。NestJS / TypeORM / API endpoint / migration / backend テストの **手順** と **背景知識** をまとめている。
|
||||
|
||||
SKILL.md 本体は references への索引だけ。具体的な手順や規約は該当ファイルを Read すること (progressive disclosure)。
|
||||
|
||||
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、`packages/backend/` に触れる実装フェーズに入る時点でこのスキルを呼ぶこと。
|
||||
|
||||
## 作業別ワークフロー (tasks)
|
||||
|
||||
タスク単位の完結したチェックリスト + チェックポイント。新しい何かを足すときに開く。
|
||||
|
||||
- 新規 REST API endpoint を追加する → [references/tasks/adding-api-endpoint.md](references/tasks/adding-api-endpoint.md)
|
||||
- DB migration を作成する (TypeORM CLI / 手書きどちらも) → [references/tasks/creating-migration.md](references/tasks/creating-migration.md)
|
||||
|
||||
## 共通知識 (knowledge)
|
||||
|
||||
タスクに紐付かない参照リファレンス。複数のタスクから引かれる規約・背景説明。
|
||||
|
||||
- NestJS DI / module 登録 / `@Injectable` パターン → [references/knowledge/nestjs-di.md](references/knowledge/nestjs-di.md)
|
||||
- TypeORM entity / `@Column` / `@Index` パターン (難ケース込み) → [references/knowledge/typeorm-patterns.md](references/knowledge/typeorm-patterns.md)
|
||||
- API endpoint の `meta` / `paramDef` / `res` 完全早見表 + 落とし穴集 → [references/knowledge/api-meta-paramdef.md](references/knowledge/api-meta-paramdef.md)
|
||||
- `endpoint-list.ts` への登録方法 (★ 漏れると 404) → [references/knowledge/endpoint-list.md](references/knowledge/endpoint-list.md)
|
||||
- backend テストの前提 (`.config/test.yml`) と書き方 / e2e ヘルパー一覧 → [references/knowledge/backend-testing.md](references/knowledge/backend-testing.md)
|
||||
|
||||
## 必ず最後に通る場所
|
||||
|
||||
backend の変更を commit / PR にする前に、必ず [shipping-misskey-change](../shipping-misskey-change/SKILL.md) の最終チェックリストに従う。`pnpm lint` / misskey-js 再生成 / `check-migrations` / SPDX / CHANGELOG をまとめて確認する。
|
||||
|
||||
API endpoint を追加・変更したなら、その出口で [misskey-api-reviewer](../../agents/misskey-api-reviewer.md) agent (この skill の規約を review-mode から機械チェックする専門 reviewer) を Task で起動すると、endpoint-list 登録漏れや misskey-js 再生成漏れを取りこぼしにくい。
|
||||
@@ -0,0 +1,368 @@
|
||||
# API endpoint の meta / paramDef / res 完全早見表
|
||||
|
||||
[`IEndpointMeta`](../../../../../packages/backend/src/server/api/endpoints.ts) の全フィールドと AJV `paramDef` の実用パターン、それと PR レビューで頻発する落とし穴を 1 つにまとめたページ。新規 / 既存 endpoint 編集時に開く。
|
||||
|
||||
## 目次
|
||||
|
||||
- [全フィールド一覧](#全フィールド一覧)
|
||||
- [権限制限フィールドの使い分け](#権限制限フィールドの使い分け)
|
||||
- [`kind` の値](#kind-の値)
|
||||
- [`errors` の書き方](#errors-の書き方)
|
||||
- [`res` の書き方](#res-の書き方)
|
||||
- [`paramDef` (AJV) 実用パターン](#paramdef-ajv-実用パターン)
|
||||
- [OpenAPI への反映マップ](#openapi-への反映マップ)
|
||||
- [落とし穴](#落とし穴)
|
||||
|
||||
## 全フィールド一覧
|
||||
|
||||
[endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の `IEndpointMetaBase` 型より。
|
||||
|
||||
| フィールド | 型 | デフォルト | 用途 |
|
||||
|---|---|---|---|
|
||||
| `stability` | `'deprecated' \| 'experimental' \| 'stable'` | (未指定) | 安定度のヒント。`'deprecated'` を付けた API は新規利用を避ける |
|
||||
| `tags` | `ReadonlyArray<string>` | — | OpenAPI タグ。実質 `tags[0]` のみが反映される |
|
||||
| `errors` | `Record<key, { message, code, id }>` | — | クライアントに返す業務エラー定義。各 `id` は UUID v4 で一意 |
|
||||
| `res` | `Schema` (`@/misc/json-schema.js`) | — | レスポンス JSON Schema。`ref: 'Note'` のような packed entity 参照も可 |
|
||||
| `requireCredential` | `boolean` | `false` | 認証必須か。`true` のとき `kind` を必ず設定する |
|
||||
| `requireModerator` | `boolean` | `false` | isModerator ロール必須。`true` のとき `kind` 必須 |
|
||||
| `requireAdmin` | `boolean` | `false` | isAdministrator ロール必須。`true` のとき `kind` 必須 |
|
||||
| `requiredRolePolicy` | `KeyOf<'RolePolicies'>` | (未指定) | 特定のロールポリシー (例: `'canCreateChannel'`) を満たすロールを要求 |
|
||||
| `prohibitMoved` | `boolean` | `false` | アカウント移行済ユーザーを拒否 (主に write 系で検討) |
|
||||
| `limit` | `{ key?, duration?, max?, minInterval? }` | なし | レート制限。`duration` と `max` はセットで設定する |
|
||||
| `requireFile` | `boolean` | `false` | multipart/form-data でファイル添付必須。`true` だと `exec` の `file` 引数が確実に渡る |
|
||||
| `secure` | `boolean` | `false` | サードパーティアプリからは利用不可。OpenAPI に "Internal Endpoint" 表記が出る |
|
||||
| `kind` | `(typeof permissions)[number]` | — | OAuth スコープ。`'read:account'` / `'write:notes'` 等。型は require* 系と相互排他制約あり ([endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の型ユニオン定義) |
|
||||
| `description` | `string` | — | OpenAPI の operation description に入る |
|
||||
| `allowGet` | `boolean` | `false` | GET メソッドを許可するか (デフォルトは POST のみ)。冪等な read 系で有用 |
|
||||
| `cacheSec` | `number` | — | 正常応答に `Cache-Control: public, max-age=<秒>` を付与 |
|
||||
|
||||
## 権限制限フィールドの使い分け
|
||||
|
||||
[endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) で型ユニオンとして表現されており、組み合わせに制約がある:
|
||||
|
||||
| ケース | `requireCredential` | `requireModerator` | `requireAdmin` | `kind` |
|
||||
|---|---|---|---|---|
|
||||
| 認証不要 | `false` または省略 | (省略) | (省略) | 不要 |
|
||||
| 一般ユーザー認証必須 | `true` | (省略) | (省略) | **必須** (`'read:account'` 等) |
|
||||
| モデレーター以上必須 | (省略) | `true` | (省略) | **必須** (例: `'read:admin:show-user'`) |
|
||||
| 管理者必須 | (省略) | (省略) | `true` | **必須** (例: `'write:admin:emoji'`) |
|
||||
| Misskey 本体専用 (`secure: true`) | 任意 | 任意 | 任意 | **不要** (型 union で除外) |
|
||||
|
||||
**`secure: true` の例外**: [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の `secure: true` union variant は他の require* と独立しており、`kind` を要求しない。実例: [auth/accept.ts](../../../../../packages/backend/src/server/api/endpoints/auth/accept.ts) (`secure: true + requireCredential: true` で `kind` なし)、[i/export-user-lists.ts](../../../../../packages/backend/src/server/api/endpoints/i/export-user-lists.ts) も同様。サードパーティアプリから叩けないので OAuth scope の必要がない。
|
||||
|
||||
加えて以下も使える:
|
||||
|
||||
- **`requiredRolePolicy: 'canCreateChannel'`** — 特定のロールポリシーが許可されているユーザーだけに絞る。**`requireCredential: true` 必須**: [ApiCallService.ts](../../../../../packages/backend/src/server/api/ApiCallService.ts) が `requiredRolePolicy` 分岐で `user!.id` を非null前提アクセスするため、匿名許可と組み合わせると TypeError で 500 になる。匿名も許したいなら、`meta` ではなく実行時に `RoleService.getUserPolicies(me ? me.id : null)` で判定する ([endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) のパターン)。ポリシーの一覧は [`RolePolicies`](../../../../../packages/backend/src/core/RoleService.ts) を参照
|
||||
- **`secure: true`** — Misskey 本体フロントエンドからしか叩けないようにする (OAuth トークンで叩けなくなる)。上記の通り `kind` は不要
|
||||
|
||||
## `kind` の値
|
||||
|
||||
完全な一覧は [`packages/misskey-js/src/consts.ts`](../../../../../packages/misskey-js/src/consts.ts) の `permissions` 配列。代表例:
|
||||
|
||||
| パターン | 例 |
|
||||
|---|---|
|
||||
| 一般 read | `'read:account'`, `'read:notifications'`, `'read:drive'`, `'read:reactions'` |
|
||||
| 一般 write | `'write:account'`, `'write:notes'`, `'write:reactions'`, `'write:drive'` |
|
||||
| Admin read | `'read:admin:meta'`, `'read:admin:server-info'`, `'read:admin:show-user'`, `'read:admin:user-ips'` |
|
||||
| Admin write | `'write:admin:reset-password'`, `'write:admin:suspend-user'`, `'write:admin:emoji'`, `'write:admin:roles'` |
|
||||
|
||||
新しい操作領域を追加する場合は `consts.ts` の `permissions` 配列にも追加する必要がある。
|
||||
|
||||
## `errors` の書き方
|
||||
|
||||
```ts
|
||||
errors: {
|
||||
noSuchNote: { // ← キーは camelCase
|
||||
message: 'No such note.', // ← 英語ハードコード (バックエンドに i18n 機構なし)
|
||||
code: 'NO_SUCH_NOTE', // ← code は SCREAMING_SNAKE_CASE
|
||||
id: '17a0e0fa-3f3e-4f3e-9f3e-3f3e3f3e3f3e', // ← UUID v4。リポジトリ内で一意
|
||||
httpStatusCode: 404, // ← オプション。HTTP ステータスを上書き
|
||||
kind: 'client', // ← オプション。'client' (デフォルト) / 'server' / 'permission'
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
`httpStatusCode` と `kind` は [error.ts](../../../../../packages/backend/src/server/api/error.ts) の型 `E` 経由で受け付けられる。指定しないとデフォルト挙動 (クライアントエラーは 400 系) になる。
|
||||
|
||||
命名規則 (既存実装で一貫):
|
||||
|
||||
- キー: `camelCase` (`noSuchNote`, `cannotReRenote`, `alreadyBlocking`, `youHaveBeenBlocked`)
|
||||
- `code`: `SCREAMING_SNAKE_CASE` (`'NO_SUCH_NOTE'`, `'CANNOT_RENOTE_TO_A_PURE_RENOTE'`)
|
||||
- 接頭辞パターン: `NO_SUCH_*` / `CANNOT_*` / `ALREADY_*` / `TOO_MANY_*` / `INVALID_*` / `*_REQUIRED`
|
||||
|
||||
`throw new ApiError(meta.errors.noSuchNote, { reason: '詳細情報' })` の第 2 引数は `info` に入り、レスポンス JSON の `error.info` として返却される。
|
||||
|
||||
## `res` の書き方
|
||||
|
||||
JSON Schema または packed entity への参照:
|
||||
|
||||
```ts
|
||||
// 単純なオブジェクト
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
count: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
|
||||
// packed entity 参照
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note', // ← packages/backend/src/models/json-schema/*.ts の定義名
|
||||
},
|
||||
|
||||
// 配列
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
各プロパティに `optional: false, nullable: false` を **必ず明示する**。省略すると schema が緩くなり、生成される misskey-js 型も曖昧になる。
|
||||
|
||||
## `paramDef` (AJV) 実用パターン
|
||||
|
||||
`paramDef` は AJV (`new Ajv({ useDefaults: true })`) でコンパイルされた JSON Schema 7 互換のスキーマ。詳細は [endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の AJV 初期化を参照。
|
||||
|
||||
### カスタム format
|
||||
|
||||
**`format: 'misskey:id'`** だけが Misskey 独自 ([endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の `addFormat`):
|
||||
|
||||
```ts
|
||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
```
|
||||
|
||||
その他 (`'date-time'`, `'email'`, `'url'` 等) は JSON Schema 標準。AJV はデフォルトでは format 検証を行わないが、Misskey の AJV 設定ではフォーマット名はバリデーションエラーを出さず通過する程度の動作になっている (ID パターンのみ実際に正規表現検証される)。
|
||||
|
||||
### 基本パターン
|
||||
|
||||
```ts
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' }, // 必須 ID
|
||||
text: { type: 'string', minLength: 1, maxLength: 500 }, // 文字長制約
|
||||
count: { type: 'integer', minimum: 0, maximum: 100, default: 10 },
|
||||
isPublic: { type: 'boolean', default: false },
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
```
|
||||
|
||||
`as const` を必ず付ける。これで `SchemaType<typeof paramDef>` が型推論される。
|
||||
|
||||
### ページネーション (sinceId / untilId / limit)
|
||||
|
||||
[notes/timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/timeline.ts):
|
||||
|
||||
```ts
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
```
|
||||
|
||||
`QueryService.makePaginationQuery(qb, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)` で TypeORM クエリビルダに反映する。
|
||||
|
||||
### 配列とアイテム制約
|
||||
|
||||
```ts
|
||||
properties: {
|
||||
// 一意・最小1・最大100 個のID リスト
|
||||
noteIds: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
実例: [notes/show-partial-bulk.ts](../../../../../packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts) (`noteIds`), [notes/drafts/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/drafts/create.ts) (`fileIds` / `visibleUserIds` は `uniqueItems` 付き)
|
||||
|
||||
### `oneOf` / `anyOf` (排他的選択)
|
||||
|
||||
複数のリクエストパラメータ形態を許す場合:
|
||||
|
||||
```ts
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
username: { type: 'string' },
|
||||
host: { type: 'string', nullable: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
{ required: ['username'] },
|
||||
],
|
||||
```
|
||||
|
||||
`res` 側でも `oneOf` を使ってバリアントレスポンスを表現できる ([ap/show.ts](../../../../../packages/backend/src/server/api/endpoints/ap/show.ts) の `res`):
|
||||
|
||||
```ts
|
||||
res: {
|
||||
optional: false, nullable: false,
|
||||
oneOf: [
|
||||
{ type: 'object', properties: { type: { enum: ['User'] }, object: { ref: 'UserDetailedNotMe' } } },
|
||||
{ type: 'object', properties: { type: { enum: ['Note'] }, object: { ref: 'Note' } } },
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
### `additionalProperties` (動的キー)
|
||||
|
||||
固定の `properties` ではなく「任意のキー → 値の型」を表すとき:
|
||||
|
||||
```ts
|
||||
data: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
anyOf: [{ type: 'number' }],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
実例: [retention.ts](../../../../../packages/backend/src/server/api/endpoints/retention.ts), [admin/get-table-stats.ts](../../../../../packages/backend/src/server/api/endpoints/admin/get-table-stats.ts)
|
||||
|
||||
`type: 'object', additionalProperties: true` だと「任意の中身を受け入れる」(検証なし) になる。
|
||||
|
||||
### `default` (値補完)
|
||||
|
||||
AJV を `useDefaults: true` で構築しているため、`default` を書くとリクエストに値が無い場合に自動で埋まる:
|
||||
|
||||
```ts
|
||||
properties: {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
},
|
||||
```
|
||||
|
||||
クライアントの省略を吸収できるため、後方互換変更で重宝する。
|
||||
|
||||
### nullable プロパティ
|
||||
|
||||
```ts
|
||||
properties: {
|
||||
parentId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
```
|
||||
|
||||
`nullable: true` を付けると `null` を明示的に受け付ける。
|
||||
|
||||
## OpenAPI への反映マップ
|
||||
|
||||
[gen-spec.ts](../../../../../packages/backend/src/server/api/openapi/gen-spec.ts) より:
|
||||
|
||||
| meta フィールド | OpenAPI への反映 |
|
||||
|---|---|
|
||||
| `description` | operation description (先頭) |
|
||||
| `secure: true` | description に "**Internal Endpoint**: ..." の警告 |
|
||||
| `requireCredential: true` | description に "**Credential required**: *Yes*" + `security: [bearerAuth]` |
|
||||
| `kind` | description に "**Permission**: *<kind>*" |
|
||||
| `tags[0]` | operation tag (実質 1 個目のみ) |
|
||||
| `requireFile: true` | requestBody が `multipart/form-data` になり `file: { type: 'string', format: 'binary' }` が追加される |
|
||||
| `errors` | examples (operation の `responses` 配下) |
|
||||
| `res` | response body schema |
|
||||
| `limit` | `429 Too many requests` レスポンスが `responses` に追加される |
|
||||
| `allowGet` | 同一 path に `get` operation が追加される (POST と両方が生える) |
|
||||
|
||||
**OpenAPI に反映されない (内部のみ)**: `requireModerator` / `requireAdmin` / `requiredRolePolicy` / `prohibitMoved` / `cacheSec` / `stability`。
|
||||
|
||||
## 落とし穴
|
||||
|
||||
PR レビューで頻発するミスを「**症状 → 原因 → 修正**」で集めた。
|
||||
|
||||
### 1. エンドポイントが 404 になる
|
||||
|
||||
- **症状**: 開発サーバーで叩くと `{"error": {"code": "UNKNOWN_API_ENDPOINT", ...}}` (GET の catch-all 経由)、または素の 404 (POST など)
|
||||
- **原因**: [endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) への登録漏れ。エンドポイントは glob 自動収集されない
|
||||
- **修正**: → [knowledge/endpoint-list.md](endpoint-list.md)
|
||||
|
||||
### 2. CI `check-misskey-js-autogen` で落ちる
|
||||
|
||||
- **症状**: PR に `Please regenerate misskey-js` のコメント
|
||||
- **原因**: `meta` / `paramDef` / `res` を変えたのに misskey-js の自動生成物を再生成していない
|
||||
- **修正**: → [shipping-misskey-change/references/tasks/regenerate-misskey-js.md](../../../shipping-misskey-change/references/tasks/regenerate-misskey-js.md)
|
||||
|
||||
### 3. CI `spdx` ジョブで落ちる
|
||||
|
||||
- **症状**: `SPDX header missing` のメッセージ
|
||||
- **原因**: 新規 `.ts` ファイルに SPDX ヘッダーが無い
|
||||
- **修正**: ファイル冒頭に SPDX を貼る。注: `packages/misskey-js/` 配下は MIT 別ライセンスなので SPDX 不要
|
||||
|
||||
### 4. クライアントが 500 + error 型不在 を受け取る
|
||||
|
||||
- **症状**: フロントエンド側で `result.error.code` を分岐したいが、misskey-js の型に出てこない。レスポンスは 500
|
||||
- **原因**: `meta.errors` に列挙していないエラーを `throw new ApiError({...})` または `throw new Error(...)` した
|
||||
- **修正**: 業務エラーは必ず `meta.errors` に登録してから `throw new ApiError(meta.errors.<key>)`
|
||||
- **逆方向の罠**: 「想定外バグまで全部 `ApiError` で包む」のもダメ。`endpoints/notes/create.ts` の `catch` 節末尾の `throw err;` が手本
|
||||
|
||||
### 5. `me.id` で `Cannot read properties of null`
|
||||
|
||||
- **症状**: 認証なしリクエストで TypeError
|
||||
- **原因**: `requireCredential: false` のとき `me` は `MiLocalUser | null` なのに null チェックなしで `me.id` を使った
|
||||
- **修正**: null チェックを入れるか、認証必須なら `requireCredential: true` に変更
|
||||
|
||||
### 6. UUID が他エンドポイントと衝突
|
||||
|
||||
- **症状**: `errors.id` を再利用してしまうと misskey-js 側で型が混線
|
||||
- **原因**: UUID をハードコードして再利用
|
||||
- **修正**: 衝突確認
|
||||
|
||||
```bash
|
||||
grep -r "id: '<生成した UUID>'" packages/backend/src/server/api/endpoints/
|
||||
```
|
||||
|
||||
新規生成は `node -e "console.log(crypto.randomUUID())"`
|
||||
|
||||
### 7. `paramDef` に `policies` を書く
|
||||
|
||||
- **症状**: 「`gtlAvailable: true` を payload で渡してください」のような不自然な API になっている / クライアントが指定したらバイパスできる
|
||||
- **原因**: ロールポリシーは **動的に取得するもの**
|
||||
- **修正**: paramDef からは外し、`exec` 内で `RoleService.getUserPolicies(me?.id)` を呼んで判定する
|
||||
|
||||
### 8. エラーメッセージを日本語で書く
|
||||
|
||||
- **症状**: `message: 'ノートが見つかりません'` のような日本語が i18n されずクライアントに渡る
|
||||
- **原因**: バックエンドに i18n 機構が無い
|
||||
- **修正**: `message` は英語ハードコードに統一。フロントエンドは `error.id` (UUID) または `error.code` をキーに自前で localize する
|
||||
|
||||
### 9. `as const` を忘れる
|
||||
|
||||
- **症状**: `Endpoint<typeof meta, typeof paramDef>` の型推論が壊れて `ps` の型が `any` になる
|
||||
- **修正**: `export const meta = { ... } as const;` と `export const paramDef = { ... } as const;` を必ず付ける
|
||||
|
||||
### 10. `requireCredential: true` なのに `kind` を書き忘れる
|
||||
|
||||
- **症状**: TypeScript の型エラー (`Property 'kind' is missing`)
|
||||
- **原因**: [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) のユニオン制約で `kind` が型レベルで必須
|
||||
- **修正**: 適切な OAuth スコープを `kind` に設定する
|
||||
- **例外**: `secure: true` (Misskey 本体専用) のエンドポイントは [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の別 union variant 扱いで `kind` 不要
|
||||
|
||||
### 11. `requireFile: true` の cleanup を呼び忘れて一時ファイルが残る
|
||||
|
||||
- **症状**: アップロード後にエンドポイントが正常終了/例外終了しても OS の一時ディレクトリにファイルが残り続け、ディスクが埋まる
|
||||
- **原因**: [endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) が `cleanup` を自動で呼ぶのは **AJV バリデーション失敗時のみ**
|
||||
- **修正**: `try { ... } finally { cleanup!(); }` で囲む ([drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) の `finally { cleanup!(); }` が手本)
|
||||
|
||||
### 12. `requiredRolePolicy` だけで匿名許可してしまう
|
||||
|
||||
- **症状**: API を匿名で叩くと 500 + `TypeError: Cannot read properties of null (reading 'id')`
|
||||
- **原因**: [ApiCallService.ts](../../../../../packages/backend/src/server/api/ApiCallService.ts) が `requiredRolePolicy` ありのエンドポイントで `user!.id` を非null前提でアクセス
|
||||
- **修正**: 静的に必須ポリシーを宣言するなら `requireCredential: true` と必ず併用する。匿名ユーザーにも違うポリシーセットを適用したいなら、実行時に `RoleService.getUserPolicies(me ? me.id : null)` で判定 ([notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) パターン)
|
||||
|
||||
### 13. e2e テストが起動しない
|
||||
|
||||
- **症状**: `pnpm --filter backend test:e2e` 実行直後にこける / DB 接続エラー
|
||||
- **原因**: `.config/test.yml` が無い
|
||||
- **修正**: → [knowledge/backend-testing.md §前提](backend-testing.md)
|
||||
@@ -0,0 +1,209 @@
|
||||
# Backend テストの前提と書き方
|
||||
|
||||
Misskey backend のテスト構成、`.config/test.yml` の前提、e2e テストのヘルパー関数集を 1 つにまとめたページ。
|
||||
|
||||
## 目次
|
||||
|
||||
- [前提: `.config/test.yml`](#前提-configtestyml)
|
||||
- [テスト種別と実行コマンド](#テスト種別と実行コマンド)
|
||||
- [e2e テストの配置](#e2e-テストの配置)
|
||||
- [共通 setup](#共通-setup)
|
||||
- [`api()` ヘルパー](#api-ヘルパー)
|
||||
- [`signup()` / `post()` / `uploadFile()` 等](#signup--post--uploadfile-等)
|
||||
- [ローカル DB / Redis](#ローカル-db--redis)
|
||||
|
||||
## 前提: `.config/test.yml`
|
||||
|
||||
backend のテストスクリプト (`test` / `test:e2e` / `test:fed`) はすべて内部で `cross-env NODE_ENV=test pnpm compile-config` を実行し、`.config/test.yml` を読み込む ([packages/backend/package.json](../../../../../packages/backend/package.json), [packages/backend/scripts/compile_config.js](../../../../../packages/backend/scripts/compile_config.js))。**未作成だとテスト自体が起動しない**。
|
||||
|
||||
未作成なら以下を 1 回だけ手動コピーする (どちらでも可):
|
||||
|
||||
```bash
|
||||
ncp .github/misskey/test.yml .config/test.yml
|
||||
# または
|
||||
cp .github/misskey/test.yml .config/test.yml
|
||||
```
|
||||
|
||||
補足:
|
||||
|
||||
- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
|
||||
- すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない
|
||||
- `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと)
|
||||
|
||||
## テスト種別と実行コマンド
|
||||
|
||||
| 種別 | 設定ファイル | 実行コマンド |
|
||||
| --- | --- | --- |
|
||||
| Unit | `packages/backend/vitest.config.unit.ts` | `pnpm --filter backend test` |
|
||||
| E2E (HTTP / DB) | `packages/backend/vitest.config.e2e.ts` | `pnpm --filter backend test:e2e` |
|
||||
| Federation | `packages/backend/vitest.config.fed.ts` | `pnpm --filter backend test:fed` |
|
||||
|
||||
- 配置: `packages/backend/test/` 配下
|
||||
- カバレッジ: `pnpm --filter backend test-and-coverage`
|
||||
|
||||
## e2e テストの配置
|
||||
|
||||
`packages/backend/test/e2e/` の現状ファイル例:
|
||||
|
||||
```
|
||||
note.ts ノート関連 (作成・renote・visibility・添付ファイル等)
|
||||
users.ts ユーザー関連
|
||||
timelines.ts タイムライン
|
||||
drive.ts ドライブ (アップロード/ダウンロード)
|
||||
clips.ts クリップ
|
||||
oauth.ts OAuth フロー
|
||||
streaming.ts WebSocket
|
||||
api.ts API レイヤ全般 (認証・レート制限など)
|
||||
api-visibility.ts 公開範囲チェック
|
||||
endpoints.ts 上記カテゴリに収まらない雑多なもの
|
||||
2fa.ts 2FA
|
||||
block.ts / mute.ts / antennas.ts / clips.ts / move.ts / nodeinfo.ts / ...
|
||||
```
|
||||
|
||||
**`admin.ts` は存在しない**。admin 系エンドポイントの e2e は `api.ts` (API レイヤ挙動として) または `endpoints.ts` (雑多枠) に置くのが現実的。
|
||||
|
||||
### 判断ルール
|
||||
|
||||
1. 自分の追加するエンドポイントが既存カテゴリファイル (`note.ts`, `users.ts` 等) に所属するなら、そこに `describe('...', () => { test(...) })` を追加
|
||||
2. どのカテゴリにも収まらないなら `endpoints.ts` に追加
|
||||
3. テストケースが多くなり (>200 行)、独立性が高い場合のみ新ファイル化
|
||||
|
||||
`describe` のラベル名は **人間可読** で OK (`describe('Note', ...)`, `describe('管理者操作', ...)` のような形式)。`<category>/<name>` 形式である必要はない。
|
||||
|
||||
## 共通 setup
|
||||
|
||||
`packages/backend/test/setup.e2e.ts` (vitest の `setupFiles`) が各テストファイル共通の `beforeAll` (テスト DB 初期化 + 環境リセット) を登録する。テストサーバーの起動/停止は別途 vitest の `globalSetup` (`test-server/entry.ts` の `setup()` / `teardown()`) が担う。各テストファイルでは自前の `beforeAll` でユーザーを用意する:
|
||||
|
||||
```ts
|
||||
import { describe, test, beforeAll, afterAll } from 'vitest';
|
||||
import * as assert from 'node:assert';
|
||||
import { api, signup, post, role, uploadFile } from '../utils.js';
|
||||
import type { UserToken } from '../utils.js';
|
||||
|
||||
describe('機能名', () => {
|
||||
let alice: UserToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
alice = await signup({ username: 'alice' });
|
||||
});
|
||||
|
||||
test('正常系', async () => {
|
||||
const res = await api('<category>/<name>', { /* params */ }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## `api()` ヘルパー
|
||||
|
||||
[test/utils.ts](../../../../../packages/backend/test/utils.ts) の `api()`:
|
||||
|
||||
```ts
|
||||
const res = await api('<category>/<name>', params, me?);
|
||||
// res.status : HTTP ステータス (200 / 400 / 401 / 403 / 500 等)
|
||||
// res.headers : Headers
|
||||
// res.body : レスポンス JSON (型は misskey.Endpoints から自動推論)
|
||||
```
|
||||
|
||||
`me?` を省略すると未認証リクエスト。`me` を渡すとそのユーザーの token で叩く。
|
||||
|
||||
### エラーレスポンスの検証
|
||||
|
||||
```ts
|
||||
test('存在しないノートで怒られる', async () => {
|
||||
const res = await api('notes/show', { noteId: '0000000000000000' }, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(castAsError(res.body as any).error.code, 'NO_SUCH_NOTE');
|
||||
});
|
||||
```
|
||||
|
||||
`castAsError(...).error.code` で `meta.errors.<key>.code` を検証できる ([test/utils.ts](../../../../../packages/backend/test/utils.ts) の `castAsError`)。
|
||||
|
||||
## `signup()` / `post()` / `uploadFile()` 等
|
||||
|
||||
### `signup()` — テストユーザー作成
|
||||
|
||||
```ts
|
||||
const alice = await signup({ username: 'alice' }); // 既定パスワード 'test'
|
||||
const bob = await signup({ username: 'bob', password: 'secret123' });
|
||||
```
|
||||
|
||||
戻り値はサインアップレスポンス (token を含む) で、`api()` の第 3 引数にそのまま渡せる。
|
||||
|
||||
### `post()` — ノート投稿
|
||||
|
||||
```ts
|
||||
const note = await post(alice, { text: 'hello' });
|
||||
// 戻り値は misskey.entities.Note
|
||||
```
|
||||
|
||||
複雑な公開範囲・添付ファイル付きでも `post(alice, { text: ..., visibility: 'specified', visibleUserIds: [...], fileIds: [...] })` のように渡せる。
|
||||
|
||||
### `uploadFile()` — ドライブにファイルアップロード
|
||||
|
||||
```ts
|
||||
const file = await uploadFile(alice); // resources/192.jpg をアップロード
|
||||
const file2 = await uploadFile(alice, { path: '192.png' }); // resources/192.png
|
||||
const file3 = await uploadFile(alice, { blob: new Blob([...]) }); // 任意 Blob
|
||||
// file.body.id を fileIds に渡せる
|
||||
```
|
||||
|
||||
### `role()` — ロール作成 + アサイン
|
||||
|
||||
[test/utils.ts](../../../../../packages/backend/test/utils.ts) の `role()`:
|
||||
|
||||
```ts
|
||||
const myRole = await role(adminUser, { name: 'tester' }, { canCreateChannel: { useDefault: false, priority: 0, value: true } });
|
||||
// admin/roles/create を叩く。policies 引数で個別ポリシーを上書き可能
|
||||
```
|
||||
|
||||
モデレーター・管理者ロールが要るテストは事前に `signup({ ... })` + `role(...)` で作る。
|
||||
|
||||
### `createAppToken()` — アプリ scope 付きトークン
|
||||
|
||||
```ts
|
||||
const token = await createAppToken(alice, ['write:notes', 'read:account']);
|
||||
// token は文字列。api() の me.token として使うか、{ token, bearer: true } で渡せば Bearer Auth で叩く
|
||||
```
|
||||
|
||||
OAuth scope (`kind`) のテストに使う。
|
||||
|
||||
### その他のヘルパー
|
||||
|
||||
[test/utils.ts](../../../../../packages/backend/test/utils.ts) には以下も用意されている:
|
||||
|
||||
- `userList()` — ユーザーリスト作成
|
||||
- `page()` / `play()` — Page / Flash 作成
|
||||
- `clip()` / `galleryPost()` / `channel()` — 各種リソース作成
|
||||
- `react()` — リアクション
|
||||
- `simpleGet()` — fetch ラッパ (raw HTTP)
|
||||
- `testPaginationConsistency()` — ページネーション挙動の網羅検証
|
||||
- `sendEnvUpdateRequest()` / `sendEnvResetRequest()` — テスト用環境変数の更新
|
||||
- `connectStream()` / `waitFire()` — WebSocket (Streaming API)
|
||||
|
||||
詳細はソースを直接参照。
|
||||
|
||||
### 既存テスト例
|
||||
|
||||
- [test/e2e/note.ts](../../../../../packages/backend/test/e2e/note.ts) — `describe('Note', ...)` で多数の `test(...)` を並べる伝統的なスタイル
|
||||
- [test/e2e/endpoints.ts](../../../../../packages/backend/test/e2e/endpoints.ts) — カテゴリ不問の雑多なエンドポイント
|
||||
- [test/e2e/api.ts](../../../../../packages/backend/test/e2e/api.ts) — API レイヤ (認証・レート制限) の挙動
|
||||
|
||||
## ローカル DB / Redis
|
||||
|
||||
backend の **テスト** と **開発** では用途別に別の compose ファイルを使う。ポートが異なるので混同すると接続できない。
|
||||
|
||||
| 用途 | compose ファイル | host ポート (db / redis) |
|
||||
| --- | --- | --- |
|
||||
| テスト (`test` / `test:e2e` / `test:fed`) | [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml) | `54312` / `56312` ([.github/misskey/test.yml](../../../../../.github/misskey/test.yml) のポート設定と一致) |
|
||||
| 開発 (`pnpm dev` 等) | `compose.local-db.yml` (リポジトリルート) | `5432` / `6379` |
|
||||
|
||||
```bash
|
||||
# テスト用 DB / Redis (テスト時はこちら)
|
||||
docker compose -f packages/backend/test/compose.yml up -d
|
||||
|
||||
# 開発用 DB / Redis (Misskey 本体は起動せず postgres / redis / meilisearch だけ立てる)
|
||||
docker compose -f compose.local-db.yml up -d
|
||||
```
|
||||
|
||||
`compose.local-db.yml` は開発向け (標準ポート `5432` / `6379`) で、テスト用 DB (`test-misskey` / ポート `54312` / `56312`) とは別物。CI (`.github/workflows/test-backend.yml`) は docker compose ではなく GitHub Actions の `services:` で同じテスト用ポートの postgres / redis コンテナを立ててから走る。
|
||||
@@ -0,0 +1,50 @@
|
||||
# `endpoint-list.ts` への登録
|
||||
|
||||
新規 API endpoint を追加する際の **最大の落とし穴**。エンドポイントは glob 自動収集されないため、ここへの 1 行追加を忘れると 404 になる。
|
||||
|
||||
## なぜ必要か
|
||||
|
||||
[`packages/backend/src/server/api/EndpointsModule.ts`](../../../../../packages/backend/src/server/api/EndpointsModule.ts) が [`endpoint-list.ts`](../../../../../packages/backend/src/server/api/endpoint-list.ts) の全エクスポートを `Object.entries()` で反復し、NestJS provider (`provide: 'ep:<path>'`) を生成している。**このリストが API ルーティングの単一の真実** で、ここに無いものは存在しないものとして扱われる。
|
||||
|
||||
## 登録方法
|
||||
|
||||
[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) の **同カテゴリ内** に 1 行追加する:
|
||||
|
||||
```ts
|
||||
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
|
||||
```
|
||||
|
||||
`<category>` は機能領域 (`notes`, `users`, `admin/announcements` 等)、`<name>` はエンドポイント名 (`create`, `show`, `delete` 等)。両方ともケバブケース / スラッシュ区切りで、ファイルシステムのパス構造と一致する。
|
||||
|
||||
例: `endpoints/notes/create.ts` を追加するなら:
|
||||
|
||||
```ts
|
||||
export * as 'notes/create' from './endpoints/notes/create.js';
|
||||
```
|
||||
|
||||
## 並び順
|
||||
|
||||
**並び順は厳密ではない**。同じディレクトリ (例: `admin/queue/*`) の中でも、アルファベット順ではなく追加された経緯どおりの順になっている箇所が多い。
|
||||
|
||||
- **新規追加**: 同カテゴリ内の末尾に追加すれば OK
|
||||
- **既存近傍**: 同カテゴリ内の関連エンドポイントの近くに置く判断もあり
|
||||
- **過度に整理しない**: 既存の並びを全部 sort し直すような PR は不要 (review コストだけ増える)
|
||||
|
||||
## 登録確認
|
||||
|
||||
ファイルを追加した後、grep で 1 行存在することを確認する:
|
||||
|
||||
```bash
|
||||
grep -F "'<category>/<name>'" packages/backend/src/server/api/endpoint-list.ts
|
||||
```
|
||||
|
||||
ヒットしなければ登録漏れ。
|
||||
|
||||
## 既存例 (登録漏れに気づくための grep 例)
|
||||
|
||||
`endpoint-list.ts` の冒頭コメントに「このリストが API ルーティングの単一の真実」という旨が記載されている。新規開発時はこのファイルを開いてカテゴリ単位の構造を把握してから新規 endpoint ファイルを書くのが効率的。
|
||||
|
||||
## 関連
|
||||
|
||||
- 新規 endpoint 追加の全手順 → [tasks/adding-api-endpoint.md](../tasks/adding-api-endpoint.md)
|
||||
- NestJS DI / module 構造 → [nestjs-di.md](nestjs-di.md)
|
||||
@@ -0,0 +1,97 @@
|
||||
# NestJS DI / module 登録パターン
|
||||
|
||||
Misskey の backend は NestJS 11 + Fastify 5 + TypeORM 1 (PostgreSQL) + Redis の構成。DI コンテナと Repository パターンが軸。
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
- **DI コンテナ**: NestJS の `@Injectable()` サービス + Repository (TypeORM) パターン
|
||||
- **DI トークン**: [`@/di-symbols.js`](../../../../../packages/backend/src/di-symbols.ts) の `DI` から `@Inject(DI.xxx)` で注入
|
||||
- **ビルド**: `rolldown -c` で `built/` にバンドル。型チェックは `tsgo`
|
||||
|
||||
## エンドポイント内での DI
|
||||
|
||||
API endpoint は `Endpoint<typeof meta, typeof paramDef>` を extends するクラスとして書く。`@Injectable()` を付けてコンストラクタで Repository / Service を `@Inject(DI.xxx)` で注入する。
|
||||
|
||||
```ts
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
// 他にも RoleService, UserEntityService, GlobalEventService 等を必要なだけ inject
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// this.notesRepository.findOneBy(...) のように使う
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`// eslint-disable-line import/no-default-export` は Endpoint のお約束 (NestJS が default export を要求する一方で、ESLint ルールでは制約されているため)。
|
||||
|
||||
## 主要 DI トークン
|
||||
|
||||
`@/di-symbols.js` から提供される。代表例:
|
||||
|
||||
| トークン | 型 | 用途 |
|
||||
|---|---|---|
|
||||
| `DI.notesRepository` | `NotesRepository` | notes テーブルの TypeORM Repository |
|
||||
| `DI.usersRepository` | `UsersRepository` | users テーブル |
|
||||
| `DI.driveFilesRepository` | `DriveFilesRepository` | drive_file テーブル |
|
||||
| `DI.config` | `Config` | アプリ設定 |
|
||||
| `DI.redis` | `Redis` | Redis クライアント |
|
||||
| `DI.db` | `DataSource` | TypeORM DataSource (raw SQL を打ちたい時) |
|
||||
|
||||
Service 系 (例: `NoteCreateService`, `RoleService`, `UserEntityService`) は **トークン経由ではなく型をそのまま inject** する:
|
||||
|
||||
```ts
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {}
|
||||
```
|
||||
|
||||
## Service クラスの書き方
|
||||
|
||||
Service は `@Injectable()` を付け、必要な依存をコンストラクタで宣言する。NestJS の module (`packages/backend/src/core/CoreModule.ts` 等) に provider として登録される必要がある。
|
||||
|
||||
```ts
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {}
|
||||
|
||||
async doSomething(noteId: string) {
|
||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
新規 Service を追加する場合は **module 側の `providers` 配列にも追加** する必要がある。既存 Service が `CoreModule` に登録されているか確認するのが手っ取り早い。
|
||||
|
||||
## Module 構造
|
||||
|
||||
主要 module は以下:
|
||||
|
||||
- **CoreModule** (`src/core/CoreModule.ts`) — Service 群を集約
|
||||
- **EndpointsModule** (`src/server/api/EndpointsModule.ts`) — endpoint-list.ts を `Object.entries()` で反復して NestJS provider (`provide: 'ep:<path>'`) を自動生成
|
||||
- **GlobalModule** (`src/GlobalModule.ts`) — Repository / Config / Redis / DataSource など低レベル依存
|
||||
- **QueueModule** (`src/core/QueueModule.ts`) — BullMQ ジョブキュー
|
||||
|
||||
新規 endpoint 追加時に module への明示的な登録は不要 ([knowledge/endpoint-list.md](endpoint-list.md) 参照)。新規 Service 追加時は CoreModule (または該当 module) に provider 登録が必要。
|
||||
|
||||
## 既存例 (DI / 例外処理が綺麗な参考実装)
|
||||
|
||||
- [endpoints/notes/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts) — Service を型注入 (`NoteEntityService` / `NoteCreateService`) + `meta.errors` + `try/catch` で業務エラー変換 + 末尾 `throw err;` の二段構え
|
||||
- [endpoints/i/pin.ts](../../../../../packages/backend/src/server/api/endpoints/i/pin.ts) — `.catch(err => { ... throw err; })` で同様にエラー変換
|
||||
- [endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) — `RoleService.getUserPolicies()` で動的ポリシー判定
|
||||
@@ -0,0 +1,160 @@
|
||||
# TypeORM / migration パターン
|
||||
|
||||
Misskey backend は TypeORM 1 + PostgreSQL。エンティティ定義と migration の関係、そして migration で踏みうる難ケースをまとめる。
|
||||
|
||||
## モデル / Repository
|
||||
|
||||
- エンティティ: `packages/backend/src/models/<Name>.ts` (`@Entity` + `@Column`)
|
||||
- DI 経由で注入される Repository を経由してアクセス (`@Inject(DI.notesRepository)` 等) → [nestjs-di.md](nestjs-di.md)
|
||||
|
||||
エンティティ側の `@Column` / `@Entity` / `@Index` 変更は migration の DDL と整合させる必要がある。`pnpm --filter backend check-migrations` がエンティティと migration の不一致を検出する ([scripts/check_migrations_clean.js](../../../../../packages/backend/scripts/check_migrations_clean.js))。
|
||||
|
||||
## migration ファイルの構造
|
||||
|
||||
各ファイル `packages/backend/migration/{unixMs}-{descriptive-name}.js` は ESM JS。最小形:
|
||||
|
||||
```js
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class PascalCaseName1234567890123 {
|
||||
name = 'PascalCaseName1234567890123'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`...`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`...`); // up の完全な巻き戻し
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
詳細手順は [tasks/creating-migration.md](../tasks/creating-migration.md) を参照。**マージ済 migration の編集は絶対禁止**。
|
||||
|
||||
## CONCURRENTLY (CREATE INDEX CONCURRENTLY) の扱い
|
||||
|
||||
大規模テーブルへの `CREATE INDEX` は本番で長時間ロックする恐れがある。`CONCURRENTLY` で発行するときは migration class に **「この migration は transaction を張らない」と指示する** 必要がある。PostgreSQL は `CREATE INDEX CONCURRENTLY` を transaction 内で実行できないため。
|
||||
|
||||
参照実装: [migration/1745378064470-composite-note-index.js](../../../../../packages/backend/migration/1745378064470-composite-note-index.js)
|
||||
|
||||
```js
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
|
||||
export class CompositeNoteIndex1745378064470 {
|
||||
name = 'CompositeNoteIndex1745378064470';
|
||||
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
|
||||
|
||||
async up(queryRunner) {
|
||||
const concurrently = isConcurrentIndexMigrationEnabled;
|
||||
if (concurrently) {
|
||||
// CREATE INDEX CONCURRENTLY ...
|
||||
} else {
|
||||
// CREATE INDEX ...
|
||||
}
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// 同様に環境変数で分岐
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **`transaction = isConcurrentIndexMigrationEnabled ? false : undefined;`** が必須。これがないと `CREATE INDEX CONCURRENTLY` が transaction 内で実行されて `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block` で失敗
|
||||
- 環境変数 `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` がデフォルト OFF。OFF のときは普通の `CREATE INDEX` (transaction 内) で動く必要がある。`up`/`down` 双方を環境変数で分岐させる
|
||||
- `ormconfig.js` の `migrationsTransactionMode` は **環境変数で切り替わる**: `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` のときだけ `'each'`、未設定時は `'all'` (全 migration を 1 つの transaction でラップ) ([ormconfig.js](../../../../../packages/backend/ormconfig.js) の `migrationsTransactionMode`)。普段は `'all'` 前提
|
||||
|
||||
## migration 難ケース集
|
||||
|
||||
`migration:generate` / 手書きどちらでも踏み外しやすいパターンを「**なぜ危険か → up の形 → down 戦略 → 参照実装**」でまとめる。
|
||||
|
||||
共通の鉄則: `down()` は `up()` の **完全な巻き戻し**。下記ケースは「単純な逆 SQL では戻らない」ものが多い。
|
||||
|
||||
### 1. NOT NULL 列の追加
|
||||
|
||||
**なぜ危険か**: 既存行があるテーブルに `NOT NULL` 列を `DEFAULT` 無しで足すと、既存行を埋められず `ALTER TABLE` が失敗する。
|
||||
|
||||
- **既定値で良い場合** — `DEFAULT` を付ければ 1 文で済む。これが最も多い
|
||||
|
||||
```js
|
||||
// up
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`);
|
||||
// down
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`);
|
||||
```
|
||||
|
||||
参照: [migration/1758677617888-scheduled-post.js](../../../../../packages/backend/migration/1758677617888-scheduled-post.js)
|
||||
|
||||
- **行ごとに計算した値で埋めたい / 既定値を後で外したい場合** — 3 段に分ける: ①nullable で追加 → ②`UPDATE` でバックフィル (ケース 3 参照) → ③`ALTER COLUMN ... SET NOT NULL`。`down` は `DROP COLUMN` で良い。巨大テーブルでは ② の `UPDATE` と ③ の `SET NOT NULL` (全行スキャン) が長時間ロックし得る点に注意
|
||||
|
||||
**補足:** エンティティ側で `@Column({ default: ... })` を付けると `migration:generate` が `DEFAULT` 付き DDL を出す。アプリ実行時に常に値を入れるので DB 既定値が不要なら、生成後に `DEFAULT` 句だけ手で外す判断もある (既存 migration には両スタイルある)。
|
||||
|
||||
### 2. enum 型の値の追加・変更
|
||||
|
||||
**なぜ危険か**: PostgreSQL の enum は **値を削除できない** (`ALTER TYPE ... DROP VALUE` は存在しない) ため、`ADD VALUE` した変更を素直に巻き戻せない。さらに Misskey はデフォルトで migration 全体を 1 トランザクションにまとめる (`migrationsTransactionMode: 'all'`) ので、`ADD VALUE` で足した値を同一トランザクション内で使う処理もエラーになる。そこで TypeORM `migration:generate` は **「旧型を rename → 新型を CREATE → 列を新型へ ALTER (USING キャスト) → 旧型を DROP」** という巻き戻し可能な手順を出す。手書きでもこの形に従うこと。
|
||||
|
||||
```js
|
||||
// up: 値 'app' を追加する例 (新値を含む型へ載せ替える)
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', /* ... */ 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
```
|
||||
|
||||
```js
|
||||
// down: 新値を含まない旧い値集合へ同じ手順で戻す
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', /* ... 'app' を除く ... */)`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
```
|
||||
|
||||
要点: ①列がデフォルトを持つ場合は ALTER 前に `DROP DEFAULT`、ALTER 後に `SET DEFAULT` を挟む。②配列列 (`mutingNotificationTypes` 等) は `TYPE "..."[] USING "col"::"text"::"..."[]` と配列キャストにする。③**`down` の落とし穴**: 削除する値を既存行が使っていると `USING` キャストが「該当 enum に存在しない」で失敗する。新値を追加しただけの直後の巻き戻しは安全だが、運用後に使われた値を消す巻き戻しは本質的に危うい — その場合は down で先に `UPDATE ... SET "type" = '<代替値>' WHERE "type" = '<消す値>'` で退避してからキャストする。
|
||||
|
||||
参照: [migration/1674118260469-achievement.js](../../../../../packages/backend/migration/1674118260469-achievement.js) (rename/recreate の完全な up/down)。型の新規作成は [migration/1580276619901-v12-10.js](../../../../../packages/backend/migration/1580276619901-v12-10.js)。
|
||||
|
||||
### 3. データ移行 (UPDATE バックフィル)
|
||||
|
||||
**なぜ危険か**: migration 内の `UPDATE` は本番の全行を触る可能性がある。大量行では長時間ロック・トランザクション肥大を招く。
|
||||
|
||||
- 既定値を入れるだけなら `UPDATE ... WHERE col IS NULL` で冪等に書く。複数回流れても安全な形にする
|
||||
- 巨大テーブルの全行更新は避けるのが基本。どうしても必要なら CONCURRENTLY 同様にバッチ分割や別運用を検討し、PR で相談する
|
||||
- `down` で元値に戻せないデータ移行 (情報が失われる変換) は、`down` に戻せない旨をコメントで明示し、最低限スキーマだけは巻き戻す
|
||||
|
||||
```js
|
||||
// up: nullable 追加 → バックフィル → NOT NULL 化
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "github" boolean`);
|
||||
await queryRunner.query(`UPDATE "user_profile" SET "github" = FALSE WHERE "github" IS NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "github" SET NOT NULL`);
|
||||
```
|
||||
|
||||
### 4. JSONB / 配列列のデフォルト
|
||||
|
||||
**なぜ危険か**: 既定値リテラルの書式を誤ると `migration:generate` の出力とズレてスタイル不一致になる。実績ある書式に揃える。
|
||||
|
||||
```js
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`); // オブジェクト
|
||||
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD "logs" jsonb NOT NULL DEFAULT '[]'`); // 配列(JSON)
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`); // PG 配列型
|
||||
```
|
||||
|
||||
参照: [migration/1565634203341-room.js](../../../../../packages/backend/migration/1565634203341-room.js), [migration/1704959805077-bubble-game-record.js](../../../../../packages/backend/migration/1704959805077-bubble-game-record.js), [migration/1557476068003-PinnedUsers.js](../../../../../packages/backend/migration/1557476068003-PinnedUsers.js)。`down` はいずれも `DROP COLUMN`。
|
||||
|
||||
### 5. 安全な DROP と COMMENT
|
||||
|
||||
- **DROP の冪等性**: 状況により対象が無いことがある DROP は `IF EXISTS` を付ける (`DROP INDEX IF EXISTS "..."`)。ただし `migration:generate` は通常 `IF EXISTS` を付けない素の DDL を出すので、手で足すのは「条件付きで存在する」と分かっている時だけにする (無闇に付けると本来検出すべき不整合を隠す)
|
||||
- **COMMENT ON COLUMN**: Misskey は denormalize した列に `'[Denormalized]'` コメントを付ける慣習がある。エンティティの `@Column({ comment: '[Denormalized]' })` に対応して `migration:generate` が `COMMENT ON COLUMN` を出す。`up` で付与したら `down` でも対称に書く
|
||||
|
||||
```js
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteChannelId" IS '[Denormalized]'`);
|
||||
```
|
||||
|
||||
参照: [migration/1761569941833-add-channel-muting.js](../../../../../packages/backend/migration/1761569941833-add-channel-muting.js)
|
||||
|
||||
### 6. 列リネーム
|
||||
|
||||
`migration:generate` はエンティティのプロパティ名変更を **「DROP 旧列 + ADD 新列」** と解釈しがちで、これだと **データが消える**。意図がリネームなら生成 SQL を捨て、手書きで `ALTER TABLE "t" RENAME COLUMN "old" TO "new"` (down は逆) に直す。生成結果を鵜呑みにしないこと。
|
||||
@@ -0,0 +1,291 @@
|
||||
# 新規 REST API endpoint を追加する
|
||||
|
||||
`packages/backend/src/server/api/endpoints/<category>/<name>.ts` に新規エンドポイントを追加するための手順。**配線フェーズの `endpoint-list.ts` 登録を忘れると 404** になるので、まずそこを念頭に置く。
|
||||
|
||||
## 最重要事実 (見落とすと CI / 本番が壊れる)
|
||||
|
||||
1. **エンドポイントは glob 自動収集されない**。[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) への 1 行追加が必須 → [knowledge/endpoint-list.md](../knowledge/endpoint-list.md)
|
||||
2. **`meta` / `paramDef` / `res` を変えたら misskey-js 再生成が必須**。`pnpm build-misskey-js-with-types` を忘れると CI の `check-misskey-js-autogen` で必ず落ちる
|
||||
3. **`meta.errors` の各 `id` は UUID v4 で、リポジトリ内で一意**。`crypto.randomUUID()` で生成し、`grep -r "id: '<UUID>'" packages/backend/src/server/api/endpoints/` で衝突確認
|
||||
|
||||
## ワークフロー全体図
|
||||
|
||||
```
|
||||
1. 設計 : エンドポイントの種類を決める (read/write × 認証要否 × 権限)
|
||||
2. 実装 : meta / paramDef / クラス本体を書く (SPDX ヘッダー付き)
|
||||
3. 配線 : endpoint-list.ts に登録 (★ 忘れると 404)
|
||||
4. 検証 : e2e テスト + lint + misskey-js 再生成
|
||||
5. 仕上げ : CHANGELOG エントリ (shipping-misskey-change で確認)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計フェーズ — どのテンプレートをベースにするか
|
||||
|
||||
まず作るエンドポイントの性質を確定させる。**既存実装をテンプレートとしてコピペ起点にするのが最短路**。
|
||||
|
||||
| 性質 | ベースにする既存実装 |
|
||||
|---|---|
|
||||
| 認証不要・パラメータなし・小さなレスポンス | [endpoints/ping.ts](../../../../../packages/backend/src/server/api/endpoints/ping.ts) |
|
||||
| 認証必須・DI で Repository / Service を注入・errors あり | [endpoints/notes/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts) |
|
||||
| ページネーション (sinceId/untilId/limit) | [endpoints/notes/timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/timeline.ts) |
|
||||
| ロールポリシー (動的) ベースのアクセス制御 | [endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) — `RoleService.getUserPolicies()` を使う |
|
||||
| ファイル添付 (`requireFile: true`) | [endpoints/drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) |
|
||||
| moderator / admin 専用 | [endpoints/admin/suspend-user.ts](../../../../../packages/backend/src/server/api/endpoints/admin/suspend-user.ts) (moderator), [endpoints/admin/roles/create.ts](../../../../../packages/backend/src/server/api/endpoints/admin/roles/create.ts) (admin) |
|
||||
|
||||
`<category>` は機能領域 (例: `notes`, `users`, `admin/announcements`)。ディレクトリは既存に倣う。
|
||||
|
||||
---
|
||||
|
||||
## 2. 実装フェーズ
|
||||
|
||||
### 2.1 SPDX ヘッダー (必須)
|
||||
|
||||
新規 `.ts` ファイル冒頭に必ず付ける (欠落すると CI の `spdx` ジョブで失敗):
|
||||
|
||||
```ts
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
**注:** `packages/misskey-js/src/autogen/` 配下にも diff が出るが、**misskey-js は MIT ライセンス** で別管理 (`packages/misskey-js/package.json:license` = MIT) なので SPDX ヘッダーは付けない / 不要。
|
||||
|
||||
### 2.2 最小テンプレート (認証不要 read 系)
|
||||
|
||||
```ts
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['<tag>'],
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// 実装。me は MiLocalUser | null (requireCredential: false のため null チェック必須)
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 DI / errors / limit を含むテンプレート
|
||||
|
||||
```ts
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
requireCredential: true, // 認証必須 → kind 必須 (例外: secure: true な内部 API は kind 不要)
|
||||
kind: 'write:notes', // OAuth scope (一覧は packages/misskey-js/src/consts.ts の `permissions`)
|
||||
prohibitMoved: false, // 移行済アカウントを拒否するか
|
||||
limit: {
|
||||
duration: 1000 * 60 * 60, // 1 時間
|
||||
max: 300,
|
||||
},
|
||||
errors: {
|
||||
noSuchNote: { // ← キーは camelCase
|
||||
message: 'No such note.', // ← 英語ハードコード (バックエンドに i18n 機構なし)
|
||||
code: 'NO_SUCH_NOTE', // ← code は SCREAMING_SNAKE_CASE
|
||||
id: '17a0e0fa-3f3e-4f3e-9f3e-3f3e3f3e3f3e', // ← crypto.randomUUID() で生成し衝突確認
|
||||
},
|
||||
},
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note', // packed entity を参照する場合
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// requireCredential: true なので me は MiLocalUser (null になり得ない)
|
||||
const note = await this.notesRepository.findOneBy({ id: ps.noteId });
|
||||
if (note == null) throw new ApiError(meta.errors.noSuchNote);
|
||||
// 実装
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
DI / module 登録の詳細は [knowledge/nestjs-di.md](../knowledge/nestjs-di.md) を参照。
|
||||
|
||||
### 2.4 `exec` 関数のフルシグネチャ
|
||||
|
||||
`super(meta, paramDef, cb)` の `cb` が受け取る引数は 7 つある ([endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の `Executor` 型):
|
||||
|
||||
```ts
|
||||
async (ps, me, token, file, cleanup, ip, headers) => { ... }
|
||||
```
|
||||
|
||||
| 引数 | 型 | 用途 |
|
||||
|---|---|---|
|
||||
| `ps` | `SchemaType<typeof paramDef>` | AJV 検証済の入力 |
|
||||
| `me` | `MiLocalUser` (requireCredential: true) / `MiLocalUser \| null` (false) | ローカルユーザー。`requireCredential: false` のとき必ず null チェック |
|
||||
| `token` | `MiAccessToken \| null` | OAuth トークン (アプリ識別が要るとき) |
|
||||
| `file` | `{ name, path } \| undefined` | `requireFile: true` のときのみ確実に渡る。エンドポイント基底クラスが既に null チェック済 |
|
||||
| `cleanup` | `() => any \| undefined` | アップロードされた一時ファイルを削除するコールバック。**基底クラスが自動で呼ぶのは AJV バリデーション失敗時だけ**。正常終了や endpoint 内例外時は **呼ばれない** ので、`try { ... } finally { cleanup!(); }` で必ず呼ぶ責務がある ([drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) の `finally { cleanup!(); }` が手本) |
|
||||
| `ip` | `string \| null \| undefined` | クライアント IP |
|
||||
| `headers` | `Record<string, string> \| null \| undefined` | リクエストヘッダ |
|
||||
|
||||
ほとんどのエンドポイントは `(ps, me)` だけで十分。`token` / `ip` / `headers` まで使うのは admin / debug / auth 系のごく一部。
|
||||
|
||||
### 2.5 meta / paramDef の規約
|
||||
|
||||
頻出 5 件 (`tags` / `requireCredential` / `kind` / `limit` / `errors`) の使い方や全フィールド一覧、`requiredRolePolicy` / `secure` / `cacheSec` / `allowGet` 等、それと `paramDef` の AJV 実用パターンは → [knowledge/api-meta-paramdef.md](../knowledge/api-meta-paramdef.md)。
|
||||
|
||||
### 2.6 エラー throw のバランス
|
||||
|
||||
**クライアントに返すべき業務エラー** は必ず `meta.errors` に列挙して `throw new ApiError(meta.errors.<key>)` する。これを守らないと misskey-js 側の型に出ず、レスポンスも 500 になる。第 2 引数で追加情報を渡せる:
|
||||
|
||||
```ts
|
||||
throw new ApiError(meta.errors.invalidParam, { reason: 'too short' });
|
||||
```
|
||||
|
||||
一方で **想定外の例外 (DB 不整合 / 下層 service の bug / 防御的アサーション)** は `throw new Error('...')` のままで構わない。すべての例外を `ApiError` で包むと、未知のバグが client error として隠蔽されてしまう。`endpoints/notes/create.ts` の `catch` 節末尾の `throw err;` がこの二段構えの典型。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配線フェーズ — endpoint-list.ts に登録 ★必須
|
||||
|
||||
[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) の **同カテゴリ内** に 1 行追加する:
|
||||
|
||||
```ts
|
||||
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
|
||||
```
|
||||
|
||||
詳細・落とし穴は [knowledge/endpoint-list.md](../knowledge/endpoint-list.md) を参照。**ここへの登録漏れ = 404**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 検証フェーズ
|
||||
|
||||
### 4.1 e2e テスト
|
||||
|
||||
[packages/backend/test/e2e/](../../../../../packages/backend/test/e2e/) の構造は **機能カテゴリごとのファイル分け** (`note.ts` / `users.ts` / `timelines.ts` / `drive.ts` / `clips.ts` / `oauth.ts` 等)。
|
||||
|
||||
- 既存のカテゴリファイルがあるなら、そこに `describe('<人間可読ラベル>', () => { test('正常系', ...) })` で追加
|
||||
- どのファイルにも合わないなら `test/e2e/endpoints.ts` に追加
|
||||
- `describe` 名は **人間可読 OK**
|
||||
|
||||
最小例 (詳細なヘルパー一覧は → [knowledge/backend-testing.md](../knowledge/backend-testing.md)):
|
||||
|
||||
```ts
|
||||
import { describe, test } from 'vitest';
|
||||
import * as assert from 'node:assert';
|
||||
import { api, signup } from '../utils.js';
|
||||
|
||||
describe('<人間可読ラベル>', () => {
|
||||
test('正常系', async () => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const res = await api('<category>/<name>', { /* params */ }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
実行 (前提: `.config/test.yml` — [knowledge/backend-testing.md](../knowledge/backend-testing.md) §前提 参照):
|
||||
|
||||
```bash
|
||||
pnpm --filter backend test:e2e
|
||||
```
|
||||
|
||||
### 4.2 lint / typecheck
|
||||
|
||||
```bash
|
||||
# 個別ファイルを高速にチェック
|
||||
pnpm exec eslint --fix packages/backend/src/server/api/endpoints/<category>/<name>.ts
|
||||
pnpm --filter backend typecheck # tsgo --noEmit (backend のみ)
|
||||
|
||||
# 一括 (PR 提出前)
|
||||
pnpm --filter backend lint
|
||||
```
|
||||
|
||||
### 4.3 misskey-js 再生成 (★必須)
|
||||
|
||||
`meta` / `paramDef` / `res` を変えたら必ず:
|
||||
|
||||
```bash
|
||||
pnpm build-misskey-js-with-types
|
||||
```
|
||||
|
||||
PR に `packages/misskey-js/src/autogen/` 配下の差分が含まれていないと CI の `check-misskey-js-autogen` で必ず落ちる (最頻ミス)。詳細手順は [shipping-misskey-change/references/tasks/regenerate-misskey-js.md](../../../shipping-misskey-change/references/tasks/regenerate-misskey-js.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 仕上げフェーズ — CHANGELOG
|
||||
|
||||
ユーザー影響がある (新機能 / 既存挙動変更) なら `CHANGELOG.md` の `## Unreleased` → `### Server` に 1 行追加する。詳細は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) に従う。
|
||||
|
||||
---
|
||||
|
||||
## 落とし穴サマリ (PR で頻発するミス)
|
||||
|
||||
詳細な症状 → 原因 → 修正 のフォーマット → **[knowledge/api-meta-paramdef.md](../knowledge/api-meta-paramdef.md) §落とし穴**
|
||||
|
||||
- **404 になる** → `endpoint-list.ts` 登録漏れ
|
||||
- **CI `check-misskey-js-autogen` で落ちる** → `pnpm build-misskey-js-with-types` 忘れ
|
||||
- **CI `spdx` で落ちる** → SPDX ヘッダー欠落
|
||||
- **クライアントが 500 と error 型不在を受け取る** → `meta.errors` 列挙なしに `throw new ApiError(...)` した
|
||||
- **`me.id` で TypeError** → `requireCredential: false` で null チェックを忘れた
|
||||
- **UUID 重複** → 衝突確認グレップを忘れた
|
||||
- **一時ファイルが残る** → `requireFile: true` で `cleanup!()` を `finally` で呼び忘れた
|
||||
- **`requiredRolePolicy` で匿名アクセスが 500 になる** → `ApiCallService` が `user!.id` を非null前提で参照するため `requireCredential: true` 必須
|
||||
|
||||
---
|
||||
|
||||
## 参照ファイル
|
||||
|
||||
### コードベース
|
||||
|
||||
- [endpoints.ts (meta/paramDef 型定義)](../../../../../packages/backend/src/server/api/endpoints.ts)
|
||||
- [endpoint-base.ts (Endpoint 基底クラス)](../../../../../packages/backend/src/server/api/endpoint-base.ts)
|
||||
- [endpoint-list.ts (★ ここに登録)](../../../../../packages/backend/src/server/api/endpoint-list.ts)
|
||||
- [error.ts (ApiError)](../../../../../packages/backend/src/server/api/error.ts)
|
||||
- [endpoints/ping.ts (最小例)](../../../../../packages/backend/src/server/api/endpoints/ping.ts)
|
||||
- [endpoints/notes/create.ts (DI + errors の典型)](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts)
|
||||
- [endpoints/notes/global-timeline.ts (policies 動的チェック)](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts)
|
||||
- [test/e2e/endpoints.ts (テスト例)](../../../../../packages/backend/test/e2e/endpoints.ts)
|
||||
- [test/utils.ts (api/signup/post 等のヘルパー)](../../../../../packages/backend/test/utils.ts)
|
||||
- [scripts/generate_api_json.js (misskey-js 生成元)](../../../../../packages/backend/scripts/generate_api_json.js)
|
||||
@@ -0,0 +1,180 @@
|
||||
# DB migration を作成する
|
||||
|
||||
`packages/backend/migration/` に新規 TypeORM マイグレーションを追加するための手順。
|
||||
|
||||
## 大前提 (絶対 NG)
|
||||
|
||||
- **既にマージ済み (develop / master) のマイグレーションファイルを編集しない** ([AGENTS.md](../../../../../AGENTS.md))。本番履歴の改変は深刻なデータ不整合を引き起こす。スキーマ変更は **常に新しいタイムスタンプで新規ファイル** を作る
|
||||
- ファイル名のタイムスタンプ部分を後から書き換えない (順序が壊れる)
|
||||
- マージ済 migration の `up()` / `down()` 本文も触らない (たとえ "明らかなバグ" であっても、新しい migration で打ち消すこと)
|
||||
|
||||
---
|
||||
|
||||
## どの方式を使うか決める
|
||||
|
||||
| 状況 | 方式 |
|
||||
|---|---|
|
||||
| エンティティ (`packages/backend/src/models/*.ts`) を `@Column` / `@Index` / `@Entity` 等で先に変更し、差分から自動生成したい | `typeorm migration:generate` (本ファイルの "A. 差分から自動生成") |
|
||||
| 手書き SQL / データ移行 / `CREATE INDEX CONCURRENTLY` など、エンティティ差分では表現できない変更 | `typeorm migration:create` で空雛形を作る (本ファイルの "B. 空雛形を作る") |
|
||||
|
||||
迷ったら **まずエンティティを変更 → `migration:generate`** が原則。既存 migration (`packages/backend/migration/*.js`) のほぼすべてが `queryRunner.query(\`SQL...\`)` の raw SQL なので、CLI 出力でも手書きでもスタイルは揃う。
|
||||
|
||||
---
|
||||
|
||||
## 共通: クラス命名規則
|
||||
|
||||
- ファイル名: `packages/backend/migration/{unixMs}-{descriptive-name}.js` (拡張子 `.js`)
|
||||
- ファイル名の `descriptive-name` 部分は既存履歴で混在 (PascalCase / camelCase / kebab-case)、変更を表す単一英語名なら良い
|
||||
- **クラス名は PascalCase + 13 桁タイムスタンプ** (例: `class BirthdayIndex1767169026317`)
|
||||
- **`name` プロパティもクラス名と同一文字列** にする (`name = 'BirthdayIndex1767169026317'`)
|
||||
|
||||
```js
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class PascalCaseName1234567890123 {
|
||||
name = 'PascalCaseName1234567890123'
|
||||
|
||||
async up(queryRunner) {
|
||||
// 前進マイグレーション
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// up を完全に巻き戻す
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A. エンティティ差分から自動生成
|
||||
|
||||
```bash
|
||||
# リポジトリルートから実行してよい。--filter backend exec が cwd を packages/backend に移すので、
|
||||
# 出力パス migration/<PascalName> と -d ormconfig.js は packages/backend/ 基準で解決される
|
||||
pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>
|
||||
```
|
||||
|
||||
**CONTRIBUTING.md との違い**: CONTRIBUTING.md は `pnpm dlx typeorm ...` を案内しているが、`dlx` はパッケージを一時ダウンロードするため、バージョンが backend の依存関係と揃わない可能性がある。`pnpm --filter backend exec typeorm` はワークスペースにインストール済みの typeorm を使うため **こちらを推奨**。
|
||||
|
||||
**`-o --esm` について**: `-o` (`--outputJs`) は「TS ではなく JS を出力する」オプション、`--esm` は「ESM 形式 (`export class ...`) で出力する」オプション。Misskey の既存 migration はすべて ESM JS であるため **両方が必須**。`--esm` を省略すると CommonJS 形式の JS が生成されスタイルが揃わない。
|
||||
|
||||
### 事前準備 (一括スクリプト)
|
||||
|
||||
`migration:generate` には backend ビルド + ローカル DB が必要。一括で揃えるスクリプトを同梱している (node 製。pure Windows でも動く)。リポジトリルートから:
|
||||
|
||||
```bash
|
||||
node .claude/skills/working-on-backend/scripts/prepare-generate.mjs
|
||||
```
|
||||
|
||||
スクリプトがやること:
|
||||
|
||||
- `pnpm build-pre` → `built/meta.json` を生成 (`loadConfig()` が要求)
|
||||
- `pnpm --filter backend compile-config` → `built/.config.json` を生成 (`ormconfig.js` の `loadConfig()` が要求するのはこれ。ソースの `.config/default.yml` はその入力なので、無ければ `.config/example.yml` から作っておく)
|
||||
- `pnpm --filter backend build` → エンティティを `built/` に反映 (CLI は `built/` を読む)
|
||||
- `docker compose -f compose.local-db.yml up -d --wait db` → ローカル DB (postgres) を起動。`--wait` は Docker Compose v2.1.1 (2021-11) 以降が必要 (v2 の `docker compose` 前提。EOL の `docker-compose` v1 は対象外)
|
||||
|
||||
`migration:create` (空雛形) しか使わないなら DB もビルドも不要なので、このスクリプトは不要。
|
||||
|
||||
---
|
||||
|
||||
## B. 空雛形を作る (手書き SQL / データ移行用)
|
||||
|
||||
```bash
|
||||
pnpm --filter backend exec typeorm migration:create -o --esm migration/<PascalName>
|
||||
```
|
||||
|
||||
ローカル DB の起動とビルドは不要。空の `up` / `down` だけが生成される。
|
||||
|
||||
**注意:** `-o --esm` を **必ず付ける**。これが無いと `<UnixMs>-<PascalName>.ts` (CommonJS / TS 出力) が生成されるが、Misskey の `ormconfig.js` は `migration/*.js` だけを読み、既存の他 migration も全て `export class ... { async up(queryRunner) {...} }` の ESM JS 形式なので、後で手作業で変換が必要になる。`-o --esm` を付ければそのまま `.js` ESM で出る。
|
||||
|
||||
ただし `migration:create` の雛形は **`name = '...'` プロパティを出力しない**ので、後段の SPDX 付与に加えて `name = '<PascalName><ms>'` を手で足し、`up`/`down` を埋める必要がある。雛形冒頭の `@typedef` / `@implements MigrationInterface` JSDoc は既存ファイルに無いので消して house style に揃える。
|
||||
|
||||
### B の補助: 引数だけで全部を済ませたい場合
|
||||
|
||||
引数で `<PascalCaseName>` を渡すだけで「空雛形生成 + SPDX 付与 + check-migrations 実行」までやる薄いラッパー (旧 `.claude/commands/migrate-new.md` 由来) は廃止された。同等の流れを手で踏みたい場合、上記の `typeorm migration:create` + SPDX 付与 + `name` プロパティ追加 + `check-migrations` の順で実行する。
|
||||
|
||||
---
|
||||
|
||||
## SPDX ヘッダー付与
|
||||
|
||||
CLI 出力には SPDX ヘッダーが含まれない。**必ず冒頭に追加する** (CI の `spdx` ジョブが失敗するため)。
|
||||
|
||||
```js
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## up / down の整合確認
|
||||
|
||||
- `up()` の各ステートメントに対し、`down()` で完全に巻き戻せること
|
||||
- 列追加 (`ADD COLUMN`) ↔ 列削除 (`DROP COLUMN`)、テーブル作成 ↔ テーブル削除、FK 追加 ↔ FK 削除、インデックス作成 ↔ インデックス削除 を必ずペアで書く
|
||||
- `down()` を空のまま残さない。本番ロールバック時に詰む
|
||||
|
||||
**単純な逆 SQL では戻らない難ケース** (enum 値の追加・変更 / NOT NULL 列追加 / データ移行 UPDATE / JSONB・配列デフォルト / 列リネーム / 安全な DROP・COMMENT) は [knowledge/typeorm-patterns.md §migration 難ケース](../knowledge/typeorm-patterns.md) を必ず参照。特に **enum 変更** と **列リネーム** は `migration:generate` の出力をそのまま使うと巻き戻せない / データが消えるので要注意。
|
||||
|
||||
### インデックス追加時 (CREATE INDEX CONCURRENTLY)
|
||||
|
||||
大規模テーブルへの `CREATE INDEX` は本番で長時間ロックする恐れがある。`CONCURRENTLY` で発行するときは migration class に `transaction = false` 等の対応が必要。詳細は [knowledge/typeorm-patterns.md §CONCURRENTLY](../knowledge/typeorm-patterns.md) を参照。
|
||||
|
||||
参照実装: [packages/backend/migration/1745378064470-composite-note-index.js](../../../../../packages/backend/migration/1745378064470-composite-note-index.js)。
|
||||
|
||||
---
|
||||
|
||||
## 検証
|
||||
|
||||
ルートから実行:
|
||||
|
||||
```bash
|
||||
# 未反映の差分が無いか (新規 migration が生成すべき DDL を取り逃していないか)
|
||||
pnpm --filter backend check-migrations
|
||||
|
||||
# ローカル DB に適用
|
||||
pnpm migrate
|
||||
|
||||
# ロールバック (down が壊れていないか)
|
||||
pnpm revert
|
||||
|
||||
# 再適用 (順方向にもう一度通す)
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
`check-migrations` の実体は [scripts/check_migrations_clean.js](../../../../../packages/backend/scripts/check_migrations_clean.js)。TypeORM の `dataSource.driver.createSchemaBuilder().log()` で pending DDL を取得し、`upQueries` / `downQueries` のいずれかが残っていれば非ゼロ終了する。**順序検査ではなく**「エンティティと migration が同期しているか」の検査。
|
||||
|
||||
---
|
||||
|
||||
## 既存ファイル参照テンプレ
|
||||
|
||||
新規ファイルを書くときは、変更パターンが近い既存ファイルを **必ずひとつ開いて並べて書く**。スタイルが激しくズレた PR は差し戻されやすい。
|
||||
|
||||
| パターン | 参照ファイル |
|
||||
|---|---|
|
||||
| インデックス追加 + 関数定義 | [migration/1767169026317-birthday-index.js](../../../../../packages/backend/migration/1767169026317-birthday-index.js) |
|
||||
| 列追加のみ | [migration/1766652173085-add-category-to-avatar-decorations.js](../../../../../packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js) |
|
||||
| テーブル新規作成 + FK | [migration/1761569941833-add-channel-muting.js](../../../../../packages/backend/migration/1761569941833-add-channel-muting.js) |
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG (ユーザー影響がある場合)
|
||||
|
||||
スキーマ変更がユーザーに見える挙動を生む場合のみ、`CHANGELOG.md` に追記する。内部リファクタや純粋なインデックス追加は不要。詳細は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で確認。
|
||||
|
||||
---
|
||||
|
||||
## 提出前セルフレビューチェックリスト
|
||||
|
||||
完了前に以下を上から確認する (各項目を TodoWrite 化してよい):
|
||||
|
||||
- [ ] **新規タイムスタンプ**で作成し、既にマージ済みの migration ファイルは一切編集していない (大前提)
|
||||
- [ ] ファイル冒頭に **SPDX ヘッダー**がある
|
||||
- [ ] `export class <PascalName><ms>` と `name = '<PascalName><ms>'` の **文字列が完全一致** している (PascalCase + 13 桁タイムスタンプ)
|
||||
- [ ] `up()` の各文に対応する巻き戻しが `down()` にあり、**`down()` が空でない** (難ケースは [knowledge/typeorm-patterns.md](../knowledge/typeorm-patterns.md) を確認済み)
|
||||
- [ ] `pnpm --filter backend check-migrations` が **0 件 (pending DDL なし)** で通る
|
||||
- [ ] (可能なら) `pnpm migrate` → `pnpm revert` → `pnpm migrate` が通る
|
||||
- [ ] ユーザーに見える変更なら CHANGELOG 追記 → [shipping-misskey-change](../../../shipping-misskey-change/SKILL.md)
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* typeorm migration:generate の前準備をまとめて実行する (冪等・クロスプラットフォーム)。
|
||||
* リポジトリルートから実行: node .claude/skills/working-on-backend/scripts/prepare-generate.mjs
|
||||
*
|
||||
* generate はエンティティのビルド出力 (built/)、コンパイル済み設定 (built/.config.json)、
|
||||
* 稼働中の DB を必要とする。手で 5 段並べると取りこぼすのでここに集約する。
|
||||
* migration:create (空雛形) しか使わないなら DB もビルドも不要なのでこのスクリプトは不要。
|
||||
*
|
||||
* Node で書いているのは pure Windows (bash の無い環境) でも動かすため。node はこのリポジトリの
|
||||
* ランタイムなので必ず存在し、build-pre.mjs / compile_config.js と同じ流儀に揃う。
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
// このファイルの 4 つ上が repo root
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
||||
process.chdir(root);
|
||||
|
||||
function step(msg) { console.log(`\n==> ${msg}`); }
|
||||
function run(cmd) { console.log(`$ ${cmd}`); execSync(cmd, { stdio: 'inherit' }); }
|
||||
function fail(msg) { console.error(`ERROR: ${msg}`); process.exit(1); }
|
||||
|
||||
step('1/5 設定ファイルの確認');
|
||||
if (!existsSync('.config/default.yml')) {
|
||||
fail([
|
||||
'.config/default.yml が存在しません。',
|
||||
' .config/example.yml を .config/default.yml にコピーしてから再実行してください:',
|
||||
' Unix系: cp .config/example.yml .config/default.yml',
|
||||
' PowerShell: Copy-Item .config/example.yml .config/default.yml',
|
||||
' コピー後、db.user / pass / db を .config/docker.env と一致させてください',
|
||||
' (example.yml の既定値は docker.env の例と一致するので、独自 DB を使わなければそのままで可)。',
|
||||
].join('\n'));
|
||||
}
|
||||
// compose.local-db.yml の db サービスは .config/docker.env を env_file に要求する
|
||||
if (!existsSync('.config/docker.env')) {
|
||||
fail([
|
||||
'.config/docker.env が存在しません (compose.local-db.yml の db が要求)。',
|
||||
' 例 (.config/default.yml の db.user / db.pass / db.db と一致させる):',
|
||||
' POSTGRES_USER=example-misskey-user',
|
||||
' POSTGRES_PASSWORD=example-misskey-pass',
|
||||
' POSTGRES_DB=misskey',
|
||||
].join('\n'));
|
||||
}
|
||||
console.log('OK: .config/default.yml と .config/docker.env あり');
|
||||
|
||||
step('2/5 built/meta.json の生成 (build-pre)');
|
||||
run('pnpm build-pre');
|
||||
|
||||
step('3/5 設定のコンパイル (compile-config -> built/.config.json)');
|
||||
run('pnpm --filter backend compile-config');
|
||||
|
||||
step('4/5 backend のビルド (エンティティを built/ へ反映)');
|
||||
run('pnpm --filter backend build');
|
||||
|
||||
step('5/5 ローカル DB の起動 (postgres のみ・healthcheck 完了まで待機)');
|
||||
// migration:generate が必要とするのは postgres だけ。db サービスに絞れば meilisearch.env 等が無くても動く。
|
||||
// --wait は compose の pg_isready healthcheck 完了まで待つ。直後の migration:generate が
|
||||
// DB 未起動で失敗しないために必須。--wait は Docker Compose v2.1.1 (2021-11) で導入されており、
|
||||
// このリポジトリが前提とする v2 の `docker compose` なら標準で使える (EOL の `docker-compose` v1 は対象外)。
|
||||
run('docker compose -f compose.local-db.yml up -d --wait db');
|
||||
|
||||
console.log('\n準備完了。次を実行できます:');
|
||||
console.log(' pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>');
|
||||
36
.claude/skills/working-on-frontend/SKILL.md
Normal file
36
.claude/skills/working-on-frontend/SKILL.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: working-on-frontend
|
||||
description: Use whenever editing or adding code under `packages/frontend/`, or editing `locales/ja-JP.yml` for frontend-facing UI text — including Vue 3 SFCs (`Mk*` components), i18n keys (`i18n.ts.<key>` / `i18n.tsx.<key>()`), SCSS Modules, theme/CSS variables, `os.*` UI helpers, and Storybook stories. Covers SPDX (HTML comment form), `<script setup lang="ts">` conventions, type-only defineProps, `ja-JP.yml`-only locale editing (other locale yml files are Crowdin-managed and must not be edited), and accessibility. Must be consulted before any frontend or UI-locale change to avoid CI failures, lost translations, and reviewer pushback. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this at implementation time regardless of what preceded it.
|
||||
---
|
||||
|
||||
# working-on-frontend
|
||||
|
||||
`packages/frontend/` (Misskey Web クライアント) を編集するとき、最初に参照するスキル。Vue 3 SFC / SCSS Modules / i18n / `os.*` / Storybook / アクセシビリティの **手順** と **背景知識** をまとめている。
|
||||
|
||||
SKILL.md 本体は references への索引だけ。具体的な手順や規約は該当ファイルを Read すること (progressive disclosure)。
|
||||
|
||||
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、`packages/frontend/` に触れる実装フェーズに入る時点でこのスキルを呼ぶこと。
|
||||
|
||||
## 作業別ワークフロー (tasks)
|
||||
|
||||
タスク単位の完結したチェックリスト。新しい何かを足すときに開く。
|
||||
|
||||
- 新規 / 既存 `Mk*` Vue コンポーネントを追加・改修する → [references/tasks/adding-mk-component.md](references/tasks/adding-mk-component.md)
|
||||
- i18n キーを追加・改修する (`locales/ja-JP.yml` 編集) → [references/tasks/adding-i18n-key.md](references/tasks/adding-i18n-key.md)
|
||||
|
||||
## 共通知識 (knowledge)
|
||||
|
||||
タスクに紐付かない参照リファレンス。SFC を **編集する** 場面 (新規追加でなくても) で踏みうる規約。
|
||||
|
||||
- `<script setup>` / type-only `defineProps` / `defineEmits` / generic SFC / v-model 連動など SFC 規約 → [references/knowledge/component-conventions.md](references/knowledge/component-conventions.md)
|
||||
- `i18n.ts.<key>` / `i18n.tsx.<key>(...)` の使い分け / HTML タグ埋め込み / 動的キー切替 / 既存キーのリネーム手順 → [references/knowledge/i18n-usage.md](references/knowledge/i18n-usage.md)
|
||||
- SCSS Modules / `--MI_THEME-*` `--MI-*` CSS 変数 / グローバル utility class (`_button` 等) → [references/knowledge/scss-modules.md](references/knowledge/scss-modules.md)
|
||||
- `os.alert` / `os.confirm` / `os.popup` 等 UI ヘルパー (ブラウザ標準 `alert()` 直呼びは禁止) → [references/knowledge/os-api.md](references/knowledge/os-api.md)
|
||||
- `*.stories.impl.ts` 併設規則 + 複数 story / argTypes / layout / action パターン → [references/knowledge/storybook.md](references/knowledge/storybook.md)
|
||||
- frontend Vitest / Cypress E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
|
||||
|
||||
## 必ず最後に通る場所
|
||||
|
||||
frontend の変更を commit / PR にする前に、必ず [shipping-misskey-change](../shipping-misskey-change/SKILL.md) の最終チェックリストに従う。`pnpm lint` / SPDX / `ja-JP.yml` のみ編集確認 / CHANGELOG をまとめて確認する。
|
||||
|
||||
`.vue` を追加・変更したなら、その出口で [vue-component-reviewer](../../agents/vue-component-reviewer.md) agent (この skill の規約を review-mode から機械チェックする専門 reviewer) を Task で起動すると、SPDX 形式・命名・i18n・SCSS 変数・a11y・Storybook 併設の逸脱を取りこぼしにくい。
|
||||
@@ -0,0 +1,357 @@
|
||||
# Vue SFC 規約・テンプレート集 + a11y チェックリスト
|
||||
|
||||
Misskey の Vue 3 SFC 規約と、新規 `Mk*` コンポーネント / 既存コンポーネント編集時のテンプレート / アクセシビリティ要件をまとめたページ。
|
||||
|
||||
## 目次
|
||||
|
||||
- [SFC スタイルの基本](#sfc-スタイルの基本)
|
||||
- [`<script>` / `<style>` 規約](#script--style-規約)
|
||||
- [テンプレート集](#テンプレート集)
|
||||
- [simple (`<slot>` + 単純 props)](#simple-slot--単純-props)
|
||||
- [generic + 2 ブロック script](#generic--2-ブロック-script)
|
||||
- [`defineModel` で v-model 連動](#definemodel-で-v-model-連動)
|
||||
- [emit + 名前付き slot で外部から動作を差し込む](#emit--名前付き-slot-で外部から動作を差し込む)
|
||||
- [a11y チェックリスト](#a11y-チェックリスト)
|
||||
|
||||
## SFC スタイルの基本
|
||||
|
||||
Composition API + `<script setup lang="ts">` を基本とする (Options API は新規導入しない)。型宣言や module スコープのユーティリティを置きたい時は、setup ブロックと **併用** する形で追加の `<script lang="ts">` ブロックを置いて構わない (例: [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) は `SupportedTypes` 型を別ブロックで宣言してから setup を書いている)。SCSS は **CSS Modules** で書き、`<style lang="scss" module>` を使う。
|
||||
|
||||
```vue
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<!-- ... -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
// ...
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
/* ... */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## `<script>` / `<style>` 規約
|
||||
|
||||
| 項目 | 規約 | 新規不可 |
|
||||
|---|---|---|
|
||||
| `<script>` 開始タグ | `<script lang="ts" setup>` または `<script setup lang="ts">` (順序不問) | `<script>` (lang 無し) / Options API (`export default { data() {...} }`) |
|
||||
| Props 定義 | `defineProps<{ ... }>()` (type-only) | runtime object 形式 `defineProps({ name: { type: String } })` |
|
||||
| Emits 定義 | `defineEmits<{ (ev: 'click'): void }>()` (type-only) | runtime array 形式 `defineEmits(['click'])` |
|
||||
| 型ジェネリック | `<script setup lang="ts" generic="T extends ...">` 属性で渡す。複雑な型宣言が必要なら **2 ブロック構成** ([generic パターン](#generic--2-ブロック-script)) | — |
|
||||
| `<style>` 開始タグ | `<style lang="scss" module>`、参照は `:class="$style.foo"` | `<style scoped>` (module なし) は新規不可 (legacy 混在) |
|
||||
| CSS 値 | `var(--MI_THEME-...)` (テーマ) / `var(--MI-...)` (UI 共通定数) を使う | `#fff` / `rgb(...)` / `rgba(...)` のハードコード ([scss-modules.md](scss-modules.md)) |
|
||||
| グローバル class | `_button` / `_panel` / `_selectable` / `_buttonPrimary` 等の global utility class を活用 | — |
|
||||
| アイコン | Tabler icons クラス `<i class="ti ti-info-circle">` | インライン SVG / 別アイコンセット |
|
||||
|
||||
## テンプレート集
|
||||
|
||||
### simple (`<slot>` + 単純 props)
|
||||
|
||||
下記は `<slot>` + props + `withDefaults` の典型パターンを示す**合成例** (特定ファイルの写しではない)。実在する単純コンポーネントの例は [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) 等を参照。
|
||||
|
||||
```vue
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.warn]: variant === 'warn' }]" class="_selectable">
|
||||
<i v-if="variant === 'warn'" class="ti ti-alert-triangle" :class="$style.icon"></i>
|
||||
<i v-else class="ti ti-info-circle" :class="$style.icon"></i>
|
||||
<div><slot></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
variant?: 'info' | 'warn';
|
||||
}>(), {
|
||||
variant: 'info',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
font-size: 90%;
|
||||
background: var(--MI_THEME-infoBg);
|
||||
color: var(--MI_THEME-infoFg);
|
||||
border-radius: var(--MI-radius);
|
||||
|
||||
&.warn {
|
||||
background: var(--MI_THEME-infoWarnBg);
|
||||
color: var(--MI_THEME-infoWarnFg);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- デフォルト値が必要なら `withDefaults(defineProps<{...}>(), { ... })` を使う (type-only のまま既定値を渡せる)
|
||||
- `_selectable` は本文選択を許可する global utility class ([scss-modules.md](scss-modules.md) 参照)
|
||||
- `<i class="ti ti-...">` は Tabler icons。`v-if` 切り替えで variant 別アイコンを出すのは多用パターン
|
||||
|
||||
### generic + 2 ブロック script
|
||||
|
||||
参考: [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue)
|
||||
|
||||
型ジェネリックを取りつつ、その型計算や `type` エイリアス宣言を setup ブロックの中に書きたくない場合は、**型宣言用 `<script lang="ts">` と setup 用 `<script lang="ts" setup>` を 2 つ並べる** 構成にできる。
|
||||
|
||||
```vue
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="String(item.value)"
|
||||
class="_button"
|
||||
:class="[$style.item, { [$style.active]: item.value === modelValue }]"
|
||||
@click="select(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// module scope: 型 / 定数 / 純関数のみ。setup の中から見える。
|
||||
export type ChoiceItem<T> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends string | number">
|
||||
const props = defineProps<{
|
||||
modelValue: T;
|
||||
items: ChoiceItem<T>[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: T): void;
|
||||
}>();
|
||||
|
||||
function select(value: T) {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- `generic="T extends string | number"` の制約を付けることで、`v-model` で渡された型が `string` / `number` 系に限定される
|
||||
- 2 ブロック構成にする理由は **setup ブロック内では `export type` が書けない** から
|
||||
- `MkSelect.vue` のような複雑な型エクスポートをするコンポーネントで多用される
|
||||
|
||||
### `defineModel` で v-model 連動
|
||||
|
||||
参考: [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue), [MkRadios.vue](../../../../../packages/frontend/src/components/MkRadios.vue)
|
||||
|
||||
`defineModel` を使うと `props.modelValue` + `emit('update:modelValue', v)` の 2 行が 1 行に圧縮できる。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<label :class="[$style.root, { [$style.disabled]: disabled }]">
|
||||
<input
|
||||
v-model="checked"
|
||||
type="checkbox"
|
||||
:class="$style.input"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span :class="$style.label"><slot></slot></span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const checked = defineModel<boolean>({ required: true });
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- `defineModel<boolean>()` は **自動で `props.modelValue` と `emit('update:modelValue', v)` を生成** する。返り値は `Ref` なので `checked.value = ...` で書き換えると emit される
|
||||
- `defineModel('foo')` のように引数を渡すと `v-model:foo` (`props.foo` + `emit('update:foo', v)`) の連動が作れる
|
||||
- 新規ファイルの v-model 連動は原則として `defineModel` を使う (`props.modelValue` + `emit` の手書きは既存コードに残るのみ)
|
||||
|
||||
### emit + 名前付き slot で外部から動作を差し込む
|
||||
|
||||
下記は emit + 名前付き slot の典型パターンを示す**合成例** (特定ファイルの写しではない)。クリック時の処理を呼び出し元に委ねるパターン (確認 UI など)。なお [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) 自体は `(ev: 'click', payload: PointerEvent)` のみを emit する単機能ボタンで、この合成例とは構造が異なる。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div :class="$style.root" class="_panel">
|
||||
<div :class="$style.header">
|
||||
<slot name="header">{{ i18n.ts.confirm }}</slot>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<button class="_button" :class="$style.cancel" @click="emit('cancel')">
|
||||
{{ i18n.ts.cancel }}
|
||||
</button>
|
||||
<button class="_button _buttonPrimary" :class="$style.ok" @click="emit('ok')">
|
||||
{{ i18n.ts.ok }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok'): void;
|
||||
(ev: 'cancel'): void;
|
||||
}>();
|
||||
</script>
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- 名前付き slot (`<slot name="header">`) と無名 slot (`<slot></slot>`) は両方使ってよい
|
||||
- `_panel` / `_button` / `_buttonPrimary` は global utility class なので、自前で同じスタイルを書かない
|
||||
- `emit('ok')` 等の単純 emit は中継するだけにし、`os.confirm` などの実際の確認 UI 起動は呼び出し元の責務にする (テスト・差し替えしやすくするため)
|
||||
|
||||
## a11y チェックリスト
|
||||
|
||||
Misskey の PR レビューで頻繁に出る a11y 指摘をまとめた。新規 / 既存コンポーネントを編集する時は以下を満たす。
|
||||
|
||||
### クリック可能要素
|
||||
|
||||
#### 第一選択: `<button class="_button">`
|
||||
|
||||
```vue
|
||||
<button class="_button" :class="$style.action" :disabled="disabled" @click="onClick">
|
||||
{{ i18n.ts.save }}
|
||||
</button>
|
||||
```
|
||||
|
||||
- `_button` global class はボタンの装飾を除去するリセット (背景/枠線なし + `cursor: pointer` + disabled cursor)。focus ring や ripple は**付かない** — ripple 付きのボタンが要るなら `MkButton.vue` コンポーネントを使う
|
||||
- `<button>` はデフォルトで `tabindex` / Enter / Space / `aria-disabled` の挙動とブラウザ標準のフォーカスリングを持つので、追加の ARIA を書かなくてよい
|
||||
- form の中で意図せず submit させたくない場合は `type="button"` を明示する (省略時は `type="submit"` 扱い)
|
||||
|
||||
#### やむを得ず `<div @click>` を使う場合
|
||||
|
||||
装飾やレイアウト都合で `<button>` が使えないときは、**4 点セット** を必ず揃える。
|
||||
|
||||
```vue
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-disabled="disabled"
|
||||
:class="$style.fakeButton"
|
||||
@click="onClick"
|
||||
@keydown.enter="onClick"
|
||||
@keydown.space.prevent="onClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
```
|
||||
|
||||
| 属性 / ハンドラ | なぜ必要か |
|
||||
|---|---|
|
||||
| `role="button"` | スクリーンリーダーにボタンとして読ませる |
|
||||
| `tabindex="0"` | キーボードでフォーカス可能にする |
|
||||
| `@keydown.enter` | Enter で発火 (本物の `<button>` の挙動を再現) |
|
||||
| `@keydown.space.prevent` | Space で発火 + ページスクロール防止 |
|
||||
| `:aria-disabled` | disabled スタイルだけでなく状態も伝える |
|
||||
|
||||
`@keydown.enter` を忘れて click だけ付けるのが最頻出ミス。
|
||||
|
||||
#### `<a>` をボタン代わりに使うのは原則禁止
|
||||
|
||||
URL に飛ばない `<a href="#" @click.prevent>` は a11y / SEO 両面で良くない。リンクなら `<MkA>` ([MkA.vue](../../../../../packages/frontend/src/components/global/MkA.vue))、アクションなら `<button>` を使う。
|
||||
|
||||
### フォーム要素
|
||||
|
||||
#### `<label>` 接続
|
||||
|
||||
```vue
|
||||
<!-- ✅ for / id で結ぶ -->
|
||||
<label :for="id">{{ i18n.ts.username }}</label>
|
||||
<input :id="id" v-model="username" type="text">
|
||||
|
||||
<!-- ✅ ラップする (id 不要) -->
|
||||
<label>
|
||||
{{ i18n.ts.username }}
|
||||
<input v-model="username" type="text">
|
||||
</label>
|
||||
```
|
||||
|
||||
label を slot で受け取る共通コンポーネント ([MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue), [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue)) を使うとこの規約は自然に守れる。
|
||||
|
||||
#### `aria-label` で代替
|
||||
|
||||
slot や label を見せたくない (アイコンのみのボタンなど) 場合は `aria-label`:
|
||||
|
||||
```vue
|
||||
<button class="_button" :aria-label="i18n.ts.close" @click="emit('close')">
|
||||
<i class="ti ti-x"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
`aria-label` の値も i18n 経由にする (英語直書きは禁止)。
|
||||
|
||||
**実情:** 現状コードベースでは `aria-label` の使用例自体が乏しい (アイコンの hover ヒントには `:title="i18n.ts..."` が使われるが、`title` は tooltip でありスクリーンリーダー向けラベルの代替にはならない)。このため aria-label は確立した慣習というより a11y 上の推奨ベストプラクティスとして書いている。新規でアイコンのみのボタンを足すなら付けるのが望ましい。
|
||||
|
||||
### `:disabled` と `aria-disabled` の整合
|
||||
|
||||
- 本物の `<button :disabled>` ならブラウザが click を抑止するが、`<div role="button">` は止めてくれない。`aria-disabled` を付けるだけでなく、**ハンドラ側でも早期 return** する:
|
||||
|
||||
```ts
|
||||
function onClick() {
|
||||
if (props.disabled) return; // ← これが無いと disabled でも発火する
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### キーボード操作
|
||||
|
||||
- Tab で全ての操作可能要素にたどり着けること (`tabindex="-1"` を不用意に付けない)
|
||||
- モーダル / popup を開いたら focus trap を考える ([MkModal.vue](../../../../../packages/frontend/src/components/MkModal.vue) のような既存コンポーネントは内部で対応している)
|
||||
- リスト中の項目は矢印キー操作も考慮する。Space / Enter で開く・確定する UI は `MkSelect.vue` の `@keydown.space.enter`(メニューを開く) パターンを参考にする
|
||||
|
||||
### 既存実装の参考
|
||||
|
||||
| パターン | 既存コンポーネント |
|
||||
|---|---|
|
||||
| 標準的なボタン | [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) |
|
||||
| カスタム UI でも a11y を満たす | [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue) |
|
||||
| input + label slot | [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) |
|
||||
| キーボード操作対応の選択 UI | [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue) |
|
||||
|
||||
### ありがちな PR レビュー指摘
|
||||
|
||||
- `<div @click>` に role / tabindex / keydown が無い
|
||||
- アイコンだけのボタンに `aria-label` が無い (Tabler icon 自体には意味情報が無い)
|
||||
- `disabled` スタイルだけ付けて `aria-disabled` / ハンドラ抑止が無い
|
||||
- フォーカスリング (`:focus-visible` / `outline`) を `outline: none` で消したまま放置
|
||||
@@ -0,0 +1,60 @@
|
||||
# Frontend テスト (Vitest / Cypress)
|
||||
|
||||
Misskey frontend のテスト構成。
|
||||
|
||||
## Vitest (unit)
|
||||
|
||||
```bash
|
||||
pnpm --filter frontend test # 1 回実行
|
||||
pnpm --filter frontend test-and-coverage # カバレッジ付き
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
- 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`)
|
||||
- ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts](../../../../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts))
|
||||
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー)
|
||||
|
||||
## Cypress E2E
|
||||
|
||||
Cypress は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
|
||||
|
||||
```bash
|
||||
# 1. テスト用 DB / Redis を起動 (テスト用ポート。開発用の compose.local-db.yml ではない)
|
||||
docker compose -f packages/backend/test/compose.yml up -d
|
||||
|
||||
# 2. テスト設定を配置 (未作成なら。例示なので、cpコマンドは環境にあったコマンドに適宜読み替えること)
|
||||
cp .github/misskey/test.yml .config/test.yml
|
||||
|
||||
# 3. 全体ビルド
|
||||
pnpm build
|
||||
|
||||
# 4. テストサーバー起動 + Cypress 実行 (いずれもルートから)
|
||||
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Cypress run
|
||||
pnpm cy:open # 対話的に開く (サーバーは別途 pnpm start:test で起動しておく)
|
||||
```
|
||||
|
||||
- 設定: ルート [cypress.config.ts](../../../../../cypress.config.ts)
|
||||
- テスト本体は [cypress/](../../../../../cypress/) 配下
|
||||
|
||||
新規 frontend 機能の E2E は Cypress に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
|
||||
|
||||
## Storybook (視覚確認 + Chromatic 視覚回帰)
|
||||
|
||||
詳細は → [storybook.md](storybook.md)。
|
||||
|
||||
```bash
|
||||
pnpm --filter frontend storybook-dev # http://localhost:6006
|
||||
pnpm --filter frontend build-storybook # 静的ビルド
|
||||
```
|
||||
|
||||
各コンポーネント横に `*.stories.impl.ts` を併設する慣習 (例: `MkButton.stories.impl.ts`)。Chromatic (`pnpm --filter frontend chromatic`) で視覚回帰チェック。
|
||||
|
||||
## ローカル DB / Redis
|
||||
|
||||
frontend のテスト種別で DB / Redis の要否が違う:
|
||||
|
||||
- **Vitest (unit)** — DB 不要。ロジック / コンポーネント単体のテストで backend に繋がない (CI の `vitest` ジョブにも `services:` は無い)
|
||||
- **Cypress (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Cypress E2E の手順を参照)
|
||||
|
||||
開発用の `compose.local-db.yml` (db `5432` / redis `6379`) は **テストには使わない**。テスト用の `packages/backend/test/compose.yml` (`54312` / `56312`) とはポートが異なり、混同すると接続できない。
|
||||
@@ -0,0 +1,412 @@
|
||||
# i18n 使い分け / Crowdin 安全策 / トラブルシュート
|
||||
|
||||
`i18n.ts` / `i18n.tsx` の使い分け、Crowdin との同期メカニズム、頻発する型エラー / 実行時警告の対処を 1 箇所にまとめたページ。
|
||||
|
||||
## 目次
|
||||
|
||||
- [基本: ts と tsx の使い分け](#基本-ts-と-tsx-の使い分け)
|
||||
- [実装パターン](#実装パターン)
|
||||
- [Crowdin 安全策 (既存キーのリネーム / 復旧)](#crowdin-安全策-既存キーのリネーム--復旧)
|
||||
- [トラブルシュート](#トラブルシュート)
|
||||
- [制約と補足](#制約と補足)
|
||||
|
||||
## 基本: ts と tsx の使い分け
|
||||
|
||||
文言は **必ず** [i18n.ts](../../../../../packages/frontend/src/i18n.ts) 経由で参照する。引数の有無で **使う変数名そのものが変わる**。間違えると、非パラメータキーを `i18n.tsx` で呼ぶ場合は型エラーになるが、パラメータキーを `i18n.ts` で参照する場合は型エラーにならず `{name}` 等が未展開のまま画面に出る (後述のトラブルシュート参照)。
|
||||
|
||||
- 引数なし → `i18n.ts.<key>` (プロパティアクセス)
|
||||
|
||||
```ts
|
||||
os.toast(i18n.ts.removed);
|
||||
```
|
||||
|
||||
- 引数あり → `i18n.tsx.<key>(...)` (関数呼び出し)
|
||||
|
||||
```ts
|
||||
os.alert({ type: 'info', text: i18n.tsx.unfollowConfirm({ name: user.username }) });
|
||||
```
|
||||
|
||||
YAML 側に `{name}` 形式のプレースホルダが含まれているキーは **`i18n.tsx`** からしか呼べない。誤って `i18n.ts.unfollowConfirm` と書くと値がフォーマット前の関数になってそのまま表示される。
|
||||
|
||||
- **既存キーの再利用が第一**。新キー追加が必要に見えても、まず `locales/ja-JP.yml` を grep して `deleteAreYouSure({ x })` のような汎用キー (`x` プレースホルダ) が転用可能でないか確認する。新キー追加は [tasks/adding-i18n-key.md](../tasks/adding-i18n-key.md)。他言語ファイルは Crowdin の自動配信先なので絶対に手で触らない
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{ name: string }>();
|
||||
|
||||
async function onDelete() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.driveFileDeleteConfirm({ name: props.name }), // 引数あり
|
||||
});
|
||||
if (canceled) return;
|
||||
os.toast(i18n.ts.removed); // 引数なし
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
| 用途 | 書き方 |
|
||||
|---|---|
|
||||
| 単純文字列 | `i18n.ts.save` |
|
||||
| ネスト | `i18n.ts._settings.general` |
|
||||
| パラメータ付き (1 個) | `i18n.tsx.unfollowConfirm({ name })` |
|
||||
| パラメータ付き (複数) | `i18n.tsx.monthAndDay({ month, day })` |
|
||||
| Vue テンプレート内 | `{{ i18n.ts.save }}` / `{{ i18n.tsx.unfollowConfirm({ name }) }}` |
|
||||
|
||||
## 実装パターン
|
||||
|
||||
### HTML タグ埋め込み
|
||||
|
||||
ja-JP.yml の値に `<b>` / `<br>` / `<strong>` を含めて、表示側で v-html や `<Mfm>` で描画するパターンが多用されている。
|
||||
|
||||
```yaml
|
||||
# locales/ja-JP.yml
|
||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつです。"
|
||||
|
||||
# locales/ja-JP.yml (改行 + br)
|
||||
driveAboutTip: "ドライブでは、過去に...<br>\nノートに添付する際に再利用したり...<br>\n<b>ファイルを削除すると...</b><br>\n..."
|
||||
```
|
||||
|
||||
参照側:
|
||||
```vue
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: 'Misskey' })" />
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- HTML を含むキー値は **必ずダブルクォート** で囲む (YAML パース失敗回避)
|
||||
- `v-html` 越しの XSS リスクが無いことを必ず確認する。パラメータ側にユーザー入力をそのまま渡すと事故る。安全な静的文字列か、別途エスケープ済の値だけにする
|
||||
|
||||
### リアクティブ参照 + 動的キー切替
|
||||
|
||||
時間経過などで翻訳キー自体を切り替えたい場合の慣習。`computed` でラップし、ブラケット記法で翻訳キーを動的に選ぶ。
|
||||
|
||||
出典: [packages/frontend/src/components/MkPoll.vue](../../../../../packages/frontend/src/components/MkPoll.vue) の `_poll` 動的キー
|
||||
|
||||
```ts
|
||||
const timer = computed(() => i18n.tsx._poll[
|
||||
remaining.value >= 86400 ? 'remainingDays' :
|
||||
remaining.value >= 3600 ? 'remainingHours' :
|
||||
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
|
||||
]({
|
||||
s: Math.floor(remaining.value % 60),
|
||||
m: Math.floor(remaining.value / 60) % 60,
|
||||
h: Math.floor(remaining.value / 3600) % 24,
|
||||
d: Math.floor(remaining.value / 86400),
|
||||
}));
|
||||
```
|
||||
|
||||
対応する yml (各キーで実際に使うプレースホルダは違って良い):
|
||||
|
||||
```yaml
|
||||
_poll:
|
||||
remainingDays: "終了まであと{d}日{h}時間" # {d} {h}
|
||||
remainingHours: "終了まであと{h}時間{m}分" # {h} {m}
|
||||
remainingMinutes: "終了まであと{m}分{s}秒" # {m} {s}
|
||||
remainingSeconds: "終了まであと{s}秒" # {s}
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- 各キーで使うプレースホルダは **バラバラで構わない**
|
||||
- **呼び出し側で候補キー全体に必要な全パラメータの superset を 1 つの引数オブジェクトで渡す**。各キーの内部実装は受け取ったオブジェクトから自分が必要なものだけ拾う
|
||||
|
||||
### 識別子として無効なキー名 (ブラケット記法)
|
||||
|
||||
キー名が数字始まりや予約語の場合、ドット記法ではアクセスできずブラケット記法を使う。
|
||||
|
||||
出典: [packages/frontend/src/components/MkSignin.totp.vue](../../../../../packages/frontend/src/components/MkSignin.totp.vue)
|
||||
|
||||
```vue
|
||||
<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
|
||||
```
|
||||
|
||||
新規キー追加時は **lowerCamelCase を守れば不要**。
|
||||
|
||||
### ネスト + パラメータ複合
|
||||
|
||||
```vue
|
||||
{{ i18n.tsx._uploader.maxFileSizeIsX({ x: maxSize + 'MB' }) }}
|
||||
{{ i18n.tsx._auth.shareAccess({ name: appName }) }}
|
||||
```
|
||||
|
||||
### `tsx` の引数に `ts` を埋め込む
|
||||
|
||||
別の翻訳済み文字列をパラメータとして渡せる。
|
||||
|
||||
出典: [packages/frontend/src/components/MkSignupDialog.rules.vue](../../../../../packages/frontend/src/components/MkSignupDialog.rules.vue)
|
||||
|
||||
```ts
|
||||
i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules })
|
||||
```
|
||||
|
||||
### 三項演算子で ts / tsx を切り替え
|
||||
|
||||
パラメータ有無で出し分け。
|
||||
|
||||
```vue
|
||||
{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}
|
||||
```
|
||||
|
||||
## Crowdin 安全策 (既存キーのリネーム / 復旧)
|
||||
|
||||
ja-JP.yml 以外の locales/*.yml は **Crowdin の自動配信先**。手動編集や source 側の不用意な操作で他言語の翻訳資産が失われる。
|
||||
|
||||
### 同期メカニズム
|
||||
|
||||
[crowdin.yml](../../../../../crowdin.yml):
|
||||
```yaml
|
||||
files:
|
||||
- source: /locales/ja-JP.yml
|
||||
translation: /locales/%locale%.yml
|
||||
update_option: update_as_unapproved
|
||||
```
|
||||
|
||||
- `ja-JP.yml` = **source**。これだけが翻訳元
|
||||
- `en-US.yml` / `fr-FR.yml` ほか `ja-JP.yml` 以外の全 locale = **translation**。Crowdin が自動 PR で更新する
|
||||
- 翻訳済みキーの **source 文字列が変わると** `update_as_unapproved` 設定により翻訳が "unapproved" 状態に戻る (= レビュー再要求)
|
||||
- **キー名自体が変わる** と Crowdin は別キー扱いし、旧キーの翻訳は孤立 → 同期で削除される
|
||||
|
||||
根拠: [locales/README.md](../../../../../locales/README.md) "DO NOT edit locale files except `ja-JP.yml`."
|
||||
|
||||
### 既存キーをリネームしたい時 (3 段階)
|
||||
|
||||
単純な「旧キー削除 → 新キー追加」を 1 PR で行うと、すべての言語の旧キー翻訳が失われる。以下のように分割する。
|
||||
|
||||
#### Step 1: 新キー追加 (PR A)
|
||||
|
||||
旧キーを残したまま、新キー (同等の意味の日本語) を ja-JP.yml に追加する。
|
||||
|
||||
```yaml
|
||||
# 旧キー (まだ残す)
|
||||
_settings:
|
||||
theme: "テーマ"
|
||||
# 新キー (追加)
|
||||
appearance: "外観"
|
||||
```
|
||||
|
||||
参照箇所も新キーに移行 (frontend の全 grep + 置換)。
|
||||
|
||||
#### Step 2: マージ → Crowdin 翻訳が来るのを待つ
|
||||
|
||||
Crowdin の自動 PR で他言語にも `appearance` が追加され、翻訳が入る。`update_option: update_as_unapproved` のため、初回は unapproved 状態。プロジェクト管理者が approve するまで本番には載らない (フォールバックで日本語が出る)。
|
||||
|
||||
通常は数日〜数週間。急ぐ場合は Crowdin プロジェクト管理者に依頼。
|
||||
|
||||
#### Step 3: 旧キー削除 (PR B)
|
||||
|
||||
新キーの翻訳が十分埋まった後、別 PR で旧キー (`theme`) を ja-JP.yml から削除。次の Crowdin 同期で他言語からも消える。
|
||||
|
||||
### 単純リネームをやってしまったら
|
||||
|
||||
```bash
|
||||
# git diff で他言語 yml が変更されていないか必ず確認 (出力が空なら OK)
|
||||
git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'
|
||||
```
|
||||
|
||||
`grep -v 'ja-JP.yml'` を diff 本文に当てる書き方は、ja-JP.yml 単体の変更でも追加行 (`+`) が素通りして必ず非空になるため使わない。**ファイル名にだけ grep を当てる** こと。
|
||||
|
||||
- **他言語 yml が変更されていたら即 revert**:
|
||||
```bash
|
||||
git restore --source=develop -- locales/en-US.yml locales/<lang>.yml
|
||||
```
|
||||
|
||||
- ja-JP.yml だけで旧キー削除 + 新キー追加してしまった場合は、PR を分割するか、上記 3 段階に組み直す。**マージ前なら間に合う**
|
||||
|
||||
### ja-JP.yml 以外を触ってしまったら
|
||||
|
||||
```bash
|
||||
# 最も安全な復旧: develop 側の中身に戻す
|
||||
git restore --source=develop -- locales/en-US.yml
|
||||
# あるいは特定 path だけステージから外し作業ツリーごと戻す
|
||||
git checkout HEAD -- locales/zh-CN.yml
|
||||
```
|
||||
|
||||
PR 化前なら何度でもやり直せる。**マージしてしまうと Crowdin 側との整合性が崩れて手動回復が必要** になるので、PR レビュー段階で必ず `locales/*.yml` (ja-JP 以外) の diff がゼロであることを確認する。
|
||||
|
||||
### CHANGELOG 記載の判定
|
||||
|
||||
| 変更内容 | CHANGELOG 記載 |
|
||||
|---|---|
|
||||
| 新規画面追加と一緒に新キー追加 | 必要 (`### Client` に Feat/Enhance) |
|
||||
| 既存文言の改善 (誤字脱字以外) | 必要 (`### Client` に Enhance) |
|
||||
| 誤字脱字・微妙な言い回し修正 | 不要 |
|
||||
| キーのリネーム (UI 変化なし) | 不要 |
|
||||
| キー削除 (画面から消える) | 必要 (`### Client` に Feat / 機能削除) |
|
||||
|
||||
書き方は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) を参照。
|
||||
|
||||
## トラブルシュート
|
||||
|
||||
i18n 周辺で踏みやすい失敗とその対処。エラー文字列で grep してたどり着けるよう整理。
|
||||
|
||||
### 型エラー: `Property '<key>' does not exist on type 'Locale'`
|
||||
|
||||
**症状**:
|
||||
```
|
||||
packages/frontend/src/components/MkXxx.vue
|
||||
> i18n.ts.newKey
|
||||
Property 'newKey' does not exist on type 'Locale'.
|
||||
```
|
||||
|
||||
**原因**: ja-JP.yml にキーは追加したが、`packages/i18n` の型生成 (`autogen/locale.ts`) が再生成されていない。
|
||||
|
||||
**対処**:
|
||||
|
||||
- `pnpm dev` を起動中なら、`packages/i18n` の watch (`nodemon ... tsx ./build.ts --watch`) が自動再生成するので、yml 保存後に typecheck をやり直す
|
||||
- 一回だけ手動再生成したいなら: `pnpm --filter i18n generate` (実体は `tsx scripts/generateLocaleInterface.ts`)
|
||||
- 検出経路: `pnpm --filter frontend lint`
|
||||
|
||||
実装根拠: [packages/i18n/scripts/generateLocaleInterface.ts](../../../../../packages/i18n/scripts/generateLocaleInterface.ts) (パラメータ抽出の正規表現 `/\{(\w+)\}/g`)。
|
||||
|
||||
### 型エラー: ts/tsx の取り違え
|
||||
|
||||
**症状 A** (パラメータ無しキーを tsx で呼ぶ):
|
||||
```
|
||||
i18n.tsx.save({...})
|
||||
> Property 'save' does not exist on type 'Tsx<Locale>'.
|
||||
```
|
||||
|
||||
**症状 B** (パラメータ付きキーを ts で参照、関数化されたまま使う):
|
||||
```vue
|
||||
{{ i18n.ts.unfollowConfirm }}
|
||||
<!-- 画面に "{name}のフォローを解除しますか?" が {name} 未置換のまま出る -->
|
||||
```
|
||||
|
||||
**原因**: `Tsx<T>` 型 ([packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts)) は `ParameterizedString<P>` を持つキーだけを関数として公開する。
|
||||
|
||||
**対処**: パラメータ有無は yml の `{...}` 記法で決まる。
|
||||
|
||||
| yml の値 | ts | tsx |
|
||||
|---|---|---|
|
||||
| `"保存"` | `i18n.ts.save` ✅ | (キー存在せず) ❌ |
|
||||
| `"{name}のフォローを解除しますか?"` | `i18n.ts.unfollowConfirm` → `{name}` 未置換の文字列のまま ❌ | `i18n.tsx.unfollowConfirm({ name })` ✅ |
|
||||
|
||||
### 実行時警告: `Unexpected locale key: <key>`
|
||||
|
||||
**症状**: 開発モードのコンソールに出る。
|
||||
|
||||
**原因**: dev mode の Proxy が ja-JP.yml に存在しないキーへのアクセスを検知 ([packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts) の dev 用 Proxy)。
|
||||
|
||||
**対処**: ja-JP.yml に該当キーを追加するか、参照側のタイポを直す。
|
||||
|
||||
### 実行時警告: `Missing locale parameters: <param> at <key>`
|
||||
|
||||
**症状**: dev mode コンソール。
|
||||
|
||||
**原因**:
|
||||
|
||||
- yml 側 `{name}` に対し、呼び出し側で `{ user: ... }` のように **キー名が違う**
|
||||
- あるいは引数オブジェクトに値が含まれていない
|
||||
|
||||
実装根拠: [packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts) (`Object.hasOwn(arg, expressions[i])` チェック)。
|
||||
|
||||
**対処**: yml と呼び出し側でパラメータ名を一致させる。yml 側のキー名を変更したら、呼び出し側 (frontend 全体) を grep で揃える。
|
||||
|
||||
### YAML パース失敗
|
||||
|
||||
**症状**: `pnpm --filter i18n generate` 実行時に `YAMLException: ...`、または `pnpm dev` の watch ログにエラー。
|
||||
|
||||
**原因**: 値に YAML の特殊文字 (`<` `>` `:` `'` `&` `*` `|` `>` `#`) を含むのに **クォートしていない**。
|
||||
|
||||
**対処**: 値全体を `"..."` (ダブルクォート) で囲む。
|
||||
|
||||
```yaml
|
||||
# OK: HTML タグを含む
|
||||
poweredByMisskeyDescription: "{name}は、...プラットフォーム<b>Misskey</b>のサーバーのひとつです。"
|
||||
|
||||
# OK: コロン・シングルクォート・角括弧を含む URL 説明
|
||||
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL、S3: 'https://<bucket>.s3.amazonaws.com'、GCS等: 'https://storage.googleapis.com/<bucket>'。"
|
||||
|
||||
# OK: 改行をリテラルで埋め込む
|
||||
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの...<br>\nノートに添付する際に..."
|
||||
```
|
||||
|
||||
YAML の block scalar (`|` / `>`) も使えるが、HTML タグ + プレースホルダ混在では **ダブルクォート + `\n` エスケープ** の方が安定する。
|
||||
|
||||
### キー名衝突: `_lang_` を上書きしてしまう
|
||||
|
||||
**症状**: 各言語ファイルの先頭にある `_lang_` (例: ja-JP は `"日本語"`) を別用途で使おうとして上書き。
|
||||
|
||||
**原因**: `_lang_` は **言語自身の表記** に予約されている ([packages/i18n/src/autogen/locale.ts](../../../../../packages/i18n/src/autogen/locale.ts) の先頭キー)。
|
||||
|
||||
**対処**: 新規キーは別名にする。
|
||||
|
||||
### frontend で diff を当てても変わらない
|
||||
|
||||
**症状**: ja-JP.yml を変更したが画面に反映されない。
|
||||
|
||||
**原因**:
|
||||
|
||||
- `pnpm dev` ではなく `pnpm --filter frontend watch` だけ起動していて、`packages/i18n` の watch が走っていない
|
||||
- もしくは frontend へ配信される生成物 (`built/_frontend_dist_/locales/*.json`) がブラウザ側でキャッシュされている
|
||||
|
||||
**対処**: ルートの `pnpm dev` を起動する (frontend + backend + i18n watch が全部立ち上がる)。それでも反映しないならブラウザのキャッシュをクリア、または `pnpm --filter i18n build` を手動実行。
|
||||
|
||||
## 制約と補足
|
||||
|
||||
### ICU MessageFormat 非対応
|
||||
|
||||
[packages/i18n/scripts/generateLocaleInterface.ts](../../../../../packages/i18n/scripts/generateLocaleInterface.ts) の正規表現は `/\{(\w+)\}/g`。つまり受け付けるのは **`{paramName}` 形式の単純置換のみ**。
|
||||
|
||||
```yaml
|
||||
# NG: ICU plural — そのまま画面に文字列として出るだけ
|
||||
items: "{count, plural, one {1個} other {{count}個}}"
|
||||
|
||||
# NG: ICU select
|
||||
gender: "{gender, select, male {彼} female {彼女} other {その人}}"
|
||||
```
|
||||
|
||||
代替戦略:
|
||||
|
||||
#### 1. 件数別にキーを分ける
|
||||
|
||||
```yaml
|
||||
# OK
|
||||
withNFiles: "{n}個のファイル"
|
||||
withOneFile: "1個のファイル"
|
||||
```
|
||||
|
||||
```ts
|
||||
const text = files.length === 1
|
||||
? i18n.ts.withOneFile
|
||||
: i18n.tsx.withNFiles({ n: files.length });
|
||||
```
|
||||
|
||||
#### 2. 切替パターン (動的キー)
|
||||
|
||||
時間経過のような連続的な分岐は MkPoll のパターン ([上記「リアクティブ参照」](#リアクティブ参照--動的キー切替)) を採用。
|
||||
|
||||
### 予約キー `_lang_`
|
||||
|
||||
各 yml ファイルの **トップレベル先頭** に置かれ、その言語自身の表記名を持つ。
|
||||
|
||||
```yaml
|
||||
# locales/ja-JP.yml (トップレベル先頭)
|
||||
_lang_: "日本語"
|
||||
```
|
||||
|
||||
UI の言語切替プルダウンなどで参照される。**新規キーには使わない**。
|
||||
|
||||
### Storybook での挙動
|
||||
|
||||
Storybook 環境はバンドラが別物なので、本番の i18n パッケージをそのままは使わない。代わりに [packages/frontend/.storybook/preload-locale.ts](../../../../../packages/frontend/.storybook/preload-locale.ts) がビルド時に **ja-JP の locale だけを JSON にダンプして同居 `locale.ts` を生成** する。
|
||||
|
||||
つまり Storybook では:
|
||||
|
||||
- **ja-JP の文字列だけが見える** (他言語の検証はできない)
|
||||
- ja-JP.yml にキーを追加した直後に Storybook を起動しても、`preload-locale.ts` 実行前なら反映されない。Storybook を再起動するか、`packages/i18n` を一度 build する
|
||||
- stories からの呼び方は通常通り: `i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })`
|
||||
|
||||
### backend での i18n 直接参照は基本無し
|
||||
|
||||
i18n は frontend (および一部の SSR されるエラーページ) でのみ使われる。`packages/backend` 配下から `import { i18n }` するパターンは原則無く、API エラー文言は別ルート (`ApiError` の i18n 化されていないメッセージ + frontend 側で翻訳) で扱う。
|
||||
|
||||
### 改行の扱い
|
||||
|
||||
ダブルクォート値の中で `\n` は実際の改行になる。block scalar (`|`) でも可だが、HTML タグやプレースホルダ混在では扱いづらい。慣習はダブルクォート + `\n`。
|
||||
|
||||
Vue 側で表示時に `white-space: pre-wrap` などを当てる必要あり。
|
||||
@@ -0,0 +1,96 @@
|
||||
# `os.*` UI ヘルパー
|
||||
|
||||
[`packages/frontend/src/os.ts`](../../../../../packages/frontend/src/os.ts) で公開されている UI 操作 API の一覧。**ブラウザ標準の `window.alert()` / `window.confirm()` / `window.prompt()` を直接呼ばない**。これらは Misskey のテーマ / アクセシビリティ / モーダルレイヤと整合しないため。
|
||||
|
||||
## 主要 API
|
||||
|
||||
| 関数 | 用途 |
|
||||
|---|---|
|
||||
| `os.alert({ type?, title?, text? })` | 単方向アラート (全フィールド任意) |
|
||||
| `os.confirm({ type, title?, text? })` | yes/no 確認 (`type` 必須、`{ canceled }` を返す) |
|
||||
| `os.toast(message)` | 一時通知 |
|
||||
| `os.popup(component, props, handlers)` | 任意コンポーネントの非同期ポップアップ |
|
||||
| `os.popupMenu(items, anchor?)` | コンテキストメニュー |
|
||||
| `os.contextMenu(items, ev)` | 右クリックメニュー |
|
||||
| `os.form(title, fields)` | フォームダイアログ |
|
||||
| `os.apiWithDialog(endpoint, data)` | API 呼出し + エラー時ダイアログ表示 |
|
||||
| `os.success()` / `os.waiting()` | 成功 / ローディング表示 |
|
||||
|
||||
## 使用例
|
||||
|
||||
### `os.alert` (単方向通知)
|
||||
|
||||
```ts
|
||||
await os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts.savedSuccessfully,
|
||||
});
|
||||
```
|
||||
|
||||
`type` は `'info'` / `'warning'` / `'error'` / `'question'` / `'success'` / `'waiting'`。
|
||||
|
||||
### `os.confirm` (yes/no 確認)
|
||||
|
||||
```ts
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._notes.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
// 削除処理
|
||||
```
|
||||
|
||||
`canceled === true` のとき何もしない、というパターンが頻出。
|
||||
|
||||
### `os.toast` (一時通知)
|
||||
|
||||
```ts
|
||||
os.toast(i18n.ts.deleted);
|
||||
```
|
||||
|
||||
成功通知などの軽い fire-and-forget なフィードバック。
|
||||
|
||||
### `os.popup` (任意コンポーネント)
|
||||
|
||||
```ts
|
||||
const { dispose } = os.popup(MkUserSelectDialog, {
|
||||
includeSelf: false,
|
||||
}, {
|
||||
ok: (user) => {
|
||||
// ...
|
||||
dispose();
|
||||
},
|
||||
cancel: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
カスタムダイアログを開く場合は、コンポーネント (props / emits) を `os.popup` で起動する。`dispose()` で閉じる。
|
||||
|
||||
### `os.apiWithDialog` (API + 自動エラーダイアログ)
|
||||
|
||||
```ts
|
||||
const result = await os.apiWithDialog('notes/create', {
|
||||
text: 'hello',
|
||||
});
|
||||
// 成功時: result は API レスポンス
|
||||
// 失敗時: 自動でエラーダイアログを表示。ただし promise 自体は reject されるので、await するなら try/catch が必要
|
||||
```
|
||||
|
||||
通常の `misskeyApi(...)` だと自前でエラーダイアログ表示が必要だが、`apiWithDialog` は失敗時に自動で `os.alert({ type: 'error', ... })` を表示してくれる。ただし返す promise は元の `misskeyApi(...)` と同一で **reject される** ([os.ts](../../../../../packages/frontend/src/os.ts) で `return promise`)。`await` する場合は依然 try/catch が要る (ダイアログ表示後に後続処理を止めたいだけなら catch して握りつぶす)。
|
||||
|
||||
## なぜブラウザ標準 UI を使わないか
|
||||
|
||||
- `window.alert()` は Misskey のテーマ (ダークモード / カスタムテーマ) に追従しない
|
||||
- `window.confirm()` はキーボード操作・focus trap・i18n のいずれも Misskey の規約と整合しない
|
||||
- `window.prompt()` の入力 UI も同じ
|
||||
- ブラウザ依存の表示揺れ (Firefox / Safari / Chrome で見た目が違う)
|
||||
- vue-component-reviewer から指摘される
|
||||
|
||||
代わりに `os.alert` / `os.confirm` / `os.form` / `os.popup` を使う。
|
||||
|
||||
## 参照ファイル
|
||||
|
||||
- [packages/frontend/src/os.ts](../../../../../packages/frontend/src/os.ts) — 全 API の実装
|
||||
- 既存のダイアログ系コンポーネント: `MkDialog.vue` (alert / confirm はこれを再利用)、`MkFormDialog.vue` 等
|
||||
@@ -0,0 +1,135 @@
|
||||
# SCSS Modules / CSS 変数 / utility class
|
||||
|
||||
Misskey の SCSS 規約。`<style lang="scss" module>` の書き方、`--MI_THEME-*` / `--MI-*` CSS 変数の使い分け、グローバル utility class の一覧をまとめる。
|
||||
|
||||
## CSS 変数の使い分け
|
||||
|
||||
Misskey のテーマシステムは 2 系統の CSS 変数で構成される。新規のスタイルは **必ず変数経由** にする。直接の `#fff` / `rgb()` / `rgba()` ハードコードは vue-component-reviewer から Major 指摘される。
|
||||
|
||||
### `--MI_THEME-*` (テーマ依存)
|
||||
|
||||
ユーザーが選んだテーマ (light / dark / 個別テーマ) で変わる色。`packages/frontend-shared/themes/_dark.json5` などで定義。
|
||||
|
||||
| 変数 | 用途 |
|
||||
|---|---|
|
||||
| `--MI_THEME-bg` | ページ背景 |
|
||||
| `--MI_THEME-panel` | カード / パネル背景 |
|
||||
| `--MI_THEME-panelHighlight` | 強調表示パネル |
|
||||
| `--MI_THEME-fg` | 本文文字色 |
|
||||
| `--MI_THEME-fgHighlighted` | 強調文字色 |
|
||||
| `--MI_THEME-fgOnPanel` | パネル上の文字 |
|
||||
| `--MI_THEME-fgOnAccent` | accent 色背景上の文字 (≒白系) |
|
||||
| `--MI_THEME-accent` | プライマリアクセント (リンク、active state) |
|
||||
| `--MI_THEME-accentedBg` | accent 系の薄背景 |
|
||||
| `--MI_THEME-divider` | 罫線 |
|
||||
| `--MI_THEME-error` | エラー色 |
|
||||
| `--MI_THEME-warn` / `--MI_THEME-infoWarnBg` / `--MI_THEME-infoWarnFg` | 警告系 |
|
||||
| `--MI_THEME-infoBg` / `--MI_THEME-infoFg` | 情報系 |
|
||||
| `--MI_THEME-buttonBg` / `--MI_THEME-buttonHoverBg` | ボタン背景 |
|
||||
| `--MI_THEME-inputBorder` / `--MI_THEME-inputBorderHover` | フォーム枠 |
|
||||
| `--MI_THEME-focus` | フォーカスリング色 |
|
||||
| `--MI_THEME-link` | リンク色 |
|
||||
| `--MI_THEME-mention` / `--MI_THEME-hashtag` | メンション / ハッシュタグ |
|
||||
|
||||
全部の一覧が必要なら `packages/frontend-shared/themes/_light.json5` を読むのが早い (JSON5 で全キーが揃っている)。
|
||||
|
||||
### `--MI-*` (UI 共通定数、テーマ非依存)
|
||||
|
||||
| 変数 | 用途 |
|
||||
|---|---|
|
||||
| `--MI-radius` | 標準角丸 (`12px`) |
|
||||
| `--MI-margin` | 標準余白 (大、`16px` / モバイルでは `10px`) |
|
||||
| `--MI-marginHalf` | 標準余白の半分 |
|
||||
| `--MI-modalBgFilter` | モーダル背景 (backdrop) のフィルタ |
|
||||
|
||||
`var(--MI-radius)` を使うとアプリ全体で角丸の大きさが揃う。`border-radius: 12px;` のように直書きすると、後から角丸を変える要件が来たときに全件直すことになる。
|
||||
|
||||
### ハードコードの例外
|
||||
|
||||
色は基本ハードコード禁止だが、以下のケースは正当化される:
|
||||
|
||||
- `transparent` / `currentColor` / `none` などの CSS キーワード
|
||||
- alpha だけ動的に変えたい → `color-mix(in srgb, var(--MI_THEME-fg) 50%, transparent)` のように合成する
|
||||
- アイコンサイズ等、CSS 変数化されていない数値定数 (`font-size: 14px;` 等は OK)
|
||||
|
||||
## グローバル utility class
|
||||
|
||||
`packages/frontend/src/style.scss` に定義されたグローバル class。`<style module>` 内のクラスと **併用** する (`:class="[$style.root, '_button']"` ではなく、HTML の `class="_button"` 属性で直接書く)。
|
||||
|
||||
下表は **よく使う代表例** で網羅ではない (class は随時増減するため、この一覧は腐りやすい)。手元の class が実在するか / 実装を確認したいときは正本の [packages/frontend/src/style.scss](../../../../../packages/frontend/src/style.scss) を直接見る (`grep -nE '^\._' packages/frontend/src/style.scss` で定義済み class を列挙できる)。
|
||||
|
||||
| class | 意味 |
|
||||
|---|---|
|
||||
| `_button` | クリック可能な無装飾ベース (`appearance:none` + `cursor:pointer` + disabled cursor のリセットのみ。focus ring や ripple は**含まない** — ripple が要るなら `MkButton.vue` を使う)。`<button>` または `<a>` に付ける |
|
||||
| `_buttonPrimary` | `_button` + accent 色背景 (確定アクション) |
|
||||
| `_buttonGradate` | `_button` + グラデーション背景 |
|
||||
| `_panel` | カード / パネル枠 (背景 + 角丸 + `overflow:clip`。shadow は含まない) |
|
||||
| `_selectable` | テキスト選択許可 (Misskey はデフォルトで本文以外の選択を抑止しているため) |
|
||||
| `_selectableAtomic` | 子要素まとめて 1 単位で選択 |
|
||||
| `_noSelect` | テキスト選択禁止 |
|
||||
| `_nowrap` | `white-space: nowrap;` |
|
||||
| `_help` | accent 色 + `cursor: help` (ヘルプアイコン用) |
|
||||
| `_textButton` | accent 色のテキストボタン (hover で下線) |
|
||||
| `_link` | テキストリンク強調 |
|
||||
| `_gaps` | 縦並び flex (`display: flex; flex-direction: column; gap: var(--MI-margin);`) |
|
||||
| `_gaps_m` / `_gaps_s` | 同じく縦並び flex で gap 固定 (`21px` / `10px`) |
|
||||
| `_margin` | 標準 margin (= `--MI-margin`) |
|
||||
| `_shadow` | 標準シャドウ (`box-shadow`) |
|
||||
| `_popup` | popup / dropdown 用 (背景 + 角丸 + `contain`。shadow は含まない) |
|
||||
| `_acrylic` | 半透明 + backdrop blur (アクリル風) |
|
||||
|
||||
使い方:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button class="_button _buttonPrimary" :class="$style.action" @click="onClick">
|
||||
{{ i18n.ts.save }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.action {
|
||||
padding: 8px 24px;
|
||||
/* 背景色や focus ring は _buttonPrimary が持つので書かない */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## `<style lang="scss" module>` の特殊記法
|
||||
|
||||
### `:global(...)` で module スコープから出る
|
||||
|
||||
`<style lang="scss" module>` 内に書いたクラス名はビルド時にハッシュ化されて他コンポーネントから参照できなくなる。これを意図的に外したい (子コンポーネント側の特定クラスや外部ライブラリのクラスにスタイルを当てたい) 場合のみ `:global(...)` を使う:
|
||||
|
||||
```scss
|
||||
.root {
|
||||
:global(.someThirdPartyClass) {
|
||||
color: var(--MI_THEME-fg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通常はほぼ使わない。
|
||||
|
||||
### `:deep(...)` で子コンポーネント内部を狙う
|
||||
|
||||
```scss
|
||||
.root :deep(.child-internal-class) {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
```
|
||||
|
||||
これも頻用しない (子コンポーネントを直接修正する方が望ましい)。
|
||||
|
||||
## 命名
|
||||
|
||||
- module class は **camelCase** が慣習 (`root` / `inputCore` / `headerText`)
|
||||
- BEM 風の `block__element--modifier` は使わない (CSS Modules でハッシュ化されるので名前衝突を心配する必要が無い)
|
||||
- 状態 modifier は `&.active` / `&.disabled` のようにネストする
|
||||
|
||||
## ありがちなレビュー指摘
|
||||
|
||||
- `#fff` / `#000` / `rgba(0, 0, 0, 0.5)` のハードコード → `var(--MI_THEME-fg)` / `var(--MI_THEME-bg)` / `color-mix(...)` 等に置き換える
|
||||
- `<style scoped>` で書いている (module ではない) → `<style lang="scss" module>` に直し、`:class="$style.foo"` で参照する
|
||||
- 自前で `border-radius: 8px; padding: 14px;` を書いている → `_panel` global class 使えば不要
|
||||
- 自前で button styling を書いている → `_button` global class を base に乗せる
|
||||
@@ -0,0 +1,191 @@
|
||||
# Storybook (`*.stories.impl.ts`) 規約
|
||||
|
||||
共有 `Mk*` コンポーネントには `Mk<Name>.stories.impl.ts` を **同階層** に併設するのが慣習。
|
||||
|
||||
## 配置と命名
|
||||
|
||||
- **ファイル名は `.stories.impl.ts` 固定** (`.stories.ts` は `packages/frontend/.storybook/generate.tsx` による生成物で手編集・コミット不可)
|
||||
- 同階層に置く (`components/MkButton.stories.impl.ts`、`components/global/MkAvatar.stories.impl.ts` 等)
|
||||
- 先頭に TS コメント形式の SPDX ヘッダーが必要
|
||||
|
||||
## 基本: 単一 story (Default のみ)
|
||||
|
||||
シンプルなコンポーネントならこれで十分。(以下の `MkColoredTag` は説明用の**架空のコンポーネント名**。実在しない。実物のパターンは `MkButton.stories.impl.ts` を参照。)
|
||||
|
||||
```ts
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkColoredTag from './MkColoredTag.vue';
|
||||
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: { MkColoredTag },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<MkColoredTag v-bind="args">タグ</MkColoredTag>',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
variant: 'info',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
```
|
||||
|
||||
ポイント:
|
||||
|
||||
- 上 2 つの `eslint-disable` は Storybook のお作法で必須 (render の関数が return type を明示しないため / `default export` ではないため)
|
||||
- `satisfies StoryObj<typeof MkColoredTag>` が無いと `args` の型補完が効かなくなる
|
||||
|
||||
## 複数 story (variant 別)
|
||||
|
||||
参考: [MkButton.stories.impl.ts](../../../../../packages/frontend/src/components/MkButton.stories.impl.ts)
|
||||
|
||||
variant / size / 状態などのバリエーションがあるなら、`Default` を base にして spread で派生させると簡潔。
|
||||
|
||||
```ts
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: { MkColoredTag },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<MkColoredTag v-bind="args">タグ</MkColoredTag>',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
variant: 'info',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
|
||||
export const Warn = {
|
||||
...Default,
|
||||
args: { ...Default.args, variant: 'warn' },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
|
||||
export const Danger = {
|
||||
...Default,
|
||||
args: { ...Default.args, variant: 'danger' },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
|
||||
export const Disabled = {
|
||||
...Default,
|
||||
args: { ...Default.args, disabled: true },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
```
|
||||
|
||||
## イベントを可視化する (`action()`)
|
||||
|
||||
クリック等の emit を Storybook の Actions panel で見たい場合、`storybook/actions` の `action()` を使う。
|
||||
|
||||
```ts
|
||||
import { action } from 'storybook/actions';
|
||||
// ...
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: { MkColoredTag },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return { ...this.args };
|
||||
},
|
||||
events() {
|
||||
return {
|
||||
click: action('click'),
|
||||
close: action('close'),
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkColoredTag v-bind="props" v-on="events">タグ</MkColoredTag>',
|
||||
};
|
||||
},
|
||||
args: {},
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
```
|
||||
|
||||
`MkButton.stories.impl.ts` がこのパターン。
|
||||
|
||||
## `argTypes` で controls を細かく制御
|
||||
|
||||
string union を radio に / number を range に変えるとレビューが楽になる。(標準の Storybook 機能。現状リポジトリ内の `.stories.impl.ts` では実際には使われていないので必須ではない。)
|
||||
|
||||
```ts
|
||||
export const Default = {
|
||||
render(args) { /* ... */ },
|
||||
args: { variant: 'info' },
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'inline-radio',
|
||||
options: ['info', 'warn', 'danger'],
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
```
|
||||
|
||||
## `parameters.layout` の使い分け
|
||||
|
||||
| 値 | 使い所 |
|
||||
|---|---|
|
||||
| `'centered'` | 単体表示 (ボタン、タグ、アイコン等の小さい部品) |
|
||||
| `'fullscreen'` | ページ単位、もしくはパネル全体を見せたい時 |
|
||||
| `'padded'` (デフォルト) | 周囲に余白が欲しい中サイズ部品 |
|
||||
|
||||
`layout` を変えるだけで Storybook 上の見え方が大きく変わる。レイアウト依存のコンポーネント (sticky header 等) なら `'fullscreen'` を選ぶ。
|
||||
|
||||
## slot の中身を可変にする
|
||||
|
||||
`args` に slot 用文字列フィールドを足し、template で `{{ args.label }}` のように展開する。
|
||||
|
||||
```ts
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: { MkColoredTag },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<MkColoredTag v-bind="args">{{ args.label }}</MkColoredTag>',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
label: 'タグ',
|
||||
variant: 'info',
|
||||
},
|
||||
parameters: { layout: 'centered' },
|
||||
} satisfies StoryObj<typeof MkColoredTag>;
|
||||
```
|
||||
|
||||
ただし `label` を component の props にしてしまうのは禁物 (slot で受け取る方針なら slot のままにする)。Storybook 上だけで使う表示用文字列として扱う。
|
||||
|
||||
## 確認方法
|
||||
|
||||
```bash
|
||||
pnpm --filter frontend storybook-dev # http://localhost:6006
|
||||
pnpm --filter frontend build-storybook # 静的ビルド
|
||||
```
|
||||
|
||||
新規コンポーネントの stories が Sidebar に出ない場合、多くは [generate.tsx](../../../../../packages/frontend/.storybook/generate.tsx) の生成対象 **allowlist** に入っていないため。`src/{components,pages,...}/**/*.vue` の全体 glob はコメントアウトされており、対象は `globSync('src/components/global/Mk*.vue')` / `globSync('src/components/Mk[B-E]*.vue')` などの**明示列挙**になっている。`.stories.impl.ts` を併設しただけでは自動では出ないことがあるので、対象外なら generate.tsx に 1 行追加する。加えて、ファイル名 (`.stories.impl.ts`) と SPDX ヘッダー以降に構文エラーが無いかも確認する。
|
||||
|
||||
Chromatic (`pnpm --filter frontend chromatic`) で視覚回帰チェックも行われる。
|
||||
@@ -0,0 +1,124 @@
|
||||
# i18n キーを追加・改修する
|
||||
|
||||
UI 文言の追加・変更を行う際の手順。**手動編集して良いのは `locales/ja-JP.yml` のみ**。
|
||||
|
||||
## 大前提 (絶対 NG)
|
||||
|
||||
- **`locales/<lang>.yml` (ja-JP.yml 以外) の編集は禁止**。これらは Crowdin の自動配信先で、手動編集すると次の同期で上書き喪失する ([locales/README.md](../../../../../locales/README.md), [crowdin.yml](../../../../../crowdin.yml))
|
||||
- 文字列リテラルを SFC に直書きしない (`<span>こんにちは</span>` 等)。必ず `i18n.ts.<key>` を経由する
|
||||
- 既存キーの破壊的リネームは Crowdin 翻訳資産を失わせる。**追加 → 移行 → 旧キー削除** の 3 段階に分割する。詳細手順と誤編集の復旧は [knowledge/i18n-usage.md §Crowdin 安全策](../knowledge/i18n-usage.md)
|
||||
|
||||
## ステップ 1: ja-JP.yml にキーを追加
|
||||
|
||||
[locales/ja-JP.yml](../../../../../locales/ja-JP.yml) を編集する。YAML の階層構造を維持し、関連するセクションに配置する:
|
||||
|
||||
```yaml
|
||||
# トップレベル単純キー
|
||||
save: "保存"
|
||||
|
||||
# ネストしたカテゴリ (アンダースコア接頭辞は内部カテゴリ)
|
||||
_settings:
|
||||
general: "全般"
|
||||
appearance: "外観"
|
||||
|
||||
# パラメータ付き (単純なプレースホルダ置換)
|
||||
# 受け付けるのは {name} 形式のみ。ICU MessageFormat (plural/select) は非対応
|
||||
greeting: "こんにちは、{name}さん"
|
||||
```
|
||||
|
||||
### 命名のお作法
|
||||
|
||||
- 単純キー: lowerCamelCase (例: `saveChanges`, `confirmDelete`)
|
||||
- カテゴリ: アンダースコア接頭辞 (例: `_settings`, `_abuseUserReport`)
|
||||
- 既存セクション内に追加する場合は **周辺の既存配置・意味グループに合わせる** (例えば `_settings` は機能ブロック順に並んでおりアルファベット順ではない)。新セクション全体を末尾に追加するのは可
|
||||
- **HTML タグ (`<b>` `<br>` `<strong>` 等) や `:` `'` `&` を含む値は必ずダブルクォートで囲む** (未クォートだと YAML パース失敗)
|
||||
|
||||
**詳細:** ICU 非対応の代替戦略・予約キー `_lang_`・Storybook での挙動は → [knowledge/i18n-usage.md §制約と補足](../knowledge/i18n-usage.md)
|
||||
|
||||
## ステップ 2: 型定義の自動再生成
|
||||
|
||||
`packages/i18n/build.ts` が `ja-JP.yml` を解析し、TypeScript インターフェースを [packages/i18n/src/autogen/locale.ts](../../../../../packages/i18n/src/autogen/locale.ts) に出力する。
|
||||
|
||||
### 自動 (推奨)
|
||||
|
||||
`pnpm dev` 実行中なら、`packages/i18n` の watch スクリプト (`nodemon ... tsx ./build.ts --watch`) が yml の変更を検知して自動再生成する。
|
||||
|
||||
### 手動
|
||||
|
||||
```bash
|
||||
pnpm --filter i18n generate
|
||||
```
|
||||
|
||||
実体は `tsx scripts/generateLocaleInterface.ts`。
|
||||
|
||||
### 失敗パターン
|
||||
|
||||
これを実行せずに frontend 側で `i18n.ts.<newKey>` を参照すると、`Locale` インターフェースに追加されていないため typecheck で `Property '<newKey>' does not exist on type 'Locale'` というエラーになる (`pnpm --filter frontend lint` で発覚)。型エラー・実行時警告 (`Unexpected locale key`, `Missing locale parameters`) と対処は → [knowledge/i18n-usage.md §トラブルシュート](../knowledge/i18n-usage.md)。
|
||||
|
||||
## ステップ 3: frontend での参照
|
||||
|
||||
```ts
|
||||
import { i18n } from '@/i18n.js';
|
||||
```
|
||||
|
||||
| 用途 | 書き方 |
|
||||
|---|---|
|
||||
| 単純文字列 | `i18n.ts.save` |
|
||||
| ネスト | `i18n.ts._settings.general` |
|
||||
| パラメータ付き | `i18n.tsx.greeting({ name: userName })` |
|
||||
| Vue テンプレート内 | `{{ i18n.ts.save }}` / `{{ i18n.tsx.greeting({ name }) }}` |
|
||||
|
||||
`i18n.ts` は型付き文字列、`i18n.tsx` は `{name}` プレースホルダを埋め込む関数 (パラメータ付きキーのみ存在。ICU MessageFormat ではなく単純な文字列置換)。
|
||||
|
||||
**詳細:** HTML タグ埋め込み・computed によるリアクティブ参照・動的キー切替・ブラケット記法 (`i18n.ts['2fa']`) などの実装パターンは → [knowledge/i18n-usage.md §実装パターン](../knowledge/i18n-usage.md)
|
||||
|
||||
## ステップ 4: 検証
|
||||
|
||||
```bash
|
||||
# i18n の型再生成 → typecheck + eslint (lint は generate を呼ばないので順番が必須)
|
||||
pnpm --filter i18n generate
|
||||
pnpm --filter i18n lint
|
||||
|
||||
# frontend で新キー参照箇所の型チェック
|
||||
pnpm --filter frontend lint
|
||||
|
||||
# 他言語 yml に diff が出ていないことを確認 (出力が空であれば OK)
|
||||
git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'
|
||||
```
|
||||
|
||||
**注意:** `grep -v 'ja-JP.yml'` を **diff 本文** に当てると ja-JP.yml 単体の変更でも `+追加行` が素通りして必ず非空になる。`--name-only` でファイル名だけに絞ってから完全一致で除外するのが正しい。
|
||||
|
||||
ユーザー影響のある UI 変更を伴う場合は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で CHANGELOG エントリの判定をする。
|
||||
|
||||
## 例: 「ノートを削除しますか?」確認ダイアログを追加する
|
||||
|
||||
1. `locales/ja-JP.yml`:
|
||||
```yaml
|
||||
_notes:
|
||||
deleteConfirm: "このノートを削除しますか?"
|
||||
```
|
||||
2. `pnpm --filter i18n generate` (または `pnpm dev` で watch 中)
|
||||
3. SFC:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
async function onDelete() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._notes.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
// 削除処理
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 参照ファイル
|
||||
|
||||
- [locales/README.md (★ 編集ポリシー根拠)](../../../../../locales/README.md)
|
||||
- [locales/ja-JP.yml](../../../../../locales/ja-JP.yml)
|
||||
- [packages/i18n/build.ts](../../../../../packages/i18n/build.ts)
|
||||
- [packages/i18n/src/autogen/locale.ts (生成物)](../../../../../packages/i18n/src/autogen/locale.ts)
|
||||
- [packages/frontend/src/i18n.ts](../../../../../packages/frontend/src/i18n.ts)
|
||||
@@ -0,0 +1,196 @@
|
||||
# 新規 / 既存 `Mk*` Vue コンポーネントを追加・改修する
|
||||
|
||||
`packages/frontend/src/components/` 配下に新規の共有 Vue 3 SFC を追加する、または既存コンポーネントを大きく改修する時の手順。同じ規約をレビュー側からチェックする agent が [.claude/agents/vue-component-reviewer.md](../../../../agents/vue-component-reviewer.md)。
|
||||
|
||||
## 大前提 (事故直結 / Critical)
|
||||
|
||||
1. **SPDX ヘッダー** — `.vue` は HTML コメント形式 `<!-- ... -->`、`.stories.impl.ts` は TS コメント形式 `/* ... */`。欠落すると CI (`spdx` ジョブ) が落ちる
|
||||
2. **`Mk` プレフィックス必須** — 共有コンポーネントは `MkButton.vue` / `global/MkAvatar.vue` のように `Mk` で始める。ページ固有 UI は `Mk` を付けず `pages/` 側に置く
|
||||
3. **`locales/ja-JP.yml` のみ編集可** — i18n キー追加時に他言語 (`en-US.yml` 等) を手で触ってはいけない。Crowdin の自動配信で上書きされて失われる。詳細は [tasks/adding-i18n-key.md](adding-i18n-key.md) を参照
|
||||
4. **文字列リテラルの直書き禁止** — テンプレート / JS どちらでも、ユーザーに見せる文言は必ず `i18n.ts.<key>` か `i18n.tsx.<key>(...)` 経由 → [knowledge/i18n-usage.md](../knowledge/i18n-usage.md)
|
||||
5. **ブラウザ標準 UI を直接呼ばない** — `alert()` / `confirm()` / `window.prompt()` は禁止、必ず `os.alert` / `os.confirm` / `os.popup` 経由 → [knowledge/os-api.md](../knowledge/os-api.md)
|
||||
|
||||
## ファイル配置
|
||||
|
||||
| 配置先 | 用途 | 命名 |
|
||||
|---|---|---|
|
||||
| `packages/frontend/src/components/Mk<Name>.vue` | 通常の共有 UI コンポーネント | `Mk<Name>.vue` |
|
||||
| `packages/frontend/src/components/global/Mk<Name>.vue` | `components/index.ts` で Vue グローバルコンポーネント登録 (`app.component`) され、import 無しで全テンプレートから使える基本部品 (`MkA` / `MkAvatar` / `MkAcct` 等) | `Mk<Name>.vue` (サブディレクトリ内でも `Mk` prefix 必須) |
|
||||
| `packages/frontend/src/components/grid/Mk<Name>.vue` | テーブル/グリッド系の部品セット | 同上 |
|
||||
| `packages/frontend/src/pages/<Name>.vue` | 単一ページ専用の UI (再利用しない) | `Mk` prefix **不要** |
|
||||
|
||||
迷ったら「他の `Mk*.vue` から import される可能性があるか?」で判定する。Yes なら `components/`、No なら `pages/`。
|
||||
|
||||
ストーリーが必要 (= ほぼ常に必要) なら、同階層に `Mk<Name>.stories.impl.ts` も作る → [knowledge/storybook.md](../knowledge/storybook.md)。
|
||||
|
||||
## SPDX ヘッダー
|
||||
|
||||
### `.vue` ファイル (HTML コメント)
|
||||
|
||||
```html
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
```
|
||||
|
||||
`/* ... */` (TS / JS 形式) は **使わない**。既存の `.vue` ファイルがすべて HTML コメント形式を採用しており、SFC 先頭として自然な形式に統一するため。
|
||||
|
||||
### `.stories.impl.ts` ファイル (TS コメント)
|
||||
|
||||
```ts
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
## 最小テンプレート
|
||||
|
||||
シンプルな表示コンポーネントの最小形を示す**合成例** (特定ファイルの写しではない)。実在する単純コンポーネントの例は [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) 等を参照:
|
||||
|
||||
```vue
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, $style[`variant_${variant}`]]">
|
||||
<slot></slot>
|
||||
<button
|
||||
v-if="closable"
|
||||
class="_button"
|
||||
:class="$style.close"
|
||||
:aria-label="i18n.ts.close"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<i class="ti ti-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
variant?: 'info' | 'warn' | 'danger';
|
||||
closable?: boolean;
|
||||
}>(), {
|
||||
variant: 'info',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'close'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
|
||||
.variant_info {
|
||||
background: var(--MI_THEME-infoBg);
|
||||
color: var(--MI_THEME-infoFg);
|
||||
}
|
||||
|
||||
.variant_warn {
|
||||
background: var(--MI_THEME-infoWarnBg);
|
||||
color: var(--MI_THEME-infoWarnFg);
|
||||
}
|
||||
|
||||
.variant_danger {
|
||||
background: var(--MI_THEME-error);
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
より複雑なケース (型ジェネリック / 2 ブロック script / `v-model` 連動 / 名前付き slot) は → [knowledge/component-conventions.md §テンプレート集](../knowledge/component-conventions.md)。
|
||||
|
||||
## `<script>` / `<style>` 規約サマリ
|
||||
|
||||
| 項目 | 規約 | 新規不可 |
|
||||
|---|---|---|
|
||||
| `<script>` 開始タグ | `<script lang="ts" setup>` または `<script setup lang="ts">` | `<script>` (lang 無し) / Options API |
|
||||
| Props 定義 | `defineProps<{ ... }>()` (type-only) | runtime object 形式 |
|
||||
| Emits 定義 | `defineEmits<{ (ev: 'click'): void }>()` (type-only) | runtime array 形式 |
|
||||
| `<style>` 開始タグ | `<style lang="scss" module>`、参照は `:class="$style.foo"` | `<style scoped>` (module なし) |
|
||||
| CSS 値 | `var(--MI_THEME-...)` / `var(--MI-...)` | `#fff` / `rgb(...)` のハードコード |
|
||||
| グローバル class | `_button` / `_panel` / `_selectable` 等を活用 | — |
|
||||
| アイコン | Tabler icons クラス `<i class="ti ti-info-circle">` | インライン SVG / 別アイコンセット |
|
||||
|
||||
詳細・テンプレート集は → [knowledge/component-conventions.md](../knowledge/component-conventions.md) / [knowledge/scss-modules.md](../knowledge/scss-modules.md)。
|
||||
|
||||
## i18n の使い分け
|
||||
|
||||
引数なし → `i18n.ts.<key>` / 引数あり → `i18n.tsx.<key>(...)`。詳細は → [knowledge/i18n-usage.md](../knowledge/i18n-usage.md)。
|
||||
|
||||
新キー追加が必要なら → [tasks/adding-i18n-key.md](adding-i18n-key.md)。
|
||||
|
||||
## `os.*` ヘルパー
|
||||
|
||||
`os.alert` / `os.confirm` / `os.popup` / `os.toast` / `os.popupMenu` 等。詳細は → [knowledge/os-api.md](../knowledge/os-api.md)。
|
||||
|
||||
## アクセシビリティ最低ライン
|
||||
|
||||
1. **クリック可能要素は `<button class="_button">` を第一選択**。やむを得ず `<div @click>` なら `role="button"` + `tabindex="0"` + `@keydown.enter` / `@keydown.space.prevent` の 4 点セット必須
|
||||
2. **フォーム要素 (`<input>` / `<select>` / `<textarea>`) は `<label>` 接続もしくは `aria-label`**
|
||||
3. **`:disabled` バインドと `aria-disabled` を一致**させる。ハンドラ側でも早期 return
|
||||
4. **キーボードのみで完結**できるか確認 (Tab で focus 移動できる / Enter で確定できる)
|
||||
5. ARIA 属性は最小限
|
||||
|
||||
詳細チェックリストと既存例 (`MkButton.vue` / `MkSwitch.vue`) は → [knowledge/component-conventions.md §a11y](../knowledge/component-conventions.md)。
|
||||
|
||||
## Storybook 併設
|
||||
|
||||
共有 `Mk*` コンポーネントには `Mk<Name>.stories.impl.ts` を **同階層** に併設する (サブディレクトリ含む)。詳細は → [knowledge/storybook.md](../knowledge/storybook.md)。
|
||||
|
||||
## 検証フロー
|
||||
|
||||
```bash
|
||||
# 型チェック (vue-tsc)
|
||||
pnpm --filter frontend typecheck
|
||||
|
||||
# ESLint (規約全体)
|
||||
pnpm --filter frontend eslint
|
||||
|
||||
# 単一ファイルに ESLint --fix
|
||||
pnpm exec eslint --fix packages/frontend/src/components/Mk<Name>.vue
|
||||
|
||||
# Storybook で目視確認
|
||||
pnpm --filter frontend storybook-dev # localhost:6006
|
||||
|
||||
# Vitest unit test (component spec があれば)
|
||||
pnpm --filter frontend test
|
||||
```
|
||||
|
||||
## CHANGELOG エントリ
|
||||
|
||||
ユーザーから見える変更 (新規コンポーネントが新しい UI として露出する、既存 UI の挙動を変える) なら、`CHANGELOG.md` に追記する。判定方法と書式は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で確認。
|
||||
|
||||
## 既存コンポーネントとの整合性
|
||||
|
||||
- 似た用途の既存 `Mk*` を 1-2 個読んで、props 命名 (`primary` / `danger` / `small` 等の形容詞、`onClose` ではなく `emit('close')` 等) を揃える
|
||||
- グローバル utility class (`_button` / `_panel` / `_selectable` / `_gaps_m`) を使えば独自スタイルを書かずに済む → [knowledge/scss-modules.md](../knowledge/scss-modules.md)
|
||||
- 大きな機能なら Storybook で各バリエーション (variant / size / disabled / loading) を網羅する
|
||||
|
||||
## 参照コード
|
||||
|
||||
- [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) — simple SFC 例
|
||||
- [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) — 汎用ボタン (a11y / `_button` global class)
|
||||
- [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) — generic + 2 ブロック script 例
|
||||
- [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue) — `defineModel` + 名前付き slot 例
|
||||
- [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue) — a11y 込みカスタム UI
|
||||
- [MkButton.stories.impl.ts](../../../../../packages/frontend/src/components/MkButton.stories.impl.ts) — 複数 story Storybook 雛形
|
||||
- [packages/frontend/src/os.ts](../../../../../packages/frontend/src/os.ts) — UI 操作 API 一覧
|
||||
- [packages/frontend/src/i18n.ts](../../../../../packages/frontend/src/i18n.ts) — `i18n.ts` / `i18n.tsx` 実装
|
||||
@@ -182,6 +182,9 @@ id: 'aidx'
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
|
||||
#threadPoolSize: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
@@ -194,6 +194,9 @@ id: 'aidx'
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
|
||||
#threadPoolSize: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
@@ -328,6 +328,9 @@ id: 'aidx'
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
|
||||
#threadPoolSize: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
#deliverJobConcurrency: 128
|
||||
#inboxJobConcurrency: 16
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: misskey
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- postgres-data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||
interval: 5s
|
||||
|
||||
@@ -169,6 +169,9 @@ id: 'aidx'
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
|
||||
#threadPoolSize: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
79
.github/copilot-instructions.md
vendored
79
.github/copilot-instructions.md
vendored
@@ -1,3 +1,80 @@
|
||||
# Copilot Instructions for Misskey
|
||||
|
||||
- en-US.yml を編集しないでください。
|
||||
このファイルは GitHub Copilot の repository-wide instructions として使われる。Copilot code review では `AGENTS.md` が読まれない環境があるため、レビューや軽微な実装判断に必要な規約はこのファイル単体で満たすこと。
|
||||
|
||||
リポジトリは Misskey の pnpm workspace モノレポ。主要な実装は `packages/backend` (NestJS / TypeORM) と `packages/frontend` (Vue 3) にある。より詳しいガイドはリポジトリルートの `AGENTS.md` を参照してよいが、このファイルの要件を省略してそちらへの参照だけで済ませないこと。
|
||||
|
||||
## 絶対にやってはいけない事
|
||||
|
||||
違反すると CI 失敗 / 本番事故 になる。
|
||||
|
||||
### コード・データ関連
|
||||
|
||||
- **SPDX ヘッダー必須**: AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリに新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.scss` / `.vue` / `.html` ファイルを追加する場合は冒頭に必ず付ける。詳細な対象判定は `.github/workflows/check-spdx-license-id.yml` を参照。
|
||||
|
||||
```text
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
新規 `.vue` / `.html` ファイルは HTML コメント形式で:
|
||||
|
||||
```text
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
```
|
||||
|
||||
`packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない (サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う)。
|
||||
|
||||
- **`locales/ja-JP.yml` 以外の locale YAML を編集しない**。他言語ファイル (`en-US.yml` など `ja-JP.yml` 以外すべて) は Crowdin の自動配信先で、手動編集すると次の同期で上書き喪失する。
|
||||
- **マージ済 migration を編集しない**。`packages/backend/migration/{timestamp}-*.js` のうち既に `develop` / `master` に入ったものは絶対に変更しない。スキーマ変更が必要なら新しい timestamp で新規ファイルを追加し、`up()` と `down()` の両方を実装する。
|
||||
- **secrets / 認証情報をリポジトリにコミットしない** (`.config/*.yml` の本番値、`.env` ファイル、API token、private key 等)。
|
||||
|
||||
### Git / リポジトリ操作
|
||||
|
||||
- `git push --force` / `--force-with-lease` を `main` / `develop` / `master` にしない
|
||||
- `git commit --no-verify` で hook をスキップしない
|
||||
- マージ済 / プッシュ済コミットを `git commit --amend` で書き換えない
|
||||
- 他人のブランチを `git reset --hard` / `git branch -D` で破壊しない
|
||||
- `git config` をユーザーに無断で書き換えない (特に `user.name` / `user.email` / `commit.gpgsign`)
|
||||
|
||||
### Issue / PR / 外部送信
|
||||
|
||||
- ユーザーの明示指示なしに PR を merge / close / force-push しない
|
||||
- ユーザーの明示指示なしに external service (GitHub comments / Slack / メール 等) へ送信しない
|
||||
|
||||
## 変更を出す前の最低チェック
|
||||
|
||||
1. `pnpm lint` が通る (typecheck + eslint, 全パッケージ)
|
||||
2. backend で `meta` / `paramDef` / `res` を変更した → `pnpm build-misskey-js-with-types` を実行し `packages/misskey-js/src/autogen/` の差分も commit に含めた
|
||||
3. entity / migration を変更した → `pnpm --filter backend check-migrations` が pending DDL 0 件で通る / 新規 migration は `up()` と `down()` 両方実装済
|
||||
4. 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルを追加した → SPDX ヘッダーを付けた
|
||||
5. ユーザー影響のある変更 → `CHANGELOG.md` の `## Unreleased` 配下の該当サブセクション (`### General` / `### Client` / `### Server`) に `- <Feat|Enhance|Fix>: <概要>` を 1 行追記
|
||||
6. `locales/` を編集した場合、`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空 (ja-JP.yml 以外に差分が無い) ことを確認
|
||||
|
||||
## Validation コマンド
|
||||
|
||||
- 全体ビルド: `pnpm build`
|
||||
- 全体 lint / typecheck: `pnpm lint`
|
||||
- Backend unit test: `pnpm --filter backend test`
|
||||
- Backend e2e test: `pnpm --filter backend test:e2e`
|
||||
- Backend federation test: `pnpm --filter backend test:fed`
|
||||
- Frontend test: `pnpm --filter frontend test`
|
||||
- Migration 差分検査: `pnpm --filter backend check-migrations`
|
||||
- `misskey-js` 再生成 (API 変更後必須): `pnpm build-misskey-js-with-types`
|
||||
|
||||
**注意:** backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要。未作成の場合は `ncp .github/misskey/test.yml .config/test.yml` (または `cp .github/misskey/test.yml .config/test.yml`) を実行してから走らせる。各テストスクリプトが内部で `cross-env NODE_ENV=test pnpm compile-config` を呼ぶため、コピー済みであれば追加の compile-config は不要。
|
||||
|
||||
変更範囲に応じて最も近いコマンドから優先して検証し、必要なら全体コマンドに広げること。
|
||||
|
||||
## Editing hints
|
||||
|
||||
- Backend の API / migration / TypeORM 変更は `packages/backend` を見る
|
||||
- Frontend の Vue コンポーネントやページ変更は `packages/frontend` を見る
|
||||
- `AGENTS.md` 内の相対リンクはリポジトリルート起点で解決する想定
|
||||
|
||||
**補足:** `AGENTS.md` はより詳細な正典 (Codex / Claude Code が読み込む)。Copilot code review ではこのファイルが主な入口になる。両方が読まれる環境では `AGENTS.md` を補助情報として使ってよい。
|
||||
|
||||
4
.github/workflows/api-misskey-js.yml
vendored
4
.github/workflows/api-misskey-js.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
2
.github/workflows/changelog-check.yml
vendored
2
.github/workflows/changelog-check.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
|
||||
12
.github/workflows/check-misskey-js-autogen.yml
vendored
12
.github/workflows/check-misskey-js-autogen.yml
vendored
@@ -25,11 +25,11 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
# packages/misskey-js/generator/built/autogen
|
||||
- name: Upload Generated
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: generated-misskey-js
|
||||
path: packages/misskey-js/generator/built/autogen
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: Upload From Merged
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: actual-misskey-js
|
||||
path: packages/misskey-js/src/autogen
|
||||
@@ -86,13 +86,13 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: download generated-misskey-js
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: generated-misskey-js
|
||||
path: misskey-js-generated
|
||||
|
||||
- name: download actual-misskey-js
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: actual-misskey-js
|
||||
path: misskey-js-actual
|
||||
|
||||
14
.github/workflows/docker-develop.yml
vendored
14
.github/workflows/docker-develop.yml
vendored
@@ -29,15 +29,15 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -66,15 +66,15 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -34,21 +34,21 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: ${{ env.TAGS }}
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push to Docker Hub
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -77,21 +77,21 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: ${{ env.TAGS }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
8
.github/workflows/get-api-diff.yml
vendored
8
.github/workflows/get-api-diff.yml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Copy API.json
|
||||
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact-${{ matrix.api-json-name }}
|
||||
path: ${{ matrix.api-json-name }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
echo "$PR_NUMBER" > ./pr_number
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact-pr-number
|
||||
path: pr_number
|
||||
|
||||
10
.github/workflows/get-backend-memory.yml
vendored
10
.github/workflows/get-backend-memory.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -45,9 +45,9 @@ jobs:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
# Start the server and measure memory usage
|
||||
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: memory-artifact-${{ matrix.memory-json-name }}
|
||||
path: ${{ matrix.memory-json-name }}
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
echo "$PR_NUMBER" > ./pr_number
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: memory-artifact-pr-number
|
||||
path: pr_number
|
||||
|
||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -41,8 +41,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -74,14 +74,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.3.0
|
||||
uses: actions/cache@v5.0.5
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
@@ -105,8 +105,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/locale.yml
vendored
4
.github/workflows/locale.yml
vendored
@@ -21,8 +21,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
|
||||
4
.github/workflows/on-release-created.yml
vendored
4
.github/workflows/on-release-created.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/report-api-diff.yml
vendored
4
.github/workflows/report-api-diff.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
# api-artifact
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Echo full diff
|
||||
run: cat ./api-full.json.diff
|
||||
- name: Upload full diff to Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact
|
||||
path: |
|
||||
|
||||
2
.github/workflows/report-backend-memory.yml
vendored
2
.github/workflows/report-backend-memory.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
2
.github/workflows/request-release-review.yml
vendored
2
.github/workflows/request-release-review.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Reply
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const body = `To dev team (@misskey-dev/dev):
|
||||
|
||||
8
.github/workflows/storybook.yml
vendored
8
.github/workflows/storybook.yml
vendored
@@ -37,9 +37,9 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
env:
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
- name: Notify that Chromatic detects changes
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
||||
})
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: storybook
|
||||
path: packages/frontend/storybook-static
|
||||
|
||||
24
.github/workflows/test-backend.yml
vendored
24
.github/workflows/test-backend.yml
vendored
@@ -45,11 +45,11 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.38.2
|
||||
image: getmeili/meilisearch:v1.42.1
|
||||
ports:
|
||||
- 57712:7700
|
||||
env:
|
||||
@@ -61,13 +61,13 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Setup and Restore ffmpeg/ffprobe Cache
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bin/ffmpeg
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -140,9 +140,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage:e2e
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -184,12 +184,12 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
6
.github/workflows/test-federation.yml
vendored
6
.github/workflows/test-federation.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Setup and Restore ffmpeg/ffprobe Cache
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bin/ffmpeg
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
18
.github/workflows/test-frontend.yml
vendored
18
.github/workflows/test-frontend.yml
vendored
@@ -32,9 +32,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter frontend test-and-coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/frontend/coverage/coverage-final.json
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -86,9 +86,9 @@ jobs:
|
||||
#- uses: browser-actions/setup-firefox@latest
|
||||
# if: ${{ matrix.browser == 'firefox' }}
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
- name: Cypress install
|
||||
run: pnpm exec cypress install
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
uses: cypress-io/github-action@v7.1.9
|
||||
timeout-minutes: 15
|
||||
with:
|
||||
install: false
|
||||
@@ -113,12 +113,12 @@ jobs:
|
||||
wait-on: 'http://localhost:61812'
|
||||
headed: true
|
||||
browser: ${{ matrix.browser }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-screenshots
|
||||
path: cypress/screenshots
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-videos
|
||||
|
||||
6
.github/workflows/test-misskey-js.yml
vendored
6
.github/workflows/test-misskey-js.yml
vendored
@@ -25,10 +25,10 @@ jobs:
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
CI: true
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/misskey-js/coverage/coverage-final.json
|
||||
|
||||
4
.github/workflows/test-production.yml
vendored
4
.github/workflows/test-production.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/validate-api-json.yml
vendored
4
.github/workflows/validate-api-json.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -81,3 +81,6 @@ vite.config.local-dev.ts.timestamp-*
|
||||
|
||||
# VSCode addon
|
||||
.favorites.json
|
||||
|
||||
# Affinity
|
||||
*.af~lock~
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "fluent-emojis"]
|
||||
path = fluent-emojis
|
||||
url = https://github.com/misskey-dev/emojis.git
|
||||
|
||||
104
AGENTS.md
Normal file
104
AGENTS.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Misskey – AI Agent Guide
|
||||
|
||||
このファイルは Misskey リポジトリで動く AI コーディングエージェント (Claude Code / OpenAI Codex / GitHub Copilot 等) が共通で参照する **絶対禁止事項と最低限のチェック** を集めた索引。次の 3 経路から参照・読み込みされる:
|
||||
|
||||
- **Claude Code**: ルート `CLAUDE.md` から `@AGENTS.md` で取り込まれる。詳細手順・規約は `.claude/skills/` (description で自動索引)
|
||||
- **OpenAI Codex**: ルート `AGENTS.md` を直接読み込む (skill エントリは `.agents/skills/`、実体は `.claude/skills/` を指す)
|
||||
- **GitHub Copilot**: `.github/copilot-instructions.md` (本ファイルの規約を Copilot code review 向けに再掲) 経由で参照する
|
||||
|
||||
人間 contributor 向けの一般規約 (Issue / PR の出し方、ActivityPub 拡張など) は [CONTRIBUTING.md](CONTRIBUTING.md) を参照。本ファイルは AI が **コードを書く・直す・出す** 際に踏み外してはいけない事項に絞る。
|
||||
|
||||
---
|
||||
|
||||
## 絶対にやってはいけない事
|
||||
|
||||
違反すると CI 失敗 / 本番事故 / 共有環境破壊 になる。順守すること。
|
||||
|
||||
### コード・データ関連
|
||||
|
||||
1. **SPDX ヘッダー欠落のまま AGPL 管轄ディレクトリへ新規ファイルを追加しない**
|
||||
- 対象: 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイル
|
||||
- CI の対象判定は [.github/workflows/check-spdx-license-id.yml](.github/workflows/check-spdx-license-id.yml) の `directories` 配列を参照 (`*.config.{ts,js,cjs,mjs}` と `*eslint*` は除外)
|
||||
- 欠落すると CI (`spdx` ジョブ) が失敗する
|
||||
- `packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない (サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う)
|
||||
|
||||
`.ts` / `.js` / `.cjs` / `.mjs` / `.scss`:
|
||||
|
||||
```text
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
```
|
||||
|
||||
`.vue` / `.html` (HTML コメント形式):
|
||||
|
||||
```text
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
```
|
||||
|
||||
2. **`locales/ja-JP.yml` 以外の locale YAML を手動編集しない**
|
||||
- 他言語ファイル (`en-US.yml` など `ja-JP.yml` 以外すべて) は Crowdin の自動配信先。手動編集すると次の同期で上書き喪失する
|
||||
- 根拠: [locales/README.md](locales/README.md) と [crowdin.yml](crowdin.yml) (`ja-JP.yml` → `locales/%locale%.yml` の同期設定)
|
||||
|
||||
3. **マージ済 migration ファイルを編集しない**
|
||||
- 対象: `packages/backend/migration/{unixMs}-{name}.js` のうち、既に `develop` / `master` にマージされたもの
|
||||
- 本番環境で履歴改変が起きると深刻なデータ不整合を引き起こす
|
||||
- スキーマ変更が必要な場合は **新しいタイムスタンプで新規ファイル** を作成する (`node -e "console.log(Date.now())"` でタイムスタンプ取得)
|
||||
- 新規 migration は `up()` と `down()` の両方を実装し、`pnpm --filter backend check-migrations` を通すこと (TypeORM schema builder で pending DDL を検出)
|
||||
|
||||
### Git / リポジトリ操作
|
||||
|
||||
4. **`git push --force` / `--force-with-lease` を `main` / `develop` / `master` にしない** (他人の作業を消す可能性)
|
||||
5. **`git commit --no-verify` で hook をスキップしない** (lint / format / SPDX チェックを潰す)
|
||||
6. **マージ済 / プッシュ済コミットを `git commit --amend` で書き換えない** (履歴の整合性が壊れる)
|
||||
7. **他人のブランチを `git reset --hard` / `git branch -D` で破壊しない**
|
||||
8. **`git config` をユーザーに無断で書き換えない** (特に `user.name` / `user.email` / `commit.gpgsign`)
|
||||
|
||||
### Issue / PR / 外部送信
|
||||
|
||||
9. **ユーザーの明示指示なしに PR を merge / close / force-push しない**
|
||||
10. **ユーザーの明示指示なしに external service (GitHub comments / Slack / メール 等) へ送信しない**
|
||||
11. **secrets / 認証情報をリポジトリにコミットしない** (`.config/*.yml` の本番値、`.env` ファイル、API token、private key 等)
|
||||
|
||||
### スキル呼び出し
|
||||
|
||||
上流スキルの実行・事前知識・memory の内容に関わらず免除されない。
|
||||
|
||||
12. **`working-on-backend` スキルを参照せずに `packages/backend/` 配下のファイルを編集・追加しない**
|
||||
13. **`working-on-frontend` スキルを参照せずに `packages/frontend/` 配下のファイルを編集・追加しない**
|
||||
14. **`shipping-misskey-change` スキルを参照せずに commit / PR 作成 / 作業をユーザーに返さない**
|
||||
|
||||
---
|
||||
|
||||
## 変更を出す前の最低チェック
|
||||
|
||||
各エージェントは [shipping-misskey-change スキル](.claude/skills/shipping-misskey-change/SKILL.md) を参照すること。スキルが利用できない環境でも、以下のチェックは必ず実施すること:
|
||||
|
||||
1. **lint**: `pnpm lint` が通る (typecheck + eslint, 全パッケージ)
|
||||
2. **backend API 変更時**: `pnpm build-misskey-js-with-types` を実行し `packages/misskey-js/src/autogen/` の差分も commit に含めた
|
||||
3. **entity / migration 変更時**: `pnpm --filter backend check-migrations` が pending DDL 0 件で通る / 新規 migration は `up()` と `down()` 両方実装済
|
||||
4. **新規ファイル**: SPDX ヘッダーを付けた (`.vue` / `.html` は HTML コメント形式、それ以外は TS コメント形式)
|
||||
5. **ユーザー影響のある変更**: `CHANGELOG.md` の `## Unreleased` 配下の該当サブセクション (`### General` / `### Client` / `### Server`) に `- <Feat|Enhance|Fix>: <概要>` を 1 行追記
|
||||
6. **locale safety**: `locales/` を編集した場合、`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空 (ja-JP.yml 以外に差分が無い) ことを確認
|
||||
|
||||
### Validation commands
|
||||
|
||||
各チェックで使う pnpm コマンド一覧。状況に応じて最も近いコマンドから検証する。
|
||||
|
||||
| 用途 | コマンド |
|
||||
| --- | --- |
|
||||
| 全体 lint (typecheck + eslint) | `pnpm lint` |
|
||||
| Backend unit test | `pnpm --filter backend test` |
|
||||
| Backend e2e test | `pnpm --filter backend test:e2e` |
|
||||
| Backend federation test | `pnpm --filter backend test:fed` |
|
||||
| Frontend unit test | `pnpm --filter frontend test` |
|
||||
| Migration 差分検査 (pending DDL) | `pnpm --filter backend check-migrations` |
|
||||
| `misskey-js` 再生成 (API 変更後必須) | `pnpm build-misskey-js-with-types` |
|
||||
| 全体ビルド | `pnpm build` |
|
||||
| 開発サーバー (backend + frontend watch) | `pnpm dev` |
|
||||
|
||||
**注意:** backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要 (`ncp .github/misskey/test.yml .config/test.yml` または `cp .github/misskey/test.yml .config/test.yml` で作成)。
|
||||
99
CHANGELOG.md
99
CHANGELOG.md
@@ -1,13 +1,102 @@
|
||||
## 2026.4.0
|
||||
## 2026.6.0
|
||||
|
||||
### General
|
||||
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
|
||||
- Feat: アンテナのタイムラインから個別のノートを削除できるように
|
||||
- Fix: コンパネからrootユーザーのパスワードをリセットしようとした際にエラーが通知されない問題を修正
|
||||
- Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035)
|
||||
|
||||
### Client
|
||||
- Enhance: ユーザーページのファイルタブでスクロール位置が保持されるように
|
||||
- Enhance: ドライブページでスクロール位置が保持されるように
|
||||
- Enhance: 絵文字のメニューから直接絵文字パレットに絵文字を追加できるように
|
||||
- Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正
|
||||
- Fix: 一部の実績が正しく表示されない問題を修正
|
||||
- Fix: アクセストークン発行時のダイアログのタイトルが「確認コード」となっているのを修正
|
||||
- Fix: 一部のUI要素の色が正しく表示されない問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1243)
|
||||
- Fix: 「D」キーでダークモードを切り替える際にsyncDeviceDarkModeのチェックがバイパスされる問題を修正
|
||||
- Fix: パスキー登録完了時の認証ダイアログの入力値が使われていない問題を修正
|
||||
- Fix: メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善
|
||||
- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498)
|
||||
- Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善
|
||||
- Fix: センシティブメディア自動検出周りの依存関係・ファイルの解決に失敗する問題を修正
|
||||
- Fix: フォロワー限定投稿を指名投稿で引用した際に、引用した投稿の公開範囲が意図せず変更される問題を修正
|
||||
|
||||
## 2026.5.4
|
||||
|
||||
### General
|
||||
- セキュリティに関する修正
|
||||
|
||||
### Client
|
||||
- Fix: ビルドに失敗することがある問題を修正
|
||||
|
||||
|
||||
## 2026.5.3
|
||||
|
||||
### General
|
||||
- Fix: Dockerで起動に失敗する問題を修正
|
||||
|
||||
|
||||
## 2026.5.2
|
||||
|
||||
### Note
|
||||
- config に `threadPoolSize` オプションが追加されました。
|
||||
- デフォルトは `1` で、ワーカーごとに指定した数のスレッドが作成されます。
|
||||
- スレッドプールは CPU バウンドな処理をオフロードするために使用されるため、みだりに大きな値を指定しないでください。
|
||||
|
||||
### General
|
||||
- Enhance: Unicode 17.0 に収録されている絵文字の処理・表示に対応
|
||||
- Fluent Emojiや端末ネイティブの絵文字を利用している場合は、最新の絵文字に対応しておらず正しく表示できない可能性があります。絵文字が表示できない場合は、表示に使用する絵文字をTwemojiに切り替えてご利用ください。
|
||||
- Enhance: 投稿通知設定したユーザーをリストで見ることができるように
|
||||
- 依存関係の更新
|
||||
|
||||
### Client
|
||||
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
|
||||
- Enhance: Fluent Emojiを更新し、Unicode 15+相当の絵文字の表示に対応
|
||||
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
|
||||
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
|
||||
- Fix: テーマのインストールエラーの表示を改善
|
||||
- Fix: リスト編集画面におけるユーザー追加時のユーザー選択ダイアログにおいて、自身のアカウントが検索結果の一覧に表示されない問題を修正
|
||||
- Fix: デッキのカラムから開いたアンテナ・リストの編集ウィンドウを、"ポップアウト"、"新しいタブで表示"、"リンクをコピー"した場合に誤ったリンクが与えられる問題を修正
|
||||
- Fix: チャンネルの作成ロールポリシーにて、ヘッダーにロールポリシーの値が表示されない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: RSA 署名処理のオフロード
|
||||
|
||||
|
||||
## 2026.5.1
|
||||
|
||||
### General
|
||||
- Enhance: チャンネルの作成の可否をロールポリシーで制御できるように
|
||||
- Fix: `.devcontainer/compose.yml`のvolumeのマウントパスを修正
|
||||
|
||||
### Client
|
||||
- Enhance: ノートの詳細表示での公開範囲の表示を改善
|
||||
(Cherry-picked from https://github.com/kokonect-link/cherrypick/commit/ecc75563f4e428b66adccc379bf317b5b21ed8e6)
|
||||
- Fix: ロール設定画面でロールをアサイン/アサイン解除した際、リロードしなくても画面に反映されるよう修正
|
||||
|
||||
### Server
|
||||
- Fix: ID生成アルゴリズムにULIDを使用している場合に通知が約10秒遅延する問題を修正
|
||||
- Fix: 公開範囲がフォロワーの投稿が通知されない問題を修正
|
||||
- Fix: URLプレビューが動作しない問題を修正
|
||||
|
||||
|
||||
## 2026.5.0
|
||||
|
||||
### General
|
||||
- Enhance: アバターデコレーションにカテゴリを設定できるように
|
||||
|
||||
### Client
|
||||
- Enhance: チャンネル指定リノートでリノート先のチャンネルに移動できるように
|
||||
- Enhance: ベータ版でのアップデート時のダイアログの更新情報リンクをGitHubのReleasesページに遷移するようにし、正しく閲覧できるように
|
||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
||||
- Fix: ドライブへの画像アップロード時にファイル名の変更が無視される不具合を修正
|
||||
- Fix: 連合が無効化されたサーバーで一部の設定項目が空欄で表示される問題を修正
|
||||
- Fix: オーディオ、動画の再生速度メニューが開けない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: メモリ使用量を削減
|
||||
@@ -23,6 +112,14 @@
|
||||
- Fix: ID生成アルゴリズムにULIDを使用している場合にMisskeyが正しく動作しない問題を修正
|
||||
- Fix: リレー経由で届いたノートがリノートとして表示される問題を修正
|
||||
- Fix: robots.txtの内容を調整
|
||||
- Fix: 特定のユーザーに管理者権限を持つロールが複数ついている際に、取得できるユーザーIDが重複する問題を修正
|
||||
(Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/17ed4108cec4b6bd2fd989db5a9091db91fa37a7)
|
||||
- Fix: ブロックしたサーバーからのInboxジョブが蓄積し続ける問題を修正
|
||||
(Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/3f0f4bfe923f2b3a7837017b54841598f421c6ef)
|
||||
- Fix: support activity with `actor` as an id string or embedded object in inbox processor and ActivityPub inbox service
|
||||
- Fix: コンフィグファイルに `meilisearch` の設定がある状態でほかの検索プロバイダを利用すると、UI上からリモートのノートの検索ができない問題を修正
|
||||
- Fix: ノートに関する通知で公開範囲が考慮されていない問題を修正
|
||||
(Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/cbce96c520a138b8bcd16890ff6f2952830fa166 originally presented in https://github.com/yojo-art/cherrypick/pull/743)
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
||||
7
CLAUDE.md
Normal file
7
CLAUDE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Misskey – Claude Code Guide
|
||||
|
||||
ルール本体は [AGENTS.md](AGENTS.md) (Codex / Copilot と共有する単一ソース)。本ファイルは Claude Code 用の薄いラッパーで、`@AGENTS.md` 構文で本体規約をセッション開始時にコンテキストへ展開する。
|
||||
|
||||
Claude Code 固有の補助 (skills / agents / slash commands / docs) は `.claude/` 配下にコミット済。個人ローカル設定は `.claude/settings.local.json` に、MCP 認証情報は `.claude/.credentials.json` に置く (いずれも `.gitignore` 済)。
|
||||
|
||||
@AGENTS.md
|
||||
@@ -189,6 +189,14 @@ pnpm migrate
|
||||
|
||||
After finishing the migration, you can proceed.
|
||||
|
||||
#### Cloudflare tunnel
|
||||
Cloudflare tunnelを使うとローカルのMisskeyサーバーをインターネットに公開できます。
|
||||
HTTPSでしか動作しない機能を検証したい時や、スマホなど別のデバイスからローカルのMisskeyサーバーを検証したい時に便利です。
|
||||
|
||||
##### Cloudflare warpと併用する際のtips
|
||||
|
||||
> cloudflared (Cloudflare Tunnel) は region1.v2.argotunnel.com / region2.v2.argotunnel.com に QUIC/HTTP2 でアウトバウンド接続するのですが、WARP を有効化するとこのトラフィックが WARP 経由になってループ/切断します。これら 2 ホストを WARP のトンネル除外(split tunnel)に追加することで、cloudflared だけは WARP をバイパスして直接 Cloudflare エッジへ接続できるようになります。
|
||||
|
||||
### Start developing
|
||||
During development, it is useful to use the
|
||||
```
|
||||
@@ -575,11 +583,12 @@ enumの列挙の内容の削除は、その値をもつレコードを全て削
|
||||
### Migration作成方法
|
||||
packages/backendで:
|
||||
```sh
|
||||
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
pnpm dlx typeorm migration:generate -d ormconfig.js -o --esm <migration name>
|
||||
```
|
||||
|
||||
- 生成後、ファイルをmigration下に移してください
|
||||
- 作成されたスクリプトは不必要な変更を含むため除去してください
|
||||
- `-o` (`--outputJs`) で JS 形式、`--esm` で ESM 形式に生成する。Misskey の既存 migration はすべて ESM JS なので両方のオプションが必要
|
||||
|
||||
### コネクションには`markRaw`せよ
|
||||
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
||||
|
||||
7
COPYING
7
COPYING
@@ -3,13 +3,6 @@ Copyright © 2014-2026 syuilo and contributors
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
|
||||
Misskey includes several third-party Open-Source softwares.
|
||||
|
||||
Emoji keywords for Unicode 11 and below by Mu-An Chiou
|
||||
License: MIT
|
||||
https://github.com/muan/emojilib/blob/master/LICENSE
|
||||
|
||||
RsaSignature2017 implementation by Transmute Industries Inc
|
||||
License: MIT
|
||||
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.21
|
||||
# syntax = docker/dockerfile:1.23
|
||||
|
||||
ARG NODE_VERSION=22.22.0-bookworm
|
||||
ARG NODE_VERSION=22.22.2-bookworm
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
@@ -74,6 +74,8 @@ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
|
||||
ARG UID="991"
|
||||
ARG GID="991"
|
||||
|
||||
ENV PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg tini curl libjemalloc-dev libjemalloc2 \
|
||||
@@ -103,7 +105,6 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-rev
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
|
||||
|
||||
@@ -190,6 +190,9 @@ id: "aidx"
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
|
||||
#threadPoolSize: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
Submodule fluent-emojis deleted from cae981eb4c
@@ -1408,6 +1408,7 @@ frame: "Frame"
|
||||
presets: "Preset"
|
||||
zeroPadding: "Zero padding"
|
||||
nothingToConfigure: "No configurable options available"
|
||||
viewRenotedChannel: "Show renoted channel"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "File caption"
|
||||
@@ -3401,6 +3402,8 @@ _imageEffector:
|
||||
threshold: "Threshold"
|
||||
centerX: "Center X"
|
||||
centerY: "Center Y"
|
||||
density: "Density"
|
||||
zoomLinesOutlineThickness: "Outline shadow thickness"
|
||||
zoomLinesMaskSize: "Center diameter"
|
||||
circle: "Circular"
|
||||
drafts: "Drafts"
|
||||
|
||||
@@ -1408,6 +1408,7 @@ frame: "Marco"
|
||||
presets: "Predefinido"
|
||||
zeroPadding: "Relleno cero"
|
||||
nothingToConfigure: "No hay nada que configurar"
|
||||
viewRenotedChannel: "Ver el canal al que te has suscrito"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Título del archivo"
|
||||
|
||||
@@ -753,6 +753,8 @@ optional: "任意"
|
||||
createNewClip: "新しいクリップを作成"
|
||||
unclip: "クリップ解除"
|
||||
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
|
||||
removeFromAntenna: "このアンテナから削除"
|
||||
removeNoteFromAntennaConfirm: "「{name}」からこのノートを削除しますか?"
|
||||
public: "パブリック"
|
||||
private: "非公開"
|
||||
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||
@@ -1217,6 +1219,7 @@ keepScreenOn: "デバイスの画面を常にオンにする"
|
||||
verifiedLink: "このリンク先の所有者であることが確認されました"
|
||||
notifyNotes: "投稿を通知"
|
||||
unnotifyNotes: "投稿の通知を解除"
|
||||
notifyUsers: "投稿通知を設定したユーザー"
|
||||
authentication: "認証"
|
||||
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
||||
dateAndTime: "日時"
|
||||
@@ -1409,6 +1412,14 @@ presets: "プリセット"
|
||||
zeroPadding: "ゼロ埋め"
|
||||
nothingToConfigure: "設定項目はありません"
|
||||
viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
accessToken: "アクセストークン"
|
||||
chooseEmojiPalette: "絵文字パレットを選択"
|
||||
addToEmojiPalette: "絵文字パレットに追加"
|
||||
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
|
||||
append: "末尾に追加"
|
||||
prepend: "先頭に追加"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -2106,6 +2117,7 @@ _role:
|
||||
driveCapacity: "ドライブ容量"
|
||||
maxFileSize: "アップロード可能な最大ファイルサイズ"
|
||||
maxFileSize_caption: "リバースプロキシやCDNなど、前段で別の設定値が存在する場合があります。"
|
||||
maxFileSize_caption2: "サーバー全体の最大ファイルサイズ設定は {max} です。これより大きいファイルをアップロードできるようにするには、Misskeyの設定ファイルからこの設定を緩和してください。"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
@@ -2122,6 +2134,7 @@ _role:
|
||||
canSearchNotes: "ノート検索の利用"
|
||||
canSearchUsers: "ユーザー検索の利用"
|
||||
canUseTranslator: "翻訳機能の利用"
|
||||
canCreateChannel: "チャンネルの作成"
|
||||
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
||||
canImportAntennas: "アンテナのインポートを許可"
|
||||
canImportBlocking: "ブロックのインポートを許可"
|
||||
@@ -3351,6 +3364,8 @@ _search:
|
||||
pleaseEnterServerHost: "サーバーのホストを入力してください"
|
||||
pleaseSelectUser: "ユーザーを選択してください"
|
||||
serverHostPlaceholder: "例: misskey.example.com"
|
||||
postFrom: "投稿日時from"
|
||||
postTo: "投稿日時to"
|
||||
|
||||
_serverSetupWizard:
|
||||
installCompleted: "Misskeyのインストールが完了しました!"
|
||||
|
||||
@@ -2098,6 +2098,7 @@ _role:
|
||||
canSearchNotes: "노트 검색 이용 가능 여부"
|
||||
canSearchUsers: "유저 검색 이용"
|
||||
canUseTranslator: "번역 기능의 사용"
|
||||
canCreateChannel: "패널 생성"
|
||||
avatarDecorationLimit: "아바타 장식의 최대 붙임 개수"
|
||||
canImportAntennas: "안테나 가져오기 허용"
|
||||
canImportBlocking: "차단 목록 가져오기 허용"
|
||||
|
||||
@@ -1332,7 +1332,9 @@ overrideByAccount: "Переопределить этим аккаунтом"
|
||||
untitled: "Без названия"
|
||||
noName: "Имя не указано"
|
||||
skip: "Пропустить"
|
||||
restore: "Восстановить"
|
||||
syncBetweenDevices: "Синхронизировать между устройствами"
|
||||
paste: "вставить"
|
||||
postForm: "Форма отправки"
|
||||
textCount: "Количество символов"
|
||||
information: "Описание"
|
||||
@@ -2395,6 +2397,8 @@ _imageEffector:
|
||||
opacity: "Непрозрачность"
|
||||
lightness: "Осветление"
|
||||
drafts: "Черновик"
|
||||
_drafts:
|
||||
restore: "Восстановить"
|
||||
_qr:
|
||||
showTabTitle: "Отображение"
|
||||
raw: "Текст"
|
||||
|
||||
@@ -1335,7 +1335,7 @@ markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อ
|
||||
unmarkAsSensitiveConfirm: "ต้องการยกเลิกการระบุว่าสื่อนี้มีเนื้อหาละเอียดอ่อนหรือไม่?"
|
||||
preferences: "การตั้งค่าสภาพแวดล้อม"
|
||||
accessibility: "การช่วยการเข้าถึง"
|
||||
preferencesProfile: "โปรไฟล์การกำหนดค่า"
|
||||
preferencesProfile: "โปรไฟล์ของการตั้งค่า"
|
||||
copyPreferenceId: "คัดลือก ID การตั้งค่า"
|
||||
resetToDefaultValue: "คืนค่าเป็นค่าเริ่มต้น"
|
||||
overrideByAccount: "เขียนทับด้วยบัญชี"
|
||||
@@ -1345,10 +1345,10 @@ skip: "ข้าม"
|
||||
restore: "กู้คืน"
|
||||
syncBetweenDevices: "ซิงค์ระหว่างอุปกรณ์"
|
||||
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictText: "การตั้งค่าที่เปิดใช้งานการซิงค์จะบันทึกค่าลงในเซิร์ฟเวอร์ อย่างไรก็ดี พบว่ามีค่าการตั้งค่านี้ที่เคยบันทึกไว้ในเซิร์ฟเวอร์แล้ว ต้องการดำเนินการอย่างไร?"
|
||||
preferenceSyncConflictText: "รายการตั้งค่าที่เปิดการซิงก์จะถูกบันทึกลงเซิร์ฟเวอร์ แต่รายการตั้งค่านี้ได้ถูกบันทึกลงเซิร์ฟเวอร์ไว้อยู่แล้ว ต้องการดำเนินการอย่างไร?"
|
||||
preferenceSyncConflictChoiceMerge: "รวมเข้าด้วยกัน"
|
||||
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าอุปกรณ์"
|
||||
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าของเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าของอุปกรณ์"
|
||||
preferenceSyncConflictChoiceCancel: "ยกเลิกการเปิดใช้งานการซิงค์"
|
||||
paste: "วาง"
|
||||
emojiPalette: "จานสีเอโมจิ"
|
||||
@@ -1408,6 +1408,7 @@ frame: "เฟรม"
|
||||
presets: "พรีเซ็ต"
|
||||
zeroPadding: "ห่างเป็น 0"
|
||||
nothingToConfigure: "ไม่มีอะไรให้ต้ังค่า"
|
||||
viewRenotedChannel: "แสดงช่องที่ถูกรีโน้ต"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "แคปชั่นของไฟล์"
|
||||
@@ -2097,6 +2098,7 @@ _role:
|
||||
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||
canSearchUsers: "ค้นหาผู้ใช้"
|
||||
canUseTranslator: "การใช้งานแปล"
|
||||
canCreateChannel: "สร้างช่องใหม่"
|
||||
avatarDecorationLimit: "จำนวนของตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
|
||||
canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ"
|
||||
canImportBlocking: "อนุญาตให้นำเข้าการบล็อก"
|
||||
@@ -2136,7 +2138,7 @@ _sensitiveMediaDetection:
|
||||
sensitivityDescription: "เมื่อความไวต่ำ Misdetection (ผลบวกลวง) จะลดลง, เมื่อความไวสูง Missed detection (ผลลบลวง) จะลดลง"
|
||||
setSensitiveFlagAutomatically: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน"
|
||||
setSensitiveFlagAutomaticallyDescription: "ผลลัพธ์ของการตรวจจับภายในนั้นจะยังคงอยู่ ถึงแม้ว่าจะปิดตัวเลือกนี้"
|
||||
analyzeVideos: "เปิดใช้งานวิเคราะห์ของวิดีโอ"
|
||||
analyzeVideos: "เปิดใช้งานวิเคราะห์วิดีโอ"
|
||||
analyzeVideosDescription: "การวิเคราะห์วิดีโอนอกเหนือจากรูปภาพนั้น การทำสิ่งนี้จะทำให้เพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||
_emailUnavailable:
|
||||
used: "ที่อยู่อีเมลนี้ได้ถูกใช้ไปแล้ว"
|
||||
@@ -2586,7 +2588,7 @@ _widgetOptions:
|
||||
period: "ระยะเวลา"
|
||||
_cw:
|
||||
hide: "ซ่อน"
|
||||
show: "โหลดเพิ่มเติม"
|
||||
show: "ดูเพิ่มเติม"
|
||||
chars: "{count} ตัวอักษร"
|
||||
files: "{count} ไฟล์"
|
||||
_poll:
|
||||
@@ -3029,7 +3031,7 @@ _externalResourceInstaller:
|
||||
description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
|
||||
_dataSaver:
|
||||
_media:
|
||||
title: "โหลดสื่อ"
|
||||
title: "ปิดใช้งานการโหลดสื่ออัตโนมัติ"
|
||||
description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
|
||||
_avatar:
|
||||
title: "ปิดใช้งานภาพเคลื่อนไหวของไอคอนประจำตัว"
|
||||
@@ -3111,7 +3113,7 @@ _urlPreviewSetting:
|
||||
summaryProxyDescription: "สร้างการแสดงตัวอย่างด้วย summary Proxy แทนที่จะใช้เนื้อหา Misskey"
|
||||
summaryProxyDescription2: "พารามิเตอร์ต่อไปนี้จะถูกใช้เป็นสตริงการสืบค้นเพื่อเชื่อมต่อกับพร็อกซี หากฝั่งพร็อกซีไม่รองรับการตั้งค่าเหล่านี้จะถูกละเว้น"
|
||||
_mediaControls:
|
||||
pip: "รูปภาพในรูปภาม"
|
||||
pip: "ภาพซ้อนภาพ (PiP)"
|
||||
playbackRate: "ความเร็วในการเล่น"
|
||||
loop: "เล่นวนซ้ำ"
|
||||
_contextMenu:
|
||||
|
||||
@@ -1408,6 +1408,7 @@ frame: "Çerçeve"
|
||||
presets: "Ön ayar"
|
||||
zeroPadding: "Sıfır doldurma"
|
||||
nothingToConfigure: "Ayarlar seçeneği bulunmamaktadır."
|
||||
viewRenotedChannel: "Show renoted channel"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Dosya başlığı"
|
||||
|
||||
@@ -63,7 +63,7 @@ copyUserId: "复制用户 ID"
|
||||
copyNoteId: "复制帖子 ID"
|
||||
copyFileId: "复制文件ID"
|
||||
copyFolderId: "复制文件夹ID"
|
||||
copyProfileUrl: "复制个人资料URL"
|
||||
copyProfileUrl: "复制个人资料链接"
|
||||
searchUser: "搜索用户"
|
||||
searchThisUsersNotes: "搜索用户帖子"
|
||||
reply: "回复"
|
||||
@@ -101,12 +101,12 @@ somethingHappened: "出错了"
|
||||
retry: "重试"
|
||||
pageLoadError: "页面加载失败。"
|
||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||
serverIsDead: "没有服务器响应。 请稍后再试。"
|
||||
serverIsDead: "服务器未响应。 请稍后再试。"
|
||||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
makeFollowManuallyApprove: "关注请求需要批准"
|
||||
defaultNoteVisibility: "默认可见性"
|
||||
defaultNoteVisibility: "默认可见范围"
|
||||
follow: "关注"
|
||||
followRequest: "申请关注"
|
||||
followRequests: "关注请求"
|
||||
@@ -137,10 +137,10 @@ pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显
|
||||
pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号"
|
||||
emojiPickerDisplay: "选择器显示设置"
|
||||
overwriteFromPinnedEmojisForReaction: "使用「置顶(回应)」设置覆盖"
|
||||
overwriteFromPinnedEmojis: "从全局设置覆盖"
|
||||
overwriteFromPinnedEmojis: "使用全局设置覆盖"
|
||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||
rememberNoteVisibility: "保存上次设置的可见性"
|
||||
attachCancel: "取消添加附件"
|
||||
attachCancel: "移除附件"
|
||||
deleteFile: "删除文件"
|
||||
markAsSensitive: "标记为敏感内容"
|
||||
unmarkAsSensitive: "取消标记为敏感内容"
|
||||
@@ -149,12 +149,12 @@ mute: "屏蔽"
|
||||
unmute: "取消隐藏"
|
||||
renoteMute: "隐藏转帖"
|
||||
renoteUnmute: "取消隐藏转帖"
|
||||
block: "屏蔽"
|
||||
unblock: "取消屏蔽"
|
||||
block: "禁止对方与我互动"
|
||||
unblock: "允许对方与我互动"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
blockConfirm: "确定要屏蔽吗?"
|
||||
unblockConfirm: "确定要取消屏蔽吗?"
|
||||
blockConfirm: "确定要禁止对方与我互动吗?"
|
||||
unblockConfirm: "确定要允许对方与我互动吗?"
|
||||
suspendConfirm: "要冻结吗?"
|
||||
unsuspendConfirm: "要解除冻结吗?"
|
||||
selectList: "选择列表"
|
||||
@@ -184,7 +184,7 @@ flagAsCat: "喵!!!!!!!!!!!!"
|
||||
flagAsCatDescription: "喵喵喵??"
|
||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||
autoAcceptFollowed: "自动允许回关请求"
|
||||
autoAcceptFollowed: "自动允许我关注的人的关注请求"
|
||||
addAccount: "添加账户"
|
||||
reloadAccountsList: "更新账户列表"
|
||||
loginFailed: "登录失败"
|
||||
@@ -207,7 +207,7 @@ selectSelf: "选择自己"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
instances: "服务器"
|
||||
registeredAt: "初次观测"
|
||||
latestRequestReceivedAt: "上次收到的请求"
|
||||
@@ -244,11 +244,11 @@ silencedInstances: "被静音的服务器"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。已被屏蔽的实例不受影响。"
|
||||
mediaSilencedInstances: "已隐藏媒体文件的服务器"
|
||||
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。已被屏蔽的实例不受影响。"
|
||||
federationAllowedHosts: "允许联合的服务器"
|
||||
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
|
||||
muteAndBlock: "隐藏/屏蔽"
|
||||
mutedUsers: "已隐藏的用户"
|
||||
blockedUsers: "已屏蔽的用户"
|
||||
federationAllowedHosts: "允许联邦交互的服务器"
|
||||
federationAllowedHostsDescription: "设定允许联邦通信的服务器,以换行分隔。"
|
||||
muteAndBlock: "屏蔽用户/禁止用户与我互动"
|
||||
mutedUsers: "已屏蔽的用户"
|
||||
blockedUsers: "禁止与我互动的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "确定要删除该帖子吗?"
|
||||
@@ -261,7 +261,7 @@ default: "默认"
|
||||
defaultValueIs: "默认值: {value}"
|
||||
noCustomEmojis: "没有自定义表情符号"
|
||||
noJobs: "没有任务"
|
||||
federating: "联合中"
|
||||
federating: "联邦通信中"
|
||||
blocked: "已屏蔽"
|
||||
suspended: "停止投递"
|
||||
all: "全部"
|
||||
@@ -277,7 +277,7 @@ retypedNotMatch: "两次输入不一致!"
|
||||
currentPassword: "现在的密码"
|
||||
newPassword: "新密码"
|
||||
newPasswordRetype: "重新输入密码:"
|
||||
attachFile: "插入附件"
|
||||
attachFile: "添加附件"
|
||||
more: "更多!"
|
||||
featured: "热门"
|
||||
usernameOrUserId: "用户名或用户 ID"
|
||||
@@ -342,7 +342,7 @@ selectFolders: "选择多个文件夹"
|
||||
fileNotSelected: "未选择文件"
|
||||
renameFile: "重命名文件"
|
||||
folderName: "文件夹名称"
|
||||
createFolder: "创建文件夹"
|
||||
createFolder: "新建文件夹"
|
||||
renameFolder: "重命名文件夹"
|
||||
deleteFolder: "删除文件夹"
|
||||
folder: "文件夹"
|
||||
@@ -353,7 +353,7 @@ emptyFolder: "此文件夹中无文件"
|
||||
dropHereToUpload: "将文件拖动到这里来上传"
|
||||
unableToDelete: "无法删除"
|
||||
inputNewFileName: "请输入新文件名"
|
||||
inputNewDescription: "请输入新标题"
|
||||
inputNewDescription: "请输入新的描述文本"
|
||||
inputNewFolderName: "请输入新文件夹名"
|
||||
circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。"
|
||||
hasChildFilesOrFolders: "此文件夹中有文件,无法删除。"
|
||||
@@ -396,10 +396,10 @@ driveCapacityPerLocalAccount: "每个用户的网盘容量"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
inMb: "以兆字节(MegaByte)为单位"
|
||||
bannerUrl: "横幅 URL"
|
||||
backgroundImageUrl: "背景图 URL"
|
||||
backgroundImageUrl: "背景图片的链接"
|
||||
basicInfo: "基本信息"
|
||||
pinnedUsers: "置顶用户"
|
||||
pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。"
|
||||
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
|
||||
pinnedPages: "固定页面"
|
||||
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
|
||||
pinnedClipId: "置顶的便签 ID"
|
||||
@@ -432,7 +432,7 @@ antennaExcludeBots: "排除机器人账户"
|
||||
antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
notifyAntenna: "开启通知"
|
||||
withFileAntenna: "仅带有附件的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道内的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道的帖子"
|
||||
enableServiceworker: "启用 ServiceWorker"
|
||||
antennaUsersDescription: "指定用户名,用换行符进行分隔"
|
||||
caseSensitive: "区分大小写"
|
||||
@@ -476,7 +476,7 @@ passwordLessLogin: "无密码登录"
|
||||
passwordLessLoginDescription: "不使用密码,仅使用安全密钥或 Passkey 登录"
|
||||
resetPassword: "重置密码"
|
||||
newPasswordIs: "新的密码是「{password}」"
|
||||
reduceUiAnimation: "减少 UI 动画"
|
||||
reduceUiAnimation: "减少 UI 动效"
|
||||
share: "分享"
|
||||
notFound: "未找到"
|
||||
notFoundDescription: "没有与指定 URL 对应的页面。"
|
||||
@@ -543,7 +543,7 @@ regenerate: "重新生成"
|
||||
fontSize: "字体大小"
|
||||
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
|
||||
limitTo: "上限为 {x}"
|
||||
showMediaListByGridInWideArea: "在大屏幕上并排显示媒体列表"
|
||||
showMediaListByGridInWideArea: "在宽屏上并排显示媒体列表"
|
||||
noFollowRequests: "没有关注请求"
|
||||
openImageInNewTab: "在新标签页中打开图片"
|
||||
dashboard: "管理面板"
|
||||
@@ -581,7 +581,7 @@ s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定
|
||||
serverLogs: "服务器日志"
|
||||
deleteAll: "全部删除"
|
||||
showFixedPostForm: "在时间线顶部显示发帖框"
|
||||
showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)"
|
||||
showFixedPostFormInChannel: "在时间线顶部显示发帖框(频道)"
|
||||
withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复"
|
||||
newNoteRecived: "有新的帖子"
|
||||
newNote: "新帖子"
|
||||
@@ -656,7 +656,7 @@ expandTweet: "展开帖子"
|
||||
themeEditor: "主题编辑器"
|
||||
description: "描述"
|
||||
describeFile: "添加描述"
|
||||
enterFileDescription: "输入标题"
|
||||
enterFileDescription: "输入描述文本"
|
||||
author: "作者"
|
||||
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
|
||||
manage: "管理"
|
||||
@@ -664,7 +664,7 @@ plugins: "插件"
|
||||
preferencesBackups: "备份设置"
|
||||
deck: "Deck"
|
||||
undeck: "取消 Deck"
|
||||
useBlurEffectForModal: "对话框使用模糊效果"
|
||||
useBlurEffectForModal: "发帖背景使用模糊效果"
|
||||
useFullReactionPicker: "使用全功能的回应工具栏"
|
||||
width: "宽度"
|
||||
height: "高度"
|
||||
@@ -773,13 +773,13 @@ yes: "是"
|
||||
no: "否"
|
||||
driveFilesCount: "网盘的文件数"
|
||||
driveUsage: "网盘的空间用量"
|
||||
noCrawle: "要求搜索引擎不索引该用户"
|
||||
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
|
||||
noCrawle: "拒绝搜索引擎的索引"
|
||||
noCrawleDescription: "拒绝搜索引擎收录(索引)您的个人资料,帖子,页面等。"
|
||||
lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅关注者」,任何人都可以看到您的帖子。"
|
||||
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
|
||||
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
|
||||
disableShowingAnimatedImages: "不播放动画"
|
||||
disableShowingAnimatedImages_caption: "如果即使关闭了此设置但动画仍无法播放,则可能是浏览器或操作系统的辅助功能设置,又或者是省电设置等产生了干扰。"
|
||||
disableShowingAnimatedImages: "不播放动态图像"
|
||||
disableShowingAnimatedImages_caption: "如果即使禁用了此设置,动态图像仍无法播放,可能是由于浏览器或操作系统的辅助功能设置、省电设置或其他因素所致。"
|
||||
highlightSensitiveMedia: "高亮显示敏感媒体"
|
||||
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
|
||||
notSet: "未设置"
|
||||
@@ -851,7 +851,7 @@ fullView: "全屏"
|
||||
quitFullView: "退出全屏"
|
||||
addDescription: "添加描述"
|
||||
userPagePinTip: "在帖子的菜单中选择“置顶”,即可显示该条帖子。"
|
||||
notSpecifiedMentionWarning: "有未指定的提及"
|
||||
notSpecifiedMentionWarning: "有未添加到收件人的提及"
|
||||
info: "关于"
|
||||
userInfo: "用户信息"
|
||||
unknown: "未知"
|
||||
@@ -877,9 +877,9 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
|
||||
noInquiryUrlWarning: "尚未设置联络地址。"
|
||||
noBotProtectionWarning: "尚未设置 Bot 防御。"
|
||||
configure: "设置"
|
||||
postToGallery: "创建新图集"
|
||||
postToGallery: "发表相册"
|
||||
postToHashtag: "发布至该话题"
|
||||
gallery: "图集"
|
||||
gallery: "相册"
|
||||
recentPosts: "最新发布"
|
||||
popularPosts: "热门投稿"
|
||||
shareWithNote: "分享到帖文"
|
||||
@@ -1032,7 +1032,7 @@ browserPushNotificationDisabledDescription: "{serverName}无权限发送通知
|
||||
windowMaximize: "最大化"
|
||||
windowMinimize: "最小化"
|
||||
windowRestore: "还原"
|
||||
caption: "标题"
|
||||
caption: "描述文本"
|
||||
loggedInAsBot: "以 Bot 账户登录"
|
||||
tools: "工具"
|
||||
cannotLoad: "无法加载"
|
||||
@@ -1073,7 +1073,7 @@ thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。"
|
||||
thisPostMayBeAnnoyingHome: "发到首页"
|
||||
thisPostMayBeAnnoyingCancel: "取消"
|
||||
thisPostMayBeAnnoyingIgnore: "就这样发布"
|
||||
collapseRenotes: "省略显示已经看过的转发内容"
|
||||
collapseRenotes: "折叠已经看过的转贴"
|
||||
collapseRenotesDescription: "折叠显示回应或转发过的帖文。"
|
||||
internalServerError: "内部服务器错误"
|
||||
internalServerErrorDescription: "内部服务器发生了预期外的错误"
|
||||
@@ -1081,9 +1081,9 @@ copyErrorInfo: "复制错误信息"
|
||||
joinThisServer: "在本服务器上注册"
|
||||
exploreOtherServers: "探索其他服务器"
|
||||
letsLookAtTimeline: "看看时间线"
|
||||
disableFederationConfirm: "确定要禁用联合?"
|
||||
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
|
||||
disableFederationOk: "联合禁用"
|
||||
disableFederationConfirm: "确定要禁用联邦交互?"
|
||||
disableFederationConfirmWarn: "即使禁用联邦交互,也不会将帖子设为私有。在大多数情况下,没有必要禁用联邦交互。"
|
||||
disableFederationOk: "禁用联邦"
|
||||
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
|
||||
emailNotSupported: "此服务器不支持发送邮件"
|
||||
postToTheChannel: "发布到频道"
|
||||
@@ -1093,7 +1093,7 @@ likeOnly: "仅点赞"
|
||||
likeOnlyForRemote: "全部(远程仅点赞)"
|
||||
nonSensitiveOnly: "仅限非敏感内容"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
|
||||
rolesAssignedToMe: "指派给自己的角色"
|
||||
rolesAssignedToMe: "我的角色"
|
||||
resetPasswordConfirm: "确定重置密码?"
|
||||
sensitiveWords: "敏感词"
|
||||
sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
|
||||
@@ -1148,7 +1148,7 @@ pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
|
||||
continue: "继续"
|
||||
preservedUsernames: "保留的用户名"
|
||||
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
|
||||
createNoteFromTheFile: "从文件创建帖子"
|
||||
createNoteFromTheFile: "使用该文件发帖"
|
||||
archive: "归档"
|
||||
archived: "已归档"
|
||||
unarchive: "取消归档"
|
||||
@@ -1158,7 +1158,7 @@ thisChannelArchived: "该频道已被归档。"
|
||||
displayOfNote: "显示帖子"
|
||||
initialAccountSetting: "初始设定"
|
||||
youFollowing: "正在关注"
|
||||
preventAiLearning: "拒绝接受生成式 AI 的学习"
|
||||
preventAiLearning: "拒绝用于训练生成式 AI"
|
||||
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
|
||||
options: "选项"
|
||||
specifyUser: "指定用户"
|
||||
@@ -1226,8 +1226,8 @@ notificationRecieveConfig: "通知接收设置"
|
||||
mutualFollow: "互相关注"
|
||||
followingOrFollower: "关注中或关注者"
|
||||
fileAttachedOnly: "仅限媒体"
|
||||
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
|
||||
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
|
||||
showRepliesToOthersInTimeline: "在时间线中显示对他人的回复"
|
||||
hideRepliesToOthersInTimeline: "在时间线中隐藏对他人的回复"
|
||||
showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
|
||||
hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
|
||||
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
|
||||
@@ -1325,22 +1325,22 @@ lockdown: "锁定"
|
||||
pleaseSelectAccount: "请选择帐户"
|
||||
availableRoles: "可用角色"
|
||||
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
|
||||
federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
|
||||
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
|
||||
federationSpecified: "此服务器已开启联邦白名单模式。只能与管理员指定的服务器通信。"
|
||||
federationDisabled: "此服务器已禁用联邦功能。无法与其它服务器上的用户通信。"
|
||||
draft: "草稿"
|
||||
draftsAndScheduledNotes: "草稿和定时发送"
|
||||
confirmOnReact: "发送回应前需要确认"
|
||||
reactAreYouSure: "要用「{emoji}」进行回应吗?"
|
||||
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
|
||||
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
|
||||
markAsSensitiveConfirm: "确定标记此媒体为敏感内容吗?"
|
||||
unmarkAsSensitiveConfirm: "确定取消标记为敏感内容吗?"
|
||||
preferences: "偏好设置"
|
||||
accessibility: "辅助功能"
|
||||
preferencesProfile: "设置的配置文件"
|
||||
copyPreferenceId: "复制设置 ID"
|
||||
resetToDefaultValue: "重置为默认值"
|
||||
overrideByAccount: "覆盖账号"
|
||||
overrideByAccount: "使用账户设置"
|
||||
untitled: "未命名"
|
||||
noName: "没有名字"
|
||||
noName: "未命名"
|
||||
skip: "跳过"
|
||||
restore: "恢复"
|
||||
syncBetweenDevices: "设备间同步"
|
||||
@@ -1351,7 +1351,7 @@ preferenceSyncConflictChoiceServer: "服务器上的设定值"
|
||||
preferenceSyncConflictChoiceDevice: "设备上的设定值"
|
||||
preferenceSyncConflictChoiceCancel: "取消同步"
|
||||
paste: "粘贴"
|
||||
emojiPalette: "表情符号调色板"
|
||||
emojiPalette: "表情符号选择器"
|
||||
postForm: "发帖窗口"
|
||||
textCount: "字数"
|
||||
information: "关于"
|
||||
@@ -1368,7 +1368,7 @@ embed: "嵌入"
|
||||
settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)"
|
||||
readonly: "只读"
|
||||
goToDeck: "返回至 Deck"
|
||||
federationJobs: "联合作业"
|
||||
federationJobs: "联邦作业"
|
||||
driveAboutTip: "网盘可以显示以前上传的文件。<br>\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。<br>\n<b>删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。</b><br>\n也可以新建文件夹来整理文件。"
|
||||
scrollToClose: "滑动并关闭"
|
||||
advice: "建议"
|
||||
@@ -1382,7 +1382,7 @@ unmuteX: "取消对{x}的隐藏"
|
||||
abort: "中止"
|
||||
tip: "提示和技巧"
|
||||
redisplayAllTips: "重新显示所有的提示和技巧"
|
||||
hideAllTips: "隐藏所有的提示和技巧"
|
||||
hideAllTips: "隐藏所有的 “提示与技巧”"
|
||||
defaultImageCompressionLevel: "默认图像压缩等级"
|
||||
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
|
||||
defaultCompressionLevel: "默认压缩等级"
|
||||
@@ -1394,7 +1394,7 @@ pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均
|
||||
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
|
||||
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
|
||||
thankYouForTestingBeta: "感谢您协助测试 beta 版!"
|
||||
createUserSpecifiedNote: "创建指定用户的帖子"
|
||||
createUserSpecifiedNote: "提及该用户并发帖"
|
||||
schedulePost: "定时发布"
|
||||
scheduleToPostOnX: "预定在 {x} 发出"
|
||||
scheduledToPostOnX: "已预定在 {x} 发出"
|
||||
@@ -1511,10 +1511,10 @@ _chat:
|
||||
mutual: "仅相互关注"
|
||||
none: "没有人"
|
||||
_emojiPalette:
|
||||
palettes: "调色板"
|
||||
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
|
||||
paletteForMain: "主调色板"
|
||||
paletteForReaction: "回应用调色板"
|
||||
palettes: "表情符号托盘"
|
||||
enableSyncBetweenDevicesForPalettes: "在设备间同步表情符号托盘"
|
||||
paletteForMain: "主表情符号托盘"
|
||||
paletteForReaction: "回应时的表情符号托盘"
|
||||
_settings:
|
||||
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
|
||||
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
|
||||
@@ -1535,9 +1535,9 @@ _settings:
|
||||
timelineAndNote: "时间线和帖子"
|
||||
makeEveryTextElementsSelectable: "使所有的文字均可选择"
|
||||
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
|
||||
useStickyIcons: "使图标跟随滚动"
|
||||
useStickyIcons: "用户头像跟随页面滚动"
|
||||
enableHighQualityImagePlaceholders: "显示高质量图像的占位符"
|
||||
uiAnimations: "UI 动画"
|
||||
uiAnimations: "UI 动效"
|
||||
showNavbarSubButtons: "在导航栏中显示副按钮"
|
||||
ifOn: "启用时"
|
||||
ifOff: "关闭时"
|
||||
@@ -1552,7 +1552,7 @@ _settings:
|
||||
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
|
||||
showPageTabBarBottom: "在下方显示页面标签栏"
|
||||
emojiPaletteBanner: "可以将固定显示在表情符号选择器中的预设注册为调色板,也可以自定义表情符号选择器的显示方式。"
|
||||
enableAnimatedImages: "启用动画图像"
|
||||
enableAnimatedImages: "启用动态图像"
|
||||
settingsPersistence_title: "设置持久化"
|
||||
settingsPersistence_description1: "启用设置持久化可防止设置信息丢失。"
|
||||
settingsPersistence_description2: "根据环境不同,有可能无法开启。"
|
||||
@@ -1580,12 +1580,12 @@ _accountSettings:
|
||||
requireSigninToViewContents: "需要登录才能显示内容"
|
||||
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
|
||||
requireSigninToViewContentsDescription2: "没有 URL 预览(OGP)、内嵌网页、引用帖子的功能的服务器也将无法显示。"
|
||||
requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。"
|
||||
requireSigninToViewContentsDescription3: "对于已通过联邦分发到远程服务器的内容,这些限制可能不适用。"
|
||||
makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见"
|
||||
makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。"
|
||||
makeNotesHiddenBefore: "将过去的帖子设为私密"
|
||||
makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
|
||||
mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。"
|
||||
mayNotEffectForFederatedNotes: "对于已通过联邦投递到远程服务器的帖子,此操作在远端可能无法生效。"
|
||||
mayNotEffectSomeSituations: "此限制功能非常简单,在与远程服务器联合等情形时可能不适用。"
|
||||
notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
|
||||
notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
|
||||
@@ -1637,7 +1637,7 @@ _announcement:
|
||||
_initialAccountSetting:
|
||||
accountCreated: "账户创建完成了!"
|
||||
letsStartAccountSetup: "马上来进行账户的初始设定吧。"
|
||||
letsFillYourProfile: "首先,来设定你的个人档案吧!"
|
||||
letsFillYourProfile: "首先,设置一下您的个人资料吧!"
|
||||
profileSetting: "个人资料设置"
|
||||
privacySetting: "隐私设置"
|
||||
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
|
||||
@@ -1689,10 +1689,10 @@ _initialTutorial:
|
||||
public: "向所有用户公开。\n"
|
||||
home: "仅在首页时间线上发布。 关注者、从个人资料页查看过来的用户、以及通过转帖也能被别的用户看见。"
|
||||
followers: "仅对关注者可见。 除了您自己之外,没有人可以转贴,并且只有您的关注者可以查看它。\n"
|
||||
direct: "它将仅向指定用户公开,并且他们也会收到通知。 您可以使用它来代替私信。\n"
|
||||
direct: "仅对指定用户公开,且收件人将收到通知。"
|
||||
doNotSendConfidencialOnDirect1: "发送敏感信息时请注意。\n"
|
||||
doNotSendConfidencialOnDirect2: "目标服务器的管理员可以看到发布的内容,因此如果您向不受信任的服务器上的用户发送私信,则在处理敏感信息时需要小心。"
|
||||
localOnly: "不将帖子推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
|
||||
localOnly: "不将帖子通过联邦推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
|
||||
_cw:
|
||||
title: "隐藏内容 (CW)\n"
|
||||
description: "显示「注解」里的内容而不是正文。点击「查看更多」将会把正文显示出来。"
|
||||
@@ -1701,7 +1701,7 @@ _initialTutorial:
|
||||
note: "茨了带巧克力的甜甜圈🍩😋"
|
||||
useCases: "用于服务器条款所规定的帖子,或对剧透内容和敏感内容进行自主规制。"
|
||||
_howToMakeAttachmentsSensitive:
|
||||
title: "如何将附件标注为敏感内容?"
|
||||
title: "如何标记附件为敏感内容?"
|
||||
description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n"
|
||||
tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!"
|
||||
_exampleNote:
|
||||
@@ -1746,7 +1746,7 @@ _serverSettings:
|
||||
singleUserMode: "单用户模式"
|
||||
singleUserMode_description: "若此服务器只有自己使用,开启此模式将最佳化性能。"
|
||||
signToActivityPubGet: "对 GET 请求签名"
|
||||
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联合通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
|
||||
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联邦通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
|
||||
proxyRemoteFiles: "代理远程文件"
|
||||
proxyRemoteFiles_description: "如果启用,远程服务器的文件将由代理提供。可有效保护图像预览缩略图的生成与用户隐私。"
|
||||
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
|
||||
@@ -1771,7 +1771,7 @@ _accountMigration:
|
||||
moveTo: "把这个账户迁移到新的账户"
|
||||
moveToLabel: "迁移后的账户"
|
||||
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
|
||||
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、隐藏、屏蔽也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
|
||||
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、屏蔽列表、禁止与我互动的列表也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
|
||||
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com"
|
||||
startMigration: "迁移"
|
||||
migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。"
|
||||
@@ -1889,7 +1889,7 @@ _achievements:
|
||||
description: "第一次将帖子加入收藏"
|
||||
_myNoteFavorited1:
|
||||
title: "想要星星"
|
||||
description: "自己的帖子被其他人加入收藏了"
|
||||
description: "自己的帖子被其他人收藏了"
|
||||
_profileFilled:
|
||||
title: "整装待发"
|
||||
description: "设置了个人资料"
|
||||
@@ -2083,7 +2083,7 @@ _role:
|
||||
maxFileSize: "可上传的最大文件大小"
|
||||
maxFileSize_caption: "可能在反向代理或 CDN 等前端存在其它设定值。"
|
||||
alwaysMarkNsfw: "总是将文件标记为 NSFW"
|
||||
canUpdateBioMedia: "可以更新头像和横幅"
|
||||
canUpdateBioMedia: "允许更新头像和横幅"
|
||||
pinMax: "帖子置顶数量限制"
|
||||
antennaMax: "可创建的最大天线数量"
|
||||
wordMuteMax: "折叠词的字数限制"
|
||||
@@ -2098,9 +2098,10 @@ _role:
|
||||
canSearchNotes: "是否可以搜索帖子"
|
||||
canSearchUsers: "使用用户检索"
|
||||
canUseTranslator: "使用翻译功能"
|
||||
canCreateChannel: "创建频道"
|
||||
avatarDecorationLimit: "可添加头像挂件的最大个数"
|
||||
canImportAntennas: "允许导入天线"
|
||||
canImportBlocking: "允许导入屏蔽列表"
|
||||
canImportBlocking: "允许导入禁止与我互动的列表"
|
||||
canImportFollowing: "允许导入关注列表"
|
||||
canImportMuting: "允许导入隐藏列表"
|
||||
canImportUserLists: "允许导入用户列表"
|
||||
@@ -2108,7 +2109,7 @@ _role:
|
||||
uploadableFileTypes: "可上传的文件类型"
|
||||
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)"
|
||||
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
|
||||
noteDraftLimit: "可在服务器上创建多少草稿"
|
||||
noteDraftLimit: "可在服务器上创建的草稿数量"
|
||||
scheduledNoteLimit: "可同时创建的定时帖子数量"
|
||||
watermarkAvailable: "能否使用水印功能"
|
||||
_condition:
|
||||
@@ -2165,7 +2166,7 @@ _ad:
|
||||
back: "返回"
|
||||
reduceFrequencyOfThisAd: "减少此广告的频率"
|
||||
hide: "不显示"
|
||||
timezoneinfo: "星期几是由服务器的时区所指定的。"
|
||||
timezoneinfo: "星期几是根据服务器的时区确定的。"
|
||||
adsSettings: "广告设置"
|
||||
notesPerOneAd: "在实时更新时间线中插入广告的间隔(帖子个数)"
|
||||
setZeroToDisable: "设为 0 将不在实时更新时间线中投放广告"
|
||||
@@ -2229,7 +2230,7 @@ _aboutMisskey:
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "隐藏敏感媒体"
|
||||
ignore: "显示敏感媒体"
|
||||
force: "隐藏所有内容"
|
||||
force: "隐藏所有媒体"
|
||||
_instanceTicker:
|
||||
none: "不显示"
|
||||
remote: "仅远程用户"
|
||||
@@ -2253,7 +2254,7 @@ _channel:
|
||||
allowRenoteToExternal: "允许转发到频道外和引用"
|
||||
_menuDisplay:
|
||||
sideFull: "横向"
|
||||
sideIcon: "横向(图标)"
|
||||
sideIcon: "横向(图标)"
|
||||
top: "顶部"
|
||||
hide: "隐藏"
|
||||
_wordMute:
|
||||
@@ -2315,7 +2316,7 @@ _theme:
|
||||
mention: "提及"
|
||||
mentionMe: "提及"
|
||||
renote: "转发"
|
||||
modalBg: "对话框背景"
|
||||
modalBg: "发帖背景"
|
||||
divider: "分割线"
|
||||
scrollbarHandle: "滚动条"
|
||||
scrollbarHandleHover: "滚动条(悬停)"
|
||||
@@ -2334,7 +2335,7 @@ _theme:
|
||||
fgHighlighted: "高亮显示文本"
|
||||
_sfx:
|
||||
note: "帖子"
|
||||
noteMy: "我的帖子"
|
||||
noteMy: "发帖"
|
||||
notification: "通知"
|
||||
reaction: "选择回应时"
|
||||
chatMessage: "私信"
|
||||
@@ -2403,8 +2404,8 @@ _2fa:
|
||||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
"write:account": "更改帐户信息"
|
||||
"read:blocks": "查看屏蔽列表"
|
||||
"write:blocks": "编辑屏蔽列表"
|
||||
"read:blocks": "查看禁止与我互动的列表"
|
||||
"write:blocks": "编辑禁止与我互动的列表"
|
||||
"read:drive": "查看网盘"
|
||||
"write:drive": "管理网盘文件"
|
||||
"read:favorites": "查看收藏夹"
|
||||
@@ -2429,10 +2430,10 @@ _permissions:
|
||||
"write:user-groups": "编辑用户组"
|
||||
"read:channels": "查看频道"
|
||||
"write:channels": "管理频道"
|
||||
"read:gallery": "浏览图集"
|
||||
"write:gallery": "编辑图集"
|
||||
"read:gallery-likes": "浏览喜欢的图集"
|
||||
"write:gallery-likes": "管理喜欢的图集"
|
||||
"read:gallery": "浏览相册"
|
||||
"write:gallery": "管理相册"
|
||||
"read:gallery-likes": "浏览喜欢的相册"
|
||||
"write:gallery-likes": "管理喜欢的相册"
|
||||
"read:flash": "查看 Play"
|
||||
"write:flash": "编辑 Play"
|
||||
"read:flash-likes": "查看 Play 的点赞"
|
||||
@@ -2456,7 +2457,7 @@ _permissions:
|
||||
"write:admin:unsuspend-user": "解除用户冻结"
|
||||
"write:admin:meta": "编辑实例元数据"
|
||||
"write:admin:user-note": "编辑管理笔记"
|
||||
"write:admin:roles": "编辑角色"
|
||||
"write:admin:roles": "管理角色"
|
||||
"read:admin:roles": "查看角色"
|
||||
"write:admin:relays": "编辑中继"
|
||||
"read:admin:relays": "查看中继"
|
||||
@@ -2466,8 +2467,8 @@ _permissions:
|
||||
"read:admin:announcements": "查看公告"
|
||||
"write:admin:avatar-decorations": "编辑头像挂件"
|
||||
"read:admin:avatar-decorations": "查看头像挂件"
|
||||
"write:admin:federation": "编辑联合相关信息"
|
||||
"write:admin:account": "编辑用户账户"
|
||||
"write:admin:federation": "编辑联邦相关信息"
|
||||
"write:admin:account": "管理用户账户"
|
||||
"read:admin:account": "查看用户相关情报"
|
||||
"write:admin:emoji": "编辑表情符号"
|
||||
"read:admin:emoji": "查看表情符号"
|
||||
@@ -2483,7 +2484,7 @@ _permissions:
|
||||
"read:invite-codes": "获取已发行的邀请码"
|
||||
"write:clip-favorite": "管理喜欢的便签"
|
||||
"read:clip-favorite": "查看便签的点赞"
|
||||
"read:federation": "查看联合相关信息"
|
||||
"read:federation": "查看联邦相关信息"
|
||||
"write:report-abuse": "举报用户"
|
||||
"write:chat": "撰写或删除消息"
|
||||
"read:chat": "查看私信"
|
||||
@@ -2530,7 +2531,7 @@ _widgets:
|
||||
photos: "照片"
|
||||
digitalClock: "数字时钟"
|
||||
unixClock: "UNIX 时钟"
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
instanceCloud: "服务器球状列表"
|
||||
postForm: "发帖窗口"
|
||||
slideshow: "幻灯片展示"
|
||||
@@ -2563,7 +2564,7 @@ _widgetOptions:
|
||||
graduationDots: "点"
|
||||
graduationArabic: "阿拉伯数字"
|
||||
fadeGraduations: "淡化表盘"
|
||||
sAnimation: "秒针动画"
|
||||
sAnimation: "秒针动效"
|
||||
sAnimationElastic: "跳动"
|
||||
sAnimationEaseOut: "平滑"
|
||||
twentyFour: "24 小时制"
|
||||
@@ -2621,11 +2622,11 @@ _visibility:
|
||||
followersDescription: "仅发送至关注者"
|
||||
specified: "指定用户"
|
||||
specifiedDescription: "仅发送至指定用户"
|
||||
disableFederation: "不参与联合"
|
||||
disableFederation: "仅限本地"
|
||||
disableFederationDescription: "不发送到其他服务器"
|
||||
_postForm:
|
||||
quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?"
|
||||
uploaderTip: "文件还未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在发帖时自动上传。"
|
||||
uploaderTip: "文件尚未上传。您可以在文件菜单中设置重命名、裁剪图片、添加水印以及是否压缩等功能。文件将在帖子发布时自动上传。"
|
||||
replyPlaceholder: "回复这个帖子..."
|
||||
quotePlaceholder: "引用这个帖子..."
|
||||
channelPlaceholder: "发布到频道…"
|
||||
@@ -2660,12 +2661,12 @@ _profile:
|
||||
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
|
||||
metadataLabel: "标签"
|
||||
metadataContent: "内容"
|
||||
changeAvatar: "修改头像"
|
||||
changeBanner: "修改横幅"
|
||||
changeAvatar: "更换头像"
|
||||
changeBanner: "更换横幅"
|
||||
verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。"
|
||||
avatarDecorationMax: "最多可添加 {max} 个挂件"
|
||||
followedMessage: "被关注时显示的消息"
|
||||
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
|
||||
followedMessage: "被关注时的信息"
|
||||
followedMessageDescription: "被关注时,可设置向关注者显示的信息。"
|
||||
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息会在请求被批准后显示。"
|
||||
_exportOrImport:
|
||||
allNotes: "所有帖子"
|
||||
@@ -2673,13 +2674,13 @@ _exportOrImport:
|
||||
clips: "便签"
|
||||
followingList: "关注中"
|
||||
muteList: "隐藏"
|
||||
blockingList: "屏蔽"
|
||||
blockingList: "禁止与我互动的列表"
|
||||
userLists: "列表"
|
||||
excludeMutingUsers: "排除已隐藏用户"
|
||||
excludeInactiveUsers: "排除不活跃用户"
|
||||
withReplies: "在时间线中包含导入用户的回复"
|
||||
_charts:
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
apRequest: "请求"
|
||||
usersIncDec: "用户数量:增加/减少"
|
||||
usersTotal: "用户总数"
|
||||
@@ -2977,7 +2978,7 @@ _moderationLogTypes:
|
||||
deleteAccount: "删除帐户"
|
||||
deletePage: "删除页面"
|
||||
deleteFlash: "删除 Play"
|
||||
deleteGalleryPost: "删除图集内容"
|
||||
deleteGalleryPost: "删除相册内容"
|
||||
deleteChatRoom: "删除群聊"
|
||||
updateProxyAccountDescription: "更新代理账户的简介"
|
||||
_fileViewer:
|
||||
@@ -3034,7 +3035,7 @@ _dataSaver:
|
||||
description: "防止自动加载图像和视频。 点击隐藏的图像/视频即可加载它们。\n"
|
||||
_avatar:
|
||||
title: "头像"
|
||||
description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。"
|
||||
description: "不播放头像的动画。 由于动态图像的文件大小远大于一般图像,停止播放能够进一步节省数据流量。"
|
||||
_urlPreviewThumbnail:
|
||||
title: "不显示 URL预览缩略图"
|
||||
description: "将不再加载 URL 预览缩略图。"
|
||||
@@ -3053,7 +3054,7 @@ _reversi:
|
||||
gameSettings: "对局设置"
|
||||
chooseBoard: "选择棋盘"
|
||||
blackOrWhite: "先手/后手"
|
||||
blackIs: "{name}执黑(先手)"
|
||||
blackIs: "{name}执黑(先手)"
|
||||
rules: "规则"
|
||||
thisGameIsStartedSoon: "对局即将开始"
|
||||
waitingForOther: "等待对手准备"
|
||||
@@ -3069,7 +3070,7 @@ _reversi:
|
||||
surrendered: "已认输"
|
||||
timeout: "超时"
|
||||
drawn: "平局"
|
||||
won: "{name}获胜"
|
||||
won: "{name} 获胜"
|
||||
black: "黑"
|
||||
white: "白"
|
||||
total: "总计"
|
||||
@@ -3078,7 +3079,7 @@ _reversi:
|
||||
allGames: "所有对局"
|
||||
ended: "结束"
|
||||
playing: "对局中"
|
||||
isLlotheo: "落子少的一方获胜(又名奥赛罗)"
|
||||
isLlotheo: "落子少的一方获胜(黑白棋规则)"
|
||||
loopedMap: "循环棋盘"
|
||||
canPutEverywhere: "无限制放置模式"
|
||||
timeLimitForEachTurn: "1回合的时间限制"
|
||||
@@ -3088,8 +3089,8 @@ _reversi:
|
||||
shareToTlTheGameWhenStart: "开始时在时间线发布对局"
|
||||
iStartedAGame: "对局开始!#MisskeyReversi"
|
||||
opponentHasSettingsChanged: "对手更改了设定"
|
||||
allowIrregularRules: "允许非常规规则(完全自由)"
|
||||
disallowIrregularRules: "禁止非常规规则"
|
||||
allowIrregularRules: "允许特殊规则(完全自由)"
|
||||
disallowIrregularRules: "禁止特殊规则"
|
||||
showBoardLabels: "显示行号和列号"
|
||||
useAvatarAsStone: "用头像作为棋子"
|
||||
_offlineScreen:
|
||||
@@ -3246,7 +3247,7 @@ _search:
|
||||
searchScopeLocal: "本地"
|
||||
searchScopeServer: "指定服务器"
|
||||
searchScopeUser: "指定用户"
|
||||
pleaseEnterServerHost: "请填写服务器主机名"
|
||||
pleaseEnterServerHost: "请填写服务器的主机名称"
|
||||
pleaseSelectUser: "请选择用户"
|
||||
serverHostPlaceholder: "如:misskey.example.com"
|
||||
_serverSetupWizard:
|
||||
@@ -3275,13 +3276,13 @@ _serverSetupWizard:
|
||||
largeScaleServerAdvice: "运营大规模服务器可能需要高级基础设施知识,如负载均衡和数据库复制。"
|
||||
doYouConnectToFediverse: "要加入 Fediverse 吗?"
|
||||
doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络(Fediverse),将能与其它服务器交换内容。"
|
||||
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联合」。"
|
||||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
|
||||
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联邦」。"
|
||||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器允许进行联邦交互等高级设置。"
|
||||
remoteContentsCleaning: "自动清理传入内容"
|
||||
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
remoteContentsCleaning_description: "开启联邦互通后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
adminInfo: "管理员信息"
|
||||
adminInfo_description: "设置用于接受询问的管理员信息。"
|
||||
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"
|
||||
adminInfo_mustBeFilled: "开放服务器或启用了联邦的情况下必须输入。"
|
||||
followingSettingsAreRecommended: "推荐以下设置"
|
||||
applyTheseSettings: "使用此设置"
|
||||
skipSettings: "跳过设置"
|
||||
@@ -3301,7 +3302,7 @@ _uploader:
|
||||
doneConfirm: "部分文件尚未上传,是否继续?"
|
||||
maxFileSizeIsX: "可上传最大 {x} 的文件。"
|
||||
allowedTypes: "可上传的文件类型"
|
||||
tip: "文件还没有被上传。可在此对话框中进行上传前确认、重命名、压缩、裁剪等操作。准备完成后,点击「上传」即可开始上传。"
|
||||
tip: "文件尚未上传。在此对话框中,您可以进行上传前的确认、重命名、压缩和裁剪等操作。准备就绪后,点击“上传”按钮即可开始上传。"
|
||||
_clientPerformanceIssueTip:
|
||||
title: "如果觉得电池耗电过高"
|
||||
makeSureDisabledAdBlocker: "请关闭广告拦截器"
|
||||
|
||||
39
package.json
39
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.4.0-beta.0",
|
||||
"version": "2026.6.0-alpha.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@11.1.2",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
@@ -53,44 +53,33 @@
|
||||
"cleanall": "pnpm clean-all"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssnano": "7.1.5",
|
||||
"cssnano": "8.0.1",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "8.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"postcss": "8.5.9",
|
||||
"tar": "7.5.13",
|
||||
"terser": "5.46.1"
|
||||
"postcss": "8.5.14",
|
||||
"tar": "7.5.15",
|
||||
"terser": "5.47.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260421.2",
|
||||
"@types/node": "24.12.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.3",
|
||||
"@typescript-eslint/parser": "8.59.3",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260426.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.13.1",
|
||||
"cypress": "15.15.0",
|
||||
"eslint": "9.39.4",
|
||||
"globals": "17.5.0",
|
||||
"globals": "17.6.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.33.0",
|
||||
"start-server-and-test": "3.0.2",
|
||||
"pnpm": "11.1.2",
|
||||
"start-server-and-test": "3.0.5",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.22.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@aiscript-dev/aiscript-languageserver": "-",
|
||||
"chokidar": "5.0.0",
|
||||
"lodash": "4.18.1"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"@sentry-internal/node-cpu-profiler",
|
||||
"exifreader"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
|
||||
export class NoteIdIndexForPinAndFavorite1780059833698 {
|
||||
name = 'NoteIdIndexForPinAndFavorite1780059833698';
|
||||
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
|
||||
|
||||
async up(queryRunner) {
|
||||
const concurrently = isConcurrentIndexMigrationEnabled ? 'CONCURRENTLY' : '';
|
||||
|
||||
await queryRunner.query(`CREATE INDEX ${concurrently} IF NOT EXISTS "IDX_0e00498f180193423c992bc437" ON "note_favorite" ("noteId")`);
|
||||
await queryRunner.query(`CREATE INDEX ${concurrently} IF NOT EXISTS "IDX_68881008f7c3588ad7ecae471c" ON "user_note_pining" ("noteId")`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
const concurrently = isConcurrentIndexMigrationEnabled ? 'CONCURRENTLY' : '';
|
||||
|
||||
await queryRunner.query(`DROP INDEX ${concurrently} IF EXISTS "public"."IDX_68881008f7c3588ad7ecae471c"`);
|
||||
await queryRunner.query(`DROP INDEX ${concurrently} IF EXISTS "public"."IDX_0e00498f180193423c992bc437"`);
|
||||
}
|
||||
}
|
||||
@@ -37,54 +37,52 @@
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.1.0",
|
||||
"slacc-android-arm-eabi": "0.0.10",
|
||||
"slacc-android-arm64": "0.0.10",
|
||||
"slacc-darwin-arm64": "0.0.10",
|
||||
"slacc-darwin-universal": "0.0.10",
|
||||
"slacc-darwin-x64": "0.0.10",
|
||||
"slacc-freebsd-x64": "0.0.10",
|
||||
"slacc-linux-arm-gnueabihf": "0.0.10",
|
||||
"slacc-linux-arm64-gnu": "0.0.10",
|
||||
"slacc-linux-arm64-musl": "0.0.10",
|
||||
"slacc-linux-x64-gnu": "0.0.10",
|
||||
"slacc-linux-x64-musl": "0.0.10",
|
||||
"slacc-win32-arm64-msvc": "0.0.10",
|
||||
"slacc-win32-x64-msvc": "0.0.10",
|
||||
"slacc-android-arm-eabi": "0.1.5",
|
||||
"slacc-android-arm64": "0.1.5",
|
||||
"slacc-darwin-arm64": "0.1.5",
|
||||
"slacc-darwin-universal": "0.1.5",
|
||||
"slacc-darwin-x64": "0.1.5",
|
||||
"slacc-freebsd-x64": "0.1.5",
|
||||
"slacc-linux-arm-gnueabihf": "0.1.5",
|
||||
"slacc-linux-arm64-gnu": "0.1.5",
|
||||
"slacc-linux-arm64-musl": "0.1.5",
|
||||
"slacc-linux-x64-gnu": "0.1.5",
|
||||
"slacc-linux-x64-musl": "0.1.5",
|
||||
"slacc-win32-arm64-msvc": "0.1.5",
|
||||
"slacc-win32-x64-msvc": "0.1.5",
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1030.0",
|
||||
"@aws-sdk/lib-storage": "3.1030.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@aws-sdk/client-s3": "3.1047.0",
|
||||
"@aws-sdk/lib-storage": "3.1047.0",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/express": "4.0.5",
|
||||
"@fastify/http-proxy": "11.4.4",
|
||||
"@fastify/multipart": "10.0.0",
|
||||
"@fastify/static": "9.1.3",
|
||||
"@kitajs/html": "4.2.13",
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.97",
|
||||
"@nestjs/common": "11.1.19",
|
||||
"@nestjs/core": "11.1.19",
|
||||
"@nestjs/testing": "11.1.19",
|
||||
"@oxc-project/runtime": "0.125.0",
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
"@napi-rs/canvas": "1.0.0",
|
||||
"@nestjs/common": "11.1.21",
|
||||
"@nestjs/core": "11.1.21",
|
||||
"@nestjs/testing": "11.1.21",
|
||||
"@oxc-project/runtime": "0.130.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.48.0",
|
||||
"@sentry/profiling-node": "10.48.0",
|
||||
"@sentry/node": "10.53.1",
|
||||
"@sentry/profiling-node": "10.53.1",
|
||||
"@simplewebauthn/server": "13.3.0",
|
||||
"@sinonjs/fake-timers": "15.3.2",
|
||||
"@smithy/node-http-handler": "4.5.2",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@sinonjs/fake-timers": "15.4.0",
|
||||
"@smithy/node-http-handler": "4.7.2",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.18.0",
|
||||
"ajv": "8.20.0",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "2.2.2",
|
||||
"bullmq": "5.73.5",
|
||||
"bullmq": "5.76.8",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
@@ -92,79 +90,75 @@
|
||||
"color-convert": "3.1.3",
|
||||
"content-disposition": "1.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"deep-email-validator": "0.1.27",
|
||||
"fastify": "5.8.5",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "5.2.0",
|
||||
"feed": "5.2.1",
|
||||
"file-type": "22.0.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.5",
|
||||
"got": "14.6.6",
|
||||
"got": "15.0.5",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
"ioredis": "5.10.1",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.3.0",
|
||||
"ipaddr.js": "2.4.0",
|
||||
"is-svg": "6.1.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "9.0.0",
|
||||
"juice": "11.1.1",
|
||||
"meilisearch": "0.57.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"meilisearch": "0.58.0",
|
||||
"mfm-js": "0.26.0",
|
||||
"mime-types": "3.0.2",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.7",
|
||||
"nanoid": "5.1.11",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.1.0",
|
||||
"nodemailer": "8.0.5",
|
||||
"nodemailer": "8.0.7",
|
||||
"nsfwjs": "4.3.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.5.0",
|
||||
"otpauth": "9.5.1",
|
||||
"pg": "8.20.0",
|
||||
"pkce-challenge": "6.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"probe-image-size": "7.3.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"qrcode": "1.5.4",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.24.0",
|
||||
"re2": "1.24.1",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.17.3",
|
||||
"sanitize-html": "2.17.4",
|
||||
"secure-json-parse": "4.1.0",
|
||||
"semver": "7.7.4",
|
||||
"semver": "7.8.0",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
"slacc": "0.1.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.31.5",
|
||||
"systeminformation": "5.31.6",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
"typeorm": "0.3.28",
|
||||
"tsc-alias": "1.8.17",
|
||||
"typeorm": "1.0.0",
|
||||
"ulid": "3.0.2",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.20.0",
|
||||
"ws": "8.20.1",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kitajs/ts-html-plugin": "4.1.4",
|
||||
"@nestjs/platform-express": "11.1.19",
|
||||
"@nestjs/platform-express": "11.1.21",
|
||||
"@rollup/plugin-esm-shim": "0.1.8",
|
||||
"@sentry/vue": "10.48.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@sentry/vue": "10.53.1",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "7.0.0",
|
||||
"@types/body-parser": "1.19.6",
|
||||
"@types/color-convert": "3.0.1",
|
||||
"@types/content-disposition": "0.5.9",
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
@@ -173,10 +167,8 @@
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "24.12.2",
|
||||
"@types/node": "24.12.4",
|
||||
"@types/nodemailer": "8.0.0",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/random-seed": "0.3.5",
|
||||
@@ -192,9 +184,9 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.3",
|
||||
"@typescript-eslint/parser": "8.59.3",
|
||||
"@vitest/coverage-v8": "4.1.6",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor": "10.0.12",
|
||||
"cross-env": "10.1.0",
|
||||
@@ -203,11 +195,11 @@
|
||||
"fkill": "10.0.3",
|
||||
"js-yaml": "4.1.1",
|
||||
"pid-port": "2.1.1",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"rolldown": "1.0.1",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.2.2",
|
||||
"vite": "8.0.8",
|
||||
"vitest": "4.1.4",
|
||||
"vite": "8.0.13",
|
||||
"vitest": "4.1.6",
|
||||
"vitest-mock-extended": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,14 @@ export default defineConfig((args) => {
|
||||
'@nestjs/microservices/microservices-module',
|
||||
'@nestjs/microservices',
|
||||
/^@napi-rs\/.*/,
|
||||
// @tensorflow/tfjs-node はネイティブバインディングを持つため external 必須 (#17501)。
|
||||
// あわせて nsfwjs と @tensorflow/* 全体を external にする。bundle 内の nsfwjs が
|
||||
// 抱える @tensorflow/tfjs-core と、external な tfjs-node が使う tfjs-core が
|
||||
// 別インスタンスに分裂すると、tfjs-node が登録する file:// IOHandler を nsfwjs 側が
|
||||
// 共有できず、モデル読み込みが HTTP handler(node-fetch) にフォールバックして
|
||||
// 「URL scheme "file" is not supported」で失敗するため。
|
||||
/^@tensorflow\/.*/,
|
||||
'nsfwjs',
|
||||
'mock-aws-s3',
|
||||
'aws-sdk',
|
||||
'nock',
|
||||
@@ -73,7 +81,6 @@ export default defineConfig((args) => {
|
||||
'jsdom',
|
||||
're2',
|
||||
'ipaddr.js',
|
||||
'oauth2orize',
|
||||
'file-type',
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
*/
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { init } from 'slacc';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
let slaccInitialized = false;
|
||||
|
||||
export function initExtraThreadPool(config: Config) {
|
||||
if (slaccInitialized) return;
|
||||
|
||||
const threadPoolSize = Math.max(config.threadPoolSize ?? 1, 1);
|
||||
|
||||
init(threadPoolSize);
|
||||
|
||||
slaccInitialized = true;
|
||||
}
|
||||
|
||||
export async function server() {
|
||||
const { MainModule } = await import('../MainModule.js');
|
||||
|
||||
@@ -13,7 +13,7 @@ import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||
@@ -64,6 +64,8 @@ export async function masterMain() {
|
||||
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
@@ -14,6 +14,8 @@ import { jobQueue, server } from './common.js';
|
||||
export async function workerMain() {
|
||||
const config = loadConfig();
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
@@ -85,6 +85,7 @@ type Source = {
|
||||
maxFileSize?: number;
|
||||
|
||||
clusterLimit?: number;
|
||||
threadPoolSize?: number;
|
||||
|
||||
id: string;
|
||||
|
||||
@@ -158,6 +159,7 @@ export type Config = {
|
||||
allowedPrivateNetworks: string[] | undefined;
|
||||
maxFileSize: number;
|
||||
clusterLimit: number | undefined;
|
||||
threadPoolSize: number;
|
||||
id: string;
|
||||
outgoingAddress: string | undefined;
|
||||
outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined;
|
||||
@@ -313,6 +315,7 @@ export function loadConfig(): Config {
|
||||
allowedPrivateNetworks: config.allowedPrivateNetworks,
|
||||
maxFileSize: config.maxFileSize ?? 262144000,
|
||||
clusterLimit: config.clusterLimit,
|
||||
threadPoolSize: config.threadPoolSize ?? 1,
|
||||
outgoingAddress: config.outgoingAddress,
|
||||
outgoingAddressFamily: config.outgoingAddressFamily,
|
||||
deliverJobConcurrency: config.deliverJobConcurrency,
|
||||
|
||||
@@ -182,11 +182,12 @@ export class AnnouncementService {
|
||||
@bindThis
|
||||
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
|
||||
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
|
||||
if (me) {
|
||||
if (announcement.userId && announcement.userId !== me.id) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
if (announcement.userId && (me == null || announcement.userId !== me.id)) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
if (me) {
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: announcement.id,
|
||||
userId: me.id,
|
||||
|
||||
@@ -72,7 +72,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||
fetcher: (key) => this.mutingsRepository.find({
|
||||
where: { muterId: key },
|
||||
select: { muteeId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
@@ -80,7 +83,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({
|
||||
where: { blockerId: key },
|
||||
select: { blockeeId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
@@ -88,7 +94,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({
|
||||
where: { blockeeId: key },
|
||||
select: { blockerId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
@@ -96,7 +105,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||
fetcher: (key) => this.renoteMutingsRepository.find({
|
||||
where: { muterId: key },
|
||||
select: { muteeId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
@@ -104,7 +116,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => {
|
||||
fetcher: (key) => this.followingsRepository.find({
|
||||
where: { followerId: key },
|
||||
select: { followeeId: true, withReplies: true },
|
||||
}).then(xs => {
|
||||
const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||
for (const x of xs) {
|
||||
obj[x.followeeId] = { withReplies: x.withReplies };
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ChannelFollowingService implements OnModuleInit {
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.channelFollowingsRepository.find({
|
||||
where: { followerId: key },
|
||||
select: ['followeeId'],
|
||||
select: { followeeId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ChannelMutingService {
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (userId) => this.channelMutingRepository.find({
|
||||
where: { userId: userId },
|
||||
select: ['channelId'],
|
||||
select: { channelId: true },
|
||||
}).then(xs => new Set(xs.map(x => x.channelId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
|
||||
@@ -572,6 +572,27 @@ export class ChatService {
|
||||
return created;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hasPermissionToViewRoomInfo(meId: MiUser['id'], room: MiChatRoom) {
|
||||
if (room.ownerId === meId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.isRoomMember(room, meId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: meId })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.roleService.isModerator({ id: meId })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) {
|
||||
if (room.ownerId === meId) {
|
||||
@@ -623,7 +644,10 @@ export class ChatService {
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiChatRoom['id']) {
|
||||
return this.chatRoomsRepository.findOne({ where: { id: roomId }, relations: ['owner'] });
|
||||
return this.chatRoomsRepository.findOne({
|
||||
where: { id: roomId },
|
||||
relations: { owner: true },
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -867,7 +891,7 @@ export class ChatService {
|
||||
const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null;
|
||||
|
||||
if (room) {
|
||||
if (!await this.isRoomMember(room, userId)) {
|
||||
if (!(await this.isRoomMember(room, userId))) {
|
||||
throw new Error('cannot react to others message');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
}
|
||||
const _emojis = emojisQuery.length > 0 ? await this.emojisRepository.find({
|
||||
where: emojisQuery,
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
select: {
|
||||
name: true,
|
||||
host: true,
|
||||
originalUrl: true,
|
||||
publicUrl: true,
|
||||
},
|
||||
}) : [];
|
||||
for (const emoji of _emojis) {
|
||||
this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
|
||||
@@ -69,7 +69,10 @@ export class DeleteAccountService {
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
{ followeeSharedInbox: Not(IsNull()) },
|
||||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followeeSharedInbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
@@ -112,4 +112,9 @@ export class FanoutTimelineService {
|
||||
public purge(name: FanoutTimelineName) {
|
||||
return this.redisForTimelines.del('list:' + name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public remove(name: FanoutTimelineName, id: string) {
|
||||
return this.redisForTimelines.lrem('list:' + name, 1, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,20 +63,21 @@ type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
class NotificationManager {
|
||||
private notifier: { id: MiUser['id']; };
|
||||
private note: MiNote;
|
||||
private queue: {
|
||||
private queue: Map<MiLocalUser['id'], {
|
||||
target: MiLocalUser['id'];
|
||||
reason: NotificationType;
|
||||
}[];
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
private mutingsRepository: MutingsRepository,
|
||||
private notificationService: NotificationService,
|
||||
private followingsRepository: FollowingsRepository,
|
||||
notifier: { id: MiUser['id']; },
|
||||
note: MiNote,
|
||||
) {
|
||||
this.notifier = notifier;
|
||||
this.note = note;
|
||||
this.queue = [];
|
||||
this.queue = new Map();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -84,7 +85,7 @@ class NotificationManager {
|
||||
// 自分自身へは通知しない
|
||||
if (this.notifier.id === notifiee) return;
|
||||
|
||||
const exist = this.queue.find(x => x.target === notifiee);
|
||||
const exist = this.queue.get(notifiee);
|
||||
|
||||
if (exist) {
|
||||
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
|
||||
@@ -92,7 +93,7 @@ class NotificationManager {
|
||||
exist.reason = reason;
|
||||
}
|
||||
} else {
|
||||
this.queue.push({
|
||||
this.queue.set(notifiee, {
|
||||
reason: reason,
|
||||
target: notifiee,
|
||||
});
|
||||
@@ -101,7 +102,50 @@ class NotificationManager {
|
||||
|
||||
@bindThis
|
||||
public async notify() {
|
||||
for (const x of this.queue) {
|
||||
if (this.queue.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleUserIds: Set<MiUser['id']> | null;
|
||||
|
||||
switch (this.note.visibility) {
|
||||
case 'public':
|
||||
case 'home':
|
||||
visibleUserIds = null;
|
||||
break;
|
||||
|
||||
case 'specified':
|
||||
visibleUserIds = new Set(this.note.visibleUserIds);
|
||||
break;
|
||||
|
||||
case 'followers': {
|
||||
// TODO: フォロワー限定ノートにフォロワーではない人がメンションされた場合通知されるのが正しい挙動なのか確認(一部に挙動の不一致がありそう)。現状は通知されるためフィルタしない
|
||||
// const targetUserIds = this.queue.map(x => x.target);
|
||||
// const followers = await this.followingsRepository.find({
|
||||
// where: {
|
||||
// followeeId: this.note.userId,
|
||||
// followerId: In(targetUserIds),
|
||||
// isFollowerHibernated: false,
|
||||
// },
|
||||
// select: ['followerId'],
|
||||
// });
|
||||
// visibleUserIds = new Set(followers.map(f => f.followerId));
|
||||
visibleUserIds = null;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
visibleUserIds = new Set();
|
||||
break;
|
||||
}
|
||||
|
||||
for (const x of this.queue.values()) {
|
||||
const isVisibleToTarget = visibleUserIds === null || visibleUserIds.has(x.target);
|
||||
|
||||
if (!isVisibleToTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x.reason === 'renote') {
|
||||
this.notificationService.createNotification(x.target, 'renote', {
|
||||
noteId: this.note.id,
|
||||
@@ -277,7 +321,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
// Fetch renote to note
|
||||
renote = await this.notesRepository.findOne({
|
||||
where: { id: data.renoteId },
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
relations: {
|
||||
user: true,
|
||||
renote: true,
|
||||
reply: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (renote == null) {
|
||||
@@ -326,14 +374,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
// Fetch reply
|
||||
reply = await this.notesRepository.findOne({
|
||||
where: { id: data.replyId },
|
||||
relations: ['user'],
|
||||
relations: { user: true },
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target');
|
||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||
throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote');
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) {
|
||||
} else if (!(await this.noteEntityService.isVisibleForMe(reply, user.id))) {
|
||||
throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target');
|
||||
} else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
|
||||
throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility');
|
||||
@@ -462,8 +510,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
throw new Error('Renote target is not public or home');
|
||||
}
|
||||
|
||||
// Renote対象がfollowersならfollowersにする
|
||||
data.visibility = 'followers';
|
||||
// followers noteはfollowers以下にrenote可能
|
||||
if (data.visibility === 'public' || data.visibility === 'home') {
|
||||
data.visibility = 'followers';
|
||||
}
|
||||
break;
|
||||
case 'specified':
|
||||
// specified / direct noteはreject
|
||||
@@ -528,7 +578,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens);
|
||||
|
||||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||
mentionedUsers = data.apMentions ?? (await this.extractMentionedUsers(user, combinedTokens));
|
||||
}
|
||||
|
||||
// if the host is media-silenced, custom emojis are not allowed
|
||||
@@ -772,7 +822,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj });
|
||||
|
||||
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
|
||||
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, this.followingsRepository, user, note);
|
||||
|
||||
await this.createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
@@ -1006,7 +1056,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
where: {
|
||||
followeeId: note.channelId,
|
||||
},
|
||||
select: ['followerId'],
|
||||
select: { followerId: true },
|
||||
});
|
||||
|
||||
for (const channelFollowing of channelFollowings) {
|
||||
@@ -1025,13 +1075,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
followerHost: IsNull(),
|
||||
isFollowerHibernated: false,
|
||||
},
|
||||
select: ['followerId', 'withReplies'],
|
||||
select: {
|
||||
followerId: true,
|
||||
withReplies: true,
|
||||
},
|
||||
}),
|
||||
this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: ['userListId', 'userListUserId', 'withReplies'],
|
||||
select: {
|
||||
userListId: true,
|
||||
userListUserId: true,
|
||||
withReplies: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1139,7 +1196,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
id: In(samples.map(x => x.followerId)),
|
||||
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
|
||||
},
|
||||
select: ['id'],
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (hibernatedUsers.length > 0) {
|
||||
|
||||
@@ -246,7 +246,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
|
||||
private toXListId(id: string): string {
|
||||
const { date, additional } = this.idService.parseFull(id);
|
||||
return date.toString() + '-' + additional.toString();
|
||||
// Redis Stream sequenceはunit64制約があるため、収まらない場合は下位64bitを取る
|
||||
return date.toString() + '-' + BigInt.asUintN(64, additional).toString();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -769,6 +769,18 @@ export class QueueService {
|
||||
await queue.promoteJobs();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queuePause(queueType: typeof QUEUE_TYPES[number]) {
|
||||
const queue = this.getQueue(queueType);
|
||||
await queue.pause();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueResume(queueType: typeof QUEUE_TYPES[number]) {
|
||||
const queue = this.getQueue(queueType);
|
||||
await queue.resume();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
|
||||
@@ -103,7 +103,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
relations: { user1: true, user2: true },
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
@@ -150,7 +150,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
relations: { user1: true, user2: true },
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
@@ -295,7 +295,12 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
noIrregularRules: options.noIrregularRules,
|
||||
}, { relations: ['user1', 'user2'] });
|
||||
}, {
|
||||
relations: {
|
||||
user1: true,
|
||||
user2: true,
|
||||
},
|
||||
});
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||
@@ -600,7 +605,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
} else {
|
||||
const game = await this.reversiGamesRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['user1', 'user2'],
|
||||
relations: { user1: true, user2: true },
|
||||
});
|
||||
if (game == null) return null;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user