This commit is contained in:
2026-01-08 17:34:14 +09:00
parent 591edf61f8
commit bda6b9700d
5 changed files with 11 additions and 392 deletions

View File

@@ -1 +0,0 @@
.container{max-width:800px;margin:0 auto;padding:2rem}h1{font-size:2rem;margin-bottom:1.5rem;color:#333}.input-section{margin-bottom:2rem;display:flex;gap:.5rem}.at-uri-input{flex:1;padding:.75rem;font-size:1rem;border:2px solid #ddd;border-radius:4px;font-family:monospace}.at-uri-input:focus{outline:none;border-color:#06c}.input-section button{padding:.75rem 2rem;font-size:1rem;background:#06c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500}.input-section button:hover{background:#0052a3}.info-section{background:#fff;padding:1.5rem;border-radius:8px;margin-bottom:2rem;box-shadow:0 2px 4px #0000001a}.info-section h2{font-size:1.5rem;margin-bottom:1rem;color:#333}.info-section ul{list-style-position:inside;color:#666}.info-section li{margin-bottom:.5rem}.back-link{display:inline-block;color:#06c;text-decoration:none;font-weight:500}.back-link:hover{text-decoration:underline}.record-view{background:#fff;padding:2rem;border-radius:8px;margin-bottom:2rem;box-shadow:0 2px 4px #0000001a}.record-view h2{font-size:2rem;margin-bottom:1rem;color:#333}.record-meta{margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:1px solid #eee}.record-meta p{margin:.5rem 0;color:#666;font-size:.9rem;font-family:monospace}.record-content{line-height:1.8;color:#333}.record-content pre{white-space:pre-wrap;word-wrap:break-word;font-family:inherit;margin:0}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#f5f5f5}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.container{max-width:800px;margin:0 auto;padding:2rem}h1{font-size:2rem;margin-bottom:1.5rem;color:#333}.input-section{margin-bottom:2rem;display:flex;gap:.5rem}.at-uri-input{flex:1;padding:.75rem;font-size:1rem;border:2px solid #ddd;border-radius:4px;font-family:monospace}.at-uri-input:focus{outline:none;border-color:#06c}.input-section button{padding:.75rem 2rem;font-size:1rem;background:#06c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500}.input-section button:hover{background:#0052a3}.info-section{background:#fff;padding:1.5rem;border-radius:8px;margin-bottom:2rem;box-shadow:0 2px 4px #0000001a}.info-section h2{font-size:1.5rem;margin-bottom:1rem;color:#333}.info-section ul{list-style-position:inside;color:#666}.info-section li{margin-bottom:.5rem}.back-link{display:inline-block;color:#06c;text-decoration:none;font-weight:500}.back-link:hover{text-decoration:underline}.record-view{background:#fff;padding:2rem;border-radius:8px;margin-bottom:2rem;box-shadow:0 2px 4px #0000001a}.record-view h2{font-size:2rem;margin-bottom:1rem;color:#333}.record-meta{margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:1px solid #eee}.record-meta p{margin:.5rem 0;color:#666;font-size:.9rem;font-family:monospace}.record-content{line-height:1.8;color:#333}.record-content pre{white-space:pre-wrap;word-wrap:break-word;font-family:inherit;margin:0}.error-section{background:#fee;padding:1rem;border-radius:4px;margin-bottom:1rem;color:#c33}.records-list{background:#fff;padding:2rem;border-radius:8px;margin-bottom:2rem;box-shadow:0 2px 4px #0000001a}.records-list h2{font-size:1.5rem;margin-bottom:1rem;color:#333}.records-list ul{list-style:none;padding:0;margin:0}.records-list li{border-bottom:1px solid #eee}.records-list li:last-child{border-bottom:none}.record-link{display:flex;justify-content:space-between;align-items:center;width:100%;padding:1rem;background:none;border:none;cursor:pointer;text-align:left;transition:background .2s}.record-link:hover{background:#f5f5f5}.record-title{font-size:1.1rem;color:#06c;font-weight:500}.record-date{color:#666;font-size:.9rem}.back-button{padding:.5rem 1rem;margin-bottom:1rem;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.9rem;color:#666}.back-button:hover{background:#eee}.input-section button:disabled{background:#ccc;cursor:not-allowed}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#f5f5f5}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AT Browser</title>
<script type="module" crossorigin src="/at/assets/index-DrFpc8Xj.js"></script>
<link rel="stylesheet" crossorigin href="/at/assets/index-CvFXbZtL.css">
<script type="module" crossorigin src="/at/assets/index-CyrVFHrY.js"></script>
<link rel="stylesheet" crossorigin href="/at/assets/index-t2ajyYjt.css">
</head>
<body>
<div id="root"></div>

381
claude.md
View File

@@ -1,381 +0,0 @@
# ailog
## 概要
atprotoベースの静的ブログジェネレーター。markdownファイルをatproto recordとして保存し、それを元に静的サイトを生成する。
## Collection Schema
### ai.syui.log.post (ブログ記事)
```json
{
"lexicon": 1,
"id": "ai.syui.log.post",
"defs": {
"main": {
"type": "record",
"description": "Record containing a blog post.",
"key": "tid",
"record": {
"type": "object",
"required": ["title", "content", "createdAt"],
"properties": {
"title": {
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300,
"description": "The title of the post."
},
"content": {
"type": "string",
"maxLength": 1000000,
"maxGraphemes": 100000,
"description": "The content of the post."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this post was originally created."
}
}
}
}
}
}
```
### ai.syui.log.comment (コメント)
```json
{
"lexicon": 1,
"id": "ai.syui.log.comment",
"defs": {
"main": {
"type": "record",
"description": "Record containing a comment.",
"key": "tid",
"record": {
"type": "object",
"required": ["content", "createdAt", "post"],
"properties": {
"content": {
"type": "string",
"maxLength": 100000,
"maxGraphemes": 10000,
"description": "The content of the comment."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this comment was originally created."
},
"parent": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"post": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
}
}
}
}
}
}
```
## CLIコマンド仕様
### login - ログイン
```bash
ailog login ${handle} -p ${password} -s ${pds}
```
- handleからDID, PDSを解決
- 認証情報を`~/.config/syui/ai/log/config.json`に保存
### post - 記事をatprotoに投稿
```bash
ailog post
```
- `./content/post/*.md`を読み込む
- frontmatterからtitle, dateなどを抽出
- markdown本文をcontentに設定
- `ai.syui.log.post`としてputRecord
- 既存recordがあれば更新、なければ新規作成
### build - 静的サイト生成
```bash
ailog build
```
- atprotoから`ai.syui.log.post`のrecordを取得
- markdownをHTMLに変換
- 静的ファイルとして`./public`に出力
## URL構造
### 記事一覧
```
localhost/at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/ai.syui.log.post/
```
- atprotoから全recordを取得
- 一覧表示
### 個別記事
```
localhost/.../3mbnbdt4bas2a
```
- atproto URIからrecordを取得
- markdownをレンダリングして表示
## データフロー
### 記事投稿
```
./content/post/*.md
↓ (frontmatter + markdown解析)
ailog post
↓ (putRecord)
atproto PDS (ai.syui.log.post)
```
### 静的サイト生成
```
atproto PDS (ai.syui.log.post)
↓ (listRecords)
ailog build
↓ (markdown → HTML)
./public/*.html
```
## at browser統合
- atproto URIでの記事アクセス
- handle解決 → DID → PDS URLの取得
- NetworkConfig (pdsApi, bskyApi, plcApi) の割り当て
```rust
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
```
## 参考実装
- `./repos/log` - 既存のailog実装 (Rust)
- `./repos/frontpage` - frontpage (fyi.unravel.frontpage)
- `./repos/pfrazee.com` - Paul FrazeeのLeaflet実装 (TypeScript)
## Paul Frazeeの仕組み
pfrazee.comは以下の流れでLeafletをblogに統合
1. **lexicon schema取得**
```bash
lex install pub.leaflet.document
lex build --out ./util
```
2. **データ取得**
```typescript
// handle → DID → PDS
const did = await resolver.handle.resolve('pfrazee.com')
const pds = await resolver.did.resolveAtprotoData(did).pds
// listRecords
const result = await client.list(leaflet.document, {
repo: did,
limit: 50
})
```
3. **画像取得**
```typescript
for (const blobRef of enumBlobRefs(leaflet.value)) {
const blobRes = await client.getBlob(did, blobRef.ref)
await fsp.writeFile(imagePath, blobRes.payload.body)
}
```
4. **静的サイト生成** (Next.js)
- JSONからHTMLレンダリング
- 型安全なblock rendering
## 既存のai.syui.log形式
現在のchat形式
```json
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log/2025-06-14-blog",
"value": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"post": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"date": "",
"slug": "",
"tags": [],
"title": "syui.ai",
"language": "ja"
},
"text": "test",
"type": "comment",
"$type": "ai.syui.log",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://...",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T06:24:37.386Z"
}
}
```
## 設定ファイル
### ~/.config/syui/ai/log/config.json
```json
{
"pds": "syu.is",
"handle": "ai.syui.ai",
"did": "did:plc:6qyecktefllvenje24fcxnie",
"access_jwt": "...",
"refresh_jwt": "..."
}
```
### ~/.config/syui/ai/log/mapping.json
ファイル名とatproto recordの紐付け自動生成
```json
{
"2026-01-08-test.md": {
"rkey": "3mbnbdt4bas2a",
"uri": "at://did:plc:xxx/ai.syui.log.post/3mbnbdt4bas2a",
"cid": "bafyrei..."
}
}
```
**動作:**
- すべてのAPI呼び出し前に自動でrefreshSessionを実行してJWTトークンを更新
- `ailog post` - 初回投稿時にTID形式のrkeyが自動生成され、mappingに保存
- `ailog post` - 2回目以降は既存rkeyで更新putRecord
- `ailog delete` - 全削除時にmappingもクリア
## ディレクトリ構造
```
ailog/
├── content/
│ └── post/
│ ├── 2025-01-08-example.md
│ └── ...
├── public/
│ ├── index.html
│ ├── posts/
│ │ └── 2025-01-08-example.html
│ └── at/
│ └── ...
├── lexicons/
│ └── ai/
│ └── syui/
│ └── log/
│ ├── post.json
│ └── comment.json
└── templates/
├── index.html
├── post.html
└── layout.html
```
## 開発優先順位
1. CLIコマンド実装 ✅
- [x] collection schema定義
- [x] `ailog login` - handle → DID解決 + JWT保存
- [x] `ailog post` - ./content/post/*.md → putRecord
- [x] `ailog build` - listRecords → 静的HTML生成
2. at browser統合 (進行中)
- [x] React/TypeScript環境セットアップ
- [x] atproto client library (repos/log/pdsベース)
- [ ] at browser components
- [ ] 静的サイトへの統合
- [ ] ai.syui.log.post 表示スタイル
3. スタイリング
- [ ] Tailwind CSS統合
- [ ] repos/log風デザイン
## 実装済み
### Rust CLI (src/)
- `src/main.rs` - CLIエントリーポイント
- `src/config.rs` - ~/.config/syui/ai/log/config.json管理
- `src/login.rs` - atproto認証 (describeRepo + createSession)
- `src/refresh.rs` - セッションリフレッシュ (refreshSession)
- `src/post.rs` - markdown投稿 (createRecord/putRecord)
- `src/build.rs` - 静的サイト生成 (listRecords + markdown→HTML)
- `src/delete.rs` - record削除 (deleteRecord)
### at browser (browser/)
- `browser/package.json` - React + TypeScript + Tailwind
- 実装予定: repos/log/pdsの実装をベースに作成
## 使用方法
```bash
# ログイン
ailog l ai.syui.ai -p PASSWORD -s syu.is
# 記事投稿(初回: 新規作成、2回目以降: 更新)
ailog p
# サイト生成
ailog b
# 全削除
ailog d
```
**エイリアス:**
- `l` = `login`
- `p` = `post`
- `b` = `build`
- `d` = `delete`