chore: add c4 spec

This commit is contained in:
2026-06-06 19:22:58 +08:00
commit ca9f1242f8
11 changed files with 2490 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
.DS_Store
+35
View File
@@ -0,0 +1,35 @@
# Curriculum Project Hub
教研项目工作台的架构仓库。
当前阶段先用 LikeC4 把产品边界、系统职责、核心领域模型和关键运行时流程敲定,再进入实现。
## Architecture
LikeC4 model:
- [docs/architecture/likec4/specification.c4](/Users/jiarong/local/paradigm/curriculum-project-hub/docs/architecture/likec4/specification.c4)
- [docs/architecture/likec4/model.c4](/Users/jiarong/local/paradigm/curriculum-project-hub/docs/architecture/likec4/model.c4)
- [docs/architecture/likec4/views.c4](/Users/jiarong/local/paradigm/curriculum-project-hub/docs/architecture/likec4/views.c4)
Commands:
```sh
npm install
npm run dev
npm run validate
npm run build
```
Key views to review first:
- `index`: system context
- `containers`: target containers and external systems
- `permissions`: Feishu Docs-like permission model
- `domain_model`: Project / ProjectGroupChat / AgentSession / AgentRun / ProjectAgentLock
- `run_lifecycle`: `@Claude` creates a run and holds a project lock
- `context_access`: Claude reads Feishu history on demand through MCP
## Decisions
Architecture decisions live in [docs/architecture/adr](/Users/jiarong/local/paradigm/curriculum-project-hub/docs/architecture/adr).
@@ -0,0 +1,29 @@
# ADR 0001: Feishu Project Group Is The Collaboration Space
## Status
Accepted.
## Context
Teachers need to work on multiple curriculum projects at the same time. The previous mental model tied a teacher to one active project session, which makes multi-open workflows awkward and turns "exit project" into a confusing concept.
The revised product model is:
- one project has one long-lived Feishu project group;
- teachers discuss freely in the group;
- Claude does not process every group message;
- a teacher explicitly `@Claude` when Claude should join the current context and do work.
## Decision
Treat the project group as a long-lived collaboration space, not as a lock owner and not as a temporary processing session.
The group can stay open while no Claude processing is active. A teacher leaving or muting a group is separate from project permissions and from Claude processing lifecycle.
## Consequences
- Multi-open is natural: a teacher participates in multiple project groups.
- Normal group discussion does not occupy the project.
- "Exit project" should not be the normal action in the group workflow.
- Project rebinding or group archival needs explicit product rules, separate from run cancellation.
@@ -0,0 +1,30 @@
# ADR 0002: Project Lock Is Owned By AgentRun
## Status
Accepted.
## Context
The older model effectively made a long-lived teacher/project session hold the project lock. That breaks down once project groups become long-lived collaboration spaces and Claude is invoked on demand.
We need to prevent concurrent Claude executions from modifying the same project, while still allowing teachers to keep discussing the project in the group.
## Decision
Use a project-scoped lock whose owner is the current `AgentRun`.
```text
scope = project_id
owner = run_id
lifetime = one Claude processing task
```
The lock is acquired after `@Claude` creates an `AgentRun`, and released when the run completes, fails, times out or is canceled.
## Consequences
- Group membership, teacher identity and Claude provider session do not directly own the lock.
- The same long-lived `AgentSession` can be reused by many runs.
- `cancel current processing` releases the lock by `run_id`.
- Admin `force release` remains an exceptional operation for stuck runs or lost processes.
@@ -0,0 +1,29 @@
# ADR 0003: Read Feishu Context On Demand
## Status
Proposed.
## Context
The system needs to give Claude enough context when a teacher invokes it from a project group. Storing full Feishu group history in the Hub adds retention and privacy concerns, while fixed time windows such as "recent 24 hours" are brittle.
Feishu provides message history APIs, so the Hub can keep stable anchors and read nearby context when needed.
## Decision
For the first architecture, the Hub stores minimal anchors and project memory instead of full Feishu message history:
- chat id and project binding;
- trigger message id;
- run id and status card id;
- reply/thread anchors;
- project memory, decisions and artifact references.
Claude gets seed context from the current trigger message, project memory and recent anchors. If more context is needed, Claude calls MCP tools to read current message, reply chain, thread history or recent group messages through Feishu APIs.
## Consequences
- The Hub avoids becoming a full chat history store.
- Search over old vague discussions is limited until we add summary or indexing.
- MCP tools must authorize by run/project context; Claude cannot pass arbitrary chat ids.
@@ -0,0 +1,54 @@
# ADR 0004: Use Feishu Docs-like Permission Grants
## Status
Proposed.
## Context
Project collaboration needs more nuance than a hard-coded `owner / teacher / admin` model. Teachers may need to view a project, discuss it, trigger Claude, edit artifacts, manage members, or handle exceptional cancellation. These are related but not identical privileges.
Feishu cloud docs separates collaborators from operation settings:
- collaborators can be users, groups, departments, user groups, apps, or wiki members;
- collaborator roles are read, edit, or manage;
- settings separately control sharing outside the organization, copying/downloading, commenting, and collaborator management.
That split maps well to Curriculum Project Hub.
## Decision
Model project permissions as:
```text
PermissionGrant
- resource_type: project | artifact | project_group
- resource_id
- principal_type: user | feishu_chat | department | user_group | app
- principal_id
- role: read | edit | manage
PermissionSettings
- resource_type
- resource_id
- external_share_policy
- comment_policy
- copy_download_policy
- collaborator_management_policy
- agent_trigger_policy
- agent_cancel_policy
```
Use role-derived capabilities for ordinary actions, and settings for policy knobs:
- `read`: view project, artifacts, run history and project group metadata.
- `edit`: read plus discuss, comment, edit artifacts, trigger Claude runs and answer Claude choice cards.
- `manage`: edit plus manage collaborators, project settings, group binding and normal run cancellation policy.
- admin-only override: force release stuck locks and perform exceptional audit operations.
## Consequences
- We can grant access to a teacher, a whole Feishu project group, a department, or a custom user group using the same model.
- Claude invocation is explicit policy, not just "anyone in the group can run it" forever.
- "Who can cancel current processing" can be different from "who can force release a stuck lock".
- The model stays close to Feishu users' expectations without copying every Feishu-specific API detail.
+126
View File
@@ -0,0 +1,126 @@
model {
teacher = actor 'Teacher' {
description '教研老师。可以同时参与多个项目群,在群里讨论并在需要时 @Claude。'
}
admin = actor 'Admin' {
description '管理员。管理项目、权限、异常锁释放和审计。'
}
feishu = externalSystem 'Feishu / Lark' {
description '项目群、消息、卡片交互和历史消息 API。'
}
claude = externalSystem 'Claude / Claude Code' {
description '执行课程资料生成、修改和问答的 AI agent。'
}
workspace = externalSystem 'Project Workspace' {
description '每个课程项目的文件、素材、课件、教案和生成产物所在工作区。'
}
hub = system 'Curriculum Project Hub' {
description '教研项目工作台。把项目、飞书群、Claude run、锁、上下文和产物管理成一个明确的协作系统。'
web = app 'Project Console' {
description '后台/工作台 UI,用于项目管理、权限、run 审计、异常取消和强制释放锁。'
}
api = service 'Hub API' {
description '对 UI、飞书回调、内部 agent runtime 暴露业务 API。'
}
project = service 'Project Service' {
description '管理 Project、成员、项目群绑定、项目状态和产物引用。'
}
permission = service 'Permission Service' {
description '飞书云文档式权限层。按 resource + principal + role 授权,并用 permission settings 控制分享、评论、下载、协作者管理和 Claude 调用。'
}
im = adapter 'IM Bridge Adapter' {
description '飞书/Lark 适配层。负责事件去重、卡片、消息投递、按钮回调和限流;优先评估复用 Claude-to-IM 的 adapter/delivery 能力。'
}
run = service 'Run Orchestrator' {
description '在 @Claude 时创建 AgentRun,申请项目锁,调度 Claude runtime,处理取消、失败、等待用户和完成。'
}
locks = service 'Project Lock Service' {
description '统一项目级锁。scope = project_idowner = run_idlifetime = 一次 Claude run。'
}
context = service 'Context Service' {
description '提供当前触发消息、回复链、项目记忆、产物上下文和按需历史读取。第一版不长期缓存飞书全文。'
}
mcp = service 'MCP Gateway' {
description 'Claude 可调用的内部工具入口,例如读取当前项目群历史、触发消息、附件和产物上下文。'
}
runtime = runtime 'Claude Runtime' {
description '调用 Claude Code / SDK / wrapper,维护 provider session resume,并执行具体任务。'
}
db = store 'Application Database' {
description '项目、群绑定、AgentSession、AgentRun、锁、审计和轻量索引。'
projectEntity = entity 'Project'
groupEntity = entity 'ProjectGroupChat'
sessionEntity = entity 'AgentSession'
runEntity = entity 'AgentRun'
lockEntity = entity 'ProjectAgentLock'
permissionEntity = entity 'PermissionGrant'
settingsEntity = entity 'PermissionSettings'
memoryEntity = entity 'ProjectMemory / Decisions'
auditEntity = entity 'AuditLog'
}
web -> api 'manage projects, runs and locks'
api -> project 'project commands'
api -> permission 'authorize user actions'
api -> run 'trigger/cancel/admin actions'
api -> db 'read/write application state'
im -> api 'Feishu events and card callbacks'
im -> feishu 'send messages, cards, updates'
im -> project 'bind chat to project'
im -> permission 'check card/action permissions'
im -> run 'create/cancel run on @Claude or card action'
project -> db 'persist projects and group bindings'
permission -> db 'persist grants and settings'
run -> locks 'acquire/release project lock'
run -> permission 'authorize trigger/cancel/force release'
locks -> db 'persist lock owner = run_id'
run -> context 'build seed context'
run -> runtime 'execute Claude turn'
run -> db 'persist AgentRun state'
runtime -> claude 'resume/call Claude'
runtime -> workspace 'read/write project files'
runtime -> mcp 'request project context'
mcp -> context 'resolve run-scoped context'
context -> feishu 'read messages by chat/thread/message id'
context -> db 'read project memory and anchors'
projectEntity -> groupEntity 'has project group'
projectEntity -> permissionEntity 'has collaborators'
projectEntity -> settingsEntity 'has operation settings'
projectEntity -> sessionEntity 'has long-lived Claude session'
projectEntity -> runEntity 'has many runs'
projectEntity -> lockEntity 'has current lock'
permissionEntity -> settingsEntity 'is constrained by'
runEntity -> lockEntity 'owns while running'
sessionEntity -> runEntity 'is reused by runs'
projectEntity -> memoryEntity 'stores summary and decisions'
runEntity -> auditEntity 'records lifecycle events'
}
teacher -> feishu 'discusses in project groups'
teacher -> hub.im '@Claude and card actions'
teacher -> hub.web 'reviews projects and runs'
admin -> hub.web 'administers locks, permissions and audit'
feishu -> hub.im 'message events and callbacks'
}
+80
View File
@@ -0,0 +1,80 @@
specification {
element actor {
style {
shape person
color indigo
}
}
element system {
style {
color primary
}
}
element externalSystem {
style {
color gray
}
}
element app {
style {
shape browser
color green
}
}
element service {
style {
shape component
color primary
}
}
element adapter {
style {
shape component
color amber
}
}
element runtime {
style {
shape component
color secondary
}
}
element store {
style {
shape storage
color muted
}
}
element entity {
style {
shape storage
color muted
}
}
relationship sync {
line solid
}
relationship async {
line dotted
}
relationship reads {
line dashed
color gray
}
relationship writes {
line solid
color gray
}
}
+103
View File
@@ -0,0 +1,103 @@
views {
view index {
title 'System Context'
description '''
Curriculum Project Hub treats a Feishu group as the long-lived project collaboration space.
Claude joins only when teachers @Claude, and the Hub creates one AgentRun for that work.
'''
include teacher, admin, hub, feishu, claude, workspace
}
view containers of hub {
title 'Target Containers'
description 'Main runtime containers and external integrations for the target architecture.'
include *
include teacher, admin, feishu, claude, workspace
}
view permissions of hub {
title 'Permission Model'
description '''
Permissions follow the Feishu Docs pattern: collaborator grants are separate from operation settings.
Grants are resource + principal + role; settings decide who may share, comment, copy/download, manage collaborators and invoke Claude.
'''
include permission, project, im, run, db
include db.projectEntity, db.groupEntity, db.permissionEntity, db.settingsEntity, db.auditEntity
include teacher, admin, feishu
}
view domain_model of hub.db {
title 'Core Domain Model'
description '''
The main correction from the old demo model: project groups and Claude sessions are long-lived,
but project locks are owned by AgentRun, not by teacher, chat or session.
'''
include *
}
dynamic view run_lifecycle {
title '@Claude Run Lifecycle'
description 'A teacher @Claude message creates one AgentRun, which owns the project lock until completion, cancellation or timeout.'
teacher -> feishu 'send @Claude in project group'
feishu -> hub.im 'message event'
hub.im -> hub.project 'resolve chat binding'
hub.im -> hub.permission 'check trigger permission'
hub.im -> hub.run 'create AgentRun'
hub.run -> hub.locks 'acquire project lock with run_id'
hub.run -> hub.context 'build seed context'
hub.run -> hub.runtime 'execute Claude turn'
hub.runtime -> claude 'resume/call Claude'
hub.runtime -> workspace 'read/write project files'
hub.runtime -> hub.mcp 'request extra context if needed'
hub.mcp -> hub.context 'resolve run-scoped tools'
hub.context -> feishu 'read history by message/thread/chat'
hub.runtime -> hub.run 'return result'
hub.run -> hub.im 'publish status/result card'
hub.im -> feishu 'send/update card'
hub.run -> hub.locks 'release run-owned lock'
include teacher, feishu, claude, workspace
}
dynamic view cancel_lifecycle {
title 'Cancel Current Processing'
description 'A permitted teacher can cancel the current active run from card action or @Claude cancel command.'
teacher -> feishu 'click cancel or @Claude cancel'
feishu -> hub.im 'callback/event'
hub.im -> hub.project 'resolve project group binding'
hub.im -> hub.permission 'check cancel permission'
hub.im -> hub.run 'cancel active run'
hub.run -> hub.runtime 'abort controller if local process is alive'
hub.run -> hub.locks 'release lock by run_id'
hub.run -> hub.db 'mark AgentRun canceled and audit actor'
hub.run -> hub.im 'update status card'
hub.im -> feishu 'processing canceled'
include teacher, feishu
}
dynamic view context_access {
title 'On-demand Context Access'
description '''
The Hub stores anchors and memory, not full Feishu history. Claude can call MCP tools
to fetch the current trigger message, reply chain, thread history and recent group messages.
'''
hub.runtime -> hub.mcp 'call get_current_message / get_thread_context'
hub.mcp -> hub.context 'authorize by run_id and project_id'
hub.context -> hub.db 'read anchors, memory and decisions'
hub.context -> feishu 'fetch message/thread/chat history'
feishu -> hub.context 'history page'
hub.context -> hub.mcp 'filtered context'
hub.mcp -> hub.runtime 'tool result'
hub.runtime -> claude 'continue with retrieved context'
include feishu, claude
}
}
+1984
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "curriculum-project-hub",
"version": "0.1.0",
"private": true,
"description": "Architecture model for Curriculum Project Hub.",
"scripts": {
"dev": "likec4 serve docs/architecture/likec4",
"build": "likec4 build docs/architecture/likec4 -o dist/architecture --base ./ --title \"Curriculum Project Hub Architecture\"",
"validate": "likec4 validate docs/architecture/likec4",
"format": "likec4 format docs/architecture/likec4",
"export:json": "likec4 export json docs/architecture/likec4 -o dist/architecture/model.json",
"export:png": "likec4 export png docs/architecture/likec4 -o dist/architecture/png"
},
"devDependencies": {
"likec4": "^1.58.0"
}
}