mirror of
https://github.com/certd/certd.git
synced 2026-08-03 12:05:06 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e0bb51ce | ||
|
|
1dc5a19c45 | ||
|
|
04aa9041e8 | ||
|
|
0d86c84b28 | ||
|
|
6605669113 | ||
|
|
4b8747b5da | ||
|
|
2ba4132c50 | ||
|
|
49dc2796cd | ||
|
|
1e07d69932 | ||
|
|
aabf73a736 | ||
|
|
c9be2293ab |
@@ -0,0 +1,133 @@
|
|||||||
|
```markdown
|
||||||
|
# certd Development Patterns
|
||||||
|
|
||||||
|
> Auto-generated skill from repository analysis
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
- **CamelCase** is used for file names.
|
||||||
|
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
|
||||||
|
|
||||||
|
### Import Style
|
||||||
|
|
||||||
|
- **Absolute imports** are preferred.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Style
|
||||||
|
|
||||||
|
- **Named exports** are used throughout the codebase.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
export function getUserPreferences() { ... }
|
||||||
|
export const PREFERENCE_KEYS = [ ... ];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Patterns
|
||||||
|
|
||||||
|
- **Conventional commits** are used, with the `feat` prefix for new features.
|
||||||
|
- Example:
|
||||||
|
```
|
||||||
|
feat: add account sync to preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Extend Preferences Feature
|
||||||
|
|
||||||
|
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
|
||||||
|
**Command:** `/extend-preferences`
|
||||||
|
|
||||||
|
Follow these steps to extend user preferences functionality:
|
||||||
|
|
||||||
|
1. **Update localization files**
|
||||||
|
Add or modify strings in:
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// en-US/preferences.ts
|
||||||
|
export default {
|
||||||
|
sync: "Sync Preferences",
|
||||||
|
import: "Import Preferences",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Modify or add Vue components for preferences UI**
|
||||||
|
Update or create components such as:
|
||||||
|
- `preferences-drawer.vue`
|
||||||
|
- `account-sync.ts`
|
||||||
|
```vue
|
||||||
|
<!-- preferences-drawer.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update or add supporting icon definitions**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
|
||||||
|
```typescript
|
||||||
|
export const SyncIcon = { /* icon definition */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Implement or update store logic for settings**
|
||||||
|
Update:
|
||||||
|
- `packages/ui/certd-client/src/store/settings/index.tsx`
|
||||||
|
```typescript
|
||||||
|
export function syncPreferencesToAccount() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add or update backend API/controller for user preferences**
|
||||||
|
Edit or add:
|
||||||
|
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// user-preferences.ts
|
||||||
|
export async function updateUserPreferences(req, res) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Write or update backend tests for new preference logic**
|
||||||
|
Add or update:
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
|
||||||
|
```typescript
|
||||||
|
test('should sync preferences', async () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Update backend models if necessary**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
|
||||||
|
```typescript
|
||||||
|
export interface UserPreferences { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
- **Test files** follow the `*.test.*` naming convention.
|
||||||
|
- Example: `user-preferences.test.ts`
|
||||||
|
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
|
||||||
|
- **Test Example:**
|
||||||
|
```typescript
|
||||||
|
test('should update preferences', async () => {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|----------------------|--------------------------------------------------------------|
|
||||||
|
| /extend-preferences | Guide to extend or enhance user preference functionality |
|
||||||
|
```
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Certd"
|
||||||
|
short_description: "Repo-specific patterns and workflows for certd"
|
||||||
|
default_prompt: "Use the certd repo skill to follow existing architecture, testing, and workflow conventions."
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
{
|
||||||
|
"version": "1.3",
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"generatedBy": "ecc-tools",
|
||||||
|
"generatedAt": "2026-07-30T02:07:44.046Z",
|
||||||
|
"repo": "https://github.com/certd/certd",
|
||||||
|
"referenceSetReadiness": {
|
||||||
|
"score": 0,
|
||||||
|
"present": 0,
|
||||||
|
"total": 7,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "deep-analyzer-corpus",
|
||||||
|
"label": "Deep analyzer corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rag-evaluator",
|
||||||
|
"label": "RAG/evaluator comparison",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pr-salvage",
|
||||||
|
"label": "PR salvage/review corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "discussion-triage",
|
||||||
|
"label": "Discussion triage corpus",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "harness-compatibility",
|
||||||
|
"label": "Harness compatibility",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "security-evidence",
|
||||||
|
"label": "Security evidence",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ci-failure-mode",
|
||||||
|
"label": "CI failure-mode evidence",
|
||||||
|
"status": "missing",
|
||||||
|
"evidence": [],
|
||||||
|
"recommendation": "Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"requested": "core",
|
||||||
|
"recommended": "core",
|
||||||
|
"effective": "core",
|
||||||
|
"requestedAlias": "core",
|
||||||
|
"recommendedAlias": "core",
|
||||||
|
"effectiveAlias": "core"
|
||||||
|
},
|
||||||
|
"requestedProfile": "core",
|
||||||
|
"profile": "core",
|
||||||
|
"recommendedProfile": "core",
|
||||||
|
"effectiveProfile": "core",
|
||||||
|
"tier": "free",
|
||||||
|
"requestedComponents": [
|
||||||
|
"repo-baseline"
|
||||||
|
],
|
||||||
|
"selectedComponents": [
|
||||||
|
"repo-baseline"
|
||||||
|
],
|
||||||
|
"requestedAddComponents": [],
|
||||||
|
"requestedRemoveComponents": [],
|
||||||
|
"blockedRemovalComponents": [],
|
||||||
|
"tierFilteredComponents": [],
|
||||||
|
"requestedRootPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"selectedRootPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedAddPackages": [],
|
||||||
|
"requestedRemovePackages": [],
|
||||||
|
"selectedPackages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"packages": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"blockedRemovalPackages": [],
|
||||||
|
"tierFilteredRootPackages": [],
|
||||||
|
"tierFilteredPackages": [],
|
||||||
|
"conflictingPackages": [],
|
||||||
|
"dependencyGraph": {
|
||||||
|
"runtime-core": []
|
||||||
|
},
|
||||||
|
"resolutionOrder": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"requestedModules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"selectedModules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"modules": [
|
||||||
|
"runtime-core"
|
||||||
|
],
|
||||||
|
"managedFiles": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
],
|
||||||
|
"packageFiles": {
|
||||||
|
"runtime-core": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"moduleFiles": {
|
||||||
|
"runtime-core": [
|
||||||
|
".claude/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/SKILL.md",
|
||||||
|
".agents/skills/certd/agents/openai.yaml",
|
||||||
|
".claude/identity.json",
|
||||||
|
".codex/config.toml",
|
||||||
|
".codex/AGENTS.md",
|
||||||
|
".codex/agents/explorer.toml",
|
||||||
|
".codex/agents/reviewer.toml",
|
||||||
|
".codex/agents/docs-researcher.toml",
|
||||||
|
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/skills/certd/SKILL.md",
|
||||||
|
"description": "Repository-specific Claude Code skill generated from git history."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".agents/skills/certd/SKILL.md",
|
||||||
|
"description": "Codex-facing copy of the generated repository skill."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".agents/skills/certd/agents/openai.yaml",
|
||||||
|
"description": "Codex skill metadata so the repo skill appears cleanly in the skill interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/identity.json",
|
||||||
|
"description": "Suggested identity.json baseline derived from repository conventions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/config.toml",
|
||||||
|
"description": "Repo-local Codex MCP and multi-agent baseline aligned with ECC defaults."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/AGENTS.md",
|
||||||
|
"description": "Codex usage guide that points at the generated repo skill and workflow bundle."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/explorer.toml",
|
||||||
|
"description": "Read-only explorer role config for Codex multi-agent work."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/reviewer.toml",
|
||||||
|
"description": "Read-only reviewer role config focused on correctness and security."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".codex/agents/docs-researcher.toml",
|
||||||
|
"description": "Read-only docs researcher role config for API verification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "runtime-core",
|
||||||
|
"path": ".claude/homunculus/instincts/inherited/certd-instincts.yaml",
|
||||||
|
"description": "Continuous-learning instincts derived from repository patterns."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workflows": [],
|
||||||
|
"adapters": {
|
||||||
|
"claudeCode": {
|
||||||
|
"skillPath": ".claude/skills/certd/SKILL.md",
|
||||||
|
"identityPath": ".claude/identity.json",
|
||||||
|
"commandPaths": []
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"configPath": ".codex/config.toml",
|
||||||
|
"agentsGuidePath": ".codex/AGENTS.md",
|
||||||
|
"skillPath": ".agents/skills/certd/SKILL.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# Instincts generated from https://github.com/certd/certd
|
||||||
|
# Generated: 2026-07-30T02:08:05.402Z
|
||||||
|
# Version: 2.0
|
||||||
|
# NOTE: This file supplements (does not replace) any existing curated instincts.
|
||||||
|
# High-confidence manually curated instincts should be preserved alongside these.
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-commit-conventional
|
||||||
|
trigger: "when writing a commit message"
|
||||||
|
confidence: 0.85
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Commit Conventional
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use conventional commit format with prefixes: feat
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- 2 commits analyzed
|
||||||
|
- Detected conventional commit pattern
|
||||||
|
- Examples: feat: 偏好设置支持从剪切板导入, feat: 偏好设置支持保存到账号并在登录后自动同步
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-commit-length
|
||||||
|
trigger: "when writing a commit message"
|
||||||
|
confidence: 0.6
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Commit Length
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Keep commit messages concise (~22 characters)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Average commit message length: 22 chars
|
||||||
|
- Based on 2 commits
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-naming-files
|
||||||
|
trigger: "when creating a new file"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Naming Files
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use camelCase naming convention
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Analyzed file naming patterns in repository
|
||||||
|
- Dominant pattern: camelCase
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-export-style
|
||||||
|
trigger: "when exporting from a module"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Export Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Prefer named exports
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Export pattern analysis
|
||||||
|
- Dominant style: named
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-arch-feature-based
|
||||||
|
trigger: "when adding a new feature"
|
||||||
|
confidence: 0.85
|
||||||
|
domain: architecture
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Arch Feature Based
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Create a new folder in src/features/ with all related code colocated
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Feature-based module organization detected
|
||||||
|
- Structure: src/features/[feature-name]/
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-workflow-extend-preferences-feature
|
||||||
|
trigger: "when doing extend preferences feature"
|
||||||
|
confidence: 0.6
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: https://github.com/certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Workflow Extend Preferences Feature
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Follow the extend-preferences-feature workflow:
|
||||||
|
1. Update localization files for new preference-related strings
|
||||||
|
2. Modify or add Vue components for preferences UI
|
||||||
|
3. Update or add supporting icon definitions
|
||||||
|
4. Implement or update store logic for settings
|
||||||
|
5. Add or update backend API/controller for user preferences
|
||||||
|
6. Write or update backend tests for new preference logic
|
||||||
|
7. Update backend models if necessary
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow detected from commit patterns
|
||||||
|
- Frequency: ~2x per month
|
||||||
|
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts, packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-file-naming
|
||||||
|
trigger: "When creating a new file in the codebase"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct File Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Name the file using camelCase
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.files
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-function-naming
|
||||||
|
trigger: "When defining a new function"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Function Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use camelCase for function names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.functions
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-class-naming
|
||||||
|
trigger: "When defining a new class"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Class Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use PascalCase for class names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.classes
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-constant-naming
|
||||||
|
trigger: "When declaring a constant"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Constant Naming
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use SCREAMING_SNAKE_CASE for constant names
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.namingConventions.constants
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-import-style
|
||||||
|
trigger: "When importing modules"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Import Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use absolute import paths
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.importStyle
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-export-style
|
||||||
|
trigger: "When exporting from a module"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Export Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use named exports
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in codeStyle.exportStyle
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-try-catch
|
||||||
|
trigger: "When handling errors in code"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Try Catch
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use try-catch blocks for error handling
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in errorHandling.style
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-test-location
|
||||||
|
trigger: "When adding or updating tests"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: testing
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Test Location
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Place tests in both source and test-specific folders as appropriate (mixed location)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in architecture.folderStructure.testLocation
|
||||||
|
- Seen in files like packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-backend-test-pattern
|
||||||
|
trigger: "When adding backend logic for user preferences"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: testing
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Backend Test Pattern
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Create or update a corresponding .test.ts file in the same directory
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Seen in packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-conventional-commits
|
||||||
|
trigger: "When writing a commit message"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Conventional Commits
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Use the conventional commit format with a type prefix (e.g., feat: ...)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in commits.type
|
||||||
|
- Examples: feat: 偏好设置支持从剪切板导入
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-commit-length
|
||||||
|
trigger: "When writing a commit message"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: git
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Commit Length
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Keep the commit message concise, around 22 characters on average
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Pattern in commits.averageLength
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-localization
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Localization
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update localization files for new preference-related strings
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-ui
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Ui
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Modify or add Vue components for the preferences UI
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-icons
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Icons
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update or add supporting icon definitions
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-client/src/vben/icons/lucide.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-store
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Store
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Implement or update store logic for settings
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-client/src/store/settings/index.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-backend-api
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Backend Api
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Add or update backend API/controller for user preferences
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- Files: packages/ui/certd-server/src/controller/user/mine/user-preferences.ts, packages/ui/certd-server/src/controller/user/mine/user-settings-controller.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-backend-test
|
||||||
|
trigger: "When extending or adding a user preference feature"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Backend Test
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Write or update backend tests for new preference logic
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
id: certd-instinct-extend-preferences-models
|
||||||
|
trigger: "When extending or adding a user preference feature and new data is needed"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: workflow
|
||||||
|
source: repo-analysis
|
||||||
|
source_repo: certd/certd
|
||||||
|
---
|
||||||
|
|
||||||
|
# Certd Instinct Extend Preferences Models
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Update backend models if necessary
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Workflow: extend-preferences-feature
|
||||||
|
- File: packages/ui/certd-server/src/modules/mine/service/models.ts
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0",
|
||||||
|
"technicalLevel": "technical",
|
||||||
|
"preferredStyle": {
|
||||||
|
"verbosity": "detailed",
|
||||||
|
"codeComments": true,
|
||||||
|
"explanations": true
|
||||||
|
},
|
||||||
|
"domains": [
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"suggestedBy": "ecc-tools-repo-analysis",
|
||||||
|
"createdAt": "2026-07-30T02:08:05.402Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
```markdown
|
||||||
|
# certd Development Patterns
|
||||||
|
|
||||||
|
> Auto-generated skill from repository analysis
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
- **CamelCase** is used for file names.
|
||||||
|
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
|
||||||
|
|
||||||
|
### Import Style
|
||||||
|
|
||||||
|
- **Absolute imports** are preferred.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Style
|
||||||
|
|
||||||
|
- **Named exports** are used throughout the codebase.
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
export function getUserPreferences() { ... }
|
||||||
|
export const PREFERENCE_KEYS = [ ... ];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Patterns
|
||||||
|
|
||||||
|
- **Conventional commits** are used, with the `feat` prefix for new features.
|
||||||
|
- Example:
|
||||||
|
```
|
||||||
|
feat: add account sync to preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Extend Preferences Feature
|
||||||
|
|
||||||
|
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
|
||||||
|
**Command:** `/extend-preferences`
|
||||||
|
|
||||||
|
Follow these steps to extend user preferences functionality:
|
||||||
|
|
||||||
|
1. **Update localization files**
|
||||||
|
Add or modify strings in:
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
|
||||||
|
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// en-US/preferences.ts
|
||||||
|
export default {
|
||||||
|
sync: "Sync Preferences",
|
||||||
|
import: "Import Preferences",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Modify or add Vue components for preferences UI**
|
||||||
|
Update or create components such as:
|
||||||
|
- `preferences-drawer.vue`
|
||||||
|
- `account-sync.ts`
|
||||||
|
```vue
|
||||||
|
<!-- preferences-drawer.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update or add supporting icon definitions**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
|
||||||
|
```typescript
|
||||||
|
export const SyncIcon = { /* icon definition */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Implement or update store logic for settings**
|
||||||
|
Update:
|
||||||
|
- `packages/ui/certd-client/src/store/settings/index.tsx`
|
||||||
|
```typescript
|
||||||
|
export function syncPreferencesToAccount() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add or update backend API/controller for user preferences**
|
||||||
|
Edit or add:
|
||||||
|
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
|
||||||
|
```typescript
|
||||||
|
// user-preferences.ts
|
||||||
|
export async function updateUserPreferences(req, res) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Write or update backend tests for new preference logic**
|
||||||
|
Add or update:
|
||||||
|
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
|
||||||
|
```typescript
|
||||||
|
test('should sync preferences', async () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Update backend models if necessary**
|
||||||
|
Edit:
|
||||||
|
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
|
||||||
|
```typescript
|
||||||
|
export interface UserPreferences { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
- **Test files** follow the `*.test.*` naming convention.
|
||||||
|
- Example: `user-preferences.test.ts`
|
||||||
|
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
|
||||||
|
- **Test Example:**
|
||||||
|
```typescript
|
||||||
|
test('should update preferences', async () => {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|----------------------|--------------------------------------------------------------|
|
||||||
|
| /extend-preferences | Guide to extend or enhance user preference functionality |
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ECC for Codex CLI
|
||||||
|
|
||||||
|
This supplements the root `AGENTS.md` with a repo-local ECC baseline.
|
||||||
|
|
||||||
|
## Repo Skill
|
||||||
|
|
||||||
|
- Repo-generated Codex skill: `.agents/skills/certd/SKILL.md`
|
||||||
|
- Claude-facing companion skill: `.claude/skills/certd/SKILL.md`
|
||||||
|
- Keep user-specific credentials and private MCPs in `~/.codex/config.toml`, not in this repo.
|
||||||
|
|
||||||
|
## MCP Baseline
|
||||||
|
|
||||||
|
Treat `.codex/config.toml` as the default ECC-safe baseline for work in this repository.
|
||||||
|
The generated baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking.
|
||||||
|
|
||||||
|
## Multi-Agent Support
|
||||||
|
|
||||||
|
- Explorer: read-only evidence gathering
|
||||||
|
- Reviewer: correctness, security, and regression review
|
||||||
|
- Docs researcher: API and release-note verification
|
||||||
|
|
||||||
|
## Workflow Files
|
||||||
|
|
||||||
|
- No dedicated workflow command files were generated for this repo.
|
||||||
|
|
||||||
|
Use these workflow files as reusable task scaffolds when the detected repository workflows recur.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "medium"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Verify APIs, framework behavior, and release-note claims against primary documentation before changes land.
|
||||||
|
Cite the exact docs or file paths that support each claim.
|
||||||
|
Do not invent undocumented behavior.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "medium"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Stay in exploration mode.
|
||||||
|
Trace the real execution path, cite files and symbols, and avoid proposing fixes unless the parent agent asks for them.
|
||||||
|
Prefer targeted search and file reads over broad scans.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
model = "gpt-5.4"
|
||||||
|
model_reasoning_effort = "high"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
|
developer_instructions = """
|
||||||
|
Review like an owner.
|
||||||
|
Prioritize correctness, security, behavioral regressions, and missing tests.
|
||||||
|
Lead with concrete findings and avoid style-only feedback unless it hides a real bug.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#:schema https://developers.openai.com/codex/config-schema.json
|
||||||
|
|
||||||
|
# ECC Tools generated Codex baseline
|
||||||
|
approval_policy = "on-request"
|
||||||
|
sandbox_mode = "workspace-write"
|
||||||
|
web_search = "live"
|
||||||
|
|
||||||
|
[mcp_servers.github]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||||
|
|
||||||
|
[mcp_servers.context7]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@upstash/context7-mcp@latest"]
|
||||||
|
|
||||||
|
[mcp_servers.exa]
|
||||||
|
url = "https://mcp.exa.ai/mcp"
|
||||||
|
|
||||||
|
[mcp_servers.memory]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-memory"]
|
||||||
|
|
||||||
|
[mcp_servers.playwright]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@playwright/mcp@latest", "--extension"]
|
||||||
|
|
||||||
|
[mcp_servers.sequential-thinking]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
multi_agent = true
|
||||||
|
|
||||||
|
[agents]
|
||||||
|
max_threads = 6
|
||||||
|
max_depth = 1
|
||||||
|
|
||||||
|
[agents.explorer]
|
||||||
|
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
|
||||||
|
config_file = "agents/explorer.toml"
|
||||||
|
|
||||||
|
[agents.reviewer]
|
||||||
|
description = "PR reviewer focused on correctness, security, and missing tests."
|
||||||
|
config_file = "agents/reviewer.toml"
|
||||||
|
|
||||||
|
[agents.docs_researcher]
|
||||||
|
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
|
||||||
|
config_file = "agents/docs-researcher.toml"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
|
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
|
||||||
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
|
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
|
||||||
"lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"",
|
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
|
||||||
"lint-types": "tsd --files \"types/index.test-d.ts\"",
|
"lint-types": "tsd --files \"types/index.test-d.ts\"",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
|
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-lock": "^1.4.1",
|
"async-lock": "^1.4.1",
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ export class LocalCache<V = any> {
|
|||||||
cache: Map<string, { value: V; expiresAt: number }>;
|
cache: Map<string, { value: V; expiresAt: number }>;
|
||||||
constructor(opts: { clearInterval?: number } = {}) {
|
constructor(opts: { clearInterval?: number } = {}) {
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
const intervalId = setInterval(
|
const intervalId = setInterval(() => {
|
||||||
() => {
|
|
||||||
this.clearExpires();
|
this.clearExpires();
|
||||||
},
|
}, opts.clearInterval ?? 5 * 60 * 1000);
|
||||||
opts.clearInterval ?? 5 * 60 * 1000
|
|
||||||
);
|
|
||||||
intervalId.unref?.();
|
intervalId.unref?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export function isDev() {
|
export function isDev() {
|
||||||
const nodeEnv = process.env.NODE_ENV || "dev";
|
const nodeEnv = process.env.NODE_ENV || 'dev';
|
||||||
return nodeEnv === "development" || nodeEnv.includes("local") || nodeEnv.startsWith("dev");
|
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs';
|
||||||
function getFileRootDir(rootDir?: string) {
|
function getFileRootDir(rootDir?: string) {
|
||||||
if (rootDir == null) {
|
if (rootDir == null) {
|
||||||
const userHome = process.env.HOME || process.env.USERPROFILE;
|
const userHome = process.env.HOME || process.env.USERPROFILE;
|
||||||
rootDir = userHome + "/.certd/storage/";
|
rootDir = userHome + '/.certd/storage/';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(rootDir)) {
|
if (!fs.existsSync(rootDir)) {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
import mitt from "mitt";
|
import mitt from 'mitt';
|
||||||
export const mitter = mitt();
|
export const mitter = mitt();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.42.6",
|
"@certd/basic": "^1.42.6",
|
||||||
|
|||||||
@@ -568,18 +568,11 @@ export class RuntimeDepsService {
|
|||||||
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
||||||
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
|
|
||||||
log.info("第三方依赖安装完成");
|
log.info("第三方依赖安装完成");
|
||||||
return { registryUrl: tryUrl, packageJsonPath };
|
return { registryUrl: tryUrl, packageJsonPath };
|
||||||
}
|
}
|
||||||
const errOutput = (result.stderr || "").trim();
|
lastError = result.stderr || result.stdout || "unknown error";
|
||||||
const outOutput = (result.stdout || "").trim();
|
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
||||||
lastError = errOutput || outOutput || "unknown error";
|
|
||||||
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
|
||||||
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
|
|
||||||
if (outOutput) {
|
|
||||||
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.writeInstallState(statePath, {
|
this.writeInstallState(statePath, {
|
||||||
...currentState,
|
...currentState,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build",
|
"compile": "npm run build",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build",
|
"compile": "npm run build",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^5.0.7"
|
"nanoid": "^5.0.7"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from "./lib/iframe.client.js";
|
export * from './lib/iframe.client.js';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export type IframeMessageData<T> = {
|
export type IframeMessageData<T> = {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -32,7 +32,7 @@ export class IframeClient {
|
|||||||
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
|
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
this.onError = onError;
|
this.onError = onError;
|
||||||
window.addEventListener("message", async (event: MessageEvent<IframeMessageData<any>>) => {
|
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (data.action) {
|
if (data.action) {
|
||||||
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
|
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
|
||||||
@@ -40,20 +40,20 @@ export class IframeClient {
|
|||||||
const handler = this.handlers[data.action];
|
const handler = this.handlers[data.action];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
const res = await handler(data);
|
const res = await handler(data);
|
||||||
if (data.id && data.action !== "reply") {
|
if (data.id && data.action !== 'reply') {
|
||||||
await this.send("reply", res, data.id);
|
await this.send('reply', res, data.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
|
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
await this.send("reply", {}, data.id, 500, e.message);
|
await this.send('reply', {}, data.id, 500, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.register("reply", async data => {
|
this.register('reply', async data => {
|
||||||
const req = this.requestQueue[data.replyId!];
|
const req = this.requestQueue[data.replyId!];
|
||||||
if (req) {
|
if (req) {
|
||||||
req.onReply(data);
|
req.onReply(data);
|
||||||
@@ -106,12 +106,12 @@ export class IframeClient {
|
|||||||
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
|
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
|
||||||
if (!this.iframe) {
|
if (!this.iframe) {
|
||||||
if (!window.parent) {
|
if (!window.parent) {
|
||||||
reject("当前页面不在 iframe 中");
|
reject('当前页面不在 iframe 中');
|
||||||
}
|
}
|
||||||
window.parent.postMessage(reqMessageData, "*");
|
window.parent.postMessage(reqMessageData, '*');
|
||||||
} else {
|
} else {
|
||||||
//子页面
|
//子页面
|
||||||
this.iframe.contentWindow?.postMessage(reqMessageData, "*");
|
this.iframe.contentWindow?.postMessage(reqMessageData, '*');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build",
|
"compile": "npm run build",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "Apache",
|
"license": "Apache",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import jdCloud from './lib/core.js';
|
import jdCloud from "./lib/core.js";
|
||||||
import jdService from './lib/service.js';
|
import jdService from './lib/service.js'
|
||||||
|
|
||||||
import domainService from './repo/domainservice/v2/domainservice.js';
|
import domainService from './repo/domainservice/v2/domainservice.js'
|
||||||
import cdnService from './repo/cdn/v1/cdn.js';
|
import cdnService from './repo/cdn/v1/cdn.js'
|
||||||
import sslService from './repo/ssl/v1/ssl.js';
|
import sslService from './repo/ssl/v1/ssl.js'
|
||||||
export const JDCloud = jdCloud;
|
export const JDCloud = jdCloud;
|
||||||
export const JDService = jdService;
|
export const JDService = jdService;
|
||||||
export const JDDomainService = domainService;
|
export const JDDomainService = domainService;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.42.6",
|
"@certd/basic": "^1.42.6",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "greper",
|
"author": "greper",
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export const Constants = {
|
export const Constants = {
|
||||||
dataDir: "./data",
|
dataDir: './data',
|
||||||
role: {
|
role: {
|
||||||
defaultUser: 3,
|
defaultUser: 3,
|
||||||
},
|
},
|
||||||
per: {
|
per: {
|
||||||
//无需登录
|
//无需登录
|
||||||
guest: "_guest_",
|
guest: '_guest_',
|
||||||
//无需登录
|
//无需登录
|
||||||
anonymous: "_guest_",
|
anonymous: '_guest_',
|
||||||
//无需登录,有 token 时解析当前用户
|
//无需登录,有 token 时解析当前用户
|
||||||
guestOptionalAuth: "_guestOptionalAuth_",
|
guestOptionalAuth: '_guestOptionalAuth_',
|
||||||
//仅需要登录
|
//仅需要登录
|
||||||
authOnly: "_authOnly_",
|
authOnly: '_authOnly_',
|
||||||
//仅需要登录
|
//仅需要登录
|
||||||
loginOnly: "_authOnly_",
|
loginOnly: '_authOnly_',
|
||||||
|
|
||||||
open: "_open_",
|
open: '_open_',
|
||||||
},
|
},
|
||||||
res: {
|
res: {
|
||||||
serverError(message: string) {
|
serverError(message: string) {
|
||||||
@@ -26,102 +26,102 @@ export const Constants = {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
code: 1,
|
code: 1,
|
||||||
message: "Internal server error",
|
message: 'Internal server error',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
code: 0,
|
code: 0,
|
||||||
message: "success",
|
message: 'success',
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
code: 10,
|
code: 10,
|
||||||
message: "参数错误",
|
message: '参数错误',
|
||||||
},
|
},
|
||||||
needvip: {
|
needvip: {
|
||||||
code: 88,
|
code: 88,
|
||||||
message: "需要VIP",
|
message: '需要VIP',
|
||||||
},
|
},
|
||||||
needsuite: {
|
needsuite: {
|
||||||
code: 89,
|
code: 89,
|
||||||
message: "需要购买或升级套餐",
|
message: '需要购买或升级套餐',
|
||||||
},
|
},
|
||||||
loginError: {
|
loginError: {
|
||||||
code: 2,
|
code: 2,
|
||||||
message: "登录失败",
|
message: '登录失败',
|
||||||
},
|
},
|
||||||
codeError: {
|
codeError: {
|
||||||
code: 3,
|
code: 3,
|
||||||
message: "验证码错误",
|
message: '验证码错误',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
code: 401,
|
code: 401,
|
||||||
message: "您还未登录或token已过期",
|
message: '您还未登录或token已过期',
|
||||||
},
|
},
|
||||||
permission: {
|
permission: {
|
||||||
code: 402,
|
code: 402,
|
||||||
message: "您没有权限",
|
message: '您没有权限',
|
||||||
},
|
},
|
||||||
param: {
|
param: {
|
||||||
code: 400,
|
code: 400,
|
||||||
message: "参数错误",
|
message: '参数错误',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
code: 404,
|
code: 404,
|
||||||
message: "页面/文件/资源不存在",
|
message: '页面/文件/资源不存在',
|
||||||
},
|
},
|
||||||
|
|
||||||
preview: {
|
preview: {
|
||||||
code: 10001,
|
code: 10001,
|
||||||
message: "对不起,预览环境不允许修改此数据",
|
message: '对不起,预览环境不允许修改此数据',
|
||||||
},
|
},
|
||||||
siteOff: {
|
siteOff:{
|
||||||
code: 10010,
|
code: 10010,
|
||||||
message: "站点已关闭",
|
message: '站点已关闭',
|
||||||
},
|
},
|
||||||
need2fa: {
|
need2fa:{
|
||||||
code: 10020,
|
code: 10020,
|
||||||
message: "需要2FA认证",
|
message: '需要2FA认证',
|
||||||
},
|
},
|
||||||
openKeyError: {
|
openKeyError: {
|
||||||
code: 20000,
|
code: 20000,
|
||||||
message: "ApiToken错误",
|
message: 'ApiToken错误',
|
||||||
},
|
},
|
||||||
openKeySignError: {
|
openKeySignError: {
|
||||||
code: 20001,
|
code: 20001,
|
||||||
message: "ApiToken签名错误",
|
message: 'ApiToken签名错误',
|
||||||
},
|
},
|
||||||
openKeyExpiresError: {
|
openKeyExpiresError: {
|
||||||
code: 20002,
|
code: 20002,
|
||||||
message: "ApiToken时间戳错误",
|
message: 'ApiToken时间戳错误',
|
||||||
},
|
},
|
||||||
openKeySignTypeError: {
|
openKeySignTypeError: {
|
||||||
code: 20003,
|
code: 20003,
|
||||||
message: "ApiToken签名类型不支持",
|
message: 'ApiToken签名类型不支持',
|
||||||
},
|
},
|
||||||
openParamError: {
|
openParamError: {
|
||||||
code: 20010,
|
code: 20010,
|
||||||
message: "请求参数错误",
|
message: '请求参数错误',
|
||||||
},
|
},
|
||||||
openCertNotFound: {
|
openCertNotFound: {
|
||||||
code: 20011,
|
code: 20011,
|
||||||
message: "证书不存在",
|
message: '证书不存在',
|
||||||
},
|
},
|
||||||
openCertNotReady: {
|
openCertNotReady: {
|
||||||
code: 20012,
|
code: 20012,
|
||||||
message: "证书还未生成",
|
message: '证书还未生成',
|
||||||
},
|
},
|
||||||
openCertApplying: {
|
openCertApplying: {
|
||||||
code: 20013,
|
code: 20013,
|
||||||
message: "证书正在申请中,请稍后重新获取",
|
message: '证书正在申请中,请稍后重新获取',
|
||||||
},
|
},
|
||||||
openDomainNoVerifier: {
|
openDomainNoVerifier:{
|
||||||
code: 20014,
|
code: 20014,
|
||||||
message: "域名校验方式未配置",
|
message: '域名校验方式未配置',
|
||||||
},
|
},
|
||||||
openEmailNotFound: {
|
openEmailNotFound: {
|
||||||
code: 20021,
|
code: 20021,
|
||||||
message: "用户邮箱还未配置",
|
message: '用户邮箱还未配置',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
systemUserId: 0, // 系统级别userid固定为0
|
systemUserId: 0, // 系统级别userid固定为0
|
||||||
enterpriseUserId: -1, // 企业模式用户id固定为-1
|
enterpriseUserId: -1 // 企业模式用户id固定为-1
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ALL, Body, Post, Query } from "@midwayjs/core";
|
import { ALL, Body, Post, Query } from '@midwayjs/core';
|
||||||
import { BaseController } from "./base-controller.js";
|
import { BaseController } from './base-controller.js';
|
||||||
|
|
||||||
export abstract class CrudController<T> extends BaseController {
|
export abstract class CrudController<T> extends BaseController {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
abstract getService<T>();
|
abstract getService<T>();
|
||||||
|
|
||||||
@Post("/page")
|
@Post('/page')
|
||||||
async page(@Body(ALL) body: any) {
|
async page(@Body(ALL) body: any) {
|
||||||
const pageRet = await this.getService().page({
|
const pageRet = await this.getService().page({
|
||||||
query: body.query ?? {},
|
query: body.query ?? {},
|
||||||
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
|
|||||||
return this.ok(pageRet);
|
return this.ok(pageRet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/list")
|
@Post('/list')
|
||||||
async list(@Body(ALL) body: any) {
|
async list(@Body(ALL) body: any) {
|
||||||
const listRet = await this.getService().list({
|
const listRet = await this.getService().list({
|
||||||
query: body.query ?? {},
|
query: body.query ?? {},
|
||||||
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
|
|||||||
return this.ok(listRet);
|
return this.ok(listRet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/add")
|
@Post('/add')
|
||||||
async add(@Body(ALL) bean: any) {
|
async add(@Body(ALL) bean: any) {
|
||||||
delete bean.id;
|
delete bean.id;
|
||||||
const id = await this.getService().add(bean);
|
const id = await this.getService().add(bean);
|
||||||
return this.ok(id);
|
return this.ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/info")
|
@Post('/info')
|
||||||
async info(@Query("id") id: number) {
|
async info(@Query('id') id: number) {
|
||||||
const bean = await this.getService().info(id);
|
const bean = await this.getService().info(id);
|
||||||
return this.ok(bean);
|
return this.ok(bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/update")
|
@Post('/update')
|
||||||
async update(@Body(ALL) bean: any) {
|
async update(@Body(ALL) bean: any) {
|
||||||
await this.getService().update(bean);
|
await this.getService().update(bean);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/delete")
|
@Post('/delete')
|
||||||
async delete(@Query("id") id: number) {
|
async delete(@Query('id') id: number) {
|
||||||
await this.getService().delete([id]);
|
await this.getService().delete([id]);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/deleteByIds")
|
@Post('/deleteByIds')
|
||||||
async deleteByIds(@Body("ids") ids: number[]) {
|
async deleteByIds(@Body('ids') ids: number[]) {
|
||||||
await this.getService().delete(ids);
|
await this.getService().delete(ids);
|
||||||
return this.ok(null);
|
return this.ok(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
import { TextException } from "./common-exception.js";
|
import { TextException } from "./common-exception.js";
|
||||||
/**
|
/**
|
||||||
* 授权异常
|
* 授权异常
|
||||||
*/
|
*/
|
||||||
export class AuthException extends BaseException {
|
export class AuthException extends BaseException {
|
||||||
constructor(message?: string) {
|
constructor(message?:string) {
|
||||||
super("AuthException", Constants.res.auth.code, message ? message : Constants.res.auth.message);
|
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Need2FAException extends TextException {
|
export class Need2FAException extends TextException {
|
||||||
constructor(message: string, data: any) {
|
constructor(message:string,data:any) {
|
||||||
super("Need2FAException", Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message, data);
|
super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
export class BaseException extends Error {
|
export class BaseException extends Error {
|
||||||
code: number;
|
code: number;
|
||||||
data?: any;
|
data?:any
|
||||||
constructor(name: string, code: number, message: string, data?: any) {
|
constructor(name: string, code: number, message: string ,data?:any) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 验证码异常
|
* 验证码异常
|
||||||
*/
|
*/
|
||||||
export class CodeErrorException extends BaseException {
|
export class CodeErrorException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("CodeErrorException", Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
|
super('CodeErrorException', Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
export * from "./auth-exception.js";
|
export * from './auth-exception.js';
|
||||||
export * from "./base-exception.js";
|
export * from './base-exception.js';
|
||||||
export * from "./permission-exception.js";
|
export * from './permission-exception.js';
|
||||||
export * from "./preview-exception.js";
|
export * from './preview-exception.js';
|
||||||
export * from "./validation-exception.js";
|
export * from './validation-exception.js';
|
||||||
export * from "./vip-exception.js";
|
export * from './vip-exception.js';
|
||||||
export * from "./common-exception.js";
|
export * from './common-exception.js';
|
||||||
export * from "./not-found-exception.js";
|
export * from './not-found-exception.js';
|
||||||
export * from "./param-exception.js";
|
export * from './param-exception.js';
|
||||||
export * from "./site-off-exception.js";
|
export * from './site-off-exception.js';
|
||||||
export * from "./login-error-exception.js";
|
export * from './login-error-exception.js'
|
||||||
export * from "./code-error-exception.js";
|
export * from './code-error-exception.js'
|
||||||
export * from "./non-retryable-exception.js";
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 通用异常
|
* 通用异常
|
||||||
*/
|
*/
|
||||||
export class LoginErrorException extends BaseException {
|
export class LoginErrorException extends BaseException {
|
||||||
leftCount: number;
|
leftCount: number;
|
||||||
constructor(message, leftCount: number) {
|
constructor(message, leftCount: number) {
|
||||||
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
|
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
|
||||||
this.leftCount = leftCount;
|
this.leftCount = leftCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import assert from "assert";
|
|
||||||
import { NonRetryableException } from "./non-retryable-exception.js";
|
|
||||||
|
|
||||||
describe("NonRetryableException", () => {
|
|
||||||
it("sets the standard error name and message", () => {
|
|
||||||
const error = new NonRetryableException("cannot retry");
|
|
||||||
|
|
||||||
assert.equal(error.name, "NonRetryableException");
|
|
||||||
assert.equal(error.message, "cannot retry");
|
|
||||||
assert.equal(error instanceof Error, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export class NonRetryableException extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "NonRetryableException";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 资源不存在
|
* 资源不存在
|
||||||
*/
|
*/
|
||||||
export class NotFoundException extends BaseException {
|
export class NotFoundException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("NotFoundException", Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
|
super('NotFoundException', Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 参数异常
|
* 参数异常
|
||||||
*/
|
*/
|
||||||
export class ParamException extends BaseException {
|
export class ParamException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("ParamException", Constants.res.param.code, message ? message : Constants.res.param.message);
|
super('ParamException', Constants.res.param.code, message ? message : Constants.res.param.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 授权异常
|
* 授权异常
|
||||||
*/
|
*/
|
||||||
export class PermissionException extends BaseException {
|
export class PermissionException extends BaseException {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super("PermissionException", Constants.res.permission.code, message ? message : Constants.res.permission.message);
|
super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 预览模式
|
* 预览模式
|
||||||
*/
|
*/
|
||||||
export class PreviewException extends BaseException {
|
export class PreviewException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("PreviewException", Constants.res.preview.code, message ? message : Constants.res.preview.message);
|
super(
|
||||||
|
'PreviewException',
|
||||||
|
Constants.res.preview.code,
|
||||||
|
message ? message : Constants.res.preview.message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
export class SiteOffException extends BaseException {
|
export class SiteOffException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("SiteOffException", Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
|
super('SiteOffException', Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 校验异常
|
* 校验异常
|
||||||
*/
|
*/
|
||||||
export class ValidateException extends BaseException {
|
export class ValidateException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("ValidateException", Constants.res.validation.code, message ? message : Constants.res.validation.message);
|
super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Constants } from "../constants.js";
|
import { Constants } from '../constants.js';
|
||||||
import { BaseException } from "./base-exception.js";
|
import { BaseException } from './base-exception.js';
|
||||||
/**
|
/**
|
||||||
* 需要vip异常
|
* 需要vip异常
|
||||||
*/
|
*/
|
||||||
export class NeedVIPException extends BaseException {
|
export class NeedVIPException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("NeedVIPException", Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
|
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NeedSuiteException extends BaseException {
|
export class NeedSuiteException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super("NeedSuiteException", Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
|
super('NeedSuiteException', Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export * from "./base-controller.js";
|
export * from './base-controller.js';
|
||||||
export * from "./constants.js";
|
export * from './constants.js';
|
||||||
export * from "./crud-controller.js";
|
export * from './crud-controller.js';
|
||||||
export * from "./enum-item.js";
|
export * from './enum-item.js';
|
||||||
export * from "./exception/index.js";
|
export * from './exception/index.js';
|
||||||
export * from "./result.js";
|
export * from './result.js';
|
||||||
export * from "./base-service.js";
|
export * from './base-service.js';
|
||||||
export * from "./mode.js";
|
export * from "./mode.js"
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
let adminMode = "saas";
|
let adminMode = "saas"
|
||||||
|
|
||||||
export function setAdminMode(mode: string = "saas") {
|
export function setAdminMode(mode:string = "saas"){
|
||||||
adminMode = mode;
|
adminMode = mode
|
||||||
}
|
}
|
||||||
export function getAdminMode() {
|
export function getAdminMode(){
|
||||||
return adminMode;
|
return adminMode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEnterprise() {
|
export function isEnterprise(){
|
||||||
return adminMode === "enterprise";
|
return adminMode === "enterprise"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IMidwayContainer } from "@midwayjs/core";
|
import type { IMidwayContainer } from '@midwayjs/core';
|
||||||
import { Configuration } from "@midwayjs/core";
|
import { Configuration } from '@midwayjs/core';
|
||||||
import { logger } from "@certd/basic";
|
import { logger } from '@certd/basic';
|
||||||
@Configuration({
|
@Configuration({
|
||||||
namespace: "lib-server",
|
namespace: 'lib-server',
|
||||||
})
|
})
|
||||||
export class LibServerConfiguration {
|
export class LibServerConfiguration {
|
||||||
async onReady(container: IMidwayContainer) {
|
async onReady(container: IMidwayContainer) {
|
||||||
logger.info("lib start...");
|
logger.info('lib start...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SysSettingsEntity } from "./system/index.js";
|
import { SysSettingsEntity } from './system/index.js';
|
||||||
import { AccessEntity } from "./user/access/entity/access.js";
|
import { AccessEntity } from './user/access/entity/access.js';
|
||||||
import { AddonEntity } from "./user/index.js";
|
import { AddonEntity } from "./user/index.js";
|
||||||
export * from "./basic/index.js";
|
export * from './basic/index.js';
|
||||||
export * from "./system/index.js";
|
export * from './system/index.js';
|
||||||
export * from "./user/index.js";
|
export * from './user/index.js';
|
||||||
export { LibServerConfiguration as Configuration } from "./configuration.js";
|
export { LibServerConfiguration as Configuration } from './configuration.js';
|
||||||
|
|
||||||
export const libServerEntities = [SysSettingsEntity, AccessEntity, AddonEntity];
|
export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from "./service/plus-service.js";
|
export * from './service/plus-service.js';
|
||||||
export * from "./service/file-service.js";
|
export * from './service/file-service.js';
|
||||||
export * from "./service/encryptor.js";
|
export * from './service/encryptor.js';
|
||||||
export * from "./service/ocr-service.js";
|
export * from './service/ocr-service.js';
|
||||||
export * from "./service/executor-queue.js";
|
export * from './service/executor-queue.js';
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import crypto from "crypto";
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export class Encryptor {
|
export class Encryptor {
|
||||||
secretKey: Buffer;
|
secretKey: Buffer;
|
||||||
constructor(encryptSecret: string, encoding: BufferEncoding = "base64") {
|
constructor(encryptSecret: string, encoding: BufferEncoding = 'base64') {
|
||||||
this.secretKey = Buffer.from(encryptSecret, encoding);
|
this.secretKey = Buffer.from(encryptSecret, encoding);
|
||||||
}
|
}
|
||||||
// 加密函数
|
// 加密函数
|
||||||
@@ -10,18 +10,18 @@ export class Encryptor {
|
|||||||
const iv = crypto.randomBytes(16); // 初始化向量
|
const iv = crypto.randomBytes(16); // 初始化向量
|
||||||
// const secretKey = crypto.randomBytes(32);
|
// const secretKey = crypto.randomBytes(32);
|
||||||
// const key = Buffer.from(secretKey);
|
// const key = Buffer.from(secretKey);
|
||||||
const cipher = crypto.createCipheriv("aes-256-cbc", this.secretKey, iv);
|
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
|
||||||
let encrypted = cipher.update(text);
|
let encrypted = cipher.update(text);
|
||||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
return iv.toString("hex") + ":" + encrypted.toString("hex");
|
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密函数
|
// 解密函数
|
||||||
decrypt(encryptedText: string) {
|
decrypt(encryptedText: string) {
|
||||||
const textParts = encryptedText.split(":");
|
const textParts = encryptedText.split(':');
|
||||||
const iv = Buffer.from(textParts.shift(), "hex");
|
const iv = Buffer.from(textParts.shift(), 'hex');
|
||||||
const encrypted = Buffer.from(textParts.join(":"), "hex");
|
const encrypted = Buffer.from(textParts.join(':'), 'hex');
|
||||||
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(this.secretKey), iv);
|
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
|
||||||
let decrypted = decipher.update(encrypted);
|
let decrypted = decipher.update(encrypted);
|
||||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
return decrypted.toString();
|
return decrypted.toString();
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { logger } from "@certd/basic";
|
import { logger } from "@certd/basic";
|
||||||
|
|
||||||
export type TaskItem = {
|
export type TaskItem = {
|
||||||
task: () => Promise<void>;
|
task: ()=>Promise<void>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export class UserTaskQueue {
|
export class UserTaskQueue{
|
||||||
userId: number;
|
userId: number;
|
||||||
pendingQueue: TaskItem[] = [];
|
pendingQueue: TaskItem[] = [];
|
||||||
runningQueue: TaskItem[] = [];
|
runningQueue: TaskItem[] = [];
|
||||||
getMaxRunningCount: () => number;
|
getMaxRunningCount: ()=>number ;
|
||||||
|
|
||||||
constructor(req: { userId: number; getMaxRunningCount: () => number }) {
|
constructor(req: { userId: number ,getMaxRunningCount: ()=>number }) {
|
||||||
this.userId = req.userId;
|
this.userId = req.userId;
|
||||||
this.getMaxRunningCount = req.getMaxRunningCount;
|
this.getMaxRunningCount = req.getMaxRunningCount ;
|
||||||
}
|
}
|
||||||
|
|
||||||
addTask(task: TaskItem) {
|
addTask(task: TaskItem) {
|
||||||
@@ -34,10 +34,10 @@ export class UserTaskQueue {
|
|||||||
}
|
}
|
||||||
// 执行任务
|
// 执行任务
|
||||||
this.runningQueue.push(task);
|
this.runningQueue.push(task);
|
||||||
const call = async () => {
|
const call = async ()=>{
|
||||||
try {
|
try{
|
||||||
await task.task();
|
await task.task();
|
||||||
} finally {
|
}finally{
|
||||||
// 任务执行完成,从运行队列中移除
|
// 任务执行完成,从运行队列中移除
|
||||||
const index = this.runningQueue.indexOf(task);
|
const index = this.runningQueue.indexOf(task);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -46,16 +46,17 @@ export class UserTaskQueue {
|
|||||||
// 继续执行下一个任务
|
// 继续执行下一个任务
|
||||||
this.runTask();
|
this.runTask();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
|
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
|
||||||
call();
|
call()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutorQueue {
|
export class ExecutorQueue{
|
||||||
queues: Record<number, UserTaskQueue> = {};
|
queues: Record<number, UserTaskQueue> = {};
|
||||||
maxRunningCount: number = 10;
|
maxRunningCount: number = 10;
|
||||||
|
|
||||||
|
|
||||||
setMaxRunningCount(count: number) {
|
setMaxRunningCount(count: number) {
|
||||||
this.maxRunningCount = count;
|
this.maxRunningCount = count;
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ export class ExecutorQueue {
|
|||||||
getUserQueue(userId: number) {
|
getUserQueue(userId: number) {
|
||||||
const userQueue = this.queues[userId];
|
const userQueue = this.queues[userId];
|
||||||
if (!userQueue) {
|
if (!userQueue) {
|
||||||
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: () => this.maxRunningCount });
|
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: ()=>this.maxRunningCount });
|
||||||
}
|
}
|
||||||
return this.queues[userId];
|
return this.queues[userId];
|
||||||
}
|
}
|
||||||
@@ -72,6 +73,7 @@ export class ExecutorQueue {
|
|||||||
const userQueue = this.getUserQueue(userId);
|
const userQueue = this.getUserQueue(userId);
|
||||||
userQueue.addTask(task);
|
userQueue.addTask(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const executorQueue = new ExecutorQueue();
|
export const executorQueue = new ExecutorQueue();
|
||||||
@@ -1,42 +1,42 @@
|
|||||||
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs";
|
import fs from 'fs';
|
||||||
import { cache, logger, utils } from "@certd/basic";
|
import { cache, logger, utils } from '@certd/basic';
|
||||||
import { NotFoundException, ParamException, PermissionException } from "../../../basic/index.js";
|
import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js';
|
||||||
|
|
||||||
export type UploadFileItem = {
|
export type UploadFileItem = {
|
||||||
filename: string;
|
filename: string;
|
||||||
tmpFilePath: string;
|
tmpFilePath: string;
|
||||||
};
|
};
|
||||||
const uploadRootDir = "./data/upload";
|
const uploadRootDir = './data/upload';
|
||||||
export const uploadTmpFileCacheKey = "tmpfile_key_";
|
export const uploadTmpFileCacheKey = 'tmpfile_key_';
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
export class FileService {
|
export class FileService {
|
||||||
async saveFile(userId: number, tmpCacheKey: any, permission: "public" | "private") {
|
async saveFile(userId: number, tmpCacheKey: any, permission: 'public' | 'private') {
|
||||||
if (tmpCacheKey.startsWith(`/${permission}`)) {
|
if (tmpCacheKey.startsWith(`/${permission}`)) {
|
||||||
//已经保存过,不需要再次保存
|
//已经保存过,不需要再次保存
|
||||||
return tmpCacheKey;
|
return tmpCacheKey;
|
||||||
}
|
}
|
||||||
let fileName = "";
|
let fileName = '';
|
||||||
let tmpFilePath = tmpCacheKey;
|
let tmpFilePath = tmpCacheKey;
|
||||||
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
|
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
|
||||||
const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
|
const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
|
||||||
if (!tmpFile) {
|
if (!tmpFile) {
|
||||||
throw new ParamException("文件已过期,请重新上传");
|
throw new ParamException('文件已过期,请重新上传');
|
||||||
}
|
}
|
||||||
tmpFilePath = tmpFile.tmpFilePath;
|
tmpFilePath = tmpFile.tmpFilePath;
|
||||||
fileName = tmpFile.filename || path.basename(tmpFilePath);
|
fileName = tmpFile.filename || path.basename(tmpFilePath);
|
||||||
}
|
}
|
||||||
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
|
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
|
||||||
throw new Error("文件不存在,请重新上传");
|
throw new Error('文件不存在,请重新上传');
|
||||||
}
|
}
|
||||||
const date = dayjs().format("YYYY_MM_DD");
|
const date = dayjs().format('YYYY_MM_DD');
|
||||||
const random = Math.random().toString(36).substring(7);
|
const random = Math.random().toString(36).substring(7);
|
||||||
const userIdMd5 = Buffer.from(Buffer.from(userId + "").toString("base64")).toString("hex");
|
const userIdMd5 = Buffer.from(Buffer.from(userId + '').toString('base64')).toString('hex');
|
||||||
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
|
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
|
||||||
let savePath = path.join(uploadRootDir, key);
|
let savePath = path.join(uploadRootDir, key);
|
||||||
savePath = path.resolve(savePath);
|
savePath = path.resolve(savePath);
|
||||||
@@ -44,6 +44,7 @@ export class FileService {
|
|||||||
if (!fs.existsSync(parentDir)) {
|
if (!fs.existsSync(parentDir)) {
|
||||||
fs.mkdirSync(parentDir, { recursive: true });
|
fs.mkdirSync(parentDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line node/no-unsupported-features/node-builtins
|
||||||
const copyFile = utils.promises.promisify(fs.copyFile);
|
const copyFile = utils.promises.promisify(fs.copyFile);
|
||||||
await copyFile(tmpFilePath, savePath);
|
await copyFile(tmpFilePath, savePath);
|
||||||
try {
|
try {
|
||||||
@@ -57,29 +58,29 @@ export class FileService {
|
|||||||
|
|
||||||
getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
|
getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new ParamException("参数错误");
|
throw new ParamException('参数错误');
|
||||||
}
|
}
|
||||||
if (key.indexOf("..") >= 0) {
|
if (key.indexOf('..') >= 0) {
|
||||||
//安全性判断
|
//安全性判断
|
||||||
throw new ParamException("参数错误");
|
throw new ParamException('参数错误');
|
||||||
}
|
}
|
||||||
if (!key.startsWith("/")) {
|
if (!key.startsWith('/')) {
|
||||||
throw new ParamException("参数错误");
|
throw new ParamException('参数错误');
|
||||||
}
|
}
|
||||||
const keyArr = key.split("/");
|
const keyArr = key.split('/');
|
||||||
const permission = keyArr[1];
|
const permission = keyArr[1];
|
||||||
const userIdMd5 = keyArr[2];
|
const userIdMd5 = keyArr[2];
|
||||||
if (permission !== "public" && !allowAnyPrivateUser) {
|
if (permission !== 'public' && !allowAnyPrivateUser) {
|
||||||
//非公开文件需要验证用户
|
//非公开文件需要验证用户
|
||||||
const userIdStr = Buffer.from(Buffer.from(userIdMd5, "hex").toString("base64")).toString();
|
const userIdStr = Buffer.from(Buffer.from(userIdMd5, 'hex').toString('base64')).toString();
|
||||||
const userIdInt: number = parseInt(userIdStr, 10);
|
const userIdInt: number = parseInt(userIdStr, 10);
|
||||||
if (userId == null || userIdInt !== userId) {
|
if (userId == null || userIdInt !== userId) {
|
||||||
throw new PermissionException("无访问权限");
|
throw new PermissionException('无访问权限');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const filePath = path.join(uploadRootDir, key);
|
const filePath = path.join(uploadRootDir, key);
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
throw new NotFoundException("文件不存在");
|
throw new NotFoundException('文件不存在');
|
||||||
}
|
}
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export class OcrService implements IOcrService {
|
|||||||
url: "/activation/certd/ocr",
|
url: "/activation/certd/ocr",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: {
|
||||||
image: opts.image,
|
image: opts.image
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./settings/index.js";
|
export * from './settings/index.js';
|
||||||
export * from "./basic/index.js";
|
export * from './basic/index.js';
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./service/sys-settings-service.js";
|
export * from './service/sys-settings-service.js';
|
||||||
export * from "./service/models.js";
|
export * from './service/models.js';
|
||||||
export * from "./entity/sys-settings.js";
|
export * from './entity/sys-settings.js';
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from 'typeorm';
|
||||||
import { SysSettingsEntity } from "../entity/sys-settings.js";
|
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||||
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from "./models.js";
|
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
|
||||||
|
|
||||||
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from "@certd/acme-client";
|
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
|
||||||
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from "@certd/basic";
|
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
|
||||||
import { isPlus } from "@certd/plus-core";
|
import { isPlus } from '@certd/plus-core';
|
||||||
import * as dns from "node:dns";
|
import * as dns from 'node:dns';
|
||||||
import { BaseService, setAdminMode } from "../../../basic/index.js";
|
import { BaseService, setAdminMode } from '../../../basic/index.js';
|
||||||
import { executorQueue } from "../../basic/service/executor-queue.js";
|
import { executorQueue } from '../../basic/service/executor-queue.js';
|
||||||
const { merge } = mergeUtils;
|
const { merge } = mergeUtils;
|
||||||
|
|
||||||
let lastSaveEnvVars = {};
|
let lastSaveEnvVars = {};
|
||||||
@@ -138,7 +138,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
res.reverseProxies[domain] = "";
|
res.reverseProxies[domain] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePrivateSettings(bean: SysPrivateSettings) {
|
async savePrivateSettings(bean: SysPrivateSettings) {
|
||||||
@@ -149,14 +149,14 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reloadSettings() {
|
async reloadSettings() {
|
||||||
await this.reloadPrivateSettings();
|
await this.reloadPrivateSettings()
|
||||||
await this.reloadPublicSettings();
|
await this.reloadPublicSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadPublicSettings() {
|
async reloadPublicSettings() {
|
||||||
const publicSetting = await this.getPublicSettings();
|
const publicSetting = await this.getPublicSettings()
|
||||||
if (isPlus()) {
|
if (isPlus()){
|
||||||
setAdminMode(publicSetting.adminMode);
|
setAdminMode(publicSetting.adminMode )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,28 +183,29 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
this.setEnvironmentVars(privateSetting.environmentVars);
|
this.setEnvironmentVars(privateSetting.environmentVars);
|
||||||
|
|
||||||
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
|
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseKeyValueText(text: string) {
|
parseKeyValueText(text: string) {
|
||||||
const values = {};
|
const values = {};
|
||||||
if (typeof text !== "string") {
|
if (typeof text !== 'string') {
|
||||||
text = "";
|
text = "";
|
||||||
}
|
}
|
||||||
text.split("\n").forEach(line => {
|
text.split('\n').forEach(line => {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (!line || line.startsWith("#")) {
|
if (!line || line.startsWith('#')) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const arr = line.split("#");
|
const arr = line.split("#")
|
||||||
if (arr.length > 0) {
|
if (arr.length > 0) {
|
||||||
line = arr[0].trim();
|
line = arr[0].trim();
|
||||||
}
|
}
|
||||||
if (!line.includes("=")) {
|
if (!line.includes("=")) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const eqIndex = line.indexOf("=");
|
const eqIndex = line.indexOf('=');
|
||||||
const key = line.substring(0, eqIndex).trim();
|
const key = line.substring(0, eqIndex).trim();
|
||||||
const value = line.substring(eqIndex + 1).trim();
|
const value = line.substring(eqIndex + 1).trim();
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
@@ -233,7 +234,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
entity.setting = JSON.stringify(setting);
|
entity.setting = JSON.stringify(setting);
|
||||||
await this.repository.save(entity);
|
await this.repository.save(entity);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("该设置不存在");
|
throw new Error('该设置不存在');
|
||||||
}
|
}
|
||||||
cache.delete(`settings.${key}`);
|
cache.delete(`settings.${key}`);
|
||||||
}
|
}
|
||||||
@@ -245,20 +246,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
if (settings == null) {
|
if (settings == null) {
|
||||||
const backup = new SysSecretBackup();
|
const backup = new SysSecretBackup();
|
||||||
if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
|
if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
|
||||||
logger.error("备份密钥失败,siteId或encryptSecret为空");
|
logger.error('备份密钥失败,siteId或encryptSecret为空');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
backup.siteId = installInfo.siteId;
|
backup.siteId = installInfo.siteId;
|
||||||
backup.encryptSecret = privateSettings.encryptSecret;
|
backup.encryptSecret = privateSettings.encryptSecret;
|
||||||
await this.saveSetting(backup);
|
await this.saveSetting(backup);
|
||||||
logger.info("备份密钥成功");
|
logger.info('备份密钥成功');
|
||||||
} else {
|
} else {
|
||||||
//校验是否有变化
|
//校验是否有变化
|
||||||
if (settings.siteId !== installInfo.siteId) {
|
if (settings.siteId !== installInfo.siteId) {
|
||||||
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
|
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
|
||||||
}
|
}
|
||||||
if (settings.encryptSecret !== privateSettings.encryptSecret) {
|
if (settings.encryptSecret !== privateSettings.encryptSecret) {
|
||||||
throw new Error("encryptSecret与备份不一致,可能是数据异常,请检查");
|
throw new Error('encryptSecret与备份不一致,可能是数据异常,请检查');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,12 +271,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
//从备份中读取
|
//从备份中读取
|
||||||
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
|
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
|
||||||
if (settings == null || !settings.encryptSecret) {
|
if (settings == null || !settings.encryptSecret) {
|
||||||
throw new Error("密钥备份不存在");
|
throw new Error('密钥备份不存在');
|
||||||
}
|
}
|
||||||
sysSecret.siteId = settings.siteId;
|
sysSecret.siteId = settings.siteId;
|
||||||
sysSecret.encryptSecret = settings.encryptSecret;
|
sysSecret.encryptSecret = settings.encryptSecret;
|
||||||
await this.saveSetting(sysSecret);
|
await this.saveSetting(sysSecret);
|
||||||
logger.info("密钥恢复成功");
|
logger.info('密钥恢复成功');
|
||||||
return sysSecret;
|
return sysSecret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权配置
|
* 授权配置
|
||||||
*/
|
*/
|
||||||
@Entity("cd_access")
|
@Entity('cd_access')
|
||||||
export class AccessEntity {
|
export class AccessEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
@Column({ name: "key_id", comment: "key_id", length: 100 })
|
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
|
||||||
keyId: string;
|
keyId: string;
|
||||||
|
|
||||||
@Column({ name: "user_id", comment: "用户id" })
|
@Column({ name: 'user_id', comment: '用户id' })
|
||||||
userId: number; // 0为系统级别, -1为企业,大于1为用户
|
userId: number; // 0为系统级别, -1为企业,大于1为用户
|
||||||
|
|
||||||
@Column({ comment: "名称", length: 100 })
|
@Column({ comment: '名称', length: 100 })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ comment: "类型", length: 100 })
|
@Column({ comment: '类型', length: 100 })
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
@Column({ name: "subtype", comment: "子类型", length: 100, nullable: true })
|
@Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
|
||||||
subtype: string;
|
subtype: string;
|
||||||
|
|
||||||
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
|
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
||||||
setting: string;
|
setting: string;
|
||||||
|
|
||||||
@Column({ name: "encrypt_setting", comment: "已加密设置", length: 10240, nullable: true })
|
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
|
||||||
encryptSetting: string;
|
encryptSetting: string;
|
||||||
|
|
||||||
@Column({ name: "project_id", comment: "项目id" })
|
@Column({ name: 'project_id', comment: '项目id' })
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
name: "create_time",
|
name: 'create_time',
|
||||||
comment: "创建时间",
|
comment: '创建时间',
|
||||||
default: () => "CURRENT_TIMESTAMP",
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
})
|
})
|
||||||
createTime: Date;
|
createTime: Date;
|
||||||
@Column({
|
@Column({
|
||||||
name: "update_time",
|
name: 'update_time',
|
||||||
comment: "修改时间",
|
comment: '修改时间',
|
||||||
default: () => "CURRENT_TIMESTAMP",
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
})
|
})
|
||||||
updateTime: Date;
|
updateTime: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from "./entity/access.js";
|
export * from './entity/access.js';
|
||||||
export * from "./service/access-service.js";
|
export * from './service/access-service.js';
|
||||||
export * from "./service/access-sys-getter.js";
|
export * from './service/access-sys-getter.js';
|
||||||
export * from "./service/access-getter.js";
|
export * from './service/access-getter.js';
|
||||||
export * from "./service/encrypt-service.js";
|
export * from './service/encrypt-service.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IAccessService } from "@certd/pipeline";
|
import { IAccessService } from '@certd/pipeline';
|
||||||
import { AccessService } from "./access-service.js";
|
import { AccessService } from './access-service.js';
|
||||||
|
|
||||||
export class AccessSysGetter implements IAccessService {
|
export class AccessSysGetter implements IAccessService {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { Encryptor, SysSecret, SysSettingsService } from "../../../system/index.js";
|
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newAddon(addonType: string, type: string, input: any, ctx: AddonContext) {
|
export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) {
|
||||||
const key = `${addonType}:${type}`;
|
const key = `${addonType}:${type}`
|
||||||
const register = addonRegistry.get(key);
|
const register = addonRegistry.get(key);
|
||||||
if (register == null) {
|
if (register == null) {
|
||||||
throw new Error(`${addonType} ${type} not found`);
|
throw new Error(`${addonType} ${type} not found`);
|
||||||
|
|||||||
@@ -1,46 +1,49 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@Entity("cd_addon")
|
@Entity('cd_addon')
|
||||||
export class AddonEntity {
|
export class AddonEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
@Column({ name: "key_id", comment: "key_id", length: 100 })
|
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
|
||||||
keyId: string;
|
keyId: string;
|
||||||
@Column({ name: "user_id", comment: "用户id" })
|
@Column({ name: 'user_id', comment: '用户id' })
|
||||||
userId: number;
|
userId: number;
|
||||||
@Column({ comment: "名称", length: 100 })
|
@Column({ comment: '名称', length: 100 })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ name: "addon_type", comment: "addon类型", length: 100 })
|
|
||||||
|
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
|
||||||
addonType: string;
|
addonType: string;
|
||||||
|
|
||||||
@Column({ comment: "类型", length: 100 })
|
|
||||||
|
@Column({ comment: '类型', length: 100 })
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
|
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
||||||
setting: string;
|
setting: string;
|
||||||
|
|
||||||
@Column({ name: "is_system", comment: "是否系统级别", nullable: false, default: false })
|
@Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
|
||||||
@Column({ name: "is_default", comment: "是否默认", nullable: false, default: false })
|
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
|
||||||
@Column({ name: "project_id", comment: "项目id" })
|
@Column({ name: 'project_id', comment: '项目id' })
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
name: "create_time",
|
name: 'create_time',
|
||||||
comment: "创建时间",
|
comment: '创建时间',
|
||||||
default: () => "CURRENT_TIMESTAMP",
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
})
|
})
|
||||||
createTime: Date;
|
createTime: Date;
|
||||||
@Column({
|
@Column({
|
||||||
name: "update_time",
|
name: 'update_time',
|
||||||
comment: "修改时间",
|
comment: '修改时间',
|
||||||
default: () => "CURRENT_TIMESTAMP",
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
})
|
})
|
||||||
updateTime: Date;
|
updateTime: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./api/index.js";
|
export * from './api/index.js'
|
||||||
export * from "./entity/addon.js";
|
export * from './entity/addon.js'
|
||||||
export * from "./service/addon-service.js";
|
export * from './service/addon-service.js'
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
return await super.add(param);
|
return await super.add(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改
|
* 修改
|
||||||
* @param param 数据
|
* @param param 数据
|
||||||
@@ -58,7 +59,7 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
if (oldEntity == null) {
|
if (oldEntity == null) {
|
||||||
throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
|
throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
|
||||||
}
|
}
|
||||||
delete param.keyId;
|
delete param.keyId
|
||||||
return await super.update(param);
|
return await super.update(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +75,11 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
userId: entity.userId,
|
userId: entity.userId,
|
||||||
addonType: entity.addonType,
|
addonType: entity.addonType,
|
||||||
type: entity.type,
|
type: entity.type,
|
||||||
projectId: entity.projectId,
|
projectId: entity.projectId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getDefineList(addonType: string) {
|
getDefineList(addonType: string) {
|
||||||
return addonRegistry.getDefineList(addonType);
|
return addonRegistry.getDefineList(addonType);
|
||||||
}
|
}
|
||||||
@@ -86,11 +88,12 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
return addonRegistry.getDefine(type, prefix) as AddonDefine;
|
return addonRegistry.getDefine(type, prefix) as AddonDefine;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
|
|
||||||
|
async getSimpleByIds(ids: number[], userId: any,projectId?:number) {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (userId == null) {
|
if (userId==null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||||
@@ -106,12 +109,14 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
addonType: true,
|
addonType: true,
|
||||||
type: true,
|
type: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
isSystem: true,
|
isSystem: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefault(userId: number, addonType: string, projectId?: number): Promise<any> {
|
|
||||||
|
async getDefault(userId: number, addonType: string,projectId?:number): Promise<any> {
|
||||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||||
const res = await this.repository.findOne({
|
const res = await this.repository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@@ -119,8 +124,8 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
...userProjectQuery,
|
...userProjectQuery,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
isDefault: "DESC",
|
isDefault: "DESC"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return null;
|
return null;
|
||||||
@@ -138,15 +143,15 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
name: res.name,
|
name: res.name,
|
||||||
userId: res.userId,
|
userId: res.userId,
|
||||||
setting,
|
setting,
|
||||||
projectId: res.projectId,
|
projectId: res.projectId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDefault(id: number, userId: number, addonType: string, projectId?: number) {
|
async setDefault(id: number, userId: number, addonType: string,projectId?:number) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new ValidateException("id不能为空");
|
throw new ValidateException("id不能为空");
|
||||||
}
|
}
|
||||||
if (userId == null) {
|
if (userId==null) {
|
||||||
throw new ValidateException("userId不能为空");
|
throw new ValidateException("userId不能为空");
|
||||||
}
|
}
|
||||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||||
@@ -155,27 +160,24 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
...userProjectQuery,
|
...userProjectQuery,
|
||||||
};
|
};
|
||||||
await this.repository.update(query, {
|
await this.repository.update(query, {
|
||||||
isDefault: false,
|
isDefault: false
|
||||||
|
});
|
||||||
|
await this.repository.update({ ...query, id }, {
|
||||||
|
isDefault: true
|
||||||
});
|
});
|
||||||
await this.repository.update(
|
|
||||||
{ ...query, id },
|
|
||||||
{
|
|
||||||
isDefault: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateDefault(opts: { addonType: string; type: string; inputs: any; userId: any; projectId?: number }) {
|
async getOrCreateDefault(opts: { addonType: string, type: string, inputs: any, userId: any,projectId?:number }) {
|
||||||
const { addonType, type, inputs, userId, projectId } = opts;
|
const { addonType, type, inputs, userId,projectId } = opts;
|
||||||
|
|
||||||
const addonDefine = this.getDefineByType(type, addonType);
|
const addonDefine = this.getDefineByType(type, addonType);
|
||||||
|
|
||||||
const defaultConfig = await this.getDefault(userId, addonType, projectId);
|
const defaultConfig = await this.getDefault(userId, addonType,projectId);
|
||||||
if (defaultConfig) {
|
if (defaultConfig) {
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
const setting = {
|
const setting = {
|
||||||
...inputs,
|
...inputs
|
||||||
};
|
};
|
||||||
const res = await this.repository.save({
|
const res = await this.repository.save({
|
||||||
userId,
|
userId,
|
||||||
@@ -184,19 +186,19 @@ export class AddonService extends BaseService<AddonEntity> {
|
|||||||
name: addonDefine.title,
|
name: addonDefine.title,
|
||||||
setting: JSON.stringify(setting),
|
setting: JSON.stringify(setting),
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
projectId,
|
projectId
|
||||||
});
|
});
|
||||||
return this.buildAddonInstanceConfig(res);
|
return this.buildAddonInstanceConfig(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOneByType(req: { addonType: string; type: string; userId: number; projectId?: number }) {
|
async getOneByType(req:{addonType:string,type:string,userId:number,projectId?:number}) {
|
||||||
const userProjectQuery = this.buildUserProjectQuery(req.userId, req.projectId);
|
const userProjectQuery = this.buildUserProjectQuery(req.userId, req.projectId);
|
||||||
return await this.repository.findOne({
|
return await this.repository.findOne({
|
||||||
where: {
|
where: {
|
||||||
addonType: req.addonType,
|
addonType: req.addonType,
|
||||||
type: req.type,
|
type: req.type,
|
||||||
...userProjectQuery,
|
...userProjectQuery,
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./access/index.js";
|
export * from './access/index.js';
|
||||||
export * from "./addon/index.js";
|
export * from './addon/index.js';
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build",
|
"compile": "npm run build",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "greper",
|
"author": "greper",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Config, Configuration, Logger } from "@midwayjs/core";
|
import { Config, Configuration, Logger } from '@midwayjs/core';
|
||||||
import { Flyway } from "./flyway.js";
|
import { Flyway } from './flyway.js';
|
||||||
import type { ILogger } from "@midwayjs/logger";
|
import type { ILogger } from '@midwayjs/logger';
|
||||||
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
|
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||||
import type { IMidwayContainer } from "@midwayjs/core";
|
import type { IMidwayContainer } from '@midwayjs/core';
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
namespace: "flyway",
|
namespace: 'flyway',
|
||||||
//importConfigs: [join(__dirname, './config')],
|
//importConfigs: [join(__dirname, './config')],
|
||||||
})
|
})
|
||||||
export class FlywayConfiguration {
|
export class FlywayConfiguration {
|
||||||
@@ -14,9 +14,9 @@ export class FlywayConfiguration {
|
|||||||
@Logger()
|
@Logger()
|
||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
async onReady(container: IMidwayContainer) {
|
async onReady(container: IMidwayContainer) {
|
||||||
this.logger.info("flyway start:" + JSON.stringify(this.flyway));
|
this.logger.info('flyway start:' + JSON.stringify(this.flyway));
|
||||||
const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);
|
const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);
|
||||||
const dataSourceName = this.flyway.dataSourceName || "default";
|
const dataSourceName = this.flyway.dataSourceName || 'default';
|
||||||
const connection = dataSourceManager.getDataSource(dataSourceName);
|
const connection = dataSourceManager.getDataSource(dataSourceName);
|
||||||
await new Flyway({ ...this.flyway, logger: this.logger, connection }).run();
|
await new Flyway({ ...this.flyway, logger: this.logger, connection }).run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity("flyway_history")
|
@Entity('flyway_history')
|
||||||
export class FlywayHistory {
|
export class FlywayHistory {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@Column({ comment: "文件名", length: 100 })
|
@Column({ comment: '文件名', length: 100 })
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@Column({ comment: "hash", length: 32 })
|
@Column({ comment: 'hash', length: 32 })
|
||||||
hash?: string;
|
hash?: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
comment: "执行时间",
|
comment: '执行时间',
|
||||||
})
|
})
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
comment: "执行成功",
|
comment: '执行成功',
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
|
|||||||
@@ -93,21 +93,19 @@ export class Flyway {
|
|||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
this.errorTip(err);
|
|
||||||
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
|
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
|
|
||||||
|
if (err.code === "SQLITE_IOERR_WRITE") {
|
||||||
|
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.info("[ midfly ] end-------------");
|
this.logger.info("[ midfly ] end-------------");
|
||||||
}
|
}
|
||||||
|
|
||||||
private errorTip(err: any) {
|
|
||||||
if (err.code === "SQLITE_IOERR_WRITE") {
|
|
||||||
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
|
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
|
||||||
const hash = await this.getFileHash(filepath);
|
const hash = await this.getFileHash(filepath);
|
||||||
//先删除
|
//先删除
|
||||||
@@ -267,7 +265,6 @@ export class Flyway {
|
|||||||
await queryRunner.query(sql);
|
await queryRunner.query(sql);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error("exec sql error : ", err.message, err);
|
this.logger.error("exec sql error : ", err.message, err);
|
||||||
this.errorTip(err);
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { FlywayConfiguration as Configuration } from "./configuration.js";
|
export { FlywayConfiguration as Configuration } from './configuration.js';
|
||||||
export { Flyway, setFlywayLogger } from "./flyway.js";
|
export { Flyway, setFlywayLogger } from './flyway.js';
|
||||||
export { FlywayHistory } from "./entity.js";
|
export { FlywayHistory } from './entity.js';
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/plugin-lib": "^1.42.6"
|
"@certd/plugin-lib": "^1.42.6"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch",
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint": "eslint --fix --ext .ts src"
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/acme-client": "^1.42.6",
|
"@certd/acme-client": "^1.42.6",
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export default {
|
|||||||
projectUserManager: "Project User Management",
|
projectUserManager: "Project User Management",
|
||||||
myProjectManager: "My Projects",
|
myProjectManager: "My Projects",
|
||||||
myProjectDetail: "Project Detail",
|
myProjectDetail: "Project Detail",
|
||||||
projectDetail: "Project Detail",
|
|
||||||
projectJoin: "Join Project",
|
projectJoin: "Join Project",
|
||||||
currentProject: "Current Project",
|
currentProject: "Current Project",
|
||||||
projectMemberManager: "Project Member",
|
projectMemberManager: "Project Member",
|
||||||
|
|||||||
@@ -31,15 +31,6 @@ export default {
|
|||||||
copyPreferences: "Copy Preferences",
|
copyPreferences: "Copy Preferences",
|
||||||
copyPreferencesSuccessTitle: "Copy successful",
|
copyPreferencesSuccessTitle: "Copy successful",
|
||||||
copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app",
|
copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app",
|
||||||
importPreferences: "Import from Clipboard",
|
|
||||||
importPreferencesSuccessTitle: "Import successful",
|
|
||||||
importPreferencesSuccess: "Preferences imported from clipboard",
|
|
||||||
importPreferencesErrorTitle: "Import failed",
|
|
||||||
importPreferencesError: "Clipboard content is invalid. Please copy preferences JSON first",
|
|
||||||
saveToAccount: "Save to Account",
|
|
||||||
saveToAccountSuccess: "Preferences saved to account",
|
|
||||||
saveToAccountError: "Failed to save preferences to account",
|
|
||||||
saveToAccountNeedLogin: "Please sign in before saving to account",
|
|
||||||
clearAndLogout: "Clear Cache & Logout",
|
clearAndLogout: "Clear Cache & Logout",
|
||||||
mode: "Mode",
|
mode: "Mode",
|
||||||
general: "General",
|
general: "General",
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export default {
|
|||||||
enterpriseSetting: "企业设置",
|
enterpriseSetting: "企业设置",
|
||||||
myProjectManager: "我的项目",
|
myProjectManager: "我的项目",
|
||||||
myProjectDetail: "项目详情",
|
myProjectDetail: "项目详情",
|
||||||
projectDetail: "项目详情",
|
|
||||||
projectJoin: "加入项目",
|
projectJoin: "加入项目",
|
||||||
currentProject: "当前项目",
|
currentProject: "当前项目",
|
||||||
projectMemberManager: "项目成员管理",
|
projectMemberManager: "项目成员管理",
|
||||||
|
|||||||
@@ -31,15 +31,6 @@ export default {
|
|||||||
copyPreferences: "复制偏好设置",
|
copyPreferences: "复制偏好设置",
|
||||||
copyPreferencesSuccessTitle: "复制成功",
|
copyPreferencesSuccessTitle: "复制成功",
|
||||||
copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
|
copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
|
||||||
importPreferences: "从剪切板导入",
|
|
||||||
importPreferencesSuccessTitle: "导入成功",
|
|
||||||
importPreferencesSuccess: "已从剪切板导入偏好设置",
|
|
||||||
importPreferencesErrorTitle: "导入失败",
|
|
||||||
importPreferencesError: "剪切板内容无效,请先复制偏好设置 JSON",
|
|
||||||
saveToAccount: "保存到账号",
|
|
||||||
saveToAccountSuccess: "偏好设置已保存到账号",
|
|
||||||
saveToAccountError: "保存到账号失败",
|
|
||||||
saveToAccountNeedLogin: "请先登录后再保存到账号",
|
|
||||||
clearAndLogout: "清空缓存 & 退出登录",
|
clearAndLogout: "清空缓存 & 退出登录",
|
||||||
mode: "模式",
|
mode: "模式",
|
||||||
general: "通用",
|
general: "通用",
|
||||||
|
|||||||
@@ -390,10 +390,4 @@ export const useSettingStore = defineStore({
|
|||||||
|
|
||||||
mitter.on("app.login", async () => {
|
mitter.on("app.login", async () => {
|
||||||
await useSettingStore().init();
|
await useSettingStore().init();
|
||||||
try {
|
|
||||||
const { loadPreferencesFromAccount } = await import("/@/vben/layouts/widgets/preferences/account-sync");
|
|
||||||
await loadPreferencesFromAccount();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("加载账号偏好设置失败", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ export {
|
|||||||
CircleCheckBig,
|
CircleCheckBig,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Copy,
|
Copy,
|
||||||
ClipboardPaste,
|
|
||||||
CloudUpload,
|
|
||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Expand,
|
Expand,
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { message as antdMessage } from "ant-design-vue";
|
|
||||||
|
|
||||||
import { $t, loadLocaleMessages } from "/@/locales";
|
|
||||||
import { updatePreferences } from "/@/vben/preferences";
|
|
||||||
|
|
||||||
import { PreferencesSettingsGet, PreferencesSettingsSave } from "./api";
|
|
||||||
|
|
||||||
const PREFERENCES_KNOWN_KEYS = ["app", "theme", "logo", "sidebar", "header", "tabbar", "breadcrumb", "navigation", "widget", "footer", "copyright", "shortcutKeys", "transition"];
|
|
||||||
|
|
||||||
export function isPreferencesPayload(value: unknown): value is Record<string, any> {
|
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(value, key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 保存到账号:允许空对象(重置后的默认偏好) */
|
|
||||||
export function isPreferencesSavePayload(value: unknown): value is Record<string, any> {
|
|
||||||
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const keys = Object.keys(value as Record<string, any>);
|
|
||||||
if (keys.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return isPreferencesPayload(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyPreferencesFromAccount(data: Record<string, any>) {
|
|
||||||
updatePreferences(data);
|
|
||||||
if (data.app?.locale) {
|
|
||||||
await loadLocaleMessages(data.app.locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录后从账号加载偏好并应用到本地;
|
|
||||||
* - null / {}:账号无有效配置(含保存为空),默认继续使用本地偏好
|
|
||||||
*/
|
|
||||||
export async function loadPreferencesFromAccount() {
|
|
||||||
const data = await PreferencesSettingsGet();
|
|
||||||
if (data == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof data !== "object" || Array.isArray(data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 保存为空时,后续拉取到空数据后默认从本地读取,不覆盖本地
|
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isPreferencesPayload(data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await applyPreferencesFromAccount(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function savePreferencesToAccount(preferencesPayload: Record<string, any> | null | undefined) {
|
|
||||||
const payload = preferencesPayload && typeof preferencesPayload === "object" ? preferencesPayload : {};
|
|
||||||
if (!isPreferencesSavePayload(payload)) {
|
|
||||||
throw new Error("invalid preferences payload");
|
|
||||||
}
|
|
||||||
await PreferencesSettingsSave(payload);
|
|
||||||
antdMessage.success($t("preferences.saveToAccountSuccess"));
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { request } from "/@/api/service";
|
|
||||||
|
|
||||||
const apiPrefix = "/user/settings/preferences";
|
|
||||||
|
|
||||||
export async function PreferencesSettingsGet(): Promise<Record<string, any> | null> {
|
|
||||||
const res = await request({
|
|
||||||
url: apiPrefix + "/get",
|
|
||||||
method: "post",
|
|
||||||
showErrorNotify: false,
|
|
||||||
});
|
|
||||||
return (res as Record<string, any>) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PreferencesSettingsSave(preferences: Record<string, any>) {
|
|
||||||
return await request({
|
|
||||||
url: apiPrefix + "/save",
|
|
||||||
method: "post",
|
|
||||||
data: { preferences },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+3
-56
@@ -16,27 +16,21 @@ import type { SegmentedItem } from "/@/vben//shadcn-ui";
|
|||||||
|
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
import { ClipboardPaste, CloudUpload, Copy, RotateCw, X } from "/@/vben/icons";
|
import { Copy, RotateCw, X } from "/@/vben/icons";
|
||||||
import { $t, loadLocaleMessages } from "/@/locales";
|
import { $t, loadLocaleMessages } from "/@/locales";
|
||||||
import { clearPreferencesCache, preferences, resetPreferences, usePreferences } from "/@/vben/preferences";
|
import { clearPreferencesCache, preferences, resetPreferences, usePreferences } from "/@/vben/preferences";
|
||||||
|
|
||||||
import { useVbenDrawer } from "/@/vben//popup-ui";
|
import { useVbenDrawer } from "/@/vben//popup-ui";
|
||||||
import { VbenButton, VbenIconButton, VbenSegmented } from "/@/vben//shadcn-ui";
|
import { VbenButton, VbenIconButton, VbenSegmented } from "/@/vben//shadcn-ui";
|
||||||
import { globalShareState } from "/@/vben//shared/global-state";
|
import { globalShareState } from "/@/vben//shared/global-state";
|
||||||
import { useUserStore } from "/@/store/user";
|
|
||||||
|
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { Animation, Block, Breadcrumb, BuiltinTheme, ColorMode, Content, Copyright, Footer, General, GlobalShortcutKeys, Header, Layout, Navigation, Radius, Sidebar, Tabbar, Theme, Widget } from "./blocks";
|
import { Animation, Block, Breadcrumb, BuiltinTheme, ColorMode, Content, Copyright, Footer, General, GlobalShortcutKeys, Header, Layout, Navigation, Radius, Sidebar, Tabbar, Theme, Widget } from "./blocks";
|
||||||
import { applyPreferencesFromAccount, isPreferencesPayload, savePreferencesToAccount } from "./account-sync";
|
|
||||||
|
|
||||||
import { message as antdMessage } from "ant-design-vue";
|
|
||||||
|
|
||||||
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||||
|
|
||||||
const message = globalShareState.getMessage();
|
const message = globalShareState.getMessage();
|
||||||
const userStore = useUserStore();
|
|
||||||
const savingToAccount = ref(false);
|
|
||||||
|
|
||||||
const appLocale = defineModel<SupportedLanguagesType>("appLocale");
|
const appLocale = defineModel<SupportedLanguagesType>("appLocale");
|
||||||
const appDynamicTitle = defineModel<boolean>("appDynamicTitle");
|
const appDynamicTitle = defineModel<boolean>("appDynamicTitle");
|
||||||
@@ -156,41 +150,6 @@ async function handleCopy() {
|
|||||||
await copy(JSON.stringify(diffPreference.value, null, 2));
|
await copy(JSON.stringify(diffPreference.value, null, 2));
|
||||||
|
|
||||||
message.copyPreferencesSuccess?.($t("preferences.copyPreferencesSuccessTitle"), $t("preferences.copyPreferencesSuccess"));
|
message.copyPreferencesSuccess?.($t("preferences.copyPreferencesSuccessTitle"), $t("preferences.copyPreferencesSuccess"));
|
||||||
antdMessage.success($t("preferences.copyPreferencesSuccessTitle"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleImport() {
|
|
||||||
try {
|
|
||||||
const text = await navigator.clipboard.readText();
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
if (!isPreferencesPayload(data)) {
|
|
||||||
throw new Error("invalid preferences payload");
|
|
||||||
}
|
|
||||||
await applyPreferencesFromAccount(data);
|
|
||||||
message.copyPreferencesSuccess?.($t("preferences.importPreferencesSuccessTitle"), $t("preferences.importPreferencesSuccess"));
|
|
||||||
antdMessage.success($t("preferences.importPreferencesSuccess"));
|
|
||||||
} catch {
|
|
||||||
antdMessage.error($t("preferences.importPreferencesError"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveToAccount() {
|
|
||||||
if (!userStore.isLogined) {
|
|
||||||
antdMessage.warning($t("preferences.saveToAccountNeedLogin"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (savingToAccount.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
savingToAccount.value = true;
|
|
||||||
try {
|
|
||||||
// 重置后 diff 为空时保存 {};后续登录拉取到空数据则继续使用本地偏好
|
|
||||||
await savePreferencesToAccount((diffPreference.value as Record<string, any>) || {});
|
|
||||||
} catch (e: any) {
|
|
||||||
antdMessage.error(e?.message || $t("preferences.saveToAccountError"));
|
|
||||||
} finally {
|
|
||||||
savingToAccount.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClearCache() {
|
async function handleClearCache() {
|
||||||
@@ -351,25 +310,13 @@ async function handleReset() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex w-full flex-col gap-2 px-1">
|
<VbenButton :disabled="!diffPreference" class="mx-4 w-full" size="sm" variant="default" @click="handleCopy">
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="default" @click="handleCopy">
|
|
||||||
<Copy class="mr-2 size-3" />
|
<Copy class="mr-2 size-3" />
|
||||||
{{ $t("preferences.copyPreferences") }}
|
{{ $t("preferences.copyPreferences") }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
<VbenButton class="w-full" size="sm" variant="outline" @click="handleImport">
|
<VbenButton :disabled="!diffPreference" class="mr-4 w-full" size="sm" variant="ghost" @click="handleClearCache">
|
||||||
<ClipboardPaste class="mr-2 size-3" />
|
|
||||||
{{ $t("preferences.importPreferences") }}
|
|
||||||
</VbenButton>
|
|
||||||
</div>
|
|
||||||
<VbenButton :disabled="!userStore.isLogined || savingToAccount" class="w-full" size="sm" variant="outline" @click="handleSaveToAccount">
|
|
||||||
<CloudUpload class="mr-2 size-3" />
|
|
||||||
{{ $t("preferences.saveToAccount") }}
|
|
||||||
</VbenButton>
|
|
||||||
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="ghost" @click="handleClearCache">
|
|
||||||
{{ $t("preferences.clearAndLogout") }}
|
{{ $t("preferences.clearAndLogout") }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, ref, watch, inject, onMounted, Ref } from "vue";
|
import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
|
||||||
import CertAccessModal from "./access/index.vue";
|
import CertAccessModal from "./access/index.vue";
|
||||||
import { createAccessApi } from "../api";
|
import { createAccessApi } from "../api";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
@@ -64,9 +64,9 @@ export default defineComponent({
|
|||||||
setup(props, ctx) {
|
setup(props, ctx) {
|
||||||
const api = createAccessApi(props.from);
|
const api = createAccessApi(props.from);
|
||||||
|
|
||||||
const target:Ref<any> = ref({});
|
const target = ref({});
|
||||||
const selectedId = ref();
|
const selectedId = ref();
|
||||||
async function refreshTarget(value:any) {
|
async function refreshTarget(value) {
|
||||||
selectedId.value = value;
|
selectedId.value = value;
|
||||||
if (value > 0) {
|
if (value > 0) {
|
||||||
target.value = await api.GetSimpleInfo(value);
|
target.value = await api.GetSimpleInfo(value);
|
||||||
@@ -83,7 +83,7 @@ export default defineComponent({
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
|
|
||||||
async function emitValue(value:any) {
|
async function emitValue(value) {
|
||||||
const userId = userStore.userInfo.id;
|
const userId = userStore.userInfo.id;
|
||||||
const isEnterprice = projectStore.isEnterprise;
|
const isEnterprice = projectStore.isEnterprise;
|
||||||
if (pipeline?.value) {
|
if (pipeline?.value) {
|
||||||
@@ -132,7 +132,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const providerDefine = ref({});
|
const providerDefine = ref({});
|
||||||
|
|
||||||
async function refreshProviderDefine(type:any) {
|
async function refreshProviderDefine(type) {
|
||||||
providerDefine.value = await api.GetProviderDefine(type);
|
providerDefine.value = await api.GetProviderDefine(type);
|
||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/// <reference types="mocha" />
|
|
||||||
/// <reference types="node" />
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
|
|
||||||
import { parseUserPreferencesPayload } from "./user-preferences.js";
|
|
||||||
|
|
||||||
describe("parseUserPreferencesPayload", () => {
|
|
||||||
it("parses wrapped preferences payload", () => {
|
|
||||||
const result = parseUserPreferencesPayload({
|
|
||||||
preferences: {
|
|
||||||
theme: { mode: "dark" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.deepEqual(result, { theme: { mode: "dark" } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses direct preferences payload", () => {
|
|
||||||
const result = parseUserPreferencesPayload({
|
|
||||||
app: { locale: "en-US" },
|
|
||||||
theme: { mode: "light" },
|
|
||||||
});
|
|
||||||
assert.deepEqual(result, {
|
|
||||||
app: { locale: "en-US" },
|
|
||||||
theme: { mode: "light" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses empty preferences as reset payload", () => {
|
|
||||||
assert.deepEqual(parseUserPreferencesPayload({}), {});
|
|
||||||
assert.deepEqual(parseUserPreferencesPayload({ preferences: {} }), {});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null for invalid payload", () => {
|
|
||||||
assert.equal(parseUserPreferencesPayload(null), null);
|
|
||||||
assert.equal(parseUserPreferencesPayload([]), null);
|
|
||||||
assert.equal(parseUserPreferencesPayload({ foo: 1 }), null);
|
|
||||||
assert.equal(parseUserPreferencesPayload({ preferences: { foo: 1 } }), null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const PREFERENCES_KNOWN_KEYS = [
|
|
||||||
"app",
|
|
||||||
"theme",
|
|
||||||
"logo",
|
|
||||||
"sidebar",
|
|
||||||
"header",
|
|
||||||
"tabbar",
|
|
||||||
"breadcrumb",
|
|
||||||
"navigation",
|
|
||||||
"widget",
|
|
||||||
"footer",
|
|
||||||
"copyright",
|
|
||||||
"shortcutKeys",
|
|
||||||
"transition",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析用户偏好设置 payload。
|
|
||||||
* 兼容 `{ preferences: {...} }` 与直接传偏好对象两种格式。
|
|
||||||
* 空对象 `{}` 表示已重置为默认偏好,视为合法。
|
|
||||||
*/
|
|
||||||
export function parseUserPreferencesPayload(value: unknown): Record<string, any> | null {
|
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const raw = value as Record<string, any>;
|
|
||||||
const preferences =
|
|
||||||
raw.preferences != null && typeof raw.preferences === "object" && !Array.isArray(raw.preferences)
|
|
||||||
? (raw.preferences as Record<string, any>)
|
|
||||||
: raw;
|
|
||||||
const preferenceKeys = Object.keys(preferences);
|
|
||||||
// 空对象表示重置后的默认偏好
|
|
||||||
if (preferenceKeys.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const hasKnownKey = PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(preferences, key));
|
|
||||||
if (!hasKnownKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return preferences;
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,10 @@ import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/c
|
|||||||
import { Constants, CrudController } from "@certd/lib-server";
|
import { Constants, CrudController } from "@certd/lib-server";
|
||||||
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
|
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
|
||||||
import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js";
|
import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js";
|
||||||
import { UserGrantSetting, UserPreferencesSetting } from "../../../modules/mine/service/models.js";
|
import { UserGrantSetting } from "../../../modules/mine/service/models.js";
|
||||||
import { isPlus } from "@certd/plus-core";
|
import { isPlus } from "@certd/plus-core";
|
||||||
import { merge } from "lodash-es";
|
import { merge } from "lodash-es";
|
||||||
import { ApiTags } from "@midwayjs/swagger";
|
import { ApiTags } from "@midwayjs/swagger";
|
||||||
import { parseUserPreferencesPayload } from "./user-preferences.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@@ -91,37 +90,4 @@ export class UserSettingsController extends CrudController<UserSettingsService>
|
|||||||
await this.service.saveSetting(userId, null, setting);
|
await this.service.saveSetting(userId, null, setting);
|
||||||
return this.ok({});
|
return this.ok({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/preferences/get", { description: Constants.per.authOnly, summary: "获取用户偏好设置" })
|
|
||||||
async preferencesGet() {
|
|
||||||
const userId = this.getUserId();
|
|
||||||
const entity = await this.service.getByKey(UserPreferencesSetting.__key__, userId, null);
|
|
||||||
if (!entity?.setting) {
|
|
||||||
return this.ok(null);
|
|
||||||
}
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(entity.setting);
|
|
||||||
} catch {
|
|
||||||
return this.ok(null);
|
|
||||||
}
|
|
||||||
return this.ok(parseUserPreferencesPayload(parsed));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("/preferences/save", { description: Constants.per.authOnly, summary: "保存用户偏好设置" })
|
|
||||||
async preferencesSave(@Body(ALL) bean: any) {
|
|
||||||
const userId = this.getUserId();
|
|
||||||
const preferences = parseUserPreferencesPayload(bean);
|
|
||||||
if (!preferences) {
|
|
||||||
throw new Error("偏好设置内容无效");
|
|
||||||
}
|
|
||||||
// 整份替换,避免 saveSetting 深合并留下已恢复为默认值的旧字段
|
|
||||||
const entity = new UserSettingsEntity();
|
|
||||||
entity.key = UserPreferencesSetting.__key__;
|
|
||||||
entity.title = UserPreferencesSetting.__title__;
|
|
||||||
entity.userId = userId;
|
|
||||||
entity.setting = JSON.stringify({ preferences });
|
|
||||||
await this.service.save(entity);
|
|
||||||
return this.ok({});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,14 +52,6 @@ export class UserGrantSetting extends BaseSettings {
|
|||||||
allowAdminViewCerts = false;
|
allowAdminViewCerts = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserPreferencesSetting extends BaseSettings {
|
|
||||||
static __title__ = "用户偏好设置";
|
|
||||||
static __key__ = "user.preferences";
|
|
||||||
|
|
||||||
/** 偏好差异配置(相对默认值),与前端 diffPreference 结构一致 */
|
|
||||||
preferences: Record<string, any> = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserDomainImportSetting extends BaseSettings {
|
export class UserDomainImportSetting extends BaseSettings {
|
||||||
static __title__ = "用户域名导入设置";
|
static __title__ = "用户域名导入设置";
|
||||||
static __key__ = "user.domain.import";
|
static __key__ = "user.domain.import";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { IContext } from "@certd/pipeline";
|
|||||||
import { IDnsProvider, IDomainParser } from "@certd/plugin-lib";
|
import { IDnsProvider, IDomainParser } from "@certd/plugin-lib";
|
||||||
import punycode from "punycode.js";
|
import punycode from "punycode.js";
|
||||||
import { IOssClient } from "../../../plugin-lib/index.js";
|
import { IOssClient } from "../../../plugin-lib/index.js";
|
||||||
import { NonRetryableException } from "@certd/lib-server";
|
|
||||||
export type CnameVerifyPlan = {
|
export type CnameVerifyPlan = {
|
||||||
type?: string;
|
type?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -532,7 +531,6 @@ export class AcmeService {
|
|||||||
};
|
};
|
||||||
/* 自动申请证书 */
|
/* 自动申请证书 */
|
||||||
const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"];
|
const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"];
|
||||||
try {
|
|
||||||
const crt = await client.auto({
|
const crt = await client.auto({
|
||||||
csr,
|
csr,
|
||||||
email: email,
|
email: email,
|
||||||
@@ -565,14 +563,6 @@ export class AcmeService {
|
|||||||
this.logger.debug(`Certificate:\n${cert.crt}`);
|
this.logger.debug(`Certificate:\n${cert.crt}`);
|
||||||
this.logger.info("证书申请成功");
|
this.logger.info("证书申请成功");
|
||||||
return cert;
|
return cert;
|
||||||
} catch (e) {
|
|
||||||
const message = e?.message;
|
|
||||||
const REDUNDANT_WILDCARD_DOMAIN_ERROR = "redundant with a wildcard domain in the same request";
|
|
||||||
if (message != null && message.indexOf(REDUNDANT_WILDCARD_DOMAIN_ERROR) >= 0) {
|
|
||||||
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message})`);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCommonNameByDomains(domains: string | string[]): {
|
buildCommonNameByDomains(domains: string | string[]): {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { utils } from "@certd/basic";
|
|
||||||
import { NonRetryableException } from "@certd/lib-server";
|
|
||||||
import { CertApplyPlugin } from "./apply.js";
|
import { CertApplyPlugin } from "./apply.js";
|
||||||
|
|
||||||
describe("CertApplyPlugin dns-persist verify plan", () => {
|
describe("CertApplyPlugin dns-persist verify plan", () => {
|
||||||
@@ -47,114 +45,3 @@ describe("CertApplyPlugin dns-persist verify plan", () => {
|
|||||||
assert.equal(plan["handfree.work"].dnsPersistVerifyPlan?.recordValue, "letsencrypt.org; accounturi=https://acme.example/acct/1; policy=wildcard");
|
assert.equal(plan["handfree.work"].dnsPersistVerifyPlan?.recordValue, "letsencrypt.org; accounturi=https://acme.example/acct/1; policy=wildcard");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CertApplyPlugin certificate apply retry", () => {
|
|
||||||
it("does not retry by default", async () => {
|
|
||||||
const plugin: any = new CertApplyPlugin();
|
|
||||||
let orderCount = 0;
|
|
||||||
const error = new Error("apply failed");
|
|
||||||
plugin.logger = { warn() {} };
|
|
||||||
plugin.acme = {
|
|
||||||
async order() {
|
|
||||||
orderCount++;
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await assert.rejects(plugin.orderWithRetry({}), error);
|
|
||||||
assert.equal(orderCount, 1);
|
|
||||||
assert.equal(plugin.certApplyRetryCount, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retries after a 30-second cooldown and succeeds before reaching the limit", async () => {
|
|
||||||
const plugin: any = new CertApplyPlugin();
|
|
||||||
let orderCount = 0;
|
|
||||||
const waitTimes: number[] = [];
|
|
||||||
plugin.certApplyRetryCount = 2;
|
|
||||||
plugin.logger = { warn() {} };
|
|
||||||
plugin.acme = {
|
|
||||||
async order() {
|
|
||||||
orderCount++;
|
|
||||||
if (orderCount < 3) {
|
|
||||||
throw new Error(`apply failed ${orderCount}`);
|
|
||||||
}
|
|
||||||
return { crt: "certificate", key: "private-key" };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const originalSleep = utils.sleep;
|
|
||||||
utils.sleep = async (waitTime: number) => {
|
|
||||||
waitTimes.push(waitTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cert = await plugin.orderWithRetry({});
|
|
||||||
|
|
||||||
assert.deepEqual(cert, { crt: "certificate", key: "private-key" });
|
|
||||||
assert.equal(orderCount, 3);
|
|
||||||
assert.deepEqual(waitTimes, [30_000, 30_000]);
|
|
||||||
} finally {
|
|
||||||
utils.sleep = originalSleep;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws the last error after reaching the retry limit", async () => {
|
|
||||||
const plugin: any = new CertApplyPlugin();
|
|
||||||
let orderCount = 0;
|
|
||||||
const error = new Error("apply failed");
|
|
||||||
plugin.certApplyRetryCount = 1;
|
|
||||||
plugin.logger = { warn() {} };
|
|
||||||
plugin.acme = {
|
|
||||||
async order() {
|
|
||||||
orderCount++;
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const originalSleep = utils.sleep;
|
|
||||||
utils.sleep = async () => {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await assert.rejects(plugin.orderWithRetry({}), error);
|
|
||||||
assert.equal(orderCount, 2);
|
|
||||||
} finally {
|
|
||||||
utils.sleep = originalSleep;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not retry a cancelled apply", async () => {
|
|
||||||
const plugin: any = new CertApplyPlugin();
|
|
||||||
let orderCount = 0;
|
|
||||||
const error: any = new Error("cancelled");
|
|
||||||
error.name = "CancelError";
|
|
||||||
plugin.certApplyRetryCount = 2;
|
|
||||||
plugin.logger = { warn() {} };
|
|
||||||
plugin.acme = {
|
|
||||||
async order() {
|
|
||||||
orderCount++;
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await assert.rejects(plugin.orderWithRetry({}), error);
|
|
||||||
assert.equal(orderCount, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws a non-retryable error for wildcard and normal domain conflicts", async () => {
|
|
||||||
const plugin: any = new CertApplyPlugin();
|
|
||||||
let orderCount = 0;
|
|
||||||
const message = "example.com is redundant with a wildcard domain in the same request";
|
|
||||||
plugin.certApplyRetryCount = 2;
|
|
||||||
plugin.logger = { warn() {} };
|
|
||||||
plugin.acme = {
|
|
||||||
async order() {
|
|
||||||
orderCount++;
|
|
||||||
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message})`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await assert.rejects(plugin.orderWithRetry({}), (error: any) => {
|
|
||||||
assert.equal(error instanceof NonRetryableException, true);
|
|
||||||
assert.equal(error.message, `通配符域名已经包含了普通域名,请删除其中一个(${message})`);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
assert.equal(orderCount, 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import { utils } from "@certd/basic";
|
import { utils } from "@certd/basic";
|
||||||
import { NonRetryableException } from "@certd/lib-server";
|
|
||||||
|
|
||||||
import { AcmeAccountInfo, AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
|
import { AcmeAccountInfo, AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
|
||||||
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
|
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
|
||||||
@@ -62,7 +61,6 @@ const preferredChainConfigs = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
|
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
|
||||||
const CERT_APPLY_RETRY_DELAY_MS = 30_000;
|
|
||||||
|
|
||||||
const preferredChainMergeScript = (() => {
|
const preferredChainMergeScript = (() => {
|
||||||
const configs = JSON.stringify(preferredChainConfigs);
|
const configs = JSON.stringify(preferredChainConfigs);
|
||||||
@@ -553,20 +551,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
})
|
})
|
||||||
waitDnsDiffuseTime = 30;
|
waitDnsDiffuseTime = 30;
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "证书申请失败重试次数",
|
|
||||||
value: 0,
|
|
||||||
component: {
|
|
||||||
name: "a-input-number",
|
|
||||||
vModel: "value",
|
|
||||||
min: 0,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
maybeNeed: true,
|
|
||||||
helper: "证书申请失败后,等待30秒再自动重试;0表示不重试",
|
|
||||||
})
|
|
||||||
certApplyRetryCount = 0;
|
|
||||||
|
|
||||||
acme!: AcmeService;
|
acme!: AcmeService;
|
||||||
|
|
||||||
eab!: EabAccess;
|
eab!: EabAccess;
|
||||||
@@ -667,7 +651,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
dnsProvider = await this.createDnsProvider(dnsProviderType, access);
|
dnsProvider = await this.createDnsProvider(dnsProviderType, access);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cert = await this.orderWithRetry({
|
try {
|
||||||
|
const cert = await this.acme.order({
|
||||||
email,
|
email,
|
||||||
domains,
|
domains,
|
||||||
dnsProvider,
|
dnsProvider,
|
||||||
@@ -681,39 +666,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
|
|
||||||
const certInfo = this.formatCerts(cert);
|
const certInfo = this.formatCerts(cert);
|
||||||
return new CertReader(certInfo);
|
return new CertReader(certInfo);
|
||||||
}
|
|
||||||
|
|
||||||
private async orderWithRetry(orderOptions: Parameters<AcmeService["order"]>[0]) {
|
|
||||||
const maxRetryCount = this.getCertApplyRetryCount();
|
|
||||||
let retryCount = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
return await this.acme.order(orderOptions);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e instanceof NonRetryableException) {
|
const message: string = e?.message;
|
||||||
|
if (message != null && message.indexOf("redundant with a wildcard domain in the same request") >= 0) {
|
||||||
|
this.logger.error(e);
|
||||||
|
throw new Error(`通配符域名已经包含了普通域名,请删除其中一个(${message})`);
|
||||||
|
}
|
||||||
|
if (e.name === "CancelError") {
|
||||||
|
throw new CancelError(e.message);
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
if (e?.name === "CancelError" || retryCount >= maxRetryCount) {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
retryCount++;
|
|
||||||
this.logger.warn(`证书申请失败,等待30秒后重试(${retryCount}/${maxRetryCount})`, e);
|
|
||||||
await utils.sleep(CERT_APPLY_RETRY_DELAY_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCertApplyRetryCount() {
|
|
||||||
const retryCount = Number(this.certApplyRetryCount);
|
|
||||||
if (!Number.isFinite(retryCount) || retryCount <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.floor(retryCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
|
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
|
||||||
const domainParser = this.acme.options.domainParser;
|
const domainParser = this.acme.options.domainParser;
|
||||||
const context: DnsProviderContext = {
|
const context: DnsProviderContext = {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
|
|||||||
this.clearLastStatus();
|
this.clearLastStatus();
|
||||||
|
|
||||||
if (this.successNotify) {
|
if (this.successNotify) {
|
||||||
await this.sendSuccessNotify(cert);
|
await this.sendSuccessNotify();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("申请证书失败");
|
throw new Error("申请证书失败");
|
||||||
@@ -165,14 +165,12 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
|
|||||||
nextUpdateDays: leftDays - maxDays,
|
nextUpdateDays: leftDays - maxDays,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async sendSuccessNotify(certReader: CertReader) {
|
async sendSuccessNotify() {
|
||||||
this.logger.info("发送证书申请成功通知");
|
this.logger.info("发送证书申请成功通知");
|
||||||
const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id);
|
const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id);
|
||||||
const body: NotificationBody = {
|
const body: NotificationBody = {
|
||||||
title: `证书申请成功【${this.pipeline.title}】`,
|
title: `证书申请成功【${this.pipeline.title}】`,
|
||||||
content: `域名:${this.domains.join(",")}\n
|
content: `域名:${this.domains.join(",")}`,
|
||||||
证书有效期:${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")}\n
|
|
||||||
`,
|
|
||||||
url: url,
|
url: url,
|
||||||
notificationType: "certApplySuccess",
|
notificationType: "certApplySuccess",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { BaotaAccess } from "../access.js";
|
import { BaotaAccess } from "../access.js";
|
||||||
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
|
import { HttpClient, HttpRequestConfig } from "@certd/basic";
|
||||||
import * as querystring from "node:querystring";
|
import * as querystring from "node:querystring";
|
||||||
|
|
||||||
export class BaotaClient {
|
export class BaotaClient {
|
||||||
access: BaotaAccess;
|
access: BaotaAccess;
|
||||||
http: HttpClient;
|
http: HttpClient;
|
||||||
logger: ILogger
|
|
||||||
|
|
||||||
constructor(access: BaotaAccess, http: HttpClient) {
|
constructor(access: BaotaAccess, http: HttpClient) {
|
||||||
this.access = access;
|
this.access = access;
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.logger = access.ctx.logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//将以上 java代码 翻译成nodejs 代码
|
//将以上 java代码 翻译成nodejs 代码
|
||||||
|
|||||||
+3
-14
@@ -88,7 +88,7 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
siteName!: string | string[];
|
siteName!: string | string[];
|
||||||
|
|
||||||
async onInstance() { }
|
async onInstance() {}
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
const { cert, accessId } = this;
|
const { cert, accessId } = this;
|
||||||
const access = await this.getAccess(accessId);
|
const access = await this.getAccess(accessId);
|
||||||
@@ -104,15 +104,12 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lockKey = `baota-lock-${accessId}`;
|
const lockKey = `baota-lock-${accessId}`;
|
||||||
if (this.isDockerSite) {
|
|
||||||
this.logger.info(`当前已勾选docker站点(如果部署失败,请确认站点:${siteNames}, 是否全部为docker站点)`);
|
|
||||||
}
|
|
||||||
for (const site of siteNames) {
|
for (const site of siteNames) {
|
||||||
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
|
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
|
||||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||||
try {
|
this.logger.info(`为站点:${site}设置证书,目前支持宝塔网站站点、docker站点`);
|
||||||
if (this.isDockerSite) {
|
if (this.isDockerSite) {
|
||||||
this.logger.info(`为Docker站点:${site} 设置证书`);
|
|
||||||
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
|
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
|
||||||
site_name: site,
|
site_name: site,
|
||||||
key: cert.key,
|
key: cert.key,
|
||||||
@@ -120,7 +117,6 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
|
|||||||
});
|
});
|
||||||
this.logger.info(res?.msg);
|
this.logger.info(res?.msg);
|
||||||
} else {
|
} else {
|
||||||
this.logger.info(`为非Docker站点:${site} 设置证书`);
|
|
||||||
const res = await client.doRequest("/site", "SetSSL", {
|
const res = await client.doRequest("/site", "SetSSL", {
|
||||||
type: 0,
|
type: 0,
|
||||||
siteName: site,
|
siteName: site,
|
||||||
@@ -129,13 +125,6 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
|
|||||||
});
|
});
|
||||||
this.logger.info(res?.msg);
|
this.logger.info(res?.msg);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.message?.includes("没有服务器配置文件")) {
|
|
||||||
this.logger.error(e.message);
|
|
||||||
this.logger.warn(`首先请确认站点 ${site} 是否存在,如果存在,请到宝塔上手动保存一下该站点证书试试`);
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/ind
|
|||||||
title: "腾讯云-删除即将过期证书",
|
title: "腾讯云-删除即将过期证书",
|
||||||
icon: "svg:icon-tencentcloud",
|
icon: "svg:icon-tencentcloud",
|
||||||
group: pluginGroups.tencent.key,
|
group: pluginGroups.tencent.key,
|
||||||
desc: "仅删除即将过期且未使用的证书",
|
desc: "仅删除未使用的证书",
|
||||||
dependPlugins: {
|
dependPlugins: {
|
||||||
"access:tencent": "*",
|
"access:tencent": "*",
|
||||||
},
|
},
|
||||||
|
|||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
/// <reference types="mocha" />
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { DeployCertToTencentCLB } from "./index.js";
|
|
||||||
|
|
||||||
describe("DeployCertToTencentCLB", () => {
|
|
||||||
it("uses remote single-select inputs for CLB and HTTPS listener", () => {
|
|
||||||
const input = (DeployCertToTencentCLB as any).define.input;
|
|
||||||
|
|
||||||
assert.equal(input.loadBalancerId.component.name, "remote-select");
|
|
||||||
assert.equal(input.loadBalancerId.component.single, true);
|
|
||||||
assert.equal(input.loadBalancerId.component.action, "onGetCLBList");
|
|
||||||
assert.equal(input.loadBalancerId.required, true);
|
|
||||||
|
|
||||||
assert.equal(input.listenerId.component.name, "remote-select");
|
|
||||||
assert.equal(input.listenerId.component.single, true);
|
|
||||||
assert.equal(input.listenerId.component.action, "onGetListenerList");
|
|
||||||
assert.deepEqual(input.listenerId.component.watches, ["certDomains", "accessId", "region", "loadBalancerId"]);
|
|
||||||
assert.equal(input.listenerId.required, true);
|
|
||||||
|
|
||||||
assert.equal(input.domain.component.name, "remote-select");
|
|
||||||
assert.equal(input.domain.component.single, false);
|
|
||||||
assert.equal(input.domain.component.action, "onGetDomainList");
|
|
||||||
assert.deepEqual(input.domain.component.watches, ["certDomains", "accessId", "region", "loadBalancerId", "listenerId"]);
|
|
||||||
assert.equal(input.domain.required, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps CLB API results to remote-select options", async () => {
|
|
||||||
const plugin = new DeployCertToTencentCLB();
|
|
||||||
plugin.accessId = "access-1";
|
|
||||||
plugin.logger = { info: () => undefined } as any;
|
|
||||||
(plugin as any).getClient = async () => ({});
|
|
||||||
(plugin as any).getCLBList = async () => [
|
|
||||||
{
|
|
||||||
LoadBalancerId: "lb-1",
|
|
||||||
LoadBalancerName: "业务负载均衡",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const options = await plugin.onGetCLBList({});
|
|
||||||
|
|
||||||
assert.deepEqual(options, [
|
|
||||||
{
|
|
||||||
value: "lb-1",
|
|
||||||
label: "业务负载均衡<lb-1>",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps HTTPS listener API results after selecting a CLB", async () => {
|
|
||||||
const plugin = new DeployCertToTencentCLB();
|
|
||||||
plugin.accessId = "access-1";
|
|
||||||
plugin.loadBalancerId = "lb-1";
|
|
||||||
plugin.logger = { info: () => undefined } as any;
|
|
||||||
(plugin as any).getClient = async () => ({});
|
|
||||||
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: any) => {
|
|
||||||
assert.equal(loadBalancerId, "lb-1");
|
|
||||||
assert.equal(listenerIds, null);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
ListenerId: "listener-1",
|
|
||||||
ListenerName: "HTTPS监听器",
|
|
||||||
Port: 443,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = await plugin.onGetListenerList({});
|
|
||||||
|
|
||||||
assert.deepEqual(options, [
|
|
||||||
{
|
|
||||||
value: "listener-1",
|
|
||||||
label: "HTTPS监听器:443<listener-1>",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps SNI domains from the selected listener rules", async () => {
|
|
||||||
const plugin = new DeployCertToTencentCLB();
|
|
||||||
plugin.accessId = "access-1";
|
|
||||||
plugin.loadBalancerId = "lb-1";
|
|
||||||
plugin.listenerId = "listener-1";
|
|
||||||
plugin.logger = { info: () => undefined } as any;
|
|
||||||
(plugin as any).getClient = async () => ({});
|
|
||||||
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: string[]) => {
|
|
||||||
assert.equal(loadBalancerId, "lb-1");
|
|
||||||
assert.deepEqual(listenerIds, ["listener-1"]);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
ListenerId: "listener-1",
|
|
||||||
Rules: [{ Domain: "www.example.com" }, { Domain: "api.example.com" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = await plugin.onGetDomainList({});
|
|
||||||
|
|
||||||
assert.deepEqual(options, [
|
|
||||||
{
|
|
||||||
value: "www.example.com",
|
|
||||||
label: "www.example.com",
|
|
||||||
domain: "www.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "api.example.com",
|
|
||||||
label: "api.example.com",
|
|
||||||
domain: "api.example.com",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+13
-101
@@ -1,8 +1,7 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
|
import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
|
||||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "DeployCertToTencentCLB",
|
name: "DeployCertToTencentCLB",
|
||||||
title: "腾讯云-部署到CLB",
|
title: "腾讯云-部署到CLB",
|
||||||
@@ -74,44 +73,29 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
region!: string;
|
region!: string;
|
||||||
|
|
||||||
@TaskInput(
|
@TaskInput({
|
||||||
createRemoteSelectInputDefine({
|
|
||||||
title: "负载均衡ID",
|
title: "负载均衡ID",
|
||||||
helper: "请选择要部署证书的负载均衡",
|
required: true,
|
||||||
action: DeployCertToTencentCLB.prototype.onGetCLBList.name,
|
|
||||||
watches: ["region"],
|
|
||||||
single: true,
|
|
||||||
pager: false,
|
|
||||||
search: false,
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
loadBalancerId!: string;
|
loadBalancerId!: string;
|
||||||
|
|
||||||
@TaskInput(
|
@TaskInput({
|
||||||
createRemoteSelectInputDefine({
|
|
||||||
title: "监听器ID",
|
title: "监听器ID",
|
||||||
helper: "请选择要部署证书的HTTPS监听器",
|
required: true,
|
||||||
action: DeployCertToTencentCLB.prototype.onGetListenerList.name,
|
|
||||||
watches: ["region", "loadBalancerId"],
|
|
||||||
single: true,
|
|
||||||
pager: false,
|
|
||||||
search: false,
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
listenerId!: string;
|
listenerId!: string;
|
||||||
|
|
||||||
@TaskInput(
|
@TaskInput({
|
||||||
createRemoteSelectInputDefine({
|
|
||||||
title: "域名",
|
title: "域名",
|
||||||
helper: "如果开启了SNI,请选择要部署证书的域名;未开启SNI时可以留空",
|
|
||||||
action: DeployCertToTencentCLB.prototype.onGetDomainList.name,
|
|
||||||
watches: ["region", "loadBalancerId", "listenerId"],
|
|
||||||
required: false,
|
required: false,
|
||||||
single: false,
|
component: {
|
||||||
pager: false,
|
name: "a-select",
|
||||||
search: false,
|
vModel: "value",
|
||||||
|
open: false,
|
||||||
|
mode: "tags",
|
||||||
|
},
|
||||||
|
helper: "如果开启了sni,则此项必须填写,未开启,则不要填写",
|
||||||
})
|
})
|
||||||
)
|
|
||||||
domain!: string | string[];
|
domain!: string | string[];
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
@@ -288,27 +272,6 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
|
|||||||
return ret.LoadBalancerSet;
|
return ret.LoadBalancerSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onGetCLBList(data: PageSearch) {
|
|
||||||
if (!this.accessId) {
|
|
||||||
throw new Error("请选择Access提供者");
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.getClient();
|
|
||||||
const list = await this.getCLBList(client);
|
|
||||||
if (!list || list.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.map((item: any) => {
|
|
||||||
const loadBalancerId = item.LoadBalancerId;
|
|
||||||
const loadBalancerName = item.LoadBalancerName || loadBalancerId;
|
|
||||||
return {
|
|
||||||
value: loadBalancerId,
|
|
||||||
label: `${loadBalancerName}<${loadBalancerId}>`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getListenerList(client: any, balancerId: any, listenerIds: any) {
|
async getListenerList(client: any, balancerId: any, listenerIds: any) {
|
||||||
// HTTPS
|
// HTTPS
|
||||||
const params = {
|
const params = {
|
||||||
@@ -321,57 +284,6 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
|
|||||||
return ret.Listeners;
|
return ret.Listeners;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onGetListenerList(data: PageSearch) {
|
|
||||||
if (!this.accessId) {
|
|
||||||
throw new Error("请选择Access提供者");
|
|
||||||
}
|
|
||||||
if (!this.loadBalancerId) {
|
|
||||||
throw new Error("请先选择负载均衡");
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.getClient();
|
|
||||||
const list = await this.getListenerList(client, this.loadBalancerId, null);
|
|
||||||
if (!list || list.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.map((item: any) => {
|
|
||||||
const listenerId = item.ListenerId;
|
|
||||||
const listenerName = item.ListenerName || "HTTPS监听器";
|
|
||||||
const port = item.Port ? `:${item.Port}` : "";
|
|
||||||
return {
|
|
||||||
value: listenerId,
|
|
||||||
label: `${listenerName}${port}<${listenerId}>`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onGetDomainList(data: PageSearch) {
|
|
||||||
if (!this.accessId) {
|
|
||||||
throw new Error("请选择Access提供者");
|
|
||||||
}
|
|
||||||
if (!this.loadBalancerId) {
|
|
||||||
throw new Error("请先选择负载均衡");
|
|
||||||
}
|
|
||||||
if (!this.listenerId) {
|
|
||||||
throw new Error("请先选择监听器");
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.getClient();
|
|
||||||
const listeners = await this.getListenerList(client, this.loadBalancerId, [this.listenerId]);
|
|
||||||
const listener = listeners?.[0];
|
|
||||||
const domains = listener?.Rules?.map((rule: any) => rule.Domain).filter(Boolean) || [];
|
|
||||||
const uniqueDomains = [...new Set(domains)];
|
|
||||||
|
|
||||||
return uniqueDomains.map(domain => {
|
|
||||||
return {
|
|
||||||
value: domain,
|
|
||||||
label: domain,
|
|
||||||
domain,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkRet(ret: any) {
|
checkRet(ret: any) {
|
||||||
if (!ret || ret.Error) {
|
if (!ret || ret.Error) {
|
||||||
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
||||||
|
|||||||
Reference in New Issue
Block a user