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
19 changes: 14 additions & 5 deletions deploy/preview/deploy-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,34 @@ deploy() {

# The policy itself contains no endpoint or credential, but a configured
# transport without a policy would otherwise silently become the disabled
# "[]" configuration below. Flatten formatted JSON before writing the
# Compose dotenv file and fail before replacing a preview with rule-only AI.
# "[]" configuration below. Flatten formatted JSON before handing it to
# Compose and fail before replacing a preview with rule-only AI.
ai_providers_json="$(printf '%s' "${ANGUI_AI_PROVIDERS_JSON:-}" | tr -d '\r\n')"
ai_policy_without_whitespace="$(printf '%s' "${ai_providers_json}" | tr -d '[:space:]')"
if { [[ -z "${ai_policy_without_whitespace}" ]] || [[ "${ai_policy_without_whitespace}" == "[]" ]]; } \
&& { [[ -n "${ANGUI_PREVIEW_AI_ENDPOINT:-}" ]] || [[ -n "${ANGUI_PREVIEW_AI_KEY:-}" ]]; }; then
die "ANGUI_AI_PROVIDERS_JSON is required when ANGUI_PREVIEW_AI_ENDPOINT or ANGUI_PREVIEW_AI_KEY is set"
fi

# Keep AI values out of the Compose dotenv file. A dotenv file is line based,
# so even a valid formatted JSON policy can be split into unrelated entries
# before Compose resolves the api environment. Process environment values take
# precedence over --env-file values during Compose interpolation.
export ANGUI_AI_PROVIDERS_JSON="${ai_providers_json:-[]}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
export ANGUI_PREVIEW_AI_ENDPOINT="${ANGUI_PREVIEW_AI_ENDPOINT:-}"
export ANGUI_PREVIEW_AI_KEY="${ANGUI_PREVIEW_AI_KEY:-}"

PREVIEW_DIR="${PREVIEW_ROOT}/${PREVIEW_ID}"
ensure_preview_dir
[[ -f "${PREVIEW_BACKEND_DIR}/angui" ]] || die "backend artifact angui is missing"
[[ -f "${PREVIEW_BACKEND_DIR}/angui-admin" ]] || die "backend artifact angui-admin is missing"
[[ -f "${PREVIEW_BACKEND_DIR}/migration" ]] || die "backend artifact migration is missing"
[[ -d "${PREVIEW_FRONTEND_DIR}" ]] || die "frontend artifact directory is missing"

if ! "${PREVIEW_BACKEND_DIR}/angui" validate-ai-config; then
die "ANGUI_AI_PROVIDERS_JSON failed application configuration validation"
fi

ensure_runtime_image
ensure_proxy
install -d -m 700 "${PREVIEW_DIR}/runtime"
Expand All @@ -105,9 +117,6 @@ PREVIEW_ORIGIN=${PREVIEW_ORIGIN}
PREVIEW_DEMO_PASSWORD=${PREVIEW_DEMO_PASSWORD}
PREVIEW_PROXY_NETWORK=${PREVIEW_PROXY_NETWORK:-angui-proxy}
AMAP_WEBSERVICE_KEY=${AMAP_WEBSERVICE_KEY:-}
ANGUI_AI_PROVIDERS_JSON=${ai_providers_json:-[]}
ANGUI_PREVIEW_AI_ENDPOINT=${ANGUI_PREVIEW_AI_ENDPOINT:-}
ANGUI_PREVIEW_AI_KEY=${ANGUI_PREVIEW_AI_KEY:-}
EOF

# A preview is a fresh, disposable environment. Removing its named SQLite
Expand Down
5 changes: 5 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
| `PATCH` | `/api/admin/users/{user_id}/status` | `200` | 管理员启用、停用或锁定账号 |
| `POST` | `/api/admin/archive-drafts/{draft_id}/deidentify` | `200` | 管理员记录归档草稿的人工脱敏确认或拒绝 |
| `PATCH` | `/api/admin/archive-drafts/{draft_id}/review` | `200` | 管理员发布、拒绝或撤回已脱敏归档草稿 |
| `GET` | `/api/admin/archive-drafts/{draft_id}/review-materials` | `200` | 管理员查看不可变 review material 版本链和当前 AI 输入版本 |
| `GET` | `/api/admin/archive-drafts/{draft_id}/review-materials/diff/{from_version}/{to_version}` | `200` | 管理员比较两个脱敏材料版本 |
| `POST` | `/api/admin/archive-drafts/{draft_id}/review-materials/{version}/restore` | `200` | 管理员从历史已脱敏版本创建新的恢复版本 |

除健康检查和登录外,所有接口都要求:

Expand Down Expand Up @@ -322,6 +325,8 @@ Example:
`POST /api/admin/archive-drafts/{draft_id}/deidentify` 只接受人工 `confirm` 或 `reject` 结果、长度受限的理由,以及在确认时提交的人工脱敏文本。确认会创建新的已脱敏材料版本,再以该版本作为唯一允许交给 AI 的输入,并把草稿转为 `pending_review` 和 `deidentified`;拒绝会转为 `rejected`,不能发布。每次结果记录操作者、时间、版本和理由长度,审计不保存理由原文。

`PATCH /api/admin/archive-drafts/{draft_id}/review` 只允许已确认脱敏的 `pending_review` 草稿 `publish` 或 `reject`,以及将 `published` 草稿 `withdraw`。发布仅把该受控版本标记为 `learning_resource`;当前不会创建知识问答/RAG 索引、导出、打印、公开读取或跨案件读取接口。撤回立即将用途恢复为 `internal_archive` 并标记 `withdrawn`,同时保留版本和审计历史;拒绝或撤回绝不修改或删除原案件。

管理员可通过 `GET /api/admin/archive-drafts/{draft_id}/review-materials` 查看不可变的 review material 版本链、来源范围、审核状态和当前 AI 输入版本;`GET .../review-materials/diff/{from_version}/{to_version}` 提供两个版本的增删行差异。`POST .../review-materials/{version}/restore` 只能恢复管理员已确认的 `deidentified` 版本,恢复操作会生成带 `parent_material_id` 的新版本、切换草稿的 AI 输入指针并将草稿重新置为 `pending_review`,不会修改任何历史版本。审计只记录版本、ID 和理由长度,不记录材料原文。
# Controlled AI draft endpoints

The following authenticated endpoints produce only drafts; none confirms a clue, publishes a summary, dispatches a task, or inserts material into a knowledge base automatically.
Expand Down
117 changes: 117 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,87 @@ paths:
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }

/api/admin/archive-drafts/{draft_id}/review-materials:
parameters:
- name: draft_id
in: path
required: true
schema: { type: string, format: uuid }
get:
tags: [Admin]
operationId: listArchiveReviewMaterials
summary: List immutable archive review-material versions
description: Admin capability only. Returns the version chain, source-scope metadata, review state, and the de-identified material currently selected as the AI input. Raw source records are never returned by this endpoint.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-global-capabilities: [admin]
x-data-classification: restricted-admin
responses:
"200": { description: Archive review-material versions, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/ArchiveReviewMaterial" } } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }

/api/admin/archive-drafts/{draft_id}/review-materials/diff/{from_version}/{to_version}:
parameters:
- name: draft_id
in: path
required: true
schema: { type: string, format: uuid }
- name: from_version
in: path
required: true
schema: { type: integer, minimum: 1 }
- name: to_version
in: path
required: true
schema: { type: integer, minimum: 1 }
get:
tags: [Admin]
operationId: diffArchiveReviewMaterials
summary: Compare two archive review-material versions
description: Admin capability only. Returns line-level additions and removals without mutating either immutable material version.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-global-capabilities: [admin]
x-data-classification: restricted-admin
responses:
"200": { description: Material version diff, content: { application/json: { schema: { $ref: "#/components/schemas/ArchiveReviewMaterialDiff" } } } }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/api/admin/archive-drafts/{draft_id}/review-materials/{version}/restore:
parameters:
- name: draft_id
in: path
required: true
schema: { type: string, format: uuid }
- name: version
in: path
required: true
schema: { type: integer, minimum: 1 }
post:
tags: [Admin]
operationId: restoreArchiveReviewMaterial
summary: Restore an approved material version as a new immutable version
description: Admin capability only. The selected de-identified version is copied into a new version with a parent link, the archive draft points to that new version, and the draft returns to pending_review. No historical row is mutated.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-global-capabilities: [admin]
x-data-classification: restricted-admin
requestBody:
required: true
content: { application/json: { schema: { $ref: "#/components/schemas/RestoreArchiveReviewMaterialRequest" } } }
responses:
"200": { description: Archive draft regenerated from the restored approved material, content: { application/json: { schema: { $ref: "#/components/schemas/ArchiveDraft" } } } }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }

components:
securitySchemes:
bearerAuth:
Expand Down Expand Up @@ -3225,6 +3306,42 @@ components:
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }

ArchiveReviewMaterial:
type: object
additionalProperties: false
required: [id, case_id, version, parent_material_id, content, source_scope, status, created_by_user_id, reviewed_by_user_id, reviewed_at, review_reason, created_at, selected_for_ai]
properties:
id: { type: string, format: uuid }
case_id: { type: string, format: uuid }
version: { type: integer, minimum: 1 }
parent_material_id: { type: [string, "null"], format: uuid }
content: { type: string, description: Administrator-visible de-identified review material or the initial controlled material awaiting de-identification. }
source_scope: { type: array, items: { type: string } }
status: { type: string, enum: [draft, deidentified, rejected] }
created_by_user_id: { type: string, format: uuid }
reviewed_by_user_id: { type: [string, "null"], format: uuid }
reviewed_at: { type: [string, "null"], format: date-time }
review_reason: { type: [string, "null"], description: Review reason is returned only within the administrator boundary. }
created_at: { type: string, format: date-time }
selected_for_ai: { type: boolean }

ArchiveReviewMaterialDiff:
type: object
additionalProperties: false
required: [from_version, to_version, added, removed]
properties:
from_version: { type: integer, minimum: 1 }
to_version: { type: integer, minimum: 1 }
added: { type: array, items: { type: string } }
removed: { type: array, items: { type: string } }

RestoreArchiveReviewMaterialRequest:
type: object
additionalProperties: false
required: [reason]
properties:
reason: { type: string, minLength: 1, maxLength: 1000 }

DeidentifyArchiveDraftRequest:
type: object
additionalProperties: false
Expand Down
58 changes: 58 additions & 0 deletions frontend/src/api/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,27 @@ export interface ArchiveDraft {
created_at: string;
updated_at: string;
}
export interface ArchiveReviewMaterial {
id: string;
case_id: string;
version: number;
parent_material_id: string | null;
content: string;
source_scope: string[];
status: "draft" | "deidentified" | "rejected";
created_by_user_id: string;
reviewed_by_user_id: string | null;
reviewed_at: string | null;
review_reason: string | null;
created_at: string;
selected_for_ai: boolean;
}
export interface ArchiveReviewMaterialDiff {
from_version: number;
to_version: number;
added: string[];
removed: string[];
}
export type TaskStatus =
| "pending_claim"
| "assigned"
Expand Down Expand Up @@ -904,6 +925,43 @@ export function reviewArchiveDraft(
);
}

export function listArchiveReviewMaterials(
token: string,
draftId: string,
): Promise<ArchiveReviewMaterial[]> {
return apiRequest<ArchiveReviewMaterial[]>(
`/admin/archive-drafts/${draftId}/review-materials`,
{},
token,
);
}

export function diffArchiveReviewMaterials(
token: string,
draftId: string,
fromVersion: number,
toVersion: number,
): Promise<ArchiveReviewMaterialDiff> {
return apiRequest<ArchiveReviewMaterialDiff>(
`/admin/archive-drafts/${draftId}/review-materials/diff/${fromVersion}/${toVersion}`,
{},
token,
);
}

export function restoreArchiveReviewMaterial(
token: string,
draftId: string,
version: number,
reason: string,
): Promise<ArchiveDraft> {
return apiRequest<ArchiveDraft>(
`/admin/archive-drafts/${draftId}/review-materials/${version}/restore`,
{ method: "POST", body: JSON.stringify({ reason }) },
token,
);
}

export function listCaseClues(
token: string,
caseId: string,
Expand Down
Loading
Loading