Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ var/
web/vite.config.js
web/vite.config.d.ts
*.log
.playwright-mcp/
register-create.png
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

ContentCloud 的重要变更记录在此文件中。

## [0.2.0] - 2026-07-26

### Added

- 增加 V2 本地工作区能力,覆盖来源登记、知识治理、Brief、创意批次、ScriptPackage、LocalRun、发布与拉取流程。
- 增加 V2 JSON Schema、CLI/MCP 命令、XLSX 导出能力及完整回归测试。
- 增加邀请注册流程,受邀用户可直接加入邀请方租户。
- 增加无 Docker 的 systemd 部署配置,支持独立运行 Server 与 Worker。

### Changed

- 更新 V2 产品路线、业务能力、领域模型、交付计划和实现状态文档。
- 统一 CLI 与 npm 工作区版本为 `0.2.0`,GitHub 发布标签为 `v0.2`。

### Fixed

- 移除核心迁移对 `pgcrypto` 扩展的无效依赖。
- 将邀请接受和邀请注册改为原子存储操作,避免并发重复兑换和部分写入。
- 加强本地工作区路径校验,阻止通过符号链接读取工作区外文件。
34 changes: 34 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# V2 单轨审批收敛实施计划

对应 `docs/roadmap/v2/03-domain-and-data-model.md` §2.1/§2.2 与 `14-implementation-status.md` 的 P0 缺口。

目标:把客户审批、导出、交付和结果绑定从 V1 `script_version` 收敛到 `SubmissionRevision` / `ApprovedSnapshot`,并补上 Brief 的策略血缘。

## Stage 1: Brief 策略血缘
**Goal**: `contracts/brief-2.0.schema.json` 已要求 `strategy_version_id`,让本地 Brief lint 与之一致。
**Success Criteria**: 缺少 `strategy_version_id` 的 Brief 被 lint 拒绝并给出稳定错误码。
**Tests**: `internal/localworkspace/script_test.go` 增加缺字段用例;既有用例补字段后仍通过。
**Status**: Complete
**Notes**: `LocalBrief.StrategyVersionID` 加入必填校验;新增 `BRIEF_STRATEGY_NOT_APPROVED` 校验其落在已 pull 的 strategy ApprovedSnapshot;新增 `domain.IsNotFound` helper 用于区分"缺对象"与真实故障。

## Stage 2: 客户审批改挂 SubmissionRevision
**Goal**: ReviewGrant 绑定具体 SubmissionRevision,内部/客户两阶段决定写入同一 revision,客户批准后生成 ApprovedSnapshot。
**Success Criteria**:
- `ApprovalDecision.DecisionStage` 区分 `internal` / `client`
- 客户批准前必须已有同一 revision 的 internal 批准
- 新 revision 出现后旧 grant 自动失效
- 客户批准生成 ApprovedSnapshot,hash 等于 revision content_hash
**Tests**: internal→client 正常路径、跳过 internal 被拒、grant 失效、OTP 错误、重复决定。
**Status**: Not Started

## Stage 3: 导出改由 ApprovedSnapshot 驱动
**Goal**: JSON/Markdown/XLSX 从批准快照的 canonical 内容生成,不再要求 V1 ScriptVersion。
**Success Criteria**: 三种格式由同一快照生成,manifest 记录 snapshot ID 与 revision hash。
**Tests**: 三格式导出内容一致性、未批准快照拒绝导出。
**Status**: Not Started

## Stage 4: 交付、结果与影子快照回填
**Goal**: DeliveryPackage 引用 ApprovedSnapshot;PerformanceObservation 绑定快照;V1 已批准 ScriptVersion 回填 `origin=v1_import` 只读影子快照。
**Success Criteria**: 回填后历史导出内容与 hash 不变;dry-run 报告数量与不可映射项。
**Tests**: 回填幂等、hash 不变、跨租户负测。
**Status**: Not Started
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ export CONTENTCLOUD_S3_SECRET_ACCESS_KEY='...'
./bin/contentcloud-server
```

Worker 使用相同数据库与对象存储配置,并要求 ClamAV:
Worker 使用相同数据库与对象存储配置。v0.2 默认不安装或调用 ClamAV;开放不可信文件上传前,可显式启用恶意文件扫描

```bash
CONTENTCLOUD_REQUIRE_MALWARE_SCAN=1 ./bin/contentcloud-worker
```

生产环境可按 [systemd 部署说明](deploy/systemd/README.md) 在无 Docker 模式下运行 Server 与 Worker,并由 Nginx 反向代理到 Server 的本地监听地址。

可用 `docker compose up --build` 启动 Server、Worker、PostgreSQL 和 MinIO 的完整本地拓扑。Compose 中的凭据只用于本机开发。

## 首次项目连接
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.2.0
54 changes: 54 additions & 0 deletions contracts/brief-2.0.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://contentcloud.goodvision.cn/schemas/brief-2.0.schema.json",
"title": "ContentCloud Marketing Video Brief 2.0",
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "status", "schema_version", "deliverability", "strategy_version_id", "campaign_id", "experiment_id", "channel", "objective", "audience", "scenario", "demand_moment", "pain_point", "primary_selling_point", "support_points", "positioning", "visualization_plan_ids", "asset_ids", "truth_strategy", "plan_b", "tone", "brand_rule_ids", "approved_claim_ids", "forbidden_claims", "hook_expectation", "narrative_constraints", "cta", "primary_variable", "controlled_variables", "measurement_window", "eligible_knowledge_ids", "blocked_knowledge_ids", "rights_ids", "risk_decision_ids", "duration_min_ms", "duration_max_ms", "aspect_ratio", "blocked_reasons", "missing_inputs"],
"properties": {
"id": {"type": "string", "minLength": 1},
"kind": {"const": "brief"},
"status": {"enum": ["candidate", "blocked"]},
"schema_version": {"const": "2.0"},
"deliverability": {"enum": ["blocked", "review_ready"]},
"strategy_version_id": {"type": "string", "minLength": 1},
"campaign_id": {"type": "string", "minLength": 1},
"experiment_id": {"type": "string", "minLength": 1},
"channel": {"type": "string", "minLength": 1},
"objective": {"type": "string", "minLength": 1},
"audience": {"type": "string", "minLength": 1},
"scenario": {"type": "string", "minLength": 1},
"demand_moment": {"type": "string", "minLength": 1},
"pain_point": {"type": "string", "minLength": 1},
"primary_selling_point": {"type": "string", "minLength": 1},
"support_points": {"type": "array", "maxItems": 3, "items": {"type": "string"}},
"positioning": {"type": "string", "minLength": 1},
"visualization_plan_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"asset_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"truth_strategy": {"type": "string", "minLength": 1},
"plan_b": {"type": "string", "minLength": 1},
"tone": {"type": "string", "minLength": 1},
"brand_rule_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"approved_claim_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"forbidden_claims": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"hook_expectation": {"type": "string", "minLength": 1},
"narrative_constraints": {"type": "array", "items": {"type": "string"}},
"cta": {"type": "string", "minLength": 1},
"primary_variable": {"enum": ["hook", "audience", "scenario", "visualization", "cta", "duration"]},
"controlled_variables": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"measurement_window": {"type": "string", "minLength": 1},
"eligible_knowledge_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"blocked_knowledge_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"rights_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"risk_decision_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"duration_min_ms": {"type": "integer", "minimum": 1000, "maximum": 600000},
"duration_max_ms": {"type": "integer", "minimum": 1000, "maximum": 600000},
"aspect_ratio": {"enum": ["9:16", "16:9", "1:1", "4:5"]},
"blocked_reasons": {"type": "array", "items": {"type": "string"}},
"missing_inputs": {"type": "array", "items": {"type": "string"}}
},
"allOf": [
{"if": {"properties": {"deliverability": {"const": "blocked"}}}, "then": {"properties": {"status": {"const": "blocked"}, "blocked_reasons": {"minItems": 1}}}},
{"if": {"properties": {"deliverability": {"const": "review_ready"}}}, "then": {"properties": {"status": {"const": "candidate"}, "eligible_knowledge_ids": {"minItems": 1}, "visualization_plan_ids": {"minItems": 1}, "blocked_reasons": {"maxItems": 0}, "missing_inputs": {"maxItems": 0}}}}
]
}
25 changes: 25 additions & 0 deletions contracts/creative-directions-2.0.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://contentcloud.goodvision.cn/schemas/creative-directions-2.0.schema.json",
"title": "ContentCloud Creative Directions 2.0",
"type": "array",
"minItems": 1,
"maxItems": 20,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "title", "angle", "hook_type", "visual_motif", "narrative", "tone", "target_emotion", "risk_refs", "status"],
"properties": {
"id": {"type": "string", "minLength": 1},
"title": {"type": "string", "minLength": 1},
"angle": {"type": "string", "minLength": 1},
"hook_type": {"type": "string", "minLength": 1},
"visual_motif": {"type": "string", "minLength": 1},
"narrative": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"tone": {"type": "string"},
"target_emotion": {"type": "string"},
"risk_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
"status": {"enum": ["candidate", "selected", "rejected"]}
}
}
}
9 changes: 9 additions & 0 deletions contracts/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import _ "embed"
//go:embed script-package-1.1.schema.json
var ScriptPackageSchema []byte

//go:embed script-package-2.0.schema.json
var ScriptPackageV2Schema []byte

//go:embed brief-2.0.schema.json
var BriefV2Schema []byte

//go:embed creative-directions-2.0.schema.json
var CreativeDirectionsV2Schema []byte

//go:embed task-contract-1.0.schema.json
var TaskContractSchema []byte

Expand Down
22 changes: 22 additions & 0 deletions contracts/embed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package contracts

import (
"encoding/json"
"testing"
)

func TestEmbeddedSchemasAreValidJSON(t *testing.T) {
for name, body := range map[string][]byte{
"knowledge-candidates-1.0": KnowledgeCandidatesSchema,
"brief-2.0": BriefV2Schema,
"creative-directions-2.0": CreativeDirectionsV2Schema,
"script-package-1.1": ScriptPackageSchema,
"script-package-2.0": ScriptPackageV2Schema,
"task-contract-1.0": TaskContractSchema,
} {
var schema map[string]any
if len(body) == 0 || json.Unmarshal(body, &schema) != nil || schema["$id"] == "" {
t.Fatalf("embedded schema %s is missing or invalid", name)
}
}
}
Loading
Loading