mirror of
https://github.com/langgenius/dify.git
synced 2026-09-03 15:27:49 +08:00
fix(knowledge-fs): remove provider mode from sync policies and update related constraints
This commit is contained in:
parent
20fc6c130d
commit
ae495d3f97
@ -190,7 +190,7 @@ class KnowledgeFSOnlineDriveWorkflowImportItemPayload(BaseModel):
|
||||
|
||||
class KnowledgeFSInitialSyncPolicyPayload(BaseModel):
|
||||
custom_interval_seconds: int | None = Field(default=None, ge=3_600, le=2_592_000)
|
||||
sync_policy: Literal["provider", "daily", "manual", "custom"] = "provider"
|
||||
sync_policy: Literal["daily", "manual", "custom"] = "daily"
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@ -2113,7 +2113,7 @@ class KnowledgeFSSourceSyncPolicyResponse(ResponseModel):
|
||||
)
|
||||
id: str
|
||||
knowledge_space_id: str = Field(validation_alias=AliasChoices("knowledge_space_id", "knowledgeSpaceId"))
|
||||
mode: Literal["provider", "manual", "interval", "custom"]
|
||||
mode: Literal["manual", "interval", "custom"]
|
||||
next_run_at: datetime | None = Field(default=None, validation_alias=AliasChoices("next_run_at", "nextRunAt"))
|
||||
revision: int = Field(ge=1)
|
||||
source_id: str = Field(validation_alias=AliasChoices("source_id", "sourceId"))
|
||||
@ -2350,7 +2350,7 @@ class KnowledgeFSSourceSyncPolicyPayload(BaseModel):
|
||||
enabled: bool
|
||||
expected_revision: int = Field(ge=0, alias="expectedRevision")
|
||||
expected_source_version: int = Field(ge=1, alias="expectedSourceVersion")
|
||||
mode: Literal["provider", "manual", "interval", "custom"]
|
||||
mode: Literal["manual", "interval", "custom"]
|
||||
|
||||
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)
|
||||
|
||||
@ -2390,7 +2390,7 @@ class KnowledgeFSCrawlPreviewSelectionPayload(BaseModel):
|
||||
class KnowledgeFSDeferredSyncPolicyPayload(BaseModel):
|
||||
custom_interval_seconds: int | None = Field(default=None, ge=3_600, le=2_592_000, alias="customIntervalSeconds")
|
||||
enabled: bool
|
||||
mode: Literal["provider", "manual", "interval", "custom"]
|
||||
mode: Literal["manual", "interval", "custom"]
|
||||
|
||||
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)
|
||||
|
||||
|
||||
@ -339,12 +339,7 @@ def _sync_policy_payload(
|
||||
expectedRevision=expected_revision,
|
||||
expectedSourceVersion=source_version,
|
||||
)
|
||||
return KnowledgeFSSourceSyncPolicyPayload(
|
||||
enabled=True,
|
||||
mode="provider",
|
||||
expectedRevision=expected_revision,
|
||||
expectedSourceVersion=source_version,
|
||||
)
|
||||
raise ValueError(f"Unsupported sync policy: {payload.sync_policy}")
|
||||
|
||||
|
||||
def start_initial_source_import(
|
||||
|
||||
@ -252,7 +252,7 @@ class RecordingRemote:
|
||||
"expectedSourceVersion": 1,
|
||||
"id": "policy-1",
|
||||
"knowledgeSpaceId": "space-1",
|
||||
"mode": "provider",
|
||||
"mode": "interval",
|
||||
"revision": 1,
|
||||
"sourceId": "source-1",
|
||||
"updatedAt": "2030-01-01T00:00:00Z",
|
||||
@ -1089,7 +1089,7 @@ def test_basic_product_facade_resolves_control_space_then_uses_exact_kfs_routes(
|
||||
assert sources.data[0].sync_workflow.state == "syncing"
|
||||
assert sources.data[0].last_synced_at == datetime(2030, 1, 1, 1, tzinfo=UTC)
|
||||
assert sources.data[0].sync_policy is not None
|
||||
assert sources.data[0].sync_policy.mode == "provider"
|
||||
assert sources.data[0].sync_policy.mode == "interval"
|
||||
assert source.knowledge_space_id == "space-1"
|
||||
assert tasks.data == []
|
||||
assert traces.data == []
|
||||
|
||||
@ -279,7 +279,7 @@ def test_product_application_create_schedules_selected_website_import() -> None:
|
||||
"title": "Getting started",
|
||||
}
|
||||
],
|
||||
"sync_policy": "provider",
|
||||
"sync_policy": "daily",
|
||||
},
|
||||
)
|
||||
|
||||
@ -333,7 +333,7 @@ def test_product_application_create_schedules_selected_connector_import() -> Non
|
||||
"workspace_id": "workspace-1",
|
||||
}
|
||||
],
|
||||
"sync_policy": "provider",
|
||||
"sync_policy": "daily",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ def test_commit_online_document_import_uses_same_async_reconciliation() -> None:
|
||||
"workspaceId": "workspace-1",
|
||||
}
|
||||
],
|
||||
"syncPolicy": {"enabled": True, "mode": "provider"},
|
||||
"syncPolicy": {"enabled": True, "mode": "interval"},
|
||||
}
|
||||
).root
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ export const SourceSyncPolicyResponseSchema = z
|
||||
expectedSourceVersion: z.number().int().min(1),
|
||||
id: z.string().uuid(),
|
||||
knowledgeSpaceId: z.string().uuid(),
|
||||
mode: z.enum(["provider", "manual", "interval", "custom"]),
|
||||
mode: z.enum(["manual", "interval", "custom"]),
|
||||
nextRunAt: z.string().optional(),
|
||||
revision: z.number().int().min(1),
|
||||
sourceId: z.string().uuid(),
|
||||
|
||||
@ -1521,7 +1521,7 @@ describe("source handlers without optional collaborators", () => {
|
||||
expectedSourceVersion: 1,
|
||||
id: "00000000-0000-4000-8000-000000000111",
|
||||
knowledgeSpaceId: spaceId,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
permissionSnapshotId: "permission-1",
|
||||
permissionSnapshotRevision: 1,
|
||||
requestedBySubjectId: "u1",
|
||||
@ -1577,7 +1577,7 @@ describe("source handlers without optional collaborators", () => {
|
||||
status: "syncing",
|
||||
syncPolicy: {
|
||||
enabled: true,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
sourceId,
|
||||
},
|
||||
},
|
||||
|
||||
@ -511,7 +511,7 @@ describe("source-product handlers", () => {
|
||||
`/knowledge-spaces/${spaceId}/sources/${sourceId}/sync-policy`,
|
||||
);
|
||||
expect(policy.status).toBe(200);
|
||||
await expect(policy.json()).resolves.toMatchObject({ mode: "provider", revision: 1 });
|
||||
await expect(policy.json()).resolves.toMatchObject({ mode: "interval", revision: 1 });
|
||||
const missingPolicy = await app.request(
|
||||
`/knowledge-spaces/${spaceId}/sources/${sourceId}/sync-policy`,
|
||||
);
|
||||
@ -891,7 +891,7 @@ describe("source-product handlers", () => {
|
||||
enabled: true,
|
||||
expectedRevision: 1,
|
||||
expectedSourceVersion: 2,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
}),
|
||||
headers: { "content-type": "application/json" },
|
||||
method: "PUT",
|
||||
@ -968,7 +968,7 @@ describe("source-product handlers", () => {
|
||||
enabled: true,
|
||||
expectedRevision: 1,
|
||||
expectedSourceVersion: 2,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
}),
|
||||
headers: { "content-type": "application/json" },
|
||||
method: "PUT",
|
||||
@ -1101,8 +1101,8 @@ function syncPolicy(patch: Record<string, unknown> = {}) {
|
||||
expectedSourceVersion: 2,
|
||||
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c44",
|
||||
knowledgeSpaceId: spaceId,
|
||||
mode: "provider" as const,
|
||||
nextRunAt: "2026-07-14T13:00:00.000Z",
|
||||
mode: "interval" as const,
|
||||
nextRunAt: "2026-07-15T12:00:00.000Z",
|
||||
permissionSnapshotId: "permission-policy",
|
||||
permissionSnapshotRevision: 1,
|
||||
requestedBySubjectId: "editor-a",
|
||||
|
||||
@ -435,7 +435,7 @@ export const putSourceSyncPolicyRoute = createRoute({
|
||||
enabled: z.boolean(),
|
||||
expectedRevision: z.number().int().min(0),
|
||||
expectedSourceVersion: z.number().int().min(1),
|
||||
mode: z.enum(["provider", "manual", "interval", "custom"]),
|
||||
mode: z.enum(["manual", "interval", "custom"]),
|
||||
})
|
||||
.strict(),
|
||||
},
|
||||
|
||||
@ -1365,7 +1365,7 @@ function policyRecord(
|
||||
expectedSourceVersion: 1,
|
||||
id,
|
||||
knowledgeSpaceId,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
nextRunAt: createdAt,
|
||||
permissionSnapshotId: `permission-${id}`,
|
||||
permissionSnapshotRevision: 1,
|
||||
|
||||
@ -1036,7 +1036,7 @@ describe("source product workflow service boundaries", () => {
|
||||
expectedRevision: 0,
|
||||
expectedSourceVersion: 1,
|
||||
knowledgeSpaceId,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
sourceId: source.id,
|
||||
subject: editor,
|
||||
});
|
||||
@ -1067,7 +1067,7 @@ describe("source product workflow service boundaries", () => {
|
||||
enabled: true,
|
||||
expectedRevision: 1,
|
||||
expectedSourceVersion: 1,
|
||||
mode: "provider",
|
||||
mode: "interval",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "SOURCE_NOT_FOUND" });
|
||||
});
|
||||
@ -1100,7 +1100,7 @@ describe("source product workflow service boundaries", () => {
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
service.putSyncPolicy({ ...base, enabled: true, expectedSourceVersion: 2, mode: "provider" }),
|
||||
service.putSyncPolicy({ ...base, enabled: true, expectedSourceVersion: 2, mode: "interval" }),
|
||||
).rejects.toMatchObject({ code: "SOURCE_SYNC_POLICY_SOURCE_CONFLICT" });
|
||||
|
||||
for (const request of [
|
||||
@ -1116,11 +1116,11 @@ describe("source product workflow service boundaries", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const created = await service.putSyncPolicy({ ...base, enabled: true, mode: "provider" });
|
||||
const created = await service.putSyncPolicy({ ...base, enabled: true, mode: "interval" });
|
||||
expect(created).toMatchObject({
|
||||
createdAt: "2026-02-03T00:00:00.000Z",
|
||||
enabled: true,
|
||||
nextRunAt: "2026-02-03T01:00:00.000Z",
|
||||
nextRunAt: "2026-02-04T00:00:00.000Z",
|
||||
revision: 1,
|
||||
});
|
||||
await expect(
|
||||
@ -1437,9 +1437,6 @@ describe("source product workflow service boundaries", () => {
|
||||
});
|
||||
|
||||
it("computes supported policy intervals and rejects invalid scheduling anchors", () => {
|
||||
expect(nextSyncPolicyRunAt("provider", undefined, "2026-02-06T00:00:00.000Z")).toBe(
|
||||
"2026-02-06T01:00:00.000Z",
|
||||
);
|
||||
expect(nextSyncPolicyRunAt("interval", undefined, "2026-02-06T00:00:00.000Z")).toBe(
|
||||
"2026-02-07T00:00:00.000Z",
|
||||
);
|
||||
@ -1449,7 +1446,7 @@ describe("source product workflow service boundaries", () => {
|
||||
expect(() => nextSyncPolicyRunAt("manual", undefined, "2026-02-06T00:00:00.000Z")).toThrow(
|
||||
SourceWorkflowError,
|
||||
);
|
||||
expect(() => nextSyncPolicyRunAt("provider", undefined, "invalid")).toThrow(
|
||||
expect(() => nextSyncPolicyRunAt("interval", undefined, "invalid")).toThrow(
|
||||
SourceWorkflowError,
|
||||
);
|
||||
expect(() => nextSyncPolicyRunAt("custom", undefined, "2026-02-06T00:00:00.000Z")).toThrow(
|
||||
|
||||
@ -213,7 +213,7 @@ interface SourceSyncPolicyBase {
|
||||
readonly expectedSourceVersion: number;
|
||||
readonly id: string;
|
||||
readonly knowledgeSpaceId: string;
|
||||
readonly mode: "provider" | "manual" | "interval" | "custom";
|
||||
readonly mode: "manual" | "interval" | "custom";
|
||||
readonly nextRunAt?: string | undefined;
|
||||
readonly revision: number;
|
||||
readonly sourceId: string;
|
||||
@ -1329,7 +1329,7 @@ export function nextSyncPolicyRunAt(
|
||||
if (!Number.isFinite(timestamp) || mode === "manual") {
|
||||
throw new SourceWorkflowError("SOURCE_SYNC_POLICY_INVALID", "Sync policy anchor is invalid");
|
||||
}
|
||||
const seconds = mode === "custom" ? customIntervalSeconds : mode === "provider" ? 3_600 : 86_400;
|
||||
const seconds = mode === "custom" ? customIntervalSeconds : 86_400;
|
||||
if (!Number.isSafeInteger(seconds) || (seconds as number) < 1) {
|
||||
throw new SourceWorkflowError("SOURCE_SYNC_POLICY_INVALID", "Sync policy interval is invalid");
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
-- Knowledge Platform schema migration
|
||||
-- Migration id: 0046_remove_provider_sync_policy
|
||||
-- Dialect: postgres
|
||||
-- Historical provider policies must be converted to manual before this migration runs.
|
||||
|
||||
ALTER TABLE "source_sync_policies"
|
||||
DROP CONSTRAINT IF EXISTS "source_sync_policies_mode_ck";
|
||||
|
||||
ALTER TABLE "source_sync_policies"
|
||||
ADD CONSTRAINT "source_sync_policies_mode_ck"
|
||||
CHECK ("mode" IN ('manual', 'interval', 'custom'));
|
||||
@ -0,0 +1,24 @@
|
||||
-- Knowledge Platform schema migration
|
||||
-- Migration id: 0046_remove_provider_sync_policy
|
||||
-- Dialect: tidb
|
||||
-- Historical provider policies must be converted to manual before this migration runs.
|
||||
|
||||
SET @kfs_0046_source_sync_policies_mode_ck_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.tidb_check_constraints
|
||||
WHERE constraint_schema = DATABASE()
|
||||
AND table_name = 'source_sync_policies'
|
||||
AND constraint_name = 'source_sync_policies_mode_ck'
|
||||
);
|
||||
SET @kfs_0046_source_sync_policies_mode_ck_drop_sql = IF(
|
||||
@kfs_0046_source_sync_policies_mode_ck_exists > 0,
|
||||
'ALTER TABLE `source_sync_policies` DROP CONSTRAINT `source_sync_policies_mode_ck`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt
|
||||
FROM @kfs_0046_source_sync_policies_mode_ck_drop_sql;
|
||||
EXECUTE kfs_0046_source_sync_policies_mode_ck_drop_stmt;
|
||||
DEALLOCATE PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt;
|
||||
|
||||
ALTER TABLE `source_sync_policies`
|
||||
ADD CONSTRAINT `source_sync_policies_mode_ck`
|
||||
CHECK (`mode` IN ('manual', 'interval', 'custom'));
|
||||
@ -93,4 +93,6 @@ export const migrationArtifacts = [
|
||||
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0044_document_semantic_window_checkpoints\n-- Dialect: tidb\n-- Persists validated semantic-window model responses for generation-scoped retry recovery.\n\nCREATE TABLE IF NOT EXISTS `document_semantic_window_checkpoints` (\n `tenant_id` VARCHAR(255) NOT NULL,\n `knowledge_space_id` CHAR(36) NOT NULL,\n `document_asset_id` CHAR(36) NOT NULL,\n `document_version` INT NOT NULL,\n `publication_generation_id` CHAR(36) NOT NULL,\n `window_id` VARCHAR(128) NOT NULL,\n `input_fingerprint` VARCHAR(71) NOT NULL,\n `model_fingerprint` VARCHAR(71) NOT NULL,\n `response_text` MEDIUMTEXT NOT NULL,\n `completion` JSON NOT NULL,\n `created_at` DATETIME(3) NOT NULL,\n PRIMARY KEY (\n `tenant_id`, `knowledge_space_id`, `publication_generation_id`,\n `window_id`, `input_fingerprint`\n ),\n CONSTRAINT `document_semantic_window_checkpoints_scope_fk`\n FOREIGN KEY (`tenant_id`, `knowledge_space_id`)\n REFERENCES `knowledge_spaces` (`tenant_id`, `id`) ON DELETE CASCADE,\n CONSTRAINT `document_semantic_window_checkpoints_asset_fk`\n FOREIGN KEY (`knowledge_space_id`, `document_asset_id`, `document_version`)\n REFERENCES `document_assets` (`knowledge_space_id`, `id`, `version`) ON DELETE CASCADE,\n CONSTRAINT `document_semantic_window_checkpoints_fingerprint_ck`\n CHECK (\n `input_fingerprint` REGEXP '^sha256:[a-f0-9]{64}$'\n AND `model_fingerprint` REGEXP '^sha256:[a-f0-9]{64}$'\n ),\n CONSTRAINT `document_semantic_window_checkpoints_completion_ck`\n CHECK (JSON_TYPE(`completion`) = 'OBJECT'),\n CONSTRAINT `document_semantic_window_checkpoints_version_ck`\n CHECK (`document_version` >= 1)\n);\n\nCREATE INDEX IF NOT EXISTS `document_semantic_window_checkpoints_asset_idx`\n ON `document_semantic_window_checkpoints` (\n `knowledge_space_id`, `document_asset_id`, `document_version`\n );\n", path: "packages/database/migrations/0044_document_semantic_window_checkpoints.tidb.sql" },
|
||||
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0045_quality_replay_match_policy\n-- Dialect: postgres\n-- Freezes each golden question's evidence match policy into durable quality replay items.\n\nALTER TABLE \"quality_replay_items\"\n ADD COLUMN IF NOT EXISTS \"match_policy\" VARCHAR(8);\n\nUPDATE \"quality_replay_items\"\nSET \"match_policy\" = 'all'\nWHERE \"match_policy\" IS NULL;\n\nALTER TABLE \"quality_replay_items\"\n ALTER COLUMN \"match_policy\" SET NOT NULL;\n\nALTER TABLE \"quality_replay_items\"\n DROP CONSTRAINT IF EXISTS \"quality_replay_items_match_policy_ck\";\n\nALTER TABLE \"quality_replay_items\"\n ADD CONSTRAINT \"quality_replay_items_match_policy_ck\"\n CHECK (\"match_policy\" IN ('all', 'any'));\n", path: "packages/database/migrations/0045_quality_replay_match_policy.postgres.sql" },
|
||||
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0045_quality_replay_match_policy\n-- Dialect: tidb\n-- Freezes each golden question's evidence match policy into durable quality replay items.\n\nALTER TABLE `quality_replay_items`\n ADD COLUMN IF NOT EXISTS `match_policy` VARCHAR(8);\n\nUPDATE `quality_replay_items`\nSET `match_policy` = 'all'\nWHERE `match_policy` IS NULL;\n\nALTER TABLE `quality_replay_items`\n MODIFY COLUMN `match_policy` VARCHAR(8) NOT NULL;\n\nSET @qr_0045_match_policy_ck_exists = (\n SELECT COUNT(*) FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'quality_replay_items'\n AND constraint_name = 'quality_replay_items_match_policy_ck'\n);\nSET @qr_0045_match_policy_ck_drop_sql = IF(\n @qr_0045_match_policy_ck_exists > 0,\n 'ALTER TABLE `quality_replay_items` DROP CONSTRAINT `quality_replay_items_match_policy_ck`',\n 'SELECT 1'\n);\nPREPARE qr_0045_match_policy_ck_drop_stmt FROM @qr_0045_match_policy_ck_drop_sql;\nEXECUTE qr_0045_match_policy_ck_drop_stmt;\nDEALLOCATE PREPARE qr_0045_match_policy_ck_drop_stmt;\n\nALTER TABLE `quality_replay_items`\n ADD CONSTRAINT `quality_replay_items_match_policy_ck`\n CHECK (`match_policy` IN ('all', 'any'));\n", path: "packages/database/migrations/0045_quality_replay_match_policy.tidb.sql" },
|
||||
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0046_remove_provider_sync_policy\n-- Dialect: postgres\n-- Historical provider policies must be converted to manual before this migration runs.\n\nALTER TABLE \"source_sync_policies\"\n DROP CONSTRAINT IF EXISTS \"source_sync_policies_mode_ck\";\n\nALTER TABLE \"source_sync_policies\"\n ADD CONSTRAINT \"source_sync_policies_mode_ck\"\n CHECK (\"mode\" IN ('manual', 'interval', 'custom'));\n", path: "packages/database/migrations/0046_remove_provider_sync_policy.postgres.sql" },
|
||||
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0046_remove_provider_sync_policy\n-- Dialect: tidb\n-- Historical provider policies must be converted to manual before this migration runs.\n\nSET @kfs_0046_source_sync_policies_mode_ck_exists = (\n SELECT COUNT(*) FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'source_sync_policies'\n AND constraint_name = 'source_sync_policies_mode_ck'\n);\nSET @kfs_0046_source_sync_policies_mode_ck_drop_sql = IF(\n @kfs_0046_source_sync_policies_mode_ck_exists > 0,\n 'ALTER TABLE `source_sync_policies` DROP CONSTRAINT `source_sync_policies_mode_ck`',\n 'SELECT 1'\n);\nPREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt\n FROM @kfs_0046_source_sync_policies_mode_ck_drop_sql;\nEXECUTE kfs_0046_source_sync_policies_mode_ck_drop_stmt;\nDEALLOCATE PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt;\n\nALTER TABLE `source_sync_policies`\n ADD CONSTRAINT `source_sync_policies_mode_ck`\n CHECK (`mode` IN ('manual', 'interval', 'custom'));\n", path: "packages/database/migrations/0046_remove_provider_sync_policy.tidb.sql" },
|
||||
] as const satisfies readonly MigrationArtifact[];
|
||||
|
||||
@ -1363,8 +1363,8 @@ const tables = [
|
||||
checkConstraints: [
|
||||
{
|
||||
expression: {
|
||||
postgres: "\"mode\" IN ('provider', 'manual', 'interval', 'custom')",
|
||||
tidb: "`mode` IN ('provider', 'manual', 'interval', 'custom')",
|
||||
postgres: "\"mode\" IN ('manual', 'interval', 'custom')",
|
||||
tidb: "`mode` IN ('manual', 'interval', 'custom')",
|
||||
},
|
||||
name: "source_sync_policies_mode_ck",
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user